github.com/simpleiot/simpleiot@v0.18.3/client/network-manager-conn.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  package client
     5  
     6  import (
     7  	"encoding/binary"
     8  	"math/bits"
     9  	"net"
    10  	"reflect"
    11  	"strings"
    12  
    13  	nm "github.com/Wifx/gonetworkmanager/v2"
    14  )
    15  
    16  // NetworkManagerConn defines a NetworkManager connection
    17  type NetworkManagerConn struct {
    18  	ID          string `node:"id"` // matches UUID in NetworkManager
    19  	Parent      string `node:"parent"`
    20  	Description string `point:"description"` // matches ID in NetworkManager
    21  	// Type is one of the NetworkManager connection types (i.e. 802-3-ethernet)
    22  	// See https://developer-old.gnome.org/NetworkManager/stable/
    23  	Type string `point:"type"`
    24  	// Managed flag indicates that SimpleIoT is managing this connection.
    25  	// All connections in NetworkManager are added to the SIOT tree, but if a
    26  	// connection is flagged "managed", the SIOT tree is used as the source of
    27  	// truth, and settings are synchronized one-way from SIOT to NetworkManager.
    28  	Managed             bool   `point:"managed"`
    29  	AutoConnect         bool   `point:"autoConnect"`
    30  	AutoConnectPriority int32  `point:"autoConnectPriority"`
    31  	InterfaceName       string `point:"interfaceName"`
    32  	// LastActivated is the timestamp the connection was last activated in
    33  	// seconds since the UNIX epoch. Called "timestamp" in NetworkManager.
    34  	LastActivated uint64     `point:"lastActivated"`
    35  	IPv4Config    IPv4Config `point:"ipv4Config"`
    36  	IPv6Config    IPv6Config `point:"ipv6Config"`
    37  	WiFiConfig    WiFiConfig `point:"wiFiConfig"`
    38  	// Error contains an error message from the last NetworkManager sync or an
    39  	// empty string if sync was successful
    40  	Error string `point:"error"`
    41  }
    42  
    43  // WiFiConfig defines 802.11 wireless configuration
    44  type WiFiConfig struct {
    45  	SSID string `point:"ssid"`
    46  	// From NetworkManager: Key management used for the connection. One of
    47  	// "none" (WEP), "ieee8021x" (Dynamic WEP), "wpa-none" (Ad-Hoc WPA-PSK),
    48  	// "wpa-psk" (infrastructure WPA-PSK), "sae" (SAE) or "wpa-eap"
    49  	// (WPA-Enterprise). This property must be set for any Wi-Fi connection that
    50  	// uses security.
    51  	KeyManagement string `point:"keyManagement"`
    52  	PSK           string `point:"psk"`
    53  }
    54  
    55  // ResolveNetworkManagerConn returns a NetworkManagerConn from D-Bus settings
    56  // Note: Secrets must be added to the connection manually
    57  func ResolveNetworkManagerConn(settings nm.ConnectionSettings) NetworkManagerConn {
    58  	sc := settings["connection"]
    59  	conn := NetworkManagerConn{
    60  		ID:          sc["uuid"].(string),
    61  		Description: sc["id"].(string),
    62  		Type:        sc["type"].(string),
    63  		AutoConnect: true,
    64  	}
    65  	if val, ok := sc["autoconnect"].(bool); ok {
    66  		conn.AutoConnect = val
    67  	}
    68  	if val, ok := sc["autoconnect-priority"].(int32); ok {
    69  		conn.AutoConnectPriority = val
    70  	}
    71  	if val, ok := sc["interface-name"].(string); ok {
    72  		conn.InterfaceName = val
    73  	}
    74  	if val, ok := sc["timestamp"].(uint64); ok {
    75  		conn.LastActivated = val
    76  	}
    77  
    78  	// Parse IPv4 / IPv6 settings
    79  	if val, ok := settings["ipv4"]; ok {
    80  		conn.IPv4Config = ResolveIPv4Config(val)
    81  	}
    82  	if val, ok := settings["ipv6"]; ok {
    83  		conn.IPv6Config = ResolveIPv6Config(val)
    84  	}
    85  
    86  	// Parse WiFiConfig
    87  	if conn.Type == "802-11-wireless" {
    88  		sWiFi := settings["802-11-wireless"]
    89  		if val, ok := sWiFi["ssid"].([]byte); ok {
    90  			conn.WiFiConfig.SSID = string(val)
    91  		}
    92  		sWiFiSecurity := settings["802-11-wireless-security"]
    93  		if val, ok := sWiFiSecurity["key-mgmt"].(string); ok {
    94  			conn.WiFiConfig.KeyManagement = val
    95  		}
    96  	}
    97  
    98  	return conn
    99  }
   100  
   101  // DBus returns an object that can be passed over D-Bus
   102  // Returns nil if the connection ID does not include the prefix `SimpleIoT:`
   103  // See https://developer-old.gnome.org/NetworkManager/stable/ch01.html
   104  func (c NetworkManagerConn) DBus() nm.ConnectionSettings {
   105  	sc := map[string]any{
   106  		"uuid":                 c.ID,
   107  		"id":                   c.Description,
   108  		"type":                 c.Type,
   109  		"autoconnect":          c.AutoConnect,
   110  		"autoconnect-priority": c.AutoConnectPriority,
   111  	}
   112  	if c.InterfaceName != "" {
   113  		sc["interface-name"] = c.InterfaceName
   114  	}
   115  	settings := nm.ConnectionSettings{
   116  		"connection": sc,
   117  		"ipv4":       c.IPv4Config.DBus(),
   118  		"ipv6":       c.IPv6Config.DBus(),
   119  	}
   120  	if c.Type == "802-11-wireless" {
   121  		settings["802-11-wireless"] = map[string]any{
   122  			"ssid": []byte(c.WiFiConfig.SSID),
   123  		}
   124  		if c.WiFiConfig.KeyManagement != "" {
   125  			wiFiSecurity := map[string]any{
   126  				"key-mgmt": c.WiFiConfig.KeyManagement,
   127  			}
   128  			settings["802-11-wireless-security"] = wiFiSecurity
   129  			// Only add PSK for Managed connections
   130  			if c.Managed {
   131  				wiFiSecurity["psk"] = c.WiFiConfig.PSK
   132  			}
   133  		}
   134  	}
   135  	return settings
   136  }
   137  
   138  // Equal returns true if and only if the two connections will produce the same
   139  // DBus settings
   140  func (c NetworkManagerConn) Equal(c2 NetworkManagerConn) bool {
   141  	v := reflect.ValueOf(c)
   142  	v2 := reflect.ValueOf(c2)
   143  	numFields := v.NumField()
   144  	t := v.Type()
   145  	for i := 0; i < numFields; i++ {
   146  		sf := t.Field(i)
   147  		// Skip certain fields
   148  		switch sf.Name {
   149  		case "Parent":
   150  			continue
   151  		case "Managed":
   152  			continue
   153  		case "LastActivated":
   154  			continue
   155  		case "IPv4Config":
   156  			continue
   157  		case "IPv6Config":
   158  			continue
   159  		case "Error":
   160  			continue
   161  		}
   162  		if !v.Field(i).Equal(v2.Field(i)) {
   163  			return false
   164  		}
   165  	}
   166  	return c.IPv4Config.Equal(c2.IPv4Config) &&
   167  		c.IPv6Config.Equal(c2.IPv6Config)
   168  }
   169  
   170  // IPv4Address is a string representation of an IPv4 address
   171  type IPv4Address string
   172  
   173  // IPv4AddressUint32 converts an IPv4 address in uint32 format to a string
   174  func IPv4AddressUint32(n uint32, order binary.ByteOrder) IPv4Address {
   175  	buf := []byte{0, 0, 0, 0}
   176  	order.PutUint32(buf, n)
   177  	return IPv4Address(net.IP(buf).String())
   178  }
   179  
   180  // Uint32 convert an IPv4 address in string format to a uint32
   181  func (addr IPv4Address) Uint32(order binary.ByteOrder) uint32 {
   182  	ip := net.ParseIP(addr.String()).To4()
   183  	if len(ip) != 4 {
   184  		return 0
   185  	}
   186  	return order.Uint32(ip)
   187  }
   188  
   189  // Valid returns true if string is valid IPv4 address
   190  func (addr IPv4Address) Valid() bool {
   191  	str := addr.String()
   192  	return strings.Contains(str, ".") && net.ParseIP(str).To4() != nil
   193  }
   194  
   195  // String returns the underlying string
   196  func (addr IPv4Address) String() string {
   197  	return string(addr)
   198  }
   199  
   200  // IPv4Netmask is a string representation of an IPv4 netmask
   201  type IPv4Netmask IPv4Address
   202  
   203  // IPv4NetmaskPrefix converts an integer IPv4 prefix to netmask string
   204  func IPv4NetmaskPrefix(prefix uint8) IPv4Netmask {
   205  	var mask uint32 = 0xFFFFFFFF << (32 - prefix)
   206  	return IPv4Netmask(IPv4AddressUint32(mask, binary.BigEndian))
   207  }
   208  
   209  // Prefix converts a subnet mask string to IPv4 prefix
   210  func (str IPv4Netmask) Prefix() uint32 {
   211  	return uint32(bits.OnesCount32(IPv4Address(str).Uint32(binary.BigEndian)))
   212  }
   213  
   214  // Valid returns true if subnet mask in dot notation is valid
   215  func (str IPv4Netmask) Valid() bool {
   216  	if !IPv4Address(str).Valid() {
   217  		return false
   218  	}
   219  
   220  	mask := IPv4Address(str).Uint32(binary.BigEndian)
   221  	return (mask & (^mask >> 1)) <= 0
   222  }
   223  
   224  // IPv4Config defines data for IPv4 config
   225  type IPv4Config struct {
   226  	StaticIP   bool        `json:"staticIP" point:"staticIP"`
   227  	Address    IPv4Address `json:"address" point:"address"`
   228  	Netmask    IPv4Netmask `json:"netmask" point:"netmask"`
   229  	Gateway    IPv4Address `json:"gateway" point:"gateway"`
   230  	DNSServer1 IPv4Address `json:"dnsServer1" point:"dnsServer1"`
   231  	DNSServer2 IPv4Address `json:"dnsServer2" point:"dnsServer2"`
   232  }
   233  
   234  // ResolveIPv4Config creates a new IPv4Config from a map of D-Bus settings
   235  func ResolveIPv4Config(settings map[string]any) IPv4Config {
   236  	c := IPv4Config{
   237  		// 'method' setting can be 'auto', 'manual', or 'link-local'
   238  		// Go is so cool; you can compare interface{} with a string no problem
   239  		StaticIP: settings["method"] == "manual",
   240  	}
   241  
   242  	// Note: 'address-data' is []any where elements are a map[string]any and
   243  	// where each map has "address" (string) and "prefix" (uint32) keys
   244  	addressData, _ := settings["address-data"].([]any)
   245  	if len(addressData) > 0 {
   246  		addr1, _ := addressData[0].(map[string]any)
   247  		str, _ := addr1["address"].(string)
   248  		c.Address = IPv4Address(str)
   249  		// Convert integer prefix to string subnet mask format
   250  		if prefix, ok := addr1["prefix"].(uint32); ok && prefix <= 32 {
   251  			c.Netmask = IPv4NetmaskPrefix(uint8(prefix))
   252  		}
   253  	}
   254  
   255  	str, _ := settings["gateway"].(string)
   256  	c.Gateway = IPv4Address(str)
   257  
   258  	// 'dns' setting is slice of IP addresses in uint32 format
   259  	dns, _ := settings["dns"].([]uint32)
   260  	if len(dns) > 0 {
   261  		c.DNSServer1 = IPv4AddressUint32(dns[0], binary.LittleEndian)
   262  	}
   263  	if len(dns) > 1 {
   264  		c.DNSServer2 = IPv4AddressUint32(dns[1], binary.LittleEndian)
   265  	}
   266  
   267  	return c
   268  }
   269  
   270  // Equal returns true if and only if the two IPv4Config structs are equivalent
   271  func (c IPv4Config) Equal(c2 IPv4Config) bool {
   272  	if c.Method() == "auto" && c2.Method() == "auto" {
   273  		// Ignore other fields; these two IP Configs are automatic / DHCP
   274  		return true
   275  	}
   276  	return reflect.DeepEqual(c, c2)
   277  }
   278  
   279  // Method returns the IP configuration method (i.e. "auto" or "manual")
   280  func (c IPv4Config) Method() string {
   281  	if c.StaticIP &&
   282  		c.Address.Valid() &&
   283  		c.Netmask.Valid() &&
   284  		c.Gateway.Valid() {
   285  		// Manual (Static IP)
   286  		return "manual"
   287  	}
   288  	// Automatic (DHCP)
   289  	return "auto"
   290  }
   291  
   292  // DBus returns the IPv4 settings in a generic map to be sent over D-Bus
   293  // See https://developer-old.gnome.org/NetworkManager/stable/settings-ipv4.html
   294  func (c IPv4Config) DBus() map[string]any {
   295  	settings := map[string]any{
   296  		"method": c.Method(),
   297  	}
   298  	if settings["method"] == "auto" {
   299  		return settings
   300  	}
   301  
   302  	// Manual (Static IP)
   303  	settings["address-data"] = []map[string]any{{
   304  		"address": c.Address.String(),
   305  		"prefix":  c.Netmask.Prefix(),
   306  	}}
   307  	settings["gateway"] = c.Gateway.String()
   308  
   309  	dns := make([]uint32, 0, 2)
   310  	if c.DNSServer1.Valid() {
   311  		dns = append(dns, c.DNSServer1.Uint32(binary.LittleEndian))
   312  	}
   313  	if c.DNSServer2.Valid() {
   314  		dns = append(dns, c.DNSServer2.Uint32(binary.LittleEndian))
   315  	}
   316  	settings["dns"] = dns
   317  
   318  	return settings
   319  }
   320  
   321  // IPv6Address is a string representation of an IPv4 address
   322  type IPv6Address string
   323  
   324  // NewIPv6Address converts an IPv6 address in []byte format to a string
   325  func NewIPv6Address(bs []byte) IPv6Address {
   326  	return IPv6Address(net.IP(bs).To16().String())
   327  }
   328  
   329  // Valid return true if string is valid IPv6 address
   330  func (addr IPv6Address) Valid() bool {
   331  	str := addr.String()
   332  	return strings.Contains(str, ":") && net.ParseIP(str).To16() != nil
   333  }
   334  
   335  // Bytes convert an IPv6 address in string format to []byte
   336  func (addr IPv6Address) Bytes() []byte {
   337  	return []byte(net.ParseIP(addr.String()).To16())
   338  }
   339  
   340  // String returns the underlying string
   341  func (addr IPv6Address) String() string {
   342  	return string(addr)
   343  }
   344  
   345  // IPv6Config defines data for IPv6 configs
   346  type IPv6Config struct {
   347  	StaticIP   bool        `json:"staticIP"`
   348  	Address    IPv6Address `json:"address"`
   349  	Prefix     uint8       `json:"prefix"`
   350  	Gateway    IPv6Address `json:"gateway"`
   351  	DNSServer1 IPv6Address `json:"dnsServer1"`
   352  	DNSServer2 IPv6Address `json:"dnsServer2"`
   353  }
   354  
   355  // ResolveIPv6Config creates a new IPv6Config from a map of D-Bus settings
   356  func ResolveIPv6Config(settings map[string]any) IPv6Config {
   357  	c := IPv6Config{
   358  		StaticIP: settings["method"] == "manual",
   359  	}
   360  
   361  	addressData, _ := settings["address-data"].([]any)
   362  	if len(addressData) > 0 {
   363  		addr1, _ := addressData[0].(map[string]any)
   364  		str, _ := addr1["address"].(string)
   365  		c.Address = IPv6Address(str)
   366  		if prefix, ok := addr1["prefix"].(uint32); ok && prefix <= 128 {
   367  			c.Prefix = uint8(prefix)
   368  		}
   369  	}
   370  
   371  	str, _ := settings["gateway"].(string)
   372  	c.Gateway = IPv6Address(str)
   373  
   374  	// 'dns' setting is slice of IP addresses as 16-byte slices
   375  	dns, _ := settings["dns"].([][]byte)
   376  	if len(dns) > 0 {
   377  		c.DNSServer1 = NewIPv6Address(dns[0])
   378  	}
   379  	if len(dns) > 1 {
   380  		c.DNSServer2 = NewIPv6Address(dns[1])
   381  	}
   382  
   383  	return c
   384  }
   385  
   386  // Equal returns true if and only if the two IPv6Config structs are equivalent
   387  func (c IPv6Config) Equal(c2 IPv6Config) bool {
   388  	if c.Method() == "auto" && c2.Method() == "auto" {
   389  		// Ignore other fields; these two IP Configs are automatic / DHCP
   390  		return true
   391  	}
   392  	return reflect.DeepEqual(c, c2)
   393  }
   394  
   395  // Method returns the IP configuration method (i.e. "auto" or "manual")
   396  func (c IPv6Config) Method() string {
   397  	if c.StaticIP &&
   398  		c.Address.Valid() &&
   399  		c.Gateway.Valid() {
   400  		// Manual (Static IP)
   401  		return "manual"
   402  	}
   403  	// Automatic (DHCP)
   404  	return "auto"
   405  }
   406  
   407  // DBus returns the IPv6 settings in a generic map to be sent over D-Bus
   408  // See https://developer-old.gnome.org/NetworkManager/stable/settings-ipv6.html
   409  func (c IPv6Config) DBus() map[string]any {
   410  	settings := map[string]any{
   411  		"method": c.Method(),
   412  	}
   413  	if settings["method"] == "auto" {
   414  		return settings
   415  	}
   416  
   417  	// Manual (Static IP)
   418  	settings["address-data"] = []map[string]any{{
   419  		"address": c.Address.String(),
   420  		"prefix":  c.Prefix,
   421  	}}
   422  	settings["gateway"] = c.Gateway.String()
   423  
   424  	dns := make([][]byte, 0, 2)
   425  	if c.DNSServer1.Valid() {
   426  		dns = append(dns, c.DNSServer1.Bytes())
   427  	}
   428  	if c.DNSServer2 != "" {
   429  		dns = append(dns, c.DNSServer2.Bytes())
   430  	}
   431  	settings["dns"] = dns
   432  
   433  	return settings
   434  }