1+ import { spawnSync } from "node:child_process"
12import { createHash , randomBytes } from "node:crypto"
23import os from "node:os"
34import { Server } from "../../server/server"
@@ -21,6 +22,13 @@ type PairPayload = {
2122 hosts : string [ ]
2223}
2324
25+ type TailscaleStatus = {
26+ Self ?: {
27+ DNSName ?: unknown
28+ TailscaleIPs ?: unknown
29+ }
30+ }
31+
2432function ipTier ( address : string ) : number {
2533 const parts = address . split ( "." )
2634 if ( parts . length !== 4 ) return 4
@@ -107,6 +115,38 @@ function secretHash(input: string) {
107115 return `${ createHash ( "sha256" ) . update ( input ) . digest ( "hex" ) . slice ( 0 , 12 ) } ...`
108116}
109117
118+ export function autoTailscaleAdvertiseHost ( hostname : string , status : unknown ) : string | undefined {
119+ const self = ( status as TailscaleStatus | undefined ) ?. Self
120+ if ( ! self ) return
121+
122+ const dnsName = typeof self . DNSName === "string" ? self . DNSName . replace ( / \. + $ / , "" ) : ""
123+ if ( ! dnsName || ! dnsName . toLowerCase ( ) . endsWith ( ".ts.net" ) ) return
124+
125+ if ( hostname === "0.0.0.0" || hostname === "::" || hostname === dnsName ) {
126+ return dnsName
127+ }
128+
129+ const tailscaleIPs = Array . isArray ( self . TailscaleIPs )
130+ ? self . TailscaleIPs . filter ( ( item ) : item is string => typeof item === "string" && item . length > 0 )
131+ : [ ]
132+ if ( tailscaleIPs . includes ( hostname ) ) {
133+ return dnsName
134+ }
135+ }
136+
137+ function readTailscaleAdvertiseHost ( hostname : string ) {
138+ try {
139+ const result = spawnSync ( "tailscale" , [ "status" , "--json" ] , {
140+ encoding : "utf8" ,
141+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
142+ } )
143+ if ( result . status !== 0 || result . error || ! result . stdout . trim ( ) ) return
144+ return autoTailscaleAdvertiseHost ( hostname , JSON . parse ( result . stdout ) )
145+ } catch {
146+ return
147+ }
148+ }
149+
110150async function printPairQR ( pair : PairPayload ) {
111151 const link = pairLink ( pair )
112152 const qrConfig = {
@@ -171,7 +211,14 @@ export const ServeCommand = cmd({
171211 . split ( "," )
172212 . map ( ( item ) => item . trim ( ) )
173213 . filter ( Boolean )
174- const advertiseHosts = [ ...new Set ( [ ...advertiseHostsFromArg , ...advertiseHostsFromEnv ] ) ]
214+ const tailscaleAdvertiseHost = readTailscaleAdvertiseHost ( opts . hostname )
215+ const advertiseHosts = [
216+ ...new Set ( [
217+ ...advertiseHostsFromArg ,
218+ ...advertiseHostsFromEnv ,
219+ ...( tailscaleAdvertiseHost ? [ tailscaleAdvertiseHost ] : [ ] ) ,
220+ ] ) ,
221+ ]
175222
176223 const input = ( args [ "relay-secret" ] ?? process . env . OPENCODE_EXPERIMENTAL_PUSH_RELAY_SECRET ?? "" ) . trim ( )
177224 const relaySecret = input || randomBytes ( 18 ) . toString ( "base64url" )
@@ -180,7 +227,7 @@ export const ServeCommand = cmd({
180227 if ( connectQR ) {
181228 const pairHosts = hosts ( opts . hostname , opts . port > 0 ? opts . port : 4096 , advertiseHosts , false )
182229 if ( ! pairHosts . length ) {
183- console . log ( "connect qr mode requires at least one valid --advertise-host value " )
230+ console . log ( "connect qr mode requires at least one valid advertised host " )
184231 return
185232 }
186233
0 commit comments