ALL

For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.

tunnel/unauth_reserve_test.go

SHA-256: 40171e444cb8c4b6fbe67d673b2713ea6bc871881842120289d72be126015a13
153 lines · 6102 bytes · View on GitHub

1package main
2
3import (
4 "bufio"
5 "context"
6 "crypto/tls"
7 "net"
8 "net/netip"
9 "testing"
10 "time"
11)
12
13// The unauthenticated budget keeps a reserve (policy node_limits.openai_reserved_pct), as the stream budget does. Before it, every connection took a slot of the whole
14// budget before its ClientHello, so twenty addresses holding the most one source may hold (16 half-open connections each) took all 256 of them and every other connection,
15// a ChatGPT client or a device reconnecting, was dropped with no answer for as long as the attack lasted (the chaos suite's slowloris check).
16func TestNode_TwentyAnonymousSourcesCannotStarveChatGPTNorADeviceThatReconnects(t *testing.T) {
17 release := make(chan struct{})
18 e := newVIPEnv(t, func(e *testEnv, c *Config) {
19 c.PerIPConcurrent, c.MaxUnauth, c.OpenAIReservedPct, c.HelloTimeout = 4, 16, 25, 20*time.Second
20 c.PrefixFetch = func(context.Context) ([]byte, error) { return []byte(listOf("192.0.2.0/24")), nil }
21 })
22 eventually(t, "the OpenAI list", 3*time.Second, func() bool { return e.srv.openai.size() == 1 })
23 d := e.newDevice()
24 dc := e.connectOK(d)
25 dc.serve(holdHandler(d, release))
26 defer close(release)
27 // a second device, at an address of its own, connects once and lets go: it will be a device that reconnects
28 const home = "198.51.100.200"
29 back := e.newDevice()
30 first := e.connectOKFrom(home, back)
31 _ = first.c.Close()
32 eventually(t, "the node saw the control connection go", 3*time.Second, func() bool {
33 return e.srv.ips.knownDevice(netip.MustParseAddr(home)) && !e.srv.ips.isDevice(netip.MustParseAddr(home))
34 })
35
36 // twenty anonymous sources, each holding the most it may hold, saying nothing
37 var idle []net.Conn
38 for source := 0; source < 20; source++ {
39 ip := "198.51.100." + itoa(100+source)
40 for i := 0; i < 4; i++ {
41 c, err := e.vip.dial(ip, e.httpsAddr)
42 if err != nil {
43 t.Fatal(err)
44 }
45 defer c.Close()
46 idle = append(idle, c)
47 }
48 }
49 share := e.srv.anonUnauthMax()
50 if share != 12 {
51 t.Fatalf("the anonymous share is %d, want 16 less the reserve of 25 percent: 12", share)
52 }
53 eventually(t, "the anonymous share is full and the rest were refused", 4*time.Second, func() bool {
54 return e.srv.anonUnauth.Load() == share && e.rejected("unauth_anon_limit") >= int64(len(idle))-share
55 })
56 if got := e.srv.unauth.Load(); got > share {
57 t.Fatalf("the attack holds %d unauthenticated slots of 16: more than its share of %d", got, share)
58 }
59
60 // ChatGPT (an address of the OpenAI list) is served through the attack
61 good, err := e.clientFrom("192.0.2.9", d)
62 if err != nil {
63 t.Fatalf("a client from the OpenAI list was turned away while twenty anonymous sources held the budget: %v", err)
64 }
65 _ = good.Close()
66 // a device that reconnects (it held a control connection a moment ago, from this address) gets its HELLO through; one nobody knows waits
67 _ = e.connectOKFrom(home, back)
68 stranger := e.newDevice()
69 if conn, err := e.dialFrom("198.51.100.201"); err == nil {
70 line := (&devConn{t: t, e: e, d: stranger, c: conn, br: bufio.NewReaderSize(conn, 1<<17)})
71 if _, err := line.try(2 * time.Second); err == nil {
72 t.Fatalf("a device from an address nobody knows got an answer through a full anonymous share")
73 }
74 }
75 // an anonymous source with no reason to be trusted is the one that waits: the share is the price of the reserve
76 if _, err := e.clientFrom("203.0.113.77", d); err == nil {
77 t.Fatalf("an anonymous source got through a full anonymous share")
78 }
79 // when the attack ends the share is free again
80 for _, c := range idle {
81 _ = c.Close()
82 }
83 eventually(t, "the anonymous share is free", 4*time.Second, func() bool { return e.srv.anonUnauth.Load() == 0 })
84 later, err := e.clientFrom("203.0.113.77", d)
85 if err != nil {
86 t.Fatalf("an anonymous source is still refused after the attack: %v", err)
87 }
88 _ = later.Close()
89}
90
91// dialFrom connects to the node's own name the way a device at ip does (the source address is the test's choice).
92func (e *testEnv) dialFrom(ip string) (*tls.Conn, error) {
93 raw, err := e.vip.dial(ip, e.httpsAddr)
94 if err != nil {
95 return nil, err
96 }
97 c := tls.Client(raw, &tls.Config{ServerName: testNodeHost, RootCAs: e.nodePool, NextProtos: []string{policyData.Protocol.ALPN}, MinVersion: tls.VersionTLS13})
98 _ = c.SetDeadline(time.Now().Add(3 * time.Second))
99 if err := c.Handshake(); err != nil {
100 _ = raw.Close()
101 return nil, err
102 }
103 _ = c.SetDeadline(time.Time{})
104 e.t.Cleanup(func() { _ = c.Close() })
105 return c, nil
106}
107
108// connectOKFrom is connectOK from a source address of the test's choice.
109func (e *testEnv) connectOKFrom(ip string, d *testDevice) *devConn {
110 e.t.Helper()
111 c, err := e.dialFrom(ip)
112 if err != nil {
113 e.t.Fatalf("the device at %s was turned away: %v", ip, err)
114 }
115 dc := &devConn{t: e.t, e: e, d: d, c: c, br: bufio.NewReaderSize(c, 1<<17)}
116 line := dc.hello(func() string { return e.session(d, nil) }, func(s, n string) string { return e.proof(d, s, n, nil) })
117 m, err := parseLine(line, dirN2D)
118 if err != nil || m.str("verb") != "OK" {
119 e.t.Fatalf("handshake for %s from %s: %q", d.id, ip, line)
120 }
121 dc.ok = m
122 return dc
123}
124
125func TestIPLimits_ADeviceThatLetGoIsKnownForHintTTLAndThenForgotten(t *testing.T) {
126 l, clk := newTestLimits(t, func(c *Config) { c.HintTTL = 15 * time.Minute })
127 addr := netip.MustParseAddr("203.0.113.5")
128 if l.knownDevice(addr) {
129 t.Fatal("an address nobody has seen is known as a device's")
130 }
131 unmark := l.markDevice(addr)
132 if !l.knownDevice(addr) {
133 t.Fatal("an address that holds a control connection is not known as a device's")
134 }
135 unmark()
136 if !l.knownDevice(addr) {
137 t.Fatal("a device that just let go is an anonymous source again at once: its reconnection would wait for the anonymous share")
138 }
139 clk.Advance(14 * time.Minute)
140 if !l.knownDevice(addr) {
141 t.Fatal("known for less than hint_ttl_s after the last connection ended")
142 }
143 clk.Advance(2 * time.Minute)
144 if l.knownDevice(addr) {
145 t.Fatal("known for longer than hint_ttl_s")
146 }
147 l.markDevice(addr)()
148 clk.Advance(20 * time.Minute)
149 l.Sweep()
150 if len(l.past) != 0 {
151 t.Fatalf("the sweep left %d remembered devices", len(l.past))
152 }
153}