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

     1  //go:build linux
     2  // +build linux
     3  
     4  package client
     5  
     6  import (
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"syscall"
    14  	"time"
    15  
    16  	nm "github.com/Wifx/gonetworkmanager/v2"
    17  	"github.com/godbus/dbus/v5"
    18  	"github.com/google/uuid"
    19  	"github.com/nats-io/nats.go"
    20  	"github.com/simpleiot/simpleiot/data"
    21  )
    22  
    23  /*
    24  NetworkManagerClient is a SimpleIoT client that manages network interfaces
    25  and their connections using NetworkManager via D-Bus.
    26  Network connections and device states are synchronized between
    27  NetworkManager and the SimpleIoT node tree.
    28  
    29  	==========================      ======================== ---    device state     --> ==================
    30  	| NetworkManager (D-Bus) | <--> | NetworkManagerClient |                             | SimpleIoT Tree |
    31  	==========================      ======================== <-- connection settings --> ==================
    32  
    33  The NetworkManagerClient only controls SimpleIoT "managed" connections within
    34  NetworkManager. Although all connections will be added to the SIOT tree,
    35  unmanaged NetworkManager connections will not be updated or deleted by
    36  SimpleIoT.
    37  
    38  [NetworkManager Reference Manual]: https://networkmanager.dev/docs/api/latest/
    39  [gonetworkmanager Go Reference]: https://pkg.go.dev/github.com/Wifx/gonetworkmanager/v2
    40  */
    41  type NetworkManagerClient struct {
    42  	log        *log.Logger
    43  	nc         *nats.Conn
    44  	config     NetworkManager
    45  	stopCh     chan struct{}
    46  	pointsCh   chan NewPoints
    47  	nmSettings nm.Settings       // initialized on Run()
    48  	nmObj      nm.NetworkManager // initialized on Run()
    49  	// deletedConns are managed connections previously deleted from the SIOT
    50  	// tree; map is keyed by NetworkManager's connection UUID and initialized
    51  	// on Run()
    52  	deletedConns map[string]NetworkManagerConn
    53  }
    54  
    55  // NetworkManager client configuration
    56  type NetworkManager struct {
    57  	ID                      string                 `node:"id"`
    58  	Parent                  string                 `node:"parent"`
    59  	Description             string                 `point:"description"`
    60  	Disabled                bool                   `point:"disabled"`
    61  	Hostname                string                 `point:"hostname"`
    62  	RequestWiFiScan         bool                   `point:"requestWiFiScan"`
    63  	NetworkingEnabled       *bool                  `point:"networkingEnabled"`
    64  	WirelessEnabled         *bool                  `point:"wirelessEnabled"`
    65  	WirelessHardwareEnabled *bool                  `point:"wirelessHardwareEnabled"`
    66  	Devices                 []NetworkManagerDevice `child:"networkManagerDevice"`
    67  	Connections             []NetworkManagerConn   `child:"networkManagerConn"`
    68  }
    69  
    70  const dbusSyncInterval = time.Duration(60) * time.Second
    71  const dBusPropertiesChanged = "org.freedesktop.DBus.Properties.PropertiesChanged"
    72  
    73  // Print first error to logger
    74  func logFirstError(method string, log *log.Logger, errors []error) {
    75  	if len(errors) > 0 {
    76  		plural := ""
    77  		if len(errors) != 1 {
    78  			plural = "s; the first is"
    79  		}
    80  		log.Printf(
    81  			"%v had %v error%v: %v",
    82  			method, len(errors), plural, errors[0],
    83  		)
    84  	}
    85  }
    86  
    87  // NewNetworkManagerClient returns a new NetworkManagerClient using its
    88  // configuration read from the Client Manager
    89  func NewNetworkManagerClient(nc *nats.Conn, config NetworkManager) Client {
    90  	// TODO: Ensure only one NetworkManager client exists
    91  	return &NetworkManagerClient{
    92  		log: log.New(
    93  			os.Stderr,
    94  			"networkManager: ",
    95  			log.LstdFlags|log.Lmsgprefix,
    96  		),
    97  		nc:       nc,
    98  		config:   config,
    99  		stopCh:   make(chan struct{}),
   100  		pointsCh: make(chan NewPoints),
   101  	}
   102  }
   103  
   104  // Run starts the NetworkManager Client. Restarts if `networkManager` nodes or
   105  // their descendants are added / removed.
   106  func (c *NetworkManagerClient) Run() error {
   107  	str := "Starting NetworkManager client"
   108  	if c.config.Disabled {
   109  		str += " (currently disabled)"
   110  	}
   111  	c.log.Println(str)
   112  	// c.log.Printf("config %+v", c.config)
   113  
   114  	/*
   115  		When starting this client, a few things will happen:
   116  
   117  		1. We load all previously deleted connections from the SIOT tree for
   118  			future sync operations.
   119  		2. We compare the list of SIOT "managed" connections to the SIOT tree
   120  			(the tree has already been loaded into `c.config.Connections`)
   121  			and perform a one-way synchronization to NetworkManager by
   122  			creating, updating, and deleting connections. Unmanaged connections
   123  			are copied to the SIOT tree.
   124  		3. Perform a one-way synchronization **from** NetworkManager for
   125  			NetworkManagerDevices in the SIOT tree.
   126  		4. Start polling NetworkManager and continue syncing.
   127  	*/
   128  
   129  	// Note: Writes to `doSync` channel causes a sync operation to occur as soon
   130  	// as possible. Generally, calling `queueSync` is preferred to leverage the
   131  	// syncDelayTimer and rate limit sync operations.
   132  	var syncDelayTimer *time.Timer
   133  	syncDelayTimerLock := &sync.Mutex{}
   134  	doSync := make(chan struct{}, 1)
   135  	var syncTick time.Ticker
   136  	var dbusSub <-chan *dbus.Signal
   137  
   138  	init := func() error {
   139  		var err error
   140  		// Initialize NetworkManager settings object
   141  		c.nmSettings, err = nm.NewSettings()
   142  		if err != nil {
   143  			return fmt.Errorf("error getting settings: %w", err)
   144  		}
   145  
   146  		// Initialize NetworkManager
   147  		c.nmObj, err = nm.NewNetworkManager()
   148  		if err != nil {
   149  			return fmt.Errorf("error getting NetworkManager: %w", err)
   150  		}
   151  		dbusSub = c.nmObj.Subscribe()
   152  
   153  		// Get deleted previously managed connections in SIOT tree
   154  		var allConnNodes []data.NodeEdge
   155  		allConnNodes, err = GetNodes(
   156  			c.nc, c.config.ID, "all", "networkManagerConn", true,
   157  		)
   158  		if err != nil {
   159  			return fmt.Errorf("error getting deleted connection nodes: %w", err)
   160  		}
   161  		c.deletedConns = make(map[string]NetworkManagerConn)
   162  		for _, ne := range allConnNodes {
   163  			// Check tombstone to see if this node was deleted
   164  			deleted := false
   165  			for _, p := range ne.EdgePoints {
   166  				if p.Type == data.PointTypeTombstone {
   167  					deleted = int(p.Value)%2 == 1
   168  					break
   169  				}
   170  			}
   171  			if deleted {
   172  				// Decode ne to NetworkManagerConn and add to deletedConns map
   173  				var deletedConn NetworkManagerConn
   174  				err = data.Decode(
   175  					data.NodeEdgeChildren{NodeEdge: ne, Children: nil},
   176  					&deletedConn,
   177  				)
   178  				if err != nil {
   179  					return fmt.Errorf(
   180  						"error decoding deleted connection node: %w", err,
   181  					)
   182  				}
   183  				if deletedConn.Managed {
   184  					c.deletedConns[deletedConn.ID] = deletedConn
   185  				}
   186  			}
   187  		}
   188  
   189  		// Initialize NetworkManager sync ticker
   190  		syncTick = *time.NewTicker(dbusSyncInterval)
   191  
   192  		// Queue immediate sync operation
   193  		if len(doSync) == 0 {
   194  			doSync <- struct{}{}
   195  		}
   196  
   197  		return nil
   198  	}
   199  
   200  	cleanup := func() {
   201  		// Stop tickers and nullify channels to ignore any unprocessed ticks
   202  		syncTick.Stop()
   203  		syncTick.C = nil
   204  
   205  		// It's now safe to finish cleanup
   206  		c.nmSettings = nil
   207  		c.nmObj.Unsubscribe()
   208  		c.nmObj = nil
   209  		dbusSub = nil
   210  		c.log.Println("Cleaned up")
   211  	}
   212  
   213  	queueSync := func() {
   214  		if len(doSync) > 0 {
   215  			return // sync already queued to run immediately
   216  		}
   217  		syncDelayTimerLock.Lock()
   218  		defer syncDelayTimerLock.Unlock()
   219  		if syncDelayTimer == nil {
   220  			syncDelayTimer = time.AfterFunc(5*time.Second, func() {
   221  				syncDelayTimerLock.Lock()
   222  				defer syncDelayTimerLock.Unlock()
   223  				// Queue immediate sync operation
   224  				if len(doSync) == 0 {
   225  					doSync <- struct{}{}
   226  				}
   227  				syncDelayTimer = nil
   228  			})
   229  		}
   230  		// else timer already running and will trigger sync soon
   231  	}
   232  
   233  	if !c.config.Disabled {
   234  		err := init()
   235  		if err != nil {
   236  			return err
   237  		}
   238  	}
   239  
   240  	// Flag to mute logging SyncConnections() errors when no connection nodes
   241  	// have been updated
   242  	muteSyncConnectionsError := false
   243  
   244  loop:
   245  	for {
   246  		select {
   247  		case <-c.stopCh:
   248  			break loop
   249  		case nodePoints := <-c.pointsCh:
   250  			// c.log.Print(nodePoints)
   251  
   252  			disabled := c.config.Disabled
   253  
   254  			// Update config
   255  			err := data.MergePoints(nodePoints.ID, nodePoints.Points, &c.config)
   256  			if err != nil {
   257  				log.Println("Error merging points:", err)
   258  			}
   259  
   260  			// Handle Disable flag
   261  			if c.config.Disabled {
   262  				if !disabled {
   263  					cleanup()
   264  				}
   265  			} else if disabled {
   266  				// Re-initialize
   267  				err := init()
   268  				if err != nil {
   269  					return err
   270  				}
   271  			} else {
   272  				// If this is a connection node point, unmute SyncConnections()
   273  				// errors.
   274  				for _, conn := range c.config.Connections {
   275  					if conn.ID == nodePoints.ID {
   276  						muteSyncConnectionsError = false
   277  						break
   278  					}
   279  				}
   280  				// Queue sync operation
   281  				queueSync()
   282  			}
   283  		case <-doSync:
   284  			// Perform sync operations; abort on fatal error
   285  			c.log.Println("Syncing with NetworkManager over D-Bus")
   286  			errs, fatalErr := c.SyncConnections()
   287  			// Abort on fatal error
   288  			if fatalErr != nil {
   289  				return fmt.Errorf("connection sync error: %w", fatalErr)
   290  			}
   291  			if !muteSyncConnectionsError {
   292  				logFirstError("SyncConnections", c.log, errs)
   293  				muteSyncConnectionsError = true
   294  			}
   295  
   296  			// Synchronize devices with NetworkManager
   297  			errs, fatalErr = c.SyncDevices()
   298  			// Abort on fatal error
   299  			if fatalErr != nil {
   300  				return fmt.Errorf("device sync error: %w", fatalErr)
   301  			}
   302  			logFirstError("SyncDevices", c.log, errs)
   303  
   304  			// Synchronize hostname
   305  			errs, fatalErr = c.SyncHostname()
   306  			// Abort on fatal error
   307  			if fatalErr != nil {
   308  				return fmt.Errorf("hostname sync error: %w", fatalErr)
   309  			}
   310  			logFirstError("SyncHostname", c.log, errs)
   311  		case <-syncTick.C:
   312  			muteSyncConnectionsError = false
   313  			// Queue sync operation
   314  			queueSync()
   315  		case sig, ok := <-dbusSub:
   316  			if !ok {
   317  				// D-Bus subscription closed
   318  				dbusSub = nil
   319  				break // select
   320  			}
   321  			if sig.Name == dBusPropertiesChanged ||
   322  				// TODO: Confirm these strings are sufficient
   323  				strings.HasPrefix(sig.Name, "org.freedesktop.NetworkManager.Device") ||
   324  				strings.HasPrefix(sig.Name, "org.freedesktop.NetworkManager.Connection") ||
   325  				strings.HasPrefix(sig.Name, "org.freedesktop.NetworkManager.Settings.Connection") {
   326  				queueSync()
   327  			} else {
   328  				c.log.Printf("not triggering sync %v for %+v", sig.Name, sig)
   329  			}
   330  		}
   331  
   332  		// Scan Wi-Fi networks if needed
   333  		if !c.config.Disabled && c.config.RequestWiFiScan {
   334  			// Create point to clear flag
   335  			p := data.Point{
   336  				Type:   "requestWiFiScan",
   337  				Value:  0,
   338  				Origin: c.config.ID,
   339  			}
   340  
   341  			// Trigger scan (error stored in Point)
   342  			err := c.WifiScan()
   343  			if err != nil {
   344  				c.log.Printf("Error scanning for wireless APs: %v", err)
   345  				p.Text = err.Error()
   346  			}
   347  
   348  			// Clear RequestWiFiScan
   349  			c.config.RequestWiFiScan = false
   350  			err = SendNodePoint(c.nc, c.config.ID, p, true)
   351  			// Log error only
   352  			if err != nil {
   353  				c.log.Printf("Error clearing requestWiFiScan: %v", err)
   354  			}
   355  		}
   356  	}
   357  	cleanup()
   358  	return nil
   359  }
   360  
   361  // Helper function to emit point for connection error and update
   362  // the NetworkManagerConn.Error field
   363  func (c *NetworkManagerClient) emitConnectionError(
   364  	conn *NetworkManagerConn, err error,
   365  ) error {
   366  	if err == nil {
   367  		conn.Error = ""
   368  	} else {
   369  		conn.Error = err.Error()
   370  	}
   371  	emitErr := SendNodePoint(c.nc, conn.ID, data.Point{
   372  		Type:   "error",
   373  		Text:   conn.Error,
   374  		Origin: c.config.ID,
   375  	}, true)
   376  	if emitErr != nil {
   377  		return fmt.Errorf(
   378  			"error emitting error for connection %v: %w", conn.ID, err,
   379  		)
   380  	}
   381  	return nil
   382  }
   383  
   384  // SyncConnections performs a one-way synchronization of the NetworkManagerConn
   385  // nodes in the SIOT tree with connections in NetworkManager via D-Bus. The
   386  // sync direction is determined by the connection's Managed flag. If set, the
   387  // connection in NetworkManager is updated with the data in the SIOT tree;
   388  // otherwise, the SIOT tree is updated with the data in NetworkManager.
   389  // Returns a list of errors in the order in which they are encountered. If a
   390  // fatal error occurs that aborts the sync operation, that will be included
   391  // in `errs` and returned as `fatal`.
   392  func (c *NetworkManagerClient) SyncConnections() (errs []error, fatal error) {
   393  	// Build set of connection IDs from SIOT tree
   394  	treeConnIDs := make(map[string]struct{}, len(c.config.Connections))
   395  	for _, treeConn := range c.config.Connections {
   396  		treeConnIDs[treeConn.ID] = struct{}{} // add to set
   397  	}
   398  
   399  	// Get NetworkManagerConn from NetworkManager via D-Bus
   400  	connections, err := c.nmSettings.ListConnections()
   401  	if err != nil {
   402  		fatal = fmt.Errorf("error listing connections: %w", err)
   403  		errs = append(errs, fatal)
   404  		return
   405  	}
   406  
   407  	// Build map of NetworkManager connections by ID and handle connections not
   408  	// found in the SIOT tree.
   409  	type ConnectionResolved struct {
   410  		Connection nm.Connection
   411  		Resolved   NetworkManagerConn
   412  	}
   413  	nmConns := make(map[string]ConnectionResolved, len(connections))
   414  	for _, conn := range connections {
   415  		settings, err := conn.GetSettings()
   416  		if err != nil {
   417  			errs = append(errs, fmt.Errorf(
   418  				"error getting connection settings: %w", err,
   419  			))
   420  		}
   421  		nmc := ResolveNetworkManagerConn(settings)
   422  		nmc.Parent = c.config.ID
   423  
   424  		// Handle connections not found in the SIOT tree. If a connection is not
   425  		// in the SIOT tree, we check to see if a previously deleted, managed
   426  		// connection with the same UUID existed in the SIOT tree. If so, we
   427  		// delete it from NetworkManager; otherwise, it must be a new connection
   428  		// to be added to the SIOT tree.
   429  		if _, ok := treeConnIDs[nmc.ID]; !ok {
   430  			// Note: deletedConns is keyed by UUID, not ID
   431  			if _, ok := c.deletedConns[nmc.ID]; ok {
   432  				// Delete connection from NetworkManager
   433  				if err := conn.Delete(); err != nil {
   434  					errs = append(errs, fmt.Errorf(
   435  						"error deleting connection %v: %w", nmc.ID, err,
   436  					))
   437  				}
   438  				c.log.Printf("Deleted connection %v (%v)",
   439  					nmc.ID, nmc.Description,
   440  				)
   441  			} else {
   442  				// Add connection to SIOT tree
   443  				c.log.Printf("Detected connection %v (%v)",
   444  					nmc.ID, nmc.Description,
   445  				)
   446  				err := SendNodeType(c.nc, nmc, c.config.ID)
   447  				if err != nil {
   448  					errs = append(errs, fmt.Errorf(
   449  						"error adding connection node %v: %w", nmc.ID, err,
   450  					))
   451  				}
   452  			}
   453  		} else {
   454  			// Add NetworkManager connection to the map and handle it in the
   455  			// next loop
   456  			nmConns[nmc.ID] = ConnectionResolved{conn, nmc}
   457  		}
   458  	}
   459  
   460  	// Now handle each connection already in the SIOT tree
   461  	for i := range c.config.Connections {
   462  		treeConn := &c.config.Connections[i]
   463  		var pts data.Points // points to update this connection in SIOT tree
   464  		nmc, found := nmConns[treeConn.ID]
   465  		if found {
   466  			// Connection also exists in NetworkManager
   467  			if treeConn.Managed {
   468  				// Update connection in NetworkManager, except LastActivated
   469  				if treeConn.LastActivated != nmc.Resolved.LastActivated {
   470  					treeConn.LastActivated = nmc.Resolved.LastActivated
   471  					pts.Add(data.Point{
   472  						Type:   "lastActivated",
   473  						Value:  float64(treeConn.LastActivated),
   474  						Origin: c.config.ID,
   475  					})
   476  				}
   477  
   478  				// Sync properties not populated by ResolveNetworkManagerConn
   479  				nmc.Resolved.Managed = true
   480  				if nmc.Resolved.Type == "802-11-wireless" &&
   481  					nmc.Resolved.WiFiConfig.KeyManagement == "wpa-psk" {
   482  					secrets, err := nmc.Connection.GetSecrets(
   483  						"802-11-wireless-security",
   484  					)
   485  					if err != nil {
   486  						// Wrap error, append to errs, and emit on connection
   487  						err = fmt.Errorf(
   488  							"error getting secrets for connection %v: %w",
   489  							nmc.Resolved.ID, err,
   490  						)
   491  						errs = append(errs, err)
   492  						err = c.emitConnectionError(treeConn, err)
   493  						if err != nil {
   494  							errs = append(errs, err)
   495  						}
   496  						continue
   497  					}
   498  					if psk, ok := secrets["802-11-wireless-security"]["psk"].(string); ok {
   499  						nmc.Resolved.WiFiConfig.PSK = psk
   500  					}
   501  				}
   502  				// Update existing connection
   503  				if !treeConn.Equal(nmc.Resolved) {
   504  					// diff, err := data.DiffPoints(nmc.Resolved, treeConn)
   505  					// c.log.Printf("DEBUG: %v %v", err, diff)
   506  
   507  					err = nmc.Connection.Update(treeConn.DBus())
   508  					if err != nil {
   509  						err = fmt.Errorf(
   510  							"error updating connection %v: %w", treeConn.ID, err,
   511  						)
   512  						errs = append(errs, err)
   513  						err = c.emitConnectionError(treeConn, err)
   514  						if err != nil {
   515  							errs = append(errs, err)
   516  						}
   517  						// Delete connection because update failed
   518  						err = nmc.Connection.Delete()
   519  						if err != nil {
   520  							errs = append(errs, fmt.Errorf(
   521  								"error deleting connection %v: %w",
   522  								treeConn.ID, err,
   523  							))
   524  						}
   525  						continue
   526  					}
   527  					c.log.Printf("Updated connection %v (%v)",
   528  						treeConn.ID, treeConn.Description,
   529  					)
   530  					// If this connection is currently active, reactivate it
   531  					acs, err := c.nmObj.GetPropertyActiveConnections()
   532  					if err != nil {
   533  						errs = append(errs, fmt.Errorf(
   534  							"error getting active connections: %w", err,
   535  						))
   536  						continue
   537  					}
   538  					for _, ac := range acs {
   539  						acID, err := ac.GetPropertyUUID()
   540  						if err != nil {
   541  							errs = append(errs, fmt.Errorf(
   542  								"error getting active connection UUID: %w", err,
   543  							))
   544  							break
   545  						}
   546  						if acID == treeConn.ID {
   547  							// Reactivate connection
   548  							err = c.nmObj.DeactivateConnection(ac)
   549  							if err != nil {
   550  								err = fmt.Errorf(
   551  									"error deactivating connection %v: %w",
   552  									treeConn.ID, err,
   553  								)
   554  								errs = append(errs, err)
   555  								err = c.emitConnectionError(treeConn, err)
   556  								if err != nil {
   557  									errs = append(errs, err)
   558  								}
   559  							}
   560  							// Note: Device not specified
   561  							_, err = c.nmObj.ActivateConnection(
   562  								nmc.Connection, nil, nil,
   563  							)
   564  							if err != nil {
   565  								err = fmt.Errorf(
   566  									"error activating connection %v: %w",
   567  									treeConn.ID, err,
   568  								)
   569  								errs = append(errs, err)
   570  								err = c.emitConnectionError(treeConn, err)
   571  								if err != nil {
   572  									errs = append(errs, err)
   573  								}
   574  							}
   575  
   576  							// Reapply connection settings to all devices
   577  							// devs, err := ac.GetPropertyDevices()
   578  							// if err != nil {
   579  							// 	errs = append(errs, fmt.Errorf(
   580  							// 		"error reactivating %v: %w",
   581  							// 		treeConn.ID, err,
   582  							// 	))
   583  							// 	break
   584  							// }
   585  							// for _, dev := range devs {
   586  							// 	err = dev.Reapply(treeConn.DBus(), 0, 0)
   587  							// 	if err != nil {
   588  							// 		c.log.Printf("warning: could not reapply connection %v for device %v: %v",
   589  							// 			treeConn.ID, dev.GetPath(), err,
   590  							// 		)
   591  							// 	} else {
   592  							// 		c.log.Printf("Reapplied connection %v to device %v",
   593  							// 			treeConn.ID, dev.GetPath(),
   594  							// 		)
   595  							// 	}
   596  							// }
   597  							// break
   598  						}
   599  					}
   600  				}
   601  			} else {
   602  				// Update connection in SIOT tree
   603  				diffPts, err := data.DiffPoints(treeConn, &nmc.Resolved)
   604  				if err != nil {
   605  					errs = append(errs, fmt.Errorf(
   606  						"error updating connection node %v: %w", treeConn.ID, err,
   607  					))
   608  					continue
   609  				}
   610  				if diffPts.Len() > 0 {
   611  					c.log.Printf("Updating connection node %v (%v)",
   612  						treeConn.ID, treeConn.Description,
   613  					)
   614  					// c.log.Println("DEBUG", diffPts)
   615  					pts = append(pts, diffPts...)
   616  				}
   617  			}
   618  		} else {
   619  			// Connection does not exist in NetworkManager
   620  			if treeConn.Managed {
   621  				// Handle case where node ID is not a valid UUID
   622  				_, err = uuid.Parse(treeConn.ID)
   623  				if err != nil {
   624  					err = fmt.Errorf("invalid UUID: %w", err)
   625  				} else {
   626  					// Add connection to NetworkManager
   627  					_, err = c.nmSettings.AddConnection(treeConn.DBus())
   628  				}
   629  				if err != nil {
   630  					err = fmt.Errorf(
   631  						"error adding connection %v: %w", treeConn.ID, err,
   632  					)
   633  					errs = append(errs, err)
   634  					err = c.emitConnectionError(treeConn, err)
   635  					if err != nil {
   636  						errs = append(errs, err)
   637  					}
   638  					continue
   639  				}
   640  				c.log.Printf("Added connection %v (%v)",
   641  					treeConn.ID, treeConn.Description,
   642  				)
   643  			} else {
   644  				// Delete connection from SIOT tree
   645  				err = SendEdgePoint(c.nc, treeConn.ID, treeConn.Parent, data.Point{
   646  					Type:  data.PointTypeTombstone,
   647  					Value: 1,
   648  				}, true)
   649  				if err != nil {
   650  					errs = append(errs, fmt.Errorf(
   651  						"error removing connection node %v: %w", treeConn.ID, err,
   652  					))
   653  					continue
   654  				}
   655  				c.log.Printf("Deleted connection node %v (%v)",
   656  					treeConn.ID, treeConn.Description,
   657  				)
   658  			}
   659  		}
   660  
   661  		// Update points in SIOT tree, if needed
   662  		if pts.Len() > 0 {
   663  			// Set origin on all points
   664  			for _, p := range pts {
   665  				p.Origin = c.config.ID
   666  			}
   667  			err = SendNodePoints(c.nc, treeConn.ID, pts, true)
   668  			// Log error only
   669  			if err != nil {
   670  				c.log.Printf("Error setting new connection UUID: %v", err)
   671  			}
   672  		}
   673  	}
   674  
   675  	return
   676  }
   677  
   678  // SyncDevices performs a one-way synchronization of the devices in
   679  // NetworkManager with the NetworkManagerDevices nodes in the SIOT tree via
   680  // D-Bus. Additionally, the NetworkingEnabled and WirelessHardwareEnabled flags
   681  // are copied to the SIOT tree; the WirelessEnabled flag is copied to
   682  // NetworkManager if it is non-nil and copied to the SIOT tree if it is nil.
   683  // Returns a list of errors in the order in which they are encountered.
   684  // If a fatal error occurs that aborts the sync operation, that will be included
   685  // in `errs` and returned as `fatal`.
   686  func (c *NetworkManagerClient) SyncDevices() (errs []error, fatal error) {
   687  	networkingEnabled, err := c.nmObj.GetPropertyNetworkingEnabled()
   688  	if err != nil {
   689  		errs = append(errs, fmt.Errorf("error getting network property: %w", err))
   690  	}
   691  	wirelessEnabled, err := c.nmObj.GetPropertyWirelessEnabled()
   692  	if err != nil {
   693  		errs = append(errs, fmt.Errorf("error getting network property: %w", err))
   694  	}
   695  	wirelessHwEnabled, err := c.nmObj.GetPropertyWirelessHardwareEnabled()
   696  	if err != nil {
   697  		errs = append(errs, fmt.Errorf("error getting network property: %w", err))
   698  	}
   699  	nmDevices, err := c.nmObj.GetAllDevices()
   700  	if err != nil {
   701  		errs = append(errs, fmt.Errorf("error getting devices: %w", err))
   702  	}
   703  	// Abort on any error received above
   704  	if len(errs) > 0 {
   705  		fatal = errs[0]
   706  		return
   707  	}
   708  
   709  	// Sync Networking / Wireless enabled flags
   710  	pts := data.Points{}
   711  	if c.config.NetworkingEnabled == nil ||
   712  		*c.config.NetworkingEnabled != networkingEnabled {
   713  		c.config.NetworkingEnabled = &networkingEnabled
   714  		p := data.Point{
   715  			Type:   "networkingEnabled",
   716  			Value:  0,
   717  			Origin: c.config.ID,
   718  		}
   719  		if networkingEnabled {
   720  			p.Value = 1
   721  		}
   722  		pts.Add(p)
   723  	}
   724  	if c.config.WirelessHardwareEnabled == nil ||
   725  		*c.config.WirelessHardwareEnabled != wirelessHwEnabled {
   726  		c.config.WirelessHardwareEnabled = &wirelessHwEnabled
   727  		p := data.Point{
   728  			Type:   "wirelessHardwareEnabled",
   729  			Value:  0,
   730  			Origin: c.config.ID,
   731  		}
   732  		if wirelessHwEnabled {
   733  			p.Value = 1
   734  		}
   735  		pts.Add(p)
   736  	}
   737  	if c.config.WirelessEnabled == nil {
   738  		// Copy to SIOT tree
   739  		c.config.WirelessEnabled = &wirelessEnabled
   740  		p := data.Point{
   741  			Type:   "wirelessEnabled",
   742  			Value:  0,
   743  			Origin: c.config.ID,
   744  		}
   745  		if wirelessEnabled {
   746  			p.Value = 1
   747  		}
   748  		pts.Add(p)
   749  	} else if wirelessEnabled != *c.config.WirelessEnabled {
   750  		// Copy to NetworkManager
   751  		err = c.nmObj.SetPropertyWirelessEnabled(*c.config.WirelessEnabled)
   752  		if err != nil {
   753  			errs = append(errs,
   754  				fmt.Errorf("error setting WirelessEnabled: %w", err),
   755  			)
   756  		}
   757  	}
   758  
   759  	// Send points
   760  	if pts.Len() > 0 {
   761  		err = SendNodePoints(
   762  			c.nc,
   763  			c.config.ID,
   764  			pts,
   765  			false,
   766  		)
   767  		if err != nil {
   768  			errs = append(errs, fmt.Errorf("error updating enabled flags: %w", err))
   769  		}
   770  	}
   771  
   772  	// Populate NetworkManager device info; keyed by their UUID
   773  	deviceInfo := make(map[string]NetworkManagerDevice)
   774  	for _, nmDevice := range nmDevices {
   775  		dev, err := ResolveDevice(c.config.ID, nmDevice)
   776  		if err != nil {
   777  			errs = append(errs, fmt.Errorf("error resolving device: %w", err))
   778  		}
   779  		if !dev.Managed {
   780  			continue // ignore devices not managed by NetworkManager
   781  		}
   782  		deviceInfo[dev.ID] = dev
   783  
   784  		// data, _ := json.MarshalIndent(deviceInfo[dev.ID], "", "\t")
   785  		// c.log.Println(string(data))
   786  	}
   787  
   788  	// Update devices already in SIOT tree
   789  	for i := range c.config.Devices {
   790  		device := &c.config.Devices[i]
   791  		nmDevice, ok := deviceInfo[device.ID]
   792  		if ok {
   793  			// Preserve AccessPoints
   794  			nmDevice.AccessPoints = device.AccessPoints
   795  			// Update device
   796  			pts, err := data.DiffPoints(device, &nmDevice)
   797  			// Set origin on all points
   798  			for _, p := range pts {
   799  				p.Origin = c.config.ID
   800  			}
   801  			if err != nil {
   802  				errs = append(errs, fmt.Errorf(
   803  					"error updating device %v: %w", device.ID, err,
   804  				))
   805  			}
   806  			if len(pts) > 0 {
   807  				c.log.Printf("Updating device %v\n%v", device.ID, pts)
   808  				err := SendNodePoints(
   809  					c.nc,
   810  					device.ID,
   811  					pts,
   812  					false,
   813  				)
   814  				if err != nil {
   815  					errs = append(errs, fmt.Errorf(
   816  						"error updating device %v: %w", device.ID, err,
   817  					))
   818  				}
   819  
   820  				err = data.Decode(data.NodeEdgeChildren{
   821  					NodeEdge: data.NodeEdge{
   822  						ID:     device.ID,
   823  						Parent: device.Parent,
   824  						Points: pts,
   825  					},
   826  				}, device)
   827  
   828  				if err != nil {
   829  					errs = append(errs, fmt.Errorf(
   830  						"error decoding data %v: %w", device.ID, err,
   831  					))
   832  				}
   833  			}
   834  			// Delete from deviceInfo to avoid duplicating it later
   835  			delete(deviceInfo, device.ID)
   836  		} else {
   837  			// Delete device
   838  			c.log.Printf("Deleting device %v", device.ID)
   839  			err := SendEdgePoint(
   840  				c.nc,
   841  				device.ID,
   842  				device.Parent,
   843  				data.Point{
   844  					Type:   "tombstone",
   845  					Value:  1,
   846  					Origin: c.config.ID,
   847  				},
   848  				true,
   849  			)
   850  			if err != nil {
   851  				errs = append(errs, fmt.Errorf(
   852  					"error deleting device %v: %w", device.ID, err,
   853  				))
   854  			}
   855  		}
   856  	}
   857  
   858  	// Add devices not in SIOT tree
   859  	// Note: updated devices are deleted from deviceInfo above
   860  	for _, nmDevice := range deviceInfo {
   861  		c.log.Printf("Adding device %v", nmDevice.ID)
   862  		err := SendNodeType(c.nc, nmDevice, c.config.ID)
   863  		if err != nil {
   864  			errs = append(errs, fmt.Errorf(
   865  				"error adding device %v: %w", nmDevice.ID, err,
   866  			))
   867  		}
   868  	}
   869  
   870  	return
   871  }
   872  
   873  // SyncHostname writes the hostname from the SimpleIoT tree to NetworkManager;
   874  // however, if SimpleIoT does not have a hostname set, the current hostname
   875  // will be stored in the tree instead.
   876  func (c *NetworkManagerClient) SyncHostname() (errs []error, fatal error) {
   877  	hostname, err := c.nmSettings.GetPropertyHostname()
   878  	if err != nil {
   879  		fatal = fmt.Errorf("error getting hostname: %w", err)
   880  		errs = append(errs, fatal)
   881  	}
   882  	if c.config.Hostname == "" {
   883  		// Write hostname to tree
   884  		c.config.Hostname = hostname
   885  		err = SendNodePoint(c.nc, c.config.ID, data.Point{
   886  			Type:   "hostname",
   887  			Text:   hostname,
   888  			Origin: c.config.ID,
   889  		}, true)
   890  		if err != nil {
   891  			errs = append(errs, err)
   892  		}
   893  	} else if hostname != c.config.Hostname {
   894  		// Write hostname to NetworkManager
   895  		err = c.nmSettings.SaveHostname(c.config.Hostname)
   896  		if err != nil {
   897  			errs = append(errs, err)
   898  		}
   899  	}
   900  	return
   901  }
   902  
   903  // WifiScan scans for Wi-Fi access points using available Wi-Fi devices.
   904  // When scanning is complete, the access points are saved as points on the
   905  // NetworkManagerDevice node.
   906  func (c *NetworkManagerClient) WifiScan() error {
   907  	c.log.Println("Scanning for wireless APs...")
   908  
   909  	nmDevices, err := c.nmObj.GetAllDevices()
   910  	if err != nil {
   911  		return fmt.Errorf("error getting devices: %w", err)
   912  	}
   913  
   914  	// Populate NetworkManager device info; keyed by their UUID
   915  	nmDeviceMap := make(map[string]nm.DeviceWireless)
   916  	for _, nmDevice := range nmDevices {
   917  		dev, err := ResolveDevice(c.config.ID, nmDevice)
   918  		if err != nil {
   919  			return fmt.Errorf("error resolving device: %w", err)
   920  		}
   921  		if !dev.Managed {
   922  			continue // ignore devices not managed by NetworkManager
   923  		}
   924  		if nmWifiDevice, ok := nmDevice.(nm.DeviceWireless); ok {
   925  			nmDeviceMap[dev.ID] = nmWifiDevice
   926  		}
   927  	}
   928  
   929  	// For each Wi-Fi device in SIOT tree
   930  	found := false
   931  	for devIndex := range c.config.Devices {
   932  		device := &c.config.Devices[devIndex]
   933  		if device.DeviceType != nm.NmDeviceTypeWifi.String() ||
   934  			device.State == nm.NmDeviceStateUnmanaged.String() ||
   935  			device.State == nm.NmDeviceStateUnavailable.String() {
   936  			continue
   937  		}
   938  		found = true
   939  		nmDevice, ok := nmDeviceMap[device.ID]
   940  		if !ok {
   941  			continue // no longer available
   942  		}
   943  		lastScan, _ := nmDevice.GetPropertyLastScan() // Ignore error
   944  
   945  		// Get system uptime because LastScan property is milliseconds since
   946  		// CLOCK_BOOTTIME
   947  		var sysInfo syscall.Sysinfo_t
   948  		err = syscall.Sysinfo(&sysInfo)
   949  		if err != nil {
   950  			return fmt.Errorf("error getting system uptime: %v", err)
   951  		}
   952  
   953  		// If last scan was more than RescanTimeoutSeconds ago, re-scan APs
   954  		if int64(sysInfo.Uptime)-lastScan/1000 > RescanTimeoutSeconds {
   955  			sigChan := c.nmObj.Subscribe()
   956  			err = nmDevice.RequestScan()
   957  
   958  			// Wait for "LastScan" property of device to be updated.
   959  			// This indicates that the scan is complete
   960  			timeout := time.After(5 * time.Second)
   961  		scanLoop:
   962  			for {
   963  				select {
   964  				case sig, ok := <-sigChan:
   965  					if !ok {
   966  						return fmt.Errorf(
   967  							"D-Bus subscription closed while scanning for access points",
   968  						)
   969  					} else if sig.Path == nmDevice.GetPath() &&
   970  						sig.Name == dBusPropertiesChanged &&
   971  						len(sig.Body) >= 2 {
   972  
   973  						/* Note: sig.Body should be
   974  						[
   975  							interface_name: string,
   976  							changed_properties: map[string]dbus.Variant,
   977  							invalidated_properties: []string
   978  						]
   979  						*/
   980  						changed, ok := sig.Body[1].(map[string]dbus.Variant)
   981  						if !ok {
   982  							return fmt.Errorf(
   983  								"D-Bus signal body had unexpected format",
   984  							)
   985  						}
   986  
   987  						if _, ok := changed["LastScan"]; ok {
   988  							break scanLoop
   989  						}
   990  					}
   991  				case <-timeout:
   992  					// On timeout, just exit loop and return APs that have
   993  					// already been found
   994  					break scanLoop
   995  				}
   996  			}
   997  			c.nmObj.Unsubscribe()
   998  		}
   999  
  1000  		// device.GetPropertyAccessPoints() can return dbus object paths
  1001  		// that become invalidated when `ResolveAccessPoint` tries to read from
  1002  		// them.  Rather than silently ignoring these errors and excluding
  1003  		// the access point from the slice, we instead just try calling the
  1004  		// function up to 3 times, assuming it will probably work on the 2nd
  1005  		// attempt.
  1006  	getPropertyAccessPoints:
  1007  		for i := 0; i < 3; i++ {
  1008  			var nmAPs []nm.AccessPoint
  1009  			nmAPs, err = nmDevice.GetPropertyAccessPoints()
  1010  			if err != nil {
  1011  				continue getPropertyAccessPoints
  1012  			}
  1013  			// Convert nm.AccessPoint to AccessPoint
  1014  			pts := make(data.Points, 0, len(nmAPs))
  1015  			for i, nmAP := range nmAPs {
  1016  				var ap AccessPoint
  1017  				ap, err = ResolveAccessPoint(nmAP)
  1018  				if err != nil {
  1019  					continue getPropertyAccessPoints
  1020  				}
  1021  				apJSON, err := ap.MarshallJSON()
  1022  				if err != nil {
  1023  					return fmt.Errorf("error encoding: %v", err)
  1024  				}
  1025  				pts.Add(data.Point{
  1026  					Type:   "accessPoints",
  1027  					Key:    strconv.Itoa(i),
  1028  					Text:   string(apJSON),
  1029  					Origin: c.config.ID,
  1030  				})
  1031  			}
  1032  			c.log.Printf("Discovered %v access points", len(pts))
  1033  
  1034  			// Add tombstone points
  1035  			currAPLen := len(device.AccessPoints)
  1036  			for i := currAPLen - 1; i >= len(nmAPs); i-- {
  1037  				pts.Add(data.Point{
  1038  					Type:      "accessPoints",
  1039  					Key:       strconv.Itoa(i),
  1040  					Tombstone: 1,
  1041  					Origin:    c.config.ID,
  1042  				})
  1043  			}
  1044  
  1045  			// Send points to device
  1046  			err := SendNodePoints(
  1047  				c.nc,
  1048  				device.ID,
  1049  				pts,
  1050  				false,
  1051  			)
  1052  			if err != nil {
  1053  				return fmt.Errorf(
  1054  					"error updating device %v: %w", device.ID, err,
  1055  				)
  1056  			}
  1057  
  1058  			err = data.Decode(data.NodeEdgeChildren{
  1059  				NodeEdge: data.NodeEdge{
  1060  					ID:     device.ID,
  1061  					Parent: device.Parent,
  1062  					Points: pts,
  1063  				},
  1064  			}, device)
  1065  			if err != nil {
  1066  				return fmt.Errorf("error decoding data: %w", err)
  1067  			}
  1068  
  1069  			break getPropertyAccessPoints
  1070  		}
  1071  		if err != nil {
  1072  			return fmt.Errorf("cannot get AccessPoints property from D-Bus")
  1073  		}
  1074  	}
  1075  	if !found {
  1076  		return fmt.Errorf("no Wi-Fi devices found")
  1077  	}
  1078  	return nil
  1079  }
  1080  
  1081  // Stop stops the NetworkManager Client
  1082  func (c *NetworkManagerClient) Stop(error) {
  1083  	close(c.stopCh)
  1084  }
  1085  
  1086  // Points is called when the client's node points are updated
  1087  func (c *NetworkManagerClient) Points(nodeID string, points []data.Point) {
  1088  	c.pointsCh <- NewPoints{
  1089  		ID:     nodeID,
  1090  		Points: points,
  1091  	}
  1092  }
  1093  
  1094  // EdgePoints is called when the client's node edge points are updated
  1095  func (c *NetworkManagerClient) EdgePoints(
  1096  	_ string, _ string, _ []data.Point,
  1097  ) {
  1098  	// c.edgePointsCh <- NewPoints{
  1099  	// 	ID:     nodeID,
  1100  	// 	Parent: parentID,
  1101  	// 	Points: points,
  1102  	// }
  1103  }