Skip to content

Instantly share code, notes, and snippets.

@fuji246
Created March 16, 2024 18:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fuji246/faf6b5afb2b85db166c227a49a64e021 to your computer and use it in GitHub Desktop.
Save fuji246/faf6b5afb2b85db166c227a49a64e021 to your computer and use it in GitHub Desktop.
HTTP Forward Proxy with TLS interception
// 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