From 4cdd11b9504ece8b2a2e42e16351b5f2982aa46d Mon Sep 17 00:00:00 2001 From: lyle Date: Sat, 8 Jul 2023 12:10:38 +0800 Subject: [PATCH] Initial commit --- .github/workflows/go.yml | 27 +++++ .github/workflows/release.yml | 25 +++++ .gitignore | 23 +++++ README.md | 49 +++++++++ cert.go | 52 ++++++++++ go.mod | 13 +++ go.sum | 12 +++ makefile | 16 +++ proxy.go | 182 ++++++++++++++++++++++++++++++++++ 9 files changed, 399 insertions(+) create mode 100644 .github/workflows/go.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cert.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 makefile create mode 100644 proxy.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..4dd2544 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,27 @@ +name: Go +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + build: + name: Build + runs-on: ${{ matrix.os }} + strategy: + matrix: + go-version: [1.19.x] + os: [ubuntu-latest, windows-latest] + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Get dependencies + run: go mod download + - name: Build + run: go build -v ./... + - name: Test + run: go test -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2c3b184 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release +on: + release: + types: [created] +permissions: + contents: write + packages: write +jobs: + releases-matrix: + name: Release ja3proxy binary + runs-on: ubuntu-latest + strategy: + matrix: + # build and publish in parallel: linux/amd64, linux/arm64, + # windows/amd64, windows/arm64, darwin/amd64, darwin/arm64 + goos: [ linux, windows, darwin ] + goarch: [ amd64, arm64 ] + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1.29 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + binary_name: "ja3proxy" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edc7860 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +*.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..44e7729 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# JA3Proxy + +Customizing TLS (JA3) Fingerprints through HTTP Proxy + +## Usage + +```bash +git clone https://github.com/lylemi/ja3proxy +cd ja3proxy +make +./ja3proxy -port 8080 -client 360Browser -version 7.5 +curl -v -k --proxy http://localhost:8080 https://www.example.com +``` + +### Perdefined clients and versions + +> for full list, see: https://github.com/refraction-networking/utls/blob/master/u_common.go + +| Client | Version | +| ------ | ------- | +| Golang | 0 | +| Firefox | 55 | +| Firefox | 56 | +| Firefox | 63 | +| Firefox | 99 | +| Firefox | 105 | +| Chrome | 58 | +| Chrome | 62 | +| Chrome | 70 | +| Chrome | 96 | +| Chrome | 102 | +| Chrome | 106 | +| iOS | 12.1 | +| iOS | 13 | +| iOS | 14 | +| Android | 11 | +| Edge | 85 | +| Edge | 106 | +| Safari | 16.0 | +| 360Browser | 7.5 | +| QQBrowser | 11.1 | + +## Contribution + +If you have any ideas or suggestions, please feel free to submit a pull request. We appreciate any contributions. + +## Contact + +If you have any questions or suggestions, please feel free to contact us. diff --git a/cert.go b/cert.go new file mode 100644 index 0000000..6562d91 --- /dev/null +++ b/cert.go @@ -0,0 +1,52 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "time" +) + +func generateCertificate() error { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "localhost"}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return err + } + + certOut, err := os.Create("cert.pem") + if err != nil { + return err + } + defer certOut.Close() + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + + keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer keyOut.Close() + pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cac3922 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/lylemi/ja3proxy + +go 1.20 + +require github.com/refraction-networking/utls v1.3.2 + +require ( + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/gaukas/godicttls v0.0.3 // indirect + github.com/klauspost/compress v1.15.15 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/sys v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..50bc90d --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= +github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= +github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/makefile b/makefile new file mode 100644 index 0000000..f19d8e3 --- /dev/null +++ b/makefile @@ -0,0 +1,16 @@ +.PHONY: all build-linux build-windows clean + +BINARY_NAME=ja3proxy +BINARY_LINUX=$(BINARY_NAME) +BINARY_WINDOWS=$(BINARY_NAME).exe + +all: build-linux build-windows + +build-linux: + GOOS=linux GOARCH=amd64 go build -o $(BINARY_LINUX) + +build-windows: + GOOS=windows GOARCH=amd64 go build -o $(BINARY_WINDOWS) + +clean: + rm -f $(BINARY_LINUX) $(BINARY_WINDOWS) diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..dfb2754 --- /dev/null +++ b/proxy.go @@ -0,0 +1,182 @@ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "strings" + "time" + + utls "github.com/refraction-networking/utls" +) + +var ( + cert string + key string + tlsClient string + tlsVersion string +) + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return !os.IsNotExist(err) +} + +func customTLSWrap(conn net.Conn, sni string) (net.Conn, error) { + clientHelloID := utls.ClientHelloID{ + tlsClient, tlsVersion, nil, nil, + } + + uTLSConn := utls.UClient( + conn, + &utls.Config{ + ServerName: sni, + InsecureSkipVerify: true, + }, + clientHelloID, + ) + if err := uTLSConn.Handshake(); err != nil { + return nil, err + } + return uTLSConn, nil +} + +func handleTunneling(w http.ResponseWriter, r *http.Request) { + log.Printf("proxy to %s", r.Host) + destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + log.Println("Tunneling err", err) + return + } + w.WriteHeader(http.StatusOK) + + hijacker, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Hijacking not supported", http.StatusInternalServerError) + log.Println("Hijacking not supported") + return + } + clientConn, _, err := hijacker.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + log.Println("Hijack error", err) + } + go connect(strings.Split(r.Host, ":")[0], destConn, clientConn) +} + +func handleHTTP(w http.ResponseWriter, req *http.Request) { + resp, err := http.DefaultTransport.RoundTrip(req) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + log.Println(err) + return + } + defer resp.Body.Close() + copyHeader(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + +func connect(sni string, destConn net.Conn, clientConn net.Conn) { + defer destConn.Close() + defer clientConn.Close() + destTLSConn, err := customTLSWrap(destConn, sni) + if err != nil { + fmt.Println("TLS handshake failed:", err) + return + } + + cert, err := tls.LoadX509KeyPair(cert, key) + if err != nil { + log.Fatal(err) + } + + clientTLSConn := tls.Server( + clientConn, + &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{cert}, + }, + ) + err = clientTLSConn.Handshake() + if err != nil { + log.Println("Failed to perform TLS handshake:", err) + return + } + + junction(destTLSConn, clientTLSConn) +} + +func junction(destConn net.Conn, clientConn net.Conn) { + chDone := make(chan bool) + + go func() { + _, err := io.Copy(destConn, clientConn) + if err != nil { + log.Println("copy dest to client error", err) + } + chDone <- true + }() + + go func() { + _, err := io.Copy(clientConn, destConn) + if err != nil { + log.Println("copy client to dest error", err) + } + chDone <- true + }() + + <-chDone +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func main() { + var ( + addr string + port string + ) + flag.StringVar(&cert, "cert", "cert.pem", "proxy tls cert") + flag.StringVar(&key, "key", "key.pem", "proxy tls key") + flag.StringVar(&addr, "addr", "", "proxy host") + flag.StringVar(&port, "port", "8080", "proxy port") + flag.StringVar(&tlsClient, "client", "Golang", "utls client") + flag.StringVar(&tlsVersion, "version", "0", "utls client version") + flag.Parse() + + if !fileExists(cert) || !fileExists(key) { + log.Println("cert not exists, generate") + cert = "" + generateCertificate() + } + + server := &http.Server{ + Addr: addr + ":" + port, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + handleTunneling(w, r) + } else { + handleHTTP(w, r) + } + }), + } + + fmt.Println("HTTP Proxy Server started at localhost Port:" + port) + err := server.ListenAndServe() + if err != nil { + log.Fatal(err) + os.Exit(-1) + } +}