Add no opening a found ufw rule already answers, since ufw takes rules differing only in comment for one, as captured on a lab machine (hq ADR 0103)
This commit is contained in:
+210
-12
@@ -119,8 +119,8 @@ 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
|
||||
policyLine string
|
||||
jumpedFrom []string
|
||||
}
|
||||
chains := map[string]*chainOf{} // by "table\x00chain"
|
||||
tableAccepts := map[string]bool{}
|
||||
@@ -469,17 +469,195 @@ func words(rule string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// A ufw rule, as `ufw show added` prints it or as it is given, reduced to what ufw compares.
|
||||
//
|
||||
// **ufw treats two rules that differ only in their comment as one rule.** Measured on a lab
|
||||
// machine (testdata/ufw-comment-only.txt): adding `route allow proto tcp to any port 8080 comment
|
||||
// 'mesh-host …'` beside an operator's `route allow 8080/tcp` answers "Rule updated", and the
|
||||
// operator's rule now carries the mesh's mark — so removing the opening later would delete the
|
||||
// operator's rule. The same holds for an incoming rule and for one with a comment of its own.
|
||||
type ufwRule struct {
|
||||
route bool
|
||||
action, in, out string
|
||||
from, fromPort, to string
|
||||
port, proto, app string
|
||||
comment string
|
||||
}
|
||||
|
||||
// parseRule reads a rule in either of ufw's forms — the short `allow 22/tcp` and the long `allow
|
||||
// in on mesh0 to any port 5432 proto tcp` — into the fields ufw compares. Not ok for anything it
|
||||
// does not recognise, which is then never taken to answer an opening.
|
||||
func parseRule(rule string) (ufwRule, bool) {
|
||||
w := words(rule)
|
||||
r := ufwRule{from: "any", to: "any", comment: comment(rule)}
|
||||
i := 0
|
||||
if i < len(w) && w[i] == "route" {
|
||||
r.route = true
|
||||
i++
|
||||
}
|
||||
if i >= len(w) {
|
||||
return r, false
|
||||
}
|
||||
switch w[i] {
|
||||
case "allow", "deny", "reject", "limit":
|
||||
r.action = w[i]
|
||||
default:
|
||||
return r, false
|
||||
}
|
||||
i++
|
||||
for i < len(w) && (w[i] == "in" || w[i] == "out") {
|
||||
dir := w[i]
|
||||
i++
|
||||
iface := ""
|
||||
if i+1 < len(w) && w[i] == "on" {
|
||||
iface = w[i+1]
|
||||
i += 2
|
||||
}
|
||||
if dir == "in" {
|
||||
r.in = iface
|
||||
} else {
|
||||
r.out = iface
|
||||
}
|
||||
}
|
||||
if i < len(w) && w[i] != "from" && w[i] != "to" && w[i] != "proto" && w[i] != "comment" &&
|
||||
w[i] != "app" && w[i] != "log" && w[i] != "log-all" {
|
||||
// The short form: a port with its protocol, a bare port, or an application's name.
|
||||
port, proto, hasProto := strings.Cut(w[i], "/")
|
||||
if isPorts(port) {
|
||||
r.port = port
|
||||
if hasProto {
|
||||
r.proto = proto
|
||||
}
|
||||
} else {
|
||||
r.app = w[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
for ; i < len(w); i++ {
|
||||
next := func() string {
|
||||
if i+1 < len(w) {
|
||||
i++
|
||||
return w[i]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
switch w[i] {
|
||||
case "from":
|
||||
r.from = next()
|
||||
if i+1 < len(w) && w[i+1] == "port" {
|
||||
i++
|
||||
r.fromPort = next()
|
||||
}
|
||||
case "to":
|
||||
r.to = next()
|
||||
if i+1 < len(w) && w[i+1] == "port" {
|
||||
i++
|
||||
r.port = next()
|
||||
}
|
||||
case "port":
|
||||
r.port = next()
|
||||
case "proto":
|
||||
r.proto = next()
|
||||
case "app":
|
||||
r.app = next()
|
||||
case "comment":
|
||||
next()
|
||||
case "log", "log-all":
|
||||
default:
|
||||
return r, false
|
||||
}
|
||||
}
|
||||
if r.proto == "any" {
|
||||
r.proto = ""
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
|
||||
func isPorts(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if (c < '0' || c > '9') && c != ':' && c != ',' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// sameAs is whether ufw would take two rules for one — everything but the comment equal.
|
||||
func (r ufwRule) sameAs(o ufwRule) bool {
|
||||
r.comment, o.comment = "", ""
|
||||
return r == o
|
||||
}
|
||||
|
||||
// admits is whether a rule already lets through what an opening says: the same path, allowed from
|
||||
// any source to any address of this machine, on the opening's port and protocol — or on any
|
||||
// protocol — and on any interface, or the private network's for an opening from it.
|
||||
func (r ufwRule) admits(o *declaration.Opening) bool {
|
||||
want, ok := parseRule(strings.Join(Rule(o), " "))
|
||||
if !ok || r.route != want.route || r.action != "allow" || r.out != "" || r.app != "" ||
|
||||
r.from != "any" || r.fromPort != "" || r.to != "any" {
|
||||
return false
|
||||
}
|
||||
if r.proto != "" && r.proto != want.proto {
|
||||
return false
|
||||
}
|
||||
if r.in != "" && r.in != want.in {
|
||||
return false
|
||||
}
|
||||
return portsInclude(r.port, want.port)
|
||||
}
|
||||
|
||||
// portsInclude is whether a ufw port list — 80, 80,443, or 8000:8100 — names a port.
|
||||
func portsInclude(list, port string) bool {
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, part := range strings.Split(list, ",") {
|
||||
lo, hi, isRange := strings.Cut(part, ":")
|
||||
a, err := strconv.Atoi(lo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
b := a
|
||||
if isRange {
|
||||
if b, err = strconv.Atoi(hi); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if a <= p && p <= b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Converged is what converging an opening did. SatisfiedBy names the rule already there that
|
||||
// answers the opening, when one does; the mesh then adds nothing, and so will remove nothing.
|
||||
type Converged struct {
|
||||
Action string
|
||||
SatisfiedBy string
|
||||
}
|
||||
|
||||
// Converge makes one opening true in ufw: its marked rule present, and any rule marked for it that
|
||||
// no longer describes it deleted. Nothing unmarked is touched. The outcome is created, updated or
|
||||
// unchanged, read back from ufw rather than assumed.
|
||||
func Converge(ctx context.Context, run Runner, o *declaration.Opening) (string, error) {
|
||||
//
|
||||
// **An opening a rule already answers is not added** (novox/hq ADR 0103). If ufw holds a rule not
|
||||
// marked for this opening that already admits what it says — the operator's, or one the mesh
|
||||
// added for another opening — the opening is satisfied by it: adding the mesh's would take that
|
||||
// rule over if it differs only in its comment, and removing the opening would then delete it.
|
||||
func Converge(ctx context.Context, run Runner, o *declaration.Opening) (Converged, error) {
|
||||
rules, err := added(ctx, run)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return Converged{}, err
|
||||
}
|
||||
mark := Mark(o)
|
||||
present := false
|
||||
var stale []string
|
||||
satisfiedBy := ""
|
||||
for _, rule := range rules {
|
||||
c := comment(rule)
|
||||
switch {
|
||||
@@ -487,25 +665,45 @@ func Converge(ctx context.Context, run Runner, o *declaration.Opening) (string,
|
||||
present = true
|
||||
case markedFor(c, o.ID):
|
||||
stale = append(stale, rule)
|
||||
default:
|
||||
if parsed, ok := parseRule(rule); ok && satisfiedBy == "" && parsed.admits(o) {
|
||||
satisfiedBy = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
if present && len(stale) == 0 {
|
||||
return "unchanged", nil
|
||||
return Converged{Action: "unchanged"}, nil
|
||||
}
|
||||
for _, rule := range stale {
|
||||
if _, err := run(ctx, "ufw", deletion(rule)...); err != nil {
|
||||
return "", fmt.Errorf("deleting the mesh's stale ufw rule %q: %w", rule, err)
|
||||
return Converged{}, fmt.Errorf("deleting the mesh's stale ufw rule %q: %w", rule, err)
|
||||
}
|
||||
}
|
||||
if !present && satisfiedBy != "" {
|
||||
after, err := added(ctx, run)
|
||||
if err != nil {
|
||||
return Converged{}, err
|
||||
}
|
||||
for _, rule := range after {
|
||||
if markedFor(comment(rule), o.ID) {
|
||||
return Converged{}, fmt.Errorf("ufw still lists a stale rule marked for %s after deleting it", o.ID)
|
||||
}
|
||||
}
|
||||
action := "unchanged"
|
||||
if len(stale) > 0 {
|
||||
action = "updated"
|
||||
}
|
||||
return Converged{Action: action, SatisfiedBy: satisfiedBy}, nil
|
||||
}
|
||||
if !present {
|
||||
args := append(Rule(o), "comment", mark)
|
||||
if _, err := run(ctx, "ufw", args...); err != nil {
|
||||
return "", fmt.Errorf("adding the ufw rule for %s: %w", o.Target(), err)
|
||||
return Converged{}, fmt.Errorf("adding the ufw rule for %s: %w", o.Target(), err)
|
||||
}
|
||||
}
|
||||
after, err := added(ctx, run)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return Converged{}, err
|
||||
}
|
||||
found, leftover := false, 0
|
||||
for _, rule := range after {
|
||||
@@ -517,15 +715,15 @@ func Converge(ctx context.Context, run Runner, o *declaration.Opening) (string,
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "", fmt.Errorf("ufw was asked for %s and does not list it afterwards", o.Target())
|
||||
return Converged{}, fmt.Errorf("ufw was asked for %s and does not list it afterwards", o.Target())
|
||||
}
|
||||
if leftover > 0 {
|
||||
return "", fmt.Errorf("ufw still lists %d stale rule(s) marked for %s after deleting them", leftover, o.ID)
|
||||
return Converged{}, fmt.Errorf("ufw still lists %d stale rule(s) marked for %s after deleting them", leftover, o.ID)
|
||||
}
|
||||
if len(stale) > 0 {
|
||||
return "updated", nil
|
||||
return Converged{Action: "updated"}, nil
|
||||
}
|
||||
return "created", nil
|
||||
return Converged{Action: "created"}, nil
|
||||
}
|
||||
|
||||
// Remove deletes the rules marked for one opening, and nothing else.
|
||||
|
||||
Reference in New Issue
Block a user