fix(macos): prefer tailnet ip for auto bind
This commit is contained in:
@@ -556,7 +556,10 @@ actor GatewayEndpointStore {
|
|||||||
case "tailnet":
|
case "tailnet":
|
||||||
tailscaleIP ?? "127.0.0.1"
|
tailscaleIP ?? "127.0.0.1"
|
||||||
case "auto":
|
case "auto":
|
||||||
"127.0.0.1"
|
if let tailscaleIP, !tailscaleIP.isEmpty {
|
||||||
|
return tailscaleIP
|
||||||
|
}
|
||||||
|
return "127.0.0.1"
|
||||||
case "custom":
|
case "custom":
|
||||||
customBindHost ?? "127.0.0.1"
|
customBindHost ?? "127.0.0.1"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import AppKit
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import os
|
import os
|
||||||
|
#if canImport(Darwin)
|
||||||
|
import Darwin
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Manages Tailscale integration and status checking.
|
/// Manages Tailscale integration and status checking.
|
||||||
@Observable
|
@Observable
|
||||||
@@ -101,15 +104,12 @@ final class TailscaleService {
|
|||||||
|
|
||||||
func checkTailscaleStatus() async {
|
func checkTailscaleStatus() async {
|
||||||
self.isInstalled = self.checkAppInstallation()
|
self.isInstalled = self.checkAppInstallation()
|
||||||
guard self.isInstalled else {
|
if !self.isInstalled {
|
||||||
self.isRunning = false
|
self.isRunning = false
|
||||||
self.tailscaleHostname = nil
|
self.tailscaleHostname = nil
|
||||||
self.tailscaleIP = nil
|
self.tailscaleIP = nil
|
||||||
self.statusError = "Tailscale is not installed"
|
self.statusError = "Tailscale is not installed"
|
||||||
return
|
} else if let apiResponse = await fetchTailscaleStatus() {
|
||||||
}
|
|
||||||
|
|
||||||
if let apiResponse = await fetchTailscaleStatus() {
|
|
||||||
self.isRunning = apiResponse.status.lowercased() == "running"
|
self.isRunning = apiResponse.status.lowercased() == "running"
|
||||||
|
|
||||||
if self.isRunning {
|
if self.isRunning {
|
||||||
@@ -138,6 +138,15 @@ final class TailscaleService {
|
|||||||
self.statusError = "Please start the Tailscale app"
|
self.statusError = "Please start the Tailscale app"
|
||||||
self.logger.info("Tailscale API not responding; app likely not running")
|
self.logger.info("Tailscale API not responding; app likely not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.tailscaleIP == nil, let fallback = Self.detectTailnetIPv4() {
|
||||||
|
self.tailscaleIP = fallback
|
||||||
|
if !self.isRunning {
|
||||||
|
self.isRunning = true
|
||||||
|
}
|
||||||
|
self.statusError = nil
|
||||||
|
self.logger.info("Tailscale interface IP detected (fallback) ip=\(fallback, privacy: .public)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openTailscaleApp() {
|
func openTailscaleApp() {
|
||||||
@@ -163,4 +172,46 @@ final class TailscaleService {
|
|||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func isTailnetIPv4(_ address: String) -> Bool {
|
||||||
|
let parts = address.split(separator: ".")
|
||||||
|
guard parts.count == 4 else { return false }
|
||||||
|
let octets = parts.compactMap { Int($0) }
|
||||||
|
guard octets.count == 4 else { return false }
|
||||||
|
let a = octets[0]
|
||||||
|
let b = octets[1]
|
||||||
|
return a == 100 && b >= 64 && b <= 127
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func detectTailnetIPv4() -> String? {
|
||||||
|
var addrList: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
|
||||||
|
defer { freeifaddrs(addrList) }
|
||||||
|
|
||||||
|
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
|
||||||
|
let flags = Int32(ptr.pointee.ifa_flags)
|
||||||
|
let isUp = (flags & IFF_UP) != 0
|
||||||
|
let isLoopback = (flags & IFF_LOOPBACK) != 0
|
||||||
|
let family = ptr.pointee.ifa_addr.pointee.sa_family
|
||||||
|
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
|
||||||
|
|
||||||
|
var addr = ptr.pointee.ifa_addr.pointee
|
||||||
|
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
let result = getnameinfo(
|
||||||
|
&addr,
|
||||||
|
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
|
||||||
|
&buffer,
|
||||||
|
socklen_t(buffer.count),
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
NI_NUMERICHOST)
|
||||||
|
guard result == 0 else { continue }
|
||||||
|
let len = buffer.prefix { $0 != 0 }
|
||||||
|
let bytes = len.map { UInt8(bitPattern: $0) }
|
||||||
|
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
|
||||||
|
if Self.isTailnetIPv4(ip) { return ip }
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import ClawdbotKit
|
import ClawdbotKit
|
||||||
import ClawdbotProtocol
|
import ClawdbotProtocol
|
||||||
import Foundation
|
import Foundation
|
||||||
|
#if canImport(Darwin)
|
||||||
|
import Darwin
|
||||||
|
#endif
|
||||||
|
|
||||||
struct ConnectOptions {
|
struct ConnectOptions {
|
||||||
var url: String?
|
var url: String?
|
||||||
@@ -268,7 +271,7 @@ private func resolveGatewayEndpoint(opts: ConnectOptions, config: GatewayConfig)
|
|||||||
}
|
}
|
||||||
|
|
||||||
let port = config.port ?? 18789
|
let port = config.port ?? 18789
|
||||||
let host = "127.0.0.1"
|
let host = resolveLocalHost(bind: config.bind)
|
||||||
guard let url = URL(string: "ws://\(host):\(port)") else {
|
guard let url = URL(string: "ws://\(host):\(port)") else {
|
||||||
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: ws://\(host):\(port)"])
|
throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: ws://\(host):\(port)"])
|
||||||
}
|
}
|
||||||
@@ -304,3 +307,56 @@ private func resolvedPassword(opts: ConnectOptions, mode: String, config: Gatewa
|
|||||||
}
|
}
|
||||||
return config.password
|
return config.password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resolveLocalHost(bind: String?) -> String {
|
||||||
|
let normalized = (bind ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
let tailnetIP = detectTailnetIPv4()
|
||||||
|
switch normalized {
|
||||||
|
case "tailnet", "auto":
|
||||||
|
return tailnetIP ?? "127.0.0.1"
|
||||||
|
default:
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func detectTailnetIPv4() -> String? {
|
||||||
|
var addrList: UnsafeMutablePointer<ifaddrs>?
|
||||||
|
guard getifaddrs(&addrList) == 0, let first = addrList else { return nil }
|
||||||
|
defer { freeifaddrs(addrList) }
|
||||||
|
|
||||||
|
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
|
||||||
|
let flags = Int32(ptr.pointee.ifa_flags)
|
||||||
|
let isUp = (flags & IFF_UP) != 0
|
||||||
|
let isLoopback = (flags & IFF_LOOPBACK) != 0
|
||||||
|
let family = ptr.pointee.ifa_addr.pointee.sa_family
|
||||||
|
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
|
||||||
|
|
||||||
|
var addr = ptr.pointee.ifa_addr.pointee
|
||||||
|
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
let result = getnameinfo(
|
||||||
|
&addr,
|
||||||
|
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
|
||||||
|
&buffer,
|
||||||
|
socklen_t(buffer.count),
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
NI_NUMERICHOST)
|
||||||
|
guard result == 0 else { continue }
|
||||||
|
let len = buffer.prefix { $0 != 0 }
|
||||||
|
let bytes = len.map { UInt8(bitPattern: $0) }
|
||||||
|
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
|
||||||
|
if isTailnetIPv4(ip) { return ip }
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isTailnetIPv4(_ address: String) -> Bool {
|
||||||
|
let parts = address.split(separator: ".")
|
||||||
|
guard parts.count == 4 else { return false }
|
||||||
|
let octets = parts.compactMap { Int($0) }
|
||||||
|
guard octets.count == 4 else { return false }
|
||||||
|
let a = octets[0]
|
||||||
|
let b = octets[1]
|
||||||
|
return a == 100 && b >= 64 && b <= 127
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user