ALL

Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.

tunnel/hardening_helpers_test.go

SHA-256: d0b55a4840340636fbe420ecb3b063373139d5a734a39e63b881cd4ab6aa0fe2
151 líneas · 5226 bytes · Ver en GitHub

1package main
2
3import (
4 "crypto/tls"
5 "errors"
6 "fmt"
7 "io"
8 "math/rand/v2"
9 "net"
10 "net/http"
11 "net/netip"
12 "strings"
13 "sync"
14 "testing"
15 "time"
16)
17
18// Every test client of this package comes from loopback, which is ONE source. The hardening tests need many: virtualIPs gives each connection the source
19// address the test names. The test dials from a local port it chose and records which address that port stands for; the node's peerOf hook reads the
20// record by the connection's remote port. (Nothing of this is in the production build: Config.peerOf is nil there.)
21type virtualIPs struct {
22 mu sync.Mutex
23 byPort map[int]netip.Addr
24 next int
25}
26
27func newVirtualIPs() *virtualIPs {
28 return &virtualIPs{byPort: map[int]netip.Addr{}, next: 20000 + rand.IntN(6000)}
29}
30
31// peerOf is the node's hook: the address a port stands for, or the real remote address of a connection nobody chose one for.
32func (v *virtualIPs) peerOf(c net.Conn) (netip.Addr, bool) {
33 if ap, err := netip.ParseAddrPort(c.RemoteAddr().String()); err == nil {
34 v.mu.Lock()
35 addr, ok := v.byPort[int(ap.Port())]
36 v.mu.Unlock()
37 if ok {
38 return addr, true
39 }
40 }
41 return peerAddr(c)
42}
43
44// dial opens a TCP connection to addr that the node sees as coming from ip.
45func (v *virtualIPs) dial(ip, addr string) (net.Conn, error) {
46 want := netip.MustParseAddr(ip)
47 var last error
48 for try := 0; try < 400; try++ {
49 v.mu.Lock()
50 port := v.next
51 if v.next++; v.next >= 32000 {
52 v.next = 20000
53 }
54 v.byPort[port] = want
55 v.mu.Unlock()
56 d := net.Dialer{LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}, Timeout: 3 * time.Second}
57 c, err := d.Dial("tcp", addr)
58 if err == nil {
59 return c, nil
60 }
61 if !strings.Contains(err.Error(), "address already in use") && !strings.Contains(err.Error(), "can't assign requested address") {
62 return nil, err
63 }
64 last = err
65 }
66 return nil, last
67}
68
69// newVIPEnv is newTestEnv with source addresses a test can choose (e.vip.dial, e.clientFrom). The per-source limits are the policy's here: a test that
70// wants other numbers sets them in mutate.
71func newVIPEnv(t *testing.T, mutate func(*testEnv, *Config)) *testEnv {
72 t.Helper()
73 vip := newVirtualIPs()
74 return newEnv(t, nil, func(e *testEnv, cfg *Config) {
75 e.vip = vip
76 cfg.peerOf = vip.peerOf
77 cfg.PerIPConcurrent, cfg.PerIPNewPerS = DefaultConfig().PerIPConcurrent, DefaultConfig().PerIPNewPerS
78 cfg.UnknownSNIPerIP, cfg.LabelNewPerS, cfg.LabelNewBurst = DefaultConfig().UnknownSNIPerIP, DefaultConfig().LabelNewPerS, DefaultConfig().LabelNewBurst
79 cfg.LabelAnonSlice = DefaultConfig().LabelAnonSlice
80 if mutate != nil {
81 mutate(e, cfg)
82 }
83 })
84}
85
86// clientFrom connects to device d the way a ChatGPT client at address ip does and completes the TLS handshake with the device. The error is what the
87// client saw: a node that drops a source without an answer shows as a closed or reset connection, one that refuses with an alert as "internal error".
88func (e *testEnv) clientFrom(ip string, d *testDevice) (*tls.Conn, error) {
89 raw, err := e.vip.dial(ip, e.httpsAddr)
90 if err != nil {
91 return nil, err
92 }
93 tc := tls.Client(raw, &tls.Config{ServerName: d.host, RootCAs: d.pool, NextProtos: []string{"http/1.1"}, MinVersion: tls.VersionTLS12})
94 _ = tc.SetDeadline(time.Now().Add(4 * time.Second))
95 if err := tc.Handshake(); err != nil {
96 _ = raw.Close()
97 return nil, err
98 }
99 _ = tc.SetDeadline(time.Time{})
100 e.t.Cleanup(func() { _ = tc.Close() })
101 return tc, nil
102}
103
104// helloFrom connects from ip and sends a ClientHello for host (no device behind it needed); it returns the connection and what the node did, as the
105// bytes it answered with until it closed (a TLS alert is 7 bytes, a dropped source is none) and whether it closed within the wait.
106func (e *testEnv) helloFrom(ip, host string, wait time.Duration) (answer []byte, closed bool) {
107 e.t.Helper()
108 raw, err := e.vip.dial(ip, e.httpsAddr)
109 if err != nil {
110 e.t.Fatal(err)
111 }
112 defer raw.Close()
113 _, _ = raw.Write(helloOf(host, 0, 16000))
114 _ = raw.SetReadDeadline(time.Now().Add(wait))
115 b, err := io.ReadAll(raw)
116 var ne net.Error
117 return b, !(errors.As(err, &ne) && ne.Timeout())
118}
119
120// metricValue reads one series of the node's metrics text ("name" or `name{label="word"}`).
121func (e *testEnv) metricValue(series string) int64 {
122 e.t.Helper()
123 for _, line := range strings.Split(e.srv.renderMetrics(), "\n") {
124 if rest, ok := strings.CutPrefix(line, series+" "); ok {
125 var n int64
126 if _, err := fmt.Sscan(rest, &n); err != nil {
127 e.t.Fatalf("series %s: %q", series, line)
128 }
129 return n
130 }
131 }
132 e.t.Fatalf("no series %s in the metrics:\n%s", series, e.srv.renderMetrics())
133 return 0
134}
135
136// rejected is how many connections the node counted as refused for reason.
137func (e *testEnv) rejected(reason string) int64 {
138 return e.metricValue(fmt.Sprintf(`all_tunnel_conn_rejected_total{reason=%q}`, reason))
139}
140
141// httpGet reads a loopback URL of the node's metrics listener.
142func httpGet(t *testing.T, url string) (int, string, http.Header) {
143 t.Helper()
144 resp, err := (&http.Client{Timeout: 5 * time.Second}).Get(url)
145 if err != nil {
146 t.Fatal(err)
147 }
148 defer resp.Body.Close()
149 body, _ := io.ReadAll(resp.Body)
150 return resp.StatusCode, string(body), resp.Header
151}