147 lines
6.0 KiB
Go
147 lines
6.0 KiB
Go
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) and the network manager's
|
|
// address configuration. ss names a process by its first fifteen characters, so both spellings are
|
|
// here.
|
|
//
|
|
// **Only what the measurement found** (novox/hq ADR 0101): these two hold every listener on a
|
|
// freshly installed lab machine (testdata/fresh-machine-listeners.txt) and nothing else does. A
|
|
// daemon joins this list with a measurement of a fresh machine that holds it, never by guess — a
|
|
// time client or an address-configuration client listening on a machine that does not run one as
|
|
// standard is something somebody installed, and that is a machine in use.
|
|
var quiet = map[string]bool{
|
|
"systemd-resolved": true, "systemd-resolve": true,
|
|
"systemd-networkd": true, "systemd-network": 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 {
|
|
// **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
|
|
}
|