Bring an administratively down interface up in AddAddress before writing its addresses and routes. The kernel installs an address's connected route only while its link is up, so adding a gateway to a down interface was rejected as unreachable. A link raised this way is part of the pre-change state AddAddress restores, so it is returned to down on any path that does not complete. Also fix the rollback masking its own error: deleting the address it added takes the default route with it, since that route resolved its nexthop through the address's connected subnet. The following RouteDel then answered ESRCH and was reported in place of "aborted operation due to loss of internet".
267 lines
10 KiB
Go
267 lines
10 KiB
Go
//go:build linux
|
|
|
|
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
dbus "github.com/coreos/go-systemd/dbus"
|
|
)
|
|
|
|
// rcRunlevels are the SysV runlevels a network service can be started in.
|
|
// Runlevels 0, 1, and 6 are halt, single-user, and reboot, so nothing is
|
|
// registered to start there.
|
|
var rcRunlevels = []string{"2", "3", "4", "5"}
|
|
|
|
// systemdActive reports whether systemd is the running init (/run/systemd/system
|
|
// exists), the same marker systemctl uses to decide it can reach the manager.
|
|
// It is checked before dialing D-Bus so hosts running Upstart (Ubuntu 14.04) or
|
|
// SysVinit (CentOS 5/6) skip a connection that cannot usefully succeed.
|
|
func systemdActive() bool {
|
|
_, err := os.Stat("/run/systemd/system")
|
|
return err == nil
|
|
}
|
|
|
|
// commandExists reports whether name resolves on PATH.
|
|
func commandExists(name string) bool {
|
|
_, err := exec.LookPath(name)
|
|
return err == nil
|
|
}
|
|
|
|
// chkconfigOn reports whether `chkconfig --list name` shows any runlevel on,
|
|
// the RHEL-family enablement signal. Returns false when chkconfig is absent.
|
|
func chkconfigOn(ctx context.Context, name string) bool {
|
|
if !commandExists("chkconfig") {
|
|
return false
|
|
}
|
|
results, err := runCommand(ctx, "chkconfig", "--list", name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, line := range results {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 || fields[0] != name {
|
|
continue
|
|
}
|
|
for _, f := range fields[1:] {
|
|
if _, status, found := strings.Cut(f, ":"); found && status == "on" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// rcRunlevelDirs returns the rcN.d directories under root that a start symlink
|
|
// can live in, covering both the Debian/Ubuntu layout (/etc/rcN.d) and the
|
|
// Slackware one (/etc/rc.d/rcN.d).
|
|
func rcRunlevelDirs(root string) []string {
|
|
dirs := make([]string, 0, len(rcRunlevels)*2)
|
|
for _, rl := range rcRunlevels {
|
|
dirs = append(dirs,
|
|
filepath.Join(root, "etc", "rc"+rl+".d"),
|
|
filepath.Join(root, "etc", "rc.d", "rc"+rl+".d"),
|
|
)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
// rcSymlinksOn reports whether an S*name start symlink exists in any rcN.d tree,
|
|
// the enablement signal for both update-rc.d (Debian/Ubuntu) and Slackware.
|
|
func rcSymlinksOn(name string) bool {
|
|
return rcSymlinksOnIn("/", name)
|
|
}
|
|
|
|
// rcSymlinksOnIn is rcSymlinksOn rooted at root, so tests can build a runlevel
|
|
// tree in a temporary directory.
|
|
func rcSymlinksOnIn(root, name string) bool {
|
|
for _, dir := range rcRunlevelDirs(root) {
|
|
if matches, _ := filepath.Glob(filepath.Join(dir, "S*"+name)); len(matches) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// openrcOn reports whether name is linked into an OpenRC runlevel directory
|
|
// (default or boot), the enablement signal created by `rc-update add` on Gentoo.
|
|
func openrcOn(name string) bool {
|
|
return openrcOnIn("/", name)
|
|
}
|
|
|
|
// openrcOnIn is openrcOn rooted at root, so tests can build a runlevel tree in a
|
|
// temporary directory.
|
|
func openrcOnIn(root, name string) bool {
|
|
for _, rl := range []string{"default", "boot"} {
|
|
if _, err := os.Lstat(filepath.Join(root, "etc", "runlevels", rl, name)); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sysvServiceEnabled reports whether name is registered to start at boot under
|
|
// a SysV-family init system: chkconfig (RHEL), update-rc.d rcN.d start symlinks
|
|
// (Debian/Ubuntu/Slackware), or an OpenRC runlevel (Gentoo). name is the service
|
|
// base name without a ".service" suffix. Any one mechanism reporting it on
|
|
// counts as enabled.
|
|
func sysvServiceEnabled(ctx context.Context, name string) bool {
|
|
return chkconfigOn(ctx, name) || rcSymlinksOn(name) || openrcOn(name)
|
|
}
|
|
|
|
// initSystemDiscoverable reports whether this host exposes any init system that
|
|
// sysvServiceEnabled or systemd can be asked about. It is false on minimal
|
|
// images and containers that have neither systemd nor chkconfig nor runlevel
|
|
// directories, where a backend's configuration file is the only evidence
|
|
// available and detection has nothing to corroborate it with.
|
|
func initSystemDiscoverable() bool {
|
|
if systemdActive() || commandExists("chkconfig") {
|
|
return true
|
|
}
|
|
for _, dir := range rcRunlevelDirs("/") {
|
|
if _, err := os.Stat(dir); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
_, err := os.Stat("/etc/runlevels")
|
|
return err == nil
|
|
}
|
|
|
|
// networkServiceUnits are the services backend detection asks systemd about.
|
|
// They are collected here so the unit-file lookup can be filtered server side
|
|
// to just these names, which is orders of magnitude cheaper than listing every
|
|
// unit file on the host. Only these names may be passed to initState.enabled
|
|
// and initState.detected; any other name silently skips the systemd answer and
|
|
// falls through to the SysV mechanisms.
|
|
var networkServiceUnits = []string{"systemd-networkd", "NetworkManager", "network", "networking"}
|
|
|
|
// initState is a snapshot of what the host's init system reports about its
|
|
// units, taken once when the configurator is constructed. Each systemd query
|
|
// costs a D-Bus round trip, so the running units and the installed unit files
|
|
// are both fetched up front and every per-service question is answered from
|
|
// memory afterwards.
|
|
type initState struct {
|
|
// active holds the unit names ("NetworkManager.service") systemd reports as
|
|
// currently running. Empty when systemd could not be queried.
|
|
active map[string]bool
|
|
// unitFiles maps an installed unit file's base name to its UnitFileState
|
|
// ("enabled", "disabled", "generated", ...). A unit that is enabled but was
|
|
// never loaded this boot appears here and not in active, which is why both
|
|
// are read. Empty when systemd could not be queried.
|
|
unitFiles map[string]string
|
|
}
|
|
|
|
// newInitState queries systemd for the host's running units and installed unit
|
|
// files. Every step degrades to an empty map rather than an error: a host that
|
|
// cannot answer over D-Bus is not broken, it is a host whose services must be
|
|
// discovered through the SysV mechanisms instead, which enabled falls back to.
|
|
// The queries are bound by ctx so a wedged systemd cannot stall construction.
|
|
func newInitState(ctx context.Context) *initState {
|
|
s := &initState{
|
|
active: make(map[string]bool),
|
|
unitFiles: make(map[string]string),
|
|
}
|
|
|
|
// Skip D-Bus entirely when systemd is not the running init. On Ubuntu 14.04
|
|
// Upstart is PID 1 and org.freedesktop.systemd1 is served by systemd-shim,
|
|
// which accepts the connection but has no ListUnits; on CentOS 5/6 there is
|
|
// no systemd1 on the bus at all. Both would otherwise cost a failed dial and
|
|
// a failed call before reaching the SysV checks that were always going to
|
|
// answer for them.
|
|
if !systemdActive() {
|
|
return s
|
|
}
|
|
|
|
conn, err := dbus.NewWithContext(ctx)
|
|
if err != nil {
|
|
logger.Printf("unable to connect to systemd, falling back to init-script detection: %v", err)
|
|
return s
|
|
}
|
|
defer conn.Close()
|
|
|
|
units, err := conn.ListUnitsContext(ctx)
|
|
if err != nil {
|
|
logger.Printf("unable to list systemd units, falling back to init-script detection: %v", err)
|
|
}
|
|
for _, unit := range units {
|
|
if unit.ActiveState == "active" {
|
|
s.active[unit.Name] = true
|
|
}
|
|
}
|
|
|
|
// The unit files report enablement, including for units that are enabled but
|
|
// have not been loaded this boot and so never appear in ListUnits.
|
|
//
|
|
// ListUnitFilesByPatterns filters server side and answers in milliseconds;
|
|
// ListUnitFiles walks every unit file on the host and can take seconds on a
|
|
// machine with a few hundred of them. Only systemd 230 and later export the
|
|
// filtered call, so fall back to the full listing for older systemd (CentOS
|
|
// 7 ships 219). Both are read into the same map keyed by unit file name.
|
|
patterns := make([]string, 0, len(networkServiceUnits))
|
|
for _, name := range networkServiceUnits {
|
|
patterns = append(patterns, name+".service")
|
|
}
|
|
files, err := conn.ListUnitFilesByPatternsContext(ctx, nil, patterns)
|
|
if err != nil {
|
|
files, err = conn.ListUnitFilesContext(ctx)
|
|
}
|
|
if err != nil {
|
|
logger.Printf("unable to list systemd unit files, falling back to init-script detection: %v", err)
|
|
return s
|
|
}
|
|
for _, uf := range files {
|
|
s.unitFiles[filepath.Base(uf.Path)] = uf.Type
|
|
}
|
|
return s
|
|
}
|
|
|
|
// running reports whether systemd has name's service unit active right now.
|
|
func (s *initState) running(name string) bool {
|
|
return s.active[name+".service"]
|
|
}
|
|
|
|
// enabled reports whether name is registered to start at boot. systemd's
|
|
// UnitFileState is authoritative when it owns the unit outright; a "generated"
|
|
// state is not, because systemd-sysv-generator wraps an /etc/init.d script into
|
|
// a unit that carries no enablement of its own. RHEL 7 ships network.service
|
|
// that way whether or not chkconfig has it on, so a generated unit's real
|
|
// enablement is read from its SysV registration, as is any unit systemd has
|
|
// never heard of.
|
|
func (s *initState) enabled(ctx context.Context, name string) bool {
|
|
state, known := s.unitFiles[name+".service"]
|
|
if known && state != "generated" {
|
|
return state == "enabled" || state == "enabled-runtime"
|
|
}
|
|
return sysvServiceEnabled(ctx, name)
|
|
}
|
|
|
|
// detected reports whether name's service manages this host's network: either
|
|
// it is running now, or it is registered to start at boot. Enablement counts
|
|
// independently of the running state because this package writes configuration
|
|
// that must survive a reboot. A manager that is enabled but not yet started —
|
|
// a host mid-provision, a chroot, an image build, a unit whose start failed —
|
|
// still owns the network on the next boot, and systemd's ListUnits does not
|
|
// report it at all.
|
|
func (s *initState) detected(ctx context.Context, name string) bool {
|
|
return s.running(name) || s.enabled(ctx, name)
|
|
}
|
|
|
|
// networkManagerRunning reports whether NetworkManager's daemon left a runtime
|
|
// pid file. It is the running-state signal on hosts where systemd cannot be
|
|
// queried, and is unioned with the systemd signals rather than replacing them,
|
|
// so a partial answer from systemd never hides a manager this can still see.
|
|
func networkManagerRunning() bool {
|
|
for _, pidFile := range []string{
|
|
"/var/run/NetworkManager/NetworkManager.pid",
|
|
"/run/NetworkManager/NetworkManager.pid",
|
|
} {
|
|
if _, err := os.Stat(pidFile); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|