├── .dockerignore ├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cert.go ├── compose.yaml ├── config.go ├── go.mod ├── go.sum ├── headers.go ├── http.go └── proxy.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | tlsproxy 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Go workspace file 17 | go.work 18 | 19 | # Keys and certificates 20 | *.pem 21 | 22 | .github 23 | credentials 24 | .dockerignore 25 | .gitignore 26 | Dockerfile 27 | compose.yaml 28 | README.md 29 | LICENSE -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | go-version: [1.19.x] 14 | os: [ubuntu-latest, windows-latest] 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | - name: Get dependencies 23 | run: go mod download 24 | - name: Build 25 | run: go build -v ./... 26 | - name: Test 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [created] 5 | permissions: 6 | contents: write 7 | packages: write 8 | jobs: 9 | releases-matrix: 10 | name: Release tlsproxy binary 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | # build and publish in parallel: linux/amd64, linux/arm64, 15 | # windows/amd64, windows/arm64, darwin/amd64, darwin/arm64 16 | goos: [linux, windows, darwin] 17 | goarch: [amd64, arm64] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: wangyoucao577/go-release-action@v1 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | goos: ${{ matrix.goos }} 24 | goarch: ${{ matrix.goarch }} 25 | binary_name: "tlsproxy" 26 | docker-image: 27 | name: Build and push Docker image to GHCR 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: Build and Push Container to ghcr.io 32 | uses: mr-smithers-excellent/docker-build-push@v6 33 | with: 34 | image: tlsproxy 35 | registry: ghcr.io 36 | multiplatform: true 37 | platform: linux/amd64,linux/arm64,linux/arm/v7 38 | addLatest: true 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | tlsproxy 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | 25 | *.pem 26 | credentials -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.5 as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | RUN go build -o /app/tlsproxy 10 | 11 | FROM gcr.io/distroless/cc-debian12 12 | 13 | COPY --from=builder /app/tlsproxy /app/tlsproxy 14 | ENTRYPOINT ["/app/tlsproxy"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 rosahaj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tlsproxy 2 | 3 | HTTP proxy with per-request uTLS fingerprint mimicry and upstream proxy tunneling. Currently WIP. 4 | 5 | Built on top of [uTLS](https://github.com/refraction-networking/utls) and [goproxy](https://github.com/elazarl/goproxy/). Inspired by [ja3proxy](https://github.com/LyleMi/ja3proxy). 6 | 7 | ## Usage 8 | 9 | ### Building from source 10 | 11 | ```bash 12 | git clone https://github.com/rosahaj/tlsproxy 13 | cd ja3proxy 14 | go build 15 | 16 | # Start proxy 17 | ./tlsproxy -client Chrome-120 18 | # Make requests 19 | curl --cacert cert.pem --proxy http://localhost:8080 https://www.example.com 20 | ``` 21 | 22 | Pre-built binaries are available in the [Releases](https://github.com/rosahaj/tlsproxy/releases) section. 23 | 24 | ### Using docker CLI 25 | 26 | ```bash 27 | docker run \ 28 | -v ./credentials:/app/credentials \ 29 | -p 8080:8080 \ 30 | ghcr.io/rosahaj/tlsproxy:latest \ 31 | -cert /app/credentials/cert.pem \ 32 | -key /app/credentials/key.pem \ 33 | -client Chrome-120 34 | ``` 35 | 36 | ### Using docker compose 37 | 38 | See [`compose.yaml`](https://github.com/rosahaj/tlsproxy/blob/master/compose.yaml) 39 | 40 | ```bash 41 | docker compose up -d 42 | ``` 43 | 44 | ### CLI usage 45 | 46 | ``` 47 | Usage of ./tlsproxy: 48 | -addr string 49 | Proxy listen address 50 | -cert string 51 | TLS CA certificate (generated automatically if not present) (default "cert.pem") 52 | -client string 53 | Default utls clientHelloID (can be overriden through x-tlsproxy-client header) (default "Chrome-120") 54 | -key string 55 | TLS CA key (generated automatically if not present) (default "key.pem") 56 | -port string 57 | Proxy listen port (default "8080") 58 | -upstream string 59 | Default upstream proxy prefixed by "socks5://" (can be overriden through x-tlsproxy-upstream header) 60 | -verbose 61 | Enable verbose logging 62 | ``` -------------------------------------------------------------------------------- /cert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "log" 7 | "os" 8 | 9 | "github.com/elazarl/goproxy" 10 | 11 | cfsr "github.com/cloudflare/cfssl/csr" 12 | "github.com/cloudflare/cfssl/initca" 13 | ) 14 | 15 | func fileExists(filename string) bool { 16 | _, err := os.Stat(filename) 17 | return !os.IsNotExist(err) 18 | } 19 | 20 | func setGoproxyCA(tlsCert tls.Certificate) { 21 | var err error 22 | if tlsCert.Leaf, err = x509.ParseCertificate(tlsCert.Certificate[0]); err != nil { 23 | log.Fatal("Unable to parse ca", err) 24 | } 25 | 26 | goproxy.GoproxyCa = tlsCert 27 | goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 28 | goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 29 | goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 30 | goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&tlsCert)} 31 | } 32 | 33 | func loadCA() { 34 | if fileExists(Flags.cert) && fileExists(Flags.key) { 35 | tlsCert, err := tls.LoadX509KeyPair(Flags.cert, Flags.key) 36 | if err != nil { 37 | log.Fatal("Unable to load CA certificate and key", err) 38 | } 39 | 40 | setGoproxyCA(tlsCert) 41 | } else { 42 | if fileExists(Flags.cert) { 43 | log.Fatalf("CA certificate exists, but found no corresponding key at %s", Flags.key) 44 | } else if fileExists(Flags.key) { 45 | log.Fatalf("CA key exists, but found no corresponding certificate at %s", Flags.cert) 46 | } 47 | 48 | log.Println("No CA found, generating certificate and key") 49 | tlsCert, err := generateCA() 50 | if err != nil { 51 | log.Fatal("Unable to generate CA certificate and key", err) 52 | } 53 | 54 | setGoproxyCA(tlsCert) 55 | } 56 | } 57 | 58 | func generateCA() (tls.Certificate, error) { 59 | csr := cfsr.CertificateRequest{ 60 | CN: "tlsproxy CA", 61 | KeyRequest: cfsr.NewKeyRequest(), 62 | } 63 | 64 | certPEM, _, keyPEM, err := initca.New(&csr) 65 | if err != nil { 66 | return tls.Certificate{}, err 67 | } 68 | 69 | caOut, err := os.Create(Flags.cert) 70 | if err != nil { 71 | return tls.Certificate{}, err 72 | } 73 | defer caOut.Close() 74 | _, err = caOut.Write(certPEM) 75 | if err != nil { 76 | return tls.Certificate{}, err 77 | } 78 | 79 | keyOut, err := os.OpenFile(Flags.key, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 80 | if err != nil { 81 | return tls.Certificate{}, err 82 | } 83 | defer keyOut.Close() 84 | 85 | _, err = keyOut.Write(keyPEM) 86 | if err != nil { 87 | return tls.Certificate{}, err 88 | } 89 | 90 | return tls.X509KeyPair(certPEM, keyPEM) 91 | } 92 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | tlsproxy: 3 | image: ghcr.io/rosahaj/tlsproxy:latest 4 | build: . 5 | entrypoint: 6 | [ 7 | "/app/tlsproxy", 8 | "-cert", 9 | "/app/credentials/cert.pem", 10 | "-key", 11 | "/app/credentials/key.pem", 12 | "-client", 13 | "Chrome-120" 14 | ] 15 | ports: 16 | - 8080:8080 17 | volumes: 18 | - ./credentials:/app/credentials 19 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/url" 6 | "strings" 7 | 8 | utls "github.com/refraction-networking/utls" 9 | ) 10 | 11 | type CLIFlags struct { 12 | addr string 13 | port string 14 | cert string 15 | key string 16 | upstreamProxy string 17 | client string 18 | verbose bool 19 | } 20 | 21 | var ( 22 | Flags CLIFlags 23 | DefaultClientHelloID utls.ClientHelloID 24 | DefaultUpstreamProxy *url.URL 25 | ) 26 | 27 | func getClientHelloID(client string) (utls.ClientHelloID, bool) { 28 | clientArr := strings.Split(client, "-") 29 | if len(clientArr) != 2 { 30 | return utls.ClientHelloID{}, false 31 | } 32 | 33 | return utls.ClientHelloID{ 34 | Client: clientArr[0], 35 | Version: clientArr[1], 36 | Seed: nil, 37 | Weights: nil, 38 | }, true 39 | } 40 | 41 | func setDefaultClientHelloID(client string) { 42 | clientHelloId, ok := getClientHelloID(client) 43 | if !ok { 44 | log.Fatalf("Invalid client format: %s", client) 45 | } 46 | 47 | DefaultClientHelloID = clientHelloId 48 | } 49 | 50 | func setDefaultUpstreamProxy(upstreamProxy string) { 51 | proxyUrl, err := url.Parse(upstreamProxy) 52 | if err != nil { 53 | log.Fatalf("Invalid upstream proxy: %s", upstreamProxy) 54 | } 55 | 56 | DefaultUpstreamProxy = proxyUrl 57 | } 58 | 59 | func loadDefaultProxyConfig() { 60 | setDefaultClientHelloID(Flags.client) 61 | 62 | if len(Flags.upstreamProxy) > 0 { 63 | setDefaultUpstreamProxy(Flags.upstreamProxy) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module tlsproxy 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/cloudflare/cfssl v1.6.4 7 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 8 | github.com/refraction-networking/utls v1.6.1 9 | gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 v2.8.1 10 | ) 11 | 12 | require ( 13 | github.com/andybalholm/brotli v1.1.0 // indirect 14 | github.com/cloudflare/circl v1.3.7 // indirect 15 | github.com/go-logr/logr v1.3.0 // indirect 16 | github.com/google/certificate-transparency-go v1.1.4 // indirect 17 | github.com/google/go-cmp v0.6.0 // indirect 18 | github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect 19 | github.com/jmoiron/sqlx v1.3.3 // indirect 20 | github.com/klauspost/compress v1.17.4 // indirect 21 | github.com/onsi/ginkgo/v2 v2.14.0 // indirect 22 | github.com/onsi/gomega v1.30.0 // indirect 23 | github.com/quic-go/quic-go v0.40.1 // indirect 24 | github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b // indirect 25 | github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc // indirect 26 | github.com/zmap/zlint/v3 v3.1.0 // indirect 27 | golang.org/x/crypto v0.18.0 // indirect 28 | golang.org/x/net v0.20.0 // indirect 29 | golang.org/x/sys v0.16.0 // indirect 30 | golang.org/x/text v0.14.0 // indirect 31 | golang.org/x/tools v0.17.0 // indirect 32 | google.golang.org/protobuf v1.31.0 // indirect 33 | k8s.io/klog/v2 v2.80.1 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 3 | github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= 4 | github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= 11 | github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 12 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= 13 | github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= 14 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 15 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 16 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 18 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 19 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 20 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 21 | github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= 22 | github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= 23 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 25 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 26 | github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8= 27 | github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 28 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 29 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 30 | github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= 31 | github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= 32 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 33 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 34 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 35 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 36 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 38 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 41 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 42 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 43 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 44 | github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 45 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 46 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 47 | github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= 48 | github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= 49 | github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= 50 | github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 51 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q= 55 | github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= 56 | github.com/refraction-networking/utls v1.6.1 h1:n1JG5karzdGWsI6iZmGrOv3SNzR4c+4M8J6KWGsk3lA= 57 | github.com/refraction-networking/utls v1.6.1/go.mod h1:+EbcQOvQvXoFV9AEKbuGlljt1doLRKAVY1jJHe9EtDo= 58 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 59 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 60 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 61 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= 62 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= 63 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= 64 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= 65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 68 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 69 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 70 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 71 | github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= 72 | github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss= 73 | github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= 74 | github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= 75 | github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= 76 | github.com/zmap/zcrypto v0.0.0-20210123152837-9cf5beac6d91/go.mod h1:R/deQh6+tSWlgI9tb4jNmXxn8nSCabl5ZQsBX9//I/E= 77 | github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw= 78 | github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk= 79 | github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0= 80 | github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU= 81 | gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 v2.8.1 h1:5EYsxocnAzdkBOBpzaTTQDcOHYDCXnqG+cf0Qrd16WU= 82 | gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2 v2.8.1/go.mod h1:rLhhb91K4IOAlFZxMGkO3ltbOCBKnZX4RAsseockQhY= 83 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 84 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 85 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 86 | golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 87 | golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= 88 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 89 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 90 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 92 | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= 93 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 94 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 101 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 102 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 103 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 104 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 105 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 107 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 108 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 109 | golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= 110 | golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 111 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 113 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 114 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 115 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 116 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 118 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 121 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 122 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 123 | k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= 124 | k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 125 | -------------------------------------------------------------------------------- /headers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" 4 | 5 | const ( 6 | UpStreamProxyHeader = "x-tlsproxy-upstream" 7 | ClientProfileHeader = "x-tlsproxy-client" 8 | ) 9 | 10 | var CustomHeaders = []string{UpStreamProxyHeader, ClientProfileHeader} 11 | 12 | type ProxyConfig struct { 13 | client string 14 | upstreamProxy string 15 | } 16 | 17 | func parseCustomHeaders(headers *http.Header) ProxyConfig { 18 | return ProxyConfig{ 19 | upstreamProxy: headers.Get(UpStreamProxyHeader), 20 | client: headers.Get(ClientProfileHeader), 21 | } 22 | } 23 | 24 | func removeCustomHeaders(headers *http.Header) { 25 | for _, header := range CustomHeaders { 26 | headers.Del(header) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/elazarl/goproxy" 7 | ) 8 | 9 | func invalidClientResponse(req *http.Request, ctx *goproxy.ProxyCtx, client string) *http.Response { 10 | ctx.Logf("Client specified invalid client: %s", client) 11 | return goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadRequest, "Invalid client: "+client) 12 | } 13 | 14 | func invalidUpstreamProxyResponse(req *http.Request, ctx *goproxy.ProxyCtx, upstreamProxy string) *http.Response { 15 | ctx.Logf("Client specified invalid upstream proxy: %s", upstreamProxy) 16 | return goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadRequest, "Invalid upstream proxy: "+upstreamProxy) 17 | } 18 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | 9 | cflog "github.com/cloudflare/cfssl/log" 10 | "github.com/elazarl/goproxy" 11 | utls "github.com/refraction-networking/utls" 12 | sf "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/v2/common/utls" 13 | ) 14 | 15 | func main() { 16 | flag.StringVar(&Flags.addr, "addr", "", "Proxy listen address") 17 | flag.StringVar(&Flags.port, "port", "8080", "Proxy listen port") 18 | flag.StringVar(&Flags.cert, "cert", "cert.pem", "TLS CA certificate (generated automatically if not present)") 19 | flag.StringVar(&Flags.key, "key", "key.pem", "TLS CA key (generated automatically if not present)") 20 | flag.StringVar(&Flags.upstreamProxy, "upstream", "", "Default upstream proxy prefixed by \"socks5://\" (can be overriden through x-tlsproxy-upstream header)") 21 | flag.StringVar(&Flags.client, "client", "Chrome-120", "Default utls clientHelloID (can be overriden through x-tlsproxy-client header)") 22 | flag.BoolVar(&Flags.verbose, "verbose", false, "Enable verbose logging") 23 | flag.Parse() 24 | 25 | if !Flags.verbose { 26 | cflog.Level = cflog.LevelError 27 | } 28 | 29 | loadDefaultProxyConfig() 30 | loadCA() 31 | 32 | proxy := goproxy.NewProxyHttpServer() 33 | proxy.Verbose = Flags.verbose 34 | 35 | proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) 36 | 37 | proxy.OnRequest().DoFunc( 38 | func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 39 | proxyConfig := parseCustomHeaders(&req.Header) 40 | removeCustomHeaders(&req.Header) 41 | 42 | clientHelloId := DefaultClientHelloID 43 | upstreamProxy := DefaultUpstreamProxy 44 | 45 | if len(proxyConfig.client) > 0 { 46 | customClientHeaderId, ok := getClientHelloID(proxyConfig.client) 47 | if !ok { 48 | return req, invalidClientResponse(req, ctx, proxyConfig.client) 49 | } 50 | 51 | clientHelloId = customClientHeaderId 52 | } 53 | 54 | if len(proxyConfig.upstreamProxy) > 0 { 55 | proxyUrl, err := url.Parse(proxyConfig.upstreamProxy) 56 | if err != nil { 57 | return req, invalidUpstreamProxyResponse(req, ctx, proxyConfig.upstreamProxy) 58 | } 59 | 60 | upstreamProxy = proxyUrl 61 | } 62 | 63 | roundTripper := sf.NewUTLSHTTPRoundTripperWithProxy(clientHelloId, &utls.Config{ 64 | InsecureSkipVerify: true, 65 | }, http.DefaultTransport, false, upstreamProxy) 66 | 67 | ctx.RoundTripper = goproxy.RoundTripperFunc( 68 | func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Response, error) { 69 | return roundTripper.RoundTrip(req) 70 | }) 71 | 72 | return req, nil 73 | }, 74 | ) 75 | 76 | listenAddr := Flags.addr + ":" + Flags.port 77 | log.Println("tlsproxy listening at " + listenAddr) 78 | log.Fatal(http.ListenAndServe(listenAddr, proxy)) 79 | } 80 | --------------------------------------------------------------------------------