tailscale 模板创建 Sandbox,该模板已预装 Tailscale,开箱即用。
本指南介绍三种将 Sandbox 接入 tailnet 的方式:
- 浏览器登录——运行
tailscale up,在浏览器中打开输出的登录 URL 以授权该 Sandbox,即可加入你的 tailnet。适合交互式的一次性配置。 - Tailscale auth key——使用预先生成的 auth key 非交互式接入。适合自动化脚本、CI/CD 流水线或任何无法手动操作浏览器的场景。
- 手动安装——当你从
tailscale之外的模板(如base)启动时,需要自行安装 Tailscale 并应用所需的 workaround。
前置条件
pip install ppio-sandbox(或npm i ppio-sandbox)export PPIO_API_KEY=...- 一个用于授权登录的 Tailscale 账号
浏览器登录
在前台运行tailscale up 且不设超时。命令会阻塞并打印一个登录 URL;在浏览器中打开该 URL 完成授权,sandbox 连接成功后命令返回。随后读取分配到的 Tailscale IP。
import os
import time
from ppio_sandbox import PPIO
ppio = PPIO()
sandbox = ppio.sandbox.create(template="tailscale")
print("Sandbox created:", sandbox.sandbox_id)
# hostname 优先取环境变量 SANDBOX_ID,为空时回退到 no-name-sandbox-<unix time>
hostname = os.environ.get("SANDBOX_ID") or f"no-name-sandbox-{int(time.time())}"
try:
# 在前台运行 `tailscale up` 并流式输出其结果。
# 命令会阻塞直到你在浏览器中完成授权,期间会将登录
# URL 打印到 stderr,连接成功后返回。
print("\n=== Open the login URL below in your browser to authorize ===")
sandbox.commands.run(
f"sudo tailscale up --hostname={hostname}",
on_stdout=lambda data: print(data, end=""),
on_stderr=lambda data: print(data, end=""),
timeout=0, # no limit — wait for the interactive browser login
)
print("=" * 60)
# 显示分配到的 Tailscale IP。
ip = sandbox.commands.run("sudo tailscale ip -4 || true")
print("Connected! Tailscale IP:", ip.stdout.strip())
# 保持 Sandbox 存活,直到用户中断。
print("\nSandbox is connected. Press Ctrl+C to disconnect and kill it.")
try:
while True:
time.sleep(3600)
except KeyboardInterrupt:
print("\nInterrupted — shutting down.")
finally:
sandbox.kill()
print("Sandbox killed")
import { PPIO } from 'ppio-sandbox'
const ppio = new PPIO()
async function main() {
const sandbox = await ppio.sandbox.create({
template: "tailscale",
})
console.log("Sandbox created:", sandbox.sandboxId)
// hostname 优先取环境变量 SANDBOX_ID,为空时回退到 no-name-sandbox-<unix time>
const hostname = process.env.SANDBOX_ID || `no-name-sandbox-${Math.floor(Date.now() / 1000)}`
try {
// 在前台运行 `tailscale up` 并流式输出其结果。
// 命令会阻塞直到你在浏览器中完成授权,期间会将登录
// URL 打印到 stderr,连接成功后返回。
console.log("\n=== Open the login URL below in your browser to authorize ===")
await sandbox.commands.run(`sudo tailscale up --hostname=${hostname}`, {
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stdout.write(data),
timeoutMs: 0, // no limit — wait for the interactive browser login
})
console.log("=".repeat(60))
// 显示分配到的 Tailscale IP。
const ip = await sandbox.commands.run("sudo tailscale ip -4 || true")
console.log("Connected! Tailscale IP:", ip.stdout.trim())
// 保持 Sandbox 存活,直到用户中断。
console.log("\nSandbox is connected. Press Ctrl+C to disconnect and kill it.")
await new Promise(() => {})
} finally {
await sandbox.kill()
console.log("Sandbox killed")
}
}
main().catch(console.error)
Tailscale auth key
使用 auth key 可以以非交互方式将 PPIO sandbox 接入 Tailscale,适合自动化脚本、CI/CD 流水线或任何无法手动操作浏览器的场景。- 进入你的 Tailscale 管理控制台。
- 点击 Add device 并选择 Linux server。
- 应用配置并点击 Generate install script。
curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --auth-key=<AUTH_KEY>
import os
import time
from ppio_sandbox import PPIO
ppio = PPIO()
AUTH_KEY = os.environ["TS_AUTH_KEY"] # tskey-auth-...
sandbox = ppio.sandbox.create(template="tailscale")
print("Sandbox created:", sandbox.sandbox_id)
# hostname 优先取环境变量 SANDBOX_ID,为空时回退到 no-name-sandbox-<unix time>
hostname = os.environ.get("SANDBOX_ID") or f"no-name-sandbox-{int(time.time())}"
result = sandbox.commands.run(
f"sudo tailscale up --auth-key={AUTH_KEY} --hostname={hostname}",
on_stdout=lambda data: print(data, end=""),
on_stderr=lambda data: print(data, end=""),
timeout=120,
)
if result.exit_code != 0:
raise RuntimeError(f"tailscale up failed:\n{result.stderr}")
ip = sandbox.commands.run("sudo tailscale ip -4 || true")
print("Connected! Tailscale IP:", ip.stdout.strip())
import { PPIO } from 'ppio-sandbox'
const ppio = new PPIO()
const AUTH_KEY = process.env.TS_AUTH_KEY // tskey-auth-...
const sandbox = await ppio.sandbox.create({
template: "tailscale",
})
console.log("Sandbox created:", sandbox.sandboxId)
// hostname 优先取环境变量 SANDBOX_ID,为空时回退到 no-name-sandbox-<unix time>
const hostname = process.env.SANDBOX_ID || `no-name-sandbox-${Math.floor(Date.now() / 1000)}`
const result = await sandbox.commands.run(
`sudo tailscale up --auth-key=${AUTH_KEY} --hostname=${hostname}`,
{
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stdout.write(data),
timeoutMs: 120_000,
}
)
if (result.exitCode !== 0) {
throw new Error(`tailscale up failed:\n${result.stderr}`)
}
const ip = await sandbox.commands.run("sudo tailscale ip -4 || true")
console.log("Connected! Tailscale IP:", ip.stdout.trim())
手动安装
tailscale 模板已包含 Tailscale 及下文所述的 workaround,因此浏览器登录开箱即用。如果你从其他模板(如 base)启动,则需要自行安装 Tailscale 并应用该 workaround,tailscale up 才能正常工作。
**为什么需要这些额外步骤?**sandbox 的
eth0 使用链路本地地址(169.254.x.x)。Tailscale 的 isUsableV4 检查会将链路本地地址视为”不可用于访问互联网”——但在 AWS Lambda / Azure App Service 环境中除外。因此 tailscaled 会报告 network is down,tailscale up 一直挂起且不会打印登录 URL。解决办法是注入四个 AWS_LAMBDA_* 环境变量,让 tailscaled 以为自己运行在 AWS Lambda 中,从而使 isUsableV4 接受链路本地地址。import os
import time
from ppio_sandbox import PPIO
ppio = PPIO()
sandbox = ppio.sandbox.create(template="base")
print("Sandbox created:", sandbox.sandbox_id)
# hostname 优先取环境变量 SANDBOX_ID,为空时回退到 no-name-sandbox-<unix time>
hostname = os.environ.get("SANDBOX_ID") or f"no-name-sandbox-{int(time.time())}"
# 1. 安装 Tailscale。
print("Installing Tailscale...")
install = sandbox.commands.run(
"curl -fsSL https://tailscale.com/install.sh | sh",
timeout=300,
)
if install.exit_code != 0:
raise RuntimeError(f"Install failed:\n{install.stderr}")
# 2. 添加 systemd override,让 tailscaled 以 AWS Lambda 环境变量启动,
# 然后通过 systemd(重新)启动它。这是针对上文所述
# link-local 地址问题的解决方法。
print("Configuring tailscaled systemd override...")
override = """[Service]
Environment="AWS_LAMBDA_FUNCTION_NAME=x"
Environment="AWS_LAMBDA_FUNCTION_VERSION=1"
Environment="AWS_LAMBDA_INITIALIZATION_TYPE=on-demand"
Environment="AWS_LAMBDA_RUNTIME_API=127.0.0.1:9001"
"""
sandbox.commands.run("sudo mkdir -p /etc/systemd/system/tailscaled.service.d")
sandbox.files.write(
"/etc/systemd/system/tailscaled.service.d/override.conf",
override,
)
print("Reloading systemd and (re)starting tailscaled...")
sandbox.commands.run("sudo systemctl daemon-reload")
sandbox.commands.run("sudo systemctl restart tailscaled")
time.sleep(3)
# 3. 现在登录。`tailscale up` 会打印一个登录 URL;在浏览器中打开它。
print("\n=== Open the login URL below in your browser to authorize ===")
sandbox.commands.run(
f"sudo tailscale up --hostname={hostname}",
on_stdout=lambda data: print(data, end=""),
on_stderr=lambda data: print(data, end=""),
timeout=0,
)
ip = sandbox.commands.run("sudo tailscale ip -4 || true")
print("Connected! Tailscale IP:", ip.stdout.strip())
import { PPIO } from 'ppio-sandbox'
const ppio = new PPIO()
const sandbox = await ppio.sandbox.create({
template: "base",
})
console.log("Sandbox created:", sandbox.sandboxId)
// hostname 优先取环境变量 SANDBOX_ID,为空时回退到 no-name-sandbox-<unix time>
const hostname = process.env.SANDBOX_ID || `no-name-sandbox-${Math.floor(Date.now() / 1000)}`
// 1. 安装 Tailscale。
console.log("Installing Tailscale...")
const install = await sandbox.commands.run(
"curl -fsSL https://tailscale.com/install.sh | sh",
{ timeoutMs: 300_000 }
)
if (install.exitCode !== 0) {
throw new Error(`Install failed:\n${install.stderr}`)
}
// 2. 添加 systemd override,让 tailscaled 以 AWS Lambda 环境变量启动,
// 然后通过 systemd(重新)启动它。这是针对上文所述
// link-local 地址问题的解决方法。
console.log("Configuring tailscaled systemd override...")
const override = `[Service]
Environment="AWS_LAMBDA_FUNCTION_NAME=x"
Environment="AWS_LAMBDA_FUNCTION_VERSION=1"
Environment="AWS_LAMBDA_INITIALIZATION_TYPE=on-demand"
Environment="AWS_LAMBDA_RUNTIME_API=127.0.0.1:9001"
`
await sandbox.commands.run("sudo mkdir -p /etc/systemd/system/tailscaled.service.d")
await sandbox.files.write(
"/etc/systemd/system/tailscaled.service.d/override.conf",
override,
)
console.log("Reloading systemd and (re)starting tailscaled...")
await sandbox.commands.run("sudo systemctl daemon-reload")
await sandbox.commands.run("sudo systemctl restart tailscaled")
await new Promise((r) => setTimeout(r, 3000))
// 3. 现在登录。`tailscale up` 会打印一个登录 URL;在浏览器中打开它。
console.log("\n=== Open the login URL below in your browser to authorize ===")
await sandbox.commands.run(`sudo tailscale up --hostname=${hostname}`, {
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stdout.write(data),
timeoutMs: 0,
})
const ip = await sandbox.commands.run("sudo tailscale ip -4 || true")
console.log("Connected! Tailscale IP:", ip.stdout.trim())