├── README.md ├── cert_generator └── cert_generator.go ├── go.mod ├── go.sum ├── hijackers ├── config.go ├── factory.go ├── interface.go ├── passthrough.go └── utls.go ├── mirror.sh ├── mirror_proxy.go ├── options.go ├── tls_hijacker.go └── utils ├── errors.go └── tee_conn.go /README.md: -------------------------------------------------------------------------------- 1 | TLS MITM proxy for reverse-engineering purposes 2 | === 3 | 4 | ## Why 5 | 6 | It is often necessary to decrypt TLS traffic when reverse-engineering applications. 7 | This can easily be done with tools like [mitmproxy](https://github.com/mitmproxy/mitmproxy). 8 | However, server can detect such MITM attack due to mitmproxy's a very specific TLS fingerprint 9 | (think [JA3](https://github.com/salesforce/ja3) and other implementations). OpenSSL backend 10 | cannot be easily configured to mimic given fingerprint (if you know how to do it, please 11 | open issue and tell me). Servers can detect this and refuse connection or disallow access 12 | (for example, Cloudflare often does this). 13 | 14 | ## What 15 | 16 | This tool is written to address these problems using awesome [utls](https://github.com/refraction-networking/utls) 17 | library. For software under test (SUT) it looks like a usual HTTP proxy. When SUT connects through it, ClientHello 18 | is fingerprinted and upstream connection with the same fingerprint is established. Data inside TLS tunnel are not 19 | modified so higher-level fingerprints (like [HTTP/2 fingerprint from Akamai](https://www.blackhat.com/docs/eu-17/materials/eu-17-Shuster-Passive-Fingerprinting-Of-HTTP2-Clients-wp.pdf)) 20 | are also preserved. Both client and upstream connections` TLS keys are logged so tools like Wireshark can be used 21 | to record and inspect traffic passively. 22 | 23 | ``` 24 | -------------- --------------- -------------- -------------- 25 | | Software | | Mirror | | Upstream | | Remote | 26 | | under |<----+---->| proxy |<----+---->| proxy |<-------->| Server | 27 | | test | | | (this tool) | | | (optional) | | | 28 | -------------- | --------------- | -------------- -------------- 29 | | | | 30 | +-----------+-----------)-------------+ 31 | | | 32 | v v 33 | -------------- --------------- 34 | | Sniffer | | TLS key | 35 | | (Wireshark)|<----------| log | 36 | | | | file | 37 | -------------- --------------- 38 | ``` 39 | 40 | ## How 41 | 42 | This tool only logs encryption keys and does not record traffic. You need a sniffer. Wireshark has been tested, 43 | so instruction assumes it is used. 44 | 45 | 1. Generate and install root certificate for next step (`cert.pem` and `key.pem`) 46 | 2. Start proxy (`./mirror_proxy -c cert.pem -k key.pem -s ssl.log`) 47 | 3. [Configure TLS decryption](https://wiki.wireshark.org/TLS#using-the-pre-master-secret) in Wireshark using `ssl.log` 48 | 4. Start traffic capture 49 | 5. Configure SUT to use proxy 50 | 6. Start SUT and begin looking at packets 51 | 52 | Proxy can connect to target server through another proxy (`-p`, HTTP(S) and SOCKS5 are supported). 53 | Additionally, you can disable decryption completely (`-m passthrough`) - all connection data will be forwarded 54 | unaltered. 55 | 56 | ## What else 57 | 58 | Installation: 59 | ```shell 60 | go install github.com/fedosgad/mirror_proxy@latest 61 | ``` 62 | 63 | Manual build and run: 64 | ```shell 65 | git clone https://github.com/fedosgad/mirror_proxy 66 | cd mirror_proxy/ 67 | go build 68 | ``` 69 | then 70 | ```shell 71 | ./mirror_proxy -h 72 | ``` 73 | or (this automatically uses certificate and key from installed `mitmproxy`) 74 | ```shell 75 | ./mirror.sh -h 76 | ``` 77 | 78 | CLI usage: 79 | ``` 80 | $ ./mirror_proxy -h 81 | Usage: cmd [FLAG]... 82 | 83 | Flags: 84 | --verbose, -v Turn on verbose logging (type: bool; default: false) 85 | --listen, -l Address for proxy to listen on (type: string; default: :8080) 86 | --pprof Enable profiling server on http://{pprof}/debug/pprof/ (type: string) 87 | --mode, -m Operation mode (available: mitm, passthrough) (type: string; default: mitm) 88 | --dial-timeout, -dt Remote host dialing timeout (type: string; default: 5s) 89 | --proxy, -p Upstream proxy address (direct connection if empty) (type: string) 90 | --proxy-timeout, -pt Upstream proxy timeout (type: string; default: 5s) 91 | --mutual-tls-host, -mth Host where mutual TLS is enabled (type: string) 92 | --client-cert, -cc Path to file with client certificate (type: string) 93 | --client-key, -ck Path to file with client key (type: string) 94 | --certificate, -c Path to root CA certificate (type: string) 95 | --key, -k Path to root CA key (type: string) 96 | --sslkeylog, -s Path to SSL/TLS secrets log file (type: string; default: ssl.log) 97 | --insecure, -i Allow connecting to insecure remote hosts (type: bool; default: false) 98 | -h, --help show help (type: bool) 99 | ``` 100 | 101 | ## Similar projects 102 | - https://github.com/saucesteals/utlsproxy 103 | -------------------------------------------------------------------------------- /cert_generator/cert_generator.go: -------------------------------------------------------------------------------- 1 | package cert_generator 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "fmt" 10 | "net" 11 | "time" 12 | ) 13 | 14 | type CertificateGenerator struct { 15 | ca tls.Certificate 16 | caX509 *x509.Certificate 17 | } 18 | 19 | func NewCertGenerator(ca tls.Certificate) (*CertificateGenerator, error) { 20 | caX509, err := x509.ParseCertificate(ca.Certificate[0]) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return &CertificateGenerator{ca: ca, caX509: caX509}, nil 25 | } 26 | 27 | func NewCertGeneratorFromFiles(certFile, keyFile string) (*CertificateGenerator, error) { 28 | var certs []tls.Certificate 29 | if certFile == "" || keyFile == "" { 30 | return nil, fmt.Errorf("certificate and key files required") 31 | } 32 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 33 | if err != nil { 34 | return nil, err 35 | } 36 | certs = []tls.Certificate{cert} 37 | return NewCertGenerator(certs[0]) 38 | } 39 | 40 | func (cg *CertificateGenerator) GenChildCert(ips, names []string) (*tls.Certificate, error) { 41 | private, cab, err := cg.genCertBytes(ips, names) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &tls.Certificate{ 47 | Certificate: [][]byte{cab}, 48 | PrivateKey: private, 49 | }, nil 50 | } 51 | 52 | func (cg *CertificateGenerator) genCertBytes(ips []string, names []string) (*rsa.PrivateKey, []byte, error) { 53 | s, _ := rand.Prime(rand.Reader, 128) 54 | 55 | // Certificate validity period should be less than 13 month. 56 | // See https://stackoverflow.com/a/65239775 57 | // Thanks to Johnny Bravo for the tip! 58 | 59 | template := &x509.Certificate{ 60 | SerialNumber: s, 61 | Subject: pkix.Name{Organization: []string{"mitmproxy"}}, 62 | Issuer: pkix.Name{Organization: []string{"mitmproxy"}}, 63 | NotBefore: time.Now().AddDate(0, 0, -7), 64 | NotAfter: time.Now().AddDate(0, 0, 314), 65 | BasicConstraintsValid: true, 66 | IsCA: false, 67 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 68 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 69 | } 70 | if ips != nil { 71 | is := make([]net.IP, 0) 72 | for _, i := range ips { 73 | is = append(is, net.ParseIP(i)) 74 | } 75 | template.IPAddresses = is 76 | } 77 | if names != nil { 78 | template.DNSNames = names 79 | } 80 | 81 | private := cg.ca.PrivateKey.(*rsa.PrivateKey) 82 | 83 | certP, _ := x509.ParseCertificate(cg.ca.Certificate[0]) 84 | public := certP.PublicKey.(*rsa.PublicKey) 85 | 86 | cab, err := x509.CreateCertificate(rand.Reader, template, cg.caX509, public, private) 87 | if err != nil { 88 | return nil, nil, err 89 | } 90 | return private, cab, nil 91 | } 92 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fedosgad/mirror_proxy 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | github.com/cosiner/flag v0.5.2 9 | github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021 10 | github.com/fedosgad/go-http-dialer v0.0.0-20220817082317-794079273155 11 | github.com/refraction-networking/utls v1.6.7 12 | golang.org/x/net v0.23.0 13 | ) 14 | 15 | require ( 16 | github.com/andybalholm/brotli v1.0.6 // indirect 17 | github.com/cloudflare/circl v1.3.7 // indirect 18 | github.com/klauspost/compress v1.17.4 // indirect 19 | golang.org/x/crypto v0.21.0 // indirect 20 | golang.org/x/sys v0.18.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 2 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= 4 | github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 5 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 6 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 7 | github.com/cosiner/argv v0.0.1 h1:2iAFN+sWPktbZ4tvxm33Ei8VY66FPCxdOxpncUGpAXE= 8 | github.com/cosiner/argv v0.0.1/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ= 9 | github.com/cosiner/flag v0.5.2 h1:dcI3ExLwrYt/wgg1RXZBn7FFFn3Mi5Lyremoa7Tt5ts= 10 | github.com/cosiner/flag v0.5.2/go.mod h1:+zDQNSDNnkR7CGUlSrw2d/5S26bL91amx0FVUbnmLrU= 11 | github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021 h1:EbF0UihnxWRcIMOwoVtqnAylsqcjzqpSvMdjF2Ud4rA= 12 | github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 13 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= 14 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 15 | github.com/fedosgad/go-http-dialer v0.0.0-20220817082317-794079273155 h1:oLDdwWgc4jpeAUacVjYztKiKXraThk6ZbsXF1aOvPPM= 16 | github.com/fedosgad/go-http-dialer v0.0.0-20220817082317-794079273155/go.mod h1:ZX3YliCLM85weNOa44dHN0mtrZY/COlqiRmo9f0ZYbM= 17 | github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= 18 | github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= 19 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 20 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 21 | github.com/refraction-networking/utls v1.2.2 h1:uBE6V173CwG8MQrSBpNZHAix1fxOvuLKYyjFAu3uqo0= 22 | github.com/refraction-networking/utls v1.2.2/go.mod h1:L1goe44KvhnTfctUffM2isnJpSjPlYShrhXDeZaoYKw= 23 | github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= 24 | github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 25 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 26 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 27 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 28 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 29 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 30 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 31 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 32 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 33 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 34 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 35 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 37 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 38 | -------------------------------------------------------------------------------- /hijackers/config.go: -------------------------------------------------------------------------------- 1 | package hijackers 2 | 3 | import utls "github.com/refraction-networking/utls" 4 | 5 | type ClientTLSCredentials struct { 6 | Host string 7 | Cert utls.Certificate 8 | } 9 | -------------------------------------------------------------------------------- /hijackers/factory.go: -------------------------------------------------------------------------------- 1 | package hijackers 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | ) 7 | 8 | type HijackerFactory struct { 9 | dialer Dialer 10 | allowInsecure bool 11 | keyLogWriter io.Writer 12 | generateCertFunc func(ips []string, names []string) (*tls.Certificate, error) 13 | clientTLSCredentials *ClientTLSCredentials 14 | } 15 | 16 | func NewHijackerFactory( 17 | dialer Dialer, 18 | allowInsecure bool, 19 | keyLogWriter io.Writer, 20 | generateCertFunc func(ips []string, names []string) (*tls.Certificate, error), 21 | clientTLSCredentials *ClientTLSCredentials, 22 | ) *HijackerFactory { 23 | return &HijackerFactory{ 24 | dialer: dialer, 25 | allowInsecure: allowInsecure, 26 | keyLogWriter: keyLogWriter, 27 | generateCertFunc: generateCertFunc, 28 | clientTLSCredentials: clientTLSCredentials, 29 | } 30 | } 31 | 32 | func (hf *HijackerFactory) Get(mode string) Hijacker { 33 | switch mode { 34 | case ModePassthrough: 35 | return NewPassThroughHijacker(hf.dialer) 36 | case ModeMITM: 37 | return NewUTLSHijacker( 38 | hf.dialer, 39 | hf.allowInsecure, 40 | hf.keyLogWriter, 41 | hf.generateCertFunc, 42 | hf.clientTLSCredentials, 43 | ) 44 | default: 45 | return nil 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /hijackers/interface.go: -------------------------------------------------------------------------------- 1 | package hijackers 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | ) 7 | 8 | // Hijacker operation modes 9 | const ( 10 | ModePassthrough = "passthrough" 11 | ModeMITM = "mitm" 12 | ) 13 | 14 | // Hijacker is an entity of connection interceptor. 15 | type Hijacker interface { 16 | // GetConns creates server connection and optionally wraps clientRaw into client. 17 | // Returned streams are meant to be connected to each other. 18 | // Implementation MUST answer to client "HTTP/1.1 200 OK\r\n\r\n" 19 | GetConns(url *url.URL, clientRaw net.Conn, ctxLogger Logger) (client, server net.Conn, err error) 20 | } 21 | 22 | type Logger interface { 23 | Logf(msg string, argv ...interface{}) 24 | Warnf(msg string, argv ...interface{}) 25 | } 26 | 27 | type Dialer interface { 28 | Dial(network string, addr string) (c net.Conn, err error) 29 | } 30 | -------------------------------------------------------------------------------- /hijackers/passthrough.go: -------------------------------------------------------------------------------- 1 | package hijackers 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | ) 7 | 8 | type passThroughHijacker struct { 9 | dialer Dialer 10 | } 11 | 12 | func NewPassThroughHijacker(dialer Dialer) Hijacker { 13 | return &passThroughHijacker{ 14 | dialer: dialer, 15 | } 16 | } 17 | 18 | func (h *passThroughHijacker) GetConns(url *url.URL, clientRaw net.Conn, _ Logger) (net.Conn, net.Conn, error) { 19 | remoteConn, err := h.dialer.Dial("tcp", url.Host) 20 | if err != nil { 21 | return nil, nil, err 22 | } 23 | _, err = clientRaw.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) 24 | return clientRaw, remoteConn, err 25 | } 26 | -------------------------------------------------------------------------------- /hijackers/utls.go: -------------------------------------------------------------------------------- 1 | package hijackers 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/fedosgad/mirror_proxy/utils" 7 | utls "github.com/refraction-networking/utls" 8 | "io" 9 | "net" 10 | "net/url" 11 | ) 12 | 13 | type utlsHijacker struct { 14 | dialer Dialer 15 | allowInsecure bool 16 | clientTLSConfig *tls.Config 17 | remoteUTLSConfig *utls.Config 18 | generateCertFunc func(ips []string, names []string) (*tls.Certificate, error) 19 | clientTLSCredentials *ClientTLSCredentials 20 | } 21 | 22 | func NewUTLSHijacker( 23 | dialer Dialer, 24 | allowInsecure bool, 25 | keyLogWriter io.Writer, 26 | generateCertFunc func(ips []string, names []string) (*tls.Certificate, error), 27 | clientTLSCredentials *ClientTLSCredentials, 28 | ) Hijacker { 29 | return &utlsHijacker{ 30 | dialer: dialer, 31 | allowInsecure: allowInsecure, 32 | clientTLSConfig: &tls.Config{ 33 | KeyLogWriter: keyLogWriter, 34 | }, 35 | remoteUTLSConfig: &utls.Config{ 36 | KeyLogWriter: keyLogWriter, 37 | }, 38 | generateCertFunc: generateCertFunc, 39 | clientTLSCredentials: clientTLSCredentials, 40 | } 41 | } 42 | 43 | func (h *utlsHijacker) GetConns(target *url.URL, clientRaw net.Conn, ctxLogger Logger) (net.Conn, net.Conn, error) { 44 | var remoteConn net.Conn 45 | 46 | clientConnOrig, clientConnCopy := utils.NewTeeConn(clientRaw) 47 | 48 | f := clientHelloFingerprinter{ 49 | conn: clientConnCopy, 50 | fpCh: make(chan *fpResult, 1), 51 | errCh: make(chan error, 1), 52 | log: ctxLogger, 53 | } 54 | clientConfigTemplate := h.clientTLSConfig.Clone() 55 | clientConfigTemplate.GetConfigForClient = h.clientHelloCallback(target, clientConfigTemplate, &remoteConn, f, ctxLogger) 56 | plaintextConn := tls.Server(clientConnOrig, clientConfigTemplate) 57 | _, err := clientConnOrig.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | go f.extractALPN() 63 | 64 | return plaintextConn, remoteConn, plaintextConn.Handshake() // Return connections so they can be closed 65 | } 66 | 67 | // clientHelloCallback performs the following tasks: 68 | // 69 | // - get client ALPN offers 70 | // 71 | // - connect to and perform handshake with target server 72 | // 73 | // - set correct ALPN for client connection using server response 74 | // 75 | // - generate certificate for client (according to client's SNI) 76 | func (h *utlsHijacker) clientHelloCallback( 77 | target *url.URL, 78 | clientConfigTemplate *tls.Config, 79 | remoteConnRes *net.Conn, 80 | chf clientHelloFingerprinter, 81 | ctxLog Logger, 82 | ) func(*tls.ClientHelloInfo) (*tls.Config, error) { 83 | return func(info *tls.ClientHelloInfo) (*tls.Config, error) { 84 | ctxLog.Logf("Handshake callback") 85 | var hostname string 86 | if net.ParseIP(target.Hostname()) == nil { 87 | hostname = target.Hostname() 88 | } 89 | sni := info.ServerName 90 | remotePlaintextConn, err := h.dialer.Dial("tcp", target.Host) 91 | if err != nil { 92 | return nil, err 93 | } 94 | ctxLog.Logf("Remote conn established") 95 | needClose := true 96 | defer func() { 97 | if needClose { 98 | ctxLog.Logf("Closing remotePlaintextConn") 99 | remotePlaintextConn.Close() 100 | } 101 | }() 102 | remoteConfig := h.remoteUTLSConfig.Clone() 103 | switch { 104 | case sni != "": 105 | remoteConfig.ServerName = sni 106 | case hostname != "": 107 | remoteConfig.ServerName = hostname 108 | default: 109 | if !h.allowInsecure { 110 | return nil, fmt.Errorf("no SNI or name provided and InsecureSkipVerify == false") 111 | } 112 | remoteConfig.InsecureSkipVerify = true 113 | } 114 | 115 | if h.clientTLSCredentials != nil && remoteConfig.ServerName == h.clientTLSCredentials.Host { 116 | remoteConfig.ClientAuth = utls.RequireAndVerifyClientCert 117 | remoteConfig.GetClientCertificate = func(cri *utls.CertificateRequestInfo) (*utls.Certificate, error) { 118 | return &h.clientTLSCredentials.Cert, nil 119 | } 120 | } 121 | 122 | var fpRes *fpResult 123 | 124 | ctxLog.Logf("Wait for extractALPN") 125 | select { 126 | case err := <-chf.error(): 127 | return nil, fmt.Errorf("error extracting ALPN: %v", err) 128 | case fpRes = <-chf.result(): 129 | break 130 | } 131 | ctxLog.Logf("Done extractALPN") 132 | 133 | remoteConn := utls.UClient(remotePlaintextConn, remoteConfig, utls.HelloCustom) 134 | *remoteConnRes = remoteConn // Pass connection back 135 | spec := fpRes.helloSpec 136 | if spec == nil { 137 | return nil, fmt.Errorf("empty fingerprinted spec") 138 | } 139 | if err := remoteConn.ApplyPreset(spec); err != nil { 140 | return nil, err 141 | } 142 | if sni != "" { 143 | remoteConn.SetSNI(sni) 144 | } 145 | ctxLog.Logf("Remote handshake") 146 | 147 | err = remoteConn.Handshake() 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | clientConfig := clientConfigTemplate.Clone() 153 | clientConfig.GetConfigForClient = nil 154 | 155 | cs := remoteConn.ConnectionState() 156 | alpnRes := cs.NegotiatedProtocol 157 | if alpnRes != "" { 158 | // Hot-swap ALPN response for client 159 | clientConfig.NextProtos = []string{alpnRes} 160 | } 161 | 162 | ctxLog.Logf("Certificate generation") 163 | 164 | cert, err := generateCert(info, target.Hostname(), h.generateCertFunc) 165 | if err != nil { 166 | return nil, err 167 | } 168 | clientConfig.Certificates = []tls.Certificate{*cert} 169 | 170 | needClose = false 171 | return clientConfig, nil 172 | } 173 | } 174 | 175 | func generateCert( 176 | info *tls.ClientHelloInfo, 177 | target string, 178 | generateCertFunc func(ips []string, names []string) (*tls.Certificate, error), 179 | ) (*tls.Certificate, error) { 180 | sni := info.ServerName 181 | if sni != "" { 182 | return generateCertFunc(nil, []string{sni}) 183 | } 184 | var ip, name []string 185 | if net.ParseIP(target) == nil { 186 | name = []string{target} 187 | } else { 188 | ip = []string{target} 189 | } 190 | return generateCertFunc(ip, name) 191 | } 192 | 193 | // clientHelloFingerprinter holds variables related to client TLS fingerprinting 194 | type clientHelloFingerprinter struct { 195 | conn io.Reader 196 | fpCh chan *fpResult 197 | errCh chan error 198 | log Logger 199 | } 200 | 201 | // fpResult is a container struct for client`s clientHello fingerprinting results 202 | type fpResult struct { 203 | helloSpec *utls.ClientHelloSpec 204 | nextProtos []string 205 | } 206 | 207 | func (f clientHelloFingerprinter) result() chan *fpResult { 208 | return f.fpCh 209 | } 210 | 211 | func (f clientHelloFingerprinter) error() chan error { 212 | return f.errCh 213 | } 214 | 215 | func (f clientHelloFingerprinter) extractALPN() { 216 | tlsHeader := make([]byte, 5) 217 | n, err := io.ReadAtLeast(f.conn, tlsHeader, 5) 218 | if err != nil { 219 | if err == io.EOF { 220 | f.errCh <- fmt.Errorf("TLS header: unexpected EOF at byte %d", n) 221 | return 222 | } 223 | f.errCh <- fmt.Errorf("TLS header: read error: %v", err) 224 | return 225 | } 226 | if tlsHeader[0] != 0x16 { 227 | f.errCh <- fmt.Errorf("TLS header: incorrect header: %v", tlsHeader) 228 | return 229 | } 230 | f.log.Logf("TLS header bytes: %v", tlsHeader) 231 | clientHelloLength := uint16(tlsHeader[3])<<8 + uint16(tlsHeader[4]) 232 | f.log.Logf("ClientHello length: %d", clientHelloLength) 233 | clientHelloBody := make([]byte, clientHelloLength) 234 | n, err = io.ReadAtLeast(f.conn, clientHelloBody, int(clientHelloLength)) 235 | if err != nil { 236 | if err == io.EOF { 237 | f.errCh <- fmt.Errorf("TLS body: unexpected EOF at byte %d", n) 238 | return 239 | } 240 | f.errCh <- fmt.Errorf("TLS body: read error: %v", err) 241 | return 242 | } 243 | clientHello := utls.UnmarshalClientHello(clientHelloBody) 244 | if clientHello == nil { 245 | f.errCh <- fmt.Errorf("failed to unmarshal clientHello") 246 | return 247 | } 248 | nextProtos := clientHello.AlpnProtocols 249 | f.log.Logf("Client ALPN offers: %v", nextProtos) 250 | 251 | fp := utls.Fingerprinter{ 252 | AllowBluntMimicry: true, 253 | AlwaysAddPadding: false, 254 | } 255 | clientHelloSpec, err := fp.FingerprintClientHello(append(tlsHeader, clientHelloBody...)) 256 | if err != nil { 257 | f.log.Logf("Client hello fingerprinting error %v", err) 258 | f.errCh <- err 259 | return 260 | } 261 | f.log.Logf("Sending fpRes") 262 | f.fpCh <- &fpResult{ 263 | helloSpec: clientHelloSpec, 264 | nextProtos: nextProtos, 265 | } 266 | 267 | f.log.Logf("Start sinking ALPN copy") 268 | _, err = io.Copy(io.Discard, f.conn) // Sink remaining data - we don't need them 269 | if err != nil && !utils.IsClosedConnErr(err) { 270 | f.log.Warnf("Sinking failed, error: %v", err) 271 | f.errCh <- err 272 | } 273 | 274 | return 275 | } 276 | -------------------------------------------------------------------------------- /mirror.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | go run . -c ~/.mitmproxy/mitmproxy-ca-cert.pem -k ~/.mitmproxy/mitmproxy-ca.pem "$@" 3 | -------------------------------------------------------------------------------- /mirror_proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/elazarl/goproxy" 6 | http_dialer "github.com/fedosgad/go-http-dialer" 7 | "github.com/fedosgad/mirror_proxy/cert_generator" 8 | "github.com/fedosgad/mirror_proxy/hijackers" 9 | utls "github.com/refraction-networking/utls" 10 | "golang.org/x/net/proxy" 11 | "io" 12 | "log" 13 | "net" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "regexp" 18 | ) 19 | 20 | func main() { 21 | opts := getOptions() 22 | 23 | klw, err := getSSLLogWriter(opts) 24 | if err != nil { 25 | log.Fatalf("Error opening key log file: %v", err) 26 | } 27 | defer klw.Close() 28 | 29 | cg, err := cert_generator.NewCertGeneratorFromFiles(opts.CertFile, opts.KeyFile) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | dialer, err := getDialer(opts) 35 | if err != nil { 36 | log.Fatalf("Error getting proxy dialer: %v", err) 37 | } 38 | 39 | clientTLSCredentials, err := getClientTLSCredentials(opts) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | hjf := hijackers.NewHijackerFactory( 45 | dialer, 46 | opts.AllowInsecure, 47 | klw, 48 | cg.GenChildCert, 49 | clientTLSCredentials, 50 | ) 51 | hj := hjf.Get(opts.Mode) 52 | 53 | p := goproxy.NewProxyHttpServer() 54 | // Handle all CONNECT requests 55 | p.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))). 56 | HandleConnect(goproxy.FuncHttpsHandler( 57 | func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { 58 | return &goproxy.ConnectAction{ 59 | Action: goproxy.ConnectHijack, 60 | Hijack: getTLSHijackFunc(hj), 61 | }, host 62 | })) 63 | p.Verbose = opts.Verbose 64 | 65 | if opts.PprofAddress != "" { 66 | go func() { 67 | log.Println(http.ListenAndServe(opts.PprofAddress, nil)) 68 | }() 69 | } 70 | log.Fatal(http.ListenAndServe(opts.ListenAddress, p)) 71 | } 72 | 73 | type writeNopCloser struct { 74 | io.Writer 75 | } 76 | 77 | func (c writeNopCloser) Close() error { 78 | return nil 79 | } 80 | 81 | func getSSLLogWriter(opts *Options) (klw io.WriteCloser, err error) { 82 | klw = writeNopCloser{Writer: io.Discard} 83 | 84 | if opts.SSLLogFile != "" { 85 | klw, err = os.OpenFile(opts.SSLLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 86 | } 87 | return klw, err 88 | } 89 | 90 | func getDialer(opts *Options) (proxy.Dialer, error) { 91 | // Timeout SHOULD be set. Otherwise, dialing will never succeed if the first address 92 | // returned by resolver is not responding (connection will just hang forever). 93 | d := &net.Dialer{ 94 | Timeout: opts.DialTimeout, 95 | } 96 | if opts.ProxyAddr == "" { 97 | return d, nil 98 | } 99 | proxyURL, err := url.Parse(opts.ProxyAddr) 100 | if err != nil { 101 | return nil, err 102 | } 103 | if proxyURL.Scheme == "socks5" { 104 | return proxy.FromURL(proxyURL, d) 105 | } 106 | if proxyURL.Scheme == "http" || proxyURL.Scheme == "https" { 107 | if proxyURL.User != nil { 108 | pass, _ := proxyURL.User.Password() 109 | username := proxyURL.User.Username() 110 | return http_dialer.New( 111 | proxyURL, 112 | http_dialer.WithProxyAuth(http_dialer.AuthBasic(username, pass)), 113 | http_dialer.WithConnectionTimeout(opts.ProxyTimeout), 114 | http_dialer.WithContextDialer(d), 115 | ), nil 116 | } 117 | return http_dialer.New( 118 | proxyURL, 119 | http_dialer.WithConnectionTimeout(opts.ProxyTimeout), 120 | http_dialer.WithContextDialer(d), 121 | ), nil 122 | } 123 | 124 | return nil, fmt.Errorf("cannot use proxy scheme %q", proxyURL.Scheme) 125 | } 126 | 127 | func getClientTLSCredentials(opts *Options) (*hijackers.ClientTLSCredentials, error) { 128 | if opts.HostWithMutualTLS == "" { 129 | return nil, nil 130 | } 131 | 132 | cert, err := os.ReadFile(opts.ClientCertFile) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | key, err := os.ReadFile(opts.ClientKeyFile) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | certificate, err := utls.X509KeyPair(cert, key) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | clientTLSCredentials := &hijackers.ClientTLSCredentials{ 148 | Host: opts.HostWithMutualTLS, 149 | Cert: certificate, 150 | } 151 | return clientTLSCredentials, nil 152 | } 153 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cosiner/flag" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type Options struct { 10 | Verbose bool `names:"--verbose, -v" usage:"Turn on verbose logging" default:"false"` 11 | ListenAddress string `names:"--listen, -l" usage:"Address for proxy to listen on" default:":8080"` 12 | PprofAddress string `names:"--pprof" usage:"Enable profiling server on http://{pprof}/debug/pprof/" default:""` 13 | 14 | Mode string `names:"--mode, -m" usage:"Operation mode (available: mitm, passthrough)" default:"mitm"` 15 | 16 | DialTimeout time.Duration `names:"-"` 17 | DialTimeoutArg string `names:"--dial-timeout, -dt" usage:"Remote host dialing timeout" default:"5s"` 18 | ProxyAddr string `names:"--proxy, -p" usage:"Upstream proxy address (direct connection if empty)" default:""` 19 | ProxyTimeout time.Duration `names:"-"` 20 | ProxyTimeoutArg string `names:"--proxy-timeout, -pt" usage:"Upstream proxy timeout" default:"5s"` 21 | HostWithMutualTLS string `names:"--mutual-tls-host, -mth" usage:"Host where mutual TLS is enabled"` 22 | ClientCertFile string `names:"--client-cert, -cc" usage:"Path to file with client certificate"` 23 | ClientKeyFile string `names:"--client-key, -ck" usage:"Path to file with client key"` 24 | CertFile string `names:"--certificate, -c" usage:"Path to root CA certificate" default:""` 25 | KeyFile string `names:"--key, -k" usage:"Path to root CA key" default:""` 26 | SSLLogFile string `names:"--sslkeylog, -s" usage:"Path to SSL/TLS secrets log file" default:"ssl.log"` 27 | AllowInsecure bool `names:"--insecure, -i" usage:"Allow connecting to insecure remote hosts" default:"false"` 28 | } 29 | 30 | func getOptions() *Options { 31 | opts := &Options{} 32 | err := flag.Commandline.ParseStruct(opts) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | parseDuration(opts.DialTimeoutArg, &opts.DialTimeout) 37 | parseDuration(opts.ProxyTimeoutArg, &opts.ProxyTimeout) 38 | opts.check() 39 | return opts 40 | } 41 | 42 | func (o Options) check() { 43 | failIfEmpty := func(val, err string) { 44 | if val == "" { 45 | log.Fatal(err) 46 | } 47 | } 48 | 49 | failIfEmpty(o.ListenAddress, "Please provide listen address") 50 | if o.Mode != "mitm" && o.Mode != "passthrough" { 51 | log.Fatal() 52 | } 53 | if o.Mode != "mitm" { 54 | return 55 | } 56 | // TLS-related options 57 | failIfEmpty(o.CertFile, "Please provide certificate file") 58 | failIfEmpty(o.KeyFile, "Please provide key file") 59 | failIfEmpty(o.SSLLogFile, "Please provide key log file") 60 | 61 | if o.DialTimeout == 0 { 62 | log.Println("Warning: timeout=0, connections may hang!") 63 | } 64 | 65 | // mutual TLS related options 66 | if o.HostWithMutualTLS != "" { 67 | failIfEmpty(o.ClientCertFile, "Please provide client certificate file") 68 | failIfEmpty(o.ClientKeyFile, "Please provide client key file") 69 | } 70 | } 71 | 72 | func parseDuration(inp string, res *time.Duration) { 73 | d, err := time.ParseDuration(inp) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | *res = d 78 | } 79 | -------------------------------------------------------------------------------- /tls_hijacker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/elazarl/goproxy" 5 | "github.com/fedosgad/mirror_proxy/hijackers" 6 | "github.com/fedosgad/mirror_proxy/utils" 7 | "io" 8 | "net" 9 | "net/http" 10 | "sync" 11 | ) 12 | 13 | func getTLSHijackFunc(hj hijackers.Hijacker) func(*http.Request, net.Conn, *goproxy.ProxyCtx) { 14 | return func(req *http.Request, connL net.Conn, ctx *goproxy.ProxyCtx) { 15 | var err error 16 | var tlsConnR net.Conn 17 | var closer sync.Once 18 | 19 | closeFunc := func() { 20 | ctx.Logf("Connections closed") 21 | _ = connL.Close() 22 | _ = tlsConnR.Close() 23 | } 24 | 25 | tlsConnL, tlsConnR, err := hj.GetConns(req.URL, connL, ctx) 26 | if err != nil { 27 | ctx.Warnf("Couldn't connect: %v", err) 28 | return 29 | } 30 | 31 | ctx.Logf("Connected to server: %s\n", tlsConnR.RemoteAddr()) 32 | 33 | go handleServerTLSConn(tlsConnR, tlsConnL, &closer, ctx) 34 | 35 | _, err = io.Copy(tlsConnR, tlsConnL) 36 | if err != nil && !utils.IsClosedConnErr(err) { 37 | ctx.Logf("bad io.Copy [handleConnection]: %v", err) 38 | } 39 | 40 | closer.Do(closeFunc) 41 | } 42 | } 43 | 44 | func handleServerTLSConn(connR, connL net.Conn, closer *sync.Once, ctx *goproxy.ProxyCtx) { 45 | closeFunc := func() { 46 | ctx.Logf("Connections closed.") 47 | _ = connL.Close() 48 | _ = connR.Close() 49 | } 50 | 51 | _, err := io.Copy(connL, connR) 52 | 53 | // check if error is about the closed connection 54 | // this is expected in most cases, so don't make a noise about it 55 | if err != nil && !utils.IsClosedConnErr(err) { 56 | ctx.Warnf("bad io.Copy [handleServerMessage]: %v", err) 57 | } 58 | 59 | closer.Do(closeFunc) 60 | } 61 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net" 4 | 5 | func IsClosedConnErr(err error) bool { 6 | netOpError, ok := err.(*net.OpError) 7 | return ok && netOpError.Err.Error() == "use of closed network connection" 8 | } 9 | -------------------------------------------------------------------------------- /utils/tee_conn.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | ) 8 | 9 | type TeeConn struct { 10 | net.Conn 11 | pipeR *io.PipeReader 12 | pipeW *io.PipeWriter 13 | teeOut io.Reader 14 | } 15 | 16 | func NewTeeConn(conn net.Conn) (net.Conn, io.Reader) { 17 | pipeR, pipeW := io.Pipe() 18 | teeOut := io.TeeReader(conn, pipeW) 19 | return &TeeConn{ 20 | Conn: conn, 21 | pipeR: pipeR, 22 | pipeW: pipeW, 23 | teeOut: teeOut, 24 | }, teeOut 25 | } 26 | 27 | func (tc TeeConn) Read(p []byte) (n int, err error) { 28 | return tc.pipeR.Read(p) 29 | } 30 | 31 | func (tc TeeConn) Close() error { 32 | errConn := tc.Conn.Close() 33 | errPipeR := tc.pipeR.Close() 34 | errPipeW := tc.pipeW.Close() 35 | if errConn != nil || errPipeR != nil || errPipeW != nil { 36 | return fmt.Errorf("error(s) closing TeeConn: conn %v, pipeR %v, pipeW %v", errConn, errPipeR, errPipeW) 37 | } 38 | return nil 39 | } 40 | --------------------------------------------------------------------------------