From c212a5d18cc8d0ca188c139e745a8a5ab8e37e28 Mon Sep 17 00:00:00 2001 From: Percy Wegmann Date: Mon, 29 Sep 2025 09:48:41 -0500 Subject: [PATCH] add support for testing peer connectivity The new argument `ping` allows users to specify a comma-seperated list of hosts (IP or hostname) to ping in order to verify connectivity. Ping is considered successful as soon as the peer is reachable either directly or via DERP. Updates tailscale/corp#32817 Signed-off-by: Percy Wegmann --- .github/workflows/test.yml | 4 ++++ README.md | 5 ++++- action.yml | 6 ++++- dist/index.js | 35 ++++++++++++++++++++++++++++- src/main.ts | 46 +++++++++++++++++++++++++++++++++++++- 5 files changed, 92 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfd8508..c6040c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,7 @@ jobs: runner-os: Linux arch: amd64 version: latest + ping: 100.99.0.2,lax-pve.pineapplefish.ts.net,lax-pve # Try unstable too - os: ubuntu-latest @@ -45,6 +46,7 @@ jobs: runner-os: Windows arch: amd64 version: latest + ping: 100.99.0.2,lax-pve.pineapplefish.ts.net,lax-pve - os: windows-latest runner-os: Windows @@ -62,6 +64,7 @@ jobs: runner-os: macOS arch: amd64 version: latest + ping: 100.99.0.2 # hostnames aren't resolving on MacOS, just ping IP lax-pve.pineapplefish.ts.net,lax-pve # macOS ARM - os: macos-14 @@ -99,6 +102,7 @@ jobs: use-cache: false timeout: "5m" retry: 3 + ping: "${{ matrix.ping }}" # Test Tailscale status command - name: Check Tailscale Status diff --git a/README.md b/README.md index 83165c6..22031fd 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ This action is written in Typescript using official GitHub SDKs. It provides som | `tailscaled-args` | Additional `tailscaled` arguments | false | | | `statedir` | State directory (if empty, uses memory) | false | | | `sha256sum` | Expected SHA256 checksum | false | | +| `ping` | Comma separated list of hosts (Tailscale IP addresses or machine names if MagicDNS is enabled on the tailnet) to `tailscale ping` for connectivity verification after `tailscale up` completes | false | | ## Authentication @@ -209,4 +210,6 @@ For security issues, please see our [security policy](SECURITY.md). ## CI Notes -CI tests run against the tailscalegithubactionbot.github tailnet. Check our usual credential store for credentials. \ No newline at end of file +CI tests run against the pineapplefish-tailnet.org.github tailnet. Check our usual credential store for credentials. + +`tag:ci` must have access to the `lax-pve` server. \ No newline at end of file diff --git a/action.yml b/action.yml index 7a5df69..7288802 100644 --- a/action.yml +++ b/action.yml @@ -53,7 +53,11 @@ inputs: description: 'Expected SHA256 checksum of the Tailscale package. If not provided, it will be fetched automatically.' required: false default: '' - + ping: + description: 'Comma separated list of hosts (Tailscale IP addresses or machine names if MagicDNS is enabled on the tailnet) to `tailscale ping` for connectivity verification after `tailscale up` completes.' + required: false + default: '' + runs: using: 'node20' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index e211f71..ddca8b5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41241,6 +41241,7 @@ async function run() { core.debug(`Tailscale status: ${JSON.stringify(status)}`); if (status.BackendState === "Running") { core.info("✅ Tailscale is running and connected!"); + pingHostsIfNecessary(config); // Explicitly exit to prevent hanging process.exit(0); } @@ -41252,7 +41253,9 @@ async function run() { catch (err) { core.warning(`Failed to get Tailscale status: ${err}`); // Still exit successfully since the main connection worked - core.info("✅ Tailscale connection completed successfully!"); + core.info("✅ Tailscale daemon is connected!"); + pingHostsIfNecessary(config); + // Explicitly exit to prevent hanging process.exit(0); } } @@ -41260,7 +41263,36 @@ async function run() { core.setFailed(error instanceof Error ? error.message : String(error)); } } +async function pingHostsIfNecessary(config) { + const directConnectionWarning = "direct connection not established"; + if (config.pingHosts.length == 0) { + return; + } + core.info(`Will ping hosts ${config.pingHosts.join(",")} up to 3 minutes in order to check connectivity`); + for (const host of config.pingHosts) { + core.info(`Pinging host ${host}`); + let result = await exec.getExecOutput(cmdTailscale, [ + "ping", + "-c", + "36", + host, + ]); + if (result.exitCode === 0) { + core.info(`✅ Ping host ${host} responded!`); + } + else if (result.stderr.includes(directConnectionWarning) || + result.stdout.includes(directConnectionWarning)) { + core.warning(`⚠️ Ping host ${host} reachable only via DERP, not direct connection.`); + } + else { + core.setFailed(`❌ Ping host ${host} did not respond`); + process.exit(1); + } + } +} async function getInputs() { + let ping = core.getInput("ping"); + let pingHosts = ping?.length > 0 ? ping.split(",") : []; return { version: core.getInput("version") || "1.82.0", resolvedVersion: "", @@ -41277,6 +41309,7 @@ async function getInputs() { retry: parseInt(core.getInput("retry") || "5"), useCache: core.getBooleanInput("use-cache"), sha256Sum: core.getInput("sha256sum") || "", + pingHosts: pingHosts, }; } function validateAuth(config) { diff --git a/src/main.ts b/src/main.ts index 2410981..01c5db8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,6 +40,7 @@ interface TailscaleConfig { retry: number; useCache: boolean; sha256Sum: string; + pingHosts: string[]; } // Cross-platform Tailscale local API status check @@ -166,6 +167,7 @@ async function run(): Promise { core.debug(`Tailscale status: ${JSON.stringify(status)}`); if (status.BackendState === "Running") { core.info("✅ Tailscale is running and connected!"); + pingHostsIfNecessary(config); // Explicitly exit to prevent hanging process.exit(0); } else { @@ -175,7 +177,9 @@ async function run(): Promise { } catch (err) { core.warning(`Failed to get Tailscale status: ${err}`); // Still exit successfully since the main connection worked - core.info("✅ Tailscale connection completed successfully!"); + core.info("✅ Tailscale daemon is connected!"); + pingHostsIfNecessary(config); + // Explicitly exit to prevent hanging process.exit(0); } } catch (error) { @@ -183,7 +187,46 @@ async function run(): Promise { } } +async function pingHostsIfNecessary(config: TailscaleConfig): Promise { + const directConnectionWarning = "direct connection not established"; + + if (config.pingHosts.length == 0) { + return; + } + + core.info( + `Will ping hosts ${config.pingHosts.join( + "," + )} up to 3 minutes in order to check connectivity` + ); + for (const host of config.pingHosts) { + core.info(`Pinging host ${host}`); + let result = await exec.getExecOutput(cmdTailscale, [ + "ping", + "-c", + "36", + host, + ]); + if (result.exitCode === 0) { + core.info(`✅ Ping host ${host} responded!`); + } else if ( + result.stderr.includes(directConnectionWarning) || + result.stdout.includes(directConnectionWarning) + ) { + core.warning( + `⚠️ Ping host ${host} reachable only via DERP, not direct connection.` + ); + } else { + core.setFailed(`❌ Ping host ${host} did not respond`); + process.exit(1); + } + } +} + async function getInputs(): Promise { + let ping = core.getInput("ping"); + let pingHosts = ping?.length > 0 ? ping.split(",") : []; + return { version: core.getInput("version") || "1.82.0", resolvedVersion: "", @@ -200,6 +243,7 @@ async function getInputs(): Promise { retry: parseInt(core.getInput("retry") || "5"), useCache: core.getBooleanInput("use-cache"), sha256Sum: core.getInput("sha256sum") || "", + pingHosts: pingHosts, }; }