On an adopted machine the private network takes the predecessor's tunnel over in place (hq ADR 0105). Genesis finds the one interface up besides the mesh's own, settles the hub's port and the mesh's range on it, and skips ADR 0100's non-overlap check for a range that is now the tunnel's; a --hub-port or --overlay-range that disagrees is refused naming the tunnel's. At enrolment the found interface's private key becomes this node's overlay key — the one credential the mesh takes rather than mints — stored where a generated one is stored, never printed and never sent; the tunnel (port, address, range, peers) travels with the keys so the mesh composes from it before the first declaration. The interface's service may say what it takes over. Before the mesh's unit starts, the found configuration is kept like any held file and the found unit is stopped and disabled; nothing is flushed, and an interface still up after its unit stopped refuses the takeover rather than half-working. The report says what was carried: interface, port, range, peer count, taken or not, and where the original was kept.
160 lines
6.5 KiB
Go
160 lines
6.5 KiB
Go
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")
|