Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.
tunnel/unauth_reserve_test.go
| 1 | package main |
| 2 | |
| 3 | import ( |
| 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). |
| 16 | func 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). |
| 92 | func (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. |
| 109 | func (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 | |
| 125 | func 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 | } |