diff --git a/Dockerfile b/Dockerfile index 945640f..90afd94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,6 @@ RUN if [[ "ORIGIN" == 'remote' ]] ; then git clone --depth 1 --branch "${BRANCH} COPY go.* ./ RUN go mod download COPY . . -RUN export VERSION='2' RUN cd cmd/boringproxy && CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \ go build -ldflags "-X main.Version=${VERSION}" \ @@ -25,6 +24,7 @@ RUN cd cmd/boringproxy && CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \ FROM scratch EXPOSE 80 443 +WORKDIR /storage COPY --from=builder /build/cmd/boringproxy/boringproxy / diff --git a/api.go b/api.go index 23c5347..c0258a5 100644 --- a/api.go +++ b/api.go @@ -379,10 +379,26 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err } tlsTerm := params.Get("tls-termination") - if tlsTerm != "server" && tlsTerm != "client" && tlsTerm != "passthrough" && tlsTerm != "client-tls" { + if tlsTerm != "server" && tlsTerm != "client" && tlsTerm != "passthrough" && tlsTerm != "client-tls" && tlsTerm != "server-tls" { return nil, errors.New("Invalid tls-termination parameter") } + sshServerAddr := a.db.GetAdminDomain() + sshServerAddrParam := params.Get("ssh-server-addr") + if sshServerAddrParam != "" { + sshServerAddr = sshServerAddrParam + } + + sshServerPort := a.config.SshServerPort + sshServerPortParam := params.Get("ssh-server-port") + if sshServerPortParam != "" { + var err error + sshServerPort, err = strconv.Atoi(sshServerPortParam) + if err != nil { + return nil, errors.New("Invalid ssh-server-port parameter") + } + } + request := Tunnel{ Domain: domain, Owner: owner, @@ -394,6 +410,8 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err AuthUsername: username, AuthPassword: password, TlsTermination: tlsTerm, + ServerAddress: sshServerAddr, + ServerPort: sshServerPort, Used: false, } diff --git a/boringproxy.go b/boringproxy.go index de3a5db..d6203e7 100644 --- a/boringproxy.go +++ b/boringproxy.go @@ -46,6 +46,7 @@ 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") @@ -53,7 +54,9 @@ func Listen() { 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") err := flagSet.Parse(os.Args[2:]) if err != nil { fmt.Fprintf(os.Stderr, "%s: parsing flags: %s\n", os.Args[0], err) @@ -61,7 +64,7 @@ func Listen() { log.Println("Starting up") - db, err := NewDatabase() + db, err := NewDatabase(*dbDir) if err != nil { log.Fatal(err) } @@ -101,6 +104,15 @@ func Listen() { //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 } @@ -328,11 +340,11 @@ func Listen() { continue } - go p.handleConnection(conn) + go p.handleConnection(conn, certConfig) } } -func (p *Server) handleConnection(clientConn net.Conn) { +func (p *Server) handleConnection(clientConn net.Conn, certConfig *certmagic.Config) { clientHello, clientReader, err := peekClientHello(clientConn) if err != nil { @@ -348,21 +360,31 @@ func (p *Server) handleConnection(clientConn net.Conn) { log.Println("Retrying...") } - tunnel, exists := p.db.SelectLoadBalancedTunnel(clientHello.ServerName) - if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" { - err = p.passthroughRequest(passConn, tunnel) + tunnel, exists := p.db.SelectLoadBalancedTunnel(clientHello.ServerName) + if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" { + err = p.passthroughRequest(passConn, tunnel) - if err != nil { - log.Printf("Tunnel %s|%s connection failed\n", tunnel.Domain, tunnel.ClientName) - retry++ - continue - } else { - break - } + if err != nil { + log.Printf("Tunnel %s|%s connection failed\n", tunnel.Domain, tunnel.ClientName) + retry++ + continue } else { - p.httpListener.PassConn(passConn) break } + } 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()) + log.Printf("Tunnel %s|%s connection failed\n", tunnel.Domain, tunnel.ClientName) + retry++ + continue + } else { + break + } + } else { + p.httpListener.PassConn(passConn) + break } } diff --git a/client.go b/client.go index cfbeda0..9cf2d45 100644 --- a/client.go +++ b/client.go @@ -6,12 +6,10 @@ import ( "encoding/json" "errors" "fmt" - "io" "io/ioutil" "log" "net" "net/http" - "strings" "sync" "time" @@ -321,18 +319,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { } else { - if tunnel.TlsTermination == "client-tls" { - tlsConfig := &tls.Config{ - GetCertificate: c.certConfig.GetCertificate, - } - - tlsConfig.NextProtos = append([]string{"http/1.1", "h2", "acme-tls/1"}, tlsConfig.NextProtos...) - - tlsListener := tls.NewListener(listener, tlsConfig) - - listener = tlsListener - } - go func() { for { conn, err := listener.Accept() @@ -345,17 +331,27 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { break //continue } - go c.handleConnection(conn, tunnel.ClientAddress, tunnel.ClientPort) + + var useTls bool + if tunnel.TlsTermination == "client-tls" { + useTls = true + } else { + useTls = false + } + + go ProxyTcp(conn, tunnel.ClientAddress, tunnel.ClientPort, useTls, c.certConfig) } }() } - // TODO: There's still quite a bit of duplication with what the server does. Could we - // encapsulate it into a type? - err = c.certConfig.ManageSync(ctx, []string{tunnel.Domain}) - if err != nil { - log.Println("CertMagic error at startup") - log.Println(err) + if tunnel.TlsTermination != "passthrough" { + // TODO: There's still quite a bit of duplication with what the server does. Could we + // encapsulate it into a type? + err = c.certConfig.ManageSync(ctx, []string{tunnel.Domain}) + if err != nil { + log.Println("CertMagic error at startup") + log.Println(err) + } } <-ctx.Done() @@ -363,75 +359,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { return nil } -func (c *Client) handleConnection(conn net.Conn, upstreamAddr string, port int) { - - defer conn.Close() - - useTls := false - addr := upstreamAddr - - if strings.HasPrefix(upstreamAddr, "https://") { - addr = upstreamAddr[len("https://"):] - useTls = true - } - - var upstreamConn net.Conn - var err error - - if useTls { - tlsConfig := &tls.Config{ - InsecureSkipVerify: true, - } - upstreamConn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", addr, port), tlsConfig) - } else { - upstreamConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - } - - if err != nil { - log.Print(err) - return - } - - defer upstreamConn.Close() - - var wg sync.WaitGroup - wg.Add(2) - - // Copy request to upstream - go func() { - _, err := io.Copy(upstreamConn, conn) - if err != nil { - log.Println(err.Error()) - } - - if c, ok := upstreamConn.(*net.TCPConn); ok { - c.CloseWrite() - } else if c, ok := upstreamConn.(*tls.Conn); ok { - c.CloseWrite() - } - - wg.Done() - }() - - // Copy response to downstream - go func() { - _, err := io.Copy(conn, upstreamConn) - //conn.(*net.TCPConn).CloseWrite() - if err != nil { - log.Println(err.Error()) - } - // TODO: I added this to fix a bug where the copy to - // upstreamConn was never closing, even though the copy to - // conn was. It seems related to persistent connections going - // idle and upstream closing the connection. I'm a bit worried - // this might not be thread safe. - conn.Close() - wg.Done() - }() - - wg.Wait() -} - func printJson(data interface{}) { d, _ := json.MarshalIndent(data, "", " ") fmt.Println(string(d)) diff --git a/cmd/boringproxy/main.go b/cmd/boringproxy/main.go index 4e28b61..5331dc1 100644 --- a/cmd/boringproxy/main.go +++ b/cmd/boringproxy/main.go @@ -2,9 +2,13 @@ package main import ( "context" + "crypto/tls" "flag" "fmt" + "io" + "net" "os" + "sync" "github.com/boringproxy/boringproxy" ) @@ -15,6 +19,7 @@ Commands: version Prints version information. server Start a new server. client Connect to a server. + tuntls Tunnel a raw TLS connection. Use "%[1]s command -h" for a list of flags for the command. ` @@ -25,7 +30,6 @@ func fail(msg string) { fmt.Fprintln(os.Stderr, msg) os.Exit(1) } - func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, os.Args[0]+": Need a command") @@ -40,6 +44,44 @@ func main() { fmt.Println(Version) case "help", "-h", "--help", "-help": fmt.Printf(usage, os.Args[0]) + case "tuntls": + // This command is a direct port of https://github.com/anderspitman/tuntls + flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + server := flagSet.String("server", "", "boringproxy server") + port := flagSet.Int("port", 0, "Local port to bind to") + err := flagSet.Parse(os.Args[2:]) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: parsing flags: %s\n", os.Args[0], err) + os.Exit(1) + } + + if *server == "" { + fmt.Fprintf(os.Stderr, "server argument is required\n") + os.Exit(1) + } + + if *port == 0 { + // one-time tunnel over stdin/stdout + doTlsTunnel(*server, os.Stdin, os.Stdout) + } else { + // listen on a port and create tunnels for each connection + fmt.Fprintf(os.Stderr, "Listening on port %d\n", *port) + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + for { + conn, err := listener.Accept() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + go doTlsTunnel(*server, conn, conn) + } + } case "server": boringproxy.Listen() case "client": @@ -97,3 +139,32 @@ func main() { fail(os.Args[0] + ": Invalid command " + command) } } + +func doTlsTunnel(server string, in io.Reader, out io.Writer) { + fmt.Fprintf(os.Stderr, "tuntls connecting to server: %s\n", server) + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:443", server), &tls.Config{ + //RootCAs: roots, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to connect: "+err.Error()) + os.Exit(1) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + io.Copy(conn, in) + wg.Done() + }() + + go func() { + io.Copy(out, conn) + wg.Done() + }() + + wg.Wait() + + conn.Close() +} diff --git a/database.go b/database.go index edc7b2e..a6dea9a 100644 --- a/database.go +++ b/database.go @@ -11,6 +11,8 @@ import ( "github.com/takingnames/namedrop-go" ) +var DBFolderPath string + type Database struct { AdminDomain string `json:"admin_domain"` Tokens map[string]TokenData `json:"tokens"` @@ -62,11 +64,13 @@ type Tunnel struct { Used bool } -func NewDatabase() (*Database, error) { +func NewDatabase(path string) (*Database, error) { - dbJson, err := ioutil.ReadFile("boringproxy_db.json") + DBFolderPath = path + + dbJson, err := ioutil.ReadFile(DBFolderPath + "boringproxy_db.json") if err != nil { - log.Println("failed reading boringproxy_db.json") + log.Printf("failed reading %sboringproxy_db.json\n", DBFolderPath) dbJson = []byte("{}") } @@ -359,5 +363,5 @@ func (d *Database) DeleteUser(username string) { } func (d *Database) persist() { - saveJson(d, "boringproxy_db.json") + saveJson(d, DBFolderPath+"boringproxy_db.json") } diff --git a/docker/server/README.md b/docker/server/README.md index 4e589d4..40eec09 100644 --- a/docker/server/README.md +++ b/docker/server/README.md @@ -4,6 +4,9 @@ Edit docker-compose.yml and change the following under **commands** for service **boringproxy** - bp.example.com: your admin domain +- your-email-address: the email address to register with Let's Encrypt + +***Since the -accept-ca-terms flag is set in the compose file, this will automatically accept terms and conditions of Let's Encrypt.*** ## Build image from source and run server in docker You can build the image from source. This requires that you clone the GitHub repo and start docker using the compose command below: diff --git a/docker/server/docker-compose.yml b/docker/server/docker-compose.yml index f3e8753..6addc03 100644 --- a/docker/server/docker-compose.yml +++ b/docker/server/docker-compose.yml @@ -7,11 +7,13 @@ services: - "80:80" - "443:443" volumes: - - data:/opt/boringproxy/ - command: ["server", "-admin-domain", "bp.example.com", "-cert-dir", "/certmagic"] + - storage:/storage/ + - ssh://.ssh + - /etc/ssl/certs/:/etc/ssl/certs/:ro + command: ["server", "-admin-domain", "bp.example.com", "-acme-email", "your-email-address", "-accept-ca-terms", "-cert-dir", "/storage/certmagic", "-print-login"] environment: USER: "root" volumes: - data: - certmagic: + storage: + ssh: \ No newline at end of file diff --git a/templates/edit_tunnel.tmpl b/templates/edit_tunnel.tmpl index 30c3684..94366f2 100644 --- a/templates/edit_tunnel.tmpl +++ b/templates/edit_tunnel.tmpl @@ -39,6 +39,7 @@ + @@ -58,6 +59,15 @@ +
+ + +
+
+ + +
+ diff --git a/tls_proxy.go b/tls_proxy.go new file mode 100644 index 0000000..dbc6476 --- /dev/null +++ b/tls_proxy.go @@ -0,0 +1,108 @@ +package boringproxy + +import ( + //"errors" + "crypto/tls" + "fmt" + "io" + "log" + "net" + "strings" + "sync" + + "github.com/caddyserver/certmagic" +) + +func ProxyTcp(conn net.Conn, addr string, port int, useTls bool, certConfig *certmagic.Config) error { + + if useTls { + tlsConfig := &tls.Config{ + GetCertificate: certConfig.GetCertificate, + } + + tlsConfig.NextProtos = append([]string{"http/1.1", "h2", "acme-tls/1"}, tlsConfig.NextProtos...) + + tlsConn := tls.Server(conn, tlsConfig) + + tlsConn.Handshake() + if tlsConn.ConnectionState().NegotiatedProtocol == "acme-tls/1" { + tlsConn.Close() + return nil + } + + go handleConnection(tlsConn, addr, port) + } else { + go handleConnection(conn, addr, port) + } + + return nil +} + +func handleConnection(conn net.Conn, upstreamAddr string, port int) { + + defer conn.Close() + + useTls := false + addr := upstreamAddr + + if strings.HasPrefix(upstreamAddr, "https://") { + addr = upstreamAddr[len("https://"):] + useTls = true + } + + var upstreamConn net.Conn + var err error + + if useTls { + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + upstreamConn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", addr, port), tlsConfig) + } else { + upstreamConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + } + + if err != nil { + log.Print(err) + return + } + + defer upstreamConn.Close() + + var wg sync.WaitGroup + wg.Add(2) + + // Copy request to upstream + go func() { + _, err := io.Copy(upstreamConn, conn) + if err != nil { + log.Println(err.Error()) + } + + if c, ok := upstreamConn.(*net.TCPConn); ok { + c.CloseWrite() + } else if c, ok := upstreamConn.(*tls.Conn); ok { + c.CloseWrite() + } + + wg.Done() + }() + + // Copy response to downstream + go func() { + _, err := io.Copy(conn, upstreamConn) + //conn.(*net.TCPConn).CloseWrite() + if err != nil { + log.Println(err.Error()) + } + // TODO: I added this to fix a bug where the copy to + // upstreamConn was never closing, even though the copy to + // conn was. It seems related to persistent connections going + // idle and upstream closing the connection. I'm a bit worried + // this might not be thread safe. + conn.Close() + wg.Done() + }() + + wg.Wait() +} diff --git a/tunnel_manager.go b/tunnel_manager.go index 05f5bdb..9bc58e0 100644 --- a/tunnel_manager.go +++ b/tunnel_manager.go @@ -36,7 +36,7 @@ func NewTunnelManager(config *Config, db *Database, certConfig *certmagic.Config if config.autoCerts { for _, tun := range db.GetTunnels() { - if tun.TlsTermination == "server" { + if tun.TlsTermination == "server" || tun.TlsTermination == "server-tls" { err = certConfig.ManageSync(context.Background(), []string{tun.Domain}) if err != nil { log.Println("CertMagic error at startup") @@ -64,7 +64,7 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) { return Tunnel{}, errors.New("Owner required") } - if tunReq.TlsTermination == "server" { + if tunReq.TlsTermination == "server" || tunReq.TlsTermination == "server-tls" { if m.config.autoCerts { err := m.certConfig.ManageSync(context.Background(), []string{tunReq.Domain}) if err != nil { @@ -99,8 +99,6 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) { return Tunnel{}, err } - tunReq.ServerAddress = m.db.GetAdminDomain() - tunReq.ServerPort = m.config.SshServerPort tunReq.ServerPublicKey = "" tunReq.Username = m.user.Username tunReq.TunnelPrivateKey = privKey