This commit is contained in:
JackDoan committed 2026-09-30 14:52:14 -05:00
1 parent 1cfed59214
commit 027c728702
3 files changed
+46 -53

No files matched your search

+37 -50
View File
@@ -29,12 +29,8 @@ type RulesBuilder struct {
l *slog.Logger l *slog.Logger
} }
// tableRules are one direction's rules, waiting to be built into a Table // tableRules are one direction's rules in the order they were added, waiting to be built into a Table
type tableRules struct { type tableRules []rule
anyRules []rule
// protoRules holds the rules for each protocol besides proto `any`, ICMPv6 rules are kept with ICMP
protoRules [256][]rule
}
func NewRulesBuilder(l *slog.Logger) *RulesBuilder { func NewRulesBuilder(l *slog.Logger) *RulesBuilder {
return &RulesBuilder{ return &RulesBuilder{
@@ -145,42 +141,31 @@ func parseRule(startPort, endPort int32, groups []string, host, cidr, localCidr,
return r, nil return r, nil
} }
// add keeps r for proto, or for every protocol when proto is ProtoAny // add keeps r for proto, or for every protocol when proto is ProtoAny. ICMPv6 rules are kept as ICMP rules.
func (tr *tableRules) add(proto uint8, r rule) { func (tr *tableRules) add(proto uint8, r rule) {
switch proto { if proto == iputil.IPProtocolICMPv6 {
case ProtoAny: proto = iputil.IPProtocolICMP
tr.anyRules = append(tr.anyRules, r)
case iputil.IPProtocolICMPv6:
tr.protoRules[iputil.IPProtocolICMP] = append(tr.protoRules[iputil.IPProtocolICMP], r)
default:
tr.protoRules[proto] = append(tr.protoRules[proto], r)
} }
r.proto = proto
*tr = append(*tr, r)
} }
// build turns the rules into a Table. Rules without a local cidr allow defaultLocal, // build turns the rules into a Table. Rules without a local cidr allow defaultLocal,
// or any local address when defaultLocal is nil. // or any local address when defaultLocal is nil.
func (tr *tableRules) build(defaultLocal []netip.Prefix) *Table { func (tr tableRules) build(defaultLocal []netip.Prefix) *Table {
t := &Table{} t := &Table{rules: make([]rule, len(tr))}
var hasOwn [256]bool
// A rule's id is its index in t.rules. for id, r := range tr {
add := func(rules []rule) []int { t.rules[id] = r.withDefaultLocal(defaultLocal)
ids := make([]int, 0, len(rules)) if r.proto != ProtoAny {
for _, r := range rules { hasOwn[r.proto] = true
ids = append(ids, len(t.rules))
t.rules = append(t.rules, r.withDefaultLocal(defaultLocal))
} }
return ids
}
anyIDs := add(tr.anyRules)
var protoIDs [256][]int
for proto, rules := range tr.protoRules {
protoIDs[proto] = add(rules)
} }
// Every protocol starts out sharing an index of the proto `any` rules, including protos[ProtoAny]. There is // Every protocol starts out sharing an index of the proto `any` rules, including protos[ProtoAny]. There is
// one for protocols with ports and one for protocols without. // one for protocols with ports and one for protocols without.
anyPorts := newProtoIndex(true, t.rules, anyIDs) anyPorts := newProtoIndex(true, t.rules, ProtoAny)
anyNoPorts := newProtoIndex(false, t.rules, anyIDs) anyNoPorts := newProtoIndex(false, t.rules, ProtoAny)
for proto := range t.protos { for proto := range t.protos {
if hasPorts(uint8(proto)) { if hasPorts(uint8(proto)) {
t.protos[proto] = anyPorts t.protos[proto] = anyPorts
@@ -191,9 +176,9 @@ func (tr *tableRules) build(defaultLocal []netip.Prefix) *Table {
// A protocol with rules of its own gets an index of those rules plus the proto `any` rules, so a packet // A protocol with rules of its own gets an index of those rules plus the proto `any` rules, so a packet
// checks a single index. // checks a single index.
for proto, ids := range protoIDs { for proto, own := range hasOwn {
if len(ids) > 0 { if own {
t.protos[proto] = newProtoIndex(hasPorts(uint8(proto)), t.rules, anyIDs, ids) t.protos[proto] = newProtoIndex(hasPorts(uint8(proto)), t.rules, uint8(proto))
} }
} }
@@ -203,23 +188,25 @@ func (tr *tableRules) build(defaultLocal []netip.Prefix) *Table {
return t return t
} }
// newProtoIndex builds an index of the rules with the given ids for a protocol with or without ports, or // newProtoIndex builds an index of the proto `any` rules and proto's own rules, for a protocol with or without
// returns nil when there are none. // ports, or returns nil when there are none. With ProtoAny, it indexes only the proto `any` rules.
func newProtoIndex(hasPorts bool, rules []rule, idSets ...[]int) *protoIndex { func newProtoIndex(hasPorts bool, rules []rule, proto uint8) *protoIndex {
ids := slices.Concat(idSets...) var pi *protoIndex
if len(ids) == 0 { for id := range rules {
return nil r := &rules[id]
} if r.proto != ProtoAny && r.proto != proto {
continue
}
pi := &protoIndex{hasPorts: hasPorts, fragment: newRuleSet(len(rules))} if pi == nil {
if hasPorts { pi = &protoIndex{hasPorts: hasPorts, fragment: newRuleSet(len(rules))}
pi.byPort = newRuleSets(math.MaxUint16+1, len(rules)) if hasPorts {
} else { pi.byPort = newRuleSets(math.MaxUint16+1, len(rules))
pi.packet = newRuleSet(len(rules)) } else {
} pi.packet = newRuleSet(len(rules))
}
for _, id := range ids { }
pi.add(&rules[id], id) pi.add(r, id)
} }
return pi return pi
} }
+3 -2
View File
@@ -279,11 +279,12 @@ func TestRulesBuilder_manyRules(t *testing.T) {
rules := rb.Build(nil, nil) rules := rb.Build(nil, nil)
assert.Equal(t, ruleSetLen(n+1), rules.In.protos[iputil.IPProtocolTCP].byPort.setLen) 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 { for i := range n {
rs := rules.In.rulesAt(iputil.IPProtocolTCP, int32(i+1)) rs := rules.In.rulesAt(iputil.IPProtocolTCP, int32(i+1))
require.Len(t, rs, 2, "port %d", i+1) require.Len(t, rs, 2, "port %d", i+1)
assert.Equal(t, "any", rs[0].host) assert.Equal(t, fmt.Sprintf("h%d", i), rs[0].host)
assert.Equal(t, fmt.Sprintf("h%d", i), rs[1].host) assert.Equal(t, "any", rs[1].host)
} }
assert.Len(t, rules.In.rulesAt(iputil.IPProtocolTCP, n+1), 1) assert.Len(t, rules.In.rulesAt(iputil.IPProtocolTCP, n+1), 1)
} }
+6 -1
View File
@@ -21,7 +21,8 @@ type Rules struct {
// proto AND port AND (CA SHA or CA name) AND local CIDR AND (group OR groups OR name OR remote CIDR). // proto AND port AND (CA SHA or CA name) AND local CIDR AND (group OR groups OR name OR remote CIDR).
// A Table does not change once built; see RulesBuilder. // A Table does not change once built; see RulesBuilder.
type Table struct { type Table struct {
// rules holds every rule for the direction. A rule's index in this slice is its id in the ruleSets. // rules holds every rule for the direction, in the order they were added. A rule's index in this slice is
// its id in the ruleSets.
rules []rule rules []rule
// protos holds the index for each IP protocol number, so a packet is checked against a single index. // protos holds the index for each IP protocol number, so a packet is checked against a single index.
@@ -73,6 +74,10 @@ func (pi *protoIndex) whichMatch(p *Packet, incoming bool) ruleSet {
// rule is a parsed rule. A packet is allowed by the rule when its port, certificate, and address clauses all pass. // rule is a parsed rule. A packet is allowed by the rule when its port, certificate, and address clauses all pass.
type rule struct { type rule struct {
// proto is the IP protocol number the rule applies to, or ProtoAny for every protocol. ICMPv6 rules are kept
// as ICMP. The protoIndex checks it, so match does not.
proto uint8
// startPort and endPort are the port clause. A startPort of PortAny covers every port, every packet of a // startPort and endPort are the port clause. A startPort of PortAny covers every port, every packet of a
// protocol without ports, and every fragment. PortFragment covers only fragments. A range covers the ports // protocol without ports, and every fragment. PortFragment covers only fragments. A range covers the ports
// in it, so it never applies to a protocol without ports. // in it, so it never applies to a protocol without ports.