mirror of
https://github.com/boringproxy/boringproxy.git
synced 2025-02-25 18:55:29 -06:00
Added the ability to scope tokens to a specific client. If enabled, this has the affect of limiting the token to being used to list tunnels for that specific client. It can't be used for the web UI or for any state-changing actions such as creating new tunnels.
419 lines
10 KiB
Go
419 lines
10 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")
|
|
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")
|
|
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()
|
|
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
|
|
//certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
|
|
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", "any")
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize admin user")
|
|
}
|
|
|
|
}
|
|
|
|
if *printLogin {
|
|
tokens := db.GetTokens()
|
|
|
|
for token, tokenData := range tokens {
|
|
if tokenData.Owner == "admin" {
|
|
printLoginInfo(token, db.GetAdminDomain())
|
|
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
|
|
|
|
createRecordReq := namedrop.Record{
|
|
Domain: domain,
|
|
Host: host,
|
|
Type: "A",
|
|
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)
|
|
}
|
|
}
|
|
|
|
func (p *Server) handleConnection(clientConn net.Conn) {
|
|
|
|
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 {
|
|
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) {
|
|
log.Println("Admin token: " + token)
|
|
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)
|
|
}
|