Last active
March 12, 2025 11:18
-
-
Save Wowfunhappy/61dd39eab3df18ee53b39f3447e9e204 to your computer and use it in GitHub Desktop.
Go Proxy
This file contains hidden or 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
// Written (entirely) by a combination of o1, o3-mini, and claude 3.5 sonnet, with lots of human testing and trial-and-error. | |
// This proxy will mitm https requests so that old apps on legacy versions of OS X can connect to modern https servers. | |
// Unlike Squid, this proxy does not appear to cause issues with pip or websockets. | |
// It does not require special rules for Apple domains to avoid breaking iMessage. It appears to be side effect free! | |
// Also unlike Squid, this proxy uses OS X's System Trust store for remote certificate validation. | |
// This is a good thing, but it means you probably want to manually add some modern certificates to Keychain Access, | |
// such as ISRG Root X1 certificate and GTS Root R1. | |
// Unfortunately, according to Activity Monitor, this proxy consumes much more CPU than Squid! | |
// I am currently deciding whether I want to update the Legacy Mac Proxy package to use this proxy instead of Squid. | |
// Built using Go 1.13.15. Make it work on legacy OS X via: | |
// install_name_tool -change /usr/lib/libSystem.B.dylib /path/to/libMacportsLegacySystem.B.dylib | |
package main | |
import ( | |
"context" | |
"crypto" | |
"crypto/ecdsa" | |
"crypto/elliptic" | |
"crypto/rand" | |
"crypto/tls" | |
"crypto/x509" | |
"crypto/x509/pkix" | |
"encoding/pem" | |
"errors" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"log" | |
"math/big" | |
"net" | |
"net/http" | |
"os" | |
"os/signal" | |
"strings" | |
"sync" | |
"syscall" | |
"time" | |
) | |
// Configuration variables | |
const ( | |
listenPort = "3129" | |
certFile = "/Library/Squid/Certificates/squid.pem" // CA cert | |
keyFile = "/Library/Squid/Certificates/squid-key.pem" // CA key | |
maxConnections = 1000 | |
dialTimeout = 10 * time.Second | |
handshakeTimeout = 10 * time.Second | |
idleTimeout = 60 * time.Second | |
certCacheExpiration = 24 * time.Hour | |
) | |
// Global variables | |
var ( | |
signerCert *x509.Certificate | |
signerKey crypto.PrivateKey | |
ecdsaForgedKey *ecdsa.PrivateKey // Reused for forging leaf certificates | |
certCache = &certificateCache{ | |
certs: make(map[string]*cachedCert), | |
} | |
activeConnections = &sync.WaitGroup{} | |
connectionCount = make(chan struct{}, maxConnections) | |
) | |
type cachedCert struct { | |
cert *tls.Certificate | |
expiresAt time.Time | |
} | |
type certificateCache struct { | |
sync.RWMutex | |
certs map[string]*cachedCert | |
} | |
func main() { | |
// Load the signing CA and key | |
var err error | |
signerCert, signerKey, err = loadCertAndKey(certFile, keyFile) | |
if err != nil { | |
log.Fatalf("Error loading signer certificate/key: %v", err) | |
} | |
// Generate a single ECDSA key for forging new leaf certificates | |
ecdsaForgedKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | |
if err != nil { | |
log.Fatalf("Failed to generate ECC forging key: %v", err) | |
} | |
// Create HTTP server | |
server := &http.Server{ | |
Addr: ":" + listenPort, | |
IdleTimeout: idleTimeout, | |
ReadTimeout: 30 * time.Second, | |
WriteTimeout: 30 * time.Second, | |
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
if r.Method != http.MethodConnect { | |
http.Error(w, "Only CONNECT method is supported", http.StatusMethodNotAllowed) | |
return | |
} | |
handleConnect(w, r) | |
}), | |
} | |
// Graceful shutdown | |
go func() { | |
sigChan := make(chan os.Signal, 1) | |
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) | |
<-sigChan | |
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | |
defer cancel() | |
if err := server.Shutdown(ctx); err != nil { | |
log.Printf("Server shutdown error: %v", err) | |
} | |
activeConnections.Wait() | |
os.Exit(0) | |
}() | |
// Periodic cleanup of cached forged certs | |
go func() { | |
ticker := time.NewTicker(time.Hour) | |
defer ticker.Stop() | |
for range ticker.C { | |
cleanCertCache() | |
} | |
}() | |
// Start server | |
if err := server.ListenAndServe(); err != http.ErrServerClosed { | |
log.Fatalf("Server error: %v", err) | |
} | |
} | |
func handleConnect(w http.ResponseWriter, r *http.Request) { | |
// Limit concurrent connections | |
select { | |
case connectionCount <- struct{}{}: | |
defer func() { <-connectionCount }() | |
default: | |
http.Error(w, "Too many connections", http.StatusServiceUnavailable) | |
return | |
} | |
activeConnections.Add(1) | |
defer activeConnections.Done() | |
// Hijack the HTTP connection | |
hijacker, ok := w.(http.Hijacker) | |
if !ok { | |
http.Error(w, "Hijacking not supported", http.StatusInternalServerError) | |
return | |
} | |
clientConn, _, err := hijacker.Hijack() | |
if err != nil { | |
log.Printf("Hijack error: %v", err) | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
return | |
} | |
defer clientConn.Close() | |
// Keep-alive | |
if tcpConn, ok := clientConn.(*net.TCPConn); ok { | |
tcpConn.SetKeepAlive(true) | |
tcpConn.SetKeepAlivePeriod(30 * time.Second) | |
} | |
// Acknowledge CONNECT | |
if _, err := clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil { | |
log.Printf("Error writing 200 response: %v", err) | |
return | |
} | |
// Connect to remote server | |
dialer := &net.Dialer{Timeout: dialTimeout} | |
serverConn, err := dialer.Dial("tcp", r.Host) | |
if err != nil { | |
log.Printf("Dial remote error: %v", err) | |
return | |
} | |
defer serverConn.Close() | |
// TLS handshake with the remote server | |
remoteTLSConf := &tls.Config{ | |
InsecureSkipVerify: true, // We'll do manual chain verification | |
ServerName: getServerName(r.Host), | |
} | |
remoteTLS := tls.Client(serverConn, remoteTLSConf) | |
remoteTLS.SetDeadline(time.Now().Add(handshakeTimeout)) | |
if err := remoteTLS.Handshake(); err != nil { | |
log.Printf("Remote TLS handshake error: %v", err) | |
return | |
} | |
remoteTLS.SetDeadline(time.Time{}) | |
// Gather remote chain | |
remoteState := remoteTLS.ConnectionState() | |
remoteChain := remoteState.PeerCertificates | |
if len(remoteChain) == 0 { | |
log.Printf("No certificate from remote host") | |
return | |
} | |
// Try verifying the entire remote chain with system roots | |
if err := verifyWithSystemRoots(remoteChain); err != nil { | |
// If that fails, chase AIA for the leaf. Then verify again. | |
leaf := remoteChain[0] | |
if _, aiaErr := chaseAIA(leaf); aiaErr != nil { | |
log.Printf("Certificate verification after AIA chase failed: %v", aiaErr) | |
return | |
} | |
if err2 := verifyWithSystemRoots(remoteChain); err2 != nil { | |
log.Printf("Certificate verification even after AIA chase failed: %v", err2) | |
return | |
} | |
} | |
// Forge or reuse a leaf for the client | |
host := getServerName(r.Host) | |
forgedTLSCert, err := getOrGenerateForgedCert(host, remoteChain[0]) | |
if err != nil { | |
log.Printf("Certificate forge error: %v", err) | |
return | |
} | |
// TLS handshake back to the client | |
serverTLSConf := &tls.Config{ | |
Certificates: []tls.Certificate{*forgedTLSCert}, | |
} | |
tlsClientConn := tls.Server(clientConn, serverTLSConf) | |
tlsClientConn.SetDeadline(time.Now().Add(handshakeTimeout)) | |
if err := tlsClientConn.Handshake(); err != nil { | |
log.Printf("Client TLS handshake error: %v", err) | |
return | |
} | |
tlsClientConn.SetDeadline(time.Time{}) | |
// Bidirectional copy | |
done := make(chan struct{}, 2) | |
go proxy(remoteTLS, tlsClientConn, "client->server", done) | |
go proxy(tlsClientConn, remoteTLS, "server->client", done) | |
<-done | |
<-done | |
} | |
// Copy data from src to dst | |
func proxy(dst io.Writer, src io.Reader, direction string, done chan<- struct{}) { | |
defer func() { done <- struct{}{} }() | |
buf := make([]byte, 64*1024) | |
if _, err := io.CopyBuffer(dst, src, buf); err != nil { | |
if !strings.Contains(err.Error(), "use of closed network connection") { | |
log.Printf("Error copying %s: %v", direction, err) | |
} | |
} | |
} | |
// Verify the provided chain (PeerCertificates) with system roots | |
func verifyWithSystemRoots(chain []*x509.Certificate) error { | |
if len(chain) == 0 { | |
return errors.New("empty chain") | |
} | |
pool, err := x509.SystemCertPool() | |
if err != nil { | |
return fmt.Errorf("failed to load system cert pool: %v", err) | |
} | |
// Put remote-supplied intermediates into our pool | |
intermediates := x509.NewCertPool() | |
for _, ic := range chain[1:] { | |
intermediates.AddCert(ic) | |
} | |
// Verify leaf (chain[0]) with any intermediates + system roots | |
opts := x509.VerifyOptions{ | |
Roots: pool, | |
Intermediates: intermediates, | |
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, | |
} | |
if _, err := chain[0].Verify(opts); err != nil { | |
return err | |
} | |
return nil | |
} | |
// Use AIA for the leaf if system verification fails, to fetch missing intermediates | |
func chaseAIA(cert *x509.Certificate) ([]*x509.Certificate, error) { | |
systemRoots, err := x509.SystemCertPool() | |
if err != nil { | |
return nil, fmt.Errorf("failed to load system cert pool: %v", err) | |
} | |
intermediates := x509.NewCertPool() | |
chain := []*x509.Certificate{cert} | |
current := cert | |
for { | |
if isSelfSigned(current) { | |
break | |
} | |
opts := x509.VerifyOptions{ | |
Roots: systemRoots, | |
Intermediates: intermediates, | |
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, | |
} | |
if _, vErr := current.Verify(opts); vErr == nil { | |
break | |
} | |
if len(current.IssuingCertificateURL) == 0 { | |
break | |
} | |
url := current.IssuingCertificateURL[0] | |
resp, err := http.Get(url) | |
if err != nil { | |
log.Printf("AIA fetch error: %v", err) | |
break | |
} | |
body, err := ioutil.ReadAll(resp.Body) | |
resp.Body.Close() | |
if err != nil { | |
log.Printf("AIA read error: %v", err) | |
break | |
} | |
var issuer *x509.Certificate | |
if issuer, err = x509.ParseCertificate(body); err != nil { | |
block, _ := pem.Decode(body) | |
if block == nil { | |
log.Printf("AIA: failed to decode fetched data") | |
break | |
} | |
if issuer, err = x509.ParseCertificate(block.Bytes); err != nil { | |
log.Printf("AIA: failed to parse fetched certificate: %v", err) | |
break | |
} | |
} | |
chain = append(chain, issuer) | |
intermediates.AddCert(issuer) | |
current = issuer | |
} | |
opts := x509.VerifyOptions{ | |
Roots: systemRoots, | |
Intermediates: intermediates, | |
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, | |
} | |
if _, err := cert.Verify(opts); err != nil { | |
return nil, fmt.Errorf("failed to verify certificate chain: %v", err) | |
} | |
return chain, nil | |
} | |
// Check if cert is self-signed | |
func isSelfSigned(cert *x509.Certificate) bool { | |
return cert.Subject.String() == cert.Issuer.String() | |
} | |
// Return forged cert for the host (cached if available) | |
func getOrGenerateForgedCert(host string, remoteCert *x509.Certificate) (*tls.Certificate, error) { | |
certCache.RLock() | |
if cached, ok := certCache.certs[host]; ok && time.Now().Before(cached.expiresAt) { | |
certCache.RUnlock() | |
return cached.cert, nil | |
} | |
certCache.RUnlock() | |
forged, err := generateForgedCert(host, remoteCert) | |
if err != nil { | |
return nil, err | |
} | |
certCache.Lock() | |
certCache.certs[host] = &cachedCert{ | |
cert: forged, | |
expiresAt: time.Now().Add(certCacheExpiration), | |
} | |
certCache.Unlock() | |
return forged, nil | |
} | |
// Remove expired certs from cache | |
func cleanCertCache() { | |
now := time.Now() | |
certCache.Lock() | |
for host, c := range certCache.certs { | |
if now.After(c.expiresAt) { | |
delete(certCache.certs, host) | |
} | |
} | |
certCache.Unlock() | |
} | |
// Load the signing certificate (CA) and its private key | |
func loadCertAndKey(certPath, keyPath string) (*x509.Certificate, crypto.PrivateKey, error) { | |
certPEM, err := ioutil.ReadFile(certPath) | |
if err != nil { | |
return nil, nil, err | |
} | |
keyPEM, err := ioutil.ReadFile(keyPath) | |
if err != nil { | |
return nil, nil, err | |
} | |
certBlock, _ := pem.Decode(certPEM) | |
if certBlock == nil { | |
return nil, nil, errors.New("failed to decode certificate PEM") | |
} | |
cert, err := x509.ParseCertificate(certBlock.Bytes) | |
if err != nil { | |
return nil, nil, fmt.Errorf("parse certificate error: %v", err) | |
} | |
keyBlock, _ := pem.Decode(keyPEM) | |
if keyBlock == nil { | |
return nil, nil, errors.New("failed to decode key PEM") | |
} | |
var key crypto.PrivateKey | |
switch keyBlock.Type { | |
case "RSA PRIVATE KEY": | |
key, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes) | |
case "PRIVATE KEY": | |
key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes) | |
default: | |
return nil, nil, fmt.Errorf("unsupported key type %q", keyBlock.Type) | |
} | |
if err != nil { | |
return nil, nil, fmt.Errorf("parse key error: %v", err) | |
} | |
return cert, key, nil | |
} | |
// Extract just the hostname portion from "host:port" | |
func getServerName(hostport string) string { | |
if strings.Contains(hostport, ":") { | |
if host, _, err := net.SplitHostPort(hostport); err == nil { | |
return host | |
} | |
} | |
return hostport | |
} | |
// Generate a newleaf certificate for the given hostname | |
func generateForgedCert(host string, remoteCert *x509.Certificate) (*tls.Certificate, error) { | |
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) | |
if err != nil { | |
return nil, fmt.Errorf("failed to generate serial number: %v", err) | |
} | |
template := &x509.Certificate{ | |
SerialNumber: serial, | |
Subject: pkix.Name{CommonName: host}, | |
NotBefore: time.Now().Add(-time.Hour), | |
NotAfter: time.Now().Add(24 * time.Hour), | |
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, | |
ExtKeyUsage: remoteCert.ExtKeyUsage, | |
DNSNames: remoteCert.DNSNames, | |
EmailAddresses: remoteCert.EmailAddresses, | |
IPAddresses: remoteCert.IPAddresses, | |
BasicConstraintsValid: true, | |
} | |
// Reuse the single ECDSA key (ecdsaForgedKey) | |
derBytes, err := x509.CreateCertificate( | |
rand.Reader, | |
template, | |
signerCert, | |
ecdsaForgedKey.Public(), | |
signerKey, | |
) | |
if err != nil { | |
return nil, fmt.Errorf("failed to create certificate: %v", err) | |
} | |
// Encode the forged cert and private key to PEM | |
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) | |
forgedKeyBytes, err := x509.MarshalECPrivateKey(ecdsaForgedKey) | |
if err != nil { | |
return nil, fmt.Errorf("failed to marshal ECDSA key: %v", err) | |
} | |
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: forgedKeyBytes}) | |
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) | |
if err != nil { | |
return nil, fmt.Errorf("failed to create TLS certificate: %v", err) | |
} | |
return &tlsCert, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment