Files
mesh-host/internal/network/writer.go
T
jschoubben 429ea42357 Judge the machine's own networking beside what its modules run (hq ADR 0241)
A VPN client rewrote the laptop's resolver file and every mesh name failed
while each module read healthy: nothing asked the machine. The engine now
looks every 30 s at the resolver file the uplink holder declared (naming
the program that rewrote it), the names through each listed resolver
(NXDOMAIN for a mesh name's IPv6 address is a finding, issue 262), the
tunnel's handshake with the hub, the bus and the default route; a part is
unhealthy on its second failing look, and the statement carries it.
2026-10-07 18:43:39 +02:00

120 lines
3.7 KiB
Go

package network
import (
"os"
"path/filepath"
"strings"
"time"
)
// Naming the program that rewrote the resolver file (ADR 0241 rule 2). **A guess, said as one**: the
// mesh cannot see who wrote a file after the fact, only what the file, its link and the machine show. In
// order of how much each says:
//
// 1. what the file says of itself — every program that writes it puts its name in a comment;
// 2. a backup the writer left beside it — named for the writer, or changed when the file was;
// 3. a program known to write the file, running now.
//
// Nothing found is said as nothing found, never as a name.
// signs are the words a writer leaves in the file's comments, and its name.
var signs = []struct{ word, name string }{
{"forti", "FortiClient"},
{"openfortivpn", "openfortivpn"},
{"networkmanager", "NetworkManager"},
{"systemd-resolved", "systemd-resolved"},
{"resolvconf", "resolvconf"},
{"dhcpcd", "dhcpcd"},
{"dhclient", "dhclient"},
{"netconfig", "netconfig"},
{"openvpn", "OpenVPN"},
{"openconnect", "OpenConnect"},
{"vpnc", "vpnc"},
{"tailscale", "Tailscale"},
{"connman", "ConnMan"},
}
// writers are the programs known to rewrite the file, as they run, and their name. The VPN clients
// first: they are what rewrites a file the machine's network manager was already told to keep off.
var writers = []struct{ comm, name string }{
{"fortivpn", "FortiClient"},
{"forticlient", "FortiClient"},
{"fctsched", "FortiClient"},
{"openfortivpn", "openfortivpn"},
{"openvpn", "OpenVPN"},
{"openconnect", "OpenConnect"},
{"vpnc", "vpnc"},
{"charon", "strongSwan"},
{"tailscaled", "Tailscale"},
{"dhclient", "dhclient"},
{"resolvconf", "resolvconf"},
}
// writerOf names who rewrote the file, and why that name, from the file's own words, a backup changed
// beside it, or a writer running.
func (j *Judge) writerOf(content string, changed time.Time) (string, string) {
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "#") && !strings.HasPrefix(line, ";") {
continue
}
lower := strings.ToLower(line)
for _, s := range signs {
if strings.Contains(lower, s.word) {
return s.name, "its own header names " + s.name
}
}
}
// A backup the writer kept beside it.
backup := ""
matches, _ := filepath.Glob(j.m.ResolvPath + "*")
for _, m := range matches {
if m == j.m.ResolvPath {
continue
}
info, err := os.Stat(m)
if err != nil || info.IsDir() {
continue
}
// A backup that names its writer says so whenever it was made: a client that renames the file
// aside (FortiClient does, on connect) leaves the backup with the old file's time, not its own.
lower := strings.ToLower(filepath.Base(m))
for _, s := range signs {
if strings.Contains(lower, s.word) {
return s.name, "it left " + m + " beside it"
}
}
if d := info.ModTime().Sub(changed); d >= -time.Minute && d <= time.Minute {
backup = m
}
}
why := "no program it could be is known"
if backup != "" {
why = backup + " was changed when it was: whoever wrote it kept a copy there"
}
runningNow := map[string]bool{}
for _, name := range j.m.Running() {
runningNow[strings.ToLower(name)] = true
}
for _, w := range writers {
if runningNow[w.comm] {
return w.name + "?", w.comm + " is running; " + why
}
}
return "", why
}
// writerOfLink names the program a link points the file at.
func writerOfLink(target string) string {
lower := strings.ToLower(target)
switch {
case strings.Contains(lower, "systemd/resolve"):
return "systemd-resolved"
case strings.Contains(lower, "resolvconf"):
return "resolvconf"
case strings.Contains(lower, "networkmanager"):
return "NetworkManager"
}
return ""
}