749 lines
22 KiB
Go
749 lines
22 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Wifx/gonetworkmanager/v3"
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/vishvananda/netlink"
|
|
)
|
|
|
|
type nmConnection struct {
|
|
ID string
|
|
Name string
|
|
UsingData bool
|
|
Method4 string
|
|
Method6 string
|
|
Addresses4 []*net.IPNet
|
|
Addresses6 []*net.IPNet
|
|
Gateway4 net.IP
|
|
Gateway6 net.IP
|
|
Routes4 []*Route
|
|
Routes6 []*Route
|
|
DNS []net.IP
|
|
DNSSearch []string
|
|
}
|
|
|
|
// dhcpState reports whether each family's DHCP client is enabled. IPv4 "auto"
|
|
// means DHCPv4. For IPv6, "auto" means router advertisements plus DHCPv6 when
|
|
// the router asks for it, and "dhcp" means DHCPv6 alone; both run a client.
|
|
func (c *nmConnection) dhcpState() (dhcp4, dhcp6 bool) {
|
|
dhcp4 = c.Method4 == "auto"
|
|
dhcp6 = c.Method6 == "auto" || c.Method6 == "dhcp"
|
|
return dhcp4, dhcp6
|
|
}
|
|
|
|
// nmStaticMethod4 is the ipv4.method to leave behind when DHCPv4 is turned off:
|
|
// "manual" when the connection still carries static addresses to serve, and
|
|
// "disabled" when turning off the lease leaves it with no IPv4 at all.
|
|
func nmStaticMethod4(hasAddrs bool) string {
|
|
if hasAddrs {
|
|
return "manual"
|
|
}
|
|
return "disabled"
|
|
}
|
|
|
|
// nmStaticMethod6 is the ipv6.method counterpart. An IPv6 interface with no
|
|
// addresses keeps its link-local one rather than losing IPv6 entirely, which is
|
|
// what "disabled" would do.
|
|
func nmStaticMethod6(hasAddrs bool) string {
|
|
if hasAddrs {
|
|
return "manual"
|
|
}
|
|
return "link-local"
|
|
}
|
|
|
|
type networkManager struct {
|
|
config gonetworkmanager.Settings
|
|
}
|
|
|
|
// nmNameservers extracts DNS server IPs from a NetworkManager ipv4/ipv6
|
|
// settings group. It prefers the modern "dns-data" property (array of strings)
|
|
// and falls back to the legacy "dns" property, which encodes IPv4 servers as an
|
|
// array of uint32 and IPv6 servers as an array of byte arrays.
|
|
func nmNameservers(group map[string]any) []net.IP {
|
|
var servers []net.IP
|
|
if data, ok := group["dns-data"].([]string); ok {
|
|
for _, s := range data {
|
|
if ip := net.ParseIP(s); ip != nil {
|
|
servers = append(servers, ip)
|
|
}
|
|
}
|
|
return servers
|
|
}
|
|
if data, ok := group["dns"].([]uint32); ok {
|
|
for _, u := range data {
|
|
if ip := uint2IP(u); len(ip) > 0 {
|
|
servers = append(servers, ip)
|
|
}
|
|
}
|
|
return servers
|
|
}
|
|
if data, ok := group["dns"].([][]byte); ok {
|
|
for _, b := range data {
|
|
if ip := net.IP(b); ip != nil {
|
|
servers = append(servers, ip)
|
|
}
|
|
}
|
|
}
|
|
return servers
|
|
}
|
|
|
|
// nmSearchDomains extracts the DNS search list from a NetworkManager ipv4/ipv6
|
|
// settings group.
|
|
func nmSearchDomains(group map[string]any) []string {
|
|
if data, ok := group["dns-search"].([]string); ok {
|
|
return append([]string(nil), data...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Parse a network manager connection settings map to get network configurations.
|
|
func (*networkManager) ParseConnection(settings gonetworkmanager.ConnectionSettings) (conn *nmConnection, err error) {
|
|
conn = new(nmConnection)
|
|
|
|
// Get the interface id.
|
|
id, ok := settings["connection"]["id"].(string)
|
|
if !ok {
|
|
err = fmt.Errorf("failed to get interface id")
|
|
return
|
|
}
|
|
conn.ID = id
|
|
|
|
// Get the interface name.
|
|
name, ok := settings["connection"]["interface-name"].(string)
|
|
if !ok {
|
|
err = fmt.Errorf("failed to get interface name")
|
|
return
|
|
}
|
|
conn.Name = name
|
|
|
|
// Get the addressing method of each family. These decide whether a DHCP
|
|
// client runs, independently of any static addresses parsed below.
|
|
conn.Method4, _ = settings["ipv4"]["method"].(string)
|
|
conn.Method6, _ = settings["ipv6"]["method"].(string)
|
|
|
|
// Get the IPv4 address map, and confirm the newer configuration style is used.
|
|
addrMap, ok := settings["ipv4"]["address-data"]
|
|
if ok {
|
|
// Update the information to show the newer configuration is used.
|
|
conn.UsingData = true
|
|
|
|
// Parse the IPv4 address data into the address list.
|
|
if addrMap != nil {
|
|
addrSlice := addrMap.([]map[string]any)
|
|
for _, addr := range addrSlice {
|
|
ip := net.ParseIP(addr["address"].(string))
|
|
prefix := addr["prefix"].(uint32)
|
|
conn.Addresses4 = append(conn.Addresses4, &net.IPNet{
|
|
IP: ip,
|
|
Mask: net.CIDRMask(int(prefix), 32),
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
// Parse the IPv4 gateway.
|
|
gateway4S, ok := settings["ipv4"]["gateway"].(string)
|
|
if ok {
|
|
conn.Gateway4 = net.ParseIP(gateway4S)
|
|
}
|
|
|
|
// Parse the IPv6 addresses.
|
|
addr6Map, ok := settings["ipv6"]["address-data"]
|
|
if ok && addr6Map != nil {
|
|
addrSlice := addr6Map.([]map[string]any)
|
|
for _, addr := range addrSlice {
|
|
ip := net.ParseIP(addr["address"].(string))
|
|
prefix := addr["prefix"].(uint32)
|
|
conn.Addresses6 = append(conn.Addresses6, &net.IPNet{
|
|
IP: ip,
|
|
Mask: net.CIDRMask(int(prefix), 128),
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
// Parse the IPv6 gateway.
|
|
gateway6S, ok := settings["ipv6"]["gateway"].(string)
|
|
if ok {
|
|
conn.Gateway6 = net.ParseIP(gateway6S)
|
|
}
|
|
|
|
// Parse the IPv4 static route data.
|
|
routeMap, ok := settings["ipv4"]["route-data"]
|
|
if ok && routeMap != nil {
|
|
routeSlice := routeMap.([]map[string]any)
|
|
for _, route := range routeSlice {
|
|
dstIP := net.ParseIP(route["dest"].(string))
|
|
prefix := route["prefix"].(uint32)
|
|
r := new(Route)
|
|
r.Destination = &net.IPNet{
|
|
IP: dstIP,
|
|
Mask: net.CIDRMask(int(prefix), 32),
|
|
}
|
|
r.Gateway = net.ParseIP(route["next-hop"].(string))
|
|
r.Metric = int(route["metric"].(uint32))
|
|
conn.Routes4 = append(conn.Routes4, r)
|
|
|
|
}
|
|
}
|
|
|
|
// Parse the IPv6 static route data.
|
|
route6Map, ok := settings["ipv6"]["route-data"]
|
|
if ok && route6Map != nil {
|
|
routeSlice := route6Map.([]map[string]any)
|
|
for _, route := range routeSlice {
|
|
dstIP := net.ParseIP(route["dest"].(string))
|
|
prefix := route["prefix"].(uint32)
|
|
r := new(Route)
|
|
r.Destination = &net.IPNet{
|
|
IP: dstIP,
|
|
Mask: net.CIDRMask(int(prefix), 128),
|
|
}
|
|
r.Gateway = net.ParseIP(route["next-hop"].(string))
|
|
r.Metric = int(route["metric"].(uint32))
|
|
conn.Routes6 = append(conn.Routes6, r)
|
|
|
|
}
|
|
}
|
|
} else {
|
|
// This is the old style configuration, we do not parse
|
|
// these unless the new style is missing.
|
|
|
|
// Get the zero IP assignment so we can ignore them for
|
|
// gateway addresses.
|
|
zeroIP := make(net.IP, 4)
|
|
zeroIP6 := make(net.IP, 16)
|
|
|
|
// Parse IPv4 address slices.
|
|
addrSlice, ok := settings["ipv4"]["addresses"].([][]uint32)
|
|
if ok {
|
|
for _, addr := range addrSlice {
|
|
gateway := uint2IP(addr[2])
|
|
if gateway != nil && !gateway.Equal(zeroIP) {
|
|
conn.Gateway4 = gateway
|
|
}
|
|
conn.Addresses4 = append(conn.Addresses4, &net.IPNet{
|
|
IP: uint2IP(addr[0]),
|
|
Mask: net.CIDRMask(int(addr[1]), 32),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Parse IPv6 address slices.
|
|
addr6Slice, ok := settings["ipv6"]["addresses"].([][]any)
|
|
if ok {
|
|
for _, addr := range addr6Slice {
|
|
gateway := net.IP(addr[2].([]byte))
|
|
if gateway != nil && !gateway.Equal(zeroIP6) {
|
|
conn.Gateway6 = gateway
|
|
}
|
|
conn.Addresses6 = append(conn.Addresses6, &net.IPNet{
|
|
IP: net.IP(addr[0].([]byte)),
|
|
Mask: net.CIDRMask(int(addr[1].(uint32)), 128),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Parse IPv4 static routes.
|
|
routeSlice, ok := settings["ipv4"]["routes"].([][]uint32)
|
|
if ok {
|
|
for _, route := range routeSlice {
|
|
r := new(Route)
|
|
r.Destination = &net.IPNet{
|
|
IP: uint2IP(route[0]),
|
|
Mask: net.CIDRMask(int(route[1]), 32),
|
|
}
|
|
r.Gateway = uint2IP(route[2])
|
|
r.Metric = int(route[3])
|
|
conn.Routes4 = append(conn.Routes4, r)
|
|
}
|
|
}
|
|
|
|
// Parse IPv6 static routes.
|
|
route6Slice, ok := settings["ipv6"]["routes"].([][]any)
|
|
if ok {
|
|
for _, route := range route6Slice {
|
|
r := new(Route)
|
|
r.Destination = &net.IPNet{
|
|
IP: net.IP(route[0].([]byte)),
|
|
Mask: net.CIDRMask(int(route[1].(uint32)), 128),
|
|
}
|
|
r.Gateway = net.IP(route[2].([]byte))
|
|
r.Metric = int(route[3].(uint32))
|
|
conn.Routes6 = append(conn.Routes6, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse DNS servers and search domains from both families. DNS is stored
|
|
// independently of the address style, so it is read for both new and old
|
|
// configurations.
|
|
if ipv4, ok := settings["ipv4"]; ok {
|
|
conn.DNS = append(conn.DNS, nmNameservers(ipv4)...)
|
|
conn.DNSSearch = append(conn.DNSSearch, nmSearchDomains(ipv4)...)
|
|
}
|
|
if ipv6, ok := settings["ipv6"]; ok {
|
|
conn.DNS = append(conn.DNS, nmNameservers(ipv6)...)
|
|
conn.DNSSearch = append(conn.DNSSearch, nmSearchDomains(ipv6)...)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// nmBusName is the well-known D-Bus name the NetworkManager daemon takes once
|
|
// it is running. Until some process owns it, there is nothing on the bus to
|
|
// answer a configuration call.
|
|
const nmBusName = "org.freedesktop.NetworkManager"
|
|
|
|
// nmBusNameOwned reports whether the NetworkManager daemon currently owns its
|
|
// bus name — the "is the socket live" question. It is asked before any property
|
|
// is read, because NetworkManager is D-Bus activatable: reading a property on
|
|
// an unowned name asks the bus to *start* the daemon. Waiting for a service to
|
|
// come up on its own must not be the thing that launches it.
|
|
func nmBusNameOwned(conn *dbus.Conn) (bool, error) {
|
|
var owned bool
|
|
err := conn.BusObject().Call("org.freedesktop.DBus.NameHasOwner", 0, nmBusName).Store(&owned)
|
|
return owned, err
|
|
}
|
|
|
|
// networkManagerReady reports whether NetworkManager is on the bus and has
|
|
// finished starting up. The two are distinct: the daemon takes its bus name
|
|
// early, then spends a while bringing up the connections it is configured to
|
|
// activate at boot. Its Startup property stays true for that window, and a
|
|
// connection modified during it can be overwritten as startup completes.
|
|
//
|
|
// A missing system bus is reported as "not ready" rather than as a hard error,
|
|
// because a host early enough in boot to beat NetworkManager can also be early
|
|
// enough to beat dbus-daemon.
|
|
func networkManagerReady() (bool, error) {
|
|
conn, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
owned, err := nmBusNameOwned(conn)
|
|
if err != nil || !owned {
|
|
return false, err
|
|
}
|
|
|
|
daemon, err := gonetworkmanager.NewNetworkManager()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
startup, err := daemon.GetPropertyStartup()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return !startup, nil
|
|
}
|
|
|
|
// newNetworkManager waits for the NetworkManager daemon to be ready, then opens
|
|
// its settings interface. Unlike the file backends, NetworkManager is
|
|
// configured through a running daemon, so a configurator built while it is
|
|
// still starting would hold a settings handle that reports no connections and
|
|
// accepts no changes.
|
|
func newNetworkManager(ctx context.Context, readyTimeout time.Duration) (nm *networkManager, err error) {
|
|
if err = waitForServiceReady(ctx, "NetworkManager", readyTimeout, networkManagerReady); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config, err := gonetworkmanager.NewSettings()
|
|
if err != nil {
|
|
return
|
|
}
|
|
nm = &networkManager{
|
|
config: config,
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get interfaces configured.
|
|
func (nm *networkManager) GetInterfaces() (interfaces []*Interface, err error) {
|
|
// Connect to netlink.
|
|
var h *netlink.Handle
|
|
h, err = netlink.NewHandle()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer h.Close()
|
|
|
|
// Get list of interfaces to match with MAC address.
|
|
var links []netlink.Link
|
|
links, err = h.LinkList()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Get connections from NM.
|
|
connections, err := nm.config.ListConnections()
|
|
|
|
// Add devices to the list.
|
|
for _, c := range connections {
|
|
// Get the settings of the connection.
|
|
var settings gonetworkmanager.ConnectionSettings
|
|
settings, err = c.GetSettings()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Parse the connection. A connection that fails to parse (for example
|
|
// a VPN or bridge-slave profile with no interface-name) is skipped,
|
|
// not fatal, so this must not touch the named err return: doing so
|
|
// would let the last connection processed decide whether the whole
|
|
// call reports an error, discarding an otherwise fully-populated
|
|
// interfaces slice depending purely on D-Bus connection ordering.
|
|
connection, parseErr := nm.ParseConnection(settings)
|
|
if parseErr != nil {
|
|
continue
|
|
}
|
|
|
|
// Find the MAC address and link.
|
|
var mac net.HardwareAddr
|
|
var foundLink netlink.Link
|
|
for _, link := range links {
|
|
if link.Attrs().Name == connection.Name {
|
|
mac = link.Attrs().HardwareAddr
|
|
foundLink = link
|
|
}
|
|
}
|
|
|
|
// Setup new interface.
|
|
i := new(Interface)
|
|
i.Name = connection.Name
|
|
i.MAC = mac
|
|
i.Link = foundLink
|
|
i.DHCP4, i.DHCP6 = connection.dhcpState()
|
|
|
|
// Append addresses.
|
|
for _, addr := range connection.Addresses4 {
|
|
i.Addresses = append(i.Addresses, addr)
|
|
}
|
|
for _, addr := range connection.Addresses6 {
|
|
i.Addresses = append(i.Addresses, addr)
|
|
}
|
|
|
|
// Add gateways.
|
|
if connection.Gateway4 != nil {
|
|
i.Gateway4 = connection.Gateway4
|
|
}
|
|
if connection.Gateway6 != nil {
|
|
i.Gateway6 = connection.Gateway6
|
|
}
|
|
|
|
// Append routes.
|
|
for _, route := range connection.Routes4 {
|
|
i.Routes = append(i.Routes, route)
|
|
}
|
|
for _, route := range connection.Routes6 {
|
|
i.Routes = append(i.Routes, route)
|
|
}
|
|
|
|
// Add DNS servers and search domains.
|
|
i.DNS = append(i.DNS, connection.DNS...)
|
|
i.SearchDomains = append(i.SearchDomains, connection.DNSSearch...)
|
|
|
|
// Add the interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Set the IP addresses on an interface.
|
|
func (nm *networkManager) SetIfaceAddresses(ctx context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) (err error) {
|
|
// Separate addresses by addr4 and addr6.
|
|
var addrs4 []string
|
|
var addrs6 []string
|
|
for _, addr := range addrs {
|
|
if addr.IP.To4() == nil {
|
|
addrs6 = append(addrs6, addr.String())
|
|
} else {
|
|
addrs4 = append(addrs4, addr.String())
|
|
}
|
|
}
|
|
|
|
// Get connections from NM.
|
|
connections, err := nm.config.ListConnections()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs []error
|
|
|
|
// Find the connections that has the interfaces.
|
|
for _, c := range connections {
|
|
// Get the settings of the connection.
|
|
var settings gonetworkmanager.ConnectionSettings
|
|
settings, err = c.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the interface name.
|
|
name, ok := settings["connection"]["interface-name"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if name != iface {
|
|
continue
|
|
}
|
|
|
|
// Get the interface id.
|
|
id, ok := settings["connection"]["id"].(string)
|
|
if !ok {
|
|
err = fmt.Errorf("failed to get interface id")
|
|
return
|
|
}
|
|
|
|
// Read the current addressing method of each family. NetworkManager
|
|
// serves static addresses alongside a lease when the method is "auto",
|
|
// so a connection already on DHCP keeps it: changing an address is not
|
|
// a request to stop using DHCP, and SetIfaceDHCP is how that is asked
|
|
// for. Only a family that is not already leasing is moved to "manual"
|
|
// (or off, when it is left with no addresses at all).
|
|
method4, _ := settings["ipv4"]["method"].(string)
|
|
method6, _ := settings["ipv6"]["method"].(string)
|
|
conn := &nmConnection{Method4: method4, Method6: method6}
|
|
dhcp4, dhcp6 := conn.dhcpState()
|
|
|
|
// Update address list for IPv4.
|
|
newMethod4 := nmStaticMethod4(len(addrs4) != 0)
|
|
if dhcp4 {
|
|
newMethod4 = "auto"
|
|
}
|
|
gateway4S := ""
|
|
if gateway4 != nil && len(addrs4) != 0 {
|
|
gateway4S = gateway4.String()
|
|
}
|
|
_, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv4.method", newMethod4, "ipv4.addresses", strings.Join(addrs4, ","), "ipv4.gateway", gateway4S)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv4.addresses on %s: %w", id, err))
|
|
}
|
|
|
|
// Update address list for IPv6.
|
|
newMethod6 := nmStaticMethod6(len(addrs6) != 0)
|
|
if dhcp6 {
|
|
newMethod6 = method6
|
|
}
|
|
gateway6S := ""
|
|
if gateway6 != nil && len(addrs6) != 0 {
|
|
gateway6S = gateway6.String()
|
|
}
|
|
_, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv6.method", newMethod6, "ipv6.addresses", strings.Join(addrs6, ","), "ipv6.gateway", gateway6S)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv6.addresses on %s: %w", id, err))
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// Set the DHCP client state on an interface.
|
|
func (nm *networkManager) SetIfaceDHCP(ctx context.Context, iface string, dhcp4, dhcp6 bool) (err error) {
|
|
// Get connections from NM.
|
|
connections, err := nm.config.ListConnections()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs []error
|
|
|
|
// Find the connections that has the interfaces.
|
|
for _, c := range connections {
|
|
// Get the settings of the connection.
|
|
var settings gonetworkmanager.ConnectionSettings
|
|
settings, err = c.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the interface name.
|
|
name, ok := settings["connection"]["interface-name"].(string)
|
|
if !ok || name != iface {
|
|
continue
|
|
}
|
|
|
|
// Get the interface id.
|
|
id, ok := settings["connection"]["id"].(string)
|
|
if !ok {
|
|
return fmt.Errorf("failed to get interface id")
|
|
}
|
|
|
|
// Parse the connection so turning a lease off can fall back to the
|
|
// method that still serves whatever static addresses it holds.
|
|
conn, parseErr := nm.ParseConnection(settings)
|
|
if parseErr != nil {
|
|
errs = append(errs, fmt.Errorf("failed to parse connection %s: %w", id, parseErr))
|
|
continue
|
|
}
|
|
|
|
// Switch the IPv4 method. "auto" is DHCPv4; NetworkManager keeps
|
|
// serving any ipv4.addresses the connection carries alongside it.
|
|
method4 := "auto"
|
|
if !dhcp4 {
|
|
method4 = nmStaticMethod4(len(conn.Addresses4) != 0)
|
|
}
|
|
if _, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv4.method", method4); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv4.method on %s: %w", id, err))
|
|
}
|
|
|
|
// Switch the IPv6 method. A connection already on "auto" is left there
|
|
// rather than forced to "dhcp": both run a DHCPv6 client, and "auto"
|
|
// additionally honors router advertisements, which turning it into
|
|
// "dhcp" would silently switch off.
|
|
method6 := "auto"
|
|
if dhcp6 && conn.Method6 == "dhcp" {
|
|
method6 = "dhcp"
|
|
} else if !dhcp6 {
|
|
method6 = nmStaticMethod6(len(conn.Addresses6) != 0)
|
|
}
|
|
if _, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv6.method", method6); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv6.method on %s: %w", id, err))
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// renewDHCP has NetworkManager reapply the connection profile to the device, so
|
|
// a client it was just told to run starts and acquires a lease. `device
|
|
// reapply` changes the device in place and, unlike `connection up`, does not
|
|
// tear the link down first.
|
|
func (nm *networkManager) renewDHCP(ctx context.Context, iface string) error {
|
|
_, err := runCommand(ctx, "nmcli", "device", "reapply", iface)
|
|
return err
|
|
}
|
|
|
|
// Set static routes to interface.
|
|
func (nm *networkManager) SetIfaceRoutes(ctx context.Context, iface string, routes []*Route) (err error) {
|
|
// Build route slices.
|
|
var routes4 []string
|
|
var routes6 []string
|
|
for _, route := range routes {
|
|
r := fmt.Sprintf("%s %s %d", route.Destination.String(), route.Gateway, route.Metric)
|
|
if route.Destination.IP.To4() == nil {
|
|
routes6 = append(routes6, r)
|
|
} else {
|
|
routes4 = append(routes4, r)
|
|
}
|
|
}
|
|
|
|
// Get connections from NM.
|
|
connections, err := nm.config.ListConnections()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs []error
|
|
|
|
// Find the connections that has the interfaces.
|
|
for _, c := range connections {
|
|
// Get the settings of the connection.
|
|
var settings gonetworkmanager.ConnectionSettings
|
|
settings, err = c.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the interface name.
|
|
name, ok := settings["connection"]["interface-name"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if name != iface {
|
|
continue
|
|
}
|
|
|
|
// Get the interface id.
|
|
id, ok := settings["connection"]["id"].(string)
|
|
if !ok {
|
|
err = fmt.Errorf("failed to get interface id")
|
|
return
|
|
}
|
|
|
|
// Update routes.
|
|
_, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv4.routes", strings.Join(routes4, ","))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv4.routes on %s: %w", id, err))
|
|
}
|
|
_, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv6.routes", strings.Join(routes6, ","))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv6.routes on %s: %w", id, err))
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// Set DNS servers and search domains on interface.
|
|
func (nm *networkManager) SetIfaceDNS(ctx context.Context, iface string, servers []net.IP, searchDomains []string) (err error) {
|
|
// Separate DNS servers by family; NetworkManager keeps them under the
|
|
// ipv4 and ipv6 settings.
|
|
var dns4 []string
|
|
var dns6 []string
|
|
for _, ip := range servers {
|
|
if ip == nil {
|
|
continue
|
|
}
|
|
if ip.To4() == nil {
|
|
dns6 = append(dns6, ip.String())
|
|
} else {
|
|
dns4 = append(dns4, ip.String())
|
|
}
|
|
}
|
|
search := strings.Join(searchDomains, ",")
|
|
|
|
// Get connections from NM.
|
|
connections, err := nm.config.ListConnections()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs []error
|
|
|
|
// Find the connections that has the interfaces.
|
|
for _, c := range connections {
|
|
// Get the settings of the connection.
|
|
var settings gonetworkmanager.ConnectionSettings
|
|
settings, err = c.GetSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the interface name.
|
|
name, ok := settings["connection"]["interface-name"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if name != iface {
|
|
continue
|
|
}
|
|
|
|
// Get the interface id.
|
|
id, ok := settings["connection"]["id"].(string)
|
|
if !ok {
|
|
err = fmt.Errorf("failed to get interface id")
|
|
return
|
|
}
|
|
|
|
// Update DNS servers and search domains for each family. Also disable
|
|
// automatic DNS from DHCP/RA so the static servers are the only
|
|
// resolvers used.
|
|
_, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv4.dns", strings.Join(dns4, ","), "ipv4.dns-search", search, "ipv4.ignore-auto-dns", "yes")
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv4.dns on %s: %w", id, err))
|
|
}
|
|
_, err = runCommand(ctx, "nmcli", "connection", "modify", id, "ipv6.dns", strings.Join(dns6, ","), "ipv6.dns-search", search, "ipv6.ignore-auto-dns", "yes")
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set ipv6.dns on %s: %w", id, err))
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|