NetworkService.swift 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. //
  2. // Copyright 2026 Aarav Ravindra Kharade
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. #if canImport(Glibc)
  18. import Glibc
  19. #endif
  20. // ═══════════════════════════════════════════════════════════════════
  21. // ArkOS Network Service
  22. // ═══════════════════════════════════════════════════════════════════
  23. // Background service that manages network connectivity for the OS.
  24. // Handles interface monitoring, DHCP client triggering, and DNS
  25. // resolver configuration.
  26. //
  27. // Lifecycle:
  28. // 1. Enumerate all non-loopback interfaces via /sys/class/net/
  29. // 2. Bring up interfaces via ip(8) / ifconfig(8) if available
  30. // 3. Trigger DHCP on wired interfaces via udhcpc (from busybox)
  31. // 4. Monitor link state changes and re-acquire addresses as needed
  32. // 5. Configure /etc/resolv.conf with received nameservers
  33. //
  34. // In QEMU user-mode networking, the virtual NIC gets an IP via
  35. // QEMU's built-in DHCP server (typically 10.0.2.15).
  36. // ═══════════════════════════════════════════════════════════════════
  37. /// Manages network interface lifecycle and connectivity.
  38. public class NetworkService {
  39. /// Singleton instance (started by ServiceManager or arkrt main).
  40. public static let shared = NetworkService()
  41. /// Polling interval for link state monitoring (seconds).
  42. private let pollInterval: UInt32 = 5
  43. /// Whether the service is currently running its monitor loop.
  44. private var isRunning = false
  45. private init() {}
  46. // ── Interface Bring-Up ────────────────────────────────────────
  47. /// Attempts to bring up all non-loopback network interfaces and
  48. /// acquire IP addresses via DHCP.
  49. public func configureInterfaces() {
  50. let interfaces = NetworkManager.getInterfaceNames()
  51. if interfaces.isEmpty {
  52. LogManager.log("NetworkService: No network interfaces found", level: .warn)
  53. return
  54. }
  55. LogManager.log("NetworkService: Found \(interfaces.count) interface(s): \(interfaces)")
  56. for iface in interfaces {
  57. bringUpInterface(iface)
  58. }
  59. }
  60. /// Brings up a single interface and starts DHCP if it's wired.
  61. private func bringUpInterface(_ name: String) {
  62. LogManager.log("NetworkService: Bringing up interface '\(name)'...")
  63. // Try to bring the interface up using ip(8) first, fall back to ifconfig
  64. let ipResult = runCommand("/sbin/ip link set \(name) up")
  65. if ipResult != 0 {
  66. let _ = runCommand("/sbin/ifconfig \(name) up")
  67. }
  68. // Check if the interface has carrier (cable plugged / associated)
  69. let carrierPath = "/sys/class/net/\(name)/carrier"
  70. let carrier = (try? String(contentsOfFile: carrierPath))?
  71. .trimmingCharacters(in: .whitespacesAndNewlines)
  72. if carrier == "1" {
  73. LogManager.log("NetworkService: '\(name)' has carrier — requesting DHCP lease...")
  74. requestDHCP(interface: name)
  75. } else {
  76. LogManager.log("NetworkService: '\(name)' has no carrier — will retry on link change")
  77. }
  78. }
  79. // ── DHCP Client ───────────────────────────────────────────────
  80. /// Runs udhcpc (busybox DHCP client) to acquire an IP address.
  81. /// Uses a short timeout to avoid blocking the boot process.
  82. private func requestDHCP(interface name: String) {
  83. // udhcpc flags:
  84. // -i <interface> — target interface
  85. // -n — exit if lease not obtained
  86. // -q — quit after obtaining lease
  87. // -t 3 — try 3 times
  88. // -T 2 — 2 second timeout between tries
  89. let result = runCommand("/sbin/udhcpc -i \(name) -n -q -t 3 -T 2 2>/dev/null")
  90. if result == 0 {
  91. LogManager.log("NetworkService: DHCP lease acquired on '\(name)'")
  92. configureDNS()
  93. } else {
  94. LogManager.log("NetworkService: DHCP failed on '\(name)' — will retry", level: .warn)
  95. }
  96. }
  97. // ── DNS Configuration ─────────────────────────────────────────
  98. /// Writes a basic resolv.conf with common public DNS servers.
  99. /// In production, this would be populated by DHCP lease data.
  100. private func configureDNS() {
  101. let resolv = """
  102. # Generated by ArkOS NetworkService
  103. nameserver 8.8.8.8
  104. nameserver 8.8.4.4
  105. nameserver 1.1.1.1
  106. """
  107. if let fp = fopen("/etc/resolv.conf", "w") {
  108. fputs(resolv, fp)
  109. fclose(fp)
  110. LogManager.log("NetworkService: DNS resolver configured")
  111. }
  112. }
  113. // ── Link State Monitor ────────────────────────────────────────
  114. /// Starts the background link monitoring loop. Checks for carrier
  115. /// changes and re-runs DHCP when a cable is plugged in.
  116. public func startMonitoring() {
  117. guard !isRunning else { return }
  118. isRunning = true
  119. LogManager.log("NetworkService: Starting link state monitor (poll every \(pollInterval)s)")
  120. ResourceController.executeOnCorePool { [self] in
  121. var previousState: [String: Bool] = [:]
  122. while isRunning {
  123. let interfaces = NetworkManager.getAllInterfaces()
  124. for iface in interfaces {
  125. let wasUp = previousState[iface.name] ?? false
  126. let isNowUp = iface.hasCarrier
  127. // Detect cable plug-in events
  128. if isNowUp && !wasUp {
  129. LogManager.log("NetworkService: Carrier detected on '\(iface.name)' — acquiring address...")
  130. requestDHCP(interface: iface.name)
  131. }
  132. // Detect cable unplug events
  133. if !isNowUp && wasUp {
  134. LogManager.log("NetworkService: Carrier lost on '\(iface.name)'", level: .warn)
  135. }
  136. previousState[iface.name] = isNowUp
  137. }
  138. sleep(pollInterval)
  139. }
  140. }
  141. }
  142. /// Stops the link monitoring loop.
  143. public func stopMonitoring() {
  144. isRunning = false
  145. LogManager.log("NetworkService: Link monitor stopped")
  146. }
  147. // ── Utility ───────────────────────────────────────────────────
  148. /// Execute a shell command and return its exit code.
  149. @discardableResult
  150. private func runCommand(_ cmd: String) -> Int32 {
  151. let result = system(cmd)
  152. return result
  153. }
  154. }