go-network-configurator/networkConfigurator.go
2026-07-01 15:17:11 -05:00

272 lines
8.5 KiB
Go

package netconfig
import (
"context"
"fmt"
"net"
"strings"
)
const (
defaultInternetTestAddress = "http://clients3.google.com/generate_204"
Public = "public-internet"
Public6 = "public-internet-6"
)
type Route struct {
Destination *net.IPNet
Gateway net.IP
Metric int
}
func (i *Route) String() string {
return fmt.Sprintf("%s via %s metric %d", i.Destination.String(), i.Gateway.String(), i.Metric)
}
type Interface struct {
Name string
MAC net.HardwareAddr
Addresses []*net.IPNet
Gateway4 net.IP
Gateway6 net.IP
Routes []*Route
DNS []net.IP
SearchDomains []string
Link any
}
func (i *Interface) String() string {
var routes []string
for _, route := range i.Routes {
routes = append(routes, route.String())
}
return fmt.Sprintf(
"Name: %s MAC: %s Addresses: %v Gateway4: %s Gateway6: %s Routes: [%s]",
i.Name,
i.MAC.String(),
i.Addresses,
i.Gateway4.String(),
i.Gateway6.String(),
strings.Join(routes, ", "),
)
}
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
}
// ifaceBackend persists interface address, route, and DNS changes to an on-disk
// network configuration backend such as netplan, cloud-init, networkd,
// NetworkManager, RHEL network-scripts, or ifupdown.
type ifaceBackend interface {
SetIfaceAddresses(ctx context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) error
SetIfaceRoutes(ctx context.Context, iface string, routes []*Route) error
SetIfaceDNS(ctx context.Context, iface string, servers []net.IP, searchDomains []string) error
}
// panelBackend persists IP changes to a hosting control panel such as
// cPanel, Plesk, or InterWorx.
type panelBackend interface {
reload(ctx context.Context) error
setMainIP(ctx context.Context, addr net.IP) error
removeIP(ctx context.Context, addr net.IP) error
}
// namedIfaceBackend pairs an ifaceBackend with a label used for logging.
type namedIfaceBackend struct {
name string
backend ifaceBackend
}
// namedPanelBackend pairs a panelBackend with a label used for logging.
type namedPanelBackend struct {
name string
backend panelBackend
}
// applyIfaceAddresses pushes the interface's address configuration to every
// registered file backend. Individual failures are logged but do not abort
// the others, since each backend writes an independent configuration file.
func applyIfaceAddresses(ctx context.Context, backends []namedIfaceBackend, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) {
for _, b := range backends {
if err := b.backend.SetIfaceAddresses(ctx, iface, addrs, gateway4, gateway6); err != nil {
logger.Printf("%s error: %v", b.name, err)
}
}
}
// applyIfaceRoutes pushes the interface's static routes to every registered
// file backend, logging but not aborting on individual errors.
func applyIfaceRoutes(ctx context.Context, backends []namedIfaceBackend, iface string, routes []*Route) {
for _, b := range backends {
if err := b.backend.SetIfaceRoutes(ctx, iface, routes); err != nil {
logger.Printf("%s error: %v", b.name, err)
}
}
}
// ifaceDNSReader is an optional capability of a file backend: reading back the
// interfaces — including their DNS servers and search domains — from the
// backend's persisted configuration. Every on-disk backend implements this via
// its existing GetInterfaces method.
type ifaceDNSReader interface {
GetInterfaces() ([]*Interface, error)
}
// ipIsIn reports whether ip is already present in addrs.
func ipIsIn(addrs []net.IP, ip net.IP) bool {
for _, a := range addrs {
if a.Equal(ip) {
return true
}
}
return false
}
// stringInSlice reports whether s is already present in list.
func stringInSlice(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
// mergeDNSFromBackends reads the DNS servers and search domains each backend
// has persisted and merges them into the matching runtime Interface (matched by
// name). The kernel/netlink layer does not track per-interface DNS, so the
// backends are the source of truth for these fields. Entries are de-duplicated
// so a host running several backends (e.g. netplan and cloud-init) does not
// list each resolver more than once. Per-backend errors are logged and do not
// abort the merge.
func mergeDNSFromBackends(backends []namedIfaceBackend, ifaces []*Interface) {
byName := make(map[string]*Interface, len(ifaces))
for _, i := range ifaces {
byName[i.Name] = i
}
for _, nb := range backends {
reader, ok := nb.backend.(ifaceDNSReader)
if !ok {
continue
}
backendIfaces, err := reader.GetInterfaces()
if err != nil {
logger.Printf("%s: error reading DNS: %v", nb.name, err)
continue
}
for _, bi := range backendIfaces {
target := byName[bi.Name]
if target == nil {
continue
}
for _, ip := range bi.DNS {
if ip == nil || ipIsIn(target.DNS, ip) {
continue
}
target.DNS = append(target.DNS, ip)
}
for _, d := range bi.SearchDomains {
if d == "" || stringInSlice(target.SearchDomains, d) {
continue
}
target.SearchDomains = append(target.SearchDomains, d)
}
}
}
}
// applyIfaceDNS pushes the interface's DNS servers and search domains to every
// registered file backend, logging but not aborting on individual errors.
func applyIfaceDNS(ctx context.Context, backends []namedIfaceBackend, iface string, servers []net.IP, searchDomains []string) {
for _, b := range backends {
if err := b.backend.SetIfaceDNS(ctx, iface, servers, searchDomains); err != nil {
logger.Printf("%s error: %v", b.name, err)
}
}
}
// reloadPanels tells every registered control panel to re-read the system's
// IP addresses, logging but not aborting on individual errors.
func reloadPanels(ctx context.Context, backends []namedPanelBackend) {
for _, b := range backends {
if err := b.backend.reload(ctx); err != nil {
logger.Printf("%s error: %v", b.name, err)
}
}
}
// setMainIPOnPanels tells every registered control panel to repoint its main
// IP to the given address, logging but not aborting on individual errors.
func setMainIPOnPanels(ctx context.Context, backends []namedPanelBackend, ip net.IP) {
for _, b := range backends {
if err := b.backend.setMainIP(ctx, ip); err != nil {
logger.Printf("%s error: %v", b.name, err)
}
}
}
// removeIPFromPanels tells every registered control panel to release an IP
// address, logging but not aborting on individual errors.
func removeIPFromPanels(ctx context.Context, backends []namedPanelBackend, ip net.IP) {
for _, b := range backends {
if err := b.backend.removeIP(ctx, ip); err != nil {
logger.Printf("%s error: %v", b.name, err)
}
}
}
// reorderPrimaryAddress returns addrs with target moved to the front, making it
// the first address of its family and therefore the primary in every backend
// (IPADDR0 / IPV6ADDR) and at runtime. The relative order of all other
// addresses is preserved, so the other address family is left untouched.
// Returns false if target is not present in addrs.
func reorderPrimaryAddress(addrs []*net.IPNet, target *net.IPNet) ([]*net.IPNet, bool) {
found := false
reordered := make([]*net.IPNet, 0, len(addrs))
for _, addr := range addrs {
if addr.IP.Equal(target.IP) {
found = true
continue
}
reordered = append(reordered, addr)
}
if !found {
return addrs, false
}
return append([]*net.IPNet{target}, reordered...), true
}
// Take a name and a list of interfaces and finds an interface by its name.
func FindInterfaceByName(name string, ifaces []*Interface) *Interface {
switch name {
case Public:
// The interface that carries the IPv4 default gateway.
for _, iface := range ifaces {
if iface.Gateway4 != nil {
return iface
}
}
case Public6:
// The interface that carries the IPv6 default gateway.
for _, iface := range ifaces {
if iface.Gateway6 != nil {
return iface
}
}
default:
// An interface with the specified name.
for _, iface := range ifaces {
if iface.Name == name {
return iface
}
}
}
return nil
}