From 9b747f7acbdf5393e436dab9d6ff1fdc572e389b Mon Sep 17 00:00:00 2001 From: James Coleman Date: Wed, 8 Jul 2026 16:09:57 -0500 Subject: [PATCH] Fixed --- apf_linux.go | 185 ------------------ .../refactor_order.cpython-314.pyc | Bin 11913 -> 0 bytes scripts/refactor_order.py | 174 ++++++++++++---- 3 files changed, 135 insertions(+), 224 deletions(-) delete mode 100644 scripts/__pycache__/refactor_order.cpython-314.pyc 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 7fa50080f72ec1aedc59ffcffe86c93a95537fd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11913 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 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, } )