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
| 1 | package main |
| 2 | |
| 3 | import ( |
| 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.) |
| 21 | type virtualIPs struct { |
| 22 | mu sync.Mutex |
| 23 | byPort map[int]netip.Addr |
| 24 | next int |
| 25 | } |
| 26 | |
| 27 | func 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. |
| 32 | func (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. |
| 45 | func (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. |
| 71 | func 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". |
| 88 | func (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. |
| 106 | func (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"}`). |
| 121 | func (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. |
| 137 | func (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. |
| 142 | func 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 | } |