iOS: allow unicast DNS-SD discovery domain
This commit is contained in:
@@ -20,8 +20,9 @@ final class BridgeConnectionController {
|
|||||||
self.appModel = appModel
|
self.appModel = appModel
|
||||||
|
|
||||||
BridgeSettingsStore.bootstrapPersistence()
|
BridgeSettingsStore.bootstrapPersistence()
|
||||||
self.discovery.setDebugLoggingEnabled(
|
let defaults = UserDefaults.standard
|
||||||
UserDefaults.standard.bool(forKey: "bridge.discovery.debugLogs"))
|
self.discovery.setDebugLoggingEnabled(defaults.bool(forKey: "bridge.discovery.debugLogs"))
|
||||||
|
self.discovery.setServiceDomain(defaults.string(forKey: "bridge.discovery.domain"))
|
||||||
|
|
||||||
self.updateFromDiscovery()
|
self.updateFromDiscovery()
|
||||||
self.observeDiscovery()
|
self.observeDiscovery()
|
||||||
@@ -35,6 +36,10 @@ final class BridgeConnectionController {
|
|||||||
self.discovery.setDebugLoggingEnabled(enabled)
|
self.discovery.setDebugLoggingEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDiscoveryDomain(_ domain: String?) {
|
||||||
|
self.discovery.setServiceDomain(domain)
|
||||||
|
}
|
||||||
|
|
||||||
func setScenePhase(_ phase: ScenePhase) {
|
func setScenePhase(_ phase: ScenePhase) {
|
||||||
switch phase {
|
switch phase {
|
||||||
case .background:
|
case .background:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ final class BridgeDiscoveryModel {
|
|||||||
private var browser: NWBrowser?
|
private var browser: NWBrowser?
|
||||||
private var debugLoggingEnabled = false
|
private var debugLoggingEnabled = false
|
||||||
private var lastStableIDs = Set<String>()
|
private var lastStableIDs = Set<String>()
|
||||||
|
private var serviceDomain: String = ClawdisBonjour.bridgeServiceDomain
|
||||||
|
|
||||||
func setDebugLoggingEnabled(_ enabled: Bool) {
|
func setDebugLoggingEnabled(_ enabled: Bool) {
|
||||||
let wasEnabled = self.debugLoggingEnabled
|
let wasEnabled = self.debugLoggingEnabled
|
||||||
@@ -39,13 +40,25 @@ final class BridgeDiscoveryModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setServiceDomain(_ domain: String?) {
|
||||||
|
let normalized = ClawdisBonjour.normalizeServiceDomain(domain)
|
||||||
|
guard normalized != self.serviceDomain else { return }
|
||||||
|
self.appendDebugLog("service domain: \(self.serviceDomain) → \(normalized)")
|
||||||
|
self.serviceDomain = normalized
|
||||||
|
|
||||||
|
if self.browser != nil {
|
||||||
|
self.stop()
|
||||||
|
self.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
if self.browser != nil { return }
|
if self.browser != nil { return }
|
||||||
self.appendDebugLog("start()")
|
self.appendDebugLog("start()")
|
||||||
let params = NWParameters.tcp
|
let params = NWParameters.tcp
|
||||||
params.includePeerToPeer = true
|
params.includePeerToPeer = true
|
||||||
let browser = NWBrowser(
|
let browser = NWBrowser(
|
||||||
for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: ClawdisBonjour.bridgeServiceDomain),
|
for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: self.serviceDomain),
|
||||||
using: params)
|
using: params)
|
||||||
|
|
||||||
browser.stateUpdateHandler = { [weak self] state in
|
browser.stateUpdateHandler = { [weak self] state in
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct SettingsTab: View {
|
|||||||
@AppStorage("bridge.manual.enabled") private var manualBridgeEnabled: Bool = false
|
@AppStorage("bridge.manual.enabled") private var manualBridgeEnabled: Bool = false
|
||||||
@AppStorage("bridge.manual.host") private var manualBridgeHost: String = ""
|
@AppStorage("bridge.manual.host") private var manualBridgeHost: String = ""
|
||||||
@AppStorage("bridge.manual.port") private var manualBridgePort: Int = 18790
|
@AppStorage("bridge.manual.port") private var manualBridgePort: Int = 18790
|
||||||
|
@AppStorage("bridge.discovery.domain") private var discoveryDomain: String = ""
|
||||||
@AppStorage("bridge.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false
|
@AppStorage("bridge.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false
|
||||||
@State private var connectStatus = ConnectStatusStore()
|
@State private var connectStatus = ConnectStatusStore()
|
||||||
@State private var connectingBridgeID: String?
|
@State private var connectingBridgeID: String?
|
||||||
@@ -132,6 +133,20 @@ struct SettingsTab: View {
|
|||||||
TextField("Port", value: self.$manualBridgePort, format: .number)
|
TextField("Port", value: self.$manualBridgePort, format: .number)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
|
|
||||||
|
TextField("Discovery Domain", text: self.$discoveryDomain)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.onChange(of: self.discoveryDomain) { _, newValue in
|
||||||
|
self.bridgeController.setDiscoveryDomain(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Default discovery domain is “local.” (mDNS on the same LAN). "
|
||||||
|
+
|
||||||
|
"For Wide-Area Bonjour / Unicast DNS-SD (e.g. over Tailscale), set a unicast DNS zone like “clawdis.internal.” and configure Tailnet split DNS accordingly.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Task { await self.connectManual() }
|
Task { await self.connectManual() }
|
||||||
} label: {
|
} label: {
|
||||||
@@ -179,6 +194,7 @@ struct SettingsTab: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.localIPAddress = Self.primaryIPv4Address()
|
self.localIPAddress = Self.primaryIPv4Address()
|
||||||
|
self.bridgeController.setDiscoveryDomain(self.discoveryDomain)
|
||||||
}
|
}
|
||||||
.onChange(of: self.preferredBridgeStableID) { _, newValue in
|
.onChange(of: self.preferredBridgeStableID) { _, newValue in
|
||||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|||||||
@@ -4,4 +4,18 @@ public enum ClawdisBonjour {
|
|||||||
// v0: internal-only, subject to rename.
|
// v0: internal-only, subject to rename.
|
||||||
public static let bridgeServiceType = "_clawdis-bridge._tcp"
|
public static let bridgeServiceType = "_clawdis-bridge._tcp"
|
||||||
public static let bridgeServiceDomain = "local."
|
public static let bridgeServiceDomain = "local."
|
||||||
|
|
||||||
|
public static func normalizeServiceDomain(_ raw: String?) -> String {
|
||||||
|
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.isEmpty {
|
||||||
|
return self.bridgeServiceDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower = trimmed.lowercased()
|
||||||
|
if lower == "local" || lower == "local." {
|
||||||
|
return self.bridgeServiceDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
return lower.hasSuffix(".") ? lower : (lower + ".")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,75 @@ read_when:
|
|||||||
|
|
||||||
Clawdis uses Bonjour (mDNS / DNS-SD) as a **LAN-only convenience** to discover a running Gateway and (optionally) its bridge transport. It is best-effort and does **not** replace SSH or Tailnet-based connectivity.
|
Clawdis uses Bonjour (mDNS / DNS-SD) as a **LAN-only convenience** to discover a running Gateway and (optionally) its bridge transport. It is best-effort and does **not** replace SSH or Tailnet-based connectivity.
|
||||||
|
|
||||||
|
## Wide-Area Bonjour (Unicast DNS-SD) over Tailscale
|
||||||
|
|
||||||
|
If you want Iris/iPad auto-discovery while the Gateway is on another network (e.g. Vienna ⇄ London), you can keep the `NWBrowser` UX but switch discovery from multicast mDNS (`local.`) to **unicast DNS-SD** (“Wide-Area Bonjour”) over Tailscale.
|
||||||
|
|
||||||
|
High level:
|
||||||
|
|
||||||
|
1) Run a DNS server on the gateway host (reachable via tailnet IP).
|
||||||
|
2) Publish DNS-SD records for `_clawdis-bridge._tcp` in a dedicated zone (example: `clawdis.internal.`).
|
||||||
|
3) Configure Tailscale **split DNS** so `clawdis.internal` resolves via that DNS server for clients (including iOS).
|
||||||
|
4) In Iris: Settings → Bridge → Advanced → set **Discovery Domain** to `clawdis.internal.`
|
||||||
|
|
||||||
|
### Example: CoreDNS on macOS (gateway host)
|
||||||
|
|
||||||
|
On the gateway host (macOS):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install coredns
|
||||||
|
|
||||||
|
sudo mkdir -p /opt/homebrew/etc/coredns
|
||||||
|
sudo tee /opt/homebrew/etc/coredns/Corefile >/dev/null <<'EOF'
|
||||||
|
clawdis.internal:53 {
|
||||||
|
log
|
||||||
|
errors
|
||||||
|
file /opt/homebrew/etc/coredns/clawdis.internal.db
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace `<TAILNET_IPV4>` with the gateway machine’s tailnet IP.
|
||||||
|
sudo tee /opt/homebrew/etc/coredns/clawdis.internal.db >/dev/null <<'EOF'
|
||||||
|
$ORIGIN clawdis.internal.
|
||||||
|
$TTL 60
|
||||||
|
|
||||||
|
@ IN SOA ns.clawdis.internal. hostmaster.clawdis.internal. (
|
||||||
|
2025121701 ; serial
|
||||||
|
60 ; refresh
|
||||||
|
60 ; retry
|
||||||
|
604800 ; expire
|
||||||
|
60 ; minimum
|
||||||
|
)
|
||||||
|
|
||||||
|
@ IN NS ns
|
||||||
|
ns IN A <TAILNET_IPV4>
|
||||||
|
|
||||||
|
gw-london IN A <TAILNET_IPV4>
|
||||||
|
|
||||||
|
_clawdis-bridge._tcp IN PTR ClawdisBridgeLondon._clawdis-bridge._tcp
|
||||||
|
ClawdisBridgeLondon._clawdis-bridge._tcp IN SRV 0 0 18790 gw-london
|
||||||
|
ClawdisBridgeLondon._clawdis-bridge._tcp IN TXT "displayName=Mac Studio (London)"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo brew services start coredns
|
||||||
|
```
|
||||||
|
|
||||||
|
Validate from any tailnet-connected machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dns-sd -B _clawdis-bridge._tcp clawdis.internal.
|
||||||
|
dig @<TAILNET_IPV4> -p 53 _clawdis-bridge._tcp.clawdis.internal PTR +short
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailscale DNS settings
|
||||||
|
|
||||||
|
In the Tailscale admin console:
|
||||||
|
|
||||||
|
- Add a nameserver pointing at the gateway’s tailnet IP (UDP/TCP 53).
|
||||||
|
- Add split DNS so the domain `clawdis.internal` uses that nameserver.
|
||||||
|
|
||||||
|
Once clients accept tailnet DNS, Iris can browse `_clawdis-bridge._tcp` in `clawdis.internal.` without multicast.
|
||||||
|
|
||||||
## What advertises
|
## What advertises
|
||||||
|
|
||||||
Only the **Node Gateway** (`clawd` / `clawdis gateway`) advertises Bonjour beacons.
|
Only the **Node Gateway** (`clawd` / `clawdis gateway`) advertises Bonjour beacons.
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). Iris talks t
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- You can run the Gateway on the “master” machine.
|
- You can run the Gateway on the “master” machine.
|
||||||
- Iris (iOS app) is on the same LAN (Bonjour/mDNS must work).
|
- Iris (iOS app) can reach the gateway bridge:
|
||||||
|
- Same LAN with Bonjour/mDNS, **or**
|
||||||
|
- Same Tailscale tailnet using Wide-Area Bonjour / unicast DNS-SD (see below), **or**
|
||||||
|
- Manual bridge host/port (fallback)
|
||||||
- You can run the CLI (`clawdis`) on the gateway machine (or via SSH).
|
- You can run the CLI (`clawdis`) on the gateway machine (or via SSH).
|
||||||
|
|
||||||
## 1) Start the Gateway (with bridge enabled)
|
## 1) Start the Gateway (with bridge enabled)
|
||||||
@@ -49,6 +52,16 @@ dns-sd -L "<instance name>" _clawdis-bridge._tcp local.
|
|||||||
|
|
||||||
More debugging notes: `docs/bonjour.md`.
|
More debugging notes: `docs/bonjour.md`.
|
||||||
|
|
||||||
|
### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD
|
||||||
|
|
||||||
|
If Iris and the gateway are on different networks but connected via Tailscale, multicast mDNS won’t cross the boundary. Use Wide-Area Bonjour / unicast DNS-SD instead:
|
||||||
|
|
||||||
|
1) Set up a DNS-SD zone (example `clawdis.internal.`) on the gateway host and publish `_clawdis-bridge._tcp` records.
|
||||||
|
2) Configure Tailscale split DNS for `clawdis.internal` pointing at that DNS server.
|
||||||
|
3) In Iris: Settings → Bridge → Advanced → set **Discovery Domain** to `clawdis.internal.`
|
||||||
|
|
||||||
|
Details and example CoreDNS config: `docs/bonjour.md`.
|
||||||
|
|
||||||
## 3) Connect from Iris (iOS)
|
## 3) Connect from Iris (iOS)
|
||||||
|
|
||||||
In Iris:
|
In Iris:
|
||||||
|
|||||||
Reference in New Issue
Block a user