> ## Documentation Index
> Fetch the complete documentation index at: https://ppio.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tailscale

Tailscale 可以将 PPIO Sandbox 接入你的私有 tailnet,使 Sandbox 能够安全地访问网络中的其他设备,也能被其他设备访问。请使用 `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。

<CodeGroup>
  ```python Python theme={null}
  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")
  ```

  ```javascript JavaScript & TypeScript theme={null}
  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)
  ```
</CodeGroup>

***

## Tailscale auth key

使用 auth key 可以以非交互方式将 PPIO sandbox 接入 Tailscale,适合自动化脚本、CI/CD 流水线或任何无法手动操作浏览器的场景。

1. 进入你的 [Tailscale 管理控制台](https://console.tailscale.com/admin/machines)。
2. 点击 **Add device** 并选择 **Linux server**。
3. 应用配置并点击 **Generate install script**。

这会生成一段脚本,用于安装 Tailscale 并接入 Tailscale 网络:

```bash theme={null}
curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --auth-key=<AUTH_KEY>
```

通过 SDK 在 sandbox 内运行该命令。由于 auth key 以非交互方式登录,无需浏览器步骤。

<CodeGroup>
  ```python Python theme={null}
  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())
  ```

  ```javascript JavaScript & TypeScript theme={null}
  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())
  ```
</CodeGroup>

***

## 手动安装

`tailscale` 模板已包含 Tailscale 及下文所述的 workaround,因此浏览器登录开箱即用。如果你从其他模板(如 `base`)启动,则需要自行安装 Tailscale 并应用该 workaround,`tailscale up` 才能正常工作。

<Warning>
  \*\*为什么需要这些额外步骤?\*\*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` 接受链路本地地址。
</Warning>

<CodeGroup>
  ```python Python theme={null}
  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())
  ```

  ```javascript JavaScript & TypeScript theme={null}
  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())
  ```
</CodeGroup>
