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 <percy@tailscale.com>
This commit is contained in:
Percy Wegmann 2025-09-29 09:48:41 -05:00 committed by Percy Wegmann
parent 90629cdeb4
commit 75aaac9f12
5 changed files with 92 additions and 4 deletions

View file

@ -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

View file

@ -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.
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.

View file

@ -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'

35
dist/index.js vendored
View file

@ -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) {

View file

@ -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<void> {
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<void> {
} 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<void> {
}
}
async function pingHostsIfNecessary(config: TailscaleConfig): Promise<void> {
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<TailscaleConfig> {
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<TailscaleConfig> {
retry: parseInt(core.getInput("retry") || "5"),
useCache: core.getBooleanInput("use-cache"),
sha256Sum: core.getInput("sha256sum") || "",
pingHosts: pingHosts,
};
}