The host says what filters the machine, with owners, and keeps the found firewall retired on every converged apply (hq ADR 0168)
Every table and chain that refuses traffic is reported with whose it is: the mesh's, the found firewall's, the container runtime's own, a ban, or other — the runtime's user chain is other, which is where both predecessors kept their rules, in the legacy filter on one machine and invisible to the mesh. Adoption's threshold does not move; a converged machine's report grows by its filters and its found firewall's state. Convergence is a state the host keeps: a found firewall enabled again is retired again and said; a reconcile that finds it inactive records that it was found so, never that the mesh did it; a step skipped after a failed apply is said. A retirement the mesh began and did not finish is finished. Fixtures are rulesets captured from three machines of the first mesh.
This commit is contained in:
+102
-81
@@ -128,42 +128,82 @@ func statusActive(out string) bool {
|
||||
// mesh needs, so a refusal that names the sources it refuses, in a table or a chain that accepts
|
||||
// nothing and is entered only from chains whose policy accepts, is not counted.
|
||||
func Refusing(ruleset string, ufwActive bool) []string {
|
||||
type rule struct{ table, chain, line string }
|
||||
type chainOf struct {
|
||||
base, dropping, accepts bool
|
||||
policyLine string
|
||||
jumpedFrom []string
|
||||
}
|
||||
chains := map[string]*chainOf{} // by "table\x00chain"
|
||||
tableAccepts := map[string]bool{}
|
||||
var tables []string
|
||||
var refusals []rule
|
||||
managed := map[string]bool{}
|
||||
var table, chain string
|
||||
get := func(t, c string) *chainOf {
|
||||
k := t + "\x00" + c
|
||||
if chains[k] == nil {
|
||||
chains[k] = &chainOf{}
|
||||
var refusing []string
|
||||
for _, f := range Filters(ruleset, nil, ufwActive) {
|
||||
if f.Owner != OwnerOther || f.chain == userChain {
|
||||
// A refusal in the runtime's user chain is reported as *other* and does not refuse
|
||||
// adoption (novox/hq ADR 0168, rule 4): both predecessors kept their rules there.
|
||||
continue
|
||||
}
|
||||
name := "table " + f.table
|
||||
if len(refusing) == 0 || refusing[len(refusing)-1] != name {
|
||||
if !contains(refusing, name) {
|
||||
refusing = append(refusing, name)
|
||||
}
|
||||
}
|
||||
return chains[k]
|
||||
}
|
||||
return refusing
|
||||
}
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nftRule is one line of a ruleset that refuses, with where it is.
|
||||
type nftRule struct{ table, chain, line string }
|
||||
|
||||
// nftChain is what a parse knows about one chain.
|
||||
type nftChain struct {
|
||||
base, dropping, accepts bool
|
||||
policyLine string
|
||||
jumpedFrom []string
|
||||
}
|
||||
|
||||
// nftRuleset is `nft list ruleset`, read: its tables in order, its chains, every refusing line,
|
||||
// and which tables iptables-nft manages.
|
||||
type nftRuleset struct {
|
||||
tables []string
|
||||
chains map[string]*nftChain // by "table\x00chain"
|
||||
chainOrder []string
|
||||
tableAccepts map[string]bool
|
||||
refusals []nftRule
|
||||
managed map[string]bool
|
||||
}
|
||||
|
||||
func (r *nftRuleset) get(t, c string) *nftChain {
|
||||
k := t + "\x00" + c
|
||||
if r.chains[k] == nil {
|
||||
r.chains[k] = &nftChain{}
|
||||
r.chainOrder = append(r.chainOrder, k)
|
||||
}
|
||||
return r.chains[k]
|
||||
}
|
||||
|
||||
func parseNft(ruleset string) *nftRuleset {
|
||||
r := &nftRuleset{chains: map[string]*nftChain{}, tableAccepts: map[string]bool{}, managed: map[string]bool{}}
|
||||
var table, chain string
|
||||
for _, raw := range strings.Split(ruleset, "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "# Warning: table ") && strings.Contains(line, "managed by iptables-nft"):
|
||||
name := strings.TrimPrefix(line, "# Warning: table ")
|
||||
name, _, _ = strings.Cut(name, " is managed")
|
||||
managed[name] = true
|
||||
r.managed[name] = true
|
||||
continue
|
||||
case strings.HasPrefix(line, "table "):
|
||||
table = strings.TrimSuffix(strings.TrimSpace(strings.TrimPrefix(line, "table ")), "{")
|
||||
table = strings.TrimSpace(table)
|
||||
tables = append(tables, table)
|
||||
r.tables = append(r.tables, table)
|
||||
chain = ""
|
||||
continue
|
||||
case strings.HasPrefix(line, "chain "):
|
||||
chain = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "chain "), "{"))
|
||||
get(table, chain)
|
||||
r.get(table, chain)
|
||||
continue
|
||||
case strings.HasPrefix(line, "set ") || strings.HasPrefix(line, "map ") ||
|
||||
strings.HasPrefix(line, "flowtable "):
|
||||
@@ -172,7 +212,7 @@ func Refusing(ruleset string, ufwActive bool) []string {
|
||||
case line == "" || line == "}" || strings.HasPrefix(line, "#") || chain == "":
|
||||
continue
|
||||
}
|
||||
c := get(table, chain)
|
||||
c := r.get(table, chain)
|
||||
if strings.HasPrefix(line, "type ") {
|
||||
c.base = true
|
||||
c.policyLine = line
|
||||
@@ -183,85 +223,66 @@ func Refusing(ruleset string, ufwActive bool) []string {
|
||||
if i := strings.Index(line, verb); i >= 0 {
|
||||
target := strings.Fields(line[i+len(verb):])
|
||||
if len(target) > 0 {
|
||||
get(table, target[0]).jumpedFrom = append(get(table, target[0]).jumpedFrom, chain)
|
||||
r.get(table, target[0]).jumpedFrom = append(r.get(table, target[0]).jumpedFrom, chain)
|
||||
}
|
||||
}
|
||||
}
|
||||
if accepts(line) {
|
||||
c.accepts = true
|
||||
tableAccepts[table] = true
|
||||
r.tableAccepts[table] = true
|
||||
}
|
||||
if verdictRefuses(line) {
|
||||
refusals = append(refusals, rule{table, chain, line})
|
||||
r.refusals = append(r.refusals, nftRule{table, chain, line})
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
skipped := func(table string) bool {
|
||||
if table == "inet mesh" || table == "inet mesh_guard" {
|
||||
return true
|
||||
}
|
||||
return (managed[table] || iptablesTable(table)) && ufwActive
|
||||
// onlyBans is whether a refusal only refuses the sources it names: in a table that accepts nothing
|
||||
// and whose base chains all accept by default, or in a chain that accepts nothing and is entered
|
||||
// only from base chains that accept by default.
|
||||
func (r *nftRuleset) onlyBans(rule nftRule) bool {
|
||||
if !bansSources(rule.line) {
|
||||
return false
|
||||
}
|
||||
// onlyBans is whether a refusal only refuses the sources it names: in a table that accepts
|
||||
// nothing and whose base chains all accept by default, or in a chain that accepts nothing and
|
||||
// is entered only from base chains that accept by default.
|
||||
onlyBans := func(r rule) bool {
|
||||
if !bansSources(r.line) {
|
||||
return false
|
||||
}
|
||||
allAccepting := true
|
||||
for k, c := range chains {
|
||||
if strings.HasPrefix(k, r.table+"\x00") && c.base && !strings.Contains(c.policyLine, "policy accept") {
|
||||
allAccepting = false
|
||||
}
|
||||
}
|
||||
if !tableAccepts[r.table] && allAccepting {
|
||||
return true
|
||||
}
|
||||
c := get(r.table, r.chain)
|
||||
if c.base || c.accepts || len(c.jumpedFrom) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, from := range c.jumpedFrom {
|
||||
caller := get(r.table, from)
|
||||
if !caller.base || !strings.Contains(caller.policyLine, "policy accept") {
|
||||
return false
|
||||
}
|
||||
allAccepting := true
|
||||
for k, c := range r.chains {
|
||||
if strings.HasPrefix(k, rule.table+"\x00") && c.base && !strings.Contains(c.policyLine, "policy accept") {
|
||||
allAccepting = false
|
||||
}
|
||||
}
|
||||
if !r.tableAccepts[rule.table] && allAccepting {
|
||||
return true
|
||||
}
|
||||
return r.enteredAccepting(rule.table, rule.chain, map[string]bool{})
|
||||
}
|
||||
|
||||
counted := map[string]bool{}
|
||||
for k, c := range chains {
|
||||
t, name, _ := strings.Cut(k, "\x00")
|
||||
if skipped(t) || !c.dropping {
|
||||
continue
|
||||
}
|
||||
if (managed[t] || iptablesTable(t)) && runtimes(t, name, c.policyLine) {
|
||||
continue
|
||||
}
|
||||
counted[t] = true
|
||||
// enteredAccepting is whether a chain accepts nothing and is entered only through chains that
|
||||
// accept by default — base chains whose policy accepts, or chains that are themselves entered that
|
||||
// way and accept nothing. A ban list jumped to from the runtime's user chain, which the forward
|
||||
// chain enters with an accepting policy, is still a ban list.
|
||||
func (r *nftRuleset) enteredAccepting(table, chain string, seen map[string]bool) bool {
|
||||
if seen[chain] {
|
||||
return false
|
||||
}
|
||||
for _, r := range refusals {
|
||||
if skipped(r.table) || counted[r.table] {
|
||||
continue
|
||||
}
|
||||
if (managed[r.table] || iptablesTable(r.table)) && runtimes(r.table, r.chain, r.line) {
|
||||
continue
|
||||
}
|
||||
if onlyBans(r) {
|
||||
continue
|
||||
}
|
||||
counted[r.table] = true
|
||||
seen[chain] = true
|
||||
c := r.get(table, chain)
|
||||
if c.base || c.accepts || len(c.jumpedFrom) == 0 {
|
||||
return false
|
||||
}
|
||||
var refusing []string
|
||||
for _, t := range tables {
|
||||
if counted[t] {
|
||||
counted[t] = false
|
||||
refusing = append(refusing, "table "+t)
|
||||
for _, from := range c.jumpedFrom {
|
||||
caller := r.get(table, from)
|
||||
if caller.base {
|
||||
if !strings.Contains(caller.policyLine, "policy accept") {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if caller.accepts || !r.enteredAccepting(table, from, seen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return refusing
|
||||
return true
|
||||
}
|
||||
|
||||
// iptablesTable is whether a table is one iptables-nft writes. Named rather than read from the
|
||||
|
||||
Reference in New Issue
Block a user