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.* ./
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 /

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")
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,
}

View File

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

107
client.go
View File

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

View File

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

View File

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

View File

@ -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:

View File

@ -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:

View File

@ -39,6 +39,7 @@
<option value="client">Client HTTPS</option>
<option value="server">Server HTTPS</option>
<option value="client-tls">Client raw TLS</option>
<option value="server-tls">Server raw TLS</option>
<option value="passthrough">Passthrough</option>
</select>
</div>
@ -58,6 +59,15 @@
</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>
</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 {
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