Switch from TLS to HTTP proxying

Allows a few things:

* Can terminate HTTP/2 without the upstream needing to implement
  it
* Allows modification of headers in both directions
* Allows logging at the proxy
This commit is contained in:
Anders Pitman
2020-10-05 18:12:31 -06:00
parent 80556785d0
commit 2c4b97c0e2
4 changed files with 51 additions and 210 deletions

View File

@@ -1,32 +0,0 @@
package main
import (
"fmt"
"net"
)
type AdminListener struct {
connChan chan (net.Conn)
}
func NewAdminListener() *AdminListener {
connChan := make(chan (net.Conn))
return &AdminListener{connChan}
}
// implement net.Listener
func (l *AdminListener) Accept() (net.Conn, error) {
// TODO: error conditions?
conn := <-l.connChan
return conn, nil
}
func (l *AdminListener) Close() error {
// TODO
fmt.Println("AdminListener Close")
return nil
}
func (l *AdminListener) Addr() net.Addr {
// TODO
fmt.Println("AdminListener Addr")
return nil
}

View File

@@ -8,9 +8,7 @@ import (
"io"
"io/ioutil"
"log"
"net"
"net/http"
"sync"
)
type BoringProxyConfig struct {
@@ -29,11 +27,9 @@ type BoringProxy struct {
config *BoringProxyConfig
auth *Auth
tunMan *TunnelManager
adminListener *AdminListener
certConfig *certmagic.Config
}
func NewBoringProxy() *BoringProxy {
func Listen() {
config := &BoringProxyConfig{}
@@ -54,7 +50,6 @@ func NewBoringProxy() *BoringProxy {
certConfig := certmagic.NewDefault()
tunMan := NewTunnelManager(certConfig)
adminListener := NewAdminListener()
err = certConfig.ManageSync([]string{config.AdminDomain})
if err != nil {
@@ -64,106 +59,78 @@ func NewBoringProxy() *BoringProxy {
auth := NewAuth()
p := &BoringProxy{config, auth, tunMan, adminListener, certConfig}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p.handleAdminRequest(w, r)
})
p := &BoringProxy{config, auth, tunMan}
api := NewApi(config, auth, tunMan)
http.Handle("/api/", http.StripPrefix("/api", api))
go http.Serve(adminListener, nil)
log.Println("BoringProxy ready")
return p
}
func (p *BoringProxy) Run() {
listener, err := net.Listen("tcp", ":443")
tlsConfig := &tls.Config{
GetCertificate: certConfig.GetCertificate,
NextProtos: []string{"h2"},
}
tlsListener, err := tls.Listen("tcp", ":443", tlsConfig)
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
go p.handleConnection(conn)
}
}
func (p *BoringProxy) handleConnection(clientConn net.Conn) {
// TODO: does this need to be closed manually, or is it handled when decryptedConn is closed?
//defer clientConn.Close()
log.Println("handleConnection")
var serverName string
decryptedConn := tls.Server(clientConn, &tls.Config{
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
serverName = clientHello.ServerName
return p.certConfig.GetCertificate(clientHello)
},
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Host == config.AdminDomain {
p.handleAdminRequest(w, r)
} else {
p.proxyRequest(w, r)
}
})
//defer decryptedConn.Close()
// Need to manually do handshake to ensure serverName is populated by this point. Usually Handshake()
// is automatically called on first read/write
decryptedConn.Handshake()
log.Println("BoringProxy ready")
if serverName == p.config.AdminDomain {
p.handleAdminConnection(decryptedConn)
} else {
p.handleTunnelConnection(decryptedConn, serverName)
}
http.Serve(tlsListener, nil)
}
func (p *BoringProxy) handleAdminConnection(decryptedConn net.Conn) {
p.adminListener.connChan <- decryptedConn
}
func (p *BoringProxy) proxyRequest(w http.ResponseWriter, r *http.Request) {
func (p *BoringProxy) handleTunnelConnection(decryptedConn net.Conn, serverName string) {
defer decryptedConn.Close()
port, err := p.tunMan.GetPort(serverName)
port, err := p.tunMan.GetPort(r.Host)
if err != nil {
log.Print(err)
errMessage := fmt.Sprintf("HTTP/1.1 500 Internal server error\n\nNo tunnel attached to %s", serverName)
decryptedConn.Write([]byte(errMessage))
errMessage := fmt.Sprintf("No tunnel attached to %s", r.Host)
w.WriteHeader(500)
io.WriteString(w, errMessage)
return
}
upstreamAddr := fmt.Sprintf("127.0.0.1:%d", port)
httpClient := &http.Client{}
upstreamConn, err := net.Dial("tcp", upstreamAddr)
if err != nil {
downstreamReqHeaders := r.Header.Clone()
upstreamAddr := fmt.Sprintf("localhost:%d", port)
upstreamUrl := fmt.Sprintf("http://%s%s", upstreamAddr, r.URL.RequestURI())
upstreamReq, err := http.NewRequest(r.Method, upstreamUrl, r.Body)
if err != nil {
log.Print(err)
errMessage := fmt.Sprintf("%s", err)
w.WriteHeader(500)
io.WriteString(w, errMessage)
return
}
defer upstreamConn.Close()
}
var wg sync.WaitGroup
wg.Add(2)
upstreamReq.Header = downstreamReqHeaders
go func() {
io.Copy(decryptedConn, upstreamConn)
//decryptedConn.(*net.TCPConn).CloseWrite()
wg.Done()
}()
go func() {
io.Copy(upstreamConn, decryptedConn)
//upstreamConn.(*net.TCPConn).CloseWrite()
wg.Done()
}()
upstreamRes, err := httpClient.Do(upstreamReq)
if err != nil {
log.Print(err)
errMessage := fmt.Sprintf("%s", err)
w.WriteHeader(502)
io.WriteString(w, errMessage)
return
}
defer upstreamRes.Body.Close()
wg.Wait()
downstreamResHeaders := w.Header()
for k, v := range upstreamRes.Header {
downstreamResHeaders[k] = v
}
w.WriteHeader(upstreamRes.StatusCode)
io.Copy(w, upstreamRes.Body)
}

View File

@@ -18,8 +18,7 @@ func main() {
switch command {
case "server":
log.Println("Starting up")
proxy := NewBoringProxy()
proxy.Run()
Listen()
case "client":
client := NewBoringProxyClient()

View File

@@ -1,93 +0,0 @@
package main
import (
"fmt"
"golang.org/x/crypto/ssh"
"io/ioutil"
"log"
"net"
)
type SshServer struct {
config *ssh.ServerConfig
listener net.Listener
}
func NewSshServer() *SshServer {
config := &ssh.ServerConfig{}
privateBytes, err := ioutil.ReadFile("id_rsa_boringproxy")
if err != nil {
log.Fatal("Failed to load private key: ", err)
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
log.Fatal("Failed to parse private key: ", err)
}
config.AddHostKey(private)
listener, err := net.Listen("tcp", "0.0.0.0:2022")
if err != nil {
log.Fatal("failed to listen for connection: ", err)
}
server := &SshServer{config, listener}
go server.acceptAll()
return server
}
func (s *SshServer) acceptAll() {
for {
nConn, err := s.listener.Accept()
if err != nil {
log.Print("failed to accept incoming connection: ", err)
continue
}
go s.handleServerConn(nConn)
}
}
func (s *SshServer) handleServerConn(nConn net.Conn) {
var password string
s.config.PasswordCallback = func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
password = string(pass)
if c.User() == "user" && string(pass) == "yolo" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
}
conn, chans, reqs, err := ssh.NewServerConn(nConn, s.config)
if err != nil {
log.Print("failed to handshake: ", err)
return
}
fmt.Println(password)
go ssh.DiscardRequests(reqs)
go func() {
for newChannel := range chans {
newChannel.Reject(ssh.ResourceShortage, "too bad")
}
}()
ch, cReqs, err := conn.OpenChannel("boringproxy-tunnel", []byte{25, 25})
if err != nil {
log.Print(err)
return
}
go ssh.DiscardRequests(cReqs)
ch.Write([]byte("Hi there"))
ch.Close()
}