carlos / platform Public

relay: bound refusal state, meter rejected per spec, exempt STUN control from buckets, close orphaned allocations (review fix)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 3dd13cb6cd68b53e7fdeec9c974ab68103a5ac1f parent 6add083
3 files changed, +497 −73
  • go.mod +1 −1
  • internal/relay/server.go +303 −40
  • internal/relay/server_test.go +193 −32
diff --git a/go.mod b/go.mod
index 0d683e268..c7c41ef8f 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
github.com/aws/smithy-go v1.28.1
github.com/benbjohnson/litestream v0.5.14
github.com/pion/logging v0.2.4
+ github.com/pion/stun/v3 v3.0.1
github.com/pion/turn/v4 v4.1.4
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
@@ -57,7 +58,6 @@ require (
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pion/dtls/v3 v3.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
- github.com/pion/stun/v3 v3.0.1 // indirect
github.com/pion/transport/v3 v3.0.8 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
diff --git a/internal/relay/server.go b/internal/relay/server.go
index 403d01380..4e9bc4d48 100644
--- a/internal/relay/server.go
+++ b/internal/relay/server.go
@@ -1,6 +1,7 @@
package relay
import (
+ "encoding/binary"
"errors"
"fmt"
"math/rand/v2"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/pion/logging"
+ "github.com/pion/stun/v3"
"github.com/pion/turn/v4"
)
@@ -28,6 +30,25 @@ const pendingLife = 30 * time.Second
// and never allocates cannot grow the map without bound.
const authMemory = 10 * time.Minute
+const (
+ // maxRefusalIdentities and maxLoggedRefusals bound the two maps an
+ // unauthenticated client can otherwise grow: pion calls AuthHandler
+ // with the raw username before it checks MESSAGE-INTEGRITY, so every
+ // counter keyed on something the caller chose is remote memory. Only
+ // identities in the authority record get their own row; everything
+ // else shares the zero identity, and these caps are the backstop.
+ maxRefusalIdentities = 4096
+ maxLoggedRefusals = 4096
+
+ // refusalLogEvery is how often one identity's one refusal reason may
+ // put a line in the log.
+ refusalLogEvery = time.Minute
+
+ // minLastAuthEntries is the floor under Limits.BoxLive*2 for how many
+ // typed sources may hold a remembered username at once.
+ minLastAuthEntries = 256
+)
+
var errTCPAllocUnsupported = errors.New("relay: TCP relay allocations are not supported")
// ServerConfig is everything the relay needs from its process: where to
@@ -39,6 +60,7 @@ type ServerConfig struct {
UDP, TCP string // listen addresses, e.g. ":3478"; empty disables
MinPort, MaxPort uint16 // 49152, 65535
DenyCIDRs []*net.IPNet // from CARLOS_RELAY_DENY_CIDRS, on top of the built-in private ranges
+ ExtraAllow []*net.IPNet // peer ranges allowed despite the built-in list; empty in production
Masters func() *Masters
Authority func() *Authority
Lease func(now time.Time) bool
@@ -61,11 +83,13 @@ type Server struct {
now func() time.Time
log func(string, ...any)
- adm *Admission
- box *Bucket
- ports *portSet
- deny []*net.IPNet
- local []net.IP // our listeners' own addresses, when not wildcard
+ adm *Admission
+ box *Bucket
+ ports *portSet
+ deny []*net.IPNet
+ allow []*net.IPNet
+ local []net.IP // our listeners' own addresses, when not wildcard
+ maxLastAuth int
turn *turn.Server
udp net.PacketConn
@@ -80,23 +104,87 @@ type Server struct {
bound map[string]*Binding // typed source -> the identity it is bound to
allocs map[string]*allocEntry // typed source -> its live allocation
owners map[*relayConn]*allocEntry // socket -> what closing it must undo
+ byPort map[int]*relayConn // relay port -> its socket, however far the allocation got
refusals map[Identity]map[string]int
+ logged map[logKey]time.Time
shedding map[Identity]bool
}
+type logKey struct {
+ id Identity
+ reason string
+}
+
type allocEntry struct {
conn *relayConn
id Identity
key string // the typed source this allocation was created for
seq int64
timer *time.Timer
+ flow *flow
}
type pendingConn struct {
conn *relayConn
+ flow *flow
at time.Time
}
+// flow is one allocation's peer-to-client accounting. pion's allocation
+// reads a peer packet and then either forwards it to the client or drops
+// it for want of a permission, both on the allocation's own goroutine, so
+// a packet that is still unforwarded when the next one arrives (or when
+// the socket closes) is a packet that was dropped. That is what spec §3
+// counts as `rejected`, and it is never charged to a budget.
+type flow struct {
+ mu sync.Mutex
+ awaiting int64 // size of the peer packet read and not yet forwarded
+}
+
+// read records a peer packet and returns the bytes of the previous one if
+// it was never forwarded.
+func (f *flow) read(n int) int64 {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ dropped := f.awaiting
+ f.awaiting = int64(n)
+ return dropped
+}
+
+// forwarded clears the packet the listener has just written to the client.
+func (f *flow) forwarded() {
+ f.mu.Lock()
+ f.awaiting = 0
+ f.mu.Unlock()
+}
+
+// flush is the last packet's verdict when the socket closes.
+func (f *flow) flush() int64 {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ dropped := f.awaiting
+ f.awaiting = 0
+ return dropped
+}
+
+// readCounter is the relay socket's peer-ingress leg. It sits under
+// relayConn so that relayConn keeps counting only what it forwards.
+type readCounter struct {
+ net.PacketConn
+ f *flow
+ onDrop func(int64)
+}
+
+func (r *readCounter) ReadFrom(p []byte) (int, net.Addr, error) {
+ n, addr, err := r.PacketConn.ReadFrom(p)
+ if n > 0 {
+ if dropped := r.f.read(n); dropped > 0 && r.onDrop != nil {
+ r.onDrop(dropped)
+ }
+ }
+ return n, addr, err
+}
+
type authRec struct {
u Username
at time.Time
@@ -153,22 +241,30 @@ func NewServer(cfg ServerConfig) (*Server, error) {
return nil, errors.New("relay: no listener configured")
}
+ maxLastAuth := cfg.Limits.BoxLive * 2
+ if maxLastAuth < minLastAuthEntries {
+ maxLastAuth = minLastAuthEntries
+ }
s := &Server{
- cfg: cfg,
- now: cfg.Now,
- log: cfg.Log,
- adm: NewAdmission(cfg.Limits, cfg.Now),
- box: NewBucket(cfg.Limits.BoxBitsPerSec, cfg.Now),
- ports: &portSet{m: map[int]bool{}},
- deny: append(append([]*net.IPNet{}, builtinDeny...), cfg.DenyCIDRs...),
- pending: map[int]pendingConn{},
- lastAuth: map[string]authRec{},
- admits: map[string]admitRec{},
- bound: map[string]*Binding{},
- allocs: map[string]*allocEntry{},
- owners: map[*relayConn]*allocEntry{},
- refusals: map[Identity]map[string]int{},
- shedding: map[Identity]bool{},
+ cfg: cfg,
+ now: cfg.Now,
+ log: cfg.Log,
+ adm: NewAdmission(cfg.Limits, cfg.Now),
+ box: NewBucket(cfg.Limits.BoxBitsPerSec, cfg.Now),
+ ports: &portSet{m: map[int]bool{}},
+ deny: append(append([]*net.IPNet{}, builtinDeny...), cfg.DenyCIDRs...),
+ allow: append([]*net.IPNet{}, cfg.ExtraAllow...),
+ maxLastAuth: maxLastAuth,
+ pending: map[int]pendingConn{},
+ lastAuth: map[string]authRec{},
+ admits: map[string]admitRec{},
+ bound: map[string]*Binding{},
+ allocs: map[string]*allocEntry{},
+ owners: map[*relayConn]*allocEntry{},
+ byPort: map[int]*relayConn{},
+ refusals: map[Identity]map[string]int{},
+ logged: map[logKey]time.Time{},
+ shedding: map[Identity]bool{},
}
var packets []turn.PacketConnConfig
@@ -411,6 +507,21 @@ func (s *Server) tick() {
delete(s.lastAuth, key)
}
}
+ // Refusal counters live only for apps the authority still names; the
+ // zero identity is the shared bucket for everything else and stays.
+ for id := range s.refusals {
+ if id == (Identity{}) {
+ continue
+ }
+ if _, ok := s.cfg.Authority().Lookup(id.Account, id.App); !ok {
+ delete(s.refusals, id)
+ }
+ }
+ for k, at := range s.logged {
+ if now.Sub(at) >= refusalLogEvery {
+ delete(s.logged, k)
+ }
+ }
s.mu.Unlock()
for _, id := range lapsed {
@@ -470,11 +581,17 @@ func (s *Server) AllocatePacketConn(network string, requestedPort int) (net.Pack
continue
}
bucket := NewBucket(s.cfg.Limits.AllocBitsPerSec, s.now)
- c := newRelayConn(pc, &Binding{}, s.cfg.RelayIP, s.ports, bucket, s.box,
- s.countIn, s.countRejected, func(rc *relayConn) { s.release(port, rc) })
+ f := &flow{}
+ var c *relayConn
+ // The socket counts peer ingress under the wrapper, so a peer
+ // packet pion drops for want of a permission is still seen.
+ counted := &readCounter{PacketConn: pc, f: f, onDrop: func(n int64) { s.countRejected(c.b.ID, n) }}
+ c = newRelayConn(counted, &Binding{}, s.cfg.RelayIP, s.ports, bucket, s.box,
+ s.countIn, s.countGated, func(rc *relayConn) { s.release(port, rc) })
s.ports.add(port)
s.mu.Lock()
- s.pending[port] = pendingConn{conn: c, at: s.now()}
+ s.pending[port] = pendingConn{conn: c, flow: f, at: s.now()}
+ s.byPort[port] = c
s.mu.Unlock()
return c, &net.UDPAddr{IP: s.cfg.RelayIP, Port: port}, nil
}
@@ -537,12 +654,40 @@ func (s *Server) auth(username, realm string, src net.Addr) ([]byte, bool) {
}
key := DeriveKey(master.Key, u.Account, u.App, u.Incarnation)
- s.mu.Lock()
- s.lastAuth[typedSrc(src)] = authRec{u: u, at: now}
- s.mu.Unlock()
+ s.rememberAuth(typedSrc(src), u, now)
return turn.GenerateAuthKey(username, realm, Credential(key, username)), true
}
+// rememberAuth stores the username a source last authenticated with, for
+// the allocation event that follows. pion verifies MESSAGE-INTEGRITY only
+// after the handler returns, so this map is written for sources that have
+// not proved anything yet: tick ages entries out, and this caps them at
+// twice the box's live-allocation limit, dropping the oldest quarter when
+// it fills.
+func (s *Server) rememberAuth(key string, u Username, now time.Time) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if _, known := s.lastAuth[key]; !known && len(s.lastAuth) >= s.maxLastAuth {
+ type aged struct {
+ key string
+ at time.Time
+ }
+ all := make([]aged, 0, len(s.lastAuth))
+ for k, r := range s.lastAuth {
+ all = append(all, aged{k, r.at})
+ }
+ sort.Slice(all, func(i, j int) bool { return all[i].at.Before(all[j].at) })
+ drop := len(all) / 4
+ if drop == 0 {
+ drop = 1
+ }
+ for _, a := range all[:drop] {
+ delete(s.lastAuth, a.key)
+ }
+ }
+ s.lastAuth[key] = authRec{u: u, at: now}
+}
+
// quota is pion's QuotaHandler, called on Allocate only: a refusal answers
// 486. The lease comes first, because a relay whose authority read is
// stale must take nothing new on.
@@ -591,8 +736,17 @@ func (s *Server) created(src, _ net.Addr, _, _, _ string, relayAddr net.Addr, _
s.mu.Lock()
p, havePending := s.pending[ua.Port]
if !havePending {
+ // The sweep already reclaimed this socket, or it was never ours.
+ // Either way pion now owns an allocation no deadline of ours can
+ // end, so end it here by closing the socket it reads from.
+ orphan := s.byPort[ua.Port]
s.mu.Unlock()
- s.log("relay: allocation created on port %d with no socket of ours", ua.Port)
+ if orphan != nil {
+ s.log("relay: allocation created on port %d after its socket was reclaimed: ending it", ua.Port)
+ orphan.Close()
+ } else {
+ s.log("relay: allocation created on port %d with no socket of ours", ua.Port)
+ }
return
}
delete(s.pending, ua.Port)
@@ -610,7 +764,7 @@ func (s *Server) created(src, _ net.Addr, _, _, _ string, relayAddr net.Addr, _
c.b.Incarnation = rec.u.Incarnation
c.b.Src = key
s.seq++
- e := &allocEntry{conn: c, id: c.b.ID, key: key, seq: s.seq}
+ e := &allocEntry{conn: c, id: c.b.ID, key: key, seq: s.seq, flow: p.flow}
s.allocs[key] = e
s.owners[c] = e
s.bound[key] = c.b
@@ -643,11 +797,17 @@ func (s *Server) release(port int, c *relayConn) {
s.ports.remove(port)
var id Identity
var release bool
+ var last int64
s.mu.Lock()
if p, ok := s.pending[port]; ok && p.conn == c {
delete(s.pending, port)
+ last = p.flow.flush()
+ }
+ if cur, ok := s.byPort[port]; ok && cur == c {
+ delete(s.byPort, port)
}
if e, ok := s.owners[c]; ok {
+ last = e.flow.flush()
delete(s.owners, c)
if e.timer != nil {
e.timer.Stop()
@@ -664,6 +824,10 @@ func (s *Server) release(port int, c *relayConn) {
if release {
s.adm.Release(id)
}
+ if last > 0 {
+ // The last peer packet was read and never forwarded.
+ s.countRejected(c.b.ID, last)
+ }
}
// permit is pion's PermissionHandler: it sees only a peer IP, so the port
@@ -692,6 +856,13 @@ func (s *Server) permit(client net.Addr, peer net.IP) bool {
if s.cfg.RelayIP != nil && peer.Equal(s.cfg.RelayIP) {
return true
}
+ // ExtraAllow is the operator's (and the test fixture's) way to permit
+ // a range the built-in list denies. Empty on a real box.
+ for _, n := range s.allow {
+ if n.Contains(peer) {
+ return true
+ }
+ }
for _, n := range s.deny {
if n.Contains(peer) {
return deny("denied range " + n.String())
@@ -729,13 +900,20 @@ func (s *Server) countOut(id Identity, n int) {
}
}
-// countRejected is what we dropped rather than forwarded: a packet aimed at
-// our own EIP on a port that is not a live allocation. Never a budget.
-func (s *Server) countRejected(id Identity, n int) {
+// countRejected is spec §3's `rejected`: peer bytes that reached a relay
+// socket and were dropped for want of a permission. Metered, never charged
+// to a budget.
+func (s *Server) countRejected(id Identity, n int64) {
if s.cfg.Meter != nil {
- s.cfg.Meter.Add(id, 0, 0, int64(n))
+ s.cfg.Meter.Add(id, 0, 0, n)
}
- s.countRefusal(id, "eip port")
+}
+
+// countGated is the other kind of drop: a client packet aimed at our own
+// EIP on a port that is not a live allocation. It is a policy refusal, not
+// a meter line, so it is counted per app and goes no further.
+func (s *Server) countGated(id Identity, _ int) {
+ s.countRefusal(id, "gated")
}
// overBudget ends an app's allocations once its rolling-hour budget is
@@ -757,9 +935,29 @@ func (s *Server) overBudget(id Identity) {
}()
}
+// attrib is which identity a refusal may be counted against. pion hands
+// the AuthHandler whatever username arrived, before any integrity check,
+// so an unauthenticated sender picks the account and app strings. Only an
+// app the authority record actually names gets a row of its own; every
+// other refusal shares the zero identity.
+func (s *Server) attrib(id Identity) Identity {
+ if id.Account == "" || id.App == "" {
+ return Identity{}
+ }
+ if _, ok := s.cfg.Authority().Lookup(id.Account, id.App); !ok {
+ return Identity{}
+ }
+ return id
+}
+
func (s *Server) countRefusal(id Identity, reason string) {
+ id = s.attrib(id)
s.mu.Lock()
m := s.refusals[id]
+ if m == nil && len(s.refusals) >= maxRefusalIdentities {
+ id = Identity{}
+ m = s.refusals[id]
+ }
if m == nil {
m = map[string]int{}
s.refusals[id] = m
@@ -768,11 +966,31 @@ func (s *Server) countRefusal(id Identity, reason string) {
s.mu.Unlock()
}
+// mayLog rate-limits one identity's one refusal reason to a line a minute,
+// so a flood of refusals cannot be turned into a flood of log.
+func (s *Server) mayLog(id Identity, reason string) bool {
+ k := logKey{id: s.attrib(id), reason: reason}
+ now := s.now()
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if at, ok := s.logged[k]; ok {
+ if now.Sub(at) < refusalLogEvery {
+ return false
+ }
+ } else if len(s.logged) >= maxLoggedRefusals {
+ return false
+ }
+ s.logged[k] = now
+ return true
+}
+
// refused counts a refusal against the app and logs why. Its return values
// are the AuthHandler's, so a refusal is one line at the call site.
func (s *Server) refused(id Identity, reason, format string, args ...any) ([]byte, bool) {
s.countRefusal(id, reason)
- s.log(format, args...)
+ if s.mayLog(id, reason) {
+ s.log(format, args...)
+ }
return nil, false
}
@@ -782,6 +1000,12 @@ func (s *Server) refusalCount(id Identity, reason string) int {
return s.refusals[id][reason]
}
+func (s *Server) refusalIdentities() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return len(s.refusals)
+}
+
// entryFor is the owner binding lookup the out leg is attributed through.
func (s *Server) entryFor(key string) *allocEntry {
s.mu.Lock()
@@ -801,23 +1025,56 @@ type listenerConn struct {
}
func (c *listenerConn) WriteTo(p []byte, addr net.Addr) (int, error) {
+ // A TURN response is the client asking us something, not relayed
+ // media: bucketing it would drop Refresh responses at the app's rate
+ // cap and end calls for a reason spec §3 does not list, and metering
+ // it would bill the client for its own signalling.
+ if !relayedPayload(p) {
+ return c.PacketConn.WriteTo(p, addr)
+ }
e := c.s.entryFor(typedSrc(addr))
if e != nil {
// Peek both, then spend both: a box refusal must not leave the
// app's bucket debited (see relayConn.WriteTo).
- if !e.conn.appBucket.Peek(len(p)) || !c.s.box.Peek(len(p)) {
+ if e.conn.appBucket != nil && !e.conn.appBucket.Peek(len(p)) {
return len(p), nil
}
- e.conn.appBucket.Spend(len(p))
- c.s.box.Spend(len(p))
+ if c.s.box != nil && !c.s.box.Peek(len(p)) {
+ return len(p), nil
+ }
+ if e.conn.appBucket != nil {
+ e.conn.appBucket.Spend(len(p))
+ }
+ if c.s.box != nil {
+ c.s.box.Spend(len(p))
+ }
}
n, err := c.PacketConn.WriteTo(p, addr)
if n > 0 && e != nil {
+ e.flow.forwarded()
c.s.countOut(e.id, n)
}
return n, err
}
+// relayedPayload reports whether a packet bound for a client carries peer
+// media rather than TURN signalling: a ChannelData message (first two bits
+// 0b01, so 0x40 to 0x7f) or a Data indication.
+func relayedPayload(p []byte) bool {
+ if len(p) == 0 {
+ return false
+ }
+ if p[0]&0xc0 == 0x40 {
+ return true
+ }
+ if !stun.IsMessage(p) {
+ return false
+ }
+ var t stun.MessageType
+ t.ReadValue(binary.BigEndian.Uint16(p[0:2]))
+ return t.Method == stun.MethodData && t.Class == stun.ClassIndication
+}
+
// countingListener wraps the TCP listener so the out leg is metered for
// TCP clients too. A TCP stream cannot have bytes dropped out of it the
// way a datagram can, so the buckets are spent but never enforced here.
@@ -841,11 +1098,17 @@ type countingConn struct {
}
func (c *countingConn) Write(p []byte) (int, error) {
+ payload := relayedPayload(p)
n, err := c.Conn.Write(p)
- if n > 0 {
+ if n > 0 && payload {
if e := c.s.entryFor(c.key); e != nil {
- e.conn.appBucket.Spend(n)
- c.s.box.Spend(n)
+ if e.conn.appBucket != nil {
+ e.conn.appBucket.Spend(n)
+ }
+ if c.s.box != nil {
+ c.s.box.Spend(n)
+ }
+ e.flow.forwarded()
c.s.countOut(e.id, n)
}
}
diff --git a/internal/relay/server_test.go b/internal/relay/server_test.go
index 375245c4e..3ba4b66f6 100644
--- a/internal/relay/server_test.go
+++ b/internal/relay/server_test.go
@@ -28,7 +28,8 @@ func testMaster() []byte {
// loopback sockets must advertise something else, because the EIP port
// gate (spec §3) drops anything aimed at the advertised address on a port
// that is not a live allocation — and on loopback every peer is on it.
-func newTestServer(t *testing.T, relayIP string, meter *fakeMeter) (s *Server, setNow func(time.Time), now func() time.Time, masters *Masters, auth *Authority) {
+// tweak, when given, is applied to the config before the server starts.
+func newTestServer(t *testing.T, relayIP string, meter *fakeMeter, tweak func(*ServerConfig)) (s *Server, setNow func(time.Time), now func() time.Time, masters *Masters, auth *Authority) {
t.Helper()
master := testMaster()
masters, err := ParseMasters(strings.NewReader("k1 " + b64(master) + " -\n"))
@@ -66,6 +67,9 @@ func newTestServer(t *testing.T, relayIP string, meter *fakeMeter) (s *Server, s
if meter != nil {
cfg.Meter = meter
}
+ if tweak != nil {
+ tweak(&cfg)
+ }
s, err = NewServer(cfg)
if err != nil {
t.Fatal(err)
@@ -128,7 +132,7 @@ func waitFor(t *testing.T, cond func() bool) {
}
func TestServerEndToEnd(t *testing.T) {
- s, setNow, now, _, _ := newTestServer(t, "127.0.0.1", nil)
+ s, setNow, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
client := turnClient(t, s.UDPAddr(), username, password)
@@ -155,7 +159,9 @@ func TestServerEndToEnd(t *testing.T) {
if _, err := turnClient(t, s.UDPAddr(), otherUser, otherPass).Allocate(); err == nil {
t.Fatal("unknown app allocated")
}
- if s.refusalCount(Identity{"abc", "docs"}, "unknown app") != 1 {
+ // It is counted against the shared unknown bucket, not against an
+ // identity the caller made up.
+ if s.refusalCount(Identity{}, "unknown app") != 1 {
t.Fatalf("refusals = %v", s.refusals)
}
@@ -169,12 +175,8 @@ func TestServerEndToEnd(t *testing.T) {
// and checks both meter legs. It advertises an EIP that is not the peers'
// address, as a real box does.
func TestServerRelaysBothLegs(t *testing.T) {
- allowLoopbackPeers(t)
meter := &fakeMeter{}
- s, _, now, _, _ := newTestServer(t, "203.0.113.5", meter)
- // The fixture listens on loopback, so its own listener address is the
- // peers' address too; a real box's listener is not on the peer's net.
- s.local = nil
+ s, _, now, _, _ := newTestServer(t, "203.0.113.5", meter, allowLoopbackPeers)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
client := turnClient(t, s.UDPAddr(), username, password)
relayConn, err := client.Allocate()
@@ -225,19 +227,11 @@ func TestServerRelaysBothLegs(t *testing.T) {
}
}
-// allowLoopbackPeers drops loopback from the built-in deny list for one
-// test. A relay on a real box never permits a loopback peer; a test that
-// carries traffic between two sockets on this machine has nowhere else to
-// put them, which is why builtinDeny is a var.
-func allowLoopbackPeers(t *testing.T) {
- t.Helper()
- saved := builtinDeny
- builtinDeny = mustCIDRs(
- "169.254.0.0/16", "fe80::/10",
- "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
- "fc00::/7", "100.64.0.0/10",
- )
- t.Cleanup(func() { builtinDeny = saved })
+// allowLoopbackPeers is the ExtraAllow seam: a relay on a real box never
+// permits a loopback peer, and a test that carries traffic between two
+// sockets on this machine has nowhere else to put them.
+func allowLoopbackPeers(cfg *ServerConfig) {
+ cfg.ExtraAllow = mustCIDRs("127.0.0.0/8")
}
// fakeMeter is the Task 7 meter's shape, which ServerConfig.Meter names as
@@ -265,7 +259,7 @@ func (m *fakeMeter) totals(id Identity) (in, out, rejected int64) {
}
func TestServerRefusesPrivatePeer(t *testing.T) {
- s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil)
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
client := turnClient(t, s.UDPAddr(), username, password)
if _, err := client.Allocate(); err != nil {
@@ -277,7 +271,7 @@ func TestServerRefusesPrivatePeer(t *testing.T) {
}
func TestServerEndWhere(t *testing.T) {
- s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil)
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
client := turnClient(t, s.UDPAddr(), username, password)
if _, err := client.Allocate(); err != nil {
@@ -291,7 +285,7 @@ func TestServerEndWhere(t *testing.T) {
}
func TestServerShedOldest(t *testing.T) {
- s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil)
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
first := turnClient(t, s.UDPAddr(), username, password)
if _, err := first.Allocate(); err != nil {
@@ -312,7 +306,7 @@ func TestServerShedOldest(t *testing.T) {
// only refuses new allocations bounds nothing: seeded past its hourly
// budget, the app's live allocation goes too.
func TestServerBudgetEndsAllocations(t *testing.T) {
- s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil)
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
client := turnClient(t, s.UDPAddr(), username, password)
if _, err := client.Allocate(); err != nil {
@@ -324,12 +318,9 @@ func TestServerBudgetEndsAllocations(t *testing.T) {
}
func TestServerAuthRefusals(t *testing.T) {
- s, _, now, masters, auth := newTestServer(t, "127.0.0.1", nil)
+ s, _, now, masters, auth := newTestServer(t, "127.0.0.1", nil, nil)
src := &net.UDPAddr{IP: net.ParseIP("192.0.2.7"), Port: 5000}
- sign := func(username, account, app, incarnation string) string {
- return Credential(DeriveKey(testMaster(), account, app, incarnation), username)
- }
ok := func(username string) bool {
key, ok := s.auth(username, "carlos", src)
return ok && len(key) > 0
@@ -352,7 +343,7 @@ func TestServerAuthRefusals(t *testing.T) {
if ok(c.username) {
t.Fatalf("%s: accepted", c.name)
}
- if s.refusalCount(refusalIdentity(c.username), c.reason) == 0 {
+ if s.refusalCount(s.attrib(refusalIdentity(c.username)), c.reason) == 0 {
t.Fatalf("%s: reason %q not counted (%v)", c.name, c.reason, s.refusals)
}
}
@@ -381,7 +372,6 @@ func TestServerAuthRefusals(t *testing.T) {
s.mu.Unlock()
auth.Apps = append(auth.Apps, AuthorityApp{Account: "abc", App: "docs", Incarnation: "0123456789abcdef", Regions: []string{"test"}})
docs := fmt.Sprintf("%d:%s", now().Add(time.Hour).Unix(), Suffix("abc", "docs", "0123456789abcdef", "k1"))
- _ = sign(docs, "abc", "docs", "0123456789abcdef")
if ok(docs) {
t.Fatal("a second app allocated from a bound source")
}
@@ -399,6 +389,177 @@ func TestServerAuthRefusals(t *testing.T) {
}
}
+// TestServerRefusalStateIsBounded is the memory-exhaustion case: pion
+// hands the AuthHandler the raw username before it checks
+// MESSAGE-INTEGRITY, and the nonce is not source-bound, so an unauthenticated
+// sender chooses the account and app strings. None of that may become a
+// map entry of its own.
+func TestServerRefusalStateIsBounded(t *testing.T) {
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
+ src := &net.UDPAddr{IP: net.ParseIP("192.0.2.10"), Port: 6000}
+ exp := now().Add(time.Hour).Unix()
+ const flood = 10000
+ for i := 0; i < flood; i++ {
+ username := fmt.Sprintf("%d:%s", exp, Suffix(fmt.Sprintf("a%d", i), "meet", "0123456789abcdef", "k1"))
+ if _, ok := s.auth(username, "carlos", src); ok {
+ t.Fatalf("made-up account %d authenticated", i)
+ }
+ }
+ if n := s.refusalIdentities(); n > 2 {
+ t.Fatalf("refusal identities = %d, want at most 2", n)
+ }
+ if got := s.refusalCount(Identity{}, "unknown app"); got != flood {
+ t.Fatalf("unknown-app refusals = %d, want %d", got, flood)
+ }
+ s.mu.Lock()
+ auths, logs := len(s.lastAuth), len(s.logged)
+ s.mu.Unlock()
+ if auths != 0 {
+ t.Fatalf("lastAuth = %d, want 0: nothing authenticated", auths)
+ }
+ if logs > 2 {
+ t.Fatalf("log records = %d, want the refusal log rate-limited", logs)
+ }
+}
+
+// TestServerAuthMemoryIsCapped covers the other half: usernames that do
+// verify are remembered for the allocation event, but only so many.
+func TestServerAuthMemoryIsCapped(t *testing.T) {
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
+ username, _ := creds(now(), "abc", "meet", "0123456789abcdef")
+ for i := 0; i < s.maxLastAuth+600; i++ {
+ src := &net.UDPAddr{IP: net.ParseIP("192.0.2.11"), Port: 1024 + i}
+ if _, ok := s.auth(username, "carlos", src); !ok {
+ t.Fatalf("source %d refused", i)
+ }
+ }
+ s.mu.Lock()
+ n := len(s.lastAuth)
+ s.mu.Unlock()
+ if n > s.maxLastAuth {
+ t.Fatalf("lastAuth = %d, want at most %d", n, s.maxLastAuth)
+ }
+}
+
+// TestServerCountsRejectedPeerBytes is spec §3's `rejected`: peer bytes
+// that reached a relay socket and were dropped for want of a permission.
+func TestServerCountsRejectedPeerBytes(t *testing.T) {
+ meter := &fakeMeter{}
+ s, _, now, _, _ := newTestServer(t, "203.0.113.5", meter, nil)
+ username, password := creds(now(), "abc", "meet", "0123456789abcdef")
+ client := turnClient(t, s.UDPAddr(), username, password)
+ relayed, err := client.Allocate()
+ if err != nil {
+ t.Fatalf("allocate: %v", err)
+ }
+ defer relayed.Close()
+ port := relayed.LocalAddr().(*net.UDPAddr).Port
+
+ peer, err := net.ListenPacket("udp4", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer peer.Close()
+ // No permission was ever created for this peer, so pion drops both.
+ to := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}
+ for i := 0; i < 2; i++ {
+ if _, err := peer.WriteTo(make([]byte, 100), to); err != nil {
+ t.Fatalf("peer write: %v", err)
+ }
+ }
+ id := Identity{"abc", "meet"}
+ waitFor(t, func() bool { _, _, rejected := meter.totals(id); return rejected >= 100 })
+
+ s.EndAll("test")
+ waitFor(t, func() bool { return s.Live() == 0 })
+ in, out, rejected := meter.totals(id)
+ if rejected != 200 {
+ t.Fatalf("rejected = %d, want 200", rejected)
+ }
+ if out != 0 {
+ t.Fatalf("out = %d, want 0: nothing was forwarded to the client", out)
+ }
+ if in != 0 {
+ t.Fatalf("in = %d, want 0", in)
+ }
+}
+
+// TestServerControlIsNotBucketed: at a saturated per-allocation bucket a
+// client must still be able to allocate and refresh. Only relayed payload
+// is bucketed and metered.
+func TestServerControlIsNotBucketed(t *testing.T) {
+ meter := &fakeMeter{}
+ s, _, now, _, _ := newTestServer(t, "203.0.113.5", meter, func(cfg *ServerConfig) {
+ cfg.Limits.AllocBitsPerSec = 8 // one byte a second: every control response would be dropped
+ cfg.Limits.BoxBitsPerSec = 8
+ })
+ username, password := creds(now(), "abc", "meet", "0123456789abcdef")
+ client := turnClient(t, s.UDPAddr(), username, password)
+ relayed, err := client.Allocate()
+ if err != nil {
+ t.Fatalf("allocate at a saturated bucket: %v", err)
+ }
+ defer relayed.Close()
+ // A second round trip on the same listener, after the allocation
+ // exists: its response must get through too.
+ if _, err := client.SendBindingRequest(); err != nil {
+ t.Fatalf("binding request at a saturated bucket: %v", err)
+ }
+ if in, out, rejected := meter.totals(Identity{"abc", "meet"}); in|out|rejected != 0 {
+ t.Fatalf("signalling was metered: in=%d out=%d rejected=%d", in, out, rejected)
+ }
+}
+
+// TestServerClosesOrphanedAllocation: if the pending socket is gone by the
+// time pion reports the allocation, pion owns an allocation no deadline of
+// ours can end. It must be ended on the spot.
+func TestServerClosesOrphanedAllocation(t *testing.T) {
+ s, setNow, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
+ src := &net.UDPAddr{IP: net.ParseIP("192.0.2.12"), Port: 7000}
+
+ // The socket is still ours, but its pending record has gone.
+ _, addr, err := s.AllocatePacketConn("udp4", 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ port := addr.(*net.UDPAddr).Port
+ username, _ := creds(now(), "abc", "meet", "0123456789abcdef")
+ if _, ok := s.auth(username, "carlos", src); !ok {
+ t.Fatal("fixture credential refused")
+ }
+ s.mu.Lock()
+ delete(s.pending, port)
+ s.mu.Unlock()
+ s.created(src, nil, "udp", username, "carlos", addr, 0)
+ if s.Live() != 0 {
+ t.Fatalf("live = %d, want the orphan ended", s.Live())
+ }
+ if s.ports.has(port) {
+ t.Fatal("the orphaned socket was left open")
+ }
+
+ // And the real race: the sweep reclaimed the socket before pion
+ // reported the allocation.
+ _, addr2, err := s.AllocatePacketConn("udp4", 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ port2 := addr2.(*net.UDPAddr).Port
+ setNow(now().Add(pendingLife + time.Second))
+ s.tick()
+ if s.ports.has(port2) {
+ t.Fatal("the sweep left the socket open")
+ }
+ username2, _ := creds(now(), "abc", "meet", "0123456789abcdef")
+ if _, ok := s.auth(username2, "carlos", src); !ok {
+ t.Fatal("fixture credential refused after the clock moved")
+ }
+ s.created(src, nil, "udp", username2, "carlos", addr2, 0)
+ if s.Live() != 0 {
+ t.Fatalf("live = %d, want no allocation for a reclaimed socket", s.Live())
+ }
+}
+
// refusalIdentity is the identity a refusal is attributed to: the parsed
// username's, or the zero identity when the username would not parse.
func refusalIdentity(username string) Identity {
@@ -450,7 +611,7 @@ func TestServerPermit(t *testing.T) {
// a replacement allocation on the same typed source can exist while the old
// wrapper is still closing. The old wrapper's close must not erase it.
func TestServerCloseKeepsReplacementBinding(t *testing.T) {
- s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil)
+ s, _, now, _, _ := newTestServer(t, "127.0.0.1", nil, nil)
username, password := creds(now(), "abc", "meet", "0123456789abcdef")
client := turnClient(t, s.UDPAddr(), username, password)
if _, err := client.Allocate(); err != nil {