Created
March 16, 2024 18:47
-
-
Save fuji246/faf6b5afb2b85db166c227a49a64e021 to your computer and use it in GitHub Desktop.
HTTP Forward Proxy with TLS interception
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Implements a tunneling forward proxy for CONNECT requests, while also | |
// MITM-ing the connection and dumping the HTTPs requests/responses that cross | |
// the tunnel. | |
// | |
// Requires a certificate/key for a CA trusted by clients in order to generate | |
// and sign fake TLS certificates. | |
// | |
// Eli Bendersky [https://eli.thegreenplace.net] | |
// This code is in the public domain. | |
package main | |
import ( | |
"bufio" | |
"crypto" | |
"crypto/ecdsa" | |
"crypto/elliptic" | |
"crypto/rand" | |
"crypto/tls" | |
"crypto/x509" | |
"crypto/x509/pkix" | |
"encoding/pem" | |
"flag" | |
"io" | |
"io/ioutil" | |
"log" | |
"math/big" | |
"net" | |
"net/http" | |
"net/http/httputil" | |
"net/url" | |
"strings" | |
"time" | |
"github.com/pires/go-proxyproto" | |
) | |
// createCert creates a new certificate/private key pair for the given domains, | |
// signed by the parent/parentKey certificate. hoursValid is the duration of | |
// the new certificate's validity. | |
func createCert(dnsNames []string, parent *x509.Certificate, parentKey crypto.PrivateKey, hoursValid int) (cert []byte, priv []byte) { | |
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
if err != nil { | |
log.Fatalf("Failed to generate private key: %v", err) | |
} | |
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | |
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | |
if err != nil { | |
log.Fatalf("Failed to generate serial number: %v", err) | |
} | |
template := x509.Certificate{ | |
SerialNumber: serialNumber, | |
Subject: pkix.Name{ | |
Organization: []string{"Sample MITM proxy"}, | |
}, | |
DNSNames: dnsNames, | |
NotBefore: time.Now(), | |
NotAfter: time.Now().Add(time.Duration(hoursValid) * time.Hour), | |
KeyUsage: x509.KeyUsageDigitalSignature, | |
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, | |
BasicConstraintsValid: true, | |
} | |
derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, &privateKey.PublicKey, parentKey) | |
if err != nil { | |
log.Fatalf("Failed to create certificate: %v", err) | |
} | |
pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) | |
if pemCert == nil { | |
log.Fatal("failed to encode certificate to PEM") | |
} | |
privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) | |
if err != nil { | |
log.Fatalf("Unable to marshal private key: %v", err) | |
} | |
pemKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) | |
if pemCert == nil { | |
log.Fatal("failed to encode key to PEM") | |
} | |
err = ioutil.WriteFile("fake_cert.pem", pemCert, 0644) | |
if err != nil { | |
log.Fatalf("failed to write certificate PEM file: %v", err) | |
} | |
return pemCert, pemKey | |
} | |
// loadX509KeyPair loads a certificate/key pair from files, and unmarshals them | |
// into data structures from the x509 package. Note that private key types in Go | |
// don't have a shared named interface and use `any` (for backwards | |
// compatibility reasons). | |
func loadX509KeyPair(certFile, keyFile string) (cert *x509.Certificate, key any, err error) { | |
cf, err := ioutil.ReadFile(certFile) | |
if err != nil { | |
return nil, nil, err | |
} | |
kf, err := ioutil.ReadFile(keyFile) | |
if err != nil { | |
return nil, nil, err | |
} | |
certBlock, _ := pem.Decode(cf) | |
cert, err = x509.ParseCertificate(certBlock.Bytes) | |
if err != nil { | |
return nil, nil, err | |
} | |
keyBlock, _ := pem.Decode(kf) | |
key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes) | |
if err != nil { | |
return nil, nil, err | |
} | |
return cert, key, nil | |
} | |
// mitmProxy is a type implementing http.Handler that serves as a MITM proxy | |
// for CONNECT tunnels. Create new instances of mitmProxy using createMitmProxy. | |
type mitmProxy struct { | |
caCert *x509.Certificate | |
caKey any | |
} | |
// createMitmProxy creates a new MITM proxy. It should be passed the filenames | |
// for the certificate and private key of a certificate authority trusted by the | |
// client's machine. | |
func createMitmProxy(caCertFile, caKeyFile string) *mitmProxy { | |
caCert, caKey, err := loadX509KeyPair(caCertFile, caKeyFile) | |
if err != nil { | |
log.Fatal("Error loading CA certificate/key:", err) | |
} | |
log.Printf("loaded CA certificate and key; IsCA=%v\n", caCert.IsCA) | |
return &mitmProxy{ | |
caCert: caCert, | |
caKey: caKey, | |
} | |
} | |
func (p *mitmProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |
if req.Method == http.MethodConnect { | |
p.proxyConnect(w, req) | |
} else { | |
http.Error(w, "this proxy only supports CONNECT", http.StatusMethodNotAllowed) | |
} | |
} | |
// proxyConnect implements the MITM proxy for CONNECT tunnels. | |
func (p *mitmProxy) proxyConnect(w http.ResponseWriter, proxyReq *http.Request) { | |
log.Printf("CONNECT requested to %v (from %v)", proxyReq.Host, proxyReq.RemoteAddr) | |
// "Hijack" the client connection to get a TCP (or TLS) socket we can read | |
// and write arbitrary data to/from. | |
hj, ok := w.(http.Hijacker) | |
if !ok { | |
log.Fatal("http server doesn't support hijacking connection") | |
} | |
clientConn, _, err := hj.Hijack() | |
if err != nil { | |
log.Fatal("http hijacking failed") | |
} | |
log.Printf("CONNECT requested to %v (from %v) via (%v -> %v)", proxyReq.Host, proxyReq.RemoteAddr, clientConn.RemoteAddr(), clientConn.LocalAddr()) | |
// proxyReq.Host will hold the CONNECT target host, which will typically have | |
// a port - e.g. example.org:443 | |
// To generate a fake certificate for example.org, we have to first split off | |
// the host from the port. | |
host, _, err := net.SplitHostPort(proxyReq.Host) | |
if err != nil { | |
log.Fatal("error splitting host/port:", err) | |
} | |
// Create a fake TLS certificate for the target host, signed by our CA. The | |
// certificate will be valid for 10 days - this number can be changed. | |
pemCert, pemKey := createCert([]string{host}, p.caCert, p.caKey, 240) | |
tlsCert, err := tls.X509KeyPair(pemCert, pemKey) | |
if err != nil { | |
log.Fatal(err) | |
} | |
// Send an HTTP OK response back to the client; this initiates the CONNECT | |
// tunnel. From this point on the client will assume it's connected directly | |
// to the target. | |
if _, err := clientConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")); err != nil { | |
log.Fatal("error writing status to client:", err) | |
} | |
// Configure a new TLS server, pointing it at the client connection, using | |
// our certificate. This server will now pretend being the target. | |
tlsConfig := &tls.Config{ | |
PreferServerCipherSuites: true, | |
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, | |
MinVersion: tls.VersionTLS10, | |
Certificates: []tls.Certificate{tlsCert}, | |
NextProtos: []string{"http/1.1"}, | |
//GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { | |
// log.Printf("Received SNI: %s\n", hello.ServerName) | |
// return nil, nil | |
// }, | |
} | |
tlsConn := tls.Server(clientConn, tlsConfig) | |
defer tlsConn.Close() | |
// Create a buffered reader for the client connection; this is required to | |
// use http package functions with this connection. | |
connReader := bufio.NewReader(tlsConn) | |
// Run the proxy in a loop until the client closes the connection. | |
for { | |
// Read an HTTP request from the client; the request is sent over TLS that | |
// connReader is configured to serve. The read will run a TLS handshake in | |
// the first invocation (we could also call tlsConn.Handshake explicitly | |
// before the loop, but this isn't necessary). | |
// Note that while the client believes it's talking across an encrypted | |
// channel with the target, the proxy gets these requests in "plain text" | |
// because of the MITM setup. | |
r, err := http.ReadRequest(connReader) | |
if err == io.EOF { | |
break | |
} else if err != nil { | |
log.Fatal(err) | |
} | |
// We can dump the request; log it, modify it... | |
if b, err := httputil.DumpRequest(r, true); err == nil { | |
log.Printf("incoming request:\n%s\n", string(b)) | |
} | |
// Take the original request and changes its destination to be forwarded | |
// to the target server. | |
changeRequestToTarget(r, proxyReq.Host) | |
proxyURL, err := url.Parse("http://127.0.0.1:3128") | |
if err != nil { | |
log.Println("Error parsing proxy URL:", err) | |
log.Fatal(err) | |
} | |
transport := &http.Transport{ | |
Proxy: http.ProxyURL(proxyURL), | |
} | |
client := &http.Client{ | |
Transport: transport, | |
} | |
// Send the request to the target server and log the response. | |
resp, err := client.Do(r) | |
//resp, err := http.DefaultClient.Do(r) | |
if err != nil { | |
log.Fatal("error sending request to target:", err) | |
} | |
if b, err := httputil.DumpResponse(resp, false); err == nil { | |
log.Printf("target response:\n%s\n", string(b)) | |
} | |
defer resp.Body.Close() | |
// Send the target server's response back to the client. | |
if err := resp.Write(tlsConn); err != nil { | |
log.Println("error writing response back:", err) | |
} | |
} | |
} | |
// changeRequestToTarget modifies req to be re-routed to the given target; | |
// the target should be taken from the Host of the original tunnel (CONNECT) | |
// request. | |
func changeRequestToTarget(req *http.Request, targetHost string) { | |
targetUrl := addrToUrl(targetHost) | |
targetUrl.Path = req.URL.Path | |
targetUrl.RawQuery = req.URL.RawQuery | |
req.URL = targetUrl | |
// Make sure this is unset for sending the request through a client | |
req.RequestURI = "" | |
} | |
func addrToUrl(addr string) *url.URL { | |
if !strings.HasPrefix(addr, "https") { | |
addr = "https://" + addr | |
} | |
u, err := url.Parse(addr) | |
if err != nil { | |
log.Fatal(err) | |
} | |
return u | |
} | |
func main() { | |
var addr = flag.String("addr", "127.0.0.1:9999", "proxy address") | |
caCertFile := flag.String("cacertfile", "", "certificate .pem file for trusted CA") | |
caKeyFile := flag.String("cakeyfile", "", "key .pem file for trusted CA") | |
clientCert := flag.String("cltcertfile", "", "certificate .pem file for client verification") | |
serverCert := flag.String("srvcacertfile", "", "certificate .pem file for porxy server") | |
serverKey := flag.String("srvcakeyfile", "", "key .pem file for porxy server") | |
enableMtls := flag.Bool("mtls", false, "enable mtls") | |
enablePP := flag.Bool("pp", false, "enable receiving proxy protocol") | |
flag.Parse() | |
proxy := createMitmProxy(*caCertFile, *caKeyFile) | |
log.Println("Starting proxy server on", *addr) | |
if *enableMtls { | |
// Load CA certificate | |
caCert, err := ioutil.ReadFile(*clientCert) | |
if err != nil { | |
log.Fatalf("Failed to read Client certificate: %s", err) | |
} | |
// Create a CA certificate pool and add cert to it | |
caCertPool := x509.NewCertPool() | |
if !caCertPool.AppendCertsFromPEM(caCert) { | |
log.Fatalf("Failed to add CA certificate to pool") | |
} | |
// Load server's certificate and private key | |
cert, err := tls.LoadX509KeyPair(*serverCert, *serverKey) | |
if err != nil { | |
log.Fatalf("Failed to load key pair: %s", err) | |
} | |
// Set up TLS configuration | |
tlsConfig := &tls.Config{ | |
Certificates: []tls.Certificate{cert}, | |
ClientCAs: caCertPool, | |
ClientAuth: tls.RequireAndVerifyClientCert, | |
} | |
// Create an HTTPS server using the TLS configuration | |
server := &http.Server{ | |
Addr: *addr, | |
Handler: proxy, | |
TLSConfig: tlsConfig, | |
} | |
if *enablePP { | |
ln, err := net.Listen("tcp", server.Addr) | |
if err != nil { | |
log.Fatal("Listen:", err) | |
} | |
proxyListener := &proxyproto.Listener{ | |
Listener: ln, | |
ReadHeaderTimeout: 10 * time.Second, | |
} | |
defer proxyListener.Close() | |
if err := server.ServeTLS(proxyListener, "", ""); err != nil { | |
log.Fatal("ServeTLS:", err) | |
} | |
} else { | |
if err := server.ListenAndServeTLS("", ""); err != nil { | |
log.Fatal("ListenAndServeTLS:", err) | |
} | |
} | |
} else { | |
server := &http.Server{ | |
Addr: *addr, | |
Handler: proxy, | |
} | |
if *enablePP { | |
ln, err := net.Listen("tcp", server.Addr) | |
if err != nil { | |
log.Fatal("Listen:", err) | |
} | |
proxyListener := &proxyproto.Listener{ | |
Listener: ln, | |
ReadHeaderTimeout: 10 * time.Second, | |
} | |
defer proxyListener.Close() | |
if err := server.Serve(proxyListener); err != nil { | |
log.Fatal("Serve:", err) | |
} | |
} else { | |
if err := server.ListenAndServe(); err != nil { | |
log.Fatal("ListenAndServe:", err) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment