fix: advertise MagicDNS hosts for Tailscale pairing
Prefer .ts.net names and allow Tailscale CGNAT HTTP on iOS so mobile pairing avoids ATS-blocked raw tailnet IPs.pull/19545/head
parent
4abb464345
commit
0f58efe030
|
|
@ -24,6 +24,9 @@
|
||||||
"NSAppTransportSecurity": {
|
"NSAppTransportSecurity": {
|
||||||
"NSAllowsLocalNetworking": true,
|
"NSAllowsLocalNetworking": true,
|
||||||
"NSExceptionDomains": {
|
"NSExceptionDomains": {
|
||||||
|
"100.64.0.0/10": {
|
||||||
|
"NSExceptionAllowsInsecureHTTPLoads": true
|
||||||
|
},
|
||||||
"ts.net": {
|
"ts.net": {
|
||||||
"NSIncludesSubdomains": true,
|
"NSIncludesSubdomains": true,
|
||||||
"NSExceptionAllowsInsecureHTTPLoads": true
|
"NSExceptionAllowsInsecureHTTPLoads": true
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { spawnSync } from "node:child_process"
|
||||||
import { createHash, randomBytes } from "node:crypto"
|
import { createHash, randomBytes } from "node:crypto"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
|
|
@ -21,6 +22,13 @@ type PairPayload = {
|
||||||
hosts: string[]
|
hosts: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TailscaleStatus = {
|
||||||
|
Self?: {
|
||||||
|
DNSName?: unknown
|
||||||
|
TailscaleIPs?: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ipTier(address: string): number {
|
function ipTier(address: string): number {
|
||||||
const parts = address.split(".")
|
const parts = address.split(".")
|
||||||
if (parts.length !== 4) return 4
|
if (parts.length !== 4) return 4
|
||||||
|
|
@ -107,6 +115,38 @@ function secretHash(input: string) {
|
||||||
return `${createHash("sha256").update(input).digest("hex").slice(0, 12)}...`
|
return `${createHash("sha256").update(input).digest("hex").slice(0, 12)}...`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function autoTailscaleAdvertiseHost(hostname: string, status: unknown): string | undefined {
|
||||||
|
const self = (status as TailscaleStatus | undefined)?.Self
|
||||||
|
if (!self) return
|
||||||
|
|
||||||
|
const dnsName = typeof self.DNSName === "string" ? self.DNSName.replace(/\.+$/, "") : ""
|
||||||
|
if (!dnsName || !dnsName.toLowerCase().endsWith(".ts.net")) return
|
||||||
|
|
||||||
|
if (hostname === "0.0.0.0" || hostname === "::" || hostname === dnsName) {
|
||||||
|
return dnsName
|
||||||
|
}
|
||||||
|
|
||||||
|
const tailscaleIPs = Array.isArray(self.TailscaleIPs)
|
||||||
|
? self.TailscaleIPs.filter((item): item is string => typeof item === "string" && item.length > 0)
|
||||||
|
: []
|
||||||
|
if (tailscaleIPs.includes(hostname)) {
|
||||||
|
return dnsName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTailscaleAdvertiseHost(hostname: string) {
|
||||||
|
try {
|
||||||
|
const result = spawnSync("tailscale", ["status", "--json"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
})
|
||||||
|
if (result.status !== 0 || result.error || !result.stdout.trim()) return
|
||||||
|
return autoTailscaleAdvertiseHost(hostname, JSON.parse(result.stdout))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function printPairQR(pair: PairPayload) {
|
async function printPairQR(pair: PairPayload) {
|
||||||
const link = pairLink(pair)
|
const link = pairLink(pair)
|
||||||
const qrConfig = {
|
const qrConfig = {
|
||||||
|
|
@ -171,7 +211,14 @@ export const ServeCommand = cmd({
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
const advertiseHosts = [...new Set([...advertiseHostsFromArg, ...advertiseHostsFromEnv])]
|
const tailscaleAdvertiseHost = readTailscaleAdvertiseHost(opts.hostname)
|
||||||
|
const advertiseHosts = [
|
||||||
|
...new Set([
|
||||||
|
...advertiseHostsFromArg,
|
||||||
|
...advertiseHostsFromEnv,
|
||||||
|
...(tailscaleAdvertiseHost ? [tailscaleAdvertiseHost] : []),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
const input = (args["relay-secret"] ?? process.env.OPENCODE_EXPERIMENTAL_PUSH_RELAY_SECRET ?? "").trim()
|
const input = (args["relay-secret"] ?? process.env.OPENCODE_EXPERIMENTAL_PUSH_RELAY_SECRET ?? "").trim()
|
||||||
const relaySecret = input || randomBytes(18).toString("base64url")
|
const relaySecret = input || randomBytes(18).toString("base64url")
|
||||||
|
|
@ -180,7 +227,7 @@ export const ServeCommand = cmd({
|
||||||
if (connectQR) {
|
if (connectQR) {
|
||||||
const pairHosts = hosts(opts.hostname, opts.port > 0 ? opts.port : 4096, advertiseHosts, false)
|
const pairHosts = hosts(opts.hostname, opts.port > 0 ? opts.port : 4096, advertiseHosts, false)
|
||||||
if (!pairHosts.length) {
|
if (!pairHosts.length) {
|
||||||
console.log("connect qr mode requires at least one valid --advertise-host value")
|
console.log("connect qr mode requires at least one valid advertised host")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { autoTailscaleAdvertiseHost } from "../../src/cli/cmd/serve"
|
||||||
|
|
||||||
|
describe("autoTailscaleAdvertiseHost", () => {
|
||||||
|
const status = {
|
||||||
|
Self: {
|
||||||
|
DNSName: "exos.husky-tilapia.ts.net.",
|
||||||
|
TailscaleIPs: ["100.76.251.88", "fd7a:115c:a1e0::435:fb58"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test("advertises the MagicDNS hostname for all-interface listeners", () => {
|
||||||
|
expect(autoTailscaleAdvertiseHost("0.0.0.0", status)).toBe("exos.husky-tilapia.ts.net")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("advertises the MagicDNS hostname for Tailscale-bound listeners", () => {
|
||||||
|
expect(autoTailscaleAdvertiseHost("100.76.251.88", status)).toBe("exos.husky-tilapia.ts.net")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips the MagicDNS hostname for unrelated listeners", () => {
|
||||||
|
expect(autoTailscaleAdvertiseHost("192.168.1.20", status)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue