Skip to content

Commit 6899715

Browse files
authored
Merge pull request #135 from SenseUnit/tit_countermeasures
TiT countermeasures
2 parents d4034c6 + 3ddc6b8 commit 6899715

File tree

5 files changed

+472
-4
lines changed

5 files changed

+472
-4
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,9 @@ Supported proxy schemes are:
380380
* `curves` - colon-separated list of enabled TLS key exchange curves.
381381
* `min-tls-version` - minimum TLS version.
382382
* `max-tls-version` - maximum TLS version.
383+
* `fetchrandom` - request server to send random data in the first request via every new HTTP/2 connection. Useful to trick TLS-in-TLS detection. Value format: length as a number or range `x-y`. Example: `fetchrandom=100000-500000`.
383384
* `h2c` - HTTP/2 proxy over plaintext connection with the CONNECT method support. Examples: `h2c://example.org:8080`.
385+
* `fetchrandom` - request server to send random data in the first request via every new HTTP/2 connection. Useful to trick TLS-in-TLS detection. Value format: length as a number or range `x-y`. Example: `fetchrandom=100000-500000`.
384386
* `socks5`, `socks5h` - SOCKS5 proxy with hostname resolving via remote proxy. Example: `socks5://127.0.0.1:9050`.
385387
* `set-src-hints` - not an actual proxy, but a signal to use different source IP address hints for this connection. It's useful to route traffic across multiple network interfaces, including VPN connections. URL has to have one query parameter `hints` with a comma-separated list of IP addresses. See `-ip-hints` command line option for more details. Example: `set-src-hints://?hints=10.2.0.2`
386388
* `cached` - pseudo-dialer which caches construction of another dialer specified by URL passed in `url` parameter of query string. Useful for dialers which are constructed dynamically from JS router script and which load certificate files. Example: `cache://?url=https%3A%2F%2Fexample.org%3Fcert%3Dcert.pem%26key%3Dkey.pem&ttl=5m`. Query string parameters are:

dialer/dialer.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package dialer
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"math/rand/v2"
68
"net"
79
"net/url"
10+
"strconv"
11+
"strings"
812

913
xproxy "golang.org/x/net/proxy"
1014
)
@@ -76,3 +80,51 @@ func MaybeWrapWithContextDialer(d LegacyDialer) Dialer {
7680
}
7781
return wrappedDialer{d}
7882
}
83+
84+
func garbageLenFuncFromURL(u *url.URL, paramname string) (func() int, error) {
85+
params, err := url.ParseQuery(u.RawQuery)
86+
if err != nil {
87+
return nil, fmt.Errorf("garbage len param parse failed: %w", err)
88+
}
89+
if !params.Has(paramname) {
90+
return nil, nil
91+
}
92+
left, right, found := strings.Cut(params.Get(paramname), "-")
93+
if found {
94+
lo, err := strconv.Atoi(left)
95+
if err != nil {
96+
return nil, fmt.Errorf("can't convert lower boundary for garbage length %q to int: %w", left, err)
97+
}
98+
if lo < 0 {
99+
return nil, errors.New("negative lower boundary for garbage length is not allowed")
100+
}
101+
hi, err := strconv.Atoi(right)
102+
if err != nil {
103+
return nil, fmt.Errorf("can't convert upper boundary for garbage length %q to int: %w", right, err)
104+
}
105+
if hi < 0 {
106+
return nil, errors.New("negative upper boundary for garbage length is not allowed")
107+
}
108+
if hi < lo {
109+
hi, lo = lo, hi
110+
}
111+
if hi == lo {
112+
return func() int {
113+
return lo
114+
}, nil
115+
}
116+
return func() int {
117+
return lo + rand.IntN(hi-lo)
118+
}, nil
119+
}
120+
l, err := strconv.Atoi(left)
121+
if err != nil {
122+
return nil, fmt.Errorf("can't convert garbage length %q to int: %w", left, err)
123+
}
124+
if l < 0 {
125+
return nil, errors.New("negative garbage length is not allowed")
126+
}
127+
return func() int {
128+
return l
129+
}, nil
130+
}

dialer/h2.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"net/url"
1212
"slices"
13+
"strconv"
1314
"strings"
1415
"time"
1516

@@ -34,13 +35,15 @@ func H2ProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
3435
tlsConfig *tls.Config
3536
err error
3637
h2c bool
38+
scheme string
3739
)
3840
switch strings.ToLower(u.Scheme) {
3941
case "h2c":
4042
if port == "" {
4143
port = "80"
4244
}
4345
h2c = true
46+
scheme = "http"
4447
case "h2":
4548
if port == "" {
4649
port = "443"
@@ -52,15 +55,60 @@ func H2ProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
5255
if err != nil {
5356
return nil, fmt.Errorf("TLS configuration failed: %w", err)
5457
}
58+
scheme = "https"
5559
default:
5660
return nil, errors.New("unsupported proxy type")
5761
}
5862

5963
address := net.JoinHostPort(host, port)
64+
garbageLenFunc, err := garbageLenFuncFromURL(u, "fetchrandom")
65+
if err != nil {
66+
return nil, err
67+
}
6068
t := &http2.Transport{
6169
AllowHTTP: h2c,
6270
TLSClientConfig: tlsConfig,
6371
}
72+
t.ConnPool = &clientConnPool{
73+
t: t,
74+
prepare: func(ctx context.Context, c *http2.ClientConn) (*http2.ClientConn, error) {
75+
if garbageLenFunc == nil {
76+
return c, nil
77+
}
78+
garbageLen := garbageLenFunc()
79+
req := (&http.Request{
80+
Method: "GETRANDOM",
81+
URL: &url.URL{
82+
Scheme: scheme,
83+
Host: u.Host,
84+
Path: "/" + strconv.Itoa(garbageLen),
85+
},
86+
Header: http.Header{
87+
"User-Agent": []string{"dumbproxy"},
88+
},
89+
Host: u.Host,
90+
}).WithContext(ctx)
91+
if u.User != nil {
92+
req.Header.Set("Proxy-Authorization", basicAuthHeader(u.User))
93+
}
94+
if !c.ReserveNewRequest() {
95+
return nil, fmt.Errorf("unable to reserve garbage fetch request")
96+
}
97+
resp, err := c.RoundTrip(req)
98+
if err != nil {
99+
return nil, fmt.Errorf("garbage fetch request failed: %w", err)
100+
}
101+
defer resp.Body.Close()
102+
n, err := io.Copy(io.Discard, io.LimitReader(resp.Body, int64(garbageLen)))
103+
if err != nil {
104+
return nil, fmt.Errorf("garbage body fetch failed: %w", err)
105+
}
106+
if n < int64(garbageLen) {
107+
return nil, errors.New("incomplete garbage read")
108+
}
109+
return c, nil
110+
},
111+
}
64112
nextDialer := MaybeWrapWithContextDialer(next)
65113
if h2c {
66114
t.DialTLSContext = func(ctx context.Context, network, _ string, _ *tls.Config) (net.Conn, error) {

0 commit comments

Comments
 (0)