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".
445 lines
12 KiB
Go
445 lines
12 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/Wifx/gonetworkmanager/v3"
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockNMConnection struct {
|
|
settings gonetworkmanager.ConnectionSettings
|
|
}
|
|
|
|
func (c *mockNMConnection) GetPath() dbus.ObjectPath {
|
|
return ""
|
|
}
|
|
|
|
func (c *mockNMConnection) Update(settings gonetworkmanager.ConnectionSettings) error {
|
|
return nil
|
|
}
|
|
|
|
func (c *mockNMConnection) UpdateUnsaved(settings gonetworkmanager.ConnectionSettings) error {
|
|
return nil
|
|
}
|
|
|
|
func (c *mockNMConnection) Delete() error {
|
|
return nil
|
|
}
|
|
|
|
func (c *mockNMConnection) GetSettings() (gonetworkmanager.ConnectionSettings, error) {
|
|
return c.settings, nil
|
|
}
|
|
|
|
func (c *mockNMConnection) GetSecrets(settingName string) (gonetworkmanager.ConnectionSettings, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *mockNMConnection) ClearSecrets() error {
|
|
return nil
|
|
}
|
|
|
|
func (c *mockNMConnection) Save() error {
|
|
return nil
|
|
}
|
|
|
|
func (c *mockNMConnection) GetPropertyUnsaved() (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (c *mockNMConnection) GetPropertyFlags() (uint32, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (c *mockNMConnection) GetPropertyFilename() (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (c *mockNMConnection) MarshalJSON() ([]byte, error) {
|
|
s, _ := c.GetSettings()
|
|
return json.Marshal(s)
|
|
}
|
|
|
|
type mockNMSettings struct {
|
|
}
|
|
|
|
func (s *mockNMSettings) ListConnections() ([]gonetworkmanager.Connection, error) {
|
|
var connections []gonetworkmanager.Connection
|
|
|
|
connection := &mockNMConnection{
|
|
settings: gonetworkmanager.ConnectionSettings{
|
|
"ipv4": {
|
|
"address-data": []map[string]interface{}(nil),
|
|
"dns-search": []string{},
|
|
"method": "auto",
|
|
"route-data": []map[string]interface{}(nil),
|
|
"routes": [][]uint32{},
|
|
"addresses": [][]uint32{},
|
|
"gateway": "1.2.3.1",
|
|
"route-metric": 100,
|
|
"dhcp-timeout": 45,
|
|
},
|
|
"ipv6": {
|
|
"addr-gen-mode": 3,
|
|
"address-data": []map[string]interface{}(nil),
|
|
"routes": [][]interface{}{},
|
|
"dns-search": []string{},
|
|
"method": "auto",
|
|
"route-data": []map[string]interface{}(nil),
|
|
"dhcp-timeout": 45,
|
|
"route-metric": 100,
|
|
"addresses": [][]interface{}{},
|
|
},
|
|
"proxy": {},
|
|
"connection": {
|
|
"uuid": "843f71c2-161d-46e1-9533-195bfdc8ae56",
|
|
"autoconnect-priority": 1,
|
|
"autoconnect-retries": 0,
|
|
"id": "test_eth0",
|
|
"interface-name": "test_eth0",
|
|
"permissions": []string{},
|
|
"timestamp": 1669049774,
|
|
"type": "802-3-ethernet",
|
|
},
|
|
"802-3-ethernet": {
|
|
"auto-negotiate": false,
|
|
"mac-address-blacklist": []string{},
|
|
"s390-options": map[string]string{},
|
|
},
|
|
},
|
|
}
|
|
connections = append(connections, connection)
|
|
|
|
connection = &mockNMConnection{
|
|
settings: gonetworkmanager.ConnectionSettings{
|
|
"ipv4": {
|
|
"address-data": []map[string]interface{}{
|
|
{
|
|
"address": "1.2.3.4",
|
|
"prefix": uint32(24),
|
|
},
|
|
},
|
|
"dns-search": []string{},
|
|
"method": "manual",
|
|
"route-data": []map[string]interface{}{
|
|
{
|
|
"dest": "10.0.0.0",
|
|
"prefix": uint32(24),
|
|
"next-hop": "1.2.3.5",
|
|
"metric": uint32(100),
|
|
},
|
|
},
|
|
"routes": [][]uint32{},
|
|
"addresses": [][]uint32{},
|
|
"gateway": "1.2.3.1",
|
|
"route-metric": 100,
|
|
"dhcp-timeout": 45,
|
|
},
|
|
"ipv6": {
|
|
"addr-gen-mode": 3,
|
|
"address-data": []map[string]interface{}(nil),
|
|
"routes": [][]interface{}{},
|
|
"dns-search": []string{},
|
|
"method": "auto",
|
|
"route-data": []map[string]interface{}(nil),
|
|
"dhcp-timeout": 45,
|
|
"route-metric": 100,
|
|
"addresses": [][]interface{}{},
|
|
},
|
|
"proxy": {},
|
|
"connection": {
|
|
"uuid": "843f71c2-161d-46e1-9533-195bfdc8ae56",
|
|
"autoconnect-priority": 1,
|
|
"autoconnect-retries": 0,
|
|
"id": "main",
|
|
"interface-name": "test_eth0.1556",
|
|
"permissions": []string{},
|
|
"timestamp": 1669049774,
|
|
"type": "802-3-ethernet",
|
|
},
|
|
"802-3-ethernet": {
|
|
"auto-negotiate": false,
|
|
"mac-address-blacklist": []string{},
|
|
"s390-options": map[string]string{},
|
|
},
|
|
},
|
|
}
|
|
connections = append(connections, connection)
|
|
|
|
connection = &mockNMConnection{
|
|
settings: gonetworkmanager.ConnectionSettings{
|
|
"ipv4": {
|
|
"address-data": []map[string]interface{}(nil),
|
|
"dns-search": []string{},
|
|
"method": "auto",
|
|
"route-data": []map[string]interface{}(nil),
|
|
"routes": [][]uint32{},
|
|
"addresses": [][]uint32{},
|
|
"route-metric": 100,
|
|
"dhcp-timeout": 45,
|
|
},
|
|
"ipv6": {
|
|
"addr-gen-mode": 3,
|
|
"address-data": []map[string]interface{}{
|
|
{
|
|
"address": "fc00:aa8:7160:d9eb:1:0:1:3",
|
|
"prefix": uint32(64),
|
|
},
|
|
},
|
|
"routes": [][]interface{}{},
|
|
"dns-search": []string{},
|
|
"method": "manual",
|
|
"route-data": []map[string]interface{}{
|
|
{
|
|
"dest": "fc00:5aa8:7160:d9eb::1",
|
|
"prefix": uint32(128),
|
|
"next-hop": "fe80::1",
|
|
"metric": uint32(200),
|
|
},
|
|
},
|
|
"dhcp-timeout": 45,
|
|
"route-metric": 100,
|
|
"addresses": [][]interface{}{},
|
|
},
|
|
"proxy": {},
|
|
"connection": {
|
|
"uuid": "41f45675-787c-491e-a034-0363732908f7",
|
|
"autoconnect-priority": 1,
|
|
"autoconnect-retries": 0,
|
|
"id": "vxlan",
|
|
"interface-name": "test_eth1.1557",
|
|
"permissions": []string{},
|
|
"timestamp": 1669049774,
|
|
"type": "802-3-ethernet",
|
|
},
|
|
"802-3-ethernet": {
|
|
"auto-negotiate": false,
|
|
"mac-address-blacklist": []string{},
|
|
"s390-options": map[string]string{},
|
|
},
|
|
},
|
|
}
|
|
connections = append(connections, connection)
|
|
|
|
connection = &mockNMConnection{
|
|
settings: gonetworkmanager.ConnectionSettings{
|
|
"ipv4": {
|
|
"address-data": []map[string]interface{}{
|
|
{
|
|
"address": "203.0.113.2",
|
|
"prefix": uint32(24),
|
|
},
|
|
{
|
|
"address": "203.0.113.3",
|
|
"prefix": uint32(24),
|
|
},
|
|
},
|
|
"dns-search": []string{},
|
|
"method": "manual",
|
|
"gateway": "203.0.113.1",
|
|
"route-data": []map[string]interface{}(nil),
|
|
"routes": [][]uint32{},
|
|
"addresses": [][]uint32{},
|
|
"route-metric": 100,
|
|
"dhcp-timeout": 45,
|
|
},
|
|
"ipv6": {
|
|
"addr-gen-mode": 3,
|
|
"address-data": []map[string]interface{}{
|
|
{
|
|
"address": "abcd:ef12:3456:10::4",
|
|
"prefix": uint32(64),
|
|
},
|
|
},
|
|
"routes": [][]interface{}{},
|
|
"dns-search": []string{},
|
|
"method": "manual",
|
|
"gateway": "abcd:ef12:3456:10::1",
|
|
"route-data": []map[string]interface{}(nil),
|
|
"dhcp-timeout": 45,
|
|
"route-metric": 100,
|
|
"addresses": [][]interface{}{},
|
|
},
|
|
"proxy": {},
|
|
"connection": {
|
|
"uuid": "2f2e2453-4f68-41b1-95c5-817276bbce9f",
|
|
"autoconnect-priority": 1,
|
|
"autoconnect-retries": 0,
|
|
"id": "test",
|
|
"interface-name": "test_eth2",
|
|
"permissions": []string{},
|
|
"timestamp": 1669049774,
|
|
"type": "802-3-ethernet",
|
|
},
|
|
"802-3-ethernet": {
|
|
"auto-negotiate": false,
|
|
"mac-address-blacklist": []string{},
|
|
"s390-options": map[string]string{},
|
|
},
|
|
},
|
|
}
|
|
connections = append(connections, connection)
|
|
|
|
return connections, nil
|
|
}
|
|
|
|
func (s *mockNMSettings) ReloadConnections() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *mockNMSettings) GetConnectionByUUID(uuid string) (gonetworkmanager.Connection, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *mockNMSettings) AddConnection(settings gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *mockNMSettings) AddConnectionUnsaved(settings gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *mockNMSettings) SaveHostname(hostname string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *mockNMSettings) GetPropertyHostname() (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (s *mockNMSettings) GetPropertyCanModify() (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
// Validate the ifupdown configuration parser/writer functions.
|
|
func TestNetworkManager(t *testing.T) {
|
|
// Setup test file.
|
|
tmpDir := "/tmp/networkManager-netconfig-Test"
|
|
testDir, err := filepath.Abs("./tests/networkManager")
|
|
require.NoError(t, err)
|
|
resultsDir := filepath.Join(testDir, "results")
|
|
|
|
// Update the PATH environment variable so that our nmcli is used instead of the systems.
|
|
pathEnv := os.Getenv("PATH")
|
|
os.Setenv("PATH", testDir+":"+pathEnv)
|
|
|
|
// Setup ifupdown and parse test file.
|
|
n := new(networkManager)
|
|
n.config = new(mockNMSettings)
|
|
|
|
// Get the interfaces state.
|
|
interfaces, err := n.GetInterfaces()
|
|
require.NoError(t, err)
|
|
|
|
// Verify interfaces read from file.
|
|
err = testVerifyInterfaces(interfaces, resultsDir, 1)
|
|
require.NoError(t, err)
|
|
|
|
// Test setting the IP addresses on an interface.
|
|
err = n.SetIfaceAddresses(context.Background(), "test_eth0.1556", []*net.IPNet{
|
|
{
|
|
IP: net.ParseIP("1.2.3.4"),
|
|
Mask: net.CIDRMask(24, 32),
|
|
},
|
|
{
|
|
IP: net.ParseIP("1.2.3.43"),
|
|
Mask: net.CIDRMask(24, 32),
|
|
},
|
|
{
|
|
IP: net.ParseIP("fc00::2"),
|
|
Mask: net.CIDRMask(64, 128),
|
|
},
|
|
}, net.ParseIP("1.2.3.1"), net.ParseIP("fc00::1"))
|
|
require.NoError(t, err)
|
|
|
|
// Test setting routes on an interface.
|
|
err = n.SetIfaceRoutes(context.Background(), "test_eth2", []*Route{
|
|
{
|
|
Destination: &net.IPNet{
|
|
IP: net.ParseIP("abcd:ef12:3455:10::"),
|
|
Mask: net.CIDRMask(64, 128),
|
|
},
|
|
Gateway: net.ParseIP("abcd:ef12:3456:10::1"),
|
|
Metric: 100,
|
|
},
|
|
{
|
|
Destination: &net.IPNet{
|
|
IP: net.ParseIP("10.253.2.0"),
|
|
Mask: net.CIDRMask(24, 32),
|
|
},
|
|
Gateway: net.ParseIP("203.0.113.22"),
|
|
Metric: 100,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Test setting DNS on an interface; static DNS should disable automatic DNS.
|
|
err = n.SetIfaceDNS(context.Background(), "test_eth0", []net.IP{
|
|
net.ParseIP("8.8.8.8"),
|
|
net.ParseIP("1.1.1.1"),
|
|
net.ParseIP("2001:4860:4860::8888"),
|
|
}, []string{"example.com"})
|
|
require.NoError(t, err)
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 1)
|
|
require.NoError(t, err)
|
|
|
|
// Test setting the IP addresses on an interface.
|
|
err = n.SetIfaceAddresses(context.Background(), "test_eth0", []*net.IPNet{
|
|
{
|
|
IP: net.ParseIP("1.2.10.4"),
|
|
Mask: net.CIDRMask(24, 32),
|
|
},
|
|
}, net.ParseIP("1.2.10.254"), nil)
|
|
require.NoError(t, err)
|
|
|
|
// Test setting routes on an interface.
|
|
err = n.SetIfaceRoutes(context.Background(), "test_eth0.1556", []*Route{})
|
|
require.NoError(t, err)
|
|
|
|
// Read the current file and expected state.
|
|
err = testVerifyResults(resultsDir, tmpDir, 2)
|
|
require.NoError(t, err)
|
|
|
|
// Cleanup.
|
|
err = os.RemoveAll(tmpDir)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// nmNameservers must read the modern dns-data (strings) as well as the legacy
|
|
// "dns" property (ipv4 uint32 array, ipv6 byte-array array), and nmSearchDomains
|
|
// must read dns-search. Empty groups return nothing without error.
|
|
func TestNMDNSParsing(t *testing.T) {
|
|
// Modern dns-data + dns-search, applies to either family.
|
|
group := map[string]any{
|
|
"dns-data": []string{"8.8.8.8", "1.1.1.1"},
|
|
"dns-search": []string{"a.test", "b.test"},
|
|
}
|
|
got := nmNameservers(group)
|
|
want := []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("1.1.1.1")}
|
|
assert.Truef(t, equalIPs(got, want), "dns-data = %v, want %v", got, want)
|
|
search := nmSearchDomains(group)
|
|
assert.Falsef(t, len(search) != 2 || search[0] != "a.test" || search[1] != "b.test", "dns-search = %v, want [a.test b.test]", search)
|
|
|
|
// Legacy ipv4 "dns" as []uint32 (decoded via uint2IP, matching addresses).
|
|
ipv4Legacy := map[string]any{"dns": []uint32{ip2Uint(net.ParseIP("1.2.3.4"))}}
|
|
gotV4 := nmNameservers(ipv4Legacy)
|
|
assert.Falsef(t, len(gotV4) != 1 || !gotV4[0].Equal(net.ParseIP("1.2.3.4")), "legacy ipv4 dns = %v, want [1.2.3.4]", gotV4)
|
|
|
|
// Legacy ipv6 "dns" as [][]byte.
|
|
ipv6Legacy := map[string]any{"dns": [][]byte{net.ParseIP("2001:4860:4860::8888").To16()}}
|
|
gotV6 := nmNameservers(ipv6Legacy)
|
|
assert.Falsef(t, len(gotV6) != 1 || !gotV6[0].Equal(net.ParseIP("2001:4860:4860::8888")), "legacy ipv6 dns = %v, want [2001:4860:4860::8888]", gotV6)
|
|
|
|
// Empty group yields nothing.
|
|
assert.Emptyf(t, nmNameservers(map[string]any{}), "empty group dns, want empty")
|
|
assert.Emptyf(t, nmSearchDomains(map[string]any{}), "empty group dns-search, want empty")
|
|
}
|