mirror of
https://github.com/boringproxy/boringproxy.git
synced 2025-02-25 18:55:29 -06:00
460 lines
12 KiB
Go
460 lines
12 KiB
Go
package boringproxy
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/mdp/qrterminal/v3"
|
|
|
|
"github.com/takingnames/namedrop-go"
|
|
)
|
|
|
|
type Config struct {
|
|
SshServerPort int `json:"ssh_server_port"`
|
|
PublicIp string `json:"public_ip"`
|
|
namedropClient *namedrop.Client
|
|
autoCerts bool
|
|
}
|
|
|
|
type SmtpConfig struct {
|
|
Server string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
type Server struct {
|
|
db *Database
|
|
tunMan *TunnelManager
|
|
httpClient *http.Client
|
|
httpListener *PassthroughListener
|
|
}
|
|
|
|
func Listen() {
|
|
flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
|
newAdminDomain := flagSet.String("admin-domain", "", "Admin Domain")
|
|
sshServerPort := flagSet.Int("ssh-server-port", 22, "SSH Server Port")
|
|
dbDir := flagSet.String("db-dir", "", "Database file directory")
|
|
certDir := flagSet.String("cert-dir", "", "TLS cert directory")
|
|
printLogin := flagSet.Bool("print-login", false, "Prints admin login information")
|
|
httpPort := flagSet.Int("http-port", 80, "HTTP (insecure) port")
|
|
httpsPort := flagSet.Int("https-port", 443, "HTTPS (secure) port")
|
|
allowHttp := flagSet.Bool("allow-http", false, "Allow unencrypted (HTTP) requests")
|
|
publicIp := flagSet.String("public-ip", "", "Public IP")
|
|
behindProxy := flagSet.Bool("behind-proxy", false, "Whether we're running behind another reverse proxy")
|
|
acmeEmail := flagSet.String("acme-email", "", "Email for ACME (ie Let's Encrypt)")
|
|
acmeUseStaging := flagSet.Bool("acme-use-staging", false, "Use ACME (ie Let's Encrypt) staging servers")
|
|
acceptCATerms := flagSet.Bool("accept-ca-terms", false, "Automatically accept CA terms")
|
|
acmeCa := flagSet.String("acme-certificate-authority", "", "URI for ACME Certificate Authority")
|
|
err := flagSet.Parse(os.Args[2:])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: parsing flags: %s\n", os.Args[0], err)
|
|
}
|
|
|
|
log.Println("Starting up")
|
|
|
|
db, err := NewDatabase(*dbDir)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
namedropClient := namedrop.NewClient(db, db.GetAdminDomain(), "takingnames.io/namedrop")
|
|
|
|
var ip string
|
|
|
|
if *publicIp != "" {
|
|
ip = *publicIp
|
|
} else {
|
|
ip, err = namedropClient.GetPublicIp()
|
|
if err != nil {
|
|
fmt.Printf("WARNING: Failed to determine public IP: %s\n", err.Error())
|
|
}
|
|
}
|
|
|
|
err = namedrop.CheckPublicAddress(ip, *httpPort)
|
|
if err != nil {
|
|
fmt.Printf("WARNING: Failed to access %s:%d from the internet\n", ip, *httpPort)
|
|
}
|
|
|
|
err = namedrop.CheckPublicAddress(ip, *httpsPort)
|
|
if err != nil {
|
|
fmt.Printf("WARNING: Failed to access %s:%d from the internet\n", ip, *httpsPort)
|
|
}
|
|
|
|
autoCerts := true
|
|
if *httpPort != 80 || *httpsPort != 443 {
|
|
fmt.Printf("WARNING: LetsEncrypt only supports HTTP/HTTPS ports 80/443. You are using %d/%d. Disabling automatic certificate management\n", *httpPort, *httpsPort)
|
|
autoCerts = false
|
|
}
|
|
|
|
if *certDir != "" {
|
|
certmagic.Default.Storage = &certmagic.FileStorage{*certDir}
|
|
}
|
|
//certmagic.DefaultACME.DisableHTTPChallenge = true
|
|
//certmagic.DefaultACME.DisableTLSALPNChallenge = true
|
|
|
|
if *acmeEmail != "" {
|
|
certmagic.DefaultACME.Email = *acmeEmail
|
|
}
|
|
|
|
if *acceptCATerms {
|
|
certmagic.DefaultACME.Agreed = true
|
|
log.Print(fmt.Sprintf("Automatic agreement to CA terms with email (%s)", *acmeEmail))
|
|
}
|
|
|
|
if *acmeUseStaging {
|
|
certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
|
|
}
|
|
|
|
if *acmeCa != "" {
|
|
certmagic.DefaultACME.CA = *acmeCa
|
|
}
|
|
|
|
certConfig := certmagic.NewDefault()
|
|
|
|
if *newAdminDomain != "" {
|
|
db.SetAdminDomain(*newAdminDomain)
|
|
}
|
|
|
|
adminDomain := db.GetAdminDomain()
|
|
|
|
if adminDomain == "" {
|
|
|
|
err = setAdminDomain(certConfig, db, namedropClient, autoCerts)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else {
|
|
if autoCerts {
|
|
err = certConfig.ManageSync(context.Background(), []string{adminDomain})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
log.Print(fmt.Sprintf("Successfully acquired certificate for admin domain (%s)", adminDomain))
|
|
}
|
|
}
|
|
|
|
// Add admin user if it doesn't already exist
|
|
users := db.GetUsers()
|
|
if len(users) == 0 {
|
|
db.AddUser("admin", true)
|
|
_, err := db.AddToken("admin", "")
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize admin user")
|
|
}
|
|
|
|
}
|
|
|
|
if *printLogin {
|
|
for token, tokenData := range db.GetTokens() {
|
|
if tokenData.Owner == "admin" && tokenData.Client == "" {
|
|
printLoginInfo(token, db.GetAdminDomain(), *httpsPort)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
config := &Config{
|
|
SshServerPort: *sshServerPort,
|
|
PublicIp: ip,
|
|
namedropClient: namedropClient,
|
|
autoCerts: autoCerts,
|
|
}
|
|
|
|
tunMan := NewTunnelManager(config, db, certConfig)
|
|
|
|
auth := NewAuth(db)
|
|
|
|
api := NewApi(config, db, auth, tunMan)
|
|
|
|
webUiHandler := NewWebUiHandler(config, db, api, auth)
|
|
|
|
httpClient := &http.Client{
|
|
// Don't follow redirects
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
|
|
httpListener := NewPassthroughListener()
|
|
|
|
p := &Server{db, tunMan, httpClient, httpListener}
|
|
|
|
tlsConfig := &tls.Config{
|
|
GetCertificate: certConfig.GetCertificate,
|
|
NextProtos: []string{"h2", "acme-tls/1"},
|
|
}
|
|
tlsListener := tls.NewListener(httpListener, tlsConfig)
|
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
timestamp := time.Now().Format(time.RFC3339)
|
|
|
|
remoteIp, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
w.WriteHeader(500)
|
|
io.WriteString(w, err.Error())
|
|
return
|
|
}
|
|
fmt.Println(fmt.Sprintf("%s %s %s %s %s", timestamp, remoteIp, r.Method, r.Host, r.URL.Path))
|
|
|
|
hostParts := strings.Split(r.Host, ":")
|
|
hostDomain := hostParts[0]
|
|
|
|
if r.URL.Path == "/namedrop/callback" {
|
|
r.ParseForm()
|
|
|
|
errorParam := r.Form.Get("error")
|
|
requestId := r.Form.Get("state")
|
|
code := r.Form.Get("code")
|
|
|
|
if errorParam != "" {
|
|
db.DeleteDNSRequest(requestId)
|
|
|
|
http.Redirect(w, r, "/alert?message=Domain request failed", 303)
|
|
return
|
|
}
|
|
|
|
namedropTokenData, err := namedropClient.GetToken(requestId, code)
|
|
if err != nil {
|
|
w.WriteHeader(500)
|
|
io.WriteString(w, err.Error())
|
|
return
|
|
}
|
|
|
|
domain := namedropTokenData.Scopes[0].Domain
|
|
host := namedropTokenData.Scopes[0].Host
|
|
|
|
recordType := "AAAA"
|
|
if IsIPv4(config.PublicIp) {
|
|
recordType = "A"
|
|
}
|
|
|
|
createRecordReq := namedrop.Record{
|
|
Domain: domain,
|
|
Host: host,
|
|
Type: recordType,
|
|
Value: config.PublicIp,
|
|
TTL: 300,
|
|
}
|
|
|
|
err = namedropClient.CreateRecord(createRecordReq)
|
|
if err != nil {
|
|
w.WriteHeader(500)
|
|
io.WriteString(w, err.Error())
|
|
return
|
|
}
|
|
|
|
fqdn := host + "." + domain
|
|
|
|
if db.GetAdminDomain() == "" {
|
|
db.SetAdminDomain(fqdn)
|
|
namedropClient.SetDomain(fqdn)
|
|
|
|
if autoCerts {
|
|
// TODO: Might want to get all certs here, not just the admin domain
|
|
err := certConfig.ManageSync(r.Context(), []string{fqdn})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
url := fmt.Sprintf("https://%s", fqdn)
|
|
|
|
// Automatically log using the first found admin token. This is safe to do here
|
|
// because we know that retrieving the admin domain was initiated from the CLI.
|
|
tokens := db.GetTokens()
|
|
for token, tokenData := range tokens {
|
|
if tokenData.Owner == "admin" {
|
|
url = url + "/login?access_token=" + token
|
|
break
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, url, 303)
|
|
} else {
|
|
adminDomain := db.GetAdminDomain()
|
|
http.Redirect(w, r, fmt.Sprintf("https://%s/edit-tunnel?domain=%s", adminDomain, fqdn), 303)
|
|
}
|
|
|
|
} else if hostDomain == db.GetAdminDomain() {
|
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
http.StripPrefix("/api", api).ServeHTTP(w, r)
|
|
} else {
|
|
webUiHandler.handleWebUiRequest(w, r)
|
|
}
|
|
} else {
|
|
|
|
tunnel, exists := db.GetTunnel(hostDomain)
|
|
if !exists {
|
|
errMessage := fmt.Sprintf("No tunnel attached to %s", hostDomain)
|
|
w.WriteHeader(500)
|
|
io.WriteString(w, errMessage)
|
|
return
|
|
}
|
|
|
|
proxyRequest(w, r, tunnel, httpClient, "localhost", tunnel.TunnelPort, *behindProxy)
|
|
}
|
|
})
|
|
|
|
go func() {
|
|
|
|
if *allowHttp {
|
|
if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), nil); err != nil {
|
|
log.Fatalf("ListenAndServe error: %v", err)
|
|
}
|
|
} else {
|
|
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
|
|
url := fmt.Sprintf("https://%s:%d%s", r.Host, *httpsPort, r.RequestURI)
|
|
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
|
}
|
|
|
|
if err := http.ListenAndServe(fmt.Sprintf(":%d", *httpPort), http.HandlerFunc(redirectTLS)); err != nil {
|
|
log.Fatalf("ListenAndServe error: %v", err)
|
|
}
|
|
}
|
|
|
|
}()
|
|
|
|
go http.Serve(tlsListener, nil)
|
|
|
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *httpsPort))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
log.Println("Ready")
|
|
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
log.Print(err)
|
|
continue
|
|
}
|
|
|
|
go p.handleConnection(conn, certConfig)
|
|
}
|
|
}
|
|
|
|
func (p *Server) handleConnection(clientConn net.Conn, certConfig *certmagic.Config) {
|
|
|
|
clientHello, clientReader, err := peekClientHello(clientConn)
|
|
if err != nil {
|
|
log.Println("peekClientHello error", err)
|
|
return
|
|
}
|
|
|
|
passConn := NewProxyConn(clientConn, clientReader)
|
|
|
|
tunnel, exists := p.db.GetTunnel(clientHello.ServerName)
|
|
|
|
if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" {
|
|
p.passthroughRequest(passConn, tunnel)
|
|
} else if exists && tunnel.TlsTermination == "server-tls" {
|
|
useTls := true
|
|
err := ProxyTcp(passConn, "127.0.0.1", tunnel.TunnelPort, useTls, certConfig)
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
return
|
|
}
|
|
} else {
|
|
p.httpListener.PassConn(passConn)
|
|
}
|
|
}
|
|
|
|
func (p *Server) passthroughRequest(conn net.Conn, tunnel Tunnel) {
|
|
|
|
upstreamAddr := fmt.Sprintf("localhost:%d", tunnel.TunnelPort)
|
|
upstreamConn, err := net.Dial("tcp", upstreamAddr)
|
|
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
defer upstreamConn.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
io.Copy(conn, upstreamConn)
|
|
conn.(*ProxyConn).CloseWrite()
|
|
wg.Done()
|
|
}()
|
|
go func() {
|
|
io.Copy(upstreamConn, conn)
|
|
upstreamConn.(*net.TCPConn).CloseWrite()
|
|
wg.Done()
|
|
}()
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func setAdminDomain(certConfig *certmagic.Config, db *Database, namedropClient *namedrop.Client, autoCerts bool) error {
|
|
action := prompt("\nNo admin domain set. Select an option below:\nEnter '1' to input manually\nEnter '2' to configure through TakingNames.io\n")
|
|
switch action {
|
|
case "1":
|
|
adminDomain := prompt("\nEnter admin domain:\n")
|
|
|
|
if autoCerts {
|
|
err := certConfig.ManageSync(context.Background(), []string{adminDomain})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
db.SetAdminDomain(adminDomain)
|
|
case "2":
|
|
|
|
log.Println("Get bootstrap domain")
|
|
|
|
namedropLink, err := namedropClient.BootstrapLink()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
qrterminal.GenerateHalfBlock(namedropLink, qrterminal.L, os.Stdout)
|
|
fmt.Println("Use the link below or scan the QR code above to select an admin domain:\n")
|
|
fmt.Printf("%s\n\n", namedropLink)
|
|
|
|
default:
|
|
log.Fatal("Invalid option")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func prompt(promptText string) string {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Print(promptText)
|
|
text, _ := reader.ReadString('\n')
|
|
return strings.TrimSpace(text)
|
|
}
|
|
|
|
func printLoginInfo(token, adminDomain string, httpsPort int) {
|
|
var url string
|
|
if httpsPort != 443 {
|
|
url = fmt.Sprintf("https://%s:%d/login?access_token=%s", adminDomain, httpsPort, token)
|
|
} else {
|
|
url = fmt.Sprintf("https://%s/login?access_token=%s", adminDomain, token)
|
|
}
|
|
log.Println(fmt.Sprintf("Admin login link: %s", url))
|
|
qrterminal.GenerateHalfBlock(url, qrterminal.L, os.Stdout)
|
|
}
|
|
|
|
// Taken from https://stackoverflow.com/a/48519490/943814
|
|
func IsIPv4(address string) bool {
|
|
return strings.Count(address, ":") < 2
|
|
}
|