This commit is contained in:
Matthias Bilger 2024-07-06 12:13:39 +02:00 committed by GitHub
commit 7958c089e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 127 additions and 82 deletions

207
client.go
View File

@ -9,9 +9,12 @@ import (
"io/ioutil"
"log"
"net"
"os/exec"
"net/http"
"sync"
"strings"
"time"
"io"
"github.com/caddyserver/certmagic"
"golang.org/x/crypto/ssh"
@ -30,6 +33,7 @@ type Client struct {
certConfig *certmagic.Config
behindProxy bool
pollInterval int
proxyCommand string
}
type ClientConfig struct {
@ -44,6 +48,7 @@ type ClientConfig struct {
DnsServer string `json:"dnsServer,omitempty"`
BehindProxy bool `json:"behindProxy,omitempty"`
PollInterval int `json:"pollInterval,omitempty"`
ProxyCommand string `json:"proxyComman,omitempty"`
}
func NewClient(config *ClientConfig) (*Client, error) {
@ -116,6 +121,7 @@ func NewClient(config *ClientConfig) (*Client, error) {
certConfig: certConfig,
behindProxy: config.BehindProxy,
pollInterval: config.PollInterval,
proxyCommand: config.ProxyCommand,
}, nil
}
@ -277,107 +283,144 @@ func (c *Client) SyncTunnels(ctx context.Context, serverTunnels map[string]Tunne
}
func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error {
log.Println("BoreTunnel", tunnel.Domain)
log.Println("BoreTunnel", tunnel.Domain)
signer, err := ssh.ParsePrivateKey([]byte(tunnel.TunnelPrivateKey))
if err != nil {
return fmt.Errorf("Unable to parse private key: %v", err)
}
signer, err := ssh.ParsePrivateKey([]byte(tunnel.TunnelPrivateKey))
if err != nil {
return fmt.Errorf("Unable to parse private key: %v", err)
}
config := &ssh.ClientConfig{
User: tunnel.Username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
//var hostKey ssh.PublicKey
sshHost := fmt.Sprintf("%s:%d", tunnel.ServerAddress, tunnel.ServerPort)
config := &ssh.ClientConfig{
User: tunnel.Username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
//HostKeyCallback: ssh.FixedHostKey(hostKey),
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
var client *ssh.Client
if c.proxyCommand == "" {
// Use direct SSH connection
client, err = ssh.Dial("tcp", sshHost, config)
if err != nil {
return fmt.Errorf("Failed to dial: %v", err)
}
} else {
// Use SSH over proxy command
tmpProxyCommand := strings.Replace(c.proxyCommand, "%h", tunnel.ServerAddress, -1)
proxyCommand := strings.Replace(tmpProxyCommand, "%p", fmt.Sprintf("%d", tunnel.ServerPort), -1)
sshHost := fmt.Sprintf("%s:%d", tunnel.ServerAddress, tunnel.ServerPort)
client, err := ssh.Dial("tcp", sshHost, config)
if err != nil {
return fmt.Errorf("Failed to dial: %v", err)
}
defer client.Close()
cmd := exec.CommandContext(ctx, "sh", "-c", proxyCommand)
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("Failed to setup proxy command stdin: %v", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("Failed to setup proxy command stdout: %v", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("Failed to setup proxy command stderr: %v", err)
}
err = cmd.Start()
if err != nil {
return fmt.Errorf("Failed to start proxy command: %v", err)
}
bindAddr := "127.0.0.1"
if tunnel.AllowExternalTcp {
bindAddr = "0.0.0.0"
}
tunnelAddr := fmt.Sprintf("%s:%d", bindAddr, tunnel.TunnelPort)
listener, err := client.Listen("tcp", tunnelAddr)
if err != nil {
return fmt.Errorf("Unable to register tcp forward for %s:%d %v", bindAddr, tunnel.TunnelPort, err)
}
defer listener.Close()
// Create a full-duplex pipe pair
localConn, remoteConn := net.Pipe()
defer localConn.Close()
if tunnel.TlsTermination == "client" {
go func() {
defer remoteConn.Close()
io.Copy(remoteConn, stdout)
}()
go func() {
defer remoteConn.Close()
io.Copy(stdin, remoteConn)
}()
tlsConfig := &tls.Config{
GetCertificate: c.certConfig.GetCertificate,
NextProtos: []string{"h2", "acme-tls/1"},
}
tlsListener := tls.NewListener(listener, tlsConfig)
// Dial SSH over the proxy connection
clientConn, chans, reqs, err := ssh.NewClientConn(localConn, sshHost, config)
if err != nil {
return fmt.Errorf("Failed to create SSH client connection: %v", err)
}
client = ssh.NewClient(clientConn, chans, reqs)
httpMux := http.NewServeMux()
// Reading stderr to check for errors
go func() {
errOutput, _ := ioutil.ReadAll(stderr)
if len(errOutput) > 0 {
log.Printf("Proxy command stderr: %s", errOutput)
}
}()
}
httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
proxyRequest(w, r, tunnel, c.httpClient, tunnel.ClientAddress, tunnel.ClientPort, c.behindProxy)
})
defer client.Close()
httpServer := &http.Server{
Handler: httpMux,
}
bindAddr := "127.0.0.1"
if tunnel.AllowExternalTcp {
bindAddr = "0.0.0.0"
}
tunnelAddr := fmt.Sprintf("%s:%d", bindAddr, tunnel.TunnelPort)
listener, err := client.Listen("tcp", tunnelAddr)
if err != nil {
return fmt.Errorf("Unable to register tcp forward for %s:%d %v", bindAddr, tunnel.TunnelPort, err)
}
defer listener.Close()
// TODO: It seems inefficient to make a separate HTTP server for each TLS-passthrough tunnel,
// but the code is much simpler. The only alternative I've thought of so far involves storing
// all the tunnels in a mutexed map and retrieving them from a single HTTP server, same as the
// boringproxy server does.
go httpServer.Serve(tlsListener)
if tunnel.TlsTermination == "client" {
tlsConfig := &tls.Config{
GetCertificate: c.certConfig.GetCertificate,
NextProtos: []string{"h2", "acme-tls/1"},
}
tlsListener := tls.NewListener(listener, tlsConfig)
} else {
httpMux := http.NewServeMux()
go func() {
for {
conn, err := listener.Accept()
if err != nil {
// TODO: Currently assuming an error means the
// tunnel was manually deleted, but there
// could be other errors that we should be
// attempting to recover from rather than
// breaking.
break
//continue
}
httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
proxyRequest(w, r, tunnel, c.httpClient, tunnel.ClientAddress, tunnel.ClientPort, c.behindProxy)
})
var useTls bool
if tunnel.TlsTermination == "client-tls" {
useTls = true
} else {
useTls = false
}
httpServer := &http.Server{
Handler: httpMux,
}
go ProxyTcp(conn, tunnel.ClientAddress, tunnel.ClientPort, useTls, c.certConfig)
}
}()
}
go httpServer.Serve(tlsListener)
} else {
go func() {
for {
conn, err := listener.Accept()
if err != nil {
break
}
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)
}
}
var useTls bool
if tunnel.TlsTermination == "client-tls" {
useTls = true
} else {
useTls = false
}
<-ctx.Done()
go ProxyTcp(conn, tunnel.ClientAddress, tunnel.ClientPort, useTls, c.certConfig)
}
}()
}
return nil
if tunnel.TlsTermination != "passthrough" {
err = c.certConfig.ManageSync(ctx, []string{tunnel.Domain})
if err != nil {
log.Println("CertMagic error at startup")
log.Println(err)
}
}
<-ctx.Done()
return nil
}
func printJson(data interface{}) {

View File

@ -97,6 +97,7 @@ func main() {
dnsServer := flagSet.String("dns-server", "", "Custom DNS server")
behindProxy := flagSet.Bool("behind-proxy", false, "Whether we're running behind another reverse proxy")
pollInterval := flagSet.Int("poll-interval-ms", 2000, "Interval in milliseconds to poll for tunnel changes")
proxyCommand := flagSet.String("proxy-command", "", "ProxyCommand to use for outgoing connectin")
err := flagSet.Parse(os.Args[2:])
if err != nil {
@ -128,6 +129,7 @@ func main() {
DnsServer: *dnsServer,
BehindProxy: *behindProxy,
PollInterval: *pollInterval,
ProxyCommand: *proxyCommand,
}
ctx := context.Background()