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 "" }