Files
mesh-host/internal/bootstrap/inuse.go
T

112 lines
4.1 KiB
Go

package bootstrap
import (
"context"
"fmt"
"net"
"strings"
"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 != "<no value>") || 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 {
// 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 - "))
}