Skip to content

Commit

Permalink
Improve examples. (#15)
Browse files Browse the repository at this point in the history
* Improve examples.

* Finish README.

* Improve some description.
  • Loading branch information
wi1dcard authored May 21, 2024
1 parent 0e707c5 commit 0a25fd3
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.DS_Store
/.vscode
/testdata/*.crt
/testdata/*.key
*.crt
*.key
/bin
/example/echo-server/bin
/fingerproxy
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ The default Prometheus metrics server listens on `:9035`. Once new requests come

## Implement Your Fingerprinting Algorithm

Check out the example [`customize-fingerprint`](example/customize-fingerprint/). No code fork needed.
Check out the examples [`ja3-raw`](example/ja3-raw/) or [`my-fingerprint`](example/my-fingerprint/). No code fork needed.

## Chrome JA3 Fingerprints Change Every Time

Yes, it is an known issue of the original JA3 implementation. See [Google Chrome TLS extension permutation](https://github.com/net4people/bbs/issues/220). Sorting the TLS extension is one method to avoid the affect of this feature. Here is an example [ja3-sorted-extensions](example/ja3-sorted-extensions/).

## Use as a Library

Expand Down
39 changes: 39 additions & 0 deletions example/ja3-raw/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Fingerproxy Example - JA3 Raw

This example demonstrates passing the JA3 raw result (without final MD5 hashing) to the backend.

## Usage

```bash
# Generate fake certificates tls.crt and tls.key
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
-nodes -keyout tls.key -out tls.crt -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"

# TLS server listens on :8443, forwarding requests to httpbin
go run . -listen-addr :8443 -forward-url https://httpbin.org

# Then test in another terminal
curl "https://localhost:8443/headers" --insecure
```

Output:

```yaml
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-664c0810-09f1e3a03376e930030b20f7",
"X-Forwarded-Host": "localhost:8443",
"X-Http2-Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
"X-Ja3-Fingerprint": "0149f47eabf9a20d0893e2a44e5a6323",
## HEADER BELOW ##
"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",
## HEADER ABOVE ##
"X-Ja4-Fingerprint": "t13d3112h2_e8f1e7e78f70_6bebaf5329ac"
}
}
```
35 changes: 35 additions & 0 deletions example/ja3-raw/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"fmt"

"github.com/dreadl0ck/tlsx"
"github.com/wi1dcard/fingerproxy"
"github.com/wi1dcard/fingerproxy/pkg/fingerprint"
"github.com/wi1dcard/fingerproxy/pkg/ja3"
"github.com/wi1dcard/fingerproxy/pkg/metadata"
"github.com/wi1dcard/fingerproxy/pkg/reverseproxy"
)

func main() {
fingerproxy.GetHeaderInjectors = func() []reverseproxy.HeaderInjector {
i := fingerproxy.DefaultHeaderInjectors()
i = append(i, fingerprint.NewFingerprintHeaderInjector(
"X-JA3-Raw-Fingerprint",
fpJA3Raw,
))
return i
}
fingerproxy.Run()
}

func fpJA3Raw(data *metadata.Metadata) (string, error) {
hellobasic := &tlsx.ClientHelloBasic{}
if err := hellobasic.Unmarshal(data.ClientHelloRecord); err != nil {
return "", fmt.Errorf("ja3: %w", err)
}

fp := string(ja3.Bare(hellobasic))

return fp, nil
}
49 changes: 49 additions & 0 deletions example/ja3-sorted-extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Fingerproxy Example - JA3 Variant with Sorted Extensions

JA3 is relatively old. The original implementation is outdated in certain use cases.

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.

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.

## Usage

```bash
# Generate fake certificates tls.crt and tls.key
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
-nodes -keyout tls.key -out tls.crt -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"

# TLS server listens on :8443, forwarding requests to httpbin
go run . -listen-addr :8443 -forward-url https://httpbin.org

# Then test in another terminal
curl "https://localhost:8443/headers" --insecure
```

Output:

```yaml
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-664c0b9c-4f89ce9c411f2cf22acd59bb",
"X-Forwarded-Host": "localhost:8443",
"X-Http2-Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
"X-Ja3-Fingerprint": "0149f47eabf9a20d0893e2a44e5a6323",
"X-Ja4-Fingerprint": "t13d3112h2_e8f1e7e78f70_6bebaf5329ac",
## HERE ##
"X-Sorted-Ja3-Fingerprint": "22441e3edb4a151c17462a438c7a10a5"
}
}
```

Exit chrome and open again, you will see `X-Ja3-Fingerprint` changed but `X-Sorted-Ja3-Fingerprint` didn't.

## More Information

- <https://www.fastly.com/blog/a-first-look-at-chromes-tls-clienthello-permutation-in-the-wild/>
- <https://github.com/net4people/bbs/issues/220>
68 changes: 68 additions & 0 deletions example/ja3-sorted-extensions/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"cmp"
"fmt"
"slices"
"strconv"
"strings"

"github.com/dreadl0ck/tlsx"
"github.com/wi1dcard/fingerproxy"
"github.com/wi1dcard/fingerproxy/pkg/fingerprint"
"github.com/wi1dcard/fingerproxy/pkg/ja3"
"github.com/wi1dcard/fingerproxy/pkg/metadata"
"github.com/wi1dcard/fingerproxy/pkg/reverseproxy"
)

func main() {
fingerproxy.GetHeaderInjectors = func() []reverseproxy.HeaderInjector {
i := fingerproxy.DefaultHeaderInjectors()
i = append(i, fingerprint.NewFingerprintHeaderInjector(
"X-Sorted-JA3-Fingerprint",
fpSortedJA3,
))
return i
}
fingerproxy.Run()
}

func fpSortedJA3(data *metadata.Metadata) (string, error) {
hellobasic := &tlsx.ClientHelloBasic{}
if err := hellobasic.Unmarshal(data.ClientHelloRecord); err != nil {
return "", fmt.Errorf("ja3: %w", err)
}

fp := string(ja3.Bare(hellobasic))

fields := strings.Split(fp, ",")
if len(fields) != 5 {
// here should be impossible
return "", fmt.Errorf("bad ja3 fingerprint")
}

extensions := strings.Split(fields[2], "-")
if len(extensions) == 0 {
// no tls extension
return ja3.BareToDigestHex([]byte(fp)), nil
}

// very ugly implementations for demonstration purpose only
slices.SortFunc(extensions, func(x string, y string) int {
var _x, _y int
var err error
if _x, err = strconv.Atoi(x); err != nil {
return 0
}
if _y, err = strconv.Atoi(y); err != nil {
return 0
}
return cmp.Compare(_x, _y)
})

fields[2] = strings.Join(extensions, "-")
fp = strings.Join(fields, ",")

// return fp, nil
return ja3.BareToDigestHex([]byte(fp)), nil
}
38 changes: 38 additions & 0 deletions example/my-fingerprint/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Fingerproxy Example - My Fingerprint

This is an example of implementing a customized fingerprint with TLS ClientHello.

## Usage

```bash
# Generate fake certificates tls.crt and tls.key
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
-nodes -keyout tls.key -out tls.crt -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,DNS:*.localhost,IP:127.0.0.1"

# TLS server listens on :8443, forwarding requests to httpbin
go run . -listen-addr :8443 -forward-url https://httpbin.org

# Then test in another terminal
curl "https://localhost:8443/headers" --insecure
```

Output:

```yaml
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-664c08ef-4efb8ea50d0d59181a2b1565",
"X-Forwarded-Host": "localhost:8443",
"X-Http2-Fingerprint": "3:100;4:10485760;2:0|1048510465|0|m,s,a,p",
"X-Ja3-Fingerprint": "0149f47eabf9a20d0893e2a44e5a6323",
"X-Ja4-Fingerprint": "t13d3112h2_e8f1e7e78f70_6bebaf5329ac",
## HERE ##
"X-My-Fingerprint": "alpn:h2,http/1.1|supported_versions:772,771"
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,32 @@ func main() {
fingerproxy.GetHeaderInjectors = func() []reverseproxy.HeaderInjector {
i := fingerproxy.DefaultHeaderInjectors()
i = append(i, fingerprint.NewFingerprintHeaderInjector(
"X-MyExample-Fingerprint",
myFingerprint,
"X-My-Fingerprint",
SimpleFingerprint,
))
return i
}
fingerproxy.Run()
}

func myFingerprint(data *metadata.Metadata) (string, error) {
func SimpleFingerprint(data *metadata.Metadata) (string, error) {
// parses client hello first
chs := &utls.ClientHelloSpec{}
err := chs.FromRaw(data.ClientHelloRecord, true, true)
if err != nil {
return "", fmt.Errorf("myFingerprint: %w", err)
return "", fmt.Errorf("simple fingerprint: %w", err)
}

// prepare fingerprint buffer
var buf strings.Builder
for _, e := range chs.Extensions {
var part string
var field string
switch e := e.(type) {
// use ALPN in the fingerprint
case *utls.ALPNExtension:
part = fmt.Sprintf("alpn:%s", strings.Join(e.AlpnProtocols, ","))
field = fmt.Sprintf("alpn:%s", joinAnything(e.AlpnProtocols, ","))

// use TLS supported version in the fingerprint
case *utls.SupportedVersionsExtension:
sv := []string{}
for _, v := range e.Versions {
Expand All @@ -48,17 +52,24 @@ func myFingerprint(data *metadata.Metadata) (string, error) {
sv = append(sv, strconv.Itoa(int(v)))
}
}
part = fmt.Sprintf("supported_versions:%s", strings.Join(sv, ","))
field = fmt.Sprintf("supported_versions:%s", joinAnything(sv, ","))
}

if part != "" {
// case ...:
// use any extension in your fingerprint
// ...

if field != "" {
if buf.Len() != 0 {
// add separator if needed
buf.WriteString("|")
}
buf.WriteString(part)
// add field
buf.WriteString(field)
}
}

// return fingerprint string
return buf.String(), nil
}

Expand Down

0 comments on commit 0a25fd3

Please sign in to comment.