main.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. // Beacon Daemon - collects events from scanners and uploads to server
  2. package main
  3. import (
  4. "context"
  5. "encoding/json"
  6. "flag"
  7. "fmt"
  8. "log"
  9. "net"
  10. "os"
  11. "os/signal"
  12. "strings"
  13. "sync"
  14. "syscall"
  15. "time"
  16. "github.com/go-zeromq/zmq4"
  17. )
  18. const (
  19. defaultConfigPath = "/opt/mybeacon/etc/config.json"
  20. defaultStatePath = "/opt/mybeacon/etc/device.json"
  21. defaultBinDir = "/opt/mybeacon/bin"
  22. defaultWiFiIface = "wlan0"
  23. maxSpoolBytes = 100 * 1024 * 1024 // 100 MB
  24. )
  25. type Daemon struct {
  26. cfg *Config
  27. state *DeviceState
  28. client *APIClient
  29. spooler *Spooler
  30. tunnel *SSHTunnel
  31. scanners *ScannerManager
  32. api *APIServer
  33. netmgr *NetworkManager
  34. bleEvents []interface{}
  35. wifiEvents []interface{}
  36. mu sync.Mutex
  37. configPath string
  38. statePath string
  39. stopChan chan struct{}
  40. }
  41. func main() {
  42. var (
  43. configPath = flag.String("config", defaultConfigPath, "Config file path")
  44. statePath = flag.String("state", defaultStatePath, "Device state file path")
  45. serverAddr = flag.String("server", "", "API server address (e.g., http://192.168.5.2:5000)")
  46. binDir = flag.String("bindir", defaultBinDir, "Directory with scanner binaries")
  47. wifiIface = flag.String("wifi-iface", defaultWiFiIface, "WiFi interface for monitor mode")
  48. httpAddr = flag.String("http", ":8080", "HTTP API listen address")
  49. debug = flag.Bool("debug", false, "Enable debug logging")
  50. )
  51. flag.Parse()
  52. log.SetFlags(log.Ltime)
  53. log.Println("================================================================================")
  54. log.Println("Beacon Daemon starting...")
  55. // Load configuration
  56. cfg, err := LoadConfig(*configPath)
  57. if err != nil {
  58. log.Printf("Warning: failed to load config: %v (using defaults)", err)
  59. cfg = DefaultConfig()
  60. }
  61. cfg.Debug = *debug || cfg.Debug
  62. // Override server address if provided
  63. if *serverAddr != "" {
  64. cfg.APIBase = *serverAddr + "/api/v1"
  65. log.Printf("Using server: %s", *serverAddr)
  66. }
  67. // Store WiFi interface
  68. cfg.WiFiIface = *wifiIface
  69. // Load device state
  70. state, err := LoadDeviceState(*statePath)
  71. if err != nil {
  72. log.Printf("Warning: failed to load state: %v", err)
  73. state = &DeviceState{}
  74. }
  75. // Get device ID from MAC if not set
  76. if state.DeviceID == "" {
  77. state.DeviceID = getDeviceID()
  78. SaveDeviceState(*statePath, state)
  79. }
  80. log.Printf("Device ID: %s", state.DeviceID)
  81. // Create spooler
  82. spooler, err := NewSpooler(cfg.SpoolDir, maxSpoolBytes)
  83. if err != nil {
  84. log.Fatalf("Failed to create spooler: %v", err)
  85. }
  86. // Create SSH tunnel manager
  87. tunnel := NewSSHTunnel(cfg)
  88. // Create scanner manager
  89. scanners := NewScannerManager(*binDir, cfg.Debug)
  90. // Create network manager (manages eth0, wlan0 client, wlan0 AP fallback)
  91. netmgr := NewNetworkManager(cfg, scanners)
  92. // Create daemon
  93. daemon := &Daemon{
  94. cfg: cfg,
  95. state: state,
  96. client: NewAPIClient(cfg.APIBase),
  97. spooler: spooler,
  98. tunnel: tunnel,
  99. scanners: scanners,
  100. netmgr: netmgr,
  101. configPath: *configPath,
  102. statePath: *statePath,
  103. stopChan: make(chan struct{}),
  104. }
  105. // Create API server
  106. daemon.api = NewAPIServer(daemon)
  107. if state.DeviceToken != "" {
  108. daemon.client.SetToken(state.DeviceToken)
  109. }
  110. // Handle signals
  111. sigChan := make(chan os.Signal, 1)
  112. signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  113. go func() {
  114. <-sigChan
  115. log.Println("Shutting down...")
  116. daemon.scanners.StopAll()
  117. daemon.tunnel.Stop()
  118. daemon.netmgr.Stop()
  119. close(daemon.stopChan)
  120. }()
  121. // Start HTTP API server FIRST (dashboard should be available immediately)
  122. go func() {
  123. log.Printf("Starting HTTP API server on %s", *httpAddr)
  124. if err := daemon.api.Start(*httpAddr); err != nil {
  125. log.Printf("HTTP server error: %v", err)
  126. }
  127. }()
  128. // Give HTTP server time to bind
  129. time.Sleep(100 * time.Millisecond)
  130. // Start network manager (manages eth0, wlan0 client, wlan0 AP fallback, WiFi scanner)
  131. // Network manager will automatically handle all network priorities and scanner coordination
  132. daemon.netmgr.Start()
  133. // Start SSH tunnel if enabled
  134. if cfg.SSHTunnel.Enabled {
  135. daemon.tunnel.Start()
  136. }
  137. // Start BLE scanner if enabled (not managed by network manager)
  138. if cfg.BLE.Enabled {
  139. if !daemon.scanners.IsBLERunning() {
  140. log.Println("Starting BLE scanner...")
  141. if err := daemon.scanners.StartBLE(cfg.ZMQAddrBLE); err != nil {
  142. log.Printf("Failed to start BLE scanner: %v", err)
  143. }
  144. }
  145. }
  146. // Start registration loop (if not registered)
  147. go daemon.registrationLoop()
  148. // Start config polling loop
  149. go daemon.configLoop()
  150. // Start ZMQ subscribers
  151. go daemon.subscribeLoop("ble", cfg.ZMQAddrBLE)
  152. go daemon.subscribeLoop("wifi", cfg.ZMQAddrWiFi)
  153. // Start batch upload loops
  154. go daemon.uploadLoop("ble", "/ble", cfg.BLE.BatchIntervalMs)
  155. go daemon.uploadLoop("wifi", "/wifi", cfg.WiFi.BatchIntervalMs)
  156. // Start spool flush loop
  157. go daemon.spoolFlushLoop()
  158. // Wait for shutdown
  159. <-daemon.stopChan
  160. log.Println("Daemon stopped")
  161. }
  162. // applyScannerConfig starts/stops scanners based on current config
  163. func (d *Daemon) applyScannerConfig() {
  164. d.mu.Lock()
  165. bleEnabled := d.cfg.BLE.Enabled
  166. wifiMonitorEnabled := d.cfg.WiFi.MonitorEnabled
  167. wifiClientEnabled := d.cfg.WiFi.ClientEnabled
  168. zmqBLE := d.cfg.ZMQAddrBLE
  169. zmqWiFi := d.cfg.ZMQAddrWiFi
  170. wifiIface := d.cfg.WiFiIface
  171. d.mu.Unlock()
  172. // BLE scanner
  173. if bleEnabled {
  174. if !d.scanners.IsBLERunning() {
  175. log.Println("Starting BLE scanner...")
  176. if err := d.scanners.StartBLE(zmqBLE); err != nil {
  177. log.Printf("Failed to start BLE scanner: %v", err)
  178. }
  179. }
  180. } else {
  181. if d.scanners.IsBLERunning() {
  182. log.Println("Stopping BLE scanner...")
  183. d.scanners.StopBLE()
  184. }
  185. }
  186. // WiFi scanner - NEVER run if WiFi client is enabled (they share wlan0)
  187. // Client mode takes priority over monitor mode
  188. if wifiClientEnabled {
  189. if d.scanners.IsWiFiRunning() {
  190. log.Println("Stopping WiFi scanner (client mode enabled in config)...")
  191. d.scanners.StopWiFi()
  192. }
  193. return
  194. }
  195. // WiFi scanner - only if monitor enabled AND client disabled
  196. // Client has HIGHER priority - if both enabled, client works, scanner doesn't
  197. if wifiMonitorEnabled {
  198. if !d.scanners.IsWiFiRunning() {
  199. log.Println("Starting WiFi scanner...")
  200. if err := d.scanners.StartWiFi(zmqWiFi, wifiIface); err != nil {
  201. log.Printf("Failed to start WiFi scanner: %v", err)
  202. }
  203. }
  204. } else {
  205. if d.scanners.IsWiFiRunning() {
  206. log.Println("Stopping WiFi scanner (monitor disabled)...")
  207. d.scanners.StopWiFi()
  208. }
  209. }
  210. }
  211. // applyWiFiClientState checks if WiFi client state matches config and applies changes
  212. func (d *Daemon) applyWiFiClientState() {
  213. d.mu.Lock()
  214. clientEnabled := d.cfg.WiFi.ClientEnabled
  215. ssid := d.cfg.WiFi.SSID
  216. psk := d.cfg.WiFi.PSK
  217. d.mu.Unlock()
  218. // Check if wlan0 has an IP (meaning it's connected)
  219. wlan0IP := getInterfaceIP("wlan0")
  220. isConnected := wlan0IP != ""
  221. if clientEnabled && ssid != "" {
  222. // Should be connected
  223. if !isConnected {
  224. // Stop WiFi scanner before connecting client (they can't coexist)
  225. if d.scanners.IsWiFiRunning() {
  226. log.Println("Stopping WiFi scanner before connecting client...")
  227. d.scanners.StopWiFi()
  228. }
  229. log.Printf("WiFi client should be connected (SSID=%s), connecting...", ssid)
  230. if err := applyWiFiSettings(ssid, psk); err != nil {
  231. log.Printf("Failed to connect WiFi client: %v", err)
  232. }
  233. }
  234. } else {
  235. // Should be disconnected
  236. if isConnected {
  237. log.Println("WiFi client should be disconnected, disconnecting...")
  238. disconnectWiFiClient()
  239. }
  240. }
  241. }
  242. func (d *Daemon) registrationLoop() {
  243. for {
  244. select {
  245. case <-d.stopChan:
  246. return
  247. default:
  248. }
  249. if d.state.DeviceToken != "" {
  250. time.Sleep(60 * time.Second)
  251. continue
  252. }
  253. log.Println("Attempting device registration...")
  254. req := &RegistrationRequest{
  255. DeviceID: d.state.DeviceID,
  256. }
  257. // Try to get IPs
  258. if ip := getInterfaceIP("eth0"); ip != "" {
  259. req.EthIP = &ip
  260. }
  261. if ip := getInterfaceIP("wlan0"); ip != "" {
  262. req.WlanIP = &ip
  263. }
  264. resp, err := d.client.Register(req)
  265. if err != nil {
  266. log.Printf("Registration failed: %v", err)
  267. time.Sleep(10 * time.Second)
  268. continue
  269. }
  270. d.state.DeviceToken = resp.DeviceToken
  271. d.state.DevicePassword = resp.DevicePassword
  272. d.client.SetToken(resp.DeviceToken)
  273. SaveDeviceState(d.statePath, d.state)
  274. log.Printf("Device registered, token received")
  275. }
  276. }
  277. func (d *Daemon) configLoop() {
  278. // Initial fetch immediately
  279. d.fetchAndApplyConfig()
  280. ticker := time.NewTicker(30 * time.Second)
  281. defer ticker.Stop()
  282. for {
  283. select {
  284. case <-d.stopChan:
  285. return
  286. case <-ticker.C:
  287. }
  288. d.fetchAndApplyConfig()
  289. }
  290. }
  291. func (d *Daemon) fetchAndApplyConfig() {
  292. if d.state.DeviceToken == "" {
  293. return
  294. }
  295. serverCfg, err := d.client.GetConfig(d.state.DeviceID)
  296. if err != nil {
  297. // In LAN mode, suppress errors (server might be unreachable)
  298. if d.cfg.Mode == "lan" {
  299. // Silent - use local config
  300. return
  301. }
  302. if d.cfg.Debug {
  303. log.Printf("Config fetch failed: %v", err)
  304. }
  305. return
  306. }
  307. // Determine effective mode: force_cloud overrides local mode setting
  308. effectiveMode := d.cfg.Mode
  309. if effectiveMode == "" {
  310. effectiveMode = "cloud"
  311. }
  312. if serverCfg.ForceCloud {
  313. if effectiveMode == "lan" {
  314. log.Println("Server force_cloud enabled - switching to cloud mode")
  315. }
  316. effectiveMode = "cloud"
  317. }
  318. d.mu.Lock()
  319. // Track changes for scanner restart
  320. bleChanged := false
  321. wifiMonitorChanged := false
  322. wifiClientChanged := false
  323. oldClientEnabled := d.cfg.WiFi.ClientEnabled
  324. if effectiveMode == "cloud" {
  325. // Cloud mode: server settings have priority
  326. bleChanged = d.cfg.BLE.Enabled != serverCfg.BLE.Enabled
  327. wifiMonitorChanged = d.cfg.WiFi.MonitorEnabled != serverCfg.WiFi.MonitorEnabled
  328. wifiClientChanged = d.cfg.WiFi.ClientEnabled != serverCfg.WiFi.ClientEnabled ||
  329. d.cfg.WiFi.SSID != serverCfg.WiFi.SSID ||
  330. d.cfg.WiFi.PSK != serverCfg.WiFi.PSK
  331. d.cfg.BLE.Enabled = serverCfg.BLE.Enabled
  332. d.cfg.BLE.BatchIntervalMs = serverCfg.BLE.BatchIntervalMs
  333. d.cfg.WiFi.MonitorEnabled = serverCfg.WiFi.MonitorEnabled
  334. d.cfg.WiFi.ClientEnabled = serverCfg.WiFi.ClientEnabled
  335. d.cfg.WiFi.SSID = serverCfg.WiFi.SSID
  336. d.cfg.WiFi.PSK = serverCfg.WiFi.PSK
  337. d.cfg.WiFi.BatchIntervalMs = serverCfg.WiFi.BatchIntervalMs
  338. d.cfg.Debug = serverCfg.Debug
  339. // NTP from server in cloud mode
  340. if len(serverCfg.Net.NTP.Servers) > 0 {
  341. d.cfg.Network.NTPServers = serverCfg.Net.NTP.Servers
  342. }
  343. }
  344. // LAN mode: local settings have priority, we keep what's in d.cfg
  345. // Only SSH tunnel comes from server (for remote support)
  346. // SSH tunnel ALWAYS from server (for remote support access)
  347. sshChanged := d.cfg.SSHTunnel.Enabled != serverCfg.SSHTunnel.Enabled
  348. if serverCfg.SSHTunnel.Enabled {
  349. d.cfg.SSHTunnel.Enabled = true
  350. d.cfg.SSHTunnel.Server = serverCfg.SSHTunnel.Server
  351. d.cfg.SSHTunnel.Port = serverCfg.SSHTunnel.Port
  352. d.cfg.SSHTunnel.User = serverCfg.SSHTunnel.User
  353. d.cfg.SSHTunnel.RemotePort = serverCfg.SSHTunnel.RemotePort
  354. d.cfg.SSHTunnel.KeepaliveInterval = serverCfg.SSHTunnel.KeepaliveInterval
  355. } else {
  356. d.cfg.SSHTunnel.Enabled = false
  357. }
  358. d.mu.Unlock()
  359. // Apply scanner config changes
  360. if bleChanged || wifiMonitorChanged {
  361. log.Printf("Config changed (mode=%s): ble=%v wifi_monitor=%v", effectiveMode, d.cfg.BLE.Enabled, d.cfg.WiFi.MonitorEnabled)
  362. d.applyScannerConfig()
  363. }
  364. // Apply WiFi client changes
  365. if wifiClientChanged {
  366. if d.cfg.WiFi.ClientEnabled && d.cfg.WiFi.SSID != "" {
  367. // Stop WiFi scanner before connecting client
  368. if d.scanners.IsWiFiRunning() {
  369. log.Println("Stopping WiFi scanner before connecting client...")
  370. d.scanners.StopWiFi()
  371. }
  372. log.Printf("WiFi client enabled by server: SSID=%s", d.cfg.WiFi.SSID)
  373. if err := applyWiFiSettings(d.cfg.WiFi.SSID, d.cfg.WiFi.PSK); err != nil {
  374. log.Printf("Failed to apply WiFi client settings: %v", err)
  375. }
  376. // After client connects, update scanner config (will skip WiFi scanner)
  377. d.applyScannerConfig()
  378. } else if oldClientEnabled && !d.cfg.WiFi.ClientEnabled {
  379. log.Println("WiFi client disabled by server")
  380. disconnectWiFiClient()
  381. // After client disconnects, WiFi scanner may start (if monitor enabled)
  382. d.applyScannerConfig()
  383. }
  384. }
  385. // Update tunnel config
  386. d.tunnel.UpdateConfig(d.cfg)
  387. if sshChanged {
  388. if d.cfg.SSHTunnel.Enabled {
  389. log.Println("SSH tunnel enabled by server")
  390. d.tunnel.Start()
  391. } else {
  392. log.Println("SSH tunnel disabled by server")
  393. d.tunnel.Stop()
  394. }
  395. }
  396. // Update network manager config (eth0 settings are local-only, never from server)
  397. d.netmgr.UpdateConfig(d.cfg)
  398. // Save updated config
  399. SaveConfig(d.configPath, d.cfg)
  400. }
  401. func (d *Daemon) subscribeLoop(name string, addr string) {
  402. for {
  403. select {
  404. case <-d.stopChan:
  405. return
  406. default:
  407. }
  408. // Only try to connect if the corresponding scanner is running
  409. scannerRunning := false
  410. if name == "ble" {
  411. scannerRunning = d.scanners.IsBLERunning()
  412. } else if name == "wifi" {
  413. scannerRunning = d.scanners.IsWiFiRunning()
  414. }
  415. if !scannerRunning {
  416. // Wait before checking again
  417. time.Sleep(5 * time.Second)
  418. continue
  419. }
  420. if err := d.runSubscriber(name, addr); err != nil {
  421. log.Printf("[%s] Subscriber error: %v, reconnecting...", name, err)
  422. time.Sleep(time.Second)
  423. }
  424. }
  425. }
  426. func (d *Daemon) runSubscriber(name string, addr string) error {
  427. ctx, cancel := context.WithCancel(context.Background())
  428. defer cancel()
  429. sub := zmq4.NewSub(ctx)
  430. defer sub.Close()
  431. // Subscribe to all topics for this type
  432. if err := sub.SetOption(zmq4.OptionSubscribe, name+"."); err != nil {
  433. return err
  434. }
  435. if err := sub.Dial(addr); err != nil {
  436. return err
  437. }
  438. log.Printf("[%s] Connected to %s", name, addr)
  439. // Monitor stop channel in goroutine
  440. go func() {
  441. <-d.stopChan
  442. cancel()
  443. }()
  444. for {
  445. msg, err := sub.Recv()
  446. if err != nil {
  447. return err
  448. }
  449. // Message is in first frame
  450. data := string(msg.Frames[0])
  451. // Parse message: "topic JSON"
  452. parts := strings.SplitN(data, " ", 2)
  453. if len(parts) != 2 {
  454. continue
  455. }
  456. var event interface{}
  457. if err := json.Unmarshal([]byte(parts[1]), &event); err != nil {
  458. continue
  459. }
  460. d.mu.Lock()
  461. if name == "ble" {
  462. d.bleEvents = append(d.bleEvents, event)
  463. if d.cfg.Debug {
  464. log.Printf("[%s] Received event, queue size: %d", name, len(d.bleEvents))
  465. }
  466. // Add to API for WebSocket broadcast
  467. if d.api != nil {
  468. d.api.AddBLEEvent(event)
  469. }
  470. } else {
  471. d.wifiEvents = append(d.wifiEvents, event)
  472. if d.cfg.Debug {
  473. log.Printf("[%s] Received event, queue size: %d", name, len(d.wifiEvents))
  474. }
  475. // Add to API for WebSocket broadcast
  476. if d.api != nil {
  477. d.api.AddWiFiEvent(event)
  478. }
  479. }
  480. d.mu.Unlock()
  481. }
  482. }
  483. func (d *Daemon) uploadLoop(name string, endpoint string, intervalMs int) {
  484. if intervalMs <= 0 {
  485. intervalMs = 2500
  486. }
  487. ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
  488. defer ticker.Stop()
  489. for {
  490. select {
  491. case <-d.stopChan:
  492. return
  493. case <-ticker.C:
  494. }
  495. d.mu.Lock()
  496. var events []interface{}
  497. if name == "ble" {
  498. events = d.bleEvents
  499. d.bleEvents = nil
  500. } else {
  501. events = d.wifiEvents
  502. d.wifiEvents = nil
  503. }
  504. d.mu.Unlock()
  505. if len(events) == 0 {
  506. continue
  507. }
  508. if d.cfg.Debug {
  509. log.Printf("[%s] Batch ready: %d events, uploading...", name, len(events))
  510. }
  511. batch := &EventBatch{
  512. DeviceID: d.state.DeviceID,
  513. Events: events,
  514. }
  515. if err := d.client.UploadEvents(endpoint, batch); err != nil {
  516. log.Printf("[%s] Upload failed: %v, spooling %d events", name, err, len(events))
  517. if err := d.spooler.Save(batch, name); err != nil {
  518. log.Printf("[%s] Spool save failed: %v", name, err)
  519. }
  520. } else {
  521. log.Printf("[%s] Uploaded %d events to server", name, len(events))
  522. }
  523. }
  524. }
  525. func (d *Daemon) spoolFlushLoop() {
  526. ticker := time.NewTicker(10 * time.Second)
  527. defer ticker.Stop()
  528. for {
  529. select {
  530. case <-d.stopChan:
  531. return
  532. case <-ticker.C:
  533. }
  534. if d.state.DeviceToken == "" {
  535. continue
  536. }
  537. // Try to flush one batch
  538. batch, err := d.spooler.PopOldest()
  539. if err != nil || batch == nil {
  540. continue
  541. }
  542. // Try to upload (guess endpoint from event types)
  543. endpoint := "/events"
  544. eventType := "unknown"
  545. if len(batch.Events) > 0 {
  546. if ev, ok := batch.Events[0].(map[string]interface{}); ok {
  547. if t, ok := ev["type"].(string); ok {
  548. if strings.HasPrefix(t, "wifi") {
  549. endpoint = "/wifi"
  550. eventType = "wifi"
  551. } else {
  552. endpoint = "/ble"
  553. eventType = "ble"
  554. }
  555. }
  556. }
  557. }
  558. if err := d.client.UploadEvents(endpoint, batch); err != nil {
  559. // Re-spool if upload failed
  560. d.spooler.Save(batch, "retry")
  561. } else {
  562. log.Printf("[spool] Flushed %d %s events to server", len(batch.Events), eventType)
  563. }
  564. }
  565. }
  566. // getDeviceID returns a device ID based on MAC address
  567. func getDeviceID() string {
  568. // Try wlan0 first, then eth0
  569. for _, iface := range []string{"wlan0", "eth0"} {
  570. if mac := getInterfaceMAC(iface); mac != "" {
  571. return mac
  572. }
  573. }
  574. // Fallback to hostname
  575. host, _ := os.Hostname()
  576. return host
  577. }
  578. func getInterfaceMAC(name string) string {
  579. iface, err := net.InterfaceByName(name)
  580. if err != nil {
  581. return ""
  582. }
  583. return iface.HardwareAddr.String()
  584. }
  585. func getInterfaceIP(name string) string {
  586. iface, err := net.InterfaceByName(name)
  587. if err != nil {
  588. return ""
  589. }
  590. addrs, err := iface.Addrs()
  591. if err != nil {
  592. return ""
  593. }
  594. for _, addr := range addrs {
  595. if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
  596. // Return IP with CIDR notation (e.g., 192.168.5.244/24)
  597. ones, _ := ipnet.Mask.Size()
  598. return fmt.Sprintf("%s/%d", ipnet.IP.String(), ones)
  599. }
  600. }
  601. return ""
  602. }