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 }