diff --git a/apf_linux.go b/apf_linux.go index 352547c..14df16d 100644 --- a/apf_linux.go +++ b/apf_linux.go @@ -117,14 +117,11 @@ func NewAPF(ctx context.Context, rulePrefix string) (*APF, error) { return apf, nil } -// Type returns the backend identifier for apf. // Type returns the backend identifier for apf. func (f *APF) Type() string { return APFType } -// hook returns the managed pre-hook script used to inject iptables rules for -// features apf's native config cannot express. // Capabilities reports the firewall features apf supports. func (f *APF) Capabilities() Capabilities { return Capabilities{ @@ -152,14 +149,11 @@ func (f *APF) Capabilities() Capabilities { } } -// GetDefaultPolicy is unsupported: apf has no managed default-policy control. // GetZone returns no zone; apf has no zone support. func (f *APF) GetZone(ctx context.Context, iface string) (zoneName string, err error) { return "", nil } -// parsePortToken parses a single apf port token: a port or an underscore -// range (e.g. "6000_7000"). // parsePortToken parses a single apf port token: a port or an underscore // range (e.g. "6000_7000"). func (f *APF) parsePortToken(tok string) (PortRange, error) { @@ -182,8 +176,6 @@ func (f *APF) parsePortToken(tok string) (PortRange, error) { return pr, nil } -// portToken renders a port spec in apf notation: a port or an underscore -// range. // ParseConnLimit decodes a conf.apf IG_TCP_CLIMIT/IG_UDP_CLIMIT value // ("port:limit,...", port may be an underscore range) into connection-limit // rules: apf caps concurrent connections per source and rejects the excess, so @@ -222,8 +214,6 @@ func (f *APF) ParseConnLimit(val string, proto Protocol) (rules []*Rule) { return } -// editConnLimit renders a CLIMIT config line with a rule's "port:limit" entry -// added or removed, preserving the other entries, and records a config change. // ParseICMPTypes decodes an apf ICMP type list (IG_ICMP_TYPES/EG_ICMP_TYPES for // proto ICMP, IG_ICMPV6_TYPES/EG_ICMPV6_TYPES for proto ICMPv6) into accept rules, // one per type. The "all" wildcard (which apf applies as a typeless `-p icmp -j @@ -251,8 +241,6 @@ func (f *APF) ParseICMPTypes(val string, proto Protocol, out bool) (rules []*Rul return } -// splitAdvFields splits an apf advanced rule on ':' while leaving colons inside -// bracketed IPv6 addresses (e.g. [2001:db8::1]) intact. // parseAddr parses an apf address value, stripping the bracket notation used // to protect IPv6 addresses, and normalizing a zero-network to the empty (any) // address. It reports the family, or false when the value is not an address. @@ -279,8 +267,6 @@ func (f *APF) parseAddr(v string) (addr string, fam Family, ok bool) { return v, family, true } -// addrField renders an address for an advanced rule, wrapping an IPv6 address -// in brackets so it survives the colon-separated field format. // parseStopAction maps a conf.apf ALL_STOP/TCP_STOP/UDP_STOP value to the // action apf actually applies. "DROP", "REJECT" and "PROHIBIT" are valid; // anything else (including empty) falls back to the stock default of DROP. @@ -297,15 +283,6 @@ func (f *APF) parseStopAction(val string) Action { } } -// stopKey names the conf.apf setting apf actually applies to a deny of the -// given protocol. A bare-address deny_hosts entry (no protocol/port) is dropped -// by trust_hosts's own bare-host branch, which applies ALL_STOP directly. An -// advanced entry (proto:flow:s/d=port:s/d=ip) is instead routed through -// trust_entry_rule, which ignores ALL_STOP entirely and applies TCP_STOP for a -// tcp entry or UDP_STOP for a udp one (files/internals/apf_trust.sh). The three -// settings default to DROP and are otherwise fully independent, so an entry read -// or matched under the wrong key can report or accept the wrong action whenever -// an operator sets them differently. // readStopAction reads the named STOP setting (ALL_STOP/TCP_STOP/UDP_STOP) // from a conf.apf-format file, defaulting to the stock DROP when the key is // absent or the file cannot be read. path is a parameter (rather than always @@ -336,11 +313,6 @@ func (f *APF) readStopAction(path, key string) Action { return action } -// denyActionFor reads conf.apf's setting for the given protocol (see -// stopKey) and returns the action apf actually applies to a deny of that -// protocol. deny_hosts encodes no action of its own, so a rule read back must -// be stamped with what apf actually applies — otherwise a Drop rule reads back -// as Reject, never compares equal to the desired rule, and churns on every Sync. // stopKey names the conf.apf setting apf actually applies to a deny of the // given protocol. A bare-address deny_hosts entry (no protocol/port) is dropped // by trust_hosts's own bare-host branch, which applies ALL_STOP directly. An @@ -361,26 +333,11 @@ func (f *APF) stopKey(proto Protocol) string { } } -// readStopAction reads the named STOP setting (ALL_STOP/TCP_STOP/UDP_STOP) -// from a conf.apf-format file, defaulting to the stock DROP when the key is -// absent or the file cannot be read. path is a parameter (rather than always -// APFConf) so this can be exercised against a fixture file in tests. // denyActionFor reads conf.apf's setting for the given protocol (see -// stopKey) and returns the action apf actually applies to a deny of that -// protocol. deny_hosts encodes no action of its own, so a rule read back must -// be stamped with what apf actually applies — otherwise a Drop rule reads back -// as Reject, never compares equal to the desired rule, and churns on every Sync. func (f *APF) denyActionFor(proto Protocol) Action { return f.readStopAction(APFConf, f.stopKey(proto)) } -// resolveAction resolves the action to stamp on a rule parsed from — or -// matched against — allow_hosts/deny_hosts. base is Accept for allow_hosts -// (returned unchanged, since parseStopAction never yields Accept and -// allow_hosts has no per-protocol distinction) or the ALL_STOP-derived action -// for deny_hosts. A deny_hosts entry in apf's advanced syntax with a concrete -// tcp or udp protocol is not governed by ALL_STOP (see stopKey), so its -// action is re-derived from the matching TCP_STOP/UDP_STOP setting instead. // resolveAction resolves the action to stamp on a rule parsed from — or // matched against — allow_hosts/deny_hosts. base is Accept for allow_hosts // (returned unchanged, since parseStopAction never yields Accept and @@ -400,8 +357,6 @@ func (f *APF) resolveAction(base Action, proto Protocol) Action { } } -// ParseAdvRule decodes an apf advanced allow/deny rule of the form -// proto:flow:s/d=port:s/d=ip. IPv6 addresses use bracket notation. // splitAdvFields splits an apf advanced rule on ':' while leaving colons inside // bracketed IPv6 addresses (e.g. [2001:db8::1]) intact. func (f *APF) splitAdvFields(s string) []string { @@ -425,9 +380,6 @@ func (f *APF) splitAdvFields(s string) []string { return append(fields, s[start:]) } -// parseAddr parses an apf address value, stripping the bracket notation used -// to protect IPv6 addresses, and normalizing a zero-network to the empty (any) -// address. It reports the family, or false when the value is not an address. // ParseAdvRule decodes an apf advanced allow/deny rule of the form // proto:flow:s/d=port:s/d=ip. IPv6 addresses use bracket notation. func (f *APF) ParseAdvRule(val string, action Action) (r *Rule) { @@ -483,8 +435,6 @@ func (f *APF) ParseAdvRule(val string, action Action) (r *Rule) { return } -// MarshalAdvRule encodes a rule as an apf advanced allow/deny line. An apf -// advanced rule is tcp/udp only and must carry a source or destination address. // ParseIPList reads an apf allow_hosts/deny_hosts file and returns the rules it holds. func (f *APF) ParseIPList(filePath string, action Action) (rules []*Rule, err error) { // Read the allow_hosts/deny_hosts rule list. @@ -574,8 +524,6 @@ func (f *APF) ParseIPList(filePath string, action Action) (rules []*Rule, err er return } -// cPortsKey returns the conf.apf CPORTS list a tcp/udp port rule of the given -// direction lives in, or "" for a protocol with no such list. // ParsePorts decodes an apf comma-separated port list into accept rules, one per port entry. func (f *APF) ParsePorts(val string, proto Protocol, out bool) (rules []*Rule) { for _, port := range strings.Split(val, ",") { @@ -604,10 +552,6 @@ func (f *APF) ParsePorts(val string, proto Protocol, out bool) (rules []*Rule) { return } -// ParseICMPTypes decodes an apf ICMP type list (IG_ICMP_TYPES/EG_ICMP_TYPES for -// proto ICMP, IG_ICMPV6_TYPES/EG_ICMPV6_TYPES for proto ICMPv6) into accept rules, -// one per type. The "all" wildcard (which apf applies as a typeless `-p icmp -j -// ACCEPT`) becomes a rule with a nil ICMPType, matching every type. // hook returns the managed pre-hook script used to inject iptables rules for // features apf's native config cannot express. func (f *APF) hook() *hookScript { @@ -618,7 +562,6 @@ func (f *APF) hook() *hookScript { } } -// GetZone returns no zone; apf has no zone support. // GetRules returns the current filter rules read from conf.apf, the allow/deny lists, and the pre-hook. func (f *APF) GetRules(ctx context.Context, zoneName string) (rules []*Rule, err error) { // Read rules from conf.apf @@ -727,11 +670,6 @@ func (f *APF) GetRules(ctx context.Context, zoneName string) (rules []*Rule, err return } -// removeBareHostOneWay removes a one-way bare-address host rule. Such a rule is -// stored either as its own hook rule or as one direction of a bidirectional plain -// allow_hosts/deny_hosts line (a DirAny rule). When a matching plain line exists, -// split it: drop the line and re-add the surviving opposite direction as a hook rule -// so the untargeted direction keeps its coverage. // portToken renders a port spec in apf notation: a port or an underscore // range. func (f *APF) portToken(pr PortRange) string { @@ -742,7 +680,6 @@ func (f *APF) portToken(pr PortRange) string { return fmt.Sprintf("%d_%d", pr.Start, pr.End) } -// ParsePorts decodes an apf comma-separated port list into accept rules, one per port entry. // editConnLimit renders a CLIMIT config line with a rule's "port:limit" entry // added or removed, preserving the other entries, and records a config change. func (f *APF) editConnLimit(key, val string, r *Rule, remove bool) string { @@ -780,9 +717,6 @@ func (f *APF) editConnLimit(key, val string, r *Rule, remove bool) string { return fmt.Sprintf(`%s="%s"`, key, strings.Join(kept, ",")) } -// icmpTokens returns the conf.apf type token(s) an icmp/icmpv6 accept rule -// contributes to its type list: the numeric type, or "all" when the rule matches -// every type (a nil ICMPType, which apf applies as a typeless `-p icmp -j ACCEPT`). // icmpTokens returns the conf.apf type token(s) an icmp/icmpv6 accept rule // contributes to its type list: the numeric type, or "all" when the rule matches // every type (a nil ICMPType, which apf applies as a typeless `-p icmp -j ACCEPT`). @@ -793,7 +727,6 @@ func (f *APF) icmpTokens(r *Rule) []string { return []string{strconv.Itoa(int(*r.ICMPType))} } -// portTokens renders the rule's ports as apf config tokens. // isConnLimitRule reports whether a rule maps onto conf.apf's // IG_TCP_CLIMIT/IG_UDP_CLIMIT: a per-source cap on concurrent inbound // connections to a single tcp/udp port (or range) with no address, rejecting the @@ -805,10 +738,6 @@ func (f *APF) isConnLimitRule(r *Rule) bool { len(r.PortSpecs()) == 1 && r.Action == Reject } -// ParseConnLimit decodes a conf.apf IG_TCP_CLIMIT/IG_UDP_CLIMIT value -// ("port:limit,...", port may be an underscore range) into connection-limit -// rules: apf caps concurrent connections per source and rejects the excess, so -// each entry becomes an inbound reject rule carrying a per-source ConnLimit. // portTokens renders the rule's ports as apf config tokens. func (f *APF) portTokens(r *Rule) []string { specs := r.PortSpecs() @@ -819,7 +748,6 @@ func (f *APF) portTokens(r *Rule) []string { return tokens } -// EditRulePort returns the conf.apf line with the rule's tokens added to or removed from the list that key manages. // EditRulePort returns the conf.apf line with the rule's tokens added to or removed from the list that key manages. func (f *APF) EditRulePort(orig, key, val string, r *Rule, remove bool) string { // A connection-limit rule is expressed solely through the CLIMIT config; it @@ -966,7 +894,6 @@ func (f *APF) EditRulePort(orig, key, val string, r *Rule, remove bool) string { return fmt.Sprintf(`%s="%s"`, key, strings.Join(kept, ",")) } -// EditConf adds or removes a rule in conf.apf, rewriting the file in place. // EditConf adds or removes a rule in conf.apf, rewriting the file in place. func (f *APF) EditConf(ctx context.Context, r *Rule, remove bool) error { // For port only rules, open the standard config file. @@ -1030,13 +957,6 @@ func (f *APF) EditConf(ctx context.Context, r *Rule, remove bool) error { return af.Commit() } -// nativeICMPv6 reports whether an ICMPv6 rule can be carried by apf's native -// IG_ICMPV6_TYPES/EG_ICMPV6_TYPES lists (an address-less accept, optionally typed) -// and so belongs in conf.apf rather than the raw-iptables hook. The shared -// ruleNeedsHook diverts every ICMPv6 rule to the hook — correct for csf, which has -// no native v6 type list — so apf overrides that only for the rules its config can -// actually express, leaving an ICMPv6 rule that also needs state/interface/log/ -// rate matching (which conf.apf cannot carry) on the hook path. // addrField renders an address for an advanced rule, wrapping an IPv6 address // in brackets so it survives the colon-separated field format. func (f *APF) addrField(addr string) string { @@ -1046,13 +966,6 @@ func (f *APF) addrField(addr string) string { return addr } -// parseStopAction maps a conf.apf ALL_STOP/TCP_STOP/UDP_STOP value to the -// action apf actually applies. "DROP", "REJECT" and "PROHIBIT" are valid; -// anything else (including empty) falls back to the stock default of DROP. -// PROHIBIT jumps to apf's own PROHIBIT chain, which rejects with an ICMP -// (un)reachable-prohibited response — the same reject-like semantics as -// REJECT, just a different ICMP code — so it maps to Reject rather than Drop; -// this model has no third action to give it. // MarshalAdvRule encodes a rule as an apf advanced allow/deny line. An apf // advanced rule is tcp/udp only and must carry a source or destination address. func (f *APF) MarshalAdvRule(r *Rule) (string, error) { @@ -1105,7 +1018,6 @@ func (f *APF) MarshalAdvRule(r *Rule) (string, error) { return strings.Join(parts, ":"), nil } -// ParseIPList reads an apf allow_hosts/deny_hosts file and returns the rules it holds. // EditIPList adds or removes a rule in an apf allow_hosts/deny_hosts file, rewriting it in place. func (f *APF) EditIPList(ctx context.Context, filePath string, action Action, r *Rule, remove bool) error { // Read the allow_hosts/deny_hosts rule list. @@ -1335,8 +1247,6 @@ func (f *APF) EditIPList(ctx context.Context, filePath string, action Action, r return af.Commit() } -// removePlainHost drops the bidirectional plain allow_hosts/deny_hosts line backing -// the DirAny rule e, choosing the list by the rule's action. // isConfRule reports whether a rule is managed in conf.apf: an address-less // accept rule of ports (TCP/UDP lists) or ICMP/ICMPv6 types (a nil type is the // "all" wildcard). @@ -1347,10 +1257,6 @@ func (f *APF) isConfRule(r *Rule) bool { return r.HasPorts() || r.Proto == ICMP || r.Proto == ICMPv6 } -// isConnLimitRule reports whether a rule maps onto conf.apf's -// IG_TCP_CLIMIT/IG_UDP_CLIMIT: a per-source cap on concurrent inbound -// connections to a single tcp/udp port (or range) with no address, rejecting the -// excess. // nativeICMPv6 reports whether an ICMPv6 rule can be carried by apf's native // IG_ICMPV6_TYPES/EG_ICMPV6_TYPES lists (an address-less accept, optionally typed) // and so belongs in conf.apf rather than the raw-iptables hook. The shared @@ -1363,12 +1269,6 @@ func (f *APF) nativeICMPv6(r *Rule) bool { !r.Log && r.RateLimit == nil && f.isConfRule(r) } -// ipv6Unavailable reports whether adding r would silently write something -// apf itself never enforces: a bare IPv6 host in allow_hosts.rules/ -// deny_hosts.rules, or a native ICMPv6 type, are both no-op'd by apf's own -// shell logic when conf.apf's USE_IPV6 is not "1". A rule diverted to the -// raw-iptables hook is unaffected — the hook runs ip6tables directly, outside -// apf's USE_IPV6-gated logic — so this only applies to the two native paths. // ipv6Unavailable reports whether adding r would silently write something // apf itself never enforces: a bare IPv6 host in allow_hosts.rules/ // deny_hosts.rules, or a native ICMPv6 type, are both no-op'd by apf's own @@ -1385,12 +1285,6 @@ func (f *APF) ipv6Unavailable(r *Rule) bool { return r.impliedFamily() == IPv6 && (r.Source != "" || r.Destination != "") } -// barePortAccept reports whether a rule is an address-less tcp/udp port accept — -// the shape apf stores either in a dual-stack conf.apf CPORTS list (a FamilyAny -// port) or, per family, through the raw-iptables hook (a single-family port, or a -// FamilyAny that GetRules merged back from a v4+v6 hook pair). Both the add-time hook -// decision (dualStackPortNeedsHook) and the remove-time split/clear -// (removeDualStackPort) build on it. // barePortAccept reports whether a rule is an address-less tcp/udp port accept — // the shape apf stores either in a dual-stack conf.apf CPORTS list (a FamilyAny // port) or, per family, through the raw-iptables hook (a single-family port, or a @@ -1402,14 +1296,6 @@ func (f *APF) barePortAccept(r *Rule) bool { r.Source == "" && r.Destination == "" && r.Action == Accept } -// dualStackPortNeedsHook reports whether a bare tcp/udp port accept pinned to a -// single family must be injected through the hook. apf's IG_*_CPORTS/EG_*_CPORTS -// lists are dual-stack — one list applied to both the ip and ip6 tables — so they -// express only a FamilyAny port (unlike csf, whose TCP_IN/TCP6_IN split the -// families). A single-family one is written per-family through the hook instead, -// whose iptables (or ip6tables) rule carries just that family; removing one family -// of a FamilyAny CPORTS entry splits it (see removeDualStackPort). ICMP keeps its -// concrete family (its type lists are per-family), so this gates on a port match. // dualStackPortNeedsHook reports whether a bare tcp/udp port accept pinned to a // single family must be injected through the hook. apf's IG_*_CPORTS/EG_*_CPORTS // lists are dual-stack — one list applied to both the ip and ip6 tables — so they @@ -1422,15 +1308,6 @@ func (f *APF) dualStackPortNeedsHook(r *Rule) bool { return f.barePortAccept(r) && r.impliedFamily() != FamilyAny } -// needsHook reports whether a rule must be injected through the apf pre-hook as a -// raw iptables rule because apf's native config cannot express it. It is the single -// gate between the hook path and apf's config files: everything it rejects (returns -// true) is written to the hook, everything it accepts (returns false) maps onto -// conf.apf or the allow_hosts/deny_hosts trust files. The shared shapes -// (ruleNeedsHook, bareHostOneWay, hostNeedsHook) and the two apf shapes RemoveRule -// reuses to route a split (dualStackPortNeedsHook, nativeICMPv6) keep their own -// predicates; the apf-only, single-use port/source-port/connlimit/icmp tests are -// inlined here as their sole caller. // needsHook reports whether a rule must be injected through the apf pre-hook as a // raw iptables rule because apf's native config cannot express it. It is the single // gate between the hook path and apf's config files: everything it rejects (returns @@ -1496,7 +1373,6 @@ func (f *APF) needsHook(r *Rule) bool { return f.dualStackPortNeedsHook(r) } -// EditIPList adds or removes a rule in an apf allow_hosts/deny_hosts file, rewriting it in place. // addRule is AddRule's implementation, with the IPv6 gate optional. Restore // passes enforceIPv6Gate false so it can reproduce a Backup snapshot's exact // prior state, including inert entries the gate would reject as fresh no-op @@ -1569,26 +1445,21 @@ func (f *APF) addRule(ctx context.Context, zoneName string, r *Rule, enforceIPv6 return f.EditIPList(ctx, APFDeny, denyAction, r, false) } -// AddRule adds a rule to apf, routing it to conf.apf, the allow/deny lists, or the pre-hook. // AddRule adds a rule to apf, routing it to conf.apf, the allow/deny lists, or the pre-hook. func (f *APF) AddRule(ctx context.Context, zoneName string, r *Rule) error { return f.addRule(ctx, zoneName, r, true) } -// RemoveRule removes a rule from apf, routing it to the config file or pre-hook that holds it. // InsertRule is unsupported: APF organizes rules in config files, not an ordered list. func (f *APF) InsertRule(ctx context.Context, zoneName string, position int, r *Rule) error { return unsupportedOrdering(f.Type()) } -// InsertNATRule is unsupported: APF stores NAT in a config file it applies as a -// whole, with no explicit ordering. // MoveRule is unsupported for the same reason as InsertRule. func (f *APF) MoveRule(ctx context.Context, zoneName string, r *Rule, position int) error { return unsupportedOrdering(f.Type()) } -// Capabilities reports the firewall features apf supports. // removePlainHost drops the bidirectional plain allow_hosts/deny_hosts line backing // the DirAny rule e, choosing the list by the rule's action. func (f *APF) removePlainHost(ctx context.Context, e *Rule) error { @@ -1598,13 +1469,6 @@ func (f *APF) removePlainHost(ctx context.Context, e *Rule) error { return f.EditIPList(ctx, APFDeny, f.denyActionFor(e.Proto), e, true) } -// removeDualStackPort removes a single-family bare tcp/udp port accept. Such a rule -// is stored either as its own per-family hook rule or as one family of a dual-stack -// conf.apf CPORTS entry (a FamilyAny port). Read the CPORTS list the port would live -// in — not the merged rule view, which cannot tell a genuine CPORTS entry from a -// pair of single-family hook rules — and split it when present: drop the dual-stack -// entry and re-add the surviving opposite family as a hook rule, so the untargeted -// family keeps its coverage. Otherwise the rule is a per-family hook rule. // removeBareHostOneWay removes a one-way bare-address host rule. Such a rule is // stored either as its own hook rule or as one direction of a bidirectional plain // allow_hosts/deny_hosts line (a DirAny rule). When a matching plain line exists, @@ -1637,10 +1501,6 @@ func (f *APF) removeBareHostOneWay(ctx context.Context, zoneName string, r *Rule return err } -// addRule is AddRule's implementation, with the IPv6 gate optional. Restore -// passes enforceIPv6Gate false so it can reproduce a Backup snapshot's exact -// prior state, including inert entries the gate would reject as fresh no-op -// writes. // cPortsKey returns the conf.apf CPORTS list a tcp/udp port rule of the given // direction lives in, or "" for a protocol with no such list. func (f *APF) cPortsKey(proto Protocol, output bool) string { @@ -1657,9 +1517,6 @@ func (f *APF) cPortsKey(proto Protocol, output bool) string { return "" } -// natFile returns the routing file a NAT rule belongs in: source NAT is -// applied in POSTROUTING (postroute.rules), destination NAT in PREROUTING -// (preroute.rules). // removeDualStackPort removes a single-family bare tcp/udp port accept. Such a rule // is stored either as its own per-family hook rule or as one family of a dual-stack // conf.apf CPORTS entry (a FamilyAny port). Read the CPORTS list the port would live @@ -1704,15 +1561,6 @@ func (f *APF) removeDualStackPort(ctx context.Context, r *Rule) error { // which, so remove the rule from both backings — EditConf drops it from the CPORTS // list and the hook edit drops both per-family rows, and each no-ops when the rule is // absent — clearing the whole merged rule wherever it lives. Unlike the single-family -// removeDualStackPort there is no untargeted family to preserve, so nothing is re-added. -// removeFamilyAnyPort removes a FamilyAny address-less bare tcp/udp port accept. Its -// two families live in a dual-stack conf.apf CPORTS entry (a genuine FamilyAny add), -// in a v4+v6 pair in the hook (two separate concrete-family adds that GetRules merged -// back into one FamilyAny rule), or split across both. The merged read cannot tell -// which, so remove the rule from both backings — EditConf drops it from the CPORTS -// list and the hook edit drops both per-family rows, and each no-ops when the rule is -// absent — clearing the whole merged rule wherever it lives. Unlike the single-family -// removeDualStackPort there is no untargeted family to preserve, so nothing is re-added. func (f *APF) removeFamilyAnyPort(ctx context.Context, r *Rule) error { if err := f.EditConf(ctx, r, true); err != nil { return err @@ -1722,7 +1570,6 @@ func (f *APF) removeFamilyAnyPort(ctx context.Context, r *Rule) error { return err } -// GetRules returns the current filter rules read from conf.apf, the allow/deny lists, and the pre-hook. // RemoveRule removes a rule from apf, routing it to the config file or pre-hook that holds it. func (f *APF) RemoveRule(ctx context.Context, zoneName string, r *Rule) error { // A non-plain-line DirAny target fans out into its two concrete-direction rules, @@ -1805,7 +1652,6 @@ func (f *APF) RemoveRule(ctx context.Context, zoneName string, r *Rule) error { return f.EditIPList(ctx, APFDeny, f.denyActionFor(r.Proto), r, true) } -// Backup captures the current filter and NAT rules managed by this backend. // parseNATLine decodes a raw iptables nat command line back into a NATRule, // reporting whether the line is one this backend recognizes. func (f *APF) parseNATLine(line string) (*NATRule, bool) { @@ -1828,7 +1674,6 @@ func (f *APF) parseNATLine(line string) (*NATRule, bool) { return r, true } -// parseNATFile reads a routing-rules file and returns the NAT rules it holds. // parseNATFile reads a routing-rules file and returns the NAT rules it holds. func (f *APF) parseNATFile(path string) ([]*NATRule, error) { fd, err := os.Open(path) @@ -1861,7 +1706,6 @@ func (f *APF) parseNATFile(path string) ([]*NATRule, error) { return rules, nil } -// GetNATRules returns the NAT rules held in apf's preroute and postroute routing files. // GetNATRules returns the NAT rules held in apf's preroute and postroute routing files. func (f *APF) GetNATRules(ctx context.Context, zoneName string) ([]*NATRule, error) { var rules []*NATRule @@ -1880,9 +1724,6 @@ func (f *APF) GetNATRules(ctx context.Context, zoneName string) ([]*NATRule, err return merged, nil } -// editNATFile adds or removes a NAT rule's command line(s) in a routing file. -// A family-agnostic rule occupies one line per family; both are added or dropped -// together. It records whether a reload is needed. // natFamilies lists the address families a NAT rule is written for: a rule // pinned to a family touches only that command; a family-agnostic rule (e.g. a // portless masquerade) is written for both v4 and v6. @@ -1897,8 +1738,6 @@ func (f *APF) natFamilies(r *NATRule) []Family { } } -// natLine encodes a NAT rule as a raw iptables/ip6tables nat-table command -// line for the given family, the form apf's shell-sourced routing files expect. // natFile returns the routing file a NAT rule belongs in: source NAT is // applied in POSTROUTING (postroute.rules), destination NAT in PREROUTING // (preroute.rules). @@ -1909,7 +1748,6 @@ func (f *APF) natFile(r *NATRule) string { return APFPreroute } -// natCommand returns the iptables command name for a family. // natCommand returns the iptables command name for a family. func (f *APF) natCommand(fam Family) string { if fam == IPv6 { @@ -1918,9 +1756,6 @@ func (f *APF) natCommand(fam Family) string { return "iptables" } -// natFamilies lists the address families a NAT rule is written for: a rule -// pinned to a family touches only that command; a family-agnostic rule (e.g. a -// portless masquerade) is written for both v4 and v6. // natLine encodes a NAT rule as a raw iptables/ip6tables nat-table command // line for the given family, the form apf's shell-sourced routing files expect. func (f *APF) natLine(r *NATRule, fam Family) (string, error) { @@ -1934,8 +1769,6 @@ func (f *APF) natLine(r *NATRule, fam Family) (string, error) { return f.natCommand(fam) + " -t nat " + spec, nil } -// parseNATLine decodes a raw iptables nat command line back into a NATRule, -// reporting whether the line is one this backend recognizes. // editNATFile adds or removes a NAT rule's command line(s) in a routing file. // A family-agnostic rule occupies one line per family; both are added or dropped // together. It records whether a reload is needed. @@ -2042,7 +1875,6 @@ func (f *APF) editNATFile(r *NATRule, remove bool) error { return nil } -// AddNATRule adds a NAT rule to apf's preroute or postroute routing file. // AddNATRule adds a NAT rule to apf's preroute or postroute routing file. func (f *APF) AddNATRule(ctx context.Context, zoneName string, r *NATRule) error { if err := r.validate(); err != nil { @@ -2051,14 +1883,12 @@ func (f *APF) AddNATRule(ctx context.Context, zoneName string, r *NATRule) error return f.editNATFile(r, false) } -// RemoveNATRule removes a NAT rule from apf's preroute or postroute routing file. // InsertNATRule is unsupported: APF stores NAT in a config file it applies as a // whole, with no explicit ordering. func (f *APF) InsertNATRule(ctx context.Context, zoneName string, position int, r *NATRule) error { return unsupportedOrdering(f.Type()) } -// MoveRule is unsupported for the same reason as InsertRule. // RemoveNATRule removes a NAT rule from apf's preroute or postroute routing file. func (f *APF) RemoveNATRule(ctx context.Context, zoneName string, r *NATRule) error { if err := r.validate(); err != nil { @@ -2067,9 +1897,6 @@ func (f *APF) RemoveNATRule(ctx context.Context, zoneName string, r *NATRule) er return f.editNATFile(r, true) } -// isConfRule reports whether a rule is managed in conf.apf: an address-less -// accept rule of ports (TCP/UDP lists) or ICMP/ICMPv6 types (a nil type is the -// "all" wildcard). // Backup captures the current filter and NAT rules managed by this backend. func (f *APF) Backup(ctx context.Context, zoneName string) (*Backup, error) { rules, err := f.GetRules(ctx, zoneName) @@ -2085,7 +1912,6 @@ func (f *APF) Backup(ctx context.Context, zoneName string) (*Backup, error) { return &Backup{Rules: rules, NATRules: natRules}, nil } -// Restore replaces the managed rules with the contents of a Backup. // Restore replaces the managed rules with the contents of a Backup. func (f *APF) Restore(ctx context.Context, zoneName string, backup *Backup) error { if backup == nil { @@ -2126,49 +1952,41 @@ func (f *APF) Restore(ctx context.Context, zoneName string, backup *Backup) erro return nil } -// Reload restarts apf to apply config changes, but only when a mutation changed its files. // GetDefaultPolicy is unsupported: apf has no managed default-policy control. func (f *APF) GetDefaultPolicy(ctx context.Context, zoneName string) (*DefaultPolicy, error) { return nil, unsupportedPolicy(f.Type()) } -// SetDefaultPolicy is unsupported: apf has no managed default-policy control. // SetDefaultPolicy is unsupported: apf has no managed default-policy control. func (f *APF) SetDefaultPolicy(ctx context.Context, zoneName string, policy *DefaultPolicy) error { return unsupportedPolicy(f.Type()) } -// GetAddressSets is unsupported: apf has no address-set support. // GetAddressSets is unsupported: apf has no address-set support. func (f *APF) GetAddressSets(ctx context.Context) ([]*AddressSet, error) { return nil, unsupportedSet(f.Type()) } -// GetAddressSet is unsupported: apf has no address-set support. // GetAddressSet is unsupported: apf has no address-set support. func (f *APF) GetAddressSet(ctx context.Context, name string) (*AddressSet, error) { return nil, unsupportedSet(f.Type()) } -// AddAddressSet is unsupported: apf has no address-set support. // AddAddressSet is unsupported: apf has no address-set support. func (f *APF) AddAddressSet(ctx context.Context, set *AddressSet) error { return unsupportedSet(f.Type()) } -// RemoveAddressSet is unsupported: apf has no address-set support. // RemoveAddressSet is unsupported: apf has no address-set support. func (f *APF) RemoveAddressSet(ctx context.Context, name string) error { return unsupportedSet(f.Type()) } -// AddAddressSetEntry is unsupported: apf has no address-set support. // AddAddressSetEntry is unsupported: apf has no address-set support. func (f *APF) AddAddressSetEntry(ctx context.Context, name, entry string) error { return unsupportedSet(f.Type()) } -// RemoveAddressSetEntry is unsupported: apf has no address-set support. // RemoveAddressSetEntry is unsupported: apf has no address-set support. func (f *APF) RemoveAddressSetEntry(ctx context.Context, name, entry string) error { return unsupportedSet(f.Type()) @@ -2187,10 +2005,7 @@ func (f *APF) Reload(ctx context.Context) error { return nil } -// Close releases resources held by the manager; apf holds none. // Close releases resources held by the manager; apf holds none. func (f *APF) Close(ctx context.Context) error { return nil } - -// InsertRule is unsupported: APF organizes rules in config files, not an ordered list. diff --git a/scripts/__pycache__/refactor_order.cpython-314.pyc b/scripts/__pycache__/refactor_order.cpython-314.pyc deleted file mode 100644 index 7fa5008..0000000 Binary files a/scripts/__pycache__/refactor_order.cpython-314.pyc and /dev/null differ diff --git a/scripts/refactor_order.py b/scripts/refactor_order.py index 6f7e6f1..989d15e 100644 --- a/scripts/refactor_order.py +++ b/scripts/refactor_order.py @@ -21,64 +21,160 @@ import re import subprocess import sys -# Matches a function declaration together with its immediately preceding -# column-0 `//` doc comments. Groups: -# 3: receiver variable (None for a plain function) -# 4: receiver type (None for a plain function) -# 5: function name -FUNC_RE = re.compile( - r"(?m)^((?://.*\n)*)^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?([A-Za-z_]\w*)\s*\(" -) +# Regex used to parse a column-0 `func` declaration line. Groups: +# 1: receiver variable (None for a plain function) +# 2: receiver type (None for a plain function) +# 3: function name +DECL_RE = re.compile(r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?([A-Za-z_]\w*)\s*\(") def split_blocks(text): """Split source into (preamble, [block_dict, ...]). - Each block begins with the doc comments immediately preceding a `func` line - and ends at the start of the next block, so comments stay attached to their - function. + Each block begins with the doc comment for the function and ends at the + start of the next function's doc comment. This keeps comments attached to + the function they document, even when the file has been reordered before. + + Doc comments are detected by their first line: a column-0 `//` line whose + leading word is the function name. The first function in the file is used + to delimit the preamble. """ lines = text.split("\n") func_lines = [i for i, line in enumerate(lines) if line.startswith("func ")] if not func_lines: return text, [] - decl_re = re.compile(r"^func\s+(?:\((\w+)\s+\*?(\w+)\)\s+)?([A-Za-z_]\w*)\s*\(") - - def comment_start(func_line): - i = func_line - 1 - # Allow a single blank line between the previous block and the doc comment. - if i >= 0 and lines[i].strip() == "": - i -= 1 - while i >= 0 and lines[i].startswith("//"): - i -= 1 - return i + 1 - - comment_starts = [comment_start(fl) for fl in func_lines] - first_start = comment_starts[0] - preamble_lines = lines[:first_start] - preamble = "\n".join(preamble_lines) - if preamble_lines: - preamble += "\n" - - blocks = [] + # Parse all function declarations. + funcs = [] for idx, fl in enumerate(func_lines): - end_line = func_lines[idx + 1] if idx + 1 < len(func_lines) else len(lines) - block_lines = lines[comment_starts[idx] : end_line] - block_text = "\n".join(block_lines) - if block_text and not block_text.endswith("\n"): - block_text += "\n" - - m = decl_re.match(lines[fl]) + m = DECL_RE.match(lines[fl]) if not m: die(f"could not parse declaration: {lines[fl]}") assert m - blocks.append( + funcs.append( { "recv_var": m.group(1), "recv_type": m.group(2), "name": m.group(3), - "text": block_text, + "func_line": fl, + "body_end": func_lines[idx + 1] + if idx + 1 < len(func_lines) + else len(lines), + } + ) + func_names = {f["name"] for f in funcs} + + # Find all doc comment blocks in the file. A column-0 `//` sequence is split + # into separate doc comments at lines that start with another function name. + doc_re = re.compile(r"//\s+([a-zA-Z_][a-zA-Z0-9_]*)\b") + doc_comments = {} # name -> list of comment blocks (each block is a list of lines) + i = 0 + while i < len(lines): + if not lines[i].startswith("//"): + i += 1 + continue + seq = [] + while i < len(lines) and lines[i].startswith("//"): + seq.append(lines[i]) + i += 1 + + current = [] + for line in seq: + mm = doc_re.match(line) + if mm and mm.group(1) in func_names and current: + # Line starts a new doc comment. Save the current one if valid. + first = current[0] + fm = doc_re.match(first) + if fm and fm.group(1) in func_names: + doc_comments.setdefault(fm.group(1), []).append(current) + current = [line] + else: + current.append(line) + if current: + first = current[0] + fm = doc_re.match(first) + if fm and fm.group(1) in func_names: + doc_comments.setdefault(fm.group(1), []).append(current) + + # Deduplicate and choose the longest doc comment for each function. + func_comment = {} + for fname, blocks in doc_comments.items(): + seen = set() + unique = [] + for block in blocks: + key = "\n".join(block) + if key not in seen: + seen.add(key) + unique.append(block) + if unique: + func_comment[fname] = max(unique, key=len) + + # Preamble is everything before the first function's doc comment or func line. + first_func = funcs[0] + if first_func["name"] in func_comment: + comment = func_comment[first_func["name"]] + # Find the earliest occurrence of this comment block before the function. + preamble_end = first_func["func_line"] + for pos in range(first_func["func_line"] - len(comment), -1, -1): + if lines[pos : pos + len(comment)] == comment: + preamble_end = pos + break + else: + preamble_end = first_func["func_line"] + preamble_lines = lines[:preamble_end] + preamble = "\n".join(preamble_lines) + if preamble_lines: + preamble += "\n" + + # Build blocks. Body is from func_line to next_func_line, with the next + # function's doc comment stripped off. The last function also has trailing + # blank lines stripped so no orphan comments are left at the end of file. + blocks = [] + for idx, f in enumerate(funcs): + name = f["name"] + is_last = idx == len(funcs) - 1 + body_end = f["body_end"] + + if is_last: + # Skip trailing blanks so orphan comments after the last function are + # reachable. + while body_end > f["func_line"] and lines[body_end - 1] == "": + body_end -= 1 + + # Strip trailing // lines (the next function's doc comment, or orphan + # comments after the last function). + while body_end > f["func_line"] and lines[body_end - 1].startswith("//"): + body_end -= 1 + + if is_last: + # Strip any remaining blanks after the orphan comments. + while body_end > f["func_line"] and lines[body_end - 1] == "": + body_end -= 1 + + body_lines = lines[f["func_line"] : body_end] + body_text = "\n".join(body_lines) + # Preserve a separator blank line that sits immediately before the next + # function's doc comment. Python's split/join drops the blank when it is + # the last element of the slice, so add it back explicitly. + if ( + body_lines + and body_lines[-1] == "" + and any(line != "" for line in lines[body_end:]) + ): + body_text += "\n" + if body_text and not body_text.endswith("\n"): + body_text += "\n" + + comment_text = "" + if name in func_comment: + comment_text = "\n".join(func_comment[name]) + "\n" + + blocks.append( + { + "recv_var": f["recv_var"], + "recv_type": f["recv_type"], + "name": name, + "text": comment_text + body_text, } )