package bootstrap import ( "context" "fmt" "net" "strings" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/reachable" "github.com/novox/mesh-host/internal/store" ) // quiet are the processes every fresh machine runs that serve nobody: name resolution (whose // link-local resolver listens on TCP as well as UDP, on every address), address configuration and // time. ss names a process by its first fifteen characters, so both spellings are here. Measured // on a freshly installed lab machine (testdata/fresh-machine-listeners.txt): these and nothing else. var quiet = map[string]bool{ "systemd-resolved": true, "systemd-resolve": true, "systemd-networkd": true, "systemd-network": true, "systemd-timesyncd": true, "systemd-timesyn": true, "dhcpcd": true, } // InUse says what makes this machine a machine in use (novox/hq ADR 0100): every running container // no host made, and every socket listening on an address other than loopback that is neither ssh's // nor held by what every fresh machine runs. ours names // what the mesh itself runs, which a re-run of genesis finds and does not count. func InUse(ctx context.Context, run Runner, ours func(name string) bool) ([]string, []reachable.Reach, error) { var containers []string out, err := run(ctx, "docker", "ps", "--format", "{{.Names}}\t{{.Label \"mesh-host.spec\"}}") if err != nil { return nil, nil, fmt.Errorf("cannot ask the container runtime what is running here: %w", err) } for _, line := range strings.Split(out, "\n") { name, label, _ := strings.Cut(strings.TrimSpace(line), "\t") label = strings.TrimSpace(label) if name == "" || (label != "" && label != "") || ours(name) { continue } containers = append(containers, name) } listening, err := run(ctx, "ss", "-Hltunp") if err != nil { return nil, nil, fmt.Errorf("cannot read what listens on this machine: %w", err) } var listeners []reachable.Reach for _, r := range reachable.Sockets(listening) { if counts(r) && !ours(r.By) { listeners = append(listeners, r) } } return containers, listeners, nil } func counts(r reachable.Reach) bool { if ip := net.ParseIP(r.Address); ip != nil && ip.IsLoopback() { return false } switch r.Protocol { case "tcp": return r.By != "sshd" && !(r.By == "" && r.Port == 22) && !quiet[r.By] case "udp": return !quiet[r.By] } return false } // RefuseAMachineInUse is the check a converged genesis makes before changing anything: a machine // in use is refused, naming every container and listener counted, because raising the foundation's // filter there would close what it serves — a forgotten --adopted must not close a working machine. // An adopted genesis is told what it found, and goes on. func RefuseAMachineInUse(ctx context.Context, o Options, run Runner, say func(string)) error { known, err := store.Load(o.State) if err != nil { return err } if len(known.Resources) > 0 { // **The machine says how it was raised** (novox/hq ADR 0103). A re-run must not change // the node's mode by a flag forgotten or added: without --adopted the bundle would load // the foundation's dropping filter over the found firewall, and with it on a converged // machine the filter the node relies on would be removed as no longer carried. switch adopted := RecordsAdoption(known); { case adopted && !o.Adopted: return fmt.Errorf("this machine was raised adopted, and genesis was run again without --adopted. " + "Run it again the way it was raised: pass --adopted. Returning it to converged is the " + "controller's act (converge), never genesis's; nothing was changed") case !adopted && o.Adopted: return fmt.Errorf("this machine was raised converged, and genesis was run again with --adopted, " + "which would remove the foundation's filter it relies on. Run it again without --adopted; " + "returning a node to adopted is the controller's act (adopt); nothing was changed") } // What genesis raised on an earlier run is the mesh's, and it is what the machine now // serves; the question was answered the first time. say(" in use not asked: this machine carries what an earlier genesis raised") return nil } containers, listeners, err := InUse(ctx, run, func(string) bool { return false }) if err != nil { return err } if len(containers) == 0 && len(listeners) == 0 { say(" in use no: no container runs and nothing listens beyond ssh") return nil } var named []string for _, c := range containers { named = append(named, "container "+c) } for _, l := range listeners { by := l.By if by == "" { by = "an unnamed process" } named = append(named, fmt.Sprintf("%s %s:%d by %s", l.Protocol, l.Address, l.Port, by)) } if o.Adopted { say(fmt.Sprintf(" in use yes, and adopted: %d thing(s) found are kept", len(named))) return nil } return fmt.Errorf("this machine is in use, and a converged genesis would close what it serves:\n - %s\n"+ "If it is meant to join the mesh keeping what it runs, pass --adopted: its firewall stays in "+ "force and every module is taken on it one at a time. Nothing was changed", strings.Join(named, "\n - ")) } // RecordsAdoption is whether this machine's state says it is an adopted node: it holds something // of the mesh's that only an adopted node has — the guard or an opening, under the adoption prefix // — or something it found and holds. A node the controller converged has neither any more; the // record of the firewall it found outlives the flip, so it is not read as the mode. func RecordsAdoption(known store.State) bool { if len(known.Held) > 0 { return true } for _, r := range known.Resources { if strings.HasPrefix(r.ID, declaration.AdoptionPrefix) { return true } } return false }