NetworkService.swift 7.8 KB

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