OpenWrt の Tailscale アップデートスクリプト

## TL;DR

この記事は、フラッシュメモリの容量がそこそこ(潤沢でも極少でもない)の OpenWrt 機器で [Tailscale](https://tailscale.com/kb/1017/install) のアップデートをするための手順です。 
(具体的には、 `df -h /overlay` での `Size` 表示が、 40MB ～ 80MB の OpenWrt 機器を想定しています。)

より厳しい容量環境下の場合、以下の記事を参考にしてください。

1. Tailscale をインストールすると、ほとんど空き容量に余裕がなくなる → [OpenWrt で縮小版 Tailscale を使用する](https://qiita.com/Bacalhau/items/b59bc86f67e58444b39e)
1. Tailscale をインストールしなくても、10MB 程度の空き容量しかない → [フラッシュメモリの空き容量が 10MB しかない OpenWrt で Tailscale を使用する](https://qiita.com/Bacalhau/items/26bf74627532275f8616)

## この記事の概要

フラッシュメモリの空き容量に余裕があれば、 OpenWrt 機器でも `tailscale update` で簡単に Tailscale のアップデートができるのですが、多くの OpenWrt 機器では `tailscale update` を実行するのに必要なフラッシュメモリがありません。

そこで以下の手順を実行するコードを作成したので紹介します。

1. Tailscale の停止 ( `service tailscale stop` の実行)
1. バイナリファイルを削除 ( `/usr/sbin/tailscale` , `/usr/sbin/tailscaled` の削除)
1. <https://pkgs.tailscale.com/stable/#static> から tgz ファイルをダウンロード
1. tgz ファイルを解凍しバイナリファイル ( `tailscale` , `tailscaled` ) を `/usr/sbin/` に保存
1. Tailscale の再開 ( `service tailscale start` の実行)

<details>
<summary>参考にしたコード</summary>

もともとは @myurar1a さんが書かれた[コード](https://qiita.com/myurar1a/items/50f39db87f27dca23d1a)を使って Tailscale のアップデートをしていたのですが、 [Tailscale 再開時のオプション指定は不要なため](https://nwgat.ninja/install-or-upgrade-tailscale-on-openwrt-arm64-aarch64/)、そこを考慮して @hide_seki さんが書かれた改良部分を取り入れてアップデートしました。

- [元祖アップデートスクリプト](https://qiita.com/myurar1a/items/50f39db87f27dca23d1a) by @myurar1a さん

- [容量不足にならないように tailscale バイナリを 1 つずつ取り出すスクリプト](https://qiita.com/hide_seki/items/041ebf5755e7cf82da90#%E6%9C%80%E6%96%B0tailscale%E3%81%AE%E5%B0%8E%E5%85%A5%E6%96%B9%E6%B3%95) by @hide_seki さん

</details>

手順としては、[Perplexity](https://www.perplexity.ai/) に相談しながらコードを書き、最終的には [GPT-4o](https://tenbin.ai/) に英文への翻訳・校正とリファクタリングをしてもらっています。🎉

<details>
<summary>👉 公式のアップデート手順の `tailscale update` を実行し、 `no space left on device` が表示されてしまった場合の対応手順 😱</summary>

以下のコマンドを実行

```shell
rm /root/.cache/tailscale-update/tailscale*.tgz*
rm /usr/sbin/tailscale*.new*
```

ファイルの削除前後に `df -h` を実行して、空き容量が回復することを確認してください。🎉

</details>

## 注意事項

- 今回のコードは 64-bit ARM CPU でしか動作しません。  
  64-bit ARM CPU に限定した理由は以下の通り。

  - MIPS 系の CPU では Tailscale の実行は重すぎるはずなので、MIPS 系の CPU は除外
  - 32-bit ARM CPU は世代的に古く、現行の MIPS 以上に演算能力が低いので、こちらも除外
  - AMD64 の CPU であれば、`tailscale update` が実行できる程度に容量に余裕があるはずなので、これも除外

  <br>

- Tailscale を使って SSH 接続していても今回のコードの実行はできますが、以下の表示が出た後 1 分程度反応がなくなるはずです。  
  (Subnet route 経由でも同様です。)

  👉 `service tailscale stop` が実行された時点でいったん接続が切れますが、そのまま待っていれば回復するはずです。

  ```console
  logpolicy: using system state directory "/var/lib/tailscale"
  dns: [rc=unknown ret=direct]
  dns: using "direct" mode
  dns: using *dns.directManager
  dns: inotify addwatch: context canceled
  linuxfw: clear ip6tables: exec: "ip6tables": executable file not found in $PATH
  flushing log.
  logger closing down
  ```

  <br>

- コードの最後で実行される `tailscale set --auto-update=false` によって、 Tailscale の[自動アップデート](https://tailscale.com/kb/1067/update#auto-updates)を無効化しています。

  👉 `auto-update` を無効化する理由は、 `Auto-updates` の `On`/`Off` は [Tailnet ごとに設定されるため](https://tailscale.com/kb/1067/update#auto-updates)、誤って適用されてしまい、知らないうちに `no space left on device` となってしまうことを避けるためです。
  <br>

- `curl` も `jq` も使っていないので、素の OpenWrt で動作します。🎉

## 使用方法

以下のコードを

1. `/etc/config/update_tailscale.sh` に保存して
1. `chmod +x /etc/config/update_tailscale.sh` で実行権限を付与します

```shell
#!/bin/sh

ARCH=arm64

# Determine if the device is running OpenWrt by checking for the existence of `/etc/openwrt_release`
openwrt_exists=false
if [ -f /etc/openwrt_release ]; then
  openwrt_exists=true
fi

# Determine if the CPU architecture is ARM64 by checking if `aarch64` is part of `uname -a` output
is_arm64=false
if uname -a | grep -q 'aarch64'; then
  is_arm64=true
fi

# Verify compatibility: Must be an OpenWrt device running on a 64-bit ARM CPU
if [ "$openwrt_exists" = false ] || [ "$is_arm64" = false ]; then
  echo "This program can only be executed on OpenWrt devices running on 64-bit ARM CPUs."
  exit 1
fi

API_URL="https://api.github.com/repos/tailscale/tailscale/releases/latest"

# Retrieve the currently installed Tailscale version
current_version=$(tailscale version | head -1)
echo "Current Tailscale version: $current_version"

# Fetch the latest available Tailscale version
latest_version=$(wget -q -O - "$API_URL" | sed -n 's/.*"tag_name":"v\([^"]*\)".*/\1/p')
echo "Latest Tailscale version: $latest_version"

# Compare versions and handle the update process
if [ "$current_version" = "$latest_version" ]; then
  echo "You are already using the latest Tailscale version ($latest_version)."
  exit 0
fi

package_name="tailscale_${latest_version}_${ARCH}"
package_url="https://pkgs.tailscale.com/stable/${package_name}.tgz"

# Check availability of the latest package
if wget --spider "$package_url" 2>/dev/null; then
  echo "An update to Tailscale $latest_version is available."
else
  echo "The latest version $latest_version is not available."
  exit 0
fi

# Prompt the user for an update confirmation
echo "Do you want to update? (y/n)"
read -r answer
case $answer in
  [nN])
    echo "Update canceled."
    exit 0
    ;;
  *)
    echo "Proceeding with the update."
    ;;
esac

# Uninstall current Tailscale
echo "Uninstalling current version of Tailscale..."
service tailscale stop
rm /usr/sbin/tailscale /usr/sbin/tailscaled
echo "Uninstall complete."

# Download the latest package
echo "Downloading Tailscale $latest_version..."
wget "$package_url" -O /tmp/tailscale.tgz

echo "Extracting package..."

# Install the new version
echo "Installing Tailscale $latest_version..."
tar zxvf /tmp/tailscale.tgz ${package_name}/tailscale -C /tmp
cp /tmp/${package_name}/tailscale /usr/sbin
rm -r /tmp/${package_name}

tar zxvf /tmp/tailscale.tgz ${package_name}/tailscaled -C /tmp
cp /tmp/${package_name}/tailscaled /usr/sbin
rm -r /tmp/${package_name}

rm /tmp/tailscale.tgz

echo "Installation complete."

# Start and configure Tailscale
service tailscale start
tailscale set --auto-update=false

echo
echo "Now you are using the latest Tailscale version ($latest_version)!"
```

👉 コードを `/etc/config/` 内に保存する理由は、`System` > `Backup` で設定のバックアップ(archive)を作成する際、 `/etc/config/` はバックアップファイルに含まれるためです。

<details>
<summary>ちなみに OpenAI o1 に書いてもらったコードはこちら。</summary>

1. tgz ファイルを 1 回で解凍しているため、@hide_seki さんの改良部分が無効化されています。😱
1. そのままでは動かなかったため、2 箇所修正しています。 ( `CURRENT_VERSION` と `LATEST_VERSION` の取得)

```shell
#!/bin/sh

# Script to update Tailscale on OpenWrt devices with 64-bit ARM CPUs

# Constants
ARCH="arm64"
API_URL="https://api.github.com/repos/tailscale/tailscale/releases/latest"
PACKAGE_BASE_URL="https://pkgs.tailscale.com/stable"

# Function to check system requirements
check_requirements() {
  # Ensure the script is running on OpenWrt
  if [ ! -f /etc/openwrt_release ]; then
    echo "Error: This script must be run on an OpenWrt device."
    exit 1
  fi

  # Ensure the CPU architecture is 64-bit ARM
  if [ "$(uname -m)" != "aarch64" ]; then
    echo "Error: This script can only be executed on devices with 64-bit ARM CPUs (aarch64)."
    exit 1
  fi
}

# Function to get the current Tailscale version
get_current_version() {
  CURRENT_VERSION=$(tailscale version | head -1)
  if [ -z "$CURRENT_VERSION" ]; then
    echo "Error: Could not determine the current Tailscale version."
    exit 1
  fi
  echo "Current Tailscale version: ${CURRENT_VERSION}"
}

# Function to get the latest Tailscale version from GitHub
get_latest_version() {
  LATEST_VERSION=$(wget -q -O - "$API_URL" | sed -n 's/.*"tag_name":"v\([^"]*\)".*/\1/p')
  if [ -z "$LATEST_VERSION" ]; then
    echo "Error: Unable to retrieve the latest Tailscale version."
    exit 1
  fi
  echo "Latest Tailscale version available: ${LATEST_VERSION}"
}

# Function to compare versions and decide if an update is needed
compare_versions() {
  if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then
    echo "You are already using the latest Tailscale version (${LATEST_VERSION})."
    exit 0
  fi

  PACKAGE_NAME="tailscale_${LATEST_VERSION}_${ARCH}"
  PACKAGE_URL="${PACKAGE_BASE_URL}/${PACKAGE_NAME}.tgz"

  # Check if the package for the latest version is available
  if ! wget --spider "$PACKAGE_URL" 2>/dev/null; then
    echo "Error: Tailscale version ${LATEST_VERSION} is not available for architecture ${ARCH}."
    exit 1
  fi

  echo "An update to Tailscale ${LATEST_VERSION} is available."
}

# Function to prompt the user for an update
prompt_update() {
  echo -n "Do you want to update to Tailscale ${LATEST_VERSION}? (y/N): "
  read -r ANSWER
  case "$ANSWER" in
    [yY][eE][sS]|[yY])
      echo "Proceeding with the update..."
      ;;
    *)
      echo "Update canceled."
      exit 0
      ;;
  esac
}

# Function to uninstall the current Tailscale version
uninstall_tailscale() {
  echo "Uninstalling the current Tailscale version..."
  service tailscale stop
  rm -f /usr/sbin/tailscale /usr/sbin/tailscaled
  echo "Uninstallation complete."
}

# Function to install the new Tailscale version
install_tailscale() {
  echo "Downloading Tailscale ${LATEST_VERSION}..."
  TEMP_TGZ="/tmp/tailscale.tgz"
  PACKAGE_NAME="tailscale_${LATEST_VERSION}_${ARCH}"
  PACKAGE_URL="${PACKAGE_BASE_URL}/${PACKAGE_NAME}.tgz"

  if ! wget "$PACKAGE_URL" -O "$TEMP_TGZ"; then
    echo "Error: Failed to download Tailscale package."
    exit 1
  fi

  echo "Extracting package..."
  if ! tar -zxf "$TEMP_TGZ" -C /tmp; then
    echo "Error: Failed to extract Tailscale package."
    exit 1
  fi

  echo "Installing Tailscale binaries..."
  if ! cp "/tmp/${PACKAGE_NAME}/tailscale" "/usr/sbin/"; then
    echo "Error: Failed to install 'tailscale' binary."
    exit 1
  fi
  if ! cp "/tmp/${PACKAGE_NAME}/tailscaled" "/usr/sbin/"; then
    echo "Error: Failed to install 'tailscaled' binary."
    exit 1
  fi

  # Clean up temporary files
  rm -rf "/tmp/${PACKAGE_NAME}"
  rm -f "$TEMP_TGZ"

  echo "Installation of Tailscale ${LATEST_VERSION} complete."
}

# Function to start Tailscale service
start_tailscale() {
  echo "Starting Tailscale service..."
  service tailscale start
  tailscale set --auto-update=false
  echo "Tailscale is up and running."
}

# Main script execution
check_requirements
get_current_version
get_latest_version
compare_versions
prompt_update
uninstall_tailscale
install_tailscale
start_tailscale
```

</details>

## Tailscale の設定の確認方法

### 方法 1. `tailscale debug prefs`

以下のコマンドで Tailscale の設定状況を確認できます。

```shell
tailscale debug prefs
```

### 方法 2. `tailscale status --json | sed -n '/Self/,/}/p'`

`tailscale status --json` を以下のように整形すると、現在のノードの設定状況が確認できます。

```shell
tailscale status --json | sed -n '/Self/,/}/p'
```

Tailscale が外部に対してどのポートを使用しているかは、 `tailscale status --json | sed -n '/Self/,/}/p' | sed -n '/Addrs/,/]/p'` で確認できます。

👉 Nested tailscale では、ポートが競合して通信が不能になってしまう場合があります。

## Tailscale の設定方法

### 稼働中の Tailscale で設定の変更を行う場合は、 `tailscale up` ではなく `tailscale set` を使用する

実行例.

```shell
tailscale set --advertise-routes=10.0.0.0/8,192.168.0.0/24 --accept-routes --advertise-exit-node

# 以下のように一つずつ設定することもできます。
tailscale set --accept-routes
tailscale set --advertise-exit-node
tailscale set --advertise-routes=10.0.0.0/8,192.168.0.0/24

# 初めて `tailscale up` を実行する時は、以下のように実行して大丈夫です。
tailscale up --advertise-routes=10.0.0.0/8,192.168.0.0/24 --accept-routes --advertise-exit-node
```

https://tailscale.com/kb/1080/cli#set

`tailscale set` は 2022 年 12 月に [1.34.0](https://github.com/tailscale/tailscale/releases/tag/v1.34.0) で導入されました。

### 全ての設定をデフォルト値に戻す場合は `tailscale up --reset` を使用する

`--reset` オプションは `tailscale set` にはなく、`tailscale up` にしかないため、全ての設定をデフォルト値に戻したい場合は `tailscale up --reset` を使用します。

## Tailscale 導入後に OpenWrt で `opkg update` ができなくなった場合 (DNS 解決エラー)

私の場合は、 `Network` > `Interfaces` で `wan6` を停止したところ、  
正常に `opkg update` が実行できるようになったため、 `wan6` を削除しました。

OpenWrt Forum や Reddit でも、 Tailscale インストール後に DNS が使えなくなったというスレッドがあります。

https://forum.openwrt.org/t/dns-issue-on-my-openwrt-after-istalling-tailscale/161753

https://www.reddit.com/r/Tailscale/comments/unabiv/tailscale_break_dns_on_my_openwrt_router/

## OpenWrt の時刻がずれている場合も `opkg update` に失敗します

OpenWrt の時刻がずれていても `opkg update` に失敗するので、 `ntpd -q -p 0.openwrt.pool.ntp.org` を `System` > `Startup` > `Local Startup` に追記して、OpenWrt を再起動した後でも時刻がずれないように設定しましょう。

![ntpd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566576/d0709871-a4d8-48a7-1598-15d1ad4a1252.png)