├── .editorconfig
├── .github
└── workflows
│ ├── ci.yaml
│ └── release.yaml
├── .gitignore
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── cmd
└── main.go
├── e2e
├── memtest
│ ├── go.mod
│ ├── go.sum
│ ├── helper.go
│ └── main.go
└── webbrowser
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── test_chrome.py
│ └── test_firefox.py
├── env.go
├── example
├── echo-server
│ ├── Makefile
│ ├── README.md
│ ├── detail.go
│ ├── main.go
│ ├── response.go
│ └── server.go
├── http2-fingerprint-dos-poc
│ └── main.go
├── ja3-raw
│ ├── README.md
│ └── main.go
├── ja3-sorted-extensions
│ ├── README.md
│ └── main.go
└── my-fingerprint
│ ├── README.md
│ └── main.go
├── fingerproxy.go
├── flags.go
├── go.mod
├── go.sum
├── pkg
├── README.md
├── certwatcher
│ └── certwatcher.go
├── debug
│ ├── debug.go
│ └── debug_stub.go
├── fingerprint
│ ├── fingerprint.go
│ └── header_injector.go
├── hack
│ ├── channel_listener.go
│ ├── hajack_clienthello_conn.go
│ └── tls_clienthello_conn.go
├── http2
│ ├── .gitignore
│ ├── LICENSE
│ ├── ascii.go
│ ├── ascii_test.go
│ ├── ciphers.go
│ ├── ciphers_test.go
│ ├── client_conn_pool.go
│ ├── clientconn_test.go
│ ├── config.go
│ ├── config_go124.go
│ ├── config_pre_go124.go
│ ├── config_test.go
│ ├── connframes_test.go
│ ├── databuffer.go
│ ├── databuffer_test.go
│ ├── errors.go
│ ├── errors_test.go
│ ├── flow.go
│ ├── flow_test.go
│ ├── frame.go
│ ├── frame_test.go
│ ├── gate_test.go
│ ├── gotrack.go
│ ├── gotrack_test.go
│ ├── h2c
│ │ ├── h2c.go
│ │ └── h2c_test.go
│ ├── h2i
│ │ ├── README.md
│ │ └── h2i.go
│ ├── headermap.go
│ ├── hpack
│ │ ├── encode.go
│ │ ├── encode_test.go
│ │ ├── gen.go
│ │ ├── hpack.go
│ │ ├── hpack_test.go
│ │ ├── huffman.go
│ │ ├── static_table.go
│ │ ├── tables.go
│ │ └── tables_test.go
│ ├── http2.go
│ ├── http2_test.go
│ ├── netconn_test.go
│ ├── pipe.go
│ ├── pipe_test.go
│ ├── server.go
│ ├── server_push_test.go
│ ├── server_test.go
│ ├── sync_test.go
│ ├── timer.go
│ ├── transport.go
│ ├── transport_test.go
│ ├── unencrypted.go
│ ├── write.go
│ ├── writesched.go
│ ├── writesched_priority.go
│ ├── writesched_priority_test.go
│ ├── writesched_random.go
│ ├── writesched_random_test.go
│ ├── writesched_roundrobin.go
│ ├── writesched_roundrobin_test.go
│ └── writesched_test.go
├── ja3
│ ├── LICENSE
│ ├── ja3.go
│ └── sync.sh
├── ja4
│ ├── LICENSE
│ ├── helper.go
│ ├── ja4.go
│ ├── ja4_test.go
│ └── types.go
├── ja4pcap
│ ├── pcap.go
│ ├── pcap_test.go
│ ├── sync-testdata.sh
│ └── testdata
│ │ ├── pcap
│ │ ├── CVE-2018-6794.pcap
│ │ ├── badcurveball.pcap
│ │ ├── browsers-x509.pcapng
│ │ ├── chrome-cloudflare-quic-with-secrets.pcapng
│ │ ├── gre-erspan-vxlan.pcap
│ │ ├── gre-sample.pcap
│ │ ├── http1-with-cookies.pcapng
│ │ ├── http1.pcapng
│ │ ├── http2-with-cookies.pcapng
│ │ ├── ipv6.pcapng
│ │ ├── latest.pcapng
│ │ ├── macos_tcp_flags.pcap
│ │ ├── quic-tls-handshake.pcapng
│ │ ├── quic-with-several-tls-frames.pcapng
│ │ ├── single-packets.pcap
│ │ ├── socks4-https.pcap
│ │ ├── ssh-r.pcap
│ │ ├── ssh-scp-1050.pcap
│ │ ├── ssh.pcapng
│ │ ├── ssh2-malformed.pcap
│ │ ├── ssh2-moloch-crash.pcap
│ │ ├── ssh2.pcapng
│ │ ├── sshv1.pcap
│ │ ├── tcpdump-geneve.pcap
│ │ ├── tls-alpn-h2.pcap
│ │ ├── tls-handshake.pcapng
│ │ ├── tls-non-ascii-alpn.pcapng
│ │ ├── tls-sni.pcapng
│ │ ├── tls12.pcap
│ │ ├── tls3.pcapng
│ │ └── v6.pcap
│ │ └── snapshots
│ │ ├── ja4__insta@CVE-2018-6794.pcap.snap
│ │ ├── ja4__insta@badcurveball.pcap.snap
│ │ ├── ja4__insta@browsers-x509.pcapng.snap
│ │ ├── ja4__insta@chrome-cloudflare-quic-with-secrets.pcapng.snap
│ │ ├── ja4__insta@gre-erspan-vxlan.pcap.snap
│ │ ├── ja4__insta@gre-sample.pcap.snap
│ │ ├── ja4__insta@gtp-iphone.pcap.snap
│ │ ├── ja4__insta@http1-with-cookies.pcapng.snap
│ │ ├── ja4__insta@http1.pcapng.snap
│ │ ├── ja4__insta@http2-with-cookies.pcapng.snap
│ │ ├── ja4__insta@ipv6.pcapng.snap
│ │ ├── ja4__insta@latest.pcapng.snap
│ │ ├── ja4__insta@macos_tcp_flags.pcap.snap
│ │ ├── ja4__insta@quic-tls-handshake.pcapng.snap
│ │ ├── ja4__insta@quic-with-several-tls-frames.pcapng.snap
│ │ ├── ja4__insta@single-packets.pcap.snap
│ │ ├── ja4__insta@socks4-https.pcap.snap
│ │ ├── ja4__insta@ssh-r.pcap.snap
│ │ ├── ja4__insta@ssh-scp-1050.pcap.snap
│ │ ├── ja4__insta@ssh.pcapng.snap
│ │ ├── ja4__insta@ssh2-malformed.pcap.snap
│ │ ├── ja4__insta@ssh2-moloch-crash.pcap.snap
│ │ ├── ja4__insta@ssh2.pcapng.snap
│ │ ├── ja4__insta@sshv1.pcap.snap
│ │ ├── ja4__insta@tcpdump-geneve.pcap.snap
│ │ ├── ja4__insta@tls-alpn-h2.pcap.snap
│ │ ├── ja4__insta@tls-handshake.pcapng.snap
│ │ ├── ja4__insta@tls-non-ascii-alpn.pcapng.snap
│ │ ├── ja4__insta@tls-sni.pcapng.snap
│ │ ├── ja4__insta@tls12.pcap.snap
│ │ ├── ja4__insta@tls3.pcapng.snap
│ │ └── ja4__insta@v6.pcap.snap
├── metadata
│ ├── context.go
│ ├── http2.go
│ └── metadata.go
├── proxyserver
│ ├── proxyserver.go
│ └── proxyserver_test.go
├── reverseproxy
│ ├── handler.go
│ ├── handler_test.go
│ └── header_injector.go
└── sync-http2-pkg.sh
└── testdata
└── gencert.sh
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.{json,yml,yaml}]
10 | indent_style = space
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags: ["*"]
5 |
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-latest
10 | permissions:
11 | # allow creating releases
12 | contents: write
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 |
18 | - uses: actions/setup-go@v5
19 | with:
20 | go-version-file: go.mod
21 |
22 | - run: |
23 | go version
24 | go get
25 |
26 | - run: |
27 | make build
28 | make sha256sum
29 |
30 | - working-directory: ./example/echo-server
31 | run: |
32 | make build
33 | make sha256sum
34 |
35 | - name: Create release
36 | env:
37 | GH_TOKEN: ${{ github.token }}
38 | run: |
39 | TAG=$(git describe --tags --abbrev=0 HEAD)
40 | LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ || true)
41 | [[ -n $LAST_TAG ]] && RANGE=$LAST_TAG..HEAD || RANGE=HEAD
42 | RELEASE_NOTES=$(git log "$RANGE" --oneline --decorate)
43 |
44 | gh release create --notes "$RELEASE_NOTES" "$TAG" \
45 | ./bin/* \
46 | ./example/echo-server/bin/*
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.vscode
3 | *.crt
4 | *.key
5 | /bin
6 | /example/echo-server/bin
7 | /fingerproxy
8 | .pytest_cache
9 | __pycache__
10 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = fingerproxy
2 |
3 | build: build_darwin_arm64 build_darwin_amd64 \
4 | build_linux_amd64 build_linux_arm build_linux_arm64 \
5 | build_windows_amd64 build_windows_arm64
6 |
7 | build_darwin_%: GOOS = darwin
8 | build_linux_%: GOOS = linux
9 | build_windows_%: GOOS = windows
10 | build_windows_%: EXT = .exe
11 |
12 | build_%_amd64: GOARCH = amd64
13 | build_%_arm: GOARCH = arm
14 | build_%_arm64: GOARCH = arm64
15 |
16 | COMMIT = $(shell git rev-parse --short HEAD || true)
17 | TAG = $(shell git describe --tags --abbrev=0 HEAD 2>/dev/null || true)
18 | BINDIR = bin
19 | BINPATH = $(BINDIR)/$(TARGET)_$(GOOS)_$(GOARCH)$(EXT)
20 |
21 | build_%:
22 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(BINPATH) \
23 | -ldflags "-X main.buildCommit=$(COMMIT) -X main.buildVersion=$(TAG)" \
24 | -gcflags "./...=-m" \
25 | -gcflags "./pkg/http2=" \
26 | ./cmd
27 |
28 | chmod +x $(BINPATH)
29 |
30 | sha256sum:
31 | cd $(BINDIR) && sha256sum $(TARGET)_* > $(TARGET).sha256sum
32 |
33 | PKG_LIST = $(shell go list ./... | grep -v github.com/wi1dcard/fingerproxy/pkg/http2)
34 | test:
35 | @go test -v $(PKG_LIST)
36 |
37 | benchmark:
38 | @go test -v $(PKG_LIST) -run=NONE -bench=^Benchmark -benchmem -count=3 -cpu=2
39 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Fingerproxy
2 | Copyright 2024 Weizhe Sun
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/wi1dcard/fingerproxy"
4 |
5 | func main() {
6 | fingerproxy.Run()
7 | }
8 |
--------------------------------------------------------------------------------
/e2e/memtest/go.mod:
--------------------------------------------------------------------------------
1 | module memtest
2 |
3 | go 1.22.0
4 |
--------------------------------------------------------------------------------
/e2e/memtest/go.sum:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/e2e/memtest/go.sum
--------------------------------------------------------------------------------
/e2e/memtest/helper.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "log"
6 | "net/http"
7 | "os/exec"
8 | )
9 |
10 | func wget(addr string) string {
11 | resp, err := http.Get(addr)
12 | if err != nil {
13 | panic(err)
14 | }
15 | defer resp.Body.Close()
16 | if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
17 | panic(resp.Status)
18 | }
19 | body, err := io.ReadAll(resp.Body)
20 | if err != nil {
21 | panic(err)
22 | }
23 | return string(body)
24 | }
25 |
26 | func printOpenedConn() {
27 | out, err := exec.Command(
28 | "bash",
29 | "-c",
30 | "lsof -nP -i TCP@localhost:8443 -sTCP:ESTABLISHED | grep -e '->127.0.0.1:8443' | wc -l",
31 | ).Output()
32 |
33 | if err != nil {
34 | panic(err)
35 | }
36 | log.Printf("lsof opened conns: %s", string(out))
37 | }
38 |
--------------------------------------------------------------------------------
/e2e/memtest/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net/http"
9 | "os"
10 | "sync"
11 | "time"
12 | )
13 |
14 | const (
15 | fingerproxyAddr = "https://localhost:8443/"
16 | fingerproxyDebugAddr = "http://localhost:9036/mem"
17 | fingerproxyGcAddr = "http://localhost:9036/gc"
18 |
19 | backendListenAddr = "localhost:8000"
20 |
21 | numConcurrentConns = 1000
22 | sleepBetweenConns = 1 * time.Millisecond
23 | )
24 |
25 | var (
26 | waitForRequestsToBackend sync.WaitGroup
27 | waitForRequestsServed sync.WaitGroup
28 |
29 | doneProfiling = false
30 | waitForProfiling = sync.NewCond(&sync.Mutex{})
31 | )
32 |
33 | func slowBackend() {
34 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
35 | waitForRequestsToBackend.Done()
36 | waitForProfiling.L.Lock()
37 | for !doneProfiling {
38 | waitForProfiling.Wait()
39 | }
40 | waitForProfiling.L.Unlock()
41 | w.WriteHeader(http.StatusOK)
42 |
43 | fmt.Fprint(w, r.Header.Get("X-HTTP2-Fingerprint"))
44 | })
45 |
46 | server := &http.Server{
47 | Addr: backendListenAddr,
48 | ReadTimeout: 0,
49 | WriteTimeout: 0,
50 | IdleTimeout: 0,
51 | }
52 |
53 | log.Printf("backend listening on %s", backendListenAddr)
54 | err := server.ListenAndServe()
55 | if err != nil {
56 | panic(err)
57 | }
58 | }
59 |
60 | func main() {
61 | log.SetOutput(os.Stdout)
62 |
63 | log.Printf("pre-check:")
64 | fmt.Println(wget(fingerproxyDebugAddr))
65 |
66 | go slowBackend()
67 | time.Sleep(1 * time.Second) // wait until http server starts
68 |
69 | waitForRequestsToBackend.Add(numConcurrentConns)
70 | for i := 0; i < numConcurrentConns; i++ {
71 | go request(i)
72 | time.Sleep(sleepBetweenConns)
73 | }
74 |
75 | waitForRequestsToBackend.Wait()
76 |
77 | printOpenedConn()
78 |
79 | log.Printf("opened %d conns:", numConcurrentConns)
80 | fmt.Println(wget(fingerproxyDebugAddr))
81 |
82 | waitForProfiling.L.Lock()
83 | doneProfiling = true
84 | waitForProfiling.L.Unlock()
85 |
86 | waitForRequestsServed.Add(numConcurrentConns)
87 | waitForProfiling.Broadcast()
88 | waitForRequestsServed.Wait()
89 |
90 | printOpenedConn()
91 |
92 | log.Printf("conns closed:")
93 | fmt.Println(wget(fingerproxyDebugAddr))
94 |
95 | log.Printf("after gc:")
96 | wget(fingerproxyGcAddr)
97 | fmt.Println(wget(fingerproxyDebugAddr))
98 | }
99 |
100 | func request(i int) {
101 | transport := &http.Transport{
102 | TLSClientConfig: &tls.Config{
103 | InsecureSkipVerify: true,
104 | },
105 |
106 | // enable http2
107 | ForceAttemptHTTP2: true,
108 |
109 | // disable connection pool
110 | DisableKeepAlives: true,
111 | MaxIdleConns: -1,
112 | }
113 |
114 | req, err := http.NewRequest("GET", fingerproxyAddr, nil)
115 | if err != nil {
116 | panic(err)
117 | }
118 |
119 | resp, err := transport.RoundTrip(req)
120 | if err != nil {
121 | panic(err)
122 | }
123 | defer resp.Body.Close()
124 |
125 | if resp.StatusCode != http.StatusOK {
126 | panic(fmt.Errorf("[%d] response code seems incorrect: %s", i, resp.Status))
127 | }
128 |
129 | body, err := io.ReadAll(resp.Body)
130 | if err != nil {
131 | panic(err)
132 | }
133 |
134 | if string(body) != "2:0;4:4194304;6:10485760|1073741824|0|a,m,p,s" {
135 | panic(fmt.Errorf("[%d] response body seems incorrect: %s", i, string(body)))
136 | }
137 |
138 | waitForRequestsServed.Done()
139 | }
140 |
--------------------------------------------------------------------------------
/e2e/webbrowser/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | selenium = "*"
8 | pytest = "*"
9 |
10 | [dev-packages]
11 |
12 | [requires]
13 | python_version = "3.12"
14 |
--------------------------------------------------------------------------------
/e2e/webbrowser/test_chrome.py:
--------------------------------------------------------------------------------
1 | import json
2 | from selenium import webdriver
3 | from selenium.webdriver.common.by import By
4 |
5 | def test_chrome():
6 | options = webdriver.ChromeOptions()
7 | options.add_argument('--ignore-certificate-errors')
8 | options.add_argument("--headless")
9 |
10 | driver = webdriver.Chrome(options)
11 | print(driver.capabilities['browserVersion'])
12 |
13 | driver.get("https://localhost:8443/anything")
14 | driver.implicitly_wait(0.5)
15 | print(driver.page_source)
16 |
17 | content = driver.find_element(by=By.TAG_NAME, value='pre').text
18 | parsed_json = json.loads(content)
19 |
20 | # chrome version: 136.0.7103.92
21 | assert parsed_json["headers"]["X-Http2-Fingerprint"] == "1:65536;2:0;4:6291456;6:262144|15663105|1:1:0:256|m,a,s,p"
22 | assert parsed_json["headers"]["X-Ja4-Fingerprint"] == "t13d1516h2_8daaf6152771_d8a2da3f94cd"
23 |
24 | driver.quit()
25 |
--------------------------------------------------------------------------------
/e2e/webbrowser/test_firefox.py:
--------------------------------------------------------------------------------
1 | import json
2 | from selenium import webdriver
3 | from selenium.webdriver.common.by import By
4 |
5 | def test_firefox():
6 | options = webdriver.FirefoxOptions()
7 | options.add_argument('--ignore-certificate-errors')
8 | options.add_argument("--headless")
9 |
10 | driver = webdriver.Firefox(options)
11 | print(driver.capabilities['browserVersion'])
12 |
13 | driver.get("view-source:https://localhost:8443/anything")
14 | driver.implicitly_wait(0.5)
15 | print(driver.page_source)
16 |
17 | content = driver.find_element(by=By.TAG_NAME, value='pre').text
18 | parsed_json = json.loads(content)
19 |
20 | # firefox verison: 138.0.1
21 | assert parsed_json["headers"]["X-Http2-Fingerprint"] == "1:65536;2:0;4:131072;5:16384|12517377|3:0:0:22|m,p,a,s"
22 | assert parsed_json["headers"]["X-Ja3-Fingerprint"] == "6f7889b9fb1a62a9577e685c1fcfa919"
23 | assert parsed_json["headers"]["X-Ja4-Fingerprint"] == "t13d1717h2_5b57614c22b0_3cbfd9057e0d"
24 |
25 | driver.quit()
26 |
--------------------------------------------------------------------------------
/env.go:
--------------------------------------------------------------------------------
1 | package fingerproxy
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | func envWithDefault(key string, defaultVal string) string {
11 | if envVal, ok := os.LookupEnv(key); ok {
12 | return envVal
13 | }
14 | return defaultVal
15 | }
16 |
17 | func envWithDefaultUint(key string, defaultVal uint) uint {
18 | if envVal, ok := os.LookupEnv(key); ok {
19 | if ret, err := strconv.ParseUint(envVal, 10, 0); err == nil {
20 | return uint(ret)
21 | } else {
22 | log.Fatalf("invalid environment variable $%s, expect uint, actual %s: %s", key, envVal, err)
23 | }
24 | }
25 | return defaultVal
26 | }
27 |
28 | func envWithDefaultBool(key string, defaultVal bool) bool {
29 | if envVal, ok := os.LookupEnv(key); ok {
30 | if strings.ToLower(envVal) == "true" {
31 | return true
32 | } else if strings.ToLower(envVal) == "false" {
33 | return false
34 | }
35 | }
36 | return defaultVal
37 | }
38 |
--------------------------------------------------------------------------------
/example/echo-server/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = echo-server
2 |
3 | build: build_darwin_arm64 build_darwin_amd64 \
4 | build_linux_amd64 build_linux_arm build_linux_arm64 \
5 | build_windows_amd64 build_windows_arm64
6 |
7 | build_darwin_%: GOOS = darwin
8 | build_linux_%: GOOS = linux
9 | build_windows_%: GOOS = windows
10 | build_windows_%: EXT = .exe
11 |
12 | build_%_amd64: GOARCH = amd64
13 | build_%_arm: GOARCH = arm
14 | build_%_arm64: GOARCH = arm64
15 |
16 | BINDIR = bin
17 | BINPATH = $(BINDIR)/$(TARGET)_$(GOOS)_$(GOARCH)$(EXT)
18 |
19 | build_%:
20 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(BINPATH) .
21 |
22 | chmod +x $(BINPATH)
23 |
24 | sha256sum:
25 | cd $(BINDIR) && sha256sum $(TARGET)_* > $(TARGET).sha256sum
26 |
--------------------------------------------------------------------------------
/example/echo-server/detail.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/dreadl0ck/tlsx"
8 | "github.com/refraction-networking/utls/dicttls"
9 | "github.com/wi1dcard/fingerproxy/pkg/ja4"
10 | )
11 |
12 | type ja3Detail tlsx.ClientHelloBasic
13 |
14 | type ja4Detail ja4.JA4Fingerprint
15 |
16 | func (j *ja3Detail) MarshalJSON() ([]byte, error) {
17 | data := struct {
18 | ja3Detail
19 | ReadableCipherSuites []string
20 | ReadableAllExtensions []string
21 | ReadableSupportedGroups []string
22 | }{
23 | ja3Detail: *j,
24 | ReadableCipherSuites: make([]string, len(j.CipherSuites)),
25 | ReadableAllExtensions: make([]string, len(j.AllExtensions)),
26 | ReadableSupportedGroups: make([]string, len(j.SupportedGroups)),
27 | }
28 |
29 | for i, v := range j.CipherSuites {
30 | u := uint16(v)
31 | if name, ok := dicttls.DictCipherSuiteValueIndexed[u]; ok {
32 | data.ReadableCipherSuites[i] = fmt.Sprintf("%s (0x%x)", name, u)
33 | } else {
34 | data.ReadableCipherSuites[i] = fmt.Sprintf("UNKNOWN (0x%x)", u)
35 | }
36 | }
37 |
38 | for i, v := range j.AllExtensions {
39 | if name, ok := dicttls.DictExtTypeValueIndexed[v]; ok {
40 | data.ReadableAllExtensions[i] = fmt.Sprintf("%s (0x%x)", name, v)
41 | } else {
42 | data.ReadableAllExtensions[i] = fmt.Sprintf("unknown (0x%x)", v)
43 | }
44 | }
45 |
46 | for i, v := range j.SupportedGroups {
47 | if name, ok := dicttls.DictSupportedGroupsValueIndexed[v]; ok {
48 | data.ReadableSupportedGroups[i] = fmt.Sprintf("%s (0x%x)", name, v)
49 | } else {
50 | data.ReadableSupportedGroups[i] = fmt.Sprintf("unknown (0x%x)", v)
51 | }
52 | }
53 |
54 | return json.Marshal(data)
55 | }
56 |
57 | func (j *ja4Detail) MarshalJSON() ([]byte, error) {
58 | data := struct {
59 | ja4Detail
60 | ReadableCipherSuites []string
61 | ReadableExtensions []string
62 | ReadableSignatureAlgorithms []string
63 | }{
64 | ja4Detail: *j,
65 | ReadableCipherSuites: make([]string, len(j.CipherSuites)),
66 | ReadableExtensions: make([]string, len(j.Extensions)),
67 | ReadableSignatureAlgorithms: make([]string, len(j.SignatureAlgorithms)),
68 | }
69 |
70 | for i, v := range j.CipherSuites {
71 | if name, ok := dicttls.DictCipherSuiteValueIndexed[v]; ok {
72 | data.ReadableCipherSuites[i] = fmt.Sprintf("%s (0x%x)", name, v)
73 | } else {
74 | data.ReadableCipherSuites[i] = fmt.Sprintf("UNKNOWN (0x%x)", v)
75 | }
76 | }
77 |
78 | for i, v := range j.Extensions {
79 | if name, ok := dicttls.DictExtTypeValueIndexed[v]; ok {
80 | data.ReadableExtensions[i] = fmt.Sprintf("%s (0x%x)", name, v)
81 | } else {
82 | data.ReadableExtensions[i] = fmt.Sprintf("unknown (0x%x)", v)
83 | }
84 | }
85 |
86 | for i, v := range j.SignatureAlgorithms {
87 | if name, ok := dicttls.DictSignatureAlgorithmValueIndexed[uint8(v)]; ok {
88 | data.ReadableSignatureAlgorithms[i] = fmt.Sprintf("%s (0x%x)", name, v)
89 | } else {
90 | data.ReadableSignatureAlgorithms[i] = fmt.Sprintf("unknown (0x%x)", v)
91 | }
92 | }
93 |
94 | return json.Marshal(data)
95 | }
96 |
--------------------------------------------------------------------------------
/example/echo-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "flag"
7 | "log"
8 | "net/http"
9 | "os/signal"
10 | "syscall"
11 |
12 | "github.com/wi1dcard/fingerproxy/pkg/debug"
13 | "github.com/wi1dcard/fingerproxy/pkg/proxyserver"
14 | )
15 |
16 | var (
17 | flagListenAddr, flagCertFilename, flagKeyFilename *string
18 |
19 | flagBenchmarkControlGroup, flagVerbose, flagQuiet *bool
20 |
21 | tlsConf *tls.Config
22 |
23 | ctx, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
24 | )
25 |
26 | func main() {
27 | parseFlags()
28 |
29 | setupTLSConfig()
30 |
31 | if *flagBenchmarkControlGroup {
32 | runAsControlGroup()
33 | } else {
34 | run()
35 | }
36 | }
37 |
38 | func parseFlags() {
39 | flagListenAddr = flag.String(
40 | "listen-addr",
41 | "localhost:8443",
42 | "Listening address",
43 | )
44 | flagCertFilename = flag.String(
45 | "cert-filename",
46 | "tls.crt",
47 | "TLS certificate filename",
48 | )
49 | flagKeyFilename = flag.String(
50 | "certkey-filename",
51 | "tls.key",
52 | "TLS certificate key file name",
53 | )
54 | flagBenchmarkControlGroup = flag.Bool(
55 | "benchmark-control-group",
56 | false,
57 | "Start a golang default TLS server as the control group for benchmarking",
58 | )
59 | flagVerbose = flag.Bool("verbose", false, "Print fingerprint detail in logs, conflict with -quiet")
60 | flagQuiet = flag.Bool("quiet", false, "Do not print fingerprints in logs, conflict with -verbose")
61 | flag.Parse()
62 |
63 | if *flagVerbose && *flagQuiet {
64 | log.Fatal("-verbose and -quiet cannot be specified at the same time")
65 | }
66 | }
67 |
68 | func setupTLSConfig() {
69 | tlsConf = &tls.Config{
70 | NextProtos: []string{"h2", "http/1.1"},
71 | }
72 |
73 | if tlsCert, err := tls.LoadX509KeyPair(*flagCertFilename, *flagKeyFilename); err != nil {
74 | log.Fatal(err)
75 | } else {
76 | tlsConf.Certificates = []tls.Certificate{tlsCert}
77 | }
78 | }
79 |
80 | func runAsControlGroup() {
81 | // create golang default https server
82 | server := &http.Server{
83 | Addr: *flagListenAddr,
84 | Handler: http.HandlerFunc(echoServer),
85 | TLSConfig: tlsConf,
86 | }
87 | go func() {
88 | <-ctx.Done()
89 | server.Shutdown(context.Background())
90 | }()
91 |
92 | // listen and serve
93 | log.Printf("server (benchmark control group) listening on %s", *flagListenAddr)
94 | err := server.ListenAndServeTLS("", "")
95 | log.Fatal(err)
96 | }
97 |
98 | func run() {
99 | // create proxyserver
100 | server := proxyserver.NewServer(ctx, http.HandlerFunc(echoServer), tlsConf)
101 |
102 | // start debug server if build tag `debug` is specified
103 | debug.StartDebugServer()
104 |
105 | // listen and serve
106 | log.Printf("server listening on %s", *flagListenAddr)
107 | err := server.ListenAndServe(*flagListenAddr)
108 | log.Fatal(err)
109 | }
110 |
--------------------------------------------------------------------------------
/example/echo-server/response.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/dreadl0ck/tlsx"
7 | "github.com/wi1dcard/fingerproxy/pkg/ja3"
8 | "github.com/wi1dcard/fingerproxy/pkg/ja4"
9 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
10 | )
11 |
12 | // echoResponse is the HTTP response struct of this echo server
13 | type echoResponse struct {
14 | Detail *detailResponse `json:"detail,omitempty"`
15 | JA3 string `json:"ja3"`
16 | JA4 string `json:"ja4"`
17 | HTTP2 string `json:"http2"`
18 |
19 | log *log.Logger
20 | }
21 |
22 | type detailResponse struct {
23 | Metadata *metadata.Metadata `json:"metadata"`
24 | UserAgent string `json:"user_agent"`
25 | JA3 *ja3Detail `json:"ja3"`
26 | JA3Raw string `json:"ja3_raw"`
27 | JA4 *ja4Detail `json:"ja4"`
28 | }
29 |
30 | func (r *echoResponse) fingerprintJA3() error {
31 | fp := &tlsx.ClientHelloBasic{}
32 | rd := r.Detail
33 | err := fp.Unmarshal(rd.Metadata.ClientHelloRecord)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | ja3Raw := ja3.Bare(fp)
39 |
40 | rd.JA3 = (*ja3Detail)(fp)
41 | rd.JA3Raw = string(ja3Raw)
42 | r.JA3 = ja3.BareToDigestHex(ja3Raw)
43 |
44 | r.logf("ja3: %s", r.JA3)
45 | return nil
46 | }
47 |
48 | func (r *echoResponse) fingerprintJA4() error {
49 | fp := &ja4.JA4Fingerprint{}
50 |
51 | err := fp.UnmarshalBytes(r.Detail.Metadata.ClientHelloRecord, 't')
52 | if err != nil {
53 | return err
54 | }
55 |
56 | r.Detail.JA4 = (*ja4Detail)(fp)
57 | r.JA4 = fp.String()
58 |
59 | r.logf("ja4: %s", r.JA4)
60 | return nil
61 | }
62 |
63 | func (r *echoResponse) fingerrpintHTTP2() {
64 | protocol := r.Detail.Metadata.ConnectionState.NegotiatedProtocol
65 | if protocol == "h2" {
66 | r.HTTP2 = r.Detail.Metadata.HTTP2Frames.String()
67 | r.logf("http2: %s", r.HTTP2)
68 | } else if *flagVerbose {
69 | r.logf("protocol is %s, skipping HTTP2 fingerprinting", protocol)
70 | }
71 | }
72 |
73 | func (r *echoResponse) logf(format string, args ...any) {
74 | if !*flagQuiet {
75 | r.log.Printf(format, args...)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/example/echo-server/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
11 | )
12 |
13 | func echoServer(w http.ResponseWriter, req *http.Request) {
14 | // create logger for this request, it outputs logs with client IP and port as prefix
15 | logger := log.New(os.Stdout, fmt.Sprintf("[client %s] ", req.RemoteAddr), log.LstdFlags|log.Lmsgprefix)
16 |
17 | // get metadata from request context
18 | data, ok := metadata.FromContext(req.Context())
19 | if !ok {
20 | logger.Printf("failed to get context")
21 | http.Error(w, "failed to get context", http.StatusInternalServerError)
22 | return
23 | }
24 |
25 | // prepare response
26 | response := &echoResponse{
27 | log: logger,
28 | Detail: &detailResponse{
29 | Metadata: data,
30 | UserAgent: req.UserAgent(),
31 | },
32 | }
33 |
34 | // calculate and add fingerprints to the response
35 | if err := response.fingerprintJA3(); err != nil {
36 | logger.Printf(err.Error())
37 | http.Error(w, err.Error(), http.StatusInternalServerError)
38 | return
39 | }
40 |
41 | if err := response.fingerprintJA4(); err != nil {
42 | logger.Printf(err.Error())
43 | http.Error(w, err.Error(), http.StatusInternalServerError)
44 | return
45 | }
46 |
47 | response.fingerrpintHTTP2()
48 |
49 | // print detail if -verbose is specified in CLI
50 | if *flagVerbose {
51 | detail, _ := json.Marshal(response.Detail)
52 | logger.Printf("detail: %s", detail)
53 | }
54 |
55 | // send HTTP response
56 | switch req.URL.Path {
57 | case "/json":
58 | w.Header().Set("Content-Type", "application/json")
59 | response.Detail = nil
60 | json.NewEncoder(w).Encode(response)
61 |
62 | case "/json/detail":
63 | w.Header().Set("Content-Type", "application/json")
64 | json.NewEncoder(w).Encode(response)
65 |
66 | default:
67 | fmt.Fprintf(w, "User-Agent: %s\n", response.Detail.UserAgent)
68 | fmt.Fprintf(w, "TLS ClientHello Record: %x\n", response.Detail.Metadata.ClientHelloRecord)
69 | fmt.Fprintf(w, "JA3 fingerprint: %s\n", response.JA3)
70 | fmt.Fprintf(w, "JA4 fingerprint: %s\n", response.JA4)
71 | fmt.Fprintf(w, "HTTP2 fingerprint: %s\n", response.HTTP2)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/example/http2-fingerprint-dos-poc/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "log"
10 | "net/http"
11 | "net/http/httptrace"
12 | "os"
13 | "strconv"
14 | "strings"
15 | "time"
16 |
17 | "github.com/wi1dcard/fingerproxy"
18 | "golang.org/x/net/http2"
19 | )
20 |
21 | /*
22 | This program demonstrates how to craft a large HTTP2 fingerprint.
23 |
24 | The HTTP2 fingerprint format suggested by Akamai is: "S[;]|WU|P[,]#|PS[,]", where
25 | all priority frames in HTTP2 request are recorded and shown in the third part. This
26 | gives attackers a chance to manually create a request with many priority frames
27 | and generate a large HTTP2 fingerprint. This program is to reproduce that.
28 |
29 | By design, Fingerproxy will send this large fingerprint through HTTP request headers
30 | to downstream. That might cause the backend server run out of resource while
31 | processing this large header. Therefore, a limit of max number of priority frames is
32 | introduced. With Fingerproxy binary, you can set the limit in CLI flag "-max-h2-priority-frames".
33 |
34 | See below example.
35 | */
36 |
37 | const numberOfPriorityFrames = 500
38 | const limitPriorityFrames = 20
39 |
40 | var isCI = "false"
41 |
42 | func main() {
43 | // fingerproxy no limit, header is long:
44 | // url := launchFingerproxy()
45 |
46 | // try with the limit:
47 | url := launchFingerproxyWithPriorityFramesLimit()
48 |
49 | // reproducable with other http2 fingerprinting services:
50 | // url := "https://tls.browserleaks.com/http2"
51 | // url := "https://tls.peet.ws/api/clean"
52 |
53 | time.Sleep(1 * time.Second)
54 | resp := sendRequest(url)
55 | log.Print(string(resp))
56 |
57 | // below is for CI
58 | if isCI == "true" {
59 | err := assertResponse(resp)
60 | if err != nil {
61 | log.Fatal(err)
62 | } else {
63 | log.Print("response asserted")
64 | }
65 | }
66 | }
67 |
68 | func launchFingerproxy() (url string) {
69 | os.Args = []string{os.Args[0], "-listen-addr=localhost:8443", "-forward-url=https://httpbin.org"}
70 | go fingerproxy.Run()
71 | return "https://localhost:8443/headers"
72 | }
73 |
74 | func launchFingerproxyWithPriorityFramesLimit() (url string) {
75 | os.Args = []string{os.Args[0], "-listen-addr=localhost:8443", "-forward-url=https://httpbin.org", "-max-h2-priority-frames", strconv.Itoa(limitPriorityFrames)}
76 | go fingerproxy.Run()
77 | return "https://localhost:8443/headers"
78 | }
79 |
80 | func sendRequest(url string) []byte {
81 | req, _ := http.NewRequest("GET", url, nil)
82 |
83 | trace := &httptrace.ClientTrace{
84 | GotConn: gotConn,
85 | }
86 | req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
87 |
88 | c := &http.Client{
89 | Transport: &http2.Transport{
90 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
91 | },
92 | }
93 |
94 | resp, err := c.Do(req)
95 |
96 | if err != nil {
97 | log.Fatal(err)
98 | }
99 |
100 | if b, err := io.ReadAll(resp.Body); err != nil {
101 | log.Fatal(err)
102 | return nil
103 | } else {
104 | return b
105 | }
106 | }
107 |
108 | func assertResponse(resp []byte) error {
109 | var parsed struct {
110 | Headers struct {
111 | XHTTP2Fingerprint string `json:"X-Http2-Fingerprint"`
112 | }
113 | }
114 |
115 | err := json.Unmarshal(resp, &parsed)
116 | if err != nil {
117 | return err
118 | }
119 |
120 | fp := parsed.Headers.XHTTP2Fingerprint
121 | parts := strings.Split(fp, "|")
122 | if len(parts) != 4 {
123 | return fmt.Errorf("incorrect http2 fingerprint format: %s", fp)
124 | }
125 |
126 | priorities := strings.Split(parts[2], ",")
127 | expect := limitPriorityFrames
128 | got := len(priorities)
129 | if got != expect {
130 | return fmt.Errorf("expect %d priority frames, got %d: %s", expect, got, parts[2])
131 | }
132 | return nil
133 | }
134 |
135 | func gotConn(info httptrace.GotConnInfo) {
136 | bw := bufio.NewWriter(info.Conn)
137 | br := bufio.NewReader(info.Conn)
138 | fr := http2.NewFramer(bw, br)
139 | for i := 1; i <= numberOfPriorityFrames; i++ {
140 | err := fr.WritePriority(uint32(i), http2.PriorityParam{Weight: 110})
141 | if err != nil {
142 | log.Fatal(err)
143 | }
144 | }
145 | bw.Flush()
146 | }
147 |
--------------------------------------------------------------------------------
/example/ja3-raw/README.md:
--------------------------------------------------------------------------------
1 | # Fingerproxy Example - JA3 Raw
2 |
3 | This example demonstrates passing the JA3 raw result (without final MD5 hashing) to the backend.
4 |
5 | ## Usage
6 |
7 | ```bash
8 | # Generate fake certificates tls.crt and tls.key
9 | openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
10 | -nodes -keyout tls.key -out tls.crt -subj "/CN=localhost" \
11 | -addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"
12 |
13 | # TLS server listens on :8443, forwarding requests to httpbin
14 | go run . -listen-addr :8443 -forward-url https://httpbin.org
15 |
16 | # Then test in another terminal
17 | curl "https://localhost:8443/headers" --insecure
18 | ```
19 |
20 | Output:
21 |
22 | ```yaml
23 | {
24 | "headers": {
25 | "Accept": "*/*",
26 | "Accept-Encoding": "gzip",
27 | "Host": "httpbin.org",
28 | "User-Agent": "curl/8.6.0",
29 | "X-Amzn-Trace-Id": "Root=1-664c0810-09f1e3a03376e930030b20f7",
30 | "X-Forwarded-Host": "localhost:8443",
31 | "X-Http2-Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
32 | "X-Ja3-Fingerprint": "0149f47eabf9a20d0893e2a44e5a6323",
33 | ## HEADER BELOW ##
34 | "X-Ja3-Raw-Fingerprint": "771,4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-16-22-23-49-13-43-45-51-21,29-23-30-25-24-256-257-258-259-260,0-1-2",
35 | ## HEADER ABOVE ##
36 | "X-Ja4-Fingerprint": "t13d3112h2_e8f1e7e78f70_6bebaf5329ac"
37 | }
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/example/ja3-raw/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/dreadl0ck/tlsx"
7 | "github.com/wi1dcard/fingerproxy"
8 | "github.com/wi1dcard/fingerproxy/pkg/fingerprint"
9 | "github.com/wi1dcard/fingerproxy/pkg/ja3"
10 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
11 | "github.com/wi1dcard/fingerproxy/pkg/reverseproxy"
12 | )
13 |
14 | func main() {
15 | fingerproxy.GetHeaderInjectors = func() []reverseproxy.HeaderInjector {
16 | i := fingerproxy.DefaultHeaderInjectors()
17 | i = append(i, fingerprint.NewFingerprintHeaderInjector(
18 | "X-JA3-Raw-Fingerprint",
19 | fpJA3Raw,
20 | ))
21 | return i
22 | }
23 | fingerproxy.Run()
24 | }
25 |
26 | func fpJA3Raw(data *metadata.Metadata) (string, error) {
27 | hellobasic := &tlsx.ClientHelloBasic{}
28 | if err := hellobasic.Unmarshal(data.ClientHelloRecord); err != nil {
29 | return "", fmt.Errorf("ja3: %w", err)
30 | }
31 |
32 | fp := string(ja3.Bare(hellobasic))
33 |
34 | return fp, nil
35 | }
36 |
--------------------------------------------------------------------------------
/example/ja3-sorted-extensions/README.md:
--------------------------------------------------------------------------------
1 | # Fingerproxy Example - JA3 Variant with Sorted Extensions
2 |
3 | JA3 is relatively old. The original implementation is outdated in certain use cases.
4 |
5 | For example, Google Chrome has a feature called [TLS ClientHello extension permutation](https://chromestatus.com/feature/5124606246518784). It permutes the set of TLS extensions sent in the ClientHello message, resulting in a different JA3 fingerprint with every new connection from the browser.
6 |
7 | Therefore we can no longer rely on the order of extensions. Sorting is necessary. Here is a very ugly example. It just demonstrates the possibility of extensibility of Fingerproxy. You might want to implement your own variant of JA3 fingerprint.
8 |
9 | ## Usage
10 |
11 | ```bash
12 | # Generate fake certificates tls.crt and tls.key
13 | openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
14 | -nodes -keyout tls.key -out tls.crt -subj "/CN=localhost" \
15 | -addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"
16 |
17 | # TLS server listens on :8443, forwarding requests to httpbin
18 | go run . -listen-addr :8443 -forward-url https://httpbin.org
19 |
20 | # Then test in another terminal
21 | curl "https://localhost:8443/headers" --insecure
22 | ```
23 |
24 | Output:
25 |
26 | ```yaml
27 | {
28 | "headers": {
29 | "Accept": "*/*",
30 | "Accept-Encoding": "gzip",
31 | "Host": "httpbin.org",
32 | "User-Agent": "curl/8.6.0",
33 | "X-Amzn-Trace-Id": "Root=1-664c0b9c-4f89ce9c411f2cf22acd59bb",
34 | "X-Forwarded-Host": "localhost:8443",
35 | "X-Http2-Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
36 | "X-Ja3-Fingerprint": "0149f47eabf9a20d0893e2a44e5a6323",
37 | "X-Ja4-Fingerprint": "t13d3112h2_e8f1e7e78f70_6bebaf5329ac",
38 | ## HERE ##
39 | "X-Sorted-Ja3-Fingerprint": "22441e3edb4a151c17462a438c7a10a5"
40 | }
41 | }
42 | ```
43 |
44 | Exit chrome and open again, you will see `X-Ja3-Fingerprint` changed but `X-Sorted-Ja3-Fingerprint` didn't.
45 |
46 | ## More Information
47 |
48 | -
49 | -
50 |
--------------------------------------------------------------------------------
/example/ja3-sorted-extensions/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "cmp"
5 | "fmt"
6 | "slices"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/dreadl0ck/tlsx"
11 | "github.com/wi1dcard/fingerproxy"
12 | "github.com/wi1dcard/fingerproxy/pkg/fingerprint"
13 | "github.com/wi1dcard/fingerproxy/pkg/ja3"
14 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
15 | "github.com/wi1dcard/fingerproxy/pkg/reverseproxy"
16 | )
17 |
18 | func main() {
19 | fingerproxy.GetHeaderInjectors = func() []reverseproxy.HeaderInjector {
20 | i := fingerproxy.DefaultHeaderInjectors()
21 | i = append(i, fingerprint.NewFingerprintHeaderInjector(
22 | "X-Sorted-JA3-Fingerprint",
23 | fpSortedJA3,
24 | ))
25 | return i
26 | }
27 | fingerproxy.Run()
28 | }
29 |
30 | func fpSortedJA3(data *metadata.Metadata) (string, error) {
31 | hellobasic := &tlsx.ClientHelloBasic{}
32 | if err := hellobasic.Unmarshal(data.ClientHelloRecord); err != nil {
33 | return "", fmt.Errorf("ja3: %w", err)
34 | }
35 |
36 | fp := string(ja3.Bare(hellobasic))
37 |
38 | fields := strings.Split(fp, ",")
39 | if len(fields) != 5 {
40 | // here should be impossible
41 | return "", fmt.Errorf("bad ja3 fingerprint")
42 | }
43 |
44 | extensions := strings.Split(fields[2], "-")
45 | if len(extensions) == 0 {
46 | // no tls extension
47 | return ja3.BareToDigestHex([]byte(fp)), nil
48 | }
49 |
50 | // very ugly implementations for demonstration purpose only
51 | slices.SortFunc(extensions, func(x string, y string) int {
52 | var _x, _y int
53 | var err error
54 | if _x, err = strconv.Atoi(x); err != nil {
55 | return 0
56 | }
57 | if _y, err = strconv.Atoi(y); err != nil {
58 | return 0
59 | }
60 | return cmp.Compare(_x, _y)
61 | })
62 |
63 | fields[2] = strings.Join(extensions, "-")
64 | fp = strings.Join(fields, ",")
65 |
66 | // return fp, nil
67 | return ja3.BareToDigestHex([]byte(fp)), nil
68 | }
69 |
--------------------------------------------------------------------------------
/example/my-fingerprint/README.md:
--------------------------------------------------------------------------------
1 | # Fingerproxy Example - My Fingerprint
2 |
3 | This is an example of implementing a customized fingerprint with TLS ClientHello.
4 |
5 | ## Usage
6 |
7 | ```bash
8 | # Generate fake certificates tls.crt and tls.key
9 | openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
10 | -nodes -keyout tls.key -out tls.crt -subj "/CN=localhost" \
11 | -addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"
12 |
13 | # TLS server listens on :8443, forwarding requests to httpbin
14 | go run . -listen-addr :8443 -forward-url https://httpbin.org
15 |
16 | # Then test in another terminal
17 | curl "https://localhost:8443/headers" --insecure
18 | ```
19 |
20 | Output:
21 |
22 | ```yaml
23 | {
24 | "headers": {
25 | "Accept": "*/*",
26 | "Accept-Encoding": "gzip",
27 | "Host": "httpbin.org",
28 | "User-Agent": "curl/8.6.0",
29 | "X-Amzn-Trace-Id": "Root=1-664c08ef-4efb8ea50d0d59181a2b1565",
30 | "X-Forwarded-Host": "localhost:8443",
31 | "X-Http2-Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
32 | "X-Ja3-Fingerprint": "0149f47eabf9a20d0893e2a44e5a6323",
33 | "X-Ja4-Fingerprint": "t13d3112h2_e8f1e7e78f70_6bebaf5329ac",
34 | ## HERE ##
35 | "X-My-Fingerprint": "alpn:h2,http/1.1|supported_versions:772,771"
36 | }
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/example/my-fingerprint/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/wi1dcard/fingerproxy"
9 | "github.com/wi1dcard/fingerproxy/pkg/fingerprint"
10 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
11 | "github.com/wi1dcard/fingerproxy/pkg/reverseproxy"
12 |
13 | utls "github.com/refraction-networking/utls"
14 | )
15 |
16 | func main() {
17 | fingerproxy.GetHeaderInjectors = func() []reverseproxy.HeaderInjector {
18 | i := fingerproxy.DefaultHeaderInjectors()
19 | i = append(i, fingerprint.NewFingerprintHeaderInjector(
20 | "X-My-Fingerprint",
21 | SimpleFingerprint,
22 | ))
23 | return i
24 | }
25 | fingerproxy.Run()
26 | }
27 |
28 | func SimpleFingerprint(data *metadata.Metadata) (string, error) {
29 | // parses client hello first
30 | chs := &utls.ClientHelloSpec{}
31 | err := chs.FromRaw(data.ClientHelloRecord, true, true)
32 | if err != nil {
33 | return "", fmt.Errorf("simple fingerprint: %w", err)
34 | }
35 |
36 | // prepare fingerprint buffer
37 | var buf strings.Builder
38 | for _, e := range chs.Extensions {
39 | var field string
40 | switch e := e.(type) {
41 | // use ALPN in the fingerprint
42 | case *utls.ALPNExtension:
43 | field = fmt.Sprintf("alpn:%s", joinAnything(e.AlpnProtocols, ","))
44 |
45 | // use TLS supported version in the fingerprint
46 | case *utls.SupportedVersionsExtension:
47 | sv := []string{}
48 | for _, v := range e.Versions {
49 | if isGREASEUint16(v) {
50 | sv = append(sv, "GREASE")
51 | } else {
52 | sv = append(sv, strconv.Itoa(int(v)))
53 | }
54 | }
55 | field = fmt.Sprintf("supported_versions:%s", joinAnything(sv, ","))
56 | }
57 |
58 | // case ...:
59 | // use any extension in your fingerprint
60 | // ...
61 |
62 | if field != "" {
63 | if buf.Len() != 0 {
64 | // add separator if needed
65 | buf.WriteString("|")
66 | }
67 | // add field
68 | buf.WriteString(field)
69 | }
70 | }
71 |
72 | // return fingerprint string
73 | return buf.String(), nil
74 | }
75 |
76 | func isGREASEUint16(v uint16) bool {
77 | // First byte is same as second byte
78 | // and lowest nibble is 0xa
79 | return ((v >> 8) == v&0xff) && v&0xf == 0xa
80 | }
81 |
82 | func joinAnything[E any](elems []E, sep string) string {
83 | var s strings.Builder
84 | s.WriteString(fmt.Sprint(elems[0]))
85 | for _, e := range elems[1:] {
86 | s.WriteString(sep)
87 | s.WriteString(fmt.Sprint(e))
88 | }
89 | return s.String()
90 | }
91 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/wi1dcard/fingerproxy
2 |
3 | go 1.21.7
4 |
5 | require (
6 | github.com/dreadl0ck/tlsx v1.0.1-google-gopacket
7 | github.com/google/gopacket v1.1.18
8 | github.com/prometheus/client_golang v1.18.0
9 | github.com/refraction-networking/utls v1.6.0
10 | golang.org/x/net v0.19.0
11 | golang.org/x/term v0.15.0
12 | gopkg.in/yaml.v3 v3.0.1
13 | )
14 |
15 | require (
16 | github.com/andybalholm/brotli v1.0.6 // indirect
17 | github.com/beorn7/perks v1.0.1 // indirect
18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
19 | github.com/cloudflare/circl v1.3.7 // indirect
20 | github.com/fsnotify/fsnotify v1.7.0 // indirect
21 | github.com/google/go-cmp v0.6.0 // indirect
22 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect
23 | github.com/klauspost/compress v1.17.4 // indirect
24 | github.com/kr/text v0.2.0 // indirect
25 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
26 | github.com/onsi/ginkgo/v2 v2.13.2 // indirect
27 | github.com/onsi/gomega v1.30.0 // indirect
28 | github.com/prometheus/client_model v0.5.0 // indirect
29 | github.com/prometheus/common v0.45.0 // indirect
30 | github.com/prometheus/procfs v0.12.0 // indirect
31 | github.com/quic-go/quic-go v0.40.1 // indirect
32 | golang.org/x/crypto v0.17.0 // indirect
33 | golang.org/x/sys v0.15.0 // indirect
34 | golang.org/x/text v0.14.0 // indirect
35 | golang.org/x/tools v0.16.1 // indirect
36 | google.golang.org/protobuf v1.31.0 // indirect
37 | )
38 |
--------------------------------------------------------------------------------
/pkg/README.md:
--------------------------------------------------------------------------------
1 | # Use Fingerproxy as a Library
2 |
3 | For the documentation, refer to [godoc](https://pkg.go.dev/github.com/wi1dcard/fingerproxy/pkg).
4 |
5 | There are some vendored packages:
6 |
7 | - Package `http2` is a fork of standard http2 package in [`x/net`](https://github.com/golang/net/tree/master/http2). Follow and sync upstream whenever you want using [./sync-http2-pkg.sh](./sync-http2-pkg.sh).
8 | - Package `ja3` is cloned from . See [./ja3/sync.sh](./ja3/sync.sh) for more info.
9 |
10 | If you want to use them, please import from the origin.
11 |
--------------------------------------------------------------------------------
/pkg/certwatcher/certwatcher.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2021 The Kubernetes Authors.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package certwatcher
18 |
19 | import (
20 | "context"
21 | "crypto/tls"
22 | "log"
23 | "sync"
24 |
25 | "github.com/fsnotify/fsnotify"
26 | )
27 |
28 | var (
29 | VerboseLogs bool
30 | Logger *log.Logger
31 | )
32 |
33 | func logf(format string, args ...any) {
34 | if Logger != nil {
35 | Logger.Printf(format, args...)
36 | } else {
37 | log.Printf(format, args...)
38 | }
39 | }
40 |
41 | func vlogf(format string, args ...any) {
42 | if VerboseLogs {
43 | logf(format, args...)
44 | }
45 | }
46 |
47 | // CertWatcher watches certificate and key files for changes. When either file
48 | // changes, it reads and parses both and calls an optional callback with the new
49 | // certificate.
50 | type CertWatcher struct {
51 | sync.RWMutex
52 |
53 | currentCert *tls.Certificate
54 | watcher *fsnotify.Watcher
55 |
56 | certPath string
57 | keyPath string
58 | }
59 |
60 | // New returns a new CertWatcher watching the given certificate and key.
61 | func New(certPath, keyPath string) (*CertWatcher, error) {
62 | var err error
63 |
64 | cw := &CertWatcher{
65 | certPath: certPath,
66 | keyPath: keyPath,
67 | }
68 |
69 | // Initial read of certificate and key.
70 | if err := cw.ReadCertificate(); err != nil {
71 | return nil, err
72 | }
73 |
74 | cw.watcher, err = fsnotify.NewWatcher()
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | return cw, nil
80 | }
81 |
82 | // GetCertificate fetches the currently loaded certificate, which may be nil.
83 | func (cw *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
84 | cw.RLock()
85 | defer cw.RUnlock()
86 | return cw.currentCert, nil
87 | }
88 |
89 | // Start starts the watch on the certificate and key files.
90 | func (cw *CertWatcher) Start(ctx context.Context) error {
91 | files := []string{cw.certPath, cw.keyPath}
92 |
93 | for _, f := range files {
94 | if err := cw.watcher.Add(f); err != nil {
95 | logf("error watching file: %s", err)
96 | return err
97 | }
98 | }
99 |
100 | go cw.Watch()
101 |
102 | // Block until the context is done.
103 | <-ctx.Done()
104 |
105 | return cw.watcher.Close()
106 | }
107 |
108 | // Watch reads events from the watcher's channel and reacts to changes.
109 | func (cw *CertWatcher) Watch() {
110 | for {
111 | select {
112 | case event, ok := <-cw.watcher.Events:
113 | // Channel is closed.
114 | if !ok {
115 | return
116 | }
117 |
118 | cw.handleEvent(event)
119 |
120 | case err, ok := <-cw.watcher.Errors:
121 | // Channel is closed.
122 | if !ok {
123 | return
124 | }
125 |
126 | logf("certificate watch error: %s", err)
127 | }
128 | }
129 | }
130 |
131 | // ReadCertificate reads the certificate and key files from disk, parses them,
132 | // and updates the current certificate on the watcher. If a callback is set, it
133 | // is invoked with the new certificate.
134 | func (cw *CertWatcher) ReadCertificate() error {
135 | cert, err := tls.LoadX509KeyPair(cw.certPath, cw.keyPath)
136 | if err != nil {
137 | return err
138 | }
139 |
140 | cw.Lock()
141 | cw.currentCert = &cert
142 | cw.Unlock()
143 |
144 | vlogf("updated current TLS certificate")
145 |
146 | return nil
147 | }
148 |
149 | func (cw *CertWatcher) handleEvent(event fsnotify.Event) {
150 | // Only care about events which may modify the contents of the file.
151 | if !(isWrite(event) || isRemove(event) || isCreate(event)) {
152 | return
153 | }
154 |
155 | vlogf("certificate event: %s", event)
156 |
157 | // If the file was removed, re-add the watch.
158 | if isRemove(event) {
159 | if err := cw.watcher.Add(event.Name); err != nil {
160 | logf("error re-watching file: %s", err)
161 | }
162 | }
163 |
164 | if err := cw.ReadCertificate(); err != nil {
165 | logf("error re-reading certificate: %s", err)
166 | }
167 | }
168 |
169 | func isWrite(event fsnotify.Event) bool {
170 | return event.Op&fsnotify.Write == fsnotify.Write
171 | }
172 |
173 | func isCreate(event fsnotify.Event) bool {
174 | return event.Op&fsnotify.Create == fsnotify.Create
175 | }
176 |
177 | func isRemove(event fsnotify.Event) bool {
178 | return event.Op&fsnotify.Remove == fsnotify.Remove
179 | }
180 |
--------------------------------------------------------------------------------
/pkg/debug/debug.go:
--------------------------------------------------------------------------------
1 | //go:build debug
2 |
3 | // Package debug is a debug server. It is enabled only
4 | // if the `debug` tag is used when building fingerproxy.
5 | package debug
6 |
7 | import (
8 | "fmt"
9 | "log"
10 | "net/http"
11 | "net/http/pprof"
12 | "runtime"
13 | "runtime/debug"
14 | )
15 |
16 | const listenAddr = "localhost:9036"
17 |
18 | // Start the debug server when build with build tag `debug`.
19 | func StartDebugServer() {
20 | mux := http.NewServeMux()
21 |
22 | mux.HandleFunc("/gc", func(w http.ResponseWriter, r *http.Request) {
23 | debug.FreeOSMemory()
24 | w.WriteHeader(http.StatusNoContent)
25 | })
26 |
27 | mux.HandleFunc("/mem", func(w http.ResponseWriter, r *http.Request) {
28 | var memStats runtime.MemStats
29 | runtime.ReadMemStats(&memStats)
30 |
31 | fmt.Fprintf(w, "Heap size: %.2f MiB\n", float64(memStats.HeapInuse)/1024/1024)
32 | fmt.Fprintf(w, "Stack size: %.2f MiB\n", float64(memStats.StackInuse)/1024/1024)
33 | fmt.Fprintf(w, "Num of goroutines: %d\n", runtime.NumGoroutine())
34 | })
35 |
36 | mux.HandleFunc("/debug/pprof/", pprof.Index)
37 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
38 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
39 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
40 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
41 |
42 | log.Printf("!!! DEBUG SERVER LISTENING ON %s !!!", listenAddr)
43 | go http.ListenAndServe(listenAddr, mux)
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/debug/debug_stub.go:
--------------------------------------------------------------------------------
1 | //go:build !debug
2 |
3 | package debug
4 |
5 | // Do nothing when build without build tag `debug`.
6 | func StartDebugServer() {}
7 |
--------------------------------------------------------------------------------
/pkg/fingerprint/fingerprint.go:
--------------------------------------------------------------------------------
1 | // Package `fingerprint` reads `metadata` and calculate the JA3, JA4,
2 | // HTTP2 fingerprints, etc.
3 | //
4 | // It also implements `header_injector` interface from package `reverseproxy`,
5 | // which allows passing fingerprints to the backend through the forwarding
6 | // request headers.
7 | package fingerprint
8 |
9 | import (
10 | "fmt"
11 | "log"
12 |
13 | "github.com/dreadl0ck/tlsx"
14 | "github.com/wi1dcard/fingerproxy/pkg/ja3"
15 | "github.com/wi1dcard/fingerproxy/pkg/ja4"
16 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
17 | )
18 |
19 | var (
20 | VerboseLogs bool
21 | Logger *log.Logger
22 | )
23 |
24 | func vlogf(format string, args ...any) {
25 | if VerboseLogs {
26 | if Logger != nil {
27 | Logger.Printf(format, args...)
28 | } else {
29 | log.Printf(format, args...)
30 | }
31 | }
32 | }
33 |
34 | // JA4Fingerprint is a FingerprintFunc
35 | func JA4Fingerprint(data *metadata.Metadata) (string, error) {
36 | fp := &ja4.JA4Fingerprint{}
37 | err := fp.UnmarshalBytes(data.ClientHelloRecord, 't') // TODO: identify connection protocol
38 | if err != nil {
39 | return "", fmt.Errorf("ja4: %w", err)
40 | }
41 |
42 | vlogf("ja4: %s", fp)
43 | return fp.String(), nil
44 | }
45 |
46 | // JA3Fingerprint is a FingerprintFunc
47 | func JA3Fingerprint(data *metadata.Metadata) (string, error) {
48 | hellobasic := &tlsx.ClientHelloBasic{}
49 | if err := hellobasic.Unmarshal(data.ClientHelloRecord); err != nil {
50 | return "", fmt.Errorf("ja3: %w", err)
51 | }
52 |
53 | fp := ja3.DigestHex(hellobasic)
54 | vlogf("ja3: %s", fp)
55 | return fp, nil
56 | }
57 |
58 | type HTTP2FingerprintParam struct {
59 | MaxPriorityFrames uint
60 | }
61 |
62 | // HTTP2Fingerprint is a FingerprintFunc, it creates Akamai HTTP2 fingerprints
63 | // as the suggested format: S[;]|WU|P[,]#|PS[,]
64 | func (p *HTTP2FingerprintParam) HTTP2Fingerprint(data *metadata.Metadata) (string, error) {
65 | if data.ConnectionState.NegotiatedProtocol == "h2" {
66 | fp := data.HTTP2Frames.Marshal(p.MaxPriorityFrames)
67 | vlogf("http2 fingerprint: %s", fp)
68 | return fp, nil
69 | }
70 |
71 | vlogf("%s connection, skipping HTTP2 fingerprinting", data.ConnectionState.NegotiatedProtocol)
72 | return "", nil
73 | }
74 |
--------------------------------------------------------------------------------
/pkg/fingerprint/header_injector.go:
--------------------------------------------------------------------------------
1 | package fingerprint
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/prometheus/client_golang/prometheus"
9 | "github.com/prometheus/client_golang/prometheus/promauto"
10 | "github.com/wi1dcard/fingerproxy/pkg/metadata"
11 | )
12 |
13 | const defaultMetricsPrefix = "fingerproxy"
14 |
15 | var (
16 | fingerprintDurationMetric *prometheus.HistogramVec
17 | )
18 |
19 | type FingerprintFunc func(*metadata.Metadata) (string, error)
20 |
21 | // FingerprintHeaderInjector implements reverseproxy.HeaderInjector
22 | type FingerprintHeaderInjector struct {
23 | HeaderName string
24 | FingerprintFunc FingerprintFunc
25 | FingerprintDurationSucceedMetric prometheus.Observer
26 | FingerprintDurationErrorMetric prometheus.Observer
27 | }
28 |
29 | func NewFingerprintHeaderInjector(headerName string, fingerprintFunc FingerprintFunc) *FingerprintHeaderInjector {
30 | i := &FingerprintHeaderInjector{
31 | HeaderName: headerName,
32 | FingerprintFunc: fingerprintFunc,
33 | }
34 |
35 | if fingerprintDurationMetric != nil {
36 | i.FingerprintDurationSucceedMetric = fingerprintDurationMetric.WithLabelValues("1", headerName)
37 | i.FingerprintDurationErrorMetric = fingerprintDurationMetric.WithLabelValues("0", headerName)
38 | }
39 |
40 | return i
41 | }
42 |
43 | func RegisterDurationMetric(registry *prometheus.Registry, buckets []float64, prefix string) {
44 | pm := promauto.With(registry)
45 |
46 | if prefix == "" {
47 | prefix = defaultMetricsPrefix
48 | }
49 |
50 | fingerprintDurationMetric = pm.NewHistogramVec(prometheus.HistogramOpts{
51 | Namespace: prefix,
52 | Name: "fingerprint_duration_seconds",
53 | Buckets: buckets,
54 | Help: "The duration of fingerprinting requests in seconds",
55 | }, []string{"ok", "header_name"})
56 | }
57 |
58 | func (i *FingerprintHeaderInjector) GetHeaderName() string {
59 | return i.HeaderName
60 | }
61 |
62 | func (i *FingerprintHeaderInjector) GetHeaderValue(req *http.Request) (string, error) {
63 | data, ok := metadata.FromContext(req.Context())
64 | if !ok {
65 | return "", fmt.Errorf("failed to get context")
66 | }
67 |
68 | start := time.Now()
69 | fp, err := i.FingerprintFunc(data)
70 | duration := time.Since(start)
71 | vlogf("fingerprint duration: %s", duration)
72 |
73 | if err == nil {
74 | if i.FingerprintDurationSucceedMetric != nil {
75 | i.FingerprintDurationSucceedMetric.Observe(duration.Seconds())
76 | }
77 | } else {
78 | if i.FingerprintDurationErrorMetric != nil {
79 | i.FingerprintDurationErrorMetric.Observe(duration.Seconds())
80 | }
81 | }
82 |
83 | return fp, err
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/hack/channel_listener.go:
--------------------------------------------------------------------------------
1 | package hack
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | )
8 |
9 | func NewChannelListener(ctx context.Context) *ChannelListener {
10 | ln := &ChannelListener{
11 | channel: make(chan net.Conn),
12 | }
13 | ln.context, ln.stop = context.WithCancel(ctx)
14 | return ln
15 | }
16 |
17 | type ChannelListener struct {
18 | channel chan net.Conn
19 | context context.Context
20 | stop context.CancelFunc
21 | }
22 |
23 | func (ln *ChannelListener) SendToChannel(conn net.Conn) {
24 | ln.channel <- conn
25 | }
26 |
27 | func (ln *ChannelListener) Accept() (net.Conn, error) {
28 | select {
29 | case <-ln.context.Done():
30 | return nil, ln.context.Err()
31 | case conn, ok := <-ln.channel:
32 | if ok {
33 | return conn, nil
34 | } else {
35 | return nil, fmt.Errorf("channel listener: internal channel closed")
36 | }
37 | }
38 | }
39 |
40 | func (ln *ChannelListener) Close() error {
41 | ln.stop()
42 | return nil
43 | }
44 |
45 | func (ln *ChannelListener) Addr() net.Addr { return nil }
46 |
--------------------------------------------------------------------------------
/pkg/hack/hajack_clienthello_conn.go:
--------------------------------------------------------------------------------
1 | // Package hack includes wraps and hacks of Go net stack.
2 | package hack
3 |
4 | import (
5 | "bytes"
6 | "crypto/tls"
7 | "errors"
8 | "fmt"
9 | "net"
10 | "time"
11 | )
12 |
13 | const (
14 | recordTypeHandshake = 0x16
15 | recordHeaderLen = 5
16 | )
17 |
18 | var (
19 | ErrIncompleteClientHello = errors.New("incomplete client hello")
20 | )
21 |
22 | type HijackClientHelloConn struct {
23 | // internal tls.Conn
24 | tlsConn net.Conn
25 |
26 | // client hello stored in buf
27 | buf bytes.Buffer
28 |
29 | // expected length of the TLS client hello record
30 | expectedLen uint16
31 |
32 | // verbose log func
33 | VerboseLogFunc func(string, ...any)
34 | }
35 |
36 | func NewHijackClientHelloConn(conn net.Conn) *HijackClientHelloConn {
37 | return &HijackClientHelloConn{
38 | tlsConn: conn,
39 | }
40 | }
41 |
42 | func (c *HijackClientHelloConn) Read(b []byte) (int, error) {
43 | n, err := c.tlsConn.Read(b)
44 | if err == nil {
45 | if c.hasCompleteClientHello() {
46 | c.vlogf("got %d bytes, but client hello is already mature, skipping hijack", n)
47 | } else {
48 | c.hijackClientHello(b[:n])
49 | }
50 | }
51 | return n, err
52 | }
53 |
54 | func (c *HijackClientHelloConn) hasCompleteClientHello() bool {
55 | bufLen := c.buf.Len()
56 | if bufLen == 0 || c.expectedLen == 0 {
57 | return false
58 | }
59 | if bufLen < int(c.expectedLen) {
60 | return false
61 | }
62 | if bufLen > int(c.expectedLen) {
63 | // if buffer content is longer than we need,
64 | // cut it to expected len
65 | c.buf.Truncate(int(c.expectedLen))
66 | c.vlogf("truncated buffer from %d to %d bytes", bufLen, c.expectedLen)
67 | }
68 | return true
69 | }
70 |
71 | func (c *HijackClientHelloConn) hijackClientHello(b []byte) {
72 | c.buf.Write(b)
73 | c.vlogf("wrote %d bytes, total %d bytes", len(b), c.buf.Len())
74 |
75 | // ignores the error which should be impossible
76 | _ = c.tryParseClientHello()
77 | }
78 |
79 | func (c *HijackClientHelloConn) tryParseClientHello() error {
80 | if c.hasCompleteClientHello() {
81 | c.vlogf("client hello is mature, skipping parse")
82 | return nil
83 | }
84 |
85 | bufBytes := c.buf.Bytes()
86 | bufLen := c.buf.Len()
87 | if bufLen < 5 {
88 | c.vlogf("buffer too short (%d bytes), skipping parse", bufLen)
89 | return ErrIncompleteClientHello
90 | }
91 |
92 | recType := bufBytes[0]
93 | if recType != recordTypeHandshake {
94 | return fmt.Errorf("tls record type 0x%x is not a handshake", recType)
95 | }
96 |
97 | vers := uint16(bufBytes[1])<<8 | uint16(bufBytes[2])
98 | if vers < tls.VersionSSL30 || vers > tls.VersionTLS13 {
99 | return fmt.Errorf("unknown tls version: 0x%x", vers)
100 | }
101 |
102 | handshakeLen := uint16(bufBytes[3])<<8 | uint16(bufBytes[4])
103 | c.expectedLen = recordHeaderLen + handshakeLen
104 |
105 | // call hasCompleteClientHello to truncate the buffer if possible
106 | if c.hasCompleteClientHello() {
107 | c.vlogf("client hello is mature after got record length")
108 | return nil
109 | } else {
110 | return ErrIncompleteClientHello
111 | }
112 | }
113 |
114 | func (c *HijackClientHelloConn) GetClientHello() ([]byte, error) {
115 | if err := c.tryParseClientHello(); err != nil {
116 | return nil, err
117 | }
118 | return c.buf.Bytes(), nil
119 | }
120 |
121 | func (c *HijackClientHelloConn) vlogf(format string, args ...any) {
122 | if c.VerboseLogFunc != nil {
123 | c.VerboseLogFunc(format, args...)
124 | }
125 | }
126 |
127 | /*
128 | implement net.Conn begin ...
129 | */
130 |
131 | func (c *HijackClientHelloConn) Write(b []byte) (n int, err error) { return c.tlsConn.Write(b) }
132 | func (c *HijackClientHelloConn) Close() error { return c.tlsConn.Close() }
133 | func (c *HijackClientHelloConn) LocalAddr() net.Addr { return c.tlsConn.LocalAddr() }
134 | func (c *HijackClientHelloConn) RemoteAddr() net.Addr { return c.tlsConn.RemoteAddr() }
135 | func (c *HijackClientHelloConn) SetDeadline(t time.Time) error { return c.tlsConn.SetDeadline(t) }
136 | func (c *HijackClientHelloConn) SetReadDeadline(t time.Time) error {
137 | return c.tlsConn.SetReadDeadline(t)
138 | }
139 | func (c *HijackClientHelloConn) SetWriteDeadline(t time.Time) error {
140 | return c.tlsConn.SetWriteDeadline(t)
141 | }
142 |
143 | /*
144 | ... implement net.Conn end
145 | */
146 |
--------------------------------------------------------------------------------
/pkg/hack/tls_clienthello_conn.go:
--------------------------------------------------------------------------------
1 | package hack
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "net"
7 | "time"
8 | )
9 |
10 | type TLSClientHelloConn struct {
11 | Conn *tls.Conn
12 | ClientHelloRecord []byte
13 | Done context.CancelFunc
14 | }
15 |
16 | func (c *TLSClientHelloConn) Read(b []byte) (n int, err error) { return c.Conn.Read(b) }
17 | func (c *TLSClientHelloConn) Write(b []byte) (n int, err error) { return c.Conn.Write(b) }
18 | func (c *TLSClientHelloConn) Close() error {
19 | c.Done()
20 | return c.Conn.Close()
21 | }
22 | func (c *TLSClientHelloConn) LocalAddr() net.Addr { return c.Conn.LocalAddr() }
23 | func (c *TLSClientHelloConn) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() }
24 | func (c *TLSClientHelloConn) SetDeadline(t time.Time) error { return c.Conn.SetDeadline(t) }
25 | func (c *TLSClientHelloConn) SetReadDeadline(t time.Time) error { return c.Conn.SetReadDeadline(t) }
26 | func (c *TLSClientHelloConn) SetWriteDeadline(t time.Time) error { return c.Conn.SetWriteDeadline(t) }
27 |
--------------------------------------------------------------------------------
/pkg/http2/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | h2i/h2i
3 |
--------------------------------------------------------------------------------
/pkg/http2/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2009 The Go Authors.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google LLC nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/pkg/http2/ascii.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import "strings"
8 |
9 | // The HTTP protocols are defined in terms of ASCII, not Unicode. This file
10 | // contains helper functions which may use Unicode-aware functions which would
11 | // otherwise be unsafe and could introduce vulnerabilities if used improperly.
12 |
13 | // asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t
14 | // are equal, ASCII-case-insensitively.
15 | func asciiEqualFold(s, t string) bool {
16 | if len(s) != len(t) {
17 | return false
18 | }
19 | for i := 0; i < len(s); i++ {
20 | if lower(s[i]) != lower(t[i]) {
21 | return false
22 | }
23 | }
24 | return true
25 | }
26 |
27 | // lower returns the ASCII lowercase version of b.
28 | func lower(b byte) byte {
29 | if 'A' <= b && b <= 'Z' {
30 | return b + ('a' - 'A')
31 | }
32 | return b
33 | }
34 |
35 | // isASCIIPrint returns whether s is ASCII and printable according to
36 | // https://tools.ietf.org/html/rfc20#section-4.2.
37 | func isASCIIPrint(s string) bool {
38 | for i := 0; i < len(s); i++ {
39 | if s[i] < ' ' || s[i] > '~' {
40 | return false
41 | }
42 | }
43 | return true
44 | }
45 |
46 | // asciiToLower returns the lowercase version of s if s is ASCII and printable,
47 | // and whether or not it was.
48 | func asciiToLower(s string) (lower string, ok bool) {
49 | if !isASCIIPrint(s) {
50 | return "", false
51 | }
52 | return strings.ToLower(s), true
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/http2/ascii_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import "testing"
8 |
9 | func TestASCIIEqualFold(t *testing.T) {
10 | var tests = []struct {
11 | name string
12 | a, b string
13 | want bool
14 | }{
15 | {
16 | name: "empty",
17 | want: true,
18 | },
19 | {
20 | name: "simple match",
21 | a: "CHUNKED",
22 | b: "chunked",
23 | want: true,
24 | },
25 | {
26 | name: "same string",
27 | a: "chunked",
28 | b: "chunked",
29 | want: true,
30 | },
31 | {
32 | name: "Unicode Kelvin symbol",
33 | a: "chunKed", // This "K" is 'KELVIN SIGN' (\u212A)
34 | b: "chunked",
35 | want: false,
36 | },
37 | }
38 | for _, tt := range tests {
39 | t.Run(tt.name, func(t *testing.T) {
40 | if got := asciiEqualFold(tt.a, tt.b); got != tt.want {
41 | t.Errorf("AsciiEqualFold(%q,%q): got %v want %v", tt.a, tt.b, got, tt.want)
42 | }
43 | })
44 | }
45 | }
46 |
47 | func TestIsASCIIPrint(t *testing.T) {
48 | var tests = []struct {
49 | name string
50 | in string
51 | want bool
52 | }{
53 | {
54 | name: "empty",
55 | want: true,
56 | },
57 | {
58 | name: "ASCII low",
59 | in: "This is a space: ' '",
60 | want: true,
61 | },
62 | {
63 | name: "ASCII high",
64 | in: "This is a tilde: '~'",
65 | want: true,
66 | },
67 | {
68 | name: "ASCII low non-print",
69 | in: "This is a unit separator: \x1F",
70 | want: false,
71 | },
72 | {
73 | name: "Ascii high non-print",
74 | in: "This is a Delete: \x7F",
75 | want: false,
76 | },
77 | {
78 | name: "Unicode letter",
79 | in: "Today it's 280K outside: it's freezing!", // This "K" is 'KELVIN SIGN' (\u212A)
80 | want: false,
81 | },
82 | {
83 | name: "Unicode emoji",
84 | in: "Gophers like 🧀",
85 | want: false,
86 | },
87 | }
88 | for _, tt := range tests {
89 | t.Run(tt.name, func(t *testing.T) {
90 | if got := isASCIIPrint(tt.in); got != tt.want {
91 | t.Errorf("IsASCIIPrint(%q): got %v want %v", tt.in, got, tt.want)
92 | }
93 | })
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/http2/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "math"
9 | "net/http"
10 | "time"
11 | )
12 |
13 | // http2Config is a package-internal version of net/http.HTTP2Config.
14 | //
15 | // http.HTTP2Config was added in Go 1.24.
16 | // When running with a version of net/http that includes HTTP2Config,
17 | // we merge the configuration with the fields in Transport or Server
18 | // to produce an http2Config.
19 | //
20 | // Zero valued fields in http2Config are interpreted as in the
21 | // net/http.HTTPConfig documentation.
22 | //
23 | // Precedence order for reconciling configurations is:
24 | //
25 | // - Use the net/http.{Server,Transport}.HTTP2Config value, when non-zero.
26 | // - Otherwise use the http2.{Server.Transport} value.
27 | // - If the resulting value is zero or out of range, use a default.
28 | type http2Config struct {
29 | MaxConcurrentStreams uint32
30 | MaxDecoderHeaderTableSize uint32
31 | MaxEncoderHeaderTableSize uint32
32 | MaxReadFrameSize uint32
33 | MaxUploadBufferPerConnection int32
34 | MaxUploadBufferPerStream int32
35 | SendPingTimeout time.Duration
36 | PingTimeout time.Duration
37 | WriteByteTimeout time.Duration
38 | PermitProhibitedCipherSuites bool
39 | CountError func(errType string)
40 | }
41 |
42 | // configFromServer merges configuration settings from
43 | // net/http.Server.HTTP2Config and http2.Server.
44 | func configFromServer(h1 *http.Server, h2 *Server) http2Config {
45 | conf := http2Config{
46 | MaxConcurrentStreams: h2.MaxConcurrentStreams,
47 | MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
48 | MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
49 | MaxReadFrameSize: h2.MaxReadFrameSize,
50 | MaxUploadBufferPerConnection: h2.MaxUploadBufferPerConnection,
51 | MaxUploadBufferPerStream: h2.MaxUploadBufferPerStream,
52 | SendPingTimeout: h2.ReadIdleTimeout,
53 | PingTimeout: h2.PingTimeout,
54 | WriteByteTimeout: h2.WriteByteTimeout,
55 | PermitProhibitedCipherSuites: h2.PermitProhibitedCipherSuites,
56 | CountError: h2.CountError,
57 | }
58 | fillNetHTTPServerConfig(&conf, h1)
59 | setConfigDefaults(&conf, true)
60 | return conf
61 | }
62 |
63 | // configFromServer merges configuration settings from h2 and h2.t1.HTTP2
64 | // (the net/http Transport).
65 | func configFromTransport(h2 *Transport) http2Config {
66 | conf := http2Config{
67 | MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
68 | MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
69 | MaxReadFrameSize: h2.MaxReadFrameSize,
70 | SendPingTimeout: h2.ReadIdleTimeout,
71 | PingTimeout: h2.PingTimeout,
72 | WriteByteTimeout: h2.WriteByteTimeout,
73 | }
74 |
75 | // Unlike most config fields, where out-of-range values revert to the default,
76 | // Transport.MaxReadFrameSize clips.
77 | if conf.MaxReadFrameSize < minMaxFrameSize {
78 | conf.MaxReadFrameSize = minMaxFrameSize
79 | } else if conf.MaxReadFrameSize > maxFrameSize {
80 | conf.MaxReadFrameSize = maxFrameSize
81 | }
82 |
83 | if h2.t1 != nil {
84 | fillNetHTTPTransportConfig(&conf, h2.t1)
85 | }
86 | setConfigDefaults(&conf, false)
87 | return conf
88 | }
89 |
90 | func setDefault[T ~int | ~int32 | ~uint32 | ~int64](v *T, minval, maxval, defval T) {
91 | if *v < minval || *v > maxval {
92 | *v = defval
93 | }
94 | }
95 |
96 | func setConfigDefaults(conf *http2Config, server bool) {
97 | setDefault(&conf.MaxConcurrentStreams, 1, math.MaxUint32, defaultMaxStreams)
98 | setDefault(&conf.MaxEncoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
99 | setDefault(&conf.MaxDecoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
100 | if server {
101 | setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, 1<<20)
102 | } else {
103 | setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, transportDefaultConnFlow)
104 | }
105 | if server {
106 | setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, 1<<20)
107 | } else {
108 | setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, transportDefaultStreamFlow)
109 | }
110 | setDefault(&conf.MaxReadFrameSize, minMaxFrameSize, maxFrameSize, defaultMaxReadFrameSize)
111 | setDefault(&conf.PingTimeout, 1, math.MaxInt64, 15*time.Second)
112 | }
113 |
114 | // adjustHTTP1MaxHeaderSize converts a limit in bytes on the size of an HTTP/1 header
115 | // to an HTTP/2 MAX_HEADER_LIST_SIZE value.
116 | func adjustHTTP1MaxHeaderSize(n int64) int64 {
117 | // http2's count is in a slightly different unit and includes 32 bytes per pair.
118 | // So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
119 | const perFieldOverhead = 32 // per http2 spec
120 | const typicalHeaders = 10 // conservative
121 | return n + typicalHeaders*perFieldOverhead
122 | }
123 |
--------------------------------------------------------------------------------
/pkg/http2/config_go124.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | //go:build go1.24
6 |
7 | package http2
8 |
9 | import "net/http"
10 |
11 | // fillNetHTTPServerConfig sets fields in conf from srv.HTTP2.
12 | func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {
13 | fillNetHTTPConfig(conf, srv.HTTP2)
14 | }
15 |
16 | // fillNetHTTPServerConfig sets fields in conf from tr.HTTP2.
17 | func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {
18 | fillNetHTTPConfig(conf, tr.HTTP2)
19 | }
20 |
21 | func fillNetHTTPConfig(conf *http2Config, h2 *http.HTTP2Config) {
22 | if h2 == nil {
23 | return
24 | }
25 | if h2.MaxConcurrentStreams != 0 {
26 | conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
27 | }
28 | if h2.MaxEncoderHeaderTableSize != 0 {
29 | conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize)
30 | }
31 | if h2.MaxDecoderHeaderTableSize != 0 {
32 | conf.MaxDecoderHeaderTableSize = uint32(h2.MaxDecoderHeaderTableSize)
33 | }
34 | if h2.MaxConcurrentStreams != 0 {
35 | conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
36 | }
37 | if h2.MaxReadFrameSize != 0 {
38 | conf.MaxReadFrameSize = uint32(h2.MaxReadFrameSize)
39 | }
40 | if h2.MaxReceiveBufferPerConnection != 0 {
41 | conf.MaxUploadBufferPerConnection = int32(h2.MaxReceiveBufferPerConnection)
42 | }
43 | if h2.MaxReceiveBufferPerStream != 0 {
44 | conf.MaxUploadBufferPerStream = int32(h2.MaxReceiveBufferPerStream)
45 | }
46 | if h2.SendPingTimeout != 0 {
47 | conf.SendPingTimeout = h2.SendPingTimeout
48 | }
49 | if h2.PingTimeout != 0 {
50 | conf.PingTimeout = h2.PingTimeout
51 | }
52 | if h2.WriteByteTimeout != 0 {
53 | conf.WriteByteTimeout = h2.WriteByteTimeout
54 | }
55 | if h2.PermitProhibitedCipherSuites {
56 | conf.PermitProhibitedCipherSuites = true
57 | }
58 | if h2.CountError != nil {
59 | conf.CountError = h2.CountError
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/http2/config_pre_go124.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | //go:build !go1.24
6 |
7 | package http2
8 |
9 | import "net/http"
10 |
11 | // Pre-Go 1.24 fallback.
12 | // The Server.HTTP2 and Transport.HTTP2 config fields were added in Go 1.24.
13 |
14 | func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {}
15 |
16 | func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {}
17 |
--------------------------------------------------------------------------------
/pkg/http2/config_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | //go:build go1.24
6 |
7 | package http2
8 |
9 | import (
10 | "net/http"
11 | "testing"
12 | "time"
13 | )
14 |
15 | func TestConfigServerSettings(t *testing.T) {
16 | config := &http.HTTP2Config{
17 | MaxConcurrentStreams: 1,
18 | MaxDecoderHeaderTableSize: 1<<20 + 2,
19 | MaxEncoderHeaderTableSize: 1<<20 + 3,
20 | MaxReadFrameSize: 1<<20 + 4,
21 | MaxReceiveBufferPerConnection: 64<<10 + 5,
22 | MaxReceiveBufferPerStream: 64<<10 + 6,
23 | }
24 | const maxHeaderBytes = 4096 + 7
25 | st := newServerTester(t, nil, func(s *http.Server) {
26 | s.MaxHeaderBytes = maxHeaderBytes
27 | s.HTTP2 = config
28 | })
29 | st.writePreface()
30 | st.writeSettings()
31 | st.wantSettings(map[SettingID]uint32{
32 | SettingMaxConcurrentStreams: uint32(config.MaxConcurrentStreams),
33 | SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
34 | SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
35 | SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
36 | SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
37 | })
38 | }
39 |
40 | func TestConfigTransportSettings(t *testing.T) {
41 | config := &http.HTTP2Config{
42 | MaxConcurrentStreams: 1, // ignored by Transport
43 | MaxDecoderHeaderTableSize: 1<<20 + 2,
44 | MaxEncoderHeaderTableSize: 1<<20 + 3,
45 | MaxReadFrameSize: 1<<20 + 4,
46 | MaxReceiveBufferPerConnection: 64<<10 + 5,
47 | MaxReceiveBufferPerStream: 64<<10 + 6,
48 | }
49 | const maxHeaderBytes = 4096 + 7
50 | tc := newTestClientConn(t, func(tr *http.Transport) {
51 | tr.HTTP2 = config
52 | tr.MaxResponseHeaderBytes = maxHeaderBytes
53 | })
54 | tc.wantSettings(map[SettingID]uint32{
55 | SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
56 | SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
57 | SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
58 | SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
59 | })
60 | tc.wantWindowUpdate(0, uint32(config.MaxReceiveBufferPerConnection))
61 | }
62 |
63 | func TestConfigPingTimeoutServer(t *testing.T) {
64 | st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
65 | }, func(s *Server) {
66 | s.ReadIdleTimeout = 2 * time.Second
67 | s.PingTimeout = 3 * time.Second
68 | })
69 | st.greet()
70 |
71 | st.advance(2 * time.Second)
72 | _ = readFrame[*PingFrame](t, st)
73 | st.advance(3 * time.Second)
74 | st.wantClosed()
75 | }
76 |
77 | func TestConfigPingTimeoutTransport(t *testing.T) {
78 | tc := newTestClientConn(t, func(tr *Transport) {
79 | tr.ReadIdleTimeout = 2 * time.Second
80 | tr.PingTimeout = 3 * time.Second
81 | })
82 | tc.greet()
83 |
84 | req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
85 | rt := tc.roundTrip(req)
86 | tc.wantFrameType(FrameHeaders)
87 |
88 | tc.advance(2 * time.Second)
89 | tc.wantFrameType(FramePing)
90 | tc.advance(3 * time.Second)
91 | err := rt.err()
92 | if err == nil {
93 | t.Fatalf("expected connection to close")
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/http2/databuffer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "errors"
9 | "fmt"
10 | "sync"
11 | )
12 |
13 | // Buffer chunks are allocated from a pool to reduce pressure on GC.
14 | // The maximum wasted space per dataBuffer is 2x the largest size class,
15 | // which happens when the dataBuffer has multiple chunks and there is
16 | // one unread byte in both the first and last chunks. We use a few size
17 | // classes to minimize overheads for servers that typically receive very
18 | // small request bodies.
19 | //
20 | // TODO: Benchmark to determine if the pools are necessary. The GC may have
21 | // improved enough that we can instead allocate chunks like this:
22 | // make([]byte, max(16<<10, expectedBytesRemaining))
23 | var dataChunkPools = [...]sync.Pool{
24 | {New: func() interface{} { return new([1 << 10]byte) }},
25 | {New: func() interface{} { return new([2 << 10]byte) }},
26 | {New: func() interface{} { return new([4 << 10]byte) }},
27 | {New: func() interface{} { return new([8 << 10]byte) }},
28 | {New: func() interface{} { return new([16 << 10]byte) }},
29 | }
30 |
31 | func getDataBufferChunk(size int64) []byte {
32 | switch {
33 | case size <= 1<<10:
34 | return dataChunkPools[0].Get().(*[1 << 10]byte)[:]
35 | case size <= 2<<10:
36 | return dataChunkPools[1].Get().(*[2 << 10]byte)[:]
37 | case size <= 4<<10:
38 | return dataChunkPools[2].Get().(*[4 << 10]byte)[:]
39 | case size <= 8<<10:
40 | return dataChunkPools[3].Get().(*[8 << 10]byte)[:]
41 | default:
42 | return dataChunkPools[4].Get().(*[16 << 10]byte)[:]
43 | }
44 | }
45 |
46 | func putDataBufferChunk(p []byte) {
47 | switch len(p) {
48 | case 1 << 10:
49 | dataChunkPools[0].Put((*[1 << 10]byte)(p))
50 | case 2 << 10:
51 | dataChunkPools[1].Put((*[2 << 10]byte)(p))
52 | case 4 << 10:
53 | dataChunkPools[2].Put((*[4 << 10]byte)(p))
54 | case 8 << 10:
55 | dataChunkPools[3].Put((*[8 << 10]byte)(p))
56 | case 16 << 10:
57 | dataChunkPools[4].Put((*[16 << 10]byte)(p))
58 | default:
59 | panic(fmt.Sprintf("unexpected buffer len=%v", len(p)))
60 | }
61 | }
62 |
63 | // dataBuffer is an io.ReadWriter backed by a list of data chunks.
64 | // Each dataBuffer is used to read DATA frames on a single stream.
65 | // The buffer is divided into chunks so the server can limit the
66 | // total memory used by a single connection without limiting the
67 | // request body size on any single stream.
68 | type dataBuffer struct {
69 | chunks [][]byte
70 | r int // next byte to read is chunks[0][r]
71 | w int // next byte to write is chunks[len(chunks)-1][w]
72 | size int // total buffered bytes
73 | expected int64 // we expect at least this many bytes in future Write calls (ignored if <= 0)
74 | }
75 |
76 | var errReadEmpty = errors.New("read from empty dataBuffer")
77 |
78 | // Read copies bytes from the buffer into p.
79 | // It is an error to read when no data is available.
80 | func (b *dataBuffer) Read(p []byte) (int, error) {
81 | if b.size == 0 {
82 | return 0, errReadEmpty
83 | }
84 | var ntotal int
85 | for len(p) > 0 && b.size > 0 {
86 | readFrom := b.bytesFromFirstChunk()
87 | n := copy(p, readFrom)
88 | p = p[n:]
89 | ntotal += n
90 | b.r += n
91 | b.size -= n
92 | // If the first chunk has been consumed, advance to the next chunk.
93 | if b.r == len(b.chunks[0]) {
94 | putDataBufferChunk(b.chunks[0])
95 | end := len(b.chunks) - 1
96 | copy(b.chunks[:end], b.chunks[1:])
97 | b.chunks[end] = nil
98 | b.chunks = b.chunks[:end]
99 | b.r = 0
100 | }
101 | }
102 | return ntotal, nil
103 | }
104 |
105 | func (b *dataBuffer) bytesFromFirstChunk() []byte {
106 | if len(b.chunks) == 1 {
107 | return b.chunks[0][b.r:b.w]
108 | }
109 | return b.chunks[0][b.r:]
110 | }
111 |
112 | // Len returns the number of bytes of the unread portion of the buffer.
113 | func (b *dataBuffer) Len() int {
114 | return b.size
115 | }
116 |
117 | // Write appends p to the buffer.
118 | func (b *dataBuffer) Write(p []byte) (int, error) {
119 | ntotal := len(p)
120 | for len(p) > 0 {
121 | // If the last chunk is empty, allocate a new chunk. Try to allocate
122 | // enough to fully copy p plus any additional bytes we expect to
123 | // receive. However, this may allocate less than len(p).
124 | want := int64(len(p))
125 | if b.expected > want {
126 | want = b.expected
127 | }
128 | chunk := b.lastChunkOrAlloc(want)
129 | n := copy(chunk[b.w:], p)
130 | p = p[n:]
131 | b.w += n
132 | b.size += n
133 | b.expected -= int64(n)
134 | }
135 | return ntotal, nil
136 | }
137 |
138 | func (b *dataBuffer) lastChunkOrAlloc(want int64) []byte {
139 | if len(b.chunks) != 0 {
140 | last := b.chunks[len(b.chunks)-1]
141 | if b.w < len(last) {
142 | return last
143 | }
144 | }
145 | chunk := getDataBufferChunk(want)
146 | b.chunks = append(b.chunks, chunk)
147 | b.w = 0
148 | return chunk
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/http2/databuffer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2017 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "bytes"
9 | "fmt"
10 | "reflect"
11 | "testing"
12 | )
13 |
14 | func fmtDataChunk(chunk []byte) string {
15 | out := ""
16 | var last byte
17 | var count int
18 | for _, c := range chunk {
19 | if c != last {
20 | if count > 0 {
21 | out += fmt.Sprintf(" x %d ", count)
22 | count = 0
23 | }
24 | out += string([]byte{c})
25 | last = c
26 | }
27 | count++
28 | }
29 | if count > 0 {
30 | out += fmt.Sprintf(" x %d", count)
31 | }
32 | return out
33 | }
34 |
35 | func fmtDataChunks(chunks [][]byte) string {
36 | var out string
37 | for _, chunk := range chunks {
38 | out += fmt.Sprintf("{%q}", fmtDataChunk(chunk))
39 | }
40 | return out
41 | }
42 |
43 | func testDataBuffer(t *testing.T, wantBytes []byte, setup func(t *testing.T) *dataBuffer) {
44 | // Run setup, then read the remaining bytes from the dataBuffer and check
45 | // that they match wantBytes. We use different read sizes to check corner
46 | // cases in Read.
47 | for _, readSize := range []int{1, 2, 1 * 1024, 32 * 1024} {
48 | t.Run(fmt.Sprintf("ReadSize=%d", readSize), func(t *testing.T) {
49 | b := setup(t)
50 | buf := make([]byte, readSize)
51 | var gotRead bytes.Buffer
52 | for {
53 | n, err := b.Read(buf)
54 | gotRead.Write(buf[:n])
55 | if err == errReadEmpty {
56 | break
57 | }
58 | if err != nil {
59 | t.Fatalf("error after %v bytes: %v", gotRead.Len(), err)
60 | }
61 | }
62 | if got, want := gotRead.Bytes(), wantBytes; !bytes.Equal(got, want) {
63 | t.Errorf("FinalRead=%q, want %q", fmtDataChunk(got), fmtDataChunk(want))
64 | }
65 | })
66 | }
67 | }
68 |
69 | func TestDataBufferAllocation(t *testing.T) {
70 | writes := [][]byte{
71 | bytes.Repeat([]byte("a"), 1*1024-1),
72 | []byte("a"),
73 | bytes.Repeat([]byte("b"), 4*1024-1),
74 | []byte("b"),
75 | bytes.Repeat([]byte("c"), 8*1024-1),
76 | []byte("c"),
77 | bytes.Repeat([]byte("d"), 16*1024-1),
78 | []byte("d"),
79 | bytes.Repeat([]byte("e"), 32*1024),
80 | }
81 | var wantRead bytes.Buffer
82 | for _, p := range writes {
83 | wantRead.Write(p)
84 | }
85 |
86 | testDataBuffer(t, wantRead.Bytes(), func(t *testing.T) *dataBuffer {
87 | b := &dataBuffer{}
88 | for _, p := range writes {
89 | if n, err := b.Write(p); n != len(p) || err != nil {
90 | t.Fatalf("Write(%q x %d)=%v,%v want %v,nil", p[:1], len(p), n, err, len(p))
91 | }
92 | }
93 | want := [][]byte{
94 | bytes.Repeat([]byte("a"), 1*1024),
95 | bytes.Repeat([]byte("b"), 4*1024),
96 | bytes.Repeat([]byte("c"), 8*1024),
97 | bytes.Repeat([]byte("d"), 16*1024),
98 | bytes.Repeat([]byte("e"), 16*1024),
99 | bytes.Repeat([]byte("e"), 16*1024),
100 | }
101 | if !reflect.DeepEqual(b.chunks, want) {
102 | t.Errorf("dataBuffer.chunks\ngot: %s\nwant: %s", fmtDataChunks(b.chunks), fmtDataChunks(want))
103 | }
104 | return b
105 | })
106 | }
107 |
108 | func TestDataBufferAllocationWithExpected(t *testing.T) {
109 | writes := [][]byte{
110 | bytes.Repeat([]byte("a"), 1*1024), // allocates 16KB
111 | bytes.Repeat([]byte("b"), 14*1024),
112 | bytes.Repeat([]byte("c"), 15*1024), // allocates 16KB more
113 | bytes.Repeat([]byte("d"), 2*1024),
114 | bytes.Repeat([]byte("e"), 1*1024), // overflows 32KB expectation, allocates just 1KB
115 | }
116 | var wantRead bytes.Buffer
117 | for _, p := range writes {
118 | wantRead.Write(p)
119 | }
120 |
121 | testDataBuffer(t, wantRead.Bytes(), func(t *testing.T) *dataBuffer {
122 | b := &dataBuffer{expected: 32 * 1024}
123 | for _, p := range writes {
124 | if n, err := b.Write(p); n != len(p) || err != nil {
125 | t.Fatalf("Write(%q x %d)=%v,%v want %v,nil", p[:1], len(p), n, err, len(p))
126 | }
127 | }
128 | want := [][]byte{
129 | append(bytes.Repeat([]byte("a"), 1*1024), append(bytes.Repeat([]byte("b"), 14*1024), bytes.Repeat([]byte("c"), 1*1024)...)...),
130 | append(bytes.Repeat([]byte("c"), 14*1024), bytes.Repeat([]byte("d"), 2*1024)...),
131 | bytes.Repeat([]byte("e"), 1*1024),
132 | }
133 | if !reflect.DeepEqual(b.chunks, want) {
134 | t.Errorf("dataBuffer.chunks\ngot: %s\nwant: %s", fmtDataChunks(b.chunks), fmtDataChunks(want))
135 | }
136 | return b
137 | })
138 | }
139 |
140 | func TestDataBufferWriteAfterPartialRead(t *testing.T) {
141 | testDataBuffer(t, []byte("cdxyz"), func(t *testing.T) *dataBuffer {
142 | b := &dataBuffer{}
143 | if n, err := b.Write([]byte("abcd")); n != 4 || err != nil {
144 | t.Fatalf("Write(\"abcd\")=%v,%v want 4,nil", n, err)
145 | }
146 | p := make([]byte, 2)
147 | if n, err := b.Read(p); n != 2 || err != nil || !bytes.Equal(p, []byte("ab")) {
148 | t.Fatalf("Read()=%q,%v,%v want \"ab\",2,nil", p, n, err)
149 | }
150 | if n, err := b.Write([]byte("xyz")); n != 3 || err != nil {
151 | t.Fatalf("Write(\"xyz\")=%v,%v want 3,nil", n, err)
152 | }
153 | return b
154 | })
155 | }
156 |
--------------------------------------------------------------------------------
/pkg/http2/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "errors"
9 | "fmt"
10 | )
11 |
12 | // An ErrCode is an unsigned 32-bit error code as defined in the HTTP/2 spec.
13 | type ErrCode uint32
14 |
15 | const (
16 | ErrCodeNo ErrCode = 0x0
17 | ErrCodeProtocol ErrCode = 0x1
18 | ErrCodeInternal ErrCode = 0x2
19 | ErrCodeFlowControl ErrCode = 0x3
20 | ErrCodeSettingsTimeout ErrCode = 0x4
21 | ErrCodeStreamClosed ErrCode = 0x5
22 | ErrCodeFrameSize ErrCode = 0x6
23 | ErrCodeRefusedStream ErrCode = 0x7
24 | ErrCodeCancel ErrCode = 0x8
25 | ErrCodeCompression ErrCode = 0x9
26 | ErrCodeConnect ErrCode = 0xa
27 | ErrCodeEnhanceYourCalm ErrCode = 0xb
28 | ErrCodeInadequateSecurity ErrCode = 0xc
29 | ErrCodeHTTP11Required ErrCode = 0xd
30 | )
31 |
32 | var errCodeName = map[ErrCode]string{
33 | ErrCodeNo: "NO_ERROR",
34 | ErrCodeProtocol: "PROTOCOL_ERROR",
35 | ErrCodeInternal: "INTERNAL_ERROR",
36 | ErrCodeFlowControl: "FLOW_CONTROL_ERROR",
37 | ErrCodeSettingsTimeout: "SETTINGS_TIMEOUT",
38 | ErrCodeStreamClosed: "STREAM_CLOSED",
39 | ErrCodeFrameSize: "FRAME_SIZE_ERROR",
40 | ErrCodeRefusedStream: "REFUSED_STREAM",
41 | ErrCodeCancel: "CANCEL",
42 | ErrCodeCompression: "COMPRESSION_ERROR",
43 | ErrCodeConnect: "CONNECT_ERROR",
44 | ErrCodeEnhanceYourCalm: "ENHANCE_YOUR_CALM",
45 | ErrCodeInadequateSecurity: "INADEQUATE_SECURITY",
46 | ErrCodeHTTP11Required: "HTTP_1_1_REQUIRED",
47 | }
48 |
49 | func (e ErrCode) String() string {
50 | if s, ok := errCodeName[e]; ok {
51 | return s
52 | }
53 | return fmt.Sprintf("unknown error code 0x%x", uint32(e))
54 | }
55 |
56 | func (e ErrCode) stringToken() string {
57 | if s, ok := errCodeName[e]; ok {
58 | return s
59 | }
60 | return fmt.Sprintf("ERR_UNKNOWN_%d", uint32(e))
61 | }
62 |
63 | // ConnectionError is an error that results in the termination of the
64 | // entire connection.
65 | type ConnectionError ErrCode
66 |
67 | func (e ConnectionError) Error() string { return fmt.Sprintf("connection error: %s", ErrCode(e)) }
68 |
69 | // StreamError is an error that only affects one stream within an
70 | // HTTP/2 connection.
71 | type StreamError struct {
72 | StreamID uint32
73 | Code ErrCode
74 | Cause error // optional additional detail
75 | }
76 |
77 | // errFromPeer is a sentinel error value for StreamError.Cause to
78 | // indicate that the StreamError was sent from the peer over the wire
79 | // and wasn't locally generated in the Transport.
80 | var errFromPeer = errors.New("received from peer")
81 |
82 | func streamError(id uint32, code ErrCode) StreamError {
83 | return StreamError{StreamID: id, Code: code}
84 | }
85 |
86 | func (e StreamError) Error() string {
87 | if e.Cause != nil {
88 | return fmt.Sprintf("stream error: stream ID %d; %v; %v", e.StreamID, e.Code, e.Cause)
89 | }
90 | return fmt.Sprintf("stream error: stream ID %d; %v", e.StreamID, e.Code)
91 | }
92 |
93 | // 6.9.1 The Flow Control Window
94 | // "If a sender receives a WINDOW_UPDATE that causes a flow control
95 | // window to exceed this maximum it MUST terminate either the stream
96 | // or the connection, as appropriate. For streams, [...]; for the
97 | // connection, a GOAWAY frame with a FLOW_CONTROL_ERROR code."
98 | type goAwayFlowError struct{}
99 |
100 | func (goAwayFlowError) Error() string { return "connection exceeded flow control window size" }
101 |
102 | // connError represents an HTTP/2 ConnectionError error code, along
103 | // with a string (for debugging) explaining why.
104 | //
105 | // Errors of this type are only returned by the frame parser functions
106 | // and converted into ConnectionError(Code), after stashing away
107 | // the Reason into the Framer's errDetail field, accessible via
108 | // the (*Framer).ErrorDetail method.
109 | type connError struct {
110 | Code ErrCode // the ConnectionError error code
111 | Reason string // additional reason
112 | }
113 |
114 | func (e connError) Error() string {
115 | return fmt.Sprintf("http2: connection error: %v: %v", e.Code, e.Reason)
116 | }
117 |
118 | type pseudoHeaderError string
119 |
120 | func (e pseudoHeaderError) Error() string {
121 | return fmt.Sprintf("invalid pseudo-header %q", string(e))
122 | }
123 |
124 | type duplicatePseudoHeaderError string
125 |
126 | func (e duplicatePseudoHeaderError) Error() string {
127 | return fmt.Sprintf("duplicate pseudo-header %q", string(e))
128 | }
129 |
130 | type headerFieldNameError string
131 |
132 | func (e headerFieldNameError) Error() string {
133 | return fmt.Sprintf("invalid header field name %q", string(e))
134 | }
135 |
136 | type headerFieldValueError string
137 |
138 | func (e headerFieldValueError) Error() string {
139 | return fmt.Sprintf("invalid header field value for %q", string(e))
140 | }
141 |
142 | var (
143 | errMixPseudoHeaderTypes = errors.New("mix of request and response pseudo headers")
144 | errPseudoAfterRegular = errors.New("pseudo header field after regular")
145 | )
146 |
--------------------------------------------------------------------------------
/pkg/http2/errors_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import "testing"
8 |
9 | func TestErrCodeString(t *testing.T) {
10 | tests := []struct {
11 | err ErrCode
12 | want string
13 | }{
14 | {ErrCodeProtocol, "PROTOCOL_ERROR"},
15 | {0xd, "HTTP_1_1_REQUIRED"},
16 | {0xf, "unknown error code 0xf"},
17 | }
18 | for i, tt := range tests {
19 | got := tt.err.String()
20 | if got != tt.want {
21 | t.Errorf("%d. Error = %q; want %q", i, got, tt.want)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/http2/flow.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Flow control
6 |
7 | package http2
8 |
9 | // inflowMinRefresh is the minimum number of bytes we'll send for a
10 | // flow control window update.
11 | const inflowMinRefresh = 4 << 10
12 |
13 | // inflow accounts for an inbound flow control window.
14 | // It tracks both the latest window sent to the peer (used for enforcement)
15 | // and the accumulated unsent window.
16 | type inflow struct {
17 | avail int32
18 | unsent int32
19 | }
20 |
21 | // init sets the initial window.
22 | func (f *inflow) init(n int32) {
23 | f.avail = n
24 | }
25 |
26 | // add adds n bytes to the window, with a maximum window size of max,
27 | // indicating that the peer can now send us more data.
28 | // For example, the user read from a {Request,Response} body and consumed
29 | // some of the buffered data, so the peer can now send more.
30 | // It returns the number of bytes to send in a WINDOW_UPDATE frame to the peer.
31 | // Window updates are accumulated and sent when the unsent capacity
32 | // is at least inflowMinRefresh or will at least double the peer's available window.
33 | func (f *inflow) add(n int) (connAdd int32) {
34 | if n < 0 {
35 | panic("negative update")
36 | }
37 | unsent := int64(f.unsent) + int64(n)
38 | // "A sender MUST NOT allow a flow-control window to exceed 2^31-1 octets."
39 | // RFC 7540 Section 6.9.1.
40 | const maxWindow = 1<<31 - 1
41 | if unsent+int64(f.avail) > maxWindow {
42 | panic("flow control update exceeds maximum window size")
43 | }
44 | f.unsent = int32(unsent)
45 | if f.unsent < inflowMinRefresh && f.unsent < f.avail {
46 | // If there aren't at least inflowMinRefresh bytes of window to send,
47 | // and this update won't at least double the window, buffer the update for later.
48 | return 0
49 | }
50 | f.avail += f.unsent
51 | f.unsent = 0
52 | return int32(unsent)
53 | }
54 |
55 | // take attempts to take n bytes from the peer's flow control window.
56 | // It reports whether the window has available capacity.
57 | func (f *inflow) take(n uint32) bool {
58 | if n > uint32(f.avail) {
59 | return false
60 | }
61 | f.avail -= int32(n)
62 | return true
63 | }
64 |
65 | // takeInflows attempts to take n bytes from two inflows,
66 | // typically connection-level and stream-level flows.
67 | // It reports whether both windows have available capacity.
68 | func takeInflows(f1, f2 *inflow, n uint32) bool {
69 | if n > uint32(f1.avail) || n > uint32(f2.avail) {
70 | return false
71 | }
72 | f1.avail -= int32(n)
73 | f2.avail -= int32(n)
74 | return true
75 | }
76 |
77 | // outflow is the outbound flow control window's size.
78 | type outflow struct {
79 | _ incomparable
80 |
81 | // n is the number of DATA bytes we're allowed to send.
82 | // An outflow is kept both on a conn and a per-stream.
83 | n int32
84 |
85 | // conn points to the shared connection-level outflow that is
86 | // shared by all streams on that conn. It is nil for the outflow
87 | // that's on the conn directly.
88 | conn *outflow
89 | }
90 |
91 | func (f *outflow) setConnFlow(cf *outflow) { f.conn = cf }
92 |
93 | func (f *outflow) available() int32 {
94 | n := f.n
95 | if f.conn != nil && f.conn.n < n {
96 | n = f.conn.n
97 | }
98 | return n
99 | }
100 |
101 | func (f *outflow) take(n int32) {
102 | if n > f.available() {
103 | panic("internal error: took too much")
104 | }
105 | f.n -= n
106 | if f.conn != nil {
107 | f.conn.n -= n
108 | }
109 | }
110 |
111 | // add adds n bytes (positive or negative) to the flow control window.
112 | // It returns false if the sum would exceed 2^31-1.
113 | func (f *outflow) add(n int32) bool {
114 | sum := f.n + n
115 | if (sum > n) == (f.n > 0) {
116 | f.n = sum
117 | return true
118 | }
119 | return false
120 | }
121 |
--------------------------------------------------------------------------------
/pkg/http2/flow_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import "testing"
8 |
9 | func TestInFlowTake(t *testing.T) {
10 | var f inflow
11 | f.init(100)
12 | if !f.take(40) {
13 | t.Fatalf("f.take(40) from 100: got false, want true")
14 | }
15 | if !f.take(40) {
16 | t.Fatalf("f.take(40) from 60: got false, want true")
17 | }
18 | if f.take(40) {
19 | t.Fatalf("f.take(40) from 20: got true, want false")
20 | }
21 | if !f.take(20) {
22 | t.Fatalf("f.take(20) from 20: got false, want true")
23 | }
24 | }
25 |
26 | func TestInflowAddSmall(t *testing.T) {
27 | var f inflow
28 | f.init(0)
29 | // Adding even a small amount when there is no flow causes an immediate send.
30 | if got, want := f.add(1), int32(1); got != want {
31 | t.Fatalf("f.add(1) to 1 = %v, want %v", got, want)
32 | }
33 | }
34 |
35 | func TestInflowAdd(t *testing.T) {
36 | var f inflow
37 | f.init(10 * inflowMinRefresh)
38 | if got, want := f.add(inflowMinRefresh-1), int32(0); got != want {
39 | t.Fatalf("f.add(minRefresh - 1) = %v, want %v", got, want)
40 | }
41 | if got, want := f.add(1), int32(inflowMinRefresh); got != want {
42 | t.Fatalf("f.add(minRefresh) = %v, want %v", got, want)
43 | }
44 | }
45 |
46 | func TestTakeInflows(t *testing.T) {
47 | var a, b inflow
48 | a.init(10)
49 | b.init(20)
50 | if !takeInflows(&a, &b, 5) {
51 | t.Fatalf("takeInflows(a, b, 5) from 10, 20: got false, want true")
52 | }
53 | if takeInflows(&a, &b, 6) {
54 | t.Fatalf("takeInflows(a, b, 6) from 5, 15: got true, want false")
55 | }
56 | if !takeInflows(&a, &b, 5) {
57 | t.Fatalf("takeInflows(a, b, 5) from 5, 15: got false, want true")
58 | }
59 | }
60 |
61 | func TestOutFlow(t *testing.T) {
62 | var st outflow
63 | var conn outflow
64 | st.add(3)
65 | conn.add(2)
66 |
67 | if got, want := st.available(), int32(3); got != want {
68 | t.Errorf("available = %d; want %d", got, want)
69 | }
70 | st.setConnFlow(&conn)
71 | if got, want := st.available(), int32(2); got != want {
72 | t.Errorf("after parent setup, available = %d; want %d", got, want)
73 | }
74 |
75 | st.take(2)
76 | if got, want := conn.available(), int32(0); got != want {
77 | t.Errorf("after taking 2, conn = %d; want %d", got, want)
78 | }
79 | if got, want := st.available(), int32(0); got != want {
80 | t.Errorf("after taking 2, stream = %d; want %d", got, want)
81 | }
82 | }
83 |
84 | func TestOutFlowAdd(t *testing.T) {
85 | var f outflow
86 | if !f.add(1) {
87 | t.Fatal("failed to add 1")
88 | }
89 | if !f.add(-1) {
90 | t.Fatal("failed to add -1")
91 | }
92 | if got, want := f.available(), int32(0); got != want {
93 | t.Fatalf("size = %d; want %d", got, want)
94 | }
95 | if !f.add(1<<31 - 1) {
96 | t.Fatal("failed to add 2^31-1")
97 | }
98 | if got, want := f.available(), int32(1<<31-1); got != want {
99 | t.Fatalf("size = %d; want %d", got, want)
100 | }
101 | if f.add(1) {
102 | t.Fatal("adding 1 to max shouldn't be allowed")
103 | }
104 | }
105 |
106 | func TestOutFlowAddOverflow(t *testing.T) {
107 | var f outflow
108 | if !f.add(0) {
109 | t.Fatal("failed to add 0")
110 | }
111 | if !f.add(-1) {
112 | t.Fatal("failed to add -1")
113 | }
114 | if !f.add(0) {
115 | t.Fatal("failed to add 0")
116 | }
117 | if !f.add(1) {
118 | t.Fatal("failed to add 1")
119 | }
120 | if !f.add(1) {
121 | t.Fatal("failed to add 1")
122 | }
123 | if !f.add(0) {
124 | t.Fatal("failed to add 0")
125 | }
126 | if !f.add(-3) {
127 | t.Fatal("failed to add -3")
128 | }
129 | if got, want := f.available(), int32(-2); got != want {
130 | t.Fatalf("size = %d; want %d", got, want)
131 | }
132 | if !f.add(1<<31 - 1) {
133 | t.Fatal("failed to add 2^31-1")
134 | }
135 | if got, want := f.available(), int32(1+-3+(1<<31-1)); got != want {
136 | t.Fatalf("size = %d; want %d", got, want)
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/pkg/http2/gate_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 | package http2
5 |
6 | import "context"
7 |
8 | // An gate is a monitor (mutex + condition variable) with one bit of state.
9 | //
10 | // The condition may be either set or unset.
11 | // Lock operations may be unconditional, or wait for the condition to be set.
12 | // Unlock operations record the new state of the condition.
13 | type gate struct {
14 | // When unlocked, exactly one of set or unset contains a value.
15 | // When locked, neither chan contains a value.
16 | set chan struct{}
17 | unset chan struct{}
18 | }
19 |
20 | // newGate returns a new, unlocked gate with the condition unset.
21 | func newGate() gate {
22 | g := newLockedGate()
23 | g.unlock(false)
24 | return g
25 | }
26 |
27 | // newLocked gate returns a new, locked gate.
28 | func newLockedGate() gate {
29 | return gate{
30 | set: make(chan struct{}, 1),
31 | unset: make(chan struct{}, 1),
32 | }
33 | }
34 |
35 | // lock acquires the gate unconditionally.
36 | // It reports whether the condition is set.
37 | func (g *gate) lock() (set bool) {
38 | select {
39 | case <-g.set:
40 | return true
41 | case <-g.unset:
42 | return false
43 | }
44 | }
45 |
46 | // waitAndLock waits until the condition is set before acquiring the gate.
47 | // If the context expires, waitAndLock returns an error and does not acquire the gate.
48 | func (g *gate) waitAndLock(ctx context.Context) error {
49 | select {
50 | case <-g.set:
51 | return nil
52 | default:
53 | }
54 | select {
55 | case <-g.set:
56 | return nil
57 | case <-ctx.Done():
58 | return ctx.Err()
59 | }
60 | }
61 |
62 | // lockIfSet acquires the gate if and only if the condition is set.
63 | func (g *gate) lockIfSet() (acquired bool) {
64 | select {
65 | case <-g.set:
66 | return true
67 | default:
68 | return false
69 | }
70 | }
71 |
72 | // unlock sets the condition and releases the gate.
73 | func (g *gate) unlock(set bool) {
74 | if set {
75 | g.set <- struct{}{}
76 | } else {
77 | g.unset <- struct{}{}
78 | }
79 | }
80 |
81 | // unlock sets the condition to the result of f and releases the gate.
82 | // Useful in defers.
83 | func (g *gate) unlockFunc(f func() bool) {
84 | g.unlock(f())
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/http2/gotrack.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Defensive debug-only utility to track that functions run on the
6 | // goroutine that they're supposed to.
7 |
8 | package http2
9 |
10 | import (
11 | "bytes"
12 | "errors"
13 | "fmt"
14 | "os"
15 | "runtime"
16 | "strconv"
17 | "sync"
18 | )
19 |
20 | var DebugGoroutines = os.Getenv("DEBUG_HTTP2_GOROUTINES") == "1"
21 |
22 | type goroutineLock uint64
23 |
24 | func newGoroutineLock() goroutineLock {
25 | if !DebugGoroutines {
26 | return 0
27 | }
28 | return goroutineLock(curGoroutineID())
29 | }
30 |
31 | func (g goroutineLock) check() {
32 | if !DebugGoroutines {
33 | return
34 | }
35 | if curGoroutineID() != uint64(g) {
36 | panic("running on the wrong goroutine")
37 | }
38 | }
39 |
40 | func (g goroutineLock) checkNotOn() {
41 | if !DebugGoroutines {
42 | return
43 | }
44 | if curGoroutineID() == uint64(g) {
45 | panic("running on the wrong goroutine")
46 | }
47 | }
48 |
49 | var goroutineSpace = []byte("goroutine ")
50 |
51 | func curGoroutineID() uint64 {
52 | bp := littleBuf.Get().(*[]byte)
53 | defer littleBuf.Put(bp)
54 | b := *bp
55 | b = b[:runtime.Stack(b, false)]
56 | // Parse the 4707 out of "goroutine 4707 ["
57 | b = bytes.TrimPrefix(b, goroutineSpace)
58 | i := bytes.IndexByte(b, ' ')
59 | if i < 0 {
60 | panic(fmt.Sprintf("No space found in %q", b))
61 | }
62 | b = b[:i]
63 | n, err := parseUintBytes(b, 10, 64)
64 | if err != nil {
65 | panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
66 | }
67 | return n
68 | }
69 |
70 | var littleBuf = sync.Pool{
71 | New: func() interface{} {
72 | buf := make([]byte, 64)
73 | return &buf
74 | },
75 | }
76 |
77 | // parseUintBytes is like strconv.ParseUint, but using a []byte.
78 | func parseUintBytes(s []byte, base int, bitSize int) (n uint64, err error) {
79 | var cutoff, maxVal uint64
80 |
81 | if bitSize == 0 {
82 | bitSize = int(strconv.IntSize)
83 | }
84 |
85 | s0 := s
86 | switch {
87 | case len(s) < 1:
88 | err = strconv.ErrSyntax
89 | goto Error
90 |
91 | case 2 <= base && base <= 36:
92 | // valid base; nothing to do
93 |
94 | case base == 0:
95 | // Look for octal, hex prefix.
96 | switch {
97 | case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'):
98 | base = 16
99 | s = s[2:]
100 | if len(s) < 1 {
101 | err = strconv.ErrSyntax
102 | goto Error
103 | }
104 | case s[0] == '0':
105 | base = 8
106 | default:
107 | base = 10
108 | }
109 |
110 | default:
111 | err = errors.New("invalid base " + strconv.Itoa(base))
112 | goto Error
113 | }
114 |
115 | n = 0
116 | cutoff = cutoff64(base)
117 | maxVal = 1<= base {
135 | n = 0
136 | err = strconv.ErrSyntax
137 | goto Error
138 | }
139 |
140 | if n >= cutoff {
141 | // n*base overflows
142 | n = 1<<64 - 1
143 | err = strconv.ErrRange
144 | goto Error
145 | }
146 | n *= uint64(base)
147 |
148 | n1 := n + uint64(v)
149 | if n1 < n || n1 > maxVal {
150 | // n+v overflows
151 | n = 1<<64 - 1
152 | err = strconv.ErrRange
153 | goto Error
154 | }
155 | n = n1
156 | }
157 |
158 | return n, nil
159 |
160 | Error:
161 | return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err}
162 | }
163 |
164 | // Return the first number n such that n*base >= 1<<64.
165 | func cutoff64(base int) uint64 {
166 | if base < 2 {
167 | return 0
168 | }
169 | return (1<<64-1)/uint64(base) + 1
170 | }
171 |
--------------------------------------------------------------------------------
/pkg/http2/gotrack_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "fmt"
9 | "strings"
10 | "testing"
11 | )
12 |
13 | func TestGoroutineLock(t *testing.T) {
14 | oldDebug := DebugGoroutines
15 | DebugGoroutines = true
16 | defer func() { DebugGoroutines = oldDebug }()
17 |
18 | g := newGoroutineLock()
19 | g.check()
20 |
21 | sawPanic := make(chan interface{})
22 | go func() {
23 | defer func() { sawPanic <- recover() }()
24 | g.check() // should panic
25 | }()
26 | e := <-sawPanic
27 | if e == nil {
28 | t.Fatal("did not see panic from check in other goroutine")
29 | }
30 | if !strings.Contains(fmt.Sprint(e), "wrong goroutine") {
31 | t.Errorf("expected on see panic about running on the wrong goroutine; got %v", e)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/http2/h2c/h2c_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package h2c
6 |
7 | import (
8 | "context"
9 | "crypto/tls"
10 | "fmt"
11 | "io"
12 | "log"
13 | "net"
14 | "net/http"
15 | "net/http/httptest"
16 | "strings"
17 | "testing"
18 |
19 | "golang.org/x/net/http2"
20 | )
21 |
22 | func ExampleNewHandler() {
23 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24 | fmt.Fprint(w, "Hello world")
25 | })
26 | h2s := &http2.Server{
27 | // ...
28 | }
29 | h1s := &http.Server{
30 | Addr: ":8080",
31 | Handler: NewHandler(handler, h2s),
32 | }
33 | log.Fatal(h1s.ListenAndServe())
34 | }
35 |
36 | func TestContext(t *testing.T) {
37 | baseCtx := context.WithValue(context.Background(), "testkey", "testvalue")
38 |
39 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40 | if r.ProtoMajor != 2 {
41 | t.Errorf("Request wasn't handled by h2c. Got ProtoMajor=%v", r.ProtoMajor)
42 | }
43 | if r.Context().Value("testkey") != "testvalue" {
44 | t.Errorf("Request doesn't have expected base context: %v", r.Context())
45 | }
46 | fmt.Fprint(w, "Hello world")
47 | })
48 |
49 | h2s := &http2.Server{}
50 | h1s := httptest.NewUnstartedServer(NewHandler(handler, h2s))
51 | h1s.Config.BaseContext = func(_ net.Listener) context.Context {
52 | return baseCtx
53 | }
54 | h1s.Start()
55 | defer h1s.Close()
56 |
57 | client := &http.Client{
58 | Transport: &http2.Transport{
59 | AllowHTTP: true,
60 | DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
61 | return net.Dial(network, addr)
62 | },
63 | },
64 | }
65 |
66 | resp, err := client.Get(h1s.URL)
67 | if err != nil {
68 | t.Fatal(err)
69 | }
70 | _, err = io.ReadAll(resp.Body)
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 | if err := resp.Body.Close(); err != nil {
75 | t.Fatal(err)
76 | }
77 | }
78 |
79 | func TestPropagation(t *testing.T) {
80 | var (
81 | server *http.Server
82 | // double the limit because http2 will compress header
83 | headerSize = 1 << 11
84 | headerLimit = 1 << 10
85 | )
86 |
87 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
88 | if r.ProtoMajor != 2 {
89 | t.Errorf("Request wasn't handled by h2c. Got ProtoMajor=%v", r.ProtoMajor)
90 | }
91 | if r.Context().Value(http.ServerContextKey).(*http.Server) != server {
92 | t.Errorf("Request doesn't have expected http server: %v", r.Context())
93 | }
94 | if len(r.Header.Get("Long-Header")) != headerSize {
95 | t.Errorf("Request doesn't have expected http header length: %v", len(r.Header.Get("Long-Header")))
96 | }
97 | fmt.Fprint(w, "Hello world")
98 | })
99 |
100 | h2s := &http2.Server{}
101 | h1s := httptest.NewUnstartedServer(NewHandler(handler, h2s))
102 |
103 | server = h1s.Config
104 | server.MaxHeaderBytes = headerLimit
105 | server.ConnState = func(conn net.Conn, state http.ConnState) {
106 | t.Logf("server conn state: conn %s -> %s, status changed to %s", conn.RemoteAddr(), conn.LocalAddr(), state)
107 | }
108 |
109 | h1s.Start()
110 | defer h1s.Close()
111 |
112 | client := &http.Client{
113 | Transport: &http2.Transport{
114 | AllowHTTP: true,
115 | DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
116 | conn, err := net.Dial(network, addr)
117 | if conn != nil {
118 | t.Logf("client dial tls: %s -> %s", conn.RemoteAddr(), conn.LocalAddr())
119 | }
120 | return conn, err
121 | },
122 | },
123 | }
124 |
125 | req, err := http.NewRequest("GET", h1s.URL, nil)
126 | if err != nil {
127 | t.Fatal(err)
128 | }
129 |
130 | req.Header.Set("Long-Header", strings.Repeat("A", headerSize))
131 |
132 | _, err = client.Do(req)
133 | if err == nil {
134 | t.Fatal("expected server err, got nil")
135 | }
136 | }
137 |
138 | func TestMaxBytesHandler(t *testing.T) {
139 | const bodyLimit = 10
140 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
141 | t.Errorf("got request, expected to be blocked by body limit")
142 | })
143 |
144 | h2s := &http2.Server{}
145 | h1s := httptest.NewUnstartedServer(http.MaxBytesHandler(NewHandler(handler, h2s), bodyLimit))
146 | h1s.Start()
147 | defer h1s.Close()
148 |
149 | // Wrap the body in a struct{io.Reader} to prevent it being rewound and resent.
150 | body := "0123456789abcdef"
151 | req, err := http.NewRequest("POST", h1s.URL, struct{ io.Reader }{strings.NewReader(body)})
152 | if err != nil {
153 | t.Fatal(err)
154 | }
155 | req.Header.Set("Http2-Settings", "")
156 | req.Header.Set("Upgrade", "h2c")
157 | req.Header.Set("Connection", "Upgrade, HTTP2-Settings")
158 |
159 | resp, err := h1s.Client().Do(req)
160 | if err != nil {
161 | t.Fatal(err)
162 | }
163 | defer resp.Body.Close()
164 | _, err = io.ReadAll(resp.Body)
165 | if err != nil {
166 | t.Fatal(err)
167 | }
168 | if got, want := resp.StatusCode, http.StatusInternalServerError; got != want {
169 | t.Errorf("resp.StatusCode = %v, want %v", got, want)
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/pkg/http2/h2i/README.md:
--------------------------------------------------------------------------------
1 | # h2i
2 |
3 | **h2i** is an interactive HTTP/2 ("h2") console debugger. Miss the good ol'
4 | days of telnetting to your HTTP/1.n servers? We're bringing you
5 | back.
6 |
7 | Features:
8 | - send raw HTTP/2 frames
9 | - PING
10 | - SETTINGS
11 | - HEADERS
12 | - etc
13 | - type in HTTP/1.n and have it auto-HPACK/frame-ify it for HTTP/2
14 | - pretty print all received HTTP/2 frames from the peer (including HPACK decoding)
15 | - tab completion of commands, options
16 |
17 | Not yet features, but soon:
18 | - unnecessary CONTINUATION frames on short boundaries, to test peer implementations
19 | - request bodies (DATA frames)
20 | - send invalid frames for testing server implementations (supported by underlying Framer)
21 |
22 | Later:
23 | - act like a server
24 |
25 | ## Installation
26 |
27 | ```
28 | $ go install golang.org/x/net/http2/h2i@latest
29 | $ h2i
30 | ```
31 |
32 | ## Demo
33 |
34 | ```
35 | $ h2i
36 | Usage: h2i
37 |
38 | -insecure
39 | Whether to skip TLS cert validation
40 | -nextproto string
41 | Comma-separated list of NPN/ALPN protocol names to negotiate. (default "h2,h2-14")
42 |
43 | $ h2i google.com
44 | Connecting to google.com:443 ...
45 | Connected to 74.125.224.41:443
46 | Negotiated protocol "h2-14"
47 | [FrameHeader SETTINGS len=18]
48 | [MAX_CONCURRENT_STREAMS = 100]
49 | [INITIAL_WINDOW_SIZE = 1048576]
50 | [MAX_FRAME_SIZE = 16384]
51 | [FrameHeader WINDOW_UPDATE len=4]
52 | Window-Increment = 983041
53 |
54 | h2i> PING h2iSayHI
55 | [FrameHeader PING flags=ACK len=8]
56 | Data = "h2iSayHI"
57 | h2i> headers
58 | (as HTTP/1.1)> GET / HTTP/1.1
59 | (as HTTP/1.1)> Host: ip.appspot.com
60 | (as HTTP/1.1)> User-Agent: h2i/brad-n-blake
61 | (as HTTP/1.1)>
62 | Opening Stream-ID 1:
63 | :authority = ip.appspot.com
64 | :method = GET
65 | :path = /
66 | :scheme = https
67 | user-agent = h2i/brad-n-blake
68 | [FrameHeader HEADERS flags=END_HEADERS stream=1 len=77]
69 | :status = "200"
70 | alternate-protocol = "443:quic,p=1"
71 | content-length = "15"
72 | content-type = "text/html"
73 | date = "Fri, 01 May 2015 23:06:56 GMT"
74 | server = "Google Frontend"
75 | [FrameHeader DATA flags=END_STREAM stream=1 len=15]
76 | "173.164.155.78\n"
77 | [FrameHeader PING len=8]
78 | Data = "\x00\x00\x00\x00\x00\x00\x00\x00"
79 | h2i> ping
80 | [FrameHeader PING flags=ACK len=8]
81 | Data = "h2i_ping"
82 | h2i> ping
83 | [FrameHeader PING flags=ACK len=8]
84 | Data = "h2i_ping"
85 | h2i> ping
86 | [FrameHeader GOAWAY len=22]
87 | Last-Stream-ID = 1; Error-Code = PROTOCOL_ERROR (1)
88 |
89 | ReadFrame: EOF
90 | ```
91 |
92 | ## Status
93 |
94 | Quick few hour hack. So much yet to do. Feel free to file issues for
95 | bugs or wishlist items, but [@bmizerany](https://github.com/bmizerany/)
96 | and I aren't yet accepting pull requests until things settle down.
97 |
98 |
--------------------------------------------------------------------------------
/pkg/http2/headermap.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "net/http"
9 | "sync"
10 | )
11 |
12 | var (
13 | commonBuildOnce sync.Once
14 | commonLowerHeader map[string]string // Go-Canonical-Case -> lower-case
15 | commonCanonHeader map[string]string // lower-case -> Go-Canonical-Case
16 | )
17 |
18 | func buildCommonHeaderMapsOnce() {
19 | commonBuildOnce.Do(buildCommonHeaderMaps)
20 | }
21 |
22 | func buildCommonHeaderMaps() {
23 | common := []string{
24 | "accept",
25 | "accept-charset",
26 | "accept-encoding",
27 | "accept-language",
28 | "accept-ranges",
29 | "age",
30 | "access-control-allow-credentials",
31 | "access-control-allow-headers",
32 | "access-control-allow-methods",
33 | "access-control-allow-origin",
34 | "access-control-expose-headers",
35 | "access-control-max-age",
36 | "access-control-request-headers",
37 | "access-control-request-method",
38 | "allow",
39 | "authorization",
40 | "cache-control",
41 | "content-disposition",
42 | "content-encoding",
43 | "content-language",
44 | "content-length",
45 | "content-location",
46 | "content-range",
47 | "content-type",
48 | "cookie",
49 | "date",
50 | "etag",
51 | "expect",
52 | "expires",
53 | "from",
54 | "host",
55 | "if-match",
56 | "if-modified-since",
57 | "if-none-match",
58 | "if-unmodified-since",
59 | "last-modified",
60 | "link",
61 | "location",
62 | "max-forwards",
63 | "origin",
64 | "proxy-authenticate",
65 | "proxy-authorization",
66 | "range",
67 | "referer",
68 | "refresh",
69 | "retry-after",
70 | "server",
71 | "set-cookie",
72 | "strict-transport-security",
73 | "trailer",
74 | "transfer-encoding",
75 | "user-agent",
76 | "vary",
77 | "via",
78 | "www-authenticate",
79 | "x-forwarded-for",
80 | "x-forwarded-proto",
81 | }
82 | commonLowerHeader = make(map[string]string, len(common))
83 | commonCanonHeader = make(map[string]string, len(common))
84 | for _, v := range common {
85 | chk := http.CanonicalHeaderKey(v)
86 | commonLowerHeader[chk] = v
87 | commonCanonHeader[v] = chk
88 | }
89 | }
90 |
91 | func lowerHeader(v string) (lower string, ascii bool) {
92 | buildCommonHeaderMapsOnce()
93 | if s, ok := commonLowerHeader[v]; ok {
94 | return s, true
95 | }
96 | return asciiToLower(v)
97 | }
98 |
99 | func canonicalHeader(v string) string {
100 | buildCommonHeaderMapsOnce()
101 | if s, ok := commonCanonHeader[v]; ok {
102 | return s
103 | }
104 | return http.CanonicalHeaderKey(v)
105 | }
106 |
--------------------------------------------------------------------------------
/pkg/http2/hpack/gen.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | //go:build ignore
6 |
7 | package main
8 |
9 | import (
10 | "bytes"
11 | "fmt"
12 | "go/format"
13 | "os"
14 | "sort"
15 |
16 | "golang.org/x/net/http2/hpack"
17 | )
18 |
19 | // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#appendix-B
20 | var staticTableEntries = [...]hpack.HeaderField{
21 | {Name: ":authority"},
22 | {Name: ":method", Value: "GET"},
23 | {Name: ":method", Value: "POST"},
24 | {Name: ":path", Value: "/"},
25 | {Name: ":path", Value: "/index.html"},
26 | {Name: ":scheme", Value: "http"},
27 | {Name: ":scheme", Value: "https"},
28 | {Name: ":status", Value: "200"},
29 | {Name: ":status", Value: "204"},
30 | {Name: ":status", Value: "206"},
31 | {Name: ":status", Value: "304"},
32 | {Name: ":status", Value: "400"},
33 | {Name: ":status", Value: "404"},
34 | {Name: ":status", Value: "500"},
35 | {Name: "accept-charset"},
36 | {Name: "accept-encoding", Value: "gzip, deflate"},
37 | {Name: "accept-language"},
38 | {Name: "accept-ranges"},
39 | {Name: "accept"},
40 | {Name: "access-control-allow-origin"},
41 | {Name: "age"},
42 | {Name: "allow"},
43 | {Name: "authorization"},
44 | {Name: "cache-control"},
45 | {Name: "content-disposition"},
46 | {Name: "content-encoding"},
47 | {Name: "content-language"},
48 | {Name: "content-length"},
49 | {Name: "content-location"},
50 | {Name: "content-range"},
51 | {Name: "content-type"},
52 | {Name: "cookie"},
53 | {Name: "date"},
54 | {Name: "etag"},
55 | {Name: "expect"},
56 | {Name: "expires"},
57 | {Name: "from"},
58 | {Name: "host"},
59 | {Name: "if-match"},
60 | {Name: "if-modified-since"},
61 | {Name: "if-none-match"},
62 | {Name: "if-range"},
63 | {Name: "if-unmodified-since"},
64 | {Name: "last-modified"},
65 | {Name: "link"},
66 | {Name: "location"},
67 | {Name: "max-forwards"},
68 | {Name: "proxy-authenticate"},
69 | {Name: "proxy-authorization"},
70 | {Name: "range"},
71 | {Name: "referer"},
72 | {Name: "refresh"},
73 | {Name: "retry-after"},
74 | {Name: "server"},
75 | {Name: "set-cookie"},
76 | {Name: "strict-transport-security"},
77 | {Name: "transfer-encoding"},
78 | {Name: "user-agent"},
79 | {Name: "vary"},
80 | {Name: "via"},
81 | {Name: "www-authenticate"},
82 | }
83 |
84 | type pairNameValue struct {
85 | name, value string
86 | }
87 |
88 | type byNameItem struct {
89 | name string
90 | id uint64
91 | }
92 |
93 | type byNameValueItem struct {
94 | pairNameValue
95 | id uint64
96 | }
97 |
98 | func headerFieldToString(f hpack.HeaderField) string {
99 | return fmt.Sprintf("{Name: \"%s\", Value:\"%s\", Sensitive: %t}", f.Name, f.Value, f.Sensitive)
100 | }
101 |
102 | func pairNameValueToString(v pairNameValue) string {
103 | return fmt.Sprintf("{name: \"%s\", value:\"%s\"}", v.name, v.value)
104 | }
105 |
106 | const header = `
107 | // go generate gen.go
108 | // Code generated by the command above; DO NOT EDIT.
109 |
110 | package hpack
111 |
112 | var staticTable = &headerFieldTable{
113 | evictCount: 0,
114 | byName: map[string]uint64{
115 | `
116 |
117 | //go:generate go run gen.go
118 | func main() {
119 | var bb bytes.Buffer
120 | fmt.Fprintf(&bb, header)
121 | byName := make(map[string]uint64)
122 | byNameValue := make(map[pairNameValue]uint64)
123 | for index, entry := range staticTableEntries {
124 | id := uint64(index) + 1
125 | byName[entry.Name] = id
126 | byNameValue[pairNameValue{entry.Name, entry.Value}] = id
127 | }
128 | // Sort maps for deterministic generation.
129 | byNameItems := sortByName(byName)
130 | byNameValueItems := sortByNameValue(byNameValue)
131 |
132 | for _, item := range byNameItems {
133 | fmt.Fprintf(&bb, "\"%s\":%d,\n", item.name, item.id)
134 | }
135 | fmt.Fprintf(&bb, "},\n")
136 | fmt.Fprintf(&bb, "byNameValue: map[pairNameValue]uint64{\n")
137 | for _, item := range byNameValueItems {
138 | fmt.Fprintf(&bb, "%s:%d,\n", pairNameValueToString(item.pairNameValue), item.id)
139 | }
140 | fmt.Fprintf(&bb, "},\n")
141 | fmt.Fprintf(&bb, "ents: []HeaderField{\n")
142 | for _, value := range staticTableEntries {
143 | fmt.Fprintf(&bb, "%s,\n", headerFieldToString(value))
144 | }
145 | fmt.Fprintf(&bb, "},\n")
146 | fmt.Fprintf(&bb, "}\n")
147 | genFile("static_table.go", &bb)
148 | }
149 |
150 | func sortByNameValue(byNameValue map[pairNameValue]uint64) []byNameValueItem {
151 | var byNameValueItems []byNameValueItem
152 | for k, v := range byNameValue {
153 | byNameValueItems = append(byNameValueItems, byNameValueItem{k, v})
154 | }
155 | sort.Slice(byNameValueItems, func(i, j int) bool {
156 | return byNameValueItems[i].id < byNameValueItems[j].id
157 | })
158 | return byNameValueItems
159 | }
160 |
161 | func sortByName(byName map[string]uint64) []byNameItem {
162 | var byNameItems []byNameItem
163 | for k, v := range byName {
164 | byNameItems = append(byNameItems, byNameItem{k, v})
165 | }
166 | sort.Slice(byNameItems, func(i, j int) bool {
167 | return byNameItems[i].id < byNameItems[j].id
168 | })
169 | return byNameItems
170 | }
171 |
172 | func genFile(name string, buf *bytes.Buffer) {
173 | b, err := format.Source(buf.Bytes())
174 | if err != nil {
175 | fmt.Fprintln(os.Stderr, err)
176 | os.Exit(1)
177 | }
178 | if err := os.WriteFile(name, b, 0644); err != nil {
179 | fmt.Fprintln(os.Stderr, err)
180 | os.Exit(1)
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/pkg/http2/pipe.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "errors"
9 | "io"
10 | "sync"
11 | )
12 |
13 | // pipe is a goroutine-safe io.Reader/io.Writer pair. It's like
14 | // io.Pipe except there are no PipeReader/PipeWriter halves, and the
15 | // underlying buffer is an interface. (io.Pipe is always unbuffered)
16 | type pipe struct {
17 | mu sync.Mutex
18 | c sync.Cond // c.L lazily initialized to &p.mu
19 | b pipeBuffer // nil when done reading
20 | unread int // bytes unread when done
21 | err error // read error once empty. non-nil means closed.
22 | breakErr error // immediate read error (caller doesn't see rest of b)
23 | donec chan struct{} // closed on error
24 | readFn func() // optional code to run in Read before error
25 | }
26 |
27 | type pipeBuffer interface {
28 | Len() int
29 | io.Writer
30 | io.Reader
31 | }
32 |
33 | // setBuffer initializes the pipe buffer.
34 | // It has no effect if the pipe is already closed.
35 | func (p *pipe) setBuffer(b pipeBuffer) {
36 | p.mu.Lock()
37 | defer p.mu.Unlock()
38 | if p.err != nil || p.breakErr != nil {
39 | return
40 | }
41 | p.b = b
42 | }
43 |
44 | func (p *pipe) Len() int {
45 | p.mu.Lock()
46 | defer p.mu.Unlock()
47 | if p.b == nil {
48 | return p.unread
49 | }
50 | return p.b.Len()
51 | }
52 |
53 | // Read waits until data is available and copies bytes
54 | // from the buffer into p.
55 | func (p *pipe) Read(d []byte) (n int, err error) {
56 | p.mu.Lock()
57 | defer p.mu.Unlock()
58 | if p.c.L == nil {
59 | p.c.L = &p.mu
60 | }
61 | for {
62 | if p.breakErr != nil {
63 | return 0, p.breakErr
64 | }
65 | if p.b != nil && p.b.Len() > 0 {
66 | return p.b.Read(d)
67 | }
68 | if p.err != nil {
69 | if p.readFn != nil {
70 | p.readFn() // e.g. copy trailers
71 | p.readFn = nil // not sticky like p.err
72 | }
73 | p.b = nil
74 | return 0, p.err
75 | }
76 | p.c.Wait()
77 | }
78 | }
79 |
80 | var (
81 | errClosedPipeWrite = errors.New("write on closed buffer")
82 | errUninitializedPipeWrite = errors.New("write on uninitialized buffer")
83 | )
84 |
85 | // Write copies bytes from p into the buffer and wakes a reader.
86 | // It is an error to write more data than the buffer can hold.
87 | func (p *pipe) Write(d []byte) (n int, err error) {
88 | p.mu.Lock()
89 | defer p.mu.Unlock()
90 | if p.c.L == nil {
91 | p.c.L = &p.mu
92 | }
93 | defer p.c.Signal()
94 | if p.err != nil || p.breakErr != nil {
95 | return 0, errClosedPipeWrite
96 | }
97 | // pipe.setBuffer is never invoked, leaving the buffer uninitialized.
98 | // We shouldn't try to write to an uninitialized pipe,
99 | // but returning an error is better than panicking.
100 | if p.b == nil {
101 | return 0, errUninitializedPipeWrite
102 | }
103 | return p.b.Write(d)
104 | }
105 |
106 | // CloseWithError causes the next Read (waking up a current blocked
107 | // Read if needed) to return the provided err after all data has been
108 | // read.
109 | //
110 | // The error must be non-nil.
111 | func (p *pipe) CloseWithError(err error) { p.closeWithError(&p.err, err, nil) }
112 |
113 | // BreakWithError causes the next Read (waking up a current blocked
114 | // Read if needed) to return the provided err immediately, without
115 | // waiting for unread data.
116 | func (p *pipe) BreakWithError(err error) { p.closeWithError(&p.breakErr, err, nil) }
117 |
118 | // closeWithErrorAndCode is like CloseWithError but also sets some code to run
119 | // in the caller's goroutine before returning the error.
120 | func (p *pipe) closeWithErrorAndCode(err error, fn func()) { p.closeWithError(&p.err, err, fn) }
121 |
122 | func (p *pipe) closeWithError(dst *error, err error, fn func()) {
123 | if err == nil {
124 | panic("err must be non-nil")
125 | }
126 | p.mu.Lock()
127 | defer p.mu.Unlock()
128 | if p.c.L == nil {
129 | p.c.L = &p.mu
130 | }
131 | defer p.c.Signal()
132 | if *dst != nil {
133 | // Already been done.
134 | return
135 | }
136 | p.readFn = fn
137 | if dst == &p.breakErr {
138 | if p.b != nil {
139 | p.unread += p.b.Len()
140 | }
141 | p.b = nil
142 | }
143 | *dst = err
144 | p.closeDoneLocked()
145 | }
146 |
147 | // requires p.mu be held.
148 | func (p *pipe) closeDoneLocked() {
149 | if p.donec == nil {
150 | return
151 | }
152 | // Close if unclosed. This isn't racy since we always
153 | // hold p.mu while closing.
154 | select {
155 | case <-p.donec:
156 | default:
157 | close(p.donec)
158 | }
159 | }
160 |
161 | // Err returns the error (if any) first set by BreakWithError or CloseWithError.
162 | func (p *pipe) Err() error {
163 | p.mu.Lock()
164 | defer p.mu.Unlock()
165 | if p.breakErr != nil {
166 | return p.breakErr
167 | }
168 | return p.err
169 | }
170 |
171 | // Done returns a channel which is closed if and when this pipe is closed
172 | // with CloseWithError.
173 | func (p *pipe) Done() <-chan struct{} {
174 | p.mu.Lock()
175 | defer p.mu.Unlock()
176 | if p.donec == nil {
177 | p.donec = make(chan struct{})
178 | if p.err != nil || p.breakErr != nil {
179 | // Already hit an error.
180 | p.closeDoneLocked()
181 | }
182 | }
183 | return p.donec
184 | }
185 |
--------------------------------------------------------------------------------
/pkg/http2/pipe_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "bytes"
9 | "errors"
10 | "io"
11 | "testing"
12 | )
13 |
14 | func TestPipeClose(t *testing.T) {
15 | var p pipe
16 | p.b = new(bytes.Buffer)
17 | a := errors.New("a")
18 | b := errors.New("b")
19 | p.CloseWithError(a)
20 | p.CloseWithError(b)
21 | _, err := p.Read(make([]byte, 1))
22 | if err != a {
23 | t.Errorf("err = %v want %v", err, a)
24 | }
25 | }
26 |
27 | func TestPipeDoneChan(t *testing.T) {
28 | var p pipe
29 | done := p.Done()
30 | select {
31 | case <-done:
32 | t.Fatal("done too soon")
33 | default:
34 | }
35 | p.CloseWithError(io.EOF)
36 | select {
37 | case <-done:
38 | default:
39 | t.Fatal("should be done")
40 | }
41 | }
42 |
43 | func TestPipeDoneChan_ErrFirst(t *testing.T) {
44 | var p pipe
45 | p.CloseWithError(io.EOF)
46 | done := p.Done()
47 | select {
48 | case <-done:
49 | default:
50 | t.Fatal("should be done")
51 | }
52 | }
53 |
54 | func TestPipeDoneChan_Break(t *testing.T) {
55 | var p pipe
56 | done := p.Done()
57 | select {
58 | case <-done:
59 | t.Fatal("done too soon")
60 | default:
61 | }
62 | p.BreakWithError(io.EOF)
63 | select {
64 | case <-done:
65 | default:
66 | t.Fatal("should be done")
67 | }
68 | }
69 |
70 | func TestPipeDoneChan_Break_ErrFirst(t *testing.T) {
71 | var p pipe
72 | p.BreakWithError(io.EOF)
73 | done := p.Done()
74 | select {
75 | case <-done:
76 | default:
77 | t.Fatal("should be done")
78 | }
79 | }
80 |
81 | func TestPipeCloseWithError(t *testing.T) {
82 | p := &pipe{b: new(bytes.Buffer)}
83 | const body = "foo"
84 | io.WriteString(p, body)
85 | a := errors.New("test error")
86 | p.CloseWithError(a)
87 | all, err := io.ReadAll(p)
88 | if string(all) != body {
89 | t.Errorf("read bytes = %q; want %q", all, body)
90 | }
91 | if err != a {
92 | t.Logf("read error = %v, %v", err, a)
93 | }
94 | if p.Len() != 0 {
95 | t.Errorf("pipe should have 0 unread bytes")
96 | }
97 | // Read and Write should fail.
98 | if n, err := p.Write([]byte("abc")); err != errClosedPipeWrite || n != 0 {
99 | t.Errorf("Write(abc) after close\ngot %v, %v\nwant 0, %v", n, err, errClosedPipeWrite)
100 | }
101 | if n, err := p.Read(make([]byte, 1)); err == nil || n != 0 {
102 | t.Errorf("Read() after close\ngot %v, nil\nwant 0, %v", n, errClosedPipeWrite)
103 | }
104 | if p.Len() != 0 {
105 | t.Errorf("pipe should have 0 unread bytes")
106 | }
107 | }
108 |
109 | func TestPipeBreakWithError(t *testing.T) {
110 | p := &pipe{b: new(bytes.Buffer)}
111 | io.WriteString(p, "foo")
112 | a := errors.New("test err")
113 | p.BreakWithError(a)
114 | all, err := io.ReadAll(p)
115 | if string(all) != "" {
116 | t.Errorf("read bytes = %q; want empty string", all)
117 | }
118 | if err != a {
119 | t.Logf("read error = %v, %v", err, a)
120 | }
121 | if p.b != nil {
122 | t.Errorf("buffer should be nil after BreakWithError")
123 | }
124 | if p.Len() != 3 {
125 | t.Errorf("pipe should have 3 unread bytes")
126 | }
127 | // Write should fail.
128 | if n, err := p.Write([]byte("abc")); err != errClosedPipeWrite || n != 0 {
129 | t.Errorf("Write(abc) after break\ngot %v, %v\nwant 0, errClosedPipeWrite", n, err)
130 | }
131 | if p.b != nil {
132 | t.Errorf("buffer should be nil after Write")
133 | }
134 | if p.Len() != 3 {
135 | t.Errorf("pipe should have 6 unread bytes")
136 | }
137 | // Read should fail.
138 | if n, err := p.Read(make([]byte, 1)); err == nil || n != 0 {
139 | t.Errorf("Read() after close\ngot %v, nil\nwant 0, not nil", n)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/pkg/http2/timer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 | package http2
5 |
6 | import "time"
7 |
8 | // A timer is a time.Timer, as an interface which can be replaced in tests.
9 | type timer = interface {
10 | C() <-chan time.Time
11 | Reset(d time.Duration) bool
12 | Stop() bool
13 | }
14 |
15 | // timeTimer adapts a time.Timer to the timer interface.
16 | type timeTimer struct {
17 | *time.Timer
18 | }
19 |
20 | func (t timeTimer) C() <-chan time.Time { return t.Timer.C }
21 |
--------------------------------------------------------------------------------
/pkg/http2/unencrypted.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "crypto/tls"
9 | "errors"
10 | "net"
11 | )
12 |
13 | const nextProtoUnencryptedHTTP2 = "unencrypted_http2"
14 |
15 | // unencryptedNetConnFromTLSConn retrieves a net.Conn wrapped in a *tls.Conn.
16 | //
17 | // TLSNextProto functions accept a *tls.Conn.
18 | //
19 | // When passing an unencrypted HTTP/2 connection to a TLSNextProto function,
20 | // we pass a *tls.Conn with an underlying net.Conn containing the unencrypted connection.
21 | // To be extra careful about mistakes (accidentally dropping TLS encryption in a place
22 | // where we want it), the tls.Conn contains a net.Conn with an UnencryptedNetConn method
23 | // that returns the actual connection we want to use.
24 | func unencryptedNetConnFromTLSConn(tc *tls.Conn) (net.Conn, error) {
25 | conner, ok := tc.NetConn().(interface {
26 | UnencryptedNetConn() net.Conn
27 | })
28 | if !ok {
29 | return nil, errors.New("http2: TLS conn unexpectedly found in unencrypted handoff")
30 | }
31 | return conner.UnencryptedNetConn(), nil
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/http2/writesched_random.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import "math"
8 |
9 | // NewRandomWriteScheduler constructs a WriteScheduler that ignores HTTP/2
10 | // priorities. Control frames like SETTINGS and PING are written before DATA
11 | // frames, but if no control frames are queued and multiple streams have queued
12 | // HEADERS or DATA frames, Pop selects a ready stream arbitrarily.
13 | func NewRandomWriteScheduler() WriteScheduler {
14 | return &randomWriteScheduler{sq: make(map[uint32]*writeQueue)}
15 | }
16 |
17 | type randomWriteScheduler struct {
18 | // zero are frames not associated with a specific stream.
19 | zero writeQueue
20 |
21 | // sq contains the stream-specific queues, keyed by stream ID.
22 | // When a stream is idle, closed, or emptied, it's deleted
23 | // from the map.
24 | sq map[uint32]*writeQueue
25 |
26 | // pool of empty queues for reuse.
27 | queuePool writeQueuePool
28 | }
29 |
30 | func (ws *randomWriteScheduler) OpenStream(streamID uint32, options OpenStreamOptions) {
31 | // no-op: idle streams are not tracked
32 | }
33 |
34 | func (ws *randomWriteScheduler) CloseStream(streamID uint32) {
35 | q, ok := ws.sq[streamID]
36 | if !ok {
37 | return
38 | }
39 | delete(ws.sq, streamID)
40 | ws.queuePool.put(q)
41 | }
42 |
43 | func (ws *randomWriteScheduler) AdjustStream(streamID uint32, priority PriorityParam) {
44 | // no-op: priorities are ignored
45 | }
46 |
47 | func (ws *randomWriteScheduler) Push(wr FrameWriteRequest) {
48 | if wr.isControl() {
49 | ws.zero.push(wr)
50 | return
51 | }
52 | id := wr.StreamID()
53 | q, ok := ws.sq[id]
54 | if !ok {
55 | q = ws.queuePool.get()
56 | ws.sq[id] = q
57 | }
58 | q.push(wr)
59 | }
60 |
61 | func (ws *randomWriteScheduler) Pop() (FrameWriteRequest, bool) {
62 | // Control and RST_STREAM frames first.
63 | if !ws.zero.empty() {
64 | return ws.zero.shift(), true
65 | }
66 | // Iterate over all non-idle streams until finding one that can be consumed.
67 | for streamID, q := range ws.sq {
68 | if wr, ok := q.consume(math.MaxInt32); ok {
69 | if q.empty() {
70 | delete(ws.sq, streamID)
71 | ws.queuePool.put(q)
72 | }
73 | return wr, true
74 | }
75 | }
76 | return FrameWriteRequest{}, false
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/http2/writesched_random_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import "testing"
8 |
9 | func TestRandomScheduler(t *testing.T) {
10 | ws := NewRandomWriteScheduler()
11 | ws.Push(makeWriteHeadersRequest(3))
12 | ws.Push(makeWriteHeadersRequest(4))
13 | ws.Push(makeWriteHeadersRequest(1))
14 | ws.Push(makeWriteHeadersRequest(2))
15 | ws.Push(makeWriteNonStreamRequest())
16 | ws.Push(makeWriteNonStreamRequest())
17 | ws.Push(makeWriteRSTStream(1))
18 |
19 | // Pop all frames. Should get the non-stream and RST stream requests first,
20 | // followed by the stream requests in any order.
21 | var order []FrameWriteRequest
22 | for {
23 | wr, ok := ws.Pop()
24 | if !ok {
25 | break
26 | }
27 | order = append(order, wr)
28 | }
29 | t.Logf("got frames: %v", order)
30 | if len(order) != 7 {
31 | t.Fatalf("got %d frames, expected 6", len(order))
32 | }
33 | if order[0].StreamID() != 0 || order[1].StreamID() != 0 {
34 | t.Fatal("expected non-stream frames first", order[0], order[1])
35 | }
36 | if _, ok := order[2].write.(StreamError); !ok {
37 | t.Fatal("expected RST stream frames first", order[2])
38 | }
39 | got := make(map[uint32]bool)
40 | for _, wr := range order[2:] {
41 | got[wr.StreamID()] = true
42 | }
43 | for id := uint32(1); id <= 4; id++ {
44 | if !got[id] {
45 | t.Errorf("frame not found for stream %d", id)
46 | }
47 | }
48 |
49 | // Verify that we clean up maps for empty queues in all cases (golang.org/issue/33812)
50 | const arbitraryStreamID = 123
51 | ws.Push(makeHandlerPanicRST(arbitraryStreamID))
52 | rws := ws.(*randomWriteScheduler)
53 | if got, want := len(rws.sq), 1; got != want {
54 | t.Fatalf("len of 123 stream = %v; want %v", got, want)
55 | }
56 | _, ok := ws.Pop()
57 | if !ok {
58 | t.Fatal("expected to be able to Pop")
59 | }
60 | if got, want := len(rws.sq), 0; got != want {
61 | t.Fatalf("len of 123 stream = %v; want %v", got, want)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/pkg/http2/writesched_roundrobin.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "fmt"
9 | "math"
10 | )
11 |
12 | type roundRobinWriteScheduler struct {
13 | // control contains control frames (SETTINGS, PING, etc.).
14 | control writeQueue
15 |
16 | // streams maps stream ID to a queue.
17 | streams map[uint32]*writeQueue
18 |
19 | // stream queues are stored in a circular linked list.
20 | // head is the next stream to write, or nil if there are no streams open.
21 | head *writeQueue
22 |
23 | // pool of empty queues for reuse.
24 | queuePool writeQueuePool
25 | }
26 |
27 | // newRoundRobinWriteScheduler constructs a new write scheduler.
28 | // The round robin scheduler priorizes control frames
29 | // like SETTINGS and PING over DATA frames.
30 | // When there are no control frames to send, it performs a round-robin
31 | // selection from the ready streams.
32 | func newRoundRobinWriteScheduler() WriteScheduler {
33 | ws := &roundRobinWriteScheduler{
34 | streams: make(map[uint32]*writeQueue),
35 | }
36 | return ws
37 | }
38 |
39 | func (ws *roundRobinWriteScheduler) OpenStream(streamID uint32, options OpenStreamOptions) {
40 | if ws.streams[streamID] != nil {
41 | panic(fmt.Errorf("stream %d already opened", streamID))
42 | }
43 | q := ws.queuePool.get()
44 | ws.streams[streamID] = q
45 | if ws.head == nil {
46 | ws.head = q
47 | q.next = q
48 | q.prev = q
49 | } else {
50 | // Queues are stored in a ring.
51 | // Insert the new stream before ws.head, putting it at the end of the list.
52 | q.prev = ws.head.prev
53 | q.next = ws.head
54 | q.prev.next = q
55 | q.next.prev = q
56 | }
57 | }
58 |
59 | func (ws *roundRobinWriteScheduler) CloseStream(streamID uint32) {
60 | q := ws.streams[streamID]
61 | if q == nil {
62 | return
63 | }
64 | if q.next == q {
65 | // This was the only open stream.
66 | ws.head = nil
67 | } else {
68 | q.prev.next = q.next
69 | q.next.prev = q.prev
70 | if ws.head == q {
71 | ws.head = q.next
72 | }
73 | }
74 | delete(ws.streams, streamID)
75 | ws.queuePool.put(q)
76 | }
77 |
78 | func (ws *roundRobinWriteScheduler) AdjustStream(streamID uint32, priority PriorityParam) {}
79 |
80 | func (ws *roundRobinWriteScheduler) Push(wr FrameWriteRequest) {
81 | if wr.isControl() {
82 | ws.control.push(wr)
83 | return
84 | }
85 | q := ws.streams[wr.StreamID()]
86 | if q == nil {
87 | // This is a closed stream.
88 | // wr should not be a HEADERS or DATA frame.
89 | // We push the request onto the control queue.
90 | if wr.DataSize() > 0 {
91 | panic("add DATA on non-open stream")
92 | }
93 | ws.control.push(wr)
94 | return
95 | }
96 | q.push(wr)
97 | }
98 |
99 | func (ws *roundRobinWriteScheduler) Pop() (FrameWriteRequest, bool) {
100 | // Control and RST_STREAM frames first.
101 | if !ws.control.empty() {
102 | return ws.control.shift(), true
103 | }
104 | if ws.head == nil {
105 | return FrameWriteRequest{}, false
106 | }
107 | q := ws.head
108 | for {
109 | if wr, ok := q.consume(math.MaxInt32); ok {
110 | ws.head = q.next
111 | return wr, true
112 | }
113 | q = q.next
114 | if q == ws.head {
115 | break
116 | }
117 | }
118 | return FrameWriteRequest{}, false
119 | }
120 |
--------------------------------------------------------------------------------
/pkg/http2/writesched_roundrobin_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package http2
6 |
7 | import (
8 | "reflect"
9 | "testing"
10 | )
11 |
12 | func TestRoundRobinScheduler(t *testing.T) {
13 | const maxFrameSize = 16
14 | sc := &serverConn{maxFrameSize: maxFrameSize}
15 | ws := newRoundRobinWriteScheduler()
16 | streams := make([]*stream, 4)
17 | for i := range streams {
18 | streamID := uint32(i) + 1
19 | streams[i] = &stream{
20 | id: streamID,
21 | sc: sc,
22 | }
23 | streams[i].flow.add(1 << 20) // arbitrary large value
24 | ws.OpenStream(streamID, OpenStreamOptions{})
25 | wr := FrameWriteRequest{
26 | write: &writeData{
27 | streamID: streamID,
28 | p: make([]byte, maxFrameSize*(i+1)),
29 | endStream: false,
30 | },
31 | stream: streams[i],
32 | }
33 | ws.Push(wr)
34 | }
35 | const controlFrames = 2
36 | for i := 0; i < controlFrames; i++ {
37 | ws.Push(makeWriteNonStreamRequest())
38 | }
39 |
40 | // We should get the control frames first.
41 | for i := 0; i < controlFrames; i++ {
42 | wr, ok := ws.Pop()
43 | if !ok || wr.StreamID() != 0 {
44 | t.Fatalf("wr.Pop() = stream %v, %v; want 0, true", wr.StreamID(), ok)
45 | }
46 | }
47 |
48 | // Each stream should write maxFrameSize bytes until it runs out of data.
49 | // Stream 1 has one frame of data, 2 has two frames, etc.
50 | want := []uint32{1, 2, 3, 4, 2, 3, 4, 3, 4, 4}
51 | var got []uint32
52 | for {
53 | wr, ok := ws.Pop()
54 | if !ok {
55 | break
56 | }
57 | if wr.DataSize() != maxFrameSize {
58 | t.Fatalf("wr.Pop() = %v data bytes, want %v", wr.DataSize(), maxFrameSize)
59 | }
60 | got = append(got, wr.StreamID())
61 | }
62 | if !reflect.DeepEqual(got, want) {
63 | t.Fatalf("popped streams %v, want %v", got, want)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/ja3/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, Salesforce.com, Inc.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13 |
--------------------------------------------------------------------------------
/pkg/ja3/sync.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # The JA3 algoritm source code is cloned from https://github.com/dreadl0ck/ja3.
5 | #
6 | # This package imports https://github.com/google/gopacket which depends on pcap. However,
7 | # Fingerproxy does not need pcap and it causes trouble while cross compiling, for example,
8 | # it requires to install libpcap-dev which makes no sense for Fingerproxy. Therefore, we
9 | # decided to take the shortcut - copy-paste the algorithm part source code.
10 | #
11 |
12 | cd $(dirname "$0")
13 |
14 | wget https://raw.githubusercontent.com/dreadl0ck/ja3/master/ja3.go
15 |
--------------------------------------------------------------------------------
/pkg/ja4/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 FoxIO
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | * Neither the name of FoxIO nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13 |
--------------------------------------------------------------------------------
/pkg/ja4/helper.go:
--------------------------------------------------------------------------------
1 | package ja4
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha256"
6 | "fmt"
7 | "sort"
8 | )
9 |
10 | func sortUint16(sl []uint16) {
11 | sort.Slice(sl, func(x int, y int) bool { return sl[x] < sl[y] })
12 | }
13 |
14 | func joinUint16(slice []uint16, sep string) string {
15 | var buffer bytes.Buffer
16 | for i, u := range slice {
17 | if i != 0 {
18 | buffer.WriteString(sep)
19 | }
20 | buffer.WriteString(fmt.Sprintf("%04x", u))
21 | }
22 | return buffer.String()
23 | }
24 |
25 | func isGREASEUint16(v uint16) bool {
26 | // First byte is same as second byte
27 | // and lowest nibble is 0xa
28 | return ((v >> 8) == v&0xff) && v&0xf == 0xa
29 | }
30 |
31 | func truncatedSha256(in string) string {
32 | sha := sha256.New()
33 | sha.Write([]byte(in))
34 | return fmt.Sprintf("%x", sha.Sum(nil))[:12]
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/ja4/types.go:
--------------------------------------------------------------------------------
1 | package ja4
2 |
3 | import (
4 | "fmt"
5 |
6 | utls "github.com/refraction-networking/utls"
7 | )
8 |
9 | type (
10 | tlsVersion uint16
11 | numberOfCipherSuites int
12 | numberOfExtensions int
13 |
14 | cipherSuites []uint16
15 | extensions []uint16
16 | signatureAlgorithms []uint16
17 | )
18 |
19 | func (x tlsVersion) String() string {
20 | switch uint16(x) {
21 | case utls.VersionTLS10:
22 | return "10"
23 | case utls.VersionTLS11:
24 | return "11"
25 | case utls.VersionTLS12:
26 | return "12"
27 | case utls.VersionTLS13:
28 | return "13"
29 | }
30 | return "00"
31 | }
32 | func (x numberOfCipherSuites) String() string { return fmt.Sprintf("%02d", min(x, 99)) }
33 | func (x numberOfExtensions) String() string { return fmt.Sprintf("%02d", min(x, 99)) }
34 | func (x cipherSuites) String() string { return joinUint16(x, cipherSuitesSeparator) }
35 | func (x extensions) String() string { return joinUint16(x, extensionsSeparator) }
36 | func (x signatureAlgorithms) String() string { return joinUint16(x, signatureAlgorithmSeparator) }
37 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/pcap.go:
--------------------------------------------------------------------------------
1 | // Package `ja4pcap` is just a test helper for the `ja4` package that adopts
2 | // the test cases from the [official JA4 repo]. Fingerproxy does not import
3 | // this package.
4 | //
5 | // Maintainers should regularlly run ./sync-testdata.sh to follow the upstream.
6 | //
7 | // It is also a demonstration of using JA4 package with `gopacket`.
8 | //
9 | // [official JA4 repo]: https://github.com/FoxIO-LLC/ja4/tree/main/pcap
10 | package ja4pcap
11 |
12 | import (
13 | "errors"
14 | "io"
15 | "os"
16 | "slices"
17 |
18 | "cmp"
19 |
20 | "github.com/google/gopacket"
21 | "github.com/google/gopacket/layers"
22 | "github.com/google/gopacket/pcapgo"
23 | "github.com/wi1dcard/fingerproxy/pkg/ja4"
24 | )
25 |
26 | type pcapClientHello struct {
27 | tcpStreamTuple
28 | StreamIndex int
29 | JA4 string
30 | }
31 |
32 | type tcpStreamTuple struct {
33 | SrcIP string
34 | SrcPort uint16
35 | DstIP string
36 | DstPort uint16
37 | }
38 |
39 | // returns true if either:
40 | // a) t1.src == t2.src and t1.dst == t2.dst
41 | // b) t1.src == t2.dst and t1.dst == t2.src
42 | func (t1 *tcpStreamTuple) Equal(t2 tcpStreamTuple) bool {
43 | // match tuple [address A + port A, address B + port B]
44 | if t1.DstIP == t2.DstIP && t1.DstPort == t2.DstPort &&
45 | t1.SrcIP == t2.SrcIP && t1.SrcPort == t2.SrcPort {
46 | return true
47 | }
48 | if t1.DstIP == t2.SrcIP && t1.DstPort == t2.SrcPort &&
49 | t1.SrcIP == t2.DstIP && t1.SrcPort == t2.DstPort {
50 | return true
51 | }
52 | return false
53 | }
54 |
55 | func openPcap(f *os.File) (gopacket.PacketDataSource, layers.LinkType) {
56 | pcapReader, errPcap := pcapgo.NewReader(f)
57 | if errPcap == nil {
58 | return pcapReader, pcapReader.LinkType()
59 | }
60 |
61 | f.Seek(0, io.SeekStart)
62 | ngReader, errPcapNg := pcapgo.NewNgReader(f, pcapgo.DefaultNgReaderOptions)
63 | if errPcapNg != nil {
64 | panic(errPcapNg)
65 | }
66 |
67 | return ngReader, ngReader.LinkType()
68 | }
69 |
70 | var errPacketIsNotClientHello = errors.New("packet is not a client hello")
71 |
72 | func ja4FromPacket(tcp *layers.TCP) (string, error) {
73 | if tcp.SYN || tcp.FIN || tcp.RST {
74 | return "", errPacketIsNotClientHello
75 | }
76 |
77 | pl := tcp.LayerPayload()
78 | if len(pl) == 0 {
79 | return "", errPacketIsNotClientHello
80 | }
81 |
82 | j := ja4.JA4Fingerprint{}
83 | err := j.UnmarshalBytes(pl, 't')
84 | if err != nil {
85 | if ie := errors.Unwrap(err); ie != nil {
86 | err = ie
87 | }
88 | switch err.Error() {
89 | // utls cannot parse the ClientHello
90 | case "record is not a handshake":
91 | return "", errPacketIsNotClientHello
92 | case "handshake message is not a ClientHello":
93 | return "", errPacketIsNotClientHello
94 | case "unable to read record type, version, and length":
95 | return "", errPacketIsNotClientHello
96 | case "unable to read handshake message type, length, and random":
97 | return "", errPacketIsNotClientHello
98 | // otherwise returns error
99 | default:
100 | return "", err
101 | }
102 | }
103 |
104 | return j.String(), nil
105 | }
106 |
107 | func readPcap(file string) []pcapClientHello {
108 | f, err := os.Open(file)
109 | if err != nil {
110 | panic(err)
111 | }
112 | defer f.Close()
113 |
114 | packetDataSource, linkType := openPcap(f)
115 | packetSource := gopacket.NewPacketSource(packetDataSource, linkType)
116 |
117 | var clientHellos []pcapClientHello
118 | var tcpStreams []tcpStreamTuple
119 |
120 | for p := range packetSource.Packets() {
121 | ch := pcapClientHello{}
122 | srcdst := tcpStreamTuple{}
123 |
124 | if ipv4Layer := p.Layer(layers.LayerTypeIPv4); ipv4Layer != nil {
125 | ip, _ := ipv4Layer.(*layers.IPv4)
126 | srcdst.DstIP = ip.DstIP.String()
127 | srcdst.SrcIP = ip.SrcIP.String()
128 | } else if ipv6Layer := p.Layer(layers.LayerTypeIPv6); ipv6Layer != nil {
129 | ip, _ := ipv6Layer.(*layers.IPv6)
130 | srcdst.DstIP = ip.DstIP.String()
131 | srcdst.SrcIP = ip.SrcIP.String()
132 | } else {
133 | // not IPv4 or IPv6
134 | continue
135 | }
136 |
137 | tcpLayer := p.Layer(layers.LayerTypeTCP)
138 | if tcpLayer == nil {
139 | // not TCP
140 | continue
141 | }
142 |
143 | tcp, _ := tcpLayer.(*layers.TCP)
144 | srcdst.DstPort = uint16(tcp.DstPort)
145 | srcdst.SrcPort = uint16(tcp.SrcPort)
146 |
147 | // get wireshark stream index before parsing JA4
148 | ch.StreamIndex = slices.IndexFunc(tcpStreams, srcdst.Equal)
149 | if ch.StreamIndex == -1 {
150 | ch.StreamIndex = len(tcpStreams)
151 | tcpStreams = append(tcpStreams, srcdst)
152 | }
153 |
154 | ch.JA4, err = ja4FromPacket(tcp)
155 | if err == nil {
156 | ch.tcpStreamTuple = srcdst
157 | clientHellos = append(clientHellos, ch)
158 | } else if !errors.Is(err, errPacketIsNotClientHello) {
159 | panic(err)
160 | }
161 | }
162 |
163 | // sort by stream index
164 | slices.SortFunc(clientHellos, func(x pcapClientHello, y pcapClientHello) int {
165 | return cmp.Compare(x.StreamIndex, y.StreamIndex)
166 | })
167 |
168 | return clientHellos
169 | }
170 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/pcap_test.go:
--------------------------------------------------------------------------------
1 | package ja4pcap
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "testing"
7 |
8 | "gopkg.in/yaml.v3"
9 | )
10 |
11 | type snap struct {
12 | StreamIndex int `yaml:"stream"`
13 | Transport string `yaml:"transport"`
14 |
15 | SrcIP string `yaml:"src"`
16 | SrcPort uint16 `yaml:"src_port"`
17 | DstIP string `yaml:"dst"`
18 | DstPort uint16 `yaml:"dst_port"`
19 |
20 | JA4 string `yaml:"ja4"`
21 | }
22 |
23 | func TestPcap(t *testing.T) {
24 | pcapFiles, err := os.ReadDir("testdata/pcap/")
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 |
29 | for _, pf := range pcapFiles {
30 | if pf.IsDir() {
31 | continue
32 | }
33 |
34 | t.Run(pf.Name(), func(t *testing.T) {
35 | clientHellos := readPcap("testdata/pcap/" + pf.Name())
36 | snapFilename := fmt.Sprintf("testdata/snapshots/ja4__insta@%s.snap", pf.Name())
37 | clientHelloSnapshots := readSnapshot(t, snapFilename)
38 |
39 | expLen := len(clientHelloSnapshots)
40 | if actLen := len(clientHellos); expLen != actLen {
41 | t.Fatalf("expected %d client hello records, actual %d", expLen, actLen)
42 | }
43 |
44 | for i := 0; i < expLen; i++ {
45 | exp := clientHelloSnapshots[i]
46 | act := clientHellos[i]
47 |
48 | if act.StreamIndex != exp.StreamIndex {
49 | t.Errorf("expected stream index %d, actual %d", exp.StreamIndex, act.StreamIndex)
50 | }
51 | if act.DstIP != exp.DstIP {
52 | t.Errorf("[%d] expected dst IP %s, actual %s", exp.StreamIndex, exp.DstIP, act.DstIP)
53 | }
54 | if act.DstPort != exp.DstPort {
55 | t.Errorf("[%d] expected dst port %d, actual %d", exp.StreamIndex, exp.DstPort, act.DstPort)
56 | }
57 | if act.SrcIP != exp.SrcIP {
58 | t.Errorf("[%d] expected src IP %s, actual %s", exp.StreamIndex, exp.SrcIP, act.SrcIP)
59 | }
60 | if act.SrcPort != exp.SrcPort {
61 | t.Errorf("[%d] expected src port %d, actual %d", exp.StreamIndex, exp.SrcPort, act.SrcPort)
62 | }
63 | if act.JA4 != exp.JA4 {
64 | t.Errorf("[%d] expected JA4 fingerprint %s, actual %s", exp.StreamIndex, exp.JA4, act.JA4)
65 | }
66 | }
67 | })
68 | }
69 | }
70 |
71 | func readSnapshot(t *testing.T, snapFilename string) []snap {
72 | t.Helper()
73 |
74 | snapFileBytes, err := os.Open(snapFilename)
75 | if err != nil {
76 | t.Fatal(err)
77 | }
78 | defer snapFileBytes.Close()
79 |
80 | var (
81 | snapshots, filteredSnapshots []snap
82 | unusedFirstYamlDoc struct{}
83 | )
84 |
85 | decoder := yaml.NewDecoder(snapFileBytes)
86 | decoder.Decode(&unusedFirstYamlDoc)
87 | decoder.Decode(&snapshots)
88 |
89 | for _, sn := range snapshots {
90 | // udp not supported yet
91 | if sn.JA4 == "" || sn.Transport == "udp" {
92 | continue
93 | }
94 |
95 | filteredSnapshots = append(filteredSnapshots, sn)
96 | }
97 |
98 | return filteredSnapshots
99 | }
100 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/sync-testdata.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S bash -exuo pipefail
2 |
3 | #
4 | # Sync the test cases from the official JA4 repo.
5 | #
6 |
7 | cd $(dirname "$0")
8 |
9 | HEAD=main
10 | HEAD_ARCHIVE_FILENAME=$HEAD.tar.gz
11 | LOCAL_ARCHIVE_FILENAME=/tmp/$HEAD_ARCHIVE_FILENAME
12 |
13 | wget -O $LOCAL_ARCHIVE_FILENAME https://github.com/FoxIO-LLC/ja4/archive/refs/heads/$HEAD_ARCHIVE_FILENAME
14 |
15 | TMP_SRCDIR=$(mktemp -d)
16 | TARBALL_ROOTDIR=$(tar tf $LOCAL_ARCHIVE_FILENAME | head -n1)
17 |
18 | tar xzf $LOCAL_ARCHIVE_FILENAME --directory $TMP_SRCDIR
19 |
20 | rsync -avhW --no-compress --delete $TMP_SRCDIR/$TARBALL_ROOTDIR/pcap/ ./testdata/pcap/
21 | rsync -avhW --no-compress --delete $TMP_SRCDIR/$TARBALL_ROOTDIR/rust/ja4/src/snapshots/ ./testdata/snapshots/
22 |
23 | rm -rf $TMP_SRCDIR
24 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/CVE-2018-6794.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/CVE-2018-6794.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/badcurveball.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/badcurveball.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/browsers-x509.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/browsers-x509.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/chrome-cloudflare-quic-with-secrets.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/chrome-cloudflare-quic-with-secrets.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/gre-erspan-vxlan.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/gre-erspan-vxlan.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/gre-sample.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/gre-sample.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/http1-with-cookies.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/http1-with-cookies.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/http1.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/http1.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/http2-with-cookies.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/http2-with-cookies.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ipv6.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ipv6.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/latest.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/latest.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/macos_tcp_flags.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/macos_tcp_flags.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/quic-tls-handshake.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/quic-tls-handshake.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/quic-with-several-tls-frames.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/quic-with-several-tls-frames.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/single-packets.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/single-packets.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/socks4-https.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/socks4-https.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ssh-r.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ssh-r.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ssh-scp-1050.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ssh-scp-1050.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ssh.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ssh.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ssh2-malformed.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ssh2-malformed.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ssh2-moloch-crash.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ssh2-moloch-crash.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/ssh2.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/ssh2.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/sshv1.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/sshv1.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tcpdump-geneve.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tcpdump-geneve.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tls-alpn-h2.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tls-alpn-h2.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tls-handshake.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tls-handshake.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tls-non-ascii-alpn.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tls-non-ascii-alpn.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tls-sni.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tls-sni.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tls12.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tls12.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/tls3.pcapng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/tls3.pcapng
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/pcap/v6.pcap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wi1dcard/fingerproxy/bcda1920cb3665dedcafebef8eaf008e74393fa9/pkg/ja4pcap/testdata/pcap/v6.pcap
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@CVE-2018-6794.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.235.1
8 | dst: 192.168.235.136
9 | src_port: 53649
10 | dst_port: 8089
11 | ja4l_c: 0_128
12 | ja4l_s: 2219_255
13 | http:
14 | - ja4h: ge11nn07ruru_6cd0fb54989b_000000000000_000000000000
15 | - stream: 1
16 | transport: tcp
17 | src: 192.168.235.1
18 | dst: 192.168.235.136
19 | src_port: 53656
20 | dst_port: 8089
21 | ja4l_c: 0_128
22 | ja4l_s: 1513_255
23 | http:
24 | - ja4h: ge11nr06ruru_cc6ec9a91856_000000000000_000000000000
25 | - stream: 2
26 | transport: tcp
27 | src: 192.168.235.1
28 | dst: 192.168.235.136
29 | src_port: 53648
30 | dst_port: 8089
31 | ja4l_c: 0_128
32 | ja4l_s: 1948_255
33 |
34 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@badcurveball.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 172.130.128.76
8 | dst: 54.226.182.138
9 | src_port: 55318
10 | dst_port: 443
11 | tls_server_name: bad.curveballtest.com
12 | ja4: t13d1615h2_46e7e9700bed_45f260be83e2
13 | ja4s: t1205h1_c02b_845f7282a956
14 | tls_certs:
15 | - x509:
16 | - ja4x: 2e9214a636bc_a373a9f83c6b_0e17604154c5
17 | issuerCountryName: HR
18 | issuerStateOrProvinceName: Zagreb
19 | issuerOrganizationName: INFIGO IS
20 | issuerCommonName: INFIGO
21 | subjectCountryName: US
22 | subjectOrganizationName: SANS Internet Storm Center
23 | subjectCommonName: SANS ISC DShield Test
24 | - ja4x: 2e9214a636bc_2e9214a636bc_795797892f9c
25 | issuerCountryName: HR
26 | issuerStateOrProvinceName: Zagreb
27 | issuerOrganizationName: INFIGO IS
28 | issuerCommonName: INFIGO
29 | subjectCountryName: HR
30 | subjectStateOrProvinceName: Zagreb
31 | subjectOrganizationName: INFIGO IS
32 | subjectCommonName: INFIGO
33 | ja4l_c: 2177_64
34 | ja4l_s: 781_238
35 |
36 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@browsers-x509.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 172.27.7.31
8 | dst: 13.107.21.239
9 | src_port: 54524
10 | dst_port: 443
11 | tls_server_name: edge.microsoft.com
12 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
13 | ja4s: t1206h2_c030_044dc9b3196d
14 | tls_certs:
15 | - x509:
16 | - ja4x: a373a9f83c6b_2bab15409345_0f2217ba412e
17 | issuerCountryName: US
18 | issuerOrganizationName: Microsoft Corporation
19 | issuerCommonName: Microsoft Azure TLS Issuing CA 05
20 | subjectCountryName: US
21 | subjectStateOrProvinceName: WA
22 | subjectLocalityName: Redmond
23 | subjectOrganizationName: Microsoft Corporation
24 | subjectCommonName: edge.microsoft.com
25 | - ja4x: 7d5dbb3783b4_a373a9f83c6b_c34b04c10969
26 | issuerCountryName: US
27 | issuerOrganizationName: DigiCert Inc
28 | issuerOrganizationalUnit: www.digicert.com
29 | issuerCommonName: DigiCert Global Root G2
30 | subjectCountryName: US
31 | subjectOrganizationName: Microsoft Corporation
32 | subjectCommonName: Microsoft Azure TLS Issuing CA 05
33 | ja4l_c: 56_128
34 | ja4l_s: 1907_112
35 | - stream: 1
36 | transport: tcp
37 | src: 172.27.7.31
38 | dst: 68.67.160.117
39 | src_port: 54525
40 | dst_port: 443
41 | tls_server_name: nym1-ib.adnxs.com
42 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
43 | ja4s: t1207h2_c02b_cf25e267ce22
44 | tls_certs:
45 | - x509:
46 | - ja4x: 7d5dbb3783b4_2bab15409345_7bf9a7bf7029
47 | issuerCountryName: US
48 | issuerOrganizationName: DigiCert Inc
49 | issuerOrganizationalUnit: www.digicert.com
50 | issuerCommonName: GeoTrust ECC CA 2018
51 | subjectCountryName: US
52 | subjectStateOrProvinceName: New York
53 | subjectLocalityName: New York
54 | subjectOrganizationName: Xandr Inc.
55 | subjectCommonName: '*.adnxs.com'
56 | - ja4x: 7d5dbb3783b4_7d5dbb3783b4_44440d41940c
57 | issuerCountryName: US
58 | issuerOrganizationName: DigiCert Inc
59 | issuerOrganizationalUnit: www.digicert.com
60 | issuerCommonName: DigiCert Global Root CA
61 | subjectCountryName: US
62 | subjectOrganizationName: DigiCert Inc
63 | subjectOrganizationalUnit: www.digicert.com
64 | subjectCommonName: GeoTrust ECC CA 2018
65 | ja4l_c: 73_128
66 | ja4l_s: 7166_41
67 | - stream: 2
68 | transport: tcp
69 | src: 172.27.7.31
70 | dst: 103.42.133.15
71 | src_port: 54603
72 | dst_port: 443
73 | tls_server_name: lptag.liveperson.net
74 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
75 | ja4s: t1205h2_c02f_845f7282a956
76 | tls_certs:
77 | - x509:
78 | - ja4x: 2bab15409345_2e9214a636bc_b891c0ad6f32
79 | issuerCountryName: GB
80 | issuerStateOrProvinceName: Greater Manchester
81 | issuerLocalityName: Salford
82 | issuerOrganizationName: Sectigo Limited
83 | issuerCommonName: Sectigo RSA Organization Validation Secure Server CA
84 | subjectCountryName: US
85 | subjectStateOrProvinceName: New York
86 | subjectOrganizationName: LivePerson, Inc
87 | subjectCommonName: '*.liveperson.net'
88 | - ja4x: 2bab15409345_2bab15409345_2367ce7fbc5b
89 | issuerCountryName: US
90 | issuerStateOrProvinceName: New Jersey
91 | issuerLocalityName: Jersey City
92 | issuerOrganizationName: The USERTRUST Network
93 | issuerCommonName: USERTrust RSA Certification Authority
94 | subjectCountryName: GB
95 | subjectStateOrProvinceName: Greater Manchester
96 | subjectLocalityName: Salford
97 | subjectOrganizationName: Sectigo Limited
98 | subjectCommonName: Sectigo RSA Organization Validation Secure Server CA
99 | - ja4x: 2bab15409345_2bab15409345_2030e37f3421
100 | issuerCountryName: GB
101 | issuerStateOrProvinceName: Greater Manchester
102 | issuerLocalityName: Salford
103 | issuerOrganizationName: Comodo CA Limited
104 | issuerCommonName: AAA Certificate Services
105 | subjectCountryName: US
106 | subjectStateOrProvinceName: New Jersey
107 | subjectLocalityName: Jersey City
108 | subjectOrganizationName: The USERTRUST Network
109 | subjectCommonName: USERTrust RSA Certification Authority
110 | ja4l_c: 78_128
111 | ja4l_s: 2948_229
112 |
113 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@chrome-cloudflare-quic-with-secrets.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 2001:db8:1::1
8 | dst: 2606:4700:10::6816:826
9 | src_port: 57098
10 | dst_port: 443
11 | tls_server_name: cloudflare-quic.com
12 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
13 | ja4s: t130200_1301_234ea6891581
14 | ja4l_c: 30_64
15 | ja4l_s: 5749_56
16 | http:
17 | - ja4h: ge20nn16enus_0f5a7a41a252_000000000000_000000000000
18 | - stream: 0
19 | transport: udp
20 | src: 2001:db8:1::1
21 | dst: 2606:4700:10::6816:826
22 | src_port: 50280
23 | dst_port: 443
24 | tls_server_name: cloudflare-quic.com
25 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
26 | ja4s: q130200_1301_234ea6891581
27 | ja4l_c: 113_64
28 | ja4l_s: 9285_56
29 |
30 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@gre-erspan-vxlan.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 10.16.27.12
8 | dst: 10.16.27.131
9 | src_port: 65174
10 | dst_port: 80
11 | ja4l_c: 953_64
12 | ja4l_s: 997_64
13 |
14 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@gre-sample.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 66.59.111.190
8 | dst: 172.28.2.3
9 | src_port: 40264
10 | dst_port: 22
11 | ja4l_c: 36_255
12 | ja4l_s: 22952_236
13 | ja4ssh:
14 | - c24s23_c4s4_c6s4
15 | ssh_extras:
16 | hassh: 5ef6678a6b060094834599ca16581b05
17 | hassh_server: 6e3242d64766f4154c11858bbd654415
18 | ssh_protocol_client: SSH-2.0-OpenSSH_3.6.1p1
19 | ssh_protocol_server: SSH-1.99-OpenSSH_3.1p1
20 | encryption_algorithm: aes128-cbc
21 |
22 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@gtp-iphone.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: fd00:183:1:1:1886:9040:8605:32b8
8 | dst: fd01::183
9 | src_port: 5060
10 | dst_port: 5060
11 | ja4l_c: 15922_64
12 | ja4l_s: 53_64
13 | - stream: 1
14 | transport: tcp
15 | src: fd00:183:1:1:1886:9040:8605:32b8
16 | dst: fd01::183
17 | src_port: 5060
18 | dst_port: 5060
19 | ja4l_c: 16068_64
20 | ja4l_s: 52_64
21 | - stream: 2
22 | transport: tcp
23 | src: fd00:183:1:1:1886:9040:8605:32b8
24 | dst: fd01::183
25 | src_port: 5060
26 | dst_port: 5060
27 | ja4l_c: 19889_64
28 | ja4l_s: 174_64
29 | - stream: 3
30 | transport: tcp
31 | src: fd00:183:1:1:1886:9040:8605:32b8
32 | dst: fd01::183
33 | src_port: 5060
34 | dst_port: 5060
35 | ja4l_c: 19962_64
36 | ja4l_s: 35_64
37 |
38 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@http1-with-cookies.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 127.0.0.1
8 | dst: 127.0.0.1
9 | src_port: 61256
10 | dst_port: 8000
11 | ja4l_c: 14_64
12 | ja4l_s: 64_64
13 | http:
14 | - ja4h: ge11cr04da00_8ddaef5d77af_280f366eaa04_c2fb0fe53442
15 |
16 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@http2-with-cookies.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.2.200
8 | dst: 142.250.187.206
9 | src_port: 58847
10 | dst_port: 443
11 | tls_server_name: youtube.com
12 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
13 | ja4s: t130200_1301_234ea6891581
14 | tls_certs:
15 | - x509:
16 | - ja4x: a373a9f83c6b_7022c563de38_2e3757343cb0
17 | issuerCountryName: US
18 | issuerOrganizationName: Google Trust Services LLC
19 | issuerCommonName: GTS CA 1C3
20 | subjectCommonName: '*.google.com'
21 | - ja4x: a373a9f83c6b_a373a9f83c6b_5d71497f7704
22 | issuerCountryName: US
23 | issuerOrganizationName: Google Trust Services LLC
24 | issuerCommonName: GTS Root R1
25 | subjectCountryName: US
26 | subjectOrganizationName: Google Trust Services LLC
27 | subjectCommonName: GTS CA 1C3
28 | - ja4x: 7d5dbb3783b4_a373a9f83c6b_2fbee3f04f3b
29 | issuerCountryName: BE
30 | issuerOrganizationName: GlobalSign nv-sa
31 | issuerOrganizationalUnit: Root CA
32 | issuerCommonName: GlobalSign Root CA
33 | subjectCountryName: US
34 | subjectOrganizationName: Google Trust Services LLC
35 | subjectCommonName: GTS Root R1
36 | ja4l_c: 47_128
37 | ja4l_s: 44840_117
38 | http:
39 | - ja4h: ge20cn23enus_641f0b6ae3f0_c7713052b7e4_348cad68b6fb
40 | - ja4h: ge20cn17enus_949f364da66f_e43af2e8abfe_015bb0ca5596
41 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
42 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
43 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
44 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
45 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
46 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
47 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
48 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
49 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
50 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
51 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
52 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
53 | - ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
54 |
55 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@ipv6.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 2001:4998:ef83:14:8000::100d
8 | dst: 2606:4700::6811:d209
9 | src_port: 64034
10 | dst_port: 443
11 | tls_server_name: www.cloudflare.com
12 | ja4: t12d4605h2_85626a9a5f7f_aaf95bb78ec9
13 | ja4s: t1204h2_cca9_1428ce7b4018
14 | tls_certs:
15 | - x509:
16 | - ja4x: 7d5dbb3783b4_ba7ce0880c07_7bf9a7bf7029
17 | issuerCountryName: US
18 | issuerOrganizationName: DigiCert Inc
19 | issuerOrganizationalUnit: www.digicert.com
20 | issuerCommonName: DigiCert ECC Extended Validation Server CA
21 | subjectBusinessCategory: Private Organization
22 | subjectMsJurisdictionCountry: US
23 | subjectMsJurisdictionStateOrProvince: Delaware
24 | subjectSerialNumber: '4710875'
25 | subjectCountryName: US
26 | subjectStateOrProvinceName: California
27 | subjectLocalityName: San Francisco
28 | subjectOrganizationName: Cloudflare, Inc.
29 | subjectCommonName: cloudflare.com
30 | - ja4x: 7d5dbb3783b4_7d5dbb3783b4_41a019652939
31 | issuerCountryName: US
32 | issuerOrganizationName: DigiCert Inc
33 | issuerOrganizationalUnit: www.digicert.com
34 | issuerCommonName: DigiCert High Assurance EV Root CA
35 | subjectCountryName: US
36 | subjectOrganizationName: DigiCert Inc
37 | subjectOrganizationalUnit: www.digicert.com
38 | subjectCommonName: DigiCert ECC Extended Validation Server CA
39 | ja4l_c: 35_64
40 | ja4l_s: 18861_59
41 |
42 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@latest.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 1
6 | transport: tcp
7 | src: 172.16.225.48
8 | dst: 34.212.93.65
9 | src_port: 52936
10 | dst_port: 443
11 | tls_server_name: pdx-col.eum-appdynamics.com
12 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
13 | ja4s: t1206h2_c02f_3603f09c43ba
14 | tls_certs:
15 | - x509:
16 | - ja4x: a373a9f83c6b_2bab15409345_7bf9a7bf7029
17 | issuerCountryName: US
18 | issuerOrganizationName: DigiCert Inc
19 | issuerCommonName: DigiCert Global G2 TLS RSA SHA256 2020 CA1
20 | subjectCountryName: US
21 | subjectStateOrProvinceName: California
22 | subjectLocalityName: San Francisco
23 | subjectOrganizationName: AppDynamics LLC
24 | subjectCommonName: '*.eum-appdynamics.com'
25 | - ja4x: 7d5dbb3783b4_a373a9f83c6b_a83ffcd6e6c2
26 | issuerCountryName: US
27 | issuerOrganizationName: DigiCert Inc
28 | issuerOrganizationalUnit: www.digicert.com
29 | issuerCommonName: DigiCert Global Root G2
30 | subjectCountryName: US
31 | subjectOrganizationName: DigiCert Inc
32 | subjectCommonName: DigiCert Global G2 TLS RSA SHA256 2020 CA1
33 | ja4l_c: 62_128
34 | ja4l_s: 33804_227
35 | - stream: 3
36 | transport: tcp
37 | src: 172.16.225.48
38 | dst: 13.33.165.101
39 | src_port: 52937
40 | dst_port: 443
41 | tls_server_name: discovery.cem.cloud.us
42 | ja4: t12d190800_d83cc789557e_7af1ed941c26
43 | ja4s: t120600_c02f_51ad275821ba
44 | tls_certs:
45 | - x509:
46 | - ja4x: a373a9f83c6b_2bab15409345_7bf9a7bf7029
47 | issuerCountryName: US
48 | issuerOrganizationName: DigiCert Inc
49 | issuerCommonName: DigiCert TLS RSA SHA256 2020 CA1
50 | subjectCountryName: US
51 | subjectStateOrProvinceName: Florida
52 | subjectLocalityName: Fort Lauderdale
53 | subjectOrganizationName: Citrix Systems, Inc.
54 | subjectCommonName: '*.cem.cloud.us'
55 | - ja4x: 7d5dbb3783b4_a373a9f83c6b_a83ffcd6e6c2
56 | issuerCountryName: US
57 | issuerOrganizationName: DigiCert Inc
58 | issuerOrganizationalUnit: www.digicert.com
59 | issuerCommonName: DigiCert Global Root CA
60 | subjectCountryName: US
61 | subjectOrganizationName: DigiCert Inc
62 | subjectCommonName: DigiCert TLS RSA SHA256 2020 CA1
63 | ja4l_c: 57_128
64 | ja4l_s: 7096_245
65 | - stream: 5
66 | transport: tcp
67 | src: 172.16.225.48
68 | dst: 34.205.195.66
69 | src_port: 52938
70 | dst_port: 443
71 | tls_server_name: app.slack.com
72 | ja4: t13d1516h2_8daaf6152771_9b887d9acb53
73 | ja4s: t130300_1301_6bbbaf601ed8
74 | ja4l_c: 47_128
75 | ja4l_s: 14207_43
76 | - stream: 6
77 | transport: tcp
78 | src: 172.16.225.48
79 | dst: 23.43.242.57
80 | src_port: 52939
81 | dst_port: 80
82 | ja4l_c: 32_128
83 | ja4l_s: 3915_57
84 | http:
85 | - ja4h: ge11nn07enus_3e3b55d61660_000000000000_000000000000
86 | - stream: 9
87 | transport: tcp
88 | src: 172.16.225.48
89 | dst: 52.249.29.248
90 | src_port: 52940
91 | dst_port: 443
92 | tls_server_name: ping-edge.smartscreen.microsoft.com
93 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
94 | ja4s: t120300_c030_09f674154ab3
95 | tls_certs:
96 | - x509:
97 | - ja4x: a373a9f83c6b_2bab15409345_0f2217ba412e
98 | issuerCountryName: US
99 | issuerOrganizationName: Microsoft Corporation
100 | issuerCommonName: Microsoft Azure TLS Issuing CA 05
101 | subjectCountryName: US
102 | subjectStateOrProvinceName: WA
103 | subjectLocalityName: Redmond
104 | subjectOrganizationName: Microsoft Corporation
105 | subjectCommonName: smartscreen.microsoft.com
106 | - ja4x: 7d5dbb3783b4_a373a9f83c6b_c34b04c10969
107 | issuerCountryName: US
108 | issuerOrganizationName: DigiCert Inc
109 | issuerOrganizationalUnit: www.digicert.com
110 | issuerCommonName: DigiCert Global Root G2
111 | subjectCountryName: US
112 | subjectOrganizationName: Microsoft Corporation
113 | subjectCommonName: Microsoft Azure TLS Issuing CA 05
114 | ja4l_c: 40_128
115 | ja4l_s: 42103_109
116 | - stream: 10
117 | transport: tcp
118 | src: 172.16.225.48
119 | dst: 52.249.29.248
120 | src_port: 52941
121 | dst_port: 443
122 | tls_server_name: data-edge.smartscreen.microsoft.com
123 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
124 | ja4s: t120300_c030_09f674154ab3
125 | tls_certs:
126 | - x509:
127 | - ja4x: a373a9f83c6b_2bab15409345_0f2217ba412e
128 | issuerCountryName: US
129 | issuerOrganizationName: Microsoft Corporation
130 | issuerCommonName: Microsoft Azure TLS Issuing CA 05
131 | subjectCountryName: US
132 | subjectStateOrProvinceName: WA
133 | subjectLocalityName: Redmond
134 | subjectOrganizationName: Microsoft Corporation
135 | subjectCommonName: smartscreen.microsoft.com
136 | - ja4x: 7d5dbb3783b4_a373a9f83c6b_c34b04c10969
137 | issuerCountryName: US
138 | issuerOrganizationName: DigiCert Inc
139 | issuerOrganizationalUnit: www.digicert.com
140 | issuerCommonName: DigiCert Global Root G2
141 | subjectCountryName: US
142 | subjectOrganizationName: Microsoft Corporation
143 | subjectCommonName: Microsoft Azure TLS Issuing CA 05
144 | ja4l_c: 61_128
145 | ja4l_s: 53595_109
146 |
147 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@macos_tcp_flags.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 172.16.5.16
8 | dst: 172.67.24.71
9 | src_port: 61311
10 | dst_port: 443
11 | tls_server_name: venarisecurity.com
12 | ja4: t13d2613h2_2802a3db6c62_845d286b0d67
13 | ja4s: t130200_1301_234ea6891581
14 | ja4l_c: 62_64
15 | ja4l_s: 17255_63
16 |
17 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@quic-tls-handshake.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: udp
7 | src: 192.168.1.168
8 | dst: 142.251.163.147
9 | src_port: 59102
10 | dst_port: 443
11 | tls_server_name: www.google.com
12 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
13 |
14 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@quic-with-several-tls-frames.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: udp
7 | src: 192.168.1.168
8 | dst: 142.251.16.100
9 | src_port: 55906
10 | dst_port: 443
11 | tls_server_name: ogs.google.com
12 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
13 |
14 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@single-packets.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.25.150
8 | dst: 74.125.24.149
9 | src_port: 49677
10 | dst_port: 80
11 | http:
12 | - ja4h: ge11cr06enus_8c2f9ef95269_2a79f5d9f8b3_7b4d78c057bc
13 | - stream: 1
14 | transport: tcp
15 | src: 192.168.25.150
16 | dst: 118.215.80.242
17 | src_port: 49654
18 | dst_port: 80
19 | http:
20 | - ja4h: ge11cr07enus_45c71a3fb6ea_a25bf252eb59_43a9e3e95c85
21 | - stream: 2
22 | transport: tcp
23 | src: 192.168.25.150
24 | dst: 13.115.50.210
25 | src_port: 49683
26 | dst_port: 80
27 | http:
28 | - ja4h: ge11nr06enus_8c2f9ef95269_000000000000_000000000000
29 | - stream: 3
30 | transport: tcp
31 | src: 192.168.25.150
32 | dst: 104.89.119.175
33 | src_port: 49708
34 | dst_port: 80
35 | http:
36 | - ja4h: po11cr09enus_130d8cd1913c_f81c0e5c6793_90689f748de6
37 | - stream: 4
38 | transport: tcp
39 | src: 192.168.25.150
40 | dst: 193.242.192.43
41 | src_port: 49735
42 | dst_port: 80
43 | http:
44 | - ja4h: ge11cr07enus_45c71a3fb6ea_9ee64e91aa30_109254663367
45 | - stream: 5
46 | transport: tcp
47 | src: 192.168.25.150
48 | dst: 74.125.24.100
49 | src_port: 49733
50 | dst_port: 80
51 | http:
52 | - ja4h: ge11nr06enus_8c2f9ef95269_000000000000_000000000000
53 | - stream: 6
54 | transport: tcp
55 | src: 192.168.25.150
56 | dst: 74.125.24.95
57 | src_port: 49743
58 | dst_port: 80
59 | http:
60 | - ja4h: ge11nr06enus_8c2f9ef95269_000000000000_000000000000
61 | - stream: 7
62 | transport: tcp
63 | src: 192.168.25.150
64 | dst: 35.174.150.168
65 | src_port: 49738
66 | dst_port: 80
67 | http:
68 | - ja4h: ge11cr06enus_8c2f9ef95269_d23bf79698dc_69e42fa741fe
69 |
70 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@socks4-https.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 10.0.0.1
8 | dst: 10.0.0.2
9 | src_port: 50606
10 | dst_port: 9901
11 | ja4l_c: 119349_126
12 | ja4l_s: 40155_52
13 |
14 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@ssh-r.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.1.169
8 | dst: 192.168.1.197
9 | src_port: 64980
10 | dst_port: 22
11 | ja4l_c: 94_128
12 | ja4l_s: 32_64
13 | ja4ssh:
14 | - c64s64_c107s93_c74s10
15 | - c64s64_c33s48_c42s2
16 | ssh_extras:
17 | hassh: e77c2db7432e8cfbc42a96909a84fc8e
18 | hassh_server: 6832f1ce43d4397c2c0a3e2f8c94334e
19 | ssh_protocol_client: SSH-2.0-PuTTY_Release_0.74
20 | ssh_protocol_server: SSH-2.0-OpenSSH_7.4
21 | encryption_algorithm: chacha20-poly1305@openssh.com
22 | - stream: 1
23 | transport: tcp
24 | src: 192.168.1.197
25 | dst: 44.212.59.210
26 | src_port: 46394
27 | dst_port: 22
28 | ja4l_c: 14_64
29 | ja4l_s: 4171_116
30 | ja4ssh:
31 | - c48s21_c7s5_c5s5
32 | ssh_extras:
33 | hassh: ec9ea89c70f5fc71cf61061bff5e4740
34 | hassh_server: 2307c390c7c9aba5b4c9519e72347f34
35 | ssh_protocol_client: SSH-2.0-OpenSSH_7.4
36 | ssh_protocol_server: SSH-2.0-OpenSSH_8.7
37 | encryption_algorithm: aes256-gcm@openssh.com
38 | - stream: 2
39 | transport: tcp
40 | src: 192.168.1.197
41 | dst: 44.212.59.210
42 | src_port: 46396
43 | dst_port: 22
44 | ja4l_c: 12_64
45 | ja4l_s: 3169_116
46 | ja4ssh:
47 | - c76s76_c104s96_c19s82
48 | - c76s76_c108s92_c0s105
49 | - c76s76_c106s94_c0s107
50 | - c76s76_c111s89_c0s102
51 | - c76s76_c67s65_c10s52
52 | ssh_extras:
53 | hassh: ec9ea89c70f5fc71cf61061bff5e4740
54 | hassh_server: 2307c390c7c9aba5b4c9519e72347f34
55 | ssh_protocol_client: SSH-2.0-OpenSSH_7.4
56 | ssh_protocol_server: SSH-2.0-OpenSSH_8.7
57 | encryption_algorithm: aes256-gcm@openssh.com
58 |
59 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@ssh-scp-1050.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.1.169
8 | dst: 192.168.1.197
9 | src_port: 49237
10 | dst_port: 22
11 | ja4l_c: 179_128
12 | ja4l_s: 38_64
13 | ja4ssh:
14 | - c112s1460_c52s148_c41s4
15 | - c112s1460_c13s187_c35s0
16 | - c0s1460_c0s200_c36s0
17 | - c0s1460_c0s200_c23s0
18 | - c0s1460_c0s53_c6s0
19 | ssh_extras:
20 | hassh: eb6d4c713c7dcaba7cfd070b095213a9
21 | hassh_server: 6832f1ce43d4397c2c0a3e2f8c94334e
22 | ssh_protocol_client: SSH-2.0-WinSCP_release_5.17.10
23 | ssh_protocol_server: SSH-2.0-OpenSSH_7.4
24 | encryption_algorithm: chacha20-poly1305@openssh.com
25 |
26 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@ssh.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 172.16.225.48
8 | dst: 54.160.114.75
9 | src_port: 57377
10 | dst_port: 22
11 | ja4ssh:
12 | - c36s36_c76s124_c0s0
13 | - c36s52_c42s76_c0s0
14 | ssh_extras:
15 | hassh: 06046964c022c6407d15a27b12a6a4fb
16 | hassh_server: 699519fdcc30cbcd093d5cd01e4b1d56
17 | ssh_protocol_client: SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.5
18 | ssh_protocol_server: SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1
19 | encryption_algorithm: chacha20-poly1305@openssh.com
20 |
21 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@ssh2-malformed.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 10.0.0.1
8 | dst: 10.0.0.2
9 | src_port: 61672
10 | dst_port: 22
11 | ja4l_c: 7_64
12 | ja4l_s: 462_60
13 | ja4ssh:
14 | - c16s23_c7s5_c3s4
15 | ssh_extras:
16 | hassh: 21b457a327ce7a2d4fce5ef2c42400bd
17 | hassh_server: f430cd6761697a6a658ee1d45ed22e49
18 | ssh_protocol_client: SSH-2.0-OpenSSH_5.3
19 | ssh_protocol_server: SSH-1.99-OpenSSH_3.9p1
20 | encryption_algorithm: aes128-cbc
21 |
22 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@ssh2-moloch-crash.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 10.0.0.1
8 | dst: 10.0.0.2
9 | src_port: 61672
10 | dst_port: 22
11 | ja4l_c: 7_64
12 | ja4l_s: 462_60
13 | ja4ssh:
14 | - c16s23_c7s5_c3s4
15 | ssh_extras:
16 | hassh: 21b457a327ce7a2d4fce5ef2c42400bd
17 | hassh_server: f430cd6761697a6a658ee1d45ed22e49
18 | ssh_protocol_client: SSH-2.0-OpenSSH_5.3
19 | ssh_protocol_server: SSH-1.99-OpenSSH_3.9p1
20 | encryption_algorithm: aes128-cbc
21 |
22 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@sshv1.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 3ffe:507:0:1:200:86ff:fe05:80da
8 | dst: 3ffe:501:410:0:2c0:dfff:fe47:33e
9 | src_port: 1022
10 | dst_port: 22
11 | ja4l_c: 271_64
12 | ja4l_s: 28494_61
13 | ja4ssh:
14 | - c20s12_c18s23_c11s2
15 | ssh_extras:
16 | hassh: null
17 | hassh_server: null
18 | ssh_protocol_client: SSH-1.5-1.2.26
19 | ssh_protocol_server: SSH-1.5-1.2.26
20 | encryption_algorithm: null
21 |
22 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@tcpdump-geneve.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 30.0.0.2
8 | dst: 30.0.0.1
9 | src_port: 51225
10 | dst_port: 22
11 | ja4l_c: 93_64
12 | ja4l_s: 24_64
13 | ja4ssh:
14 | - c144s48_c10s11_c6s4
15 | ssh_extras:
16 | hassh: 21b457a327ce7a2d4fce5ef2c42400bd
17 | hassh_server: ce3c327f37ea2ec21f317fbc3fd1ea43
18 | ssh_protocol_client: SSH-2.0-OpenSSH_5.3
19 | ssh_protocol_server: SSH-2.0-OpenSSH_5.9p1 Debian-5ubuntu1
20 | encryption_algorithm: aes128-ctr
21 |
22 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@tls-alpn-h2.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 2001:4998:ef83:14:8000::100d
8 | dst: 2606:4700::6811:d209
9 | src_port: 64034
10 | dst_port: 443
11 | tls_server_name: www.cloudflare.com
12 | ja4: t12d4605h2_85626a9a5f7f_aaf95bb78ec9
13 | ja4s: t1204h2_cca9_1428ce7b4018
14 | tls_certs:
15 | - x509:
16 | - ja4x: 7d5dbb3783b4_ba7ce0880c07_7bf9a7bf7029
17 | issuerCountryName: US
18 | issuerOrganizationName: DigiCert Inc
19 | issuerOrganizationalUnit: www.digicert.com
20 | issuerCommonName: DigiCert ECC Extended Validation Server CA
21 | subjectBusinessCategory: Private Organization
22 | subjectMsJurisdictionCountry: US
23 | subjectMsJurisdictionStateOrProvince: Delaware
24 | subjectSerialNumber: '4710875'
25 | subjectCountryName: US
26 | subjectStateOrProvinceName: California
27 | subjectLocalityName: San Francisco
28 | subjectOrganizationName: Cloudflare, Inc.
29 | subjectCommonName: cloudflare.com
30 | - ja4x: 7d5dbb3783b4_7d5dbb3783b4_41a019652939
31 | issuerCountryName: US
32 | issuerOrganizationName: DigiCert Inc
33 | issuerOrganizationalUnit: www.digicert.com
34 | issuerCommonName: DigiCert High Assurance EV Root CA
35 | subjectCountryName: US
36 | subjectOrganizationName: DigiCert Inc
37 | subjectOrganizationalUnit: www.digicert.com
38 | subjectCommonName: DigiCert ECC Extended Validation Server CA
39 | ja4l_c: 35_64
40 | ja4l_s: 18861_59
41 |
42 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@tls-non-ascii-alpn.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.1.168
8 | dst: 142.251.16.94
9 | src_port: 50112
10 | dst_port: 443
11 | tls_server_name: clientservices.googleapis.com
12 | ja4: t13d151699_8daaf6152771_e5627efa2ab1
13 | ja4s: t130200_1301_234ea6891581
14 |
15 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@tls12.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 192.168.133.129
8 | dst: 34.117.237.239
9 | src_port: 36372
10 | dst_port: 443
11 | tls_server_name: contile.services.mozilla.com
12 | ja4: t13d1715h2_5b57614c22b0_3d5424432f57
13 |
14 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@tls3.pcapng.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 7
6 | transport: tcp
7 | src: 192.168.1.169
8 | dst: 54.190.49.36
9 | src_port: 63248
10 | dst_port: 443
11 | tls_server_name: darksail.ai
12 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
13 | ja4s: t130200_1301_a56c5b993250
14 | ja4l_c: 16_128
15 | ja4l_s: 34615_35
16 | - stream: 8
17 | transport: tcp
18 | src: 192.168.1.169
19 | dst: 23.222.12.9
20 | src_port: 63249
21 | dst_port: 80
22 | ja4l_c: 14_128
23 | ja4l_s: 3181_57
24 | http:
25 | - ja4h: ge11nn07enus_3e3b55d61660_000000000000_000000000000
26 | - stream: 9
27 | transport: tcp
28 | src: 192.168.1.169
29 | dst: 54.190.49.36
30 | src_port: 63250
31 | dst_port: 443
32 | tls_server_name: darksail.ai
33 | ja4: t13d1516h2_8daaf6152771_9b887d9acb53
34 | ja4s: t130300_1301_0ee26285a86f
35 | ja4l_c: 13_128
36 | ja4l_s: 36549_35
37 | - stream: 10
38 | transport: tcp
39 | src: 192.168.1.169
40 | dst: 54.190.49.36
41 | src_port: 63251
42 | dst_port: 443
43 | tls_server_name: darksail.ai
44 | ja4: t13d1516h2_8daaf6152771_9b887d9acb53
45 | ja4s: t130300_1301_0ee26285a86f
46 | ja4l_c: 15_128
47 | ja4l_s: 34691_38
48 | - stream: 11
49 | transport: tcp
50 | src: 192.168.1.169
51 | dst: 104.21.234.234
52 | src_port: 63252
53 | dst_port: 443
54 | tls_server_name: rsms.me
55 | ja4: t13d1516h2_8daaf6152771_9b887d9acb53
56 | ja4s: t130300_1301_6bbbaf601ed8
57 | ja4l_c: 15_128
58 | ja4l_s: 2442_57
59 | - stream: 12
60 | transport: tcp
61 | src: 192.168.1.169
62 | dst: 54.190.49.36
63 | src_port: 63253
64 | dst_port: 443
65 | tls_server_name: darksail.ai
66 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
67 | ja4s: t130200_1301_a56c5b993250
68 | ja4l_c: 11_128
69 | ja4l_s: 36498_35
70 | - stream: 13
71 | transport: tcp
72 | src: 192.168.1.169
73 | dst: 54.190.49.36
74 | src_port: 63254
75 | dst_port: 443
76 | tls_server_name: darksail.ai
77 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
78 | ja4s: t130200_1301_a56c5b993250
79 | ja4l_c: 15_128
80 | ja4l_s: 33515_32
81 | - stream: 14
82 | transport: tcp
83 | src: 192.168.1.169
84 | dst: 54.190.49.36
85 | src_port: 63255
86 | dst_port: 443
87 | tls_server_name: darksail.ai
88 | ja4: t13d1516h2_8daaf6152771_e5627efa2ab1
89 | ja4s: t130200_1301_a56c5b993250
90 | ja4l_c: 11_128
91 | ja4l_s: 33738_33
92 | - stream: 21
93 | transport: udp
94 | src: 192.168.1.169
95 | dst: 172.253.122.95
96 | src_port: 62481
97 | dst_port: 443
98 | tls_server_name: fonts.googleapis.com
99 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
100 | ja4s: q130200_1301_234ea6891581
101 | ja4l_c: 59_128
102 | ja4l_s: 4213_59
103 | - stream: 22
104 | transport: udp
105 | src: 192.168.1.169
106 | dst: 104.21.234.234
107 | src_port: 61732
108 | dst_port: 443
109 | tls_server_name: rsms.me
110 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
111 | ja4s: q130200_1301_234ea6891581
112 | ja4l_c: 336_128
113 | ja4l_s: 5580_57
114 | - stream: 23
115 | transport: udp
116 | src: 192.168.1.169
117 | dst: 151.101.1.229
118 | src_port: 49791
119 | dst_port: 443
120 | tls_server_name: cdn.jsdelivr.net
121 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
122 | ja4s: q130200_1301_a56c5b993250
123 | ja4l_c: 40_128
124 | ja4l_s: 4455_58
125 | - stream: 24
126 | transport: udp
127 | src: 192.168.1.169
128 | dst: 104.17.24.14
129 | src_port: 56684
130 | dst_port: 443
131 | tls_server_name: cdnjs.cloudflare.com
132 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
133 | ja4s: q130200_1301_234ea6891581
134 | ja4l_c: 59_128
135 | ja4l_s: 3590_57
136 | - stream: 25
137 | transport: udp
138 | src: 192.168.1.169
139 | dst: 104.21.234.234
140 | src_port: 61884
141 | dst_port: 443
142 | tls_server_name: rsms.me
143 | ja4: q13d0311h3_55b375c5d22e_3512bcbbc9ec
144 | ja4s: q130300_1301_6bbbaf601ed8
145 | - stream: 28
146 | transport: udp
147 | src: 192.168.1.169
148 | dst: 142.251.111.94
149 | src_port: 58117
150 | dst_port: 443
151 | tls_server_name: fonts.gstatic.com
152 | ja4: q13d0310h3_55b375c5d22e_cd85d2d88918
153 | ja4s: q130200_1301_234ea6891581
154 | ja4l_c: 45_128
155 | ja4l_s: 3298_58
156 |
157 |
--------------------------------------------------------------------------------
/pkg/ja4pcap/testdata/snapshots/ja4__insta@v6.pcap.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: ja4/src/lib.rs
3 | expression: output
4 | ---
5 | - stream: 0
6 | transport: tcp
7 | src: 3ffe:507:0:1:200:86ff:fe05:80da
8 | dst: 3ffe:501:410:0:2c0:dfff:fe47:33e
9 | src_port: 1022
10 | dst_port: 22
11 | ja4l_c: 271_64
12 | ja4l_s: 28494_61
13 | ja4ssh:
14 | - c20s12_c18s23_c11s2
15 | ssh_extras:
16 | hassh: null
17 | hassh_server: null
18 | ssh_protocol_client: SSH-1.5-1.2.26
19 | ssh_protocol_server: SSH-1.5-1.2.26
20 | encryption_algorithm: null
21 |
22 |
--------------------------------------------------------------------------------
/pkg/metadata/context.go:
--------------------------------------------------------------------------------
1 | package metadata
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | // contextKey is a value for use with context.WithValue. It's used as
8 | // a pointer so it fits in an interface{} without allocation.
9 | type contextKey struct {
10 | name string
11 | }
12 |
13 | func (k *contextKey) String() string { return "fingerproxy context value " + k.name }
14 |
15 | var (
16 | FingerproxyContextKey = &contextKey{"fingerproxy-metadata"}
17 | )
18 |
19 | func NewContext(ctx context.Context) (context.Context, *Metadata) {
20 | md := &Metadata{}
21 | newCtx := context.WithValue(ctx, FingerproxyContextKey, md)
22 | return newCtx, md
23 | }
24 |
25 | func FromContext(ctx context.Context) (*Metadata, bool) {
26 | data, ok := ctx.Value(FingerproxyContextKey).(*Metadata)
27 | return data, ok
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/metadata/http2.go:
--------------------------------------------------------------------------------
1 | package metadata
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "math"
7 | )
8 |
9 | // https://github.com/golang/net/blob/5a444b4f2fe893ea00f0376da46aa5376c3f3e28/http2/http2.go#L112-L119
10 | type Setting struct {
11 | Id uint16
12 | Val uint32
13 | }
14 |
15 | // https://github.com/golang/net/blob/5a444b4f2fe893ea00f0376da46aa5376c3f3e28/http2/frame.go#L1142-L1156
16 | type Priority struct {
17 | StreamId uint32
18 | StreamDep uint32
19 | Exclusive bool
20 | Weight uint8
21 | }
22 |
23 | // https://github.com/golang/net/blob/5a444b4f2fe893ea00f0376da46aa5376c3f3e28/http2/hpack/hpack.go#L36-L42
24 | type HeaderField struct {
25 | Name, Value string
26 | Sensitive bool
27 | }
28 |
29 | type HTTP2FingerprintingFrames struct {
30 | // Data from SETTINGS frame
31 | Settings []Setting
32 |
33 | // Increment of WINDOW_UPDATE frame
34 | WindowUpdateIncrement uint32
35 |
36 | // PRIORITY frame(s)
37 | Priorities []Priority
38 |
39 | // HEADERS frame
40 | Headers []HeaderField
41 | }
42 |
43 | func (f *HTTP2FingerprintingFrames) String() string {
44 | return f.Marshal(math.MaxUint)
45 | }
46 |
47 | // TODO: add tests
48 | func (f *HTTP2FingerprintingFrames) Marshal(maxPriorityFrames uint) string {
49 | var buf bytes.Buffer
50 |
51 | // SETTINGS frame
52 | for i, s := range f.Settings {
53 | if i != 0 {
54 | // Multiple settings are concatenated using a semicolon (;) according to the order of their appearance.
55 | buf.WriteString(";")
56 | }
57 | // S[...] stands for a SETTINGS parameter and its value in the form of Key:Value.
58 | buf.WriteString(fmt.Sprintf("%d:%d", s.Id, s.Val))
59 | }
60 |
61 | buf.WriteString("|")
62 |
63 | // WINDOW_UPDATE frame
64 | // ‘00’ if the frame is not present
65 | buf.WriteString(fmt.Sprintf("%02d|", f.WindowUpdateIncrement))
66 |
67 | // PRIORITY frame
68 | if l := len(f.Priorities); uint(l) < maxPriorityFrames {
69 | maxPriorityFrames = uint(l)
70 | }
71 | if maxPriorityFrames == 0 {
72 | // If this feature does not exist, the value should be ‘0’.
73 | buf.WriteString("0|")
74 | } else {
75 | for i, p := range f.Priorities[:maxPriorityFrames] {
76 | if i != 0 {
77 | // Multiple priority frames are concatenated by a comma (,).
78 | buf.WriteString(",")
79 | }
80 | // StreamID:Exclusivity_Bit:Dependant_StreamID:Weight
81 | buf.WriteString(fmt.Sprintf("%d:", p.StreamId))
82 | if p.Exclusive {
83 | buf.WriteString("1:")
84 | } else {
85 | buf.WriteString("0:")
86 | }
87 | // "Add one to the value to obtain a weight between 1 and 256."
88 | // Quoted from https://httpwg.org/specs/rfc7540.html#PRIORITY
89 | buf.WriteString(fmt.Sprintf("%d:%d", p.StreamDep, int(p.Weight)+1))
90 | }
91 | buf.WriteString("|")
92 | }
93 |
94 | // HEADERS frame
95 | wrotePseudoHeader := false
96 | for _, h := range f.Headers {
97 | // filter only pseudo headers which starts with a colon
98 | if len(h.Name) >= 2 && h.Name[0] == ':' {
99 | if wrotePseudoHeader {
100 | buf.WriteString(",")
101 | }
102 | wrotePseudoHeader = true
103 | buf.WriteByte(h.Name[1])
104 | }
105 | }
106 |
107 | return buf.String()
108 | }
109 |
--------------------------------------------------------------------------------
/pkg/metadata/metadata.go:
--------------------------------------------------------------------------------
1 | // Package `metadata` has a struct that stores information captured by
2 | // `proxyserver`. Package `fingerprint` uses these information to create
3 | // fingerprints.
4 | package metadata
5 |
6 | import "crypto/tls"
7 |
8 | // Metadata is the data we captured from the connection for fingerprinting.
9 | // Currently only TLS ClientHello and certain HTTP2 frames included, more can
10 | // be added in the future.
11 | type Metadata struct {
12 | // ClientHelloRecord is the raw TLS ClientHello bytes that
13 | // include TLS record header and handshake header
14 | ClientHelloRecord []byte
15 |
16 | // ConnectionState represents the TLS connection state
17 | ConnectionState tls.ConnectionState
18 |
19 | // HTTP2Frames includes certain HTTP2 frames data
20 | HTTP2Frames HTTP2FingerprintingFrames
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/proxyserver/proxyserver_test.go:
--------------------------------------------------------------------------------
1 | package proxyserver
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "testing"
7 | )
8 |
9 | func TestIsNetworkOrClientError(t *testing.T) {
10 | // https://github.com/golang/go/blob/release-branch.go1.22/src/crypto/tls/alert.go#L77
11 | alert := fmt.Errorf("unknown certificate authority")
12 | // https://github.com/golang/go/blob/release-branch.go1.22/src/crypto/tls/conn.go#L724
13 | err := setErrorLocked(&net.OpError{Op: "remote error", Err: alert})
14 | if isNetworkOrClientError(err) == false {
15 | t.Error("expected tls alert record is client error, got false")
16 | }
17 |
18 | if isNetworkOrClientError(fmt.Errorf("some random error")) {
19 | t.Error("expected random error is not client error, got true")
20 | }
21 | }
22 |
23 | type permanentError struct {
24 | err net.Error
25 | }
26 |
27 | func (e *permanentError) Error() string { return e.err.Error() }
28 | func (e *permanentError) Unwrap() error { return e.err }
29 | func (e *permanentError) Timeout() bool { return e.err.Timeout() }
30 | func (e *permanentError) Temporary() bool { return false }
31 |
32 | func setErrorLocked(err error) error {
33 | if e, ok := err.(net.Error); ok {
34 | return &permanentError{err: e}
35 | } else {
36 | return err
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/reverseproxy/handler.go:
--------------------------------------------------------------------------------
1 | // Package `reverseproxy` forwards the requests to backends. It gets
2 | // additional request headers from `header_injectors`, and adds to the
3 | // forwarding request to downstream.
4 | package reverseproxy
5 |
6 | import (
7 | "log"
8 | "net/http"
9 | "net/http/httputil"
10 | "net/url"
11 | "strings"
12 | )
13 |
14 | type HTTPHandler struct {
15 | // required, internal reverse proxy that forwards the requests
16 | reverseProxy *httputil.ReverseProxy
17 |
18 | // required, the URL that requests will be forwarding to
19 | To *url.URL
20 |
21 | // optional, preserve the host in outbound requests
22 | PreserveHost bool
23 |
24 | // optional, but in fact required, injecting fingerprint headers to outbound requests
25 | HeaderInjectors []HeaderInjector
26 |
27 | // optional, if IsProbeRequest returns true, handler will respond with
28 | // a HTTP 200 OK instead of forwarding requests, useful for kubernetes
29 | // liveness/readiness probes. defaults to nil, which disables this behavior
30 | IsProbeRequest func(*http.Request) bool
31 | }
32 |
33 | const (
34 | ProbeStatusCode = http.StatusOK
35 | ProbeResponse = "OK"
36 | )
37 |
38 | // NewHTTPHandler creates an HTTP handler, changes `reverseProxy.Rewrite` to support request
39 | // header injection, then assigns `reverseProxy` to the handler which proxies requests to backend
40 | func NewHTTPHandler(to *url.URL, reverseProxy *httputil.ReverseProxy, headerInjectors []HeaderInjector) *HTTPHandler {
41 | f := &HTTPHandler{
42 | To: to,
43 | reverseProxy: reverseProxy,
44 | HeaderInjectors: headerInjectors,
45 | }
46 |
47 | f.reverseProxy.Rewrite = f.rewriteFunc
48 | return f
49 | }
50 |
51 | func (f *HTTPHandler) rewriteFunc(r *httputil.ProxyRequest) {
52 | r.SetURL(f.To)
53 | r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
54 | r.SetXForwarded()
55 |
56 | if f.PreserveHost {
57 | r.Out.Host = r.In.Host
58 | }
59 |
60 | for _, hj := range f.HeaderInjectors {
61 | k := hj.GetHeaderName()
62 | if v, err := hj.GetHeaderValue(r.In); err != nil {
63 | f.logf("get header %s value for %s failed: %s", k, r.In.RemoteAddr, err)
64 | } else if v != "" { // skip empty header values
65 | r.Out.Header.Set(k, v)
66 | }
67 | }
68 | }
69 |
70 | func (f *HTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
71 | if f.IsProbeRequest != nil && f.IsProbeRequest(req) {
72 | w.WriteHeader(ProbeStatusCode)
73 | w.Write([]byte(ProbeResponse))
74 | return
75 | }
76 | f.reverseProxy.ServeHTTP(w, req)
77 | }
78 |
79 | func IsKubernetesProbeRequest(r *http.Request) bool {
80 | // https://github.com/kubernetes/kubernetes/blob/656cb1028ea5af837e69b5c9c614b008d747ab63/pkg/probe/http/request.go#L91
81 | return strings.HasPrefix(r.UserAgent(), "kube-probe/")
82 | }
83 |
84 | func (f *HTTPHandler) logf(format string, args ...any) {
85 | if f.reverseProxy.ErrorLog != nil {
86 | f.reverseProxy.ErrorLog.Printf(format, args...)
87 | } else {
88 | log.Printf(format, args...)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/pkg/reverseproxy/handler_test.go:
--------------------------------------------------------------------------------
1 | package reverseproxy
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net/http"
7 | "net/http/httputil"
8 | "net/url"
9 | "strings"
10 | "testing"
11 | )
12 |
13 | type dummyResponseWriter struct {
14 | buf bytes.Buffer
15 | code int
16 | header http.Header
17 | }
18 |
19 | func (w *dummyResponseWriter) Header() http.Header {
20 | if w.header == nil {
21 | w.header = http.Header{}
22 | }
23 | return w.header
24 | }
25 | func (w *dummyResponseWriter) WriteHeader(statusCode int) { w.code = statusCode }
26 | func (w *dummyResponseWriter) Write(b []byte) (int, error) { return w.buf.Write(b) }
27 |
28 | const (
29 | dummyRemoteIP = "1.1.1.1"
30 | dummyForwardedFor = "172.17.0.1"
31 | )
32 |
33 | func dummyRequest(t *testing.T) *http.Request {
34 | t.Helper()
35 | req, err := http.NewRequest("GET", "https://dummy-host/anything?show_env=1", nil)
36 | req.RemoteAddr = dummyRemoteIP + ":30000"
37 | req.Header.Set("User-Agent", "dummy")
38 | req.Header.Set("X-Forwarded-For", dummyForwardedFor)
39 | if err != nil {
40 | t.Fatal(err)
41 | }
42 | return req
43 | }
44 |
45 | func dummyURL(t *testing.T) *url.URL {
46 | url, err := url.Parse("https://httpbin.org")
47 | if err != nil {
48 | t.Fatal(err)
49 | }
50 | return url
51 | }
52 |
53 | func TestKubernetesLivenessProbe(t *testing.T) {
54 | handler := NewHTTPHandler(dummyURL(t), &httputil.ReverseProxy{}, nil)
55 | handler.IsProbeRequest = IsKubernetesProbeRequest
56 |
57 | req := dummyRequest(t)
58 | req.Header.Set("User-Agent", "kube-probe/1.26")
59 |
60 | w := &dummyResponseWriter{}
61 | handler.ServeHTTP(w, req)
62 |
63 | if w.buf.String() != ProbeResponse {
64 | t.Fatalf("expected response %s, actual %s", ProbeResponse, w.buf.String())
65 | }
66 | if w.code != ProbeStatusCode {
67 | t.Fatalf("expected status code %d, actual %d", ProbeStatusCode, w.code)
68 | }
69 | }
70 |
71 | type dummyHeaderInjector struct{}
72 |
73 | func (i *dummyHeaderInjector) GetHeaderName() string { return "dummy" }
74 | func (i *dummyHeaderInjector) GetHeaderValue(req *http.Request) (string, error) {
75 | return "dummy-value", nil
76 | }
77 |
78 | func TestInjectHeader(t *testing.T) {
79 | hj := &dummyHeaderInjector{}
80 | handler := NewHTTPHandler(dummyURL(t), &httputil.ReverseProxy{}, []HeaderInjector{hj})
81 |
82 | w := &dummyResponseWriter{}
83 | handler.ServeHTTP(w, dummyRequest(t))
84 |
85 | j := struct {
86 | Headers struct {
87 | Dummy string
88 | Host string
89 | }
90 | }{}
91 |
92 | t.Log(w.buf.String())
93 |
94 | err := json.Unmarshal(w.buf.Bytes(), &j)
95 | if err != nil {
96 | t.Fatal(err)
97 | }
98 |
99 | if j.Headers.Host != "httpbin.org" {
100 | t.Fatalf("expected header value %s, actual %s", "httpbin.org", j.Headers.Host)
101 | }
102 |
103 | if j.Headers.Dummy != "dummy-value" {
104 | t.Fatalf("expected header value %s, actual %s", "dummy-value", j.Headers.Dummy)
105 | }
106 | }
107 |
108 | func TestPreserveHost(t *testing.T) {
109 | handler := NewHTTPHandler(dummyURL(t), &httputil.ReverseProxy{}, nil)
110 | handler.PreserveHost = true
111 |
112 | w := &dummyResponseWriter{}
113 | handler.ServeHTTP(w, dummyRequest(t))
114 |
115 | j := struct {
116 | Headers struct {
117 | Host string
118 | }
119 | }{}
120 |
121 | t.Log(w.buf.String())
122 |
123 | err := json.Unmarshal(w.buf.Bytes(), &j)
124 | if err != nil {
125 | t.Fatal(err)
126 | }
127 |
128 | if j.Headers.Host != "dummy-host" {
129 | t.Fatalf("expected header value %s, actual %s", "dummy-host", j.Headers.Host)
130 | }
131 | }
132 |
133 | func TestAppendForwardHeader(t *testing.T) {
134 | handler := NewHTTPHandler(dummyURL(t), &httputil.ReverseProxy{}, nil)
135 |
136 | w := &dummyResponseWriter{}
137 | handler.ServeHTTP(w, dummyRequest(t))
138 |
139 | j := struct {
140 | Headers struct {
141 | XForwardedFor string `json:"x-forwarded-for"`
142 | }
143 | }{}
144 |
145 | t.Log(w.buf.String())
146 |
147 | err := json.Unmarshal(w.buf.Bytes(), &j)
148 | if err != nil {
149 | t.Fatal(err)
150 | }
151 |
152 | if !strings.HasPrefix(j.Headers.XForwardedFor, "172.17.0.1, 1.1.1.1") {
153 | t.Fatalf("expected header value %s, actual %s", "dummy-host", j.Headers.XForwardedFor)
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/pkg/reverseproxy/header_injector.go:
--------------------------------------------------------------------------------
1 | package reverseproxy
2 |
3 | import "net/http"
4 |
5 | type HeaderInjector interface {
6 | GetHeaderName() string
7 |
8 | GetHeaderValue(req *http.Request) (string, error)
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/sync-http2-pkg.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S bash -exuo pipefail
2 |
3 | #
4 | # Sync upstream http2 package.
5 | #
6 |
7 | cd $(dirname "$0")
8 |
9 | TAG=v0.33.0
10 | TAG_ARCHIVE_FILENAME=$TAG.tar.gz
11 | LOCAL_ARCHIVE_FILENAME=/tmp/$TAG_ARCHIVE_FILENAME
12 |
13 | wget -O $LOCAL_ARCHIVE_FILENAME https://github.com/golang/net/archive/refs/tags/$TAG_ARCHIVE_FILENAME
14 |
15 | TMP_SRCDIR=$(mktemp -d)
16 | TARBALL_ROOTDIR=$(tar tf $LOCAL_ARCHIVE_FILENAME | head -n1)
17 |
18 | tar xzf $LOCAL_ARCHIVE_FILENAME --directory $TMP_SRCDIR
19 |
20 | rsync -avhW --no-compress --delete $TMP_SRCDIR/$TARBALL_ROOTDIR/http2/ ./http2/
21 | rsync -avhW --no-compress $TMP_SRCDIR/$TARBALL_ROOTDIR/LICENSE ./http2/
22 |
23 | rm -rf $TMP_SRCDIR
24 |
--------------------------------------------------------------------------------
/testdata/gencert.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -x
2 |
3 | SAN=example.com
4 |
5 | openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
6 | -nodes -keyout tls.key -out tls.crt -subj "/CN=$SAN" \
7 | -addext "subjectAltName=DNS:$SAN,DNS:*.$SAN,IP:127.0.0.1"
8 |
--------------------------------------------------------------------------------