diff --git a/.changeset/soft-webs-rush.md b/.changeset/soft-webs-rush.md new file mode 100644 index 000000000..efc08c840 --- /dev/null +++ b/.changeset/soft-webs-rush.md @@ -0,0 +1,6 @@ +--- +"github.com/livekit/protocol": patch +"@livekit/protocol": patch +--- + +SIP authentication: compare IP masks against remote address rather than From header. diff --git a/sip/sip.go b/sip/sip.go index f74653b4b..48f7a9522 100644 --- a/sip/sip.go +++ b/sip/sip.go @@ -428,19 +428,23 @@ func filterInvalidAddrMasks(masks []string) []string { return out } -func matchAddrMask(ip netip.Addr, mask string) bool { - if !strings.Contains(mask, "/") { - expIP, err := netip.ParseAddr(mask) +func parseIPMask(mask string) (netip.Prefix, bool) { + addr, bits, hasBits := strings.Cut(mask, "/") + if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") { + addr = addr[1 : len(addr)-1] + } + if !hasBits { + ip, err := netip.ParseAddr(addr) if err != nil { - return false + return netip.Prefix{}, false } - return ip == expIP + return netip.PrefixFrom(ip, ip.BitLen()), true } - pref, err := netip.ParsePrefix(mask) + pref, err := netip.ParsePrefix(fmt.Sprintf("%s/%s", addr, bits)) if err != nil { - return false + return netip.Prefix{}, false } - return pref.Contains(ip) + return pref, true } func matchAddrMasks(addr string, host string, masks []string) bool { @@ -453,7 +457,12 @@ func matchAddrMasks(addr string, host string, masks []string) bool { return true } for _, mask := range masks { - if mask == host || matchAddrMask(ip, mask) { + if pref, ok := parseIPMask(mask); ok { + // IP masks must only match the source IP, never the (spoofable) From host. + if pref.Contains(ip) { + return true + } + } else if mask == host { return true } } diff --git a/sip/sip_test.go b/sip/sip_test.go index 57e3a8691..cef6ad69a 100644 --- a/sip/sip_test.go +++ b/sip/sip_test.go @@ -1163,27 +1163,37 @@ func TestEvaluateDispatchRule_RespectsRuleMediaTimeout(t *testing.T) { require.Equal(t, 5*time.Minute, res.Media.MediaTimeout.AsDuration()) } -func TestMatchIP(t *testing.T) { +func TestParseIPMask(t *testing.T) { cases := []struct { - addr string - mask string - valid bool - exp bool + mask string + exp string // empty if not an IP mask }{ - {addr: "192.168.0.10", mask: "192.168.0.10", valid: true, exp: true}, - {addr: "192.168.0.10", mask: "192.168.0.11", valid: true, exp: false}, - {addr: "192.168.0.10", mask: "192.168.0.0/24", valid: true, exp: true}, - {addr: "192.168.0.10", mask: "192.168.0.10/0", valid: true, exp: true}, - {addr: "192.168.0.10", mask: "192.170.0.0/24", valid: true, exp: false}, + {mask: "192.168.0.10", exp: "192.168.0.10/32"}, + {mask: "192.168.0.0/24", exp: "192.168.0.0/24"}, + {mask: "2001:db8::1", exp: "2001:db8::1/128"}, + {mask: "[2001:db8::1]", exp: "2001:db8::1/128"}, + {mask: "[2001:db8::]/32", exp: "2001:db8::/32"}, + {mask: "[::]", exp: "::/128"}, + {mask: "[::]/0", exp: "::/0"}, + + // Not IP masks: treated as hostnames. + {mask: "sip.example.com"}, + {mask: "sip.example.com/24"}, + {mask: "192.168.0.10/33"}, + {mask: "192.168.0.10/"}, + {mask: "[192.168.0.10"}, + {mask: "[2001:db8::1]:5060"}, + {mask: "[[2001:db8::1]]"}, } for _, c := range cases { t.Run(c.mask, func(t *testing.T) { - ip, err := netip.ParseAddr(c.addr) - require.NoError(t, err) - got := isValidMask(c.mask) - require.Equal(t, c.valid, got) - got = matchAddrMask(ip, c.mask) - require.Equal(t, c.exp, got) + pref, ok := parseIPMask(c.mask) + if c.exp == "" { + require.False(t, ok) + return + } + require.True(t, ok) + require.Equal(t, netip.MustParsePrefix(c.exp), pref) }) } } @@ -1281,6 +1291,59 @@ func TestMatchMasks(t *testing.T) { }, exp: true, }, + { + name: "ip spoofed in from host", + addr: "203.0.113.5", + host: "192.168.0.10", + masks: []string{ + "192.168.0.10", + }, + exp: false, + }, + { + name: "ip range spoofed in from host", + addr: "203.0.113.5", + host: "192.168.0.0/24", + masks: []string{ + "192.168.0.0/24", + }, + exp: false, + }, + { + name: "ipv6 spoofed in from host", + addr: "203.0.113.5", + host: "[2001:db8::1]", + masks: []string{ + "[2001:db8::1]", + }, + exp: false, + }, + { + name: "ipv6 bracketed", + addr: "2001:db8::1", + host: "sip.example.com", + masks: []string{ + "[2001:db8::1]", + }, + exp: true, + }, + { + name: "ipv6 bracketed range", + addr: "2001:db8::1", + masks: []string{ + "[2001:db8::]/32", + }, + exp: true, + }, + { + name: "ip matches source, from host differs", + addr: "192.168.0.10", + host: "sip.example.com", + masks: []string{ + "192.168.0.10", + }, + exp: true, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) {