Take over the found tunnel: its key, its port, its peers; stop it, never flush

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.
This commit is contained in:
2026-09-23 23:26:35 +02:00
parent 9176aea6c4
commit 7283924a35
18 changed files with 1182 additions and 13 deletions
+36
View File
@@ -617,6 +617,21 @@ type Service struct {
// container runtime, whose restart stops every container on the machine (novox/hq ADR 0102).
// A change that is also in RestartOn restarts it, which covers a reload.
ReloadOn []string `json:"reload-on,omitempty"`
// TakesOver names the found tunnel this service replaces (novox/hq ADR 0105): before this unit
// is started, the named unit is stopped and disabled — never flushed — and its configuration
// file is kept like any held file. Only on an adopted node, and only said by the controller,
// which knows the found tunnel's key is this node's own: without that, starting this unit on
// the found one's port would drop every peer's packets.
TakesOver *TakeOver `json:"takes-over,omitempty"`
}
// TakeOver is a found tunnel a service replaces: its interface, the unit that raised it, and its
// configuration file.
type TakeOver struct {
Interface string `json:"interface"`
Unit string `json:"unit"`
Config string `json:"config"`
}
func (s *Service) Identity() string { return s.ID }
@@ -637,6 +652,19 @@ func (s *Service) validate(where string, _ bool) []string {
"%s: boot %q; a service is \"enabled\" or \"disabled\" at boot, or omits it to "+
"leave the machine's own setting alone", where, s.Boot))
}
if t := s.TakesOver; t != nil {
switch {
case t.Unit == "" || t.Config == "" || t.Interface == "":
problems = append(problems, where+": takes-over names the found tunnel's interface, unit "+
"and config, and this leaves one out")
case t.Unit == s.Unit:
problems = append(problems, fmt.Sprintf("%s: takes-over names %s, which is this service's own unit",
where, t.Unit))
case s.State != "running":
problems = append(problems, where+": a service that takes over a tunnel is running — stopping "+
"the found one for a service that will not run would leave the peers with nothing")
}
}
return problems
}
@@ -1167,6 +1195,14 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
"resource %q: an opening is for an adopted node, and this declaration does not "+
"say the node is adopted", r.Identity()))
}
if svc, ok := r.(*Service); ok && svc.TakesOver != nil {
// A tunnel is taken over on an adopted node, where what is found is kept: on a
// converged one there is nothing found to take over, and stopping a unit the
// mesh did not declare would be the host deciding (novox/hq ADR 0105).
problems = append(problems, fmt.Sprintf(
"resource %q: taking over a tunnel is for an adopted node, and this declaration "+
"does not say the node is adopted", r.Identity()))
}
}
}