Fixed
This commit is contained in:
parent
98aa68f7ed
commit
9b747f7acb
3 changed files with 135 additions and 224 deletions
185
apf_linux.go
185
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.
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue