package bootstrap import ( "context" "fmt" "net" "strings" "github.com/novox/mesh-host/internal/reachable" "github.com/novox/mesh-host/internal/store" ) // quietUDP are processes whose UDP sockets every fresh machine has — name resolution, address // configuration, time — and which serve nobody. ss names a process by its first fifteen characters, // so both spellings are here. // // **Still to be measured** (novox/hq ADR 0100): this list is what a fresh machine is expected to // hold, and it must be checked against a freshly installed lab machine before it is trusted. var quietUDP = 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 not ssh's — a // UDP one only when it is held by something other than 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) case "udp": return !quietUDP[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 { // 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 - ")) }