Skip to content

Instantly share code, notes, and snippets.

@Wowfunhappy
Last active March 12, 2025 11:18
Show Gist options
  • Save Wowfunhappy/61dd39eab3df18ee53b39f3447e9e204 to your computer and use it in GitHub Desktop.
Save Wowfunhappy/61dd39eab3df18ee53b39f3447e9e204 to your computer and use it in GitHub Desktop.
Go Proxy
// 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