package apply import ( "context" "errors" "fmt" "os" "strings" "time" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" "github.com/novox/mesh-host/internal/system" "github.com/novox/mesh-host/internal/tunnel" ) // The private network takes over the tunnel it found (novox/hq ADR 0105). // // The controller says so on the interface's service: `takes-over` names the found interface, the // unit that raised it and its configuration file. Before the mesh's unit is started, the host keeps // that file like any held file — the original recorded before anything else happens to it — and // stops and disables the found unit. Never a flush: `wg set … peer … remove` is never run, the // file is never written, and the found interface goes down the way its own unit takes it down. // Then the mesh's interface comes up, with the found key the node took at enrolment, on the found // port, with the found peers in its list — and a peer of the tunnel cannot tell it changed hands. // // Every apply, not once: a found unit somebody starts again would take the port back from the // mesh's interface, so it is stopped again and said so. That is the one place an adopted node // undoes something done by hand, and it is because the tunnel is the mesh's now. // TakenTunnel is what an apply says about a tunnel it took over, for the node's report. type TakenTunnel struct { Interface string Port int Range string Peers int // Taken is whether the found interface is down and disabled and the mesh's up in its place. Taken bool Kept string } // takeOverID is the held record's id for the found configuration: the service's own with a suffix, // so it is declared for as long as the service is and never mistaken for the service itself. func takeOverID(svc *declaration.Service) string { return svc.ID + ".takes-over" } // takeOver keeps the found tunnel's configuration and stops its unit, ahead of the service that // replaces it. Returned is the hold's outcome, and what was found for the report. func takeOver(ctx context.Context, sys system.System, svc *declaration.Service, d *declaration.Declaration, known *store.State, run Runner, keep Keep, now time.Time) (Outcome, TakenTunnel, error) { t := svc.TakesOver id := takeOverID(svc) module, _ := d.Adoption.UntakenModuleOf(svc.ID) if module == "" { module = "the private network" } facts := TakenTunnel{Interface: t.Interface} // 1. The configuration, kept like any held file. A synthetic file resource stands for it, so // the same code keeps its original, digests it and notices it changing. file := &declaration.File{ID: id, Type: declaration.TypeFile, Path: t.Config} was, already := known.HeldAt(id) out, held, err := hold(ctx, sys, file, module, was, already, "the configuration of the tunnel "+t.Interface+", taken over by "+svc.Unit, run, keep, now) if err != nil { return begin(file), facts, fmt.Errorf("keeping the found tunnel's configuration: %w", err) } known.RecordHeld(held) facts.Kept = held.Kept // What the file says, for the report: read from the machine, or from the kept original when // the machine's copy is gone. The private key stays in the file; nothing here keeps it. unread := "" raw, err := os.ReadFile(t.Config) if err != nil && held.Kept != "" { raw, err = os.ReadFile(held.Kept) } if err == nil { if found, perr := tunnel.Parse(raw); perr == nil { facts.Port, facts.Range, facts.Peers = found.Port, found.Range, len(found.Peers) } else { unread = perr.Error() } } else { unread = err.Error() } // 2. The found unit: stopped if it runs, disabled if it starts at boot. A unit that is not // there is not an error — the interface may have been raised another way, which the check // below catches — and neither is one already down. var did []string state, err := sys.ServiceState(ctx, run, t.Unit) switch { case err != nil: did = append(did, t.Unit+" is not a unit here") case state == "running": if err := sys.SetServiceState(ctx, run, t.Unit, "stopped"); err != nil { return out, facts, fmt.Errorf("stopping the found %s: %w", t.Unit, err) } after, err := sys.ServiceState(ctx, run, t.Unit) if err != nil { return out, facts, err } if after != "stopped" { return out, facts, fmt.Errorf("%s was asked to stop and is %s", t.Unit, after) } did = append(did, "stopped "+t.Unit) } if err == nil { if boot, err := sys.ServiceBoot(ctx, run, t.Unit); err == nil && boot == "enabled" { if err := sys.SetServiceBoot(ctx, run, t.Unit, "disabled"); err != nil { return out, facts, fmt.Errorf("disabling the found %s at boot: %w", t.Unit, err) } did = append(did, "disabled it at boot") } } // 3. The interface is gone. If it is still up, something other than its unit raised it, and // starting the mesh's on the same port and address would fail or, worse, half work. if up, err := run(ctx, "wg", "show", "interfaces"); err == nil { for _, iface := range strings.Fields(up) { if iface == t.Interface { return out, facts, fmt.Errorf("%s is still up after its unit %s was stopped: something other "+ "than that unit raises it, and the mesh's interface cannot take its port and address "+ "while it does. Nothing was flushed", t.Interface, t.Unit) } } } out.Detail = "the tunnel " + t.Interface + "'s configuration, kept as found" if held.Kept != "" { out.Detail += " (original at " + held.Kept + ")" } if len(did) > 0 { out.Detail += "; " + strings.Join(did, ", ") + " — never flushed" } if held.Changed != "" { out.Detail += "; " + held.Changed + " by something other than the mesh since it was found" } if unread != "" { // Said, not swallowed: the report would otherwise say a tunnel with no port and no // peers was carried, which reads as a tunnel that was not one. out.Detail += "; what it says could not be read as a tunnel's: " + unread } return out, facts, nil } // takesOver is the service in a declaration that takes over a tunnel, if any: one per node, since // a machine has one private network. func takesOver(d *declaration.Declaration) *declaration.Service { for _, r := range d.Resources { if svc, ok := r.(*declaration.Service); ok && svc.TakesOver != nil { return svc } } return nil } // errNotAdopted is a takeover on a declaration that does not say the node is adopted, which the // parser refuses already; kept as a second line of defence at the point of acting. var errNotAdopted = errors.New("a tunnel is taken over on an adopted node only")