Merge remote-tracking branch 'upstream/master' into load-balancer

This commit is contained in:
Dany Mahmalat 2022-02-25 16:17:39 -05:00
commit 7410c8bd23
11 changed files with 282 additions and 119 deletions

View File

@ -17,7 +17,6 @@ RUN if [[ "ORIGIN" == 'remote' ]] ; then git clone --depth 1 --branch "${BRANCH}
COPY go.* ./ COPY go.* ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN export VERSION='2'
RUN cd cmd/boringproxy && CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \ RUN cd cmd/boringproxy && CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \
go build -ldflags "-X main.Version=${VERSION}" \ go build -ldflags "-X main.Version=${VERSION}" \
@ -25,6 +24,7 @@ RUN cd cmd/boringproxy && CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \
FROM scratch FROM scratch
EXPOSE 80 443 EXPOSE 80 443
WORKDIR /storage
COPY --from=builder /build/cmd/boringproxy/boringproxy / COPY --from=builder /build/cmd/boringproxy/boringproxy /

20
api.go
View File

@ -379,10 +379,26 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
} }
tlsTerm := params.Get("tls-termination") 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") 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{ request := Tunnel{
Domain: domain, Domain: domain,
Owner: owner, Owner: owner,
@ -394,6 +410,8 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
AuthUsername: username, AuthUsername: username,
AuthPassword: password, AuthPassword: password,
TlsTermination: tlsTerm, TlsTermination: tlsTerm,
ServerAddress: sshServerAddr,
ServerPort: sshServerPort,
Used: false, Used: false,
} }

View File

@ -46,6 +46,7 @@ func Listen() {
flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError) flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
newAdminDomain := flagSet.String("admin-domain", "", "Admin Domain") newAdminDomain := flagSet.String("admin-domain", "", "Admin Domain")
sshServerPort := flagSet.Int("ssh-server-port", 22, "SSH Server Port") 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") certDir := flagSet.String("cert-dir", "", "TLS cert directory")
printLogin := flagSet.Bool("print-login", false, "Prints admin login information") printLogin := flagSet.Bool("print-login", false, "Prints admin login information")
httpPort := flagSet.Int("http-port", 80, "HTTP (insecure) port") 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") allowHttp := flagSet.Bool("allow-http", false, "Allow unencrypted (HTTP) requests")
publicIp := flagSet.String("public-ip", "", "Public IP") publicIp := flagSet.String("public-ip", "", "Public IP")
behindProxy := flagSet.Bool("behind-proxy", false, "Whether we're running behind another reverse proxy") 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") 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:]) err := flagSet.Parse(os.Args[2:])
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%s: parsing flags: %s\n", os.Args[0], err) fmt.Fprintf(os.Stderr, "%s: parsing flags: %s\n", os.Args[0], err)
@ -61,7 +64,7 @@ func Listen() {
log.Println("Starting up") log.Println("Starting up")
db, err := NewDatabase() db, err := NewDatabase(*dbDir)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -101,6 +104,15 @@ func Listen() {
//certmagic.DefaultACME.DisableHTTPChallenge = true //certmagic.DefaultACME.DisableHTTPChallenge = true
//certmagic.DefaultACME.DisableTLSALPNChallenge = 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 { if *acmeUseStaging {
certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
} }
@ -328,11 +340,11 @@ func Listen() {
continue 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) clientHello, clientReader, err := peekClientHello(clientConn)
if err != nil { if err != nil {
@ -348,21 +360,31 @@ func (p *Server) handleConnection(clientConn net.Conn) {
log.Println("Retrying...") log.Println("Retrying...")
} }
tunnel, exists := p.db.SelectLoadBalancedTunnel(clientHello.ServerName) tunnel, exists := p.db.SelectLoadBalancedTunnel(clientHello.ServerName)
if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" { if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" {
err = p.passthroughRequest(passConn, tunnel) err = p.passthroughRequest(passConn, tunnel)
if err != nil { if err != nil {
log.Printf("Tunnel %s|%s connection failed\n", tunnel.Domain, tunnel.ClientName) log.Printf("Tunnel %s|%s connection failed\n", tunnel.Domain, tunnel.ClientName)
retry++ retry++
continue continue
} else {
break
}
} else { } else {
p.httpListener.PassConn(passConn)
break 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
} }
} }

107
client.go
View File

@ -6,12 +6,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
@ -321,18 +319,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error {
} else { } 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() { go func() {
for { for {
conn, err := listener.Accept() conn, err := listener.Accept()
@ -345,17 +331,27 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error {
break break
//continue //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 if tunnel.TlsTermination != "passthrough" {
// encapsulate it into a type? // TODO: There's still quite a bit of duplication with what the server does. Could we
err = c.certConfig.ManageSync(ctx, []string{tunnel.Domain}) // encapsulate it into a type?
if err != nil { err = c.certConfig.ManageSync(ctx, []string{tunnel.Domain})
log.Println("CertMagic error at startup") if err != nil {
log.Println(err) log.Println("CertMagic error at startup")
log.Println(err)
}
} }
<-ctx.Done() <-ctx.Done()
@ -363,75 +359,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error {
return nil 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{}) { func printJson(data interface{}) {
d, _ := json.MarshalIndent(data, "", " ") d, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(d)) fmt.Println(string(d))

View File

@ -2,9 +2,13 @@ package main
import ( import (
"context" "context"
"crypto/tls"
"flag" "flag"
"fmt" "fmt"
"io"
"net"
"os" "os"
"sync"
"github.com/boringproxy/boringproxy" "github.com/boringproxy/boringproxy"
) )
@ -15,6 +19,7 @@ Commands:
version Prints version information. version Prints version information.
server Start a new server. server Start a new server.
client Connect to a 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. 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) fmt.Fprintln(os.Stderr, msg)
os.Exit(1) os.Exit(1)
} }
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, os.Args[0]+": Need a command") fmt.Fprintln(os.Stderr, os.Args[0]+": Need a command")
@ -40,6 +44,44 @@ func main() {
fmt.Println(Version) fmt.Println(Version)
case "help", "-h", "--help", "-help": case "help", "-h", "--help", "-help":
fmt.Printf(usage, os.Args[0]) 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": case "server":
boringproxy.Listen() boringproxy.Listen()
case "client": case "client":
@ -97,3 +139,32 @@ func main() {
fail(os.Args[0] + ": Invalid command " + command) 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()
}

View File

@ -11,6 +11,8 @@ import (
"github.com/takingnames/namedrop-go" "github.com/takingnames/namedrop-go"
) )
var DBFolderPath string
type Database struct { type Database struct {
AdminDomain string `json:"admin_domain"` AdminDomain string `json:"admin_domain"`
Tokens map[string]TokenData `json:"tokens"` Tokens map[string]TokenData `json:"tokens"`
@ -62,11 +64,13 @@ type Tunnel struct {
Used bool 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 { if err != nil {
log.Println("failed reading boringproxy_db.json") log.Printf("failed reading %sboringproxy_db.json\n", DBFolderPath)
dbJson = []byte("{}") dbJson = []byte("{}")
} }
@ -359,5 +363,5 @@ func (d *Database) DeleteUser(username string) {
} }
func (d *Database) persist() { func (d *Database) persist() {
saveJson(d, "boringproxy_db.json") saveJson(d, DBFolderPath+"boringproxy_db.json")
} }

View File

@ -4,6 +4,9 @@
Edit docker-compose.yml and change the following under **commands** for service **boringproxy** Edit docker-compose.yml and change the following under **commands** for service **boringproxy**
- bp.example.com: your admin domain - 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 ## 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: You can build the image from source. This requires that you clone the GitHub repo and start docker using the compose command below:

View File

@ -7,11 +7,13 @@ services:
- "80:80" - "80:80"
- "443:443" - "443:443"
volumes: volumes:
- data:/opt/boringproxy/ - storage:/storage/
command: ["server", "-admin-domain", "bp.example.com", "-cert-dir", "/certmagic"] - 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: environment:
USER: "root" USER: "root"
volumes: volumes:
data: storage:
certmagic: ssh:

View File

@ -39,6 +39,7 @@
<option value="client">Client HTTPS</option> <option value="client">Client HTTPS</option>
<option value="server">Server HTTPS</option> <option value="server">Server HTTPS</option>
<option value="client-tls">Client raw TLS</option> <option value="client-tls">Client raw TLS</option>
<option value="server-tls">Server raw TLS</option>
<option value="passthrough">Passthrough</option> <option value="passthrough">Passthrough</option>
</select> </select>
</div> </div>
@ -58,6 +59,15 @@
</div> </div>
</div> </div>
<div class='input'>
<label for="ssh-server-addr">Override SSH Server Address:</label>
<input type="text" id="ssh-server-addr" name="ssh-server-addr">
</div>
<div class='input'>
<label for="ssh-server-port">Override SSH Server Port:</label>
<input type="text" id="ssh-server-port" name="ssh-server-port">
</div>
<button class='button' type="submit">Submit</button> <button class='button' type="submit">Submit</button>
</form> </form>

108
tls_proxy.go Normal file
View File

@ -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()
}

View File

@ -36,7 +36,7 @@ func NewTunnelManager(config *Config, db *Database, certConfig *certmagic.Config
if config.autoCerts { if config.autoCerts {
for _, tun := range db.GetTunnels() { 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}) err = certConfig.ManageSync(context.Background(), []string{tun.Domain})
if err != nil { if err != nil {
log.Println("CertMagic error at startup") log.Println("CertMagic error at startup")
@ -64,7 +64,7 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) {
return Tunnel{}, errors.New("Owner required") return Tunnel{}, errors.New("Owner required")
} }
if tunReq.TlsTermination == "server" { if tunReq.TlsTermination == "server" || tunReq.TlsTermination == "server-tls" {
if m.config.autoCerts { if m.config.autoCerts {
err := m.certConfig.ManageSync(context.Background(), []string{tunReq.Domain}) err := m.certConfig.ManageSync(context.Background(), []string{tunReq.Domain})
if err != nil { if err != nil {
@ -99,8 +99,6 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) {
return Tunnel{}, err return Tunnel{}, err
} }
tunReq.ServerAddress = m.db.GetAdminDomain()
tunReq.ServerPort = m.config.SshServerPort
tunReq.ServerPublicKey = "" tunReq.ServerPublicKey = ""
tunReq.Username = m.user.Username tunReq.Username = m.user.Username
tunReq.TunnelPrivateKey = privKey tunReq.TunnelPrivateKey = privKey