netconfig

package module
v0.0.0-...-2ddcecb Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 31 Imported by: 0

README

go-network-configurator

Go Reference

A Go library for inspecting and changing a host's IP addresses and static routes at runtime and persisting those changes to whatever on-disk network configuration backend the system actually uses — so the change survives a reboot.

It is designed for hosting environments where an automated system needs to add, swap, or remove IPs on a server without knowing in advance whether that server is running netplan, systemd-networkd, NetworkManager, RHEL network-scripts, ifupdown, or cloud-init — and without leaving a control panel (cPanel, Plesk, InterWorx) out of sync.

import "github.com/grmrgecko/go-network-configurator"

How it works

A single Configurator applies every change in two layers:

  1. Runtime — the live kernel state is changed immediately (via netlink on Linux, the IP Helper API on Windows). Address and gateway changes are verified against an internet-reachability test and rolled back automatically if connectivity is lost.
  2. Persistence — the same change is written to every detected configuration backend so it survives a reboot, and registered control panels are told to re-read the system's IPs.

Backends are auto-detected at construction time. A host running both netplan and cloud-init, for example, has both files kept in sync; a failure writing one backend is logged but does not abort the others.

Supported backends

Network configuration (persistence)

Backend Detection
netplan netplan binary on PATH, and netplan info succeeds
cloud-init cloud-init network config present
NetworkManager NetworkManager service running or enabled, or its pid file present
systemd-networkd systemd-networkd service running or enabled
RHEL network-scripts network service running or enabled
ifupdown /etc/network/interfaces, and the networking service running or enabled

Control panels

Panel Detection
cPanel /usr/local/cpanel/bin/whmapi1
Plesk /usr/sbin/plesk
InterWorx /usr/bin/nodeworx
How service-managed backends are detected

A service counts as managing the network when it is running now or enabled to start at boot. Enablement matters on its own: this library writes configuration that must survive a reboot, and a manager that is enabled but not yet started still owns the network after the next boot.

Platforms

  • Linux — full support (all backends and panels above).
  • Windows — runtime changes via the IP Helper API, persisted to cloud-init, with Plesk panel support.

API

type Configurator interface {
    // Enumerate interfaces with their addresses, gateways, static routes,
    // DNS servers, search domains, and DHCP client state.
    GetInterfaces(ctx context.Context) ([]*Interface, error)

    // Add an IP address (optionally setting/replacing the default gateway).
    AddAddress(ctx context.Context, iface string, addr *net.IPNet, gateway net.IP) error

    // Promote an address already on the interface to be the primary of its family.
    SetPrimaryAddress(ctx context.Context, iface string, addr *net.IPNet) error

    // Remove an IP address.
    RemoveAddress(ctx context.Context, iface string, addr *net.IPNet) error

    // Add or remove a static route.
    AddRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP, metric int) error
    RemoveRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP) error

    // Set the DNS servers and search domains for an interface.
    SetDNS(ctx context.Context, iface string, servers []net.IP, searchDomains []string) error

    // Turn each address family's DHCP client on or off.
    SetDHCP(ctx context.Context, iface string, dhcp4, dhcp6 bool) error
}

// Construct the configurator for the current OS, auto-detecting backends.
// ctx bounds the detection; behaviour is tuned with Option values (see
// Options below).
func NewConfigurator(ctx context.Context, opts ...Option) (Configurator, error)

// Resolve an interface by name, or by the special "public-internet" /
// "public-internet-6" selectors.
func FindInterfaceByName(name string, ifaces []*Interface) *Interface

// Filter to the hardware-backed interfaces, best candidate for internet
// configuration first.
func FindPhysicalInterfaces(ifaces []*Interface) []*Interface

Every operation takes a context.Context. The slow steps — the ICMP probe of a new gateway and the internet-reachability test — honour cancellation and deadlines, so a caller can bound how long a change may block.

DNS

SetDNS applies an interface's resolvers and search domains to the live resolver so they take effect immediately, and persists them through whichever network management backends are detected on the host so they survive a reboot.

DHCP

SetDHCP turns each address family's DHCP client on or off independently — moving an interface to DHCPv4 while keeping a static IPv6 address is SetDHCP(ctx, "eth0", true, false). Enabling a family acquires a lease now through the detected manager; disabling only rewrites the configuration, leaving an existing lease to expire so a caller connected over the leased address is not cut off. AddAddress never changes DHCP state — it adds static addresses only. Interface.DHCP4 and Interface.DHCP6 report the state back.

Logging

The package uses a single, package-wide logger for non-fatal diagnostics from backends and control panels. It defaults to the logrus standard logger. Replace it with SetLogger before constructing any Configurator:

netconfig.SetLogger(myLogger) // anything with Printf/Println, e.g. *log.Logger
Options

NewConfigurator accepts functional options:

c, err := netconfig.NewConfigurator(ctx,
    netconfig.WithTestAddress("http://my-canary/health"), // connectivity-test URL
    netconfig.WithConnectivityCheck(false),                // skip the post-change probe + rollback
    netconfig.WithSkipConnectivityCheck(),                 // same as WithConnectivityCheck(false)
    netconfig.WithConnectivityTimeout(5 * time.Second),    // HTTP reachability timeout
    netconfig.WithPingCount(3),                            // ICMP probes per ping test
    netconfig.WithPingTimeout(10 * time.Second),           // overall ping-run timeout
    netconfig.WithBackupRetention(10),                     // .bak.* copies kept per config file (0 = keep all)
    netconfig.WithAllowPrimaryRemoval(true),               // let RemoveAddress remove the primary IP
    netconfig.WithSkipPanels(true),                        // skip all panel interaction (add/primary/remove)
    netconfig.WithAllowNoBackends(true),                   // don't fail when no persistence backend is detected
    netconfig.WithServiceReadyTimeout(90 * time.Second),   // how long to wait for a daemon-backed backend (default 60s, 0 = don't wait)
)

WithConnectivityCheck(false) (or the convenience WithSkipConnectivityCheck()) is useful on hosts with no outbound internet access, where the default reachability test would always fail and roll changes back. WithConnectivityTimeout, WithPingCount, and WithPingTimeout tune the probes used after gateway changes. WithBackupRetention limits how many .bak.<timestamp> copies each backend keeps per original config file; the oldest backups beyond this number are pruned on every save.

WithAllowPrimaryRemoval(true) overrides the default refusal to remove an address that is the primary of its family. The intended safe flow is to call SetPrimaryAddress on another address first and then remove the old one; enable the override only when you are deliberately tearing down the current primary.

WithSkipPanels(true) skips all control-panel interaction: AddAddress, SetPrimaryAddress, and RemoveAddress do not tell cPanel, Plesk, or InterWorx to reload, set the main IP, or release an IP. Only the running system and the network-manager configuration files are changed.

Choosing an interface

FindInterfaceByName accepts two well-known selectors in addition to a literal interface name:

  • "public-internet" — the interface that carries the IPv4 default gateway.
  • "public-internet-6" — the interface that carries the IPv6 default gateway.

Those answer "which interface is already on the internet". When the host is not on the internet yet — a freshly provisioned VM, an image whose NIC name is not known in advance — FindPhysicalInterfaces answers "which interface should be". It drops the devices that should never carry a public address (bridges, bonds, VLANs, tunnels, container veth pairs) and returns what is left in the order a caller should try them:

  1. Interfaces that are up, before those that are down.
  2. Interfaces already carrying a default gateway, IPv4 ahead of IPv6-only.
  3. Wired ahead of wireless.
  4. By name as a person reads it, so eth0 precedes eth1 and eth2 precedes eth10.

Paravirtual NICs (virtio, vmxnet, Xen) count as physical: a VM's only real interface is still the one to configure. The result is a ranking, not a decision — a caller with a further requirement, such as an interface not already holding a public address, filters the slice and takes the first survivor:

ifaces, err := c.GetInterfaces(ctx)
for _, iface := range netconfig.FindPhysicalInterfaces(ifaces) {
    if !hasPublicAddress(iface) {
        return iface.Name
    }
}

Interface.Up and Interface.Physical carry the two facts this ranking rests on, and are readable on their own. Both describe the running system, so they are only set by GetInterfaces.

Usage

package main

import (
    "context"
    "log"
    "net"

    netconfig "github.com/grmrgecko/go-network-configurator"
)

func main() {
    ctx := context.Background()

    c, err := netconfig.NewConfigurator(ctx)
    if err != nil {
        log.Fatal(err)
    }

    ifaces, err := c.GetInterfaces(ctx)
    if err != nil {
        log.Fatal(err)
    }
    iface := netconfig.FindInterfaceByName(netconfig.Public, ifaces)
    if iface == nil {
        log.Fatal("no public interface found")
    }

    // Swap the primary IP without dropping connectivity:
    // add a new address, switch the system over to it, then remove the old one.
    _, newIP, _ := net.ParseCIDR("203.0.113.20/24")
    _, oldIP, _ := net.ParseCIDR("203.0.113.10/24")

    if err := c.AddAddress(ctx, iface.Name, newIP, nil); err != nil {
        log.Fatal(err)
    }
    if err := c.SetPrimaryAddress(ctx, iface.Name, newIP); err != nil {
        log.Fatal(err)
    }
    if err := c.RemoveAddress(ctx, iface.Name, oldIP); err != nil {
        log.Fatal(err)
    }
}

The primary address is the first address listed on an interface. SetPrimaryAddress reorders an interface's addresses so the chosen IP is first within its family, both in the running kernel and in the written configuration. The address must already be present on the interface (compose it with AddAddress); IPv4 and IPv6 each have their own primary.

Safety

Operations that can break connectivity are guarded:

  • Adding an IP that already answers ping is refused.
  • After an address/gateway change, an internet-reachability test runs; if it fails, the previous runtime state (addresses and default route) is restored and the operation returns an error.
  • Removing an address that is the only route to the default gateway is refused.
  • Removing the primary address of its family is refused by default (use WithAllowPrimaryRemoval(true) to override). The safe flow is to promote another address with SetPrimaryAddress first, then remove the old one.

Most operations require elevated privileges (root on Linux, Administrator on Windows) to modify network state.

Development

go test ./...

Some tests exercise real kernel networking inside a throwaway network namespace and are skipped unless run as root:

sudo go test ./...

Documentation

Index

Constants

View Source
const (
	Public  = "public-internet"
	Public6 = "public-internet-6"
)

Variables

This section is empty.

Functions

func SetLogger

func SetLogger(l Logger)

SetLogger replaces the package-wide logger used for non-fatal diagnostics. It is package scoped (not a per-Configurator option) because the configuration backends and control panels log independently of any single Configurator instance; the setting therefore applies to every Configurator in the process. A nil logger is ignored.

Types

type Configurator

type Configurator interface {
	GetInterfaces(ctx context.Context) ([]*Interface, error)
	AddAddress(ctx context.Context, iface string, addr *net.IPNet, gateway net.IP) error
	SetPrimaryAddress(ctx context.Context, iface string, addr *net.IPNet) error
	RemoveAddress(ctx context.Context, iface string, addr *net.IPNet) error
	AddRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP, metric int) error
	RemoveRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP) error
	SetDNS(ctx context.Context, iface string, servers []net.IP, searchDomains []string) error

	// SetDHCP turns each address family's DHCP client on or off. Both families
	// are stated explicitly, so a caller moving an interface to DHCPv4 while
	// keeping a static IPv6 address passes (true, false).
	//
	// Adding a static address does not imply disabling DHCP: every backend
	// except ifupdown can carry static addresses alongside a lease, and which
	// of the two the operator wants is not something AddAddress can infer.
	// SetDHCP is how that choice is made.
	//
	// Enabling a family also asks the running system to acquire a lease now,
	// rather than at the next reboot. Disabling one only rewrites the
	// configuration: an interface's existing lease is left in place until it
	// expires or the network is reconfigured, so the call cannot strand a
	// caller that is connected over the leased address.
	SetDHCP(ctx context.Context, iface string, dhcp4, dhcp6 bool) error
}

func NewConfigurator

func NewConfigurator(ctx context.Context, opts ...Option) (Configurator, error)

Returns the linux network configurator. Backends are auto-detected; ctx bounds that detection, which queries systemd over D-Bus and may shell out to chkconfig and netplan. Behaviour is tuned with Option values such as WithTestAddress and WithConnectivityCheck; use SetLogger to replace the package-wide logger.

type Interface

type Interface struct {
	Name      string
	MAC       net.HardwareAddr
	Addresses []*net.IPNet
	Gateway4  net.IP
	Gateway6  net.IP
	Routes    []*Route
	DNS       []net.IP

	// Up reports whether the interface can carry traffic right now, and
	// Physical whether it is backed by a network device rather than created by
	// the kernel or the hypervisor host — a bridge, bond, VLAN, tunnel, or the
	// veth pair of a container is not physical, a virtio or vmxnet NIC is.
	// Both describe the running system and are only set by GetInterfaces; the
	// Interface values the configuration backends read back leave them false.
	Up       bool
	Physical bool

	// DHCP4 and DHCP6 report whether the interface runs a DHCP client for that
	// address family. They describe the persisted configuration, not the
	// running system: the kernel cannot be asked whether an address arrived
	// from a lease, so these are read back from the configuration backends the
	// same way DNS is. An interface can run a DHCP client and still carry
	// static addresses of the same family.
	DHCP4 bool
	DHCP6 bool

	SearchDomains []string
	Link          any
}

func FindInterfaceByName

func FindInterfaceByName(name string, ifaces []*Interface) *Interface

Take a name and a list of interfaces and finds an interface by its name.

func FindPhysicalInterfaces

func FindPhysicalInterfaces(ifaces []*Interface) []*Interface

FindPhysicalInterfaces returns the host's physical interfaces — leaving out the bridges, bonds, VLANs, tunnels, and container veth pairs that should never be handed a public address — ordered so the interface most likely to be the one a caller wants to configure for internet access comes first.

Interfaces that are up sort ahead of those that are down, then those already carrying a default gateway (IPv4 ahead of IPv6-only), then wired ahead of wireless, and finally by name read the way a human reads it, so eth0 comes before eth1 and eth2 before eth10.

This is a ranking rather than a decision: a caller looking for an interface that meets some further requirement — one not already holding a public address, say — filters the returned slice and takes the first survivor.

func (*Interface) String

func (i *Interface) String() string

type Logger

type Logger interface {
	Printf(format string, args ...interface{})
	Println(args ...interface{})
}

Logger is the minimal logging surface used across the package. It is satisfied by the standard library *log.Logger, logrus, and most other logging libraries, so callers can plug in their own logger via SetLogger.

type Option

type Option func(*configOptions)

Option configures a Configurator constructed with NewConfigurator.

func WithAllowNoBackends

func WithAllowNoBackends(allowed bool) Option

WithAllowNoBackends lets NewConfigurator return a configurator on a Linux host where backend detection found no network configuration backend. By default that is an error: netlink would apply an address change to the running system, but with nothing to write it to the change would not survive a reboot, and the caller would never be told. Enable this when the configurator is only used to read state (GetInterfaces), or when the caller accepts runtime-only changes.

func WithAllowPrimaryRemoval

func WithAllowPrimaryRemoval(allowed bool) Option

WithAllowPrimaryRemoval permits RemoveAddress to remove an address that is the primary of its family on its interface. By default removing the primary is refused so a caller cannot accidentally change the system's source address or leave a control panel without a main IP; the intended flow is to SetPrimaryAddress on another IP first. Enable this only when you are deliberately tearing down the current primary.

func WithBackupRetention

func WithBackupRetention(n int) Option

WithBackupRetention sets how many .bak.* copies are kept per original configuration file. The oldest backups beyond this number are pruned each time a backend saves. Pass 0 (or a negative value) to disable pruning and keep every backup.

func WithConnectivityCheck

func WithConnectivityCheck(enabled bool) Option

WithConnectivityCheck enables or disables the post-change internet reachability test (and the automatic rollback that depends on it). It is enabled by default; pass false to skip it, for example on hosts with no outbound internet access.

func WithConnectivityTimeout

func WithConnectivityTimeout(d time.Duration) Option

WithConnectivityTimeout sets the timeout for the HTTP reachability test run after an address or gateway change. A smaller value fails faster on a black-holed route; a larger value tolerates slower links.

func WithPingCount

func WithPingCount(n int) Option

WithPingCount sets the number of ICMP probes sent when confirming that a candidate address or gateway is reachable. More probes are more reliable on lossy links but take longer.

func WithPingTimeout

func WithPingTimeout(d time.Duration) Option

WithPingTimeout sets the overall timeout for a single ping run.

func WithServiceReadyTimeout

func WithServiceReadyTimeout(timeout time.Duration) Option

WithServiceReadyTimeout bounds how long NewConfigurator waits for a backend whose configuration is made through a daemon rather than a file. Today that is NetworkManager: it is configured over D-Bus, so a configurator built before the daemon owns its bus name would find no connections to change.

This matters at boot. A host that starts this program from a unit ordered alongside NetworkManager, rather than after it, reaches backend detection while the daemon is still starting; without the wait it would either fail to register the backend at all, or register one that reports an empty interface list. The wait ends as soon as the daemon answers and reports it has finished starting up, so a host where it is already running pays nothing.

A timeout of zero or less does not wait: detection probes once and proceeds, which is the right choice for a caller that has already ordered itself after the daemon and would rather fail fast than block.

func WithSkipConnectivityCheck

func WithSkipConnectivityCheck() Option

WithSkipConnectivityCheck is a convenience option that disables the post-change internet reachability test and its automatic rollback. It is equivalent to WithConnectivityCheck(false).

func WithSkipPanels

func WithSkipPanels(skip bool) Option

WithSkipPanels makes the configurator skip all control-panel backends (cPanel, Plesk, InterWorx) during mutating operations. When enabled, AddAddress, SetPrimaryAddress, and RemoveAddress do not tell the panels to reload, set the main IP, or release an IP; only the running system and the network-manager configuration files are changed. This is intended for maintenance paths where the panel must be left untouched, or where the panel is expected to be reconciled separately.

func WithTestAddress

func WithTestAddress(addr string) Option

WithTestAddress overrides the URL used for the post-change connectivity test. The two sentinel values "test_success" and "test_fail" force the test to pass or fail without performing a request, which is useful in tests.

type Route

type Route struct {
	Destination *net.IPNet
	Gateway     net.IP
	Metric      int
}

func (*Route) String

func (i *Route) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL