| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- package firewall
- import (
- "fmt"
- "math"
- "net/netip"
- "testing"
- "time"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/cert_test"
- "github.com/slackhq/nebula/iputil"
- "github.com/slackhq/nebula/test"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // rulesAt returns the rules that a packet on proto with the given port is checked against, in id order.
- // port may also be PortAny or PortFragment.
- func (t *Table) rulesAt(proto uint8, port int32) []*rule {
- pi := t.protos[proto]
- if pi == nil {
- return nil
- }
- p := &Packet{Protocol: proto, Fragment: port == PortFragment}
- if port > 0 {
- p.LocalPort = uint16(port)
- }
- var rules []*rule
- for id := range pi.whichMatch(p, true).all() {
- rules = append(rules, &t.rules[id])
- }
- return rules
- }
- // oneRuleAt returns the single rule that rulesAt finds, and fails the test when there is not exactly one.
- func oneRuleAt(t *testing.T, table *Table, proto uint8, key int32) *rule {
- t.Helper()
- rules := table.rulesAt(proto, key)
- require.Len(t, rules, 1)
- return rules[0]
- }
- func TestRulesBuilder_AddRule(t *testing.T) {
- l := test.NewLogger()
- ti := netip.MustParsePrefix("1.2.3.4/32")
- ti6 := netip.MustParsePrefix("fd12::34/128")
- rb := NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 1, []string{}, "", "", "", "", ""))
- rules := rb.Build(nil, nil)
- // An empty rule is any
- r := oneRuleAt(t, rules.In, iputil.IPProtocolTCP, 1)
- assert.True(t, r.remoteAny)
- assert.Empty(t, r.groups)
- assert.Empty(t, r.host)
- // It applies only to its own port.
- assert.Empty(t, rules.In.rulesAt(iputil.IPProtocolTCP, 2))
- assert.Empty(t, rules.In.rulesAt(iputil.IPProtocolTCP, PortAny))
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.In, iputil.IPProtocolUDP, 1)
- assert.False(t, r.remoteAny)
- assert.Equal(t, []string{"g1"}, r.groups)
- assert.Empty(t, r.host)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolICMP, 1, 1, []string{}, "h1", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolICMPv6, 1, 1, []string{}, "h2", "", "", "", ""))
- rules = rb.Build(nil, nil)
- // An ICMP rule is port `any` regardless of the port given, and ICMP and ICMPv6 share one set of rules.
- for _, proto := range []uint8{iputil.IPProtocolICMP, iputil.IPProtocolICMPv6} {
- rs := rules.In.rulesAt(proto, PortAny)
- require.Len(t, rs, 2, "proto %d", proto)
- assert.False(t, rs[0].remoteAny)
- assert.Equal(t, "h1", rs[0].host)
- assert.Equal(t, "h2", rs[1].host)
- }
- assert.Same(t, rules.In.protos[iputil.IPProtocolICMP], rules.In.protos[iputil.IPProtocolICMPv6])
- // Any other protocol number gets its own rules
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, 47, 0, 0, []string{"g1"}, "", "", "", "", "")) // GRE
- rules = rb.Build(nil, nil)
- assert.Equal(t, []string{"g1"}, oneRuleAt(t, rules.In, 47, PortAny).groups)
- assert.Nil(t, rules.In.protos[iputil.IPProtocolTCP])
- // A proto `any` rule with a port applies to every protocol with ports, and ProtoAny itself has none.
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", ""))
- rules = rb.Build(nil, nil)
- assert.Empty(t, rules.Out.rulesAt(ProtoAny, 1))
- r = oneRuleAt(t, rules.Out, iputil.IPProtocolTCP, 1)
- assert.False(t, r.remoteAny)
- assert.Equal(t, ti, r.cidr)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 1, 1, []string{}, "", ti6.String(), "", "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, iputil.IPProtocolTCP, 1)
- assert.False(t, r.remoteAny)
- assert.Equal(t, ti6, r.cidr)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 1, 1, []string{}, "", "", ti.String(), "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, iputil.IPProtocolTCP, 1)
- assert.True(t, r.remoteAny)
- assert.False(t, r.localAny)
- assert.Equal(t, []netip.Prefix{ti}, r.local)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 1, 1, []string{}, "", "", ti6.String(), "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, iputil.IPProtocolTCP, 1)
- assert.True(t, r.remoteAny)
- assert.False(t, r.localAny)
- assert.Equal(t, []netip.Prefix{ti6}, r.local)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "ca-name", ""))
- rules = rb.Build(nil, nil)
- assert.Equal(t, "ca-name", oneRuleAt(t, rules.In, iputil.IPProtocolUDP, 1).caName)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "", "ca-sha"))
- rules = rb.Build(nil, nil)
- assert.Equal(t, "ca-sha", oneRuleAt(t, rules.In, iputil.IPProtocolUDP, 1).caSha)
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "any", "", "", "", ""))
- rules = rb.Build(nil, nil)
- assert.True(t, oneRuleAt(t, rules.Out, ProtoAny, 0).remoteAny)
- // 0.0.0.0/0 allows any IPv4 address, but no IPv6 address.
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "", "0.0.0.0/0", "", "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, ProtoAny, 0)
- assert.False(t, r.remoteAny)
- assert.True(t, r.matchRemote(netip.MustParseAddr("1.1.1.1"), nil))
- assert.False(t, r.matchRemote(netip.MustParseAddr("9::9"), nil))
- // ::/0 allows any IPv6 address, but no IPv4 address.
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "", "::/0", "", "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, ProtoAny, 0)
- assert.False(t, r.remoteAny)
- assert.True(t, r.matchRemote(netip.MustParseAddr("9::9"), nil))
- assert.False(t, r.matchRemote(netip.MustParseAddr("1.1.1.1"), nil))
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "", "any", "", "", ""))
- rules = rb.Build(nil, nil)
- assert.True(t, oneRuleAt(t, rules.Out, ProtoAny, 0).remoteAny)
- // A local cidr is likewise limited to its own address family.
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "", "", "0.0.0.0/0", "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, ProtoAny, 0)
- assert.False(t, r.localAny)
- assert.True(t, r.matchLocal(netip.MustParseAddr("1.1.1.1")))
- assert.False(t, r.matchLocal(netip.MustParseAddr("9::9")))
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "", "", "::/0", "", ""))
- rules = rb.Build(nil, nil)
- r = oneRuleAt(t, rules.Out, ProtoAny, 0)
- assert.False(t, r.localAny)
- assert.True(t, r.matchLocal(netip.MustParseAddr("9::9")))
- assert.False(t, r.matchLocal(netip.MustParseAddr("1.1.1.1")))
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(false, ProtoAny, 0, 0, []string{}, "", "", "any", "", ""))
- rules = rb.Build(nil, nil)
- assert.True(t, oneRuleAt(t, rules.Out, ProtoAny, 0).localAny)
- // Every protocol number is accepted
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, math.MaxUint8, 0, 0, []string{}, "", "", "", "", ""))
- rules = rb.Build(nil, nil)
- assert.True(t, oneRuleAt(t, rules.In, math.MaxUint8, PortAny).remoteAny)
- // Test error conditions, a bad rule is reported when it's added
- rb = NewRulesBuilder(l)
- require.Error(t, rb.AddRule(true, ProtoAny, 10, 0, []string{}, "", "", "", "", ""))
- require.Error(t, rb.AddRule(true, ProtoAny, PortFragment-1, 0, []string{}, "", "", "", "", ""))
- require.Error(t, rb.AddRule(true, ProtoAny, 1, math.MaxUint16+1, []string{}, "", "", "", "", ""))
- require.Error(t, rb.AddRule(true, ProtoAny, 0, 0, []string{}, "", "junk", "", "", ""))
- require.Error(t, rb.AddRule(true, ProtoAny, 0, 0, []string{}, "", "", "junk", "", ""))
- // ICMP and ICMPv6 share one copy of each proto `any` rule
- rb = NewRulesBuilder(l)
- require.NoError(t, rb.AddRule(true, ProtoAny, PortAny, PortAny, []string{"default-group"}, "", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolICMPv6, PortAny, PortAny, []string{"nope"}, "", "", "", "", ""))
- rules = rb.Build(nil, nil)
- assert.Len(t, rules.In.rules, 2)
- assert.Len(t, rules.In.rulesAt(iputil.IPProtocolICMP, PortAny), 2)
- assert.Len(t, rules.In.rulesAt(iputil.IPProtocolICMPv6, PortAny), 2)
- // No rules allow nothing
- rules = NewRulesBuilder(l).Build(nil, nil)
- for proto := range rules.In.protos {
- assert.Nil(t, rules.In.protos[proto], "proto %d", proto)
- assert.Nil(t, rules.Out.protos[proto], "proto %d", proto)
- }
- }
- // TestRulesBuilder_protoIndex checks which packets each kind of port clause covers, and which protocols
- // share an index.
- func TestRulesBuilder_protoIndex(t *testing.T) {
- rb := NewRulesBuilder(test.NewLogger())
- require.NoError(t, rb.AddRule(true, ProtoAny, PortAny, PortAny, nil, "any", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 22, 22, nil, "port", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1024, math.MaxUint16, nil, "range", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, PortFragment, PortFragment, nil, "fragment", "", "", "", ""))
- // A range that includes port 0 is port `any`.
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 0, 100, nil, "zero", "", "", "", ""))
- // GRE has no ports, so a port rule on it is rejected rather than never applying, and `any` and `fragment` apply
- require.Error(t, rb.AddRule(true, 47, 22, 22, nil, "gre", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, 47, PortFragment, PortFragment, nil, "gre", "", "", "", ""))
- rules := rb.Build(nil, nil)
- hosts := func(proto uint8, key int32) []string {
- var hosts []string
- for _, r := range rules.In.rulesAt(proto, key) {
- hosts = append(hosts, r.host)
- }
- return hosts
- }
- assert.Equal(t, []string{"any", "fragment"}, hosts(iputil.IPProtocolTCP, PortFragment))
- assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolTCP, PortAny))
- assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolTCP, 21))
- assert.Equal(t, []string{"any", "port"}, hosts(iputil.IPProtocolTCP, 22))
- assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolTCP, 23))
- assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolTCP, 1023))
- assert.Equal(t, []string{"any", "range"}, hosts(iputil.IPProtocolTCP, 1024))
- assert.Equal(t, []string{"any", "range"}, hosts(iputil.IPProtocolTCP, math.MaxUint16))
- assert.Equal(t, []string{"any", "zero"}, hosts(iputil.IPProtocolUDP, PortFragment))
- assert.Equal(t, []string{"any", "zero"}, hosts(iputil.IPProtocolUDP, PortAny))
- assert.Equal(t, []string{"any", "zero"}, hosts(iputil.IPProtocolUDP, math.MaxUint16))
- // A protocol without ports sees only the port `any` rules, and the port `fragment` rules for a fragment.
- assert.False(t, rules.In.protos[47].hasPorts)
- assert.Equal(t, []string{"any"}, hosts(47, PortAny))
- assert.Equal(t, []string{"any"}, hosts(47, 22))
- assert.Equal(t, []string{"any", "gre"}, hosts(47, PortFragment))
- assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolICMP, PortFragment))
- // Protocols without rules of their own share an index of the proto `any` rules: one for protocols with
- // ports and one for protocols without.
- assert.True(t, rules.In.protos[iputil.IPProtocolSCTP].hasPorts)
- assert.Equal(t, []string{"any"}, hosts(iputil.IPProtocolSCTP, 5000))
- assert.Same(t, rules.In.protos[iputil.IPProtocolSCTP], rules.In.protos[iputil.IPProtocolDCCP])
- assert.NotSame(t, rules.In.protos[iputil.IPProtocolSCTP], rules.In.protos[iputil.IPProtocolTCP])
- assert.Equal(t, []string{"any"}, hosts(50, PortAny))
- assert.Same(t, rules.In.protos[ProtoAny], rules.In.protos[50])
- assert.Same(t, rules.In.protos[ProtoAny], rules.In.protos[iputil.IPProtocolICMP])
- assert.NotSame(t, rules.In.protos[ProtoAny], rules.In.protos[iputil.IPProtocolSCTP])
- assert.Nil(t, rules.Out.protos[iputil.IPProtocolTCP])
- }
- // TestRulesBuilder_manyRules checks that rule ids beyond the first byte of a rule set are indexed correctly.
- func TestRulesBuilder_manyRules(t *testing.T) {
- rb := NewRulesBuilder(test.NewLogger())
- const n = 130
- for i := range n {
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, int32(i+1), int32(i+1), nil, fmt.Sprintf("h%d", i), "", "", "", ""))
- }
- require.NoError(t, rb.AddRule(true, ProtoAny, PortAny, PortAny, nil, "any", "", "", "", ""))
- rules := rb.Build(nil, nil)
- assert.Equal(t, ruleSetLen(n+1), rules.In.protos[iputil.IPProtocolTCP].byPort.setLen)
- // Ids are the order the rules were added in, so the proto `any` rule comes last
- for i := range n {
- rs := rules.In.rulesAt(iputil.IPProtocolTCP, int32(i+1))
- require.Len(t, rs, 2, "port %d", i+1)
- assert.Equal(t, fmt.Sprintf("h%d", i), rs[0].host)
- assert.Equal(t, "any", rs[1].host)
- }
- assert.Len(t, rules.In.rulesAt(iputil.IPProtocolTCP, n+1), 1)
- }
- // TestRulesBuilder_defaultLocalCIDR checks which local addresses rules allow when there are unsafe networks.
- // A rule without a local cidr allows only the vpn networks, and a rule with one allows only that cidr.
- func TestRulesBuilder_defaultLocalCIDR(t *testing.T) {
- vpnNetworks := []netip.Prefix{
- netip.MustParsePrefix("10.1.0.5/16"),
- netip.MustParsePrefix("10.2.0.5/16"),
- netip.MustParsePrefix("fd00:1::5/64"),
- }
- unsafeNetworks := []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")}
- vpn1 := netip.MustParseAddr("10.1.9.9")
- vpn2 := netip.MustParseAddr("10.2.9.9")
- vpn6 := netip.MustParseAddr("fd00:1::9")
- h1Extra := netip.MustParseAddr("10.3.0.1")
- h1Extra6 := netip.MustParseAddr("fd00:2::1")
- h2Own := netip.MustParseAddr("10.4.0.1")
- rb := NewRulesBuilder(test.NewLogger())
- // h1 allows the vpn networks by default on ports 1 and 2, and 10.3.0.0/16 and fd00:2::/64 as well on port 1
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 2, nil, "h1", "", "", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 1, nil, "h1", "", "10.3.0.0/16", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 1, nil, "h1", "", "fd00:2::/64", "", ""))
- // h2 allows 10.4.0.0/16 on ports 1 and 2, and the vpn networks by default as well on port 1
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 2, nil, "h2", "", "10.4.0.0/16", "", ""))
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 1, nil, "h2", "", "", "", ""))
- // h3 only has the default
- require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 3, 3, nil, "h3", "", "", "", ""))
- rules := rb.Build(vpnNetworks, unsafeNetworks)
- ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
- caPool := cert.NewCAPool()
- require.NoError(t, caPool.AddCA(ca))
- certs := map[string]*cert.CachedCertificate{}
- for _, name := range []string{"h1", "h2", "h3"} {
- crt, _, _, _ := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, name, time.Time{}, time.Time{}, []netip.Prefix{netip.MustParsePrefix("10.1.0.9/16")}, nil, nil)
- c, err := caPool.VerifyCertificate(time.Now(), crt)
- require.NoError(t, err)
- certs[name] = c
- }
- for _, tc := range []struct {
- port uint16
- host string
- addr netip.Addr
- want bool
- }{
- {1, "h1", vpn1, true},
- {1, "h1", vpn2, true},
- {1, "h1", vpn6, true},
- {1, "h1", h1Extra, true},
- {1, "h1", h1Extra6, true},
- {1, "h1", h2Own, false},
- {2, "h1", vpn1, true},
- {2, "h1", h1Extra, false},
- {2, "h1", h1Extra6, false},
- {1, "h2", h2Own, true},
- {1, "h2", vpn1, true},
- {1, "h2", vpn2, true},
- {1, "h2", vpn6, true},
- {1, "h2", h1Extra, false},
- {2, "h2", h2Own, true},
- {2, "h2", vpn1, false},
- {2, "h2", vpn6, false},
- {3, "h3", vpn1, true},
- {3, "h3", vpn2, true},
- {3, "h3", vpn6, true},
- {3, "h3", h1Extra, false},
- {3, "h3", h1Extra6, false},
- {3, "h3", h2Own, false},
- // Only h3 is allowed on port 3.
- {3, "h1", vpn1, false},
- } {
- p := &Packet{Protocol: iputil.IPProtocolTCP, LocalPort: tc.port, LocalAddr: tc.addr, RemoteAddr: netip.MustParseAddr("10.1.0.9")}
- assert.Equal(t, tc.want, rules.In.Match(p, true, certs[tc.host], caPool), "port %d, %s, %s", tc.port, tc.host, tc.addr)
- }
- }
|