2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

■ PowerShellでTailscale API ~コードでVPN管理したいよね?ね?~

Last updated at Posted at 2025-06-01

PowerShell_banner.png

こんにちは!
インフラもコードもハードも大好き、零壱(ゼロイチ)テクトです。

最近異様にハマっているOpenWrt弄りですが。
VPN接続するため、OpenWrtに限らずPCやスマホなどに
Tailscale」という、超絶簡単なのに超絶安心できるVPNアプリを
手持ちの端末にことごとくインストールしています。

インストールした台数が増えてきた為、NW情報を管理しようと思ったのですが
WEBページを見ながらポチポチとコピペで管理するのは面倒です。

調べたら「Tailscale API」があると分かったので
PowerShellでAPIにアクセスする勉強として
Tailscale APIで機器の情報を取得するコードを書きました。

※2025/06/09追記
 改良案に出していた一定期間過ぎた端末の削除について
 削除するコードも書いたので追記しました。

1.概要/仕様

・事前にTailscaleのサイトにログインして
 「APIキーの発行」、「Tailnetのネットワーク名」をメモする事
・情報はCSV加工を前提に1つずつ取得する事
・「Exit Node」/「Subnet Router」も取得する事
・上記に伴いAPI URLで「fields=all」を付ける事
・Onlineステータスを取得できないので5分以内の接続をオンラインとする

2.事前準備

 ・Tailscaleを導入後の作業として扱うので、すでにアカウントはあるものとします。

  ■APIキー発行/取得

  ・Tailscaleの公式サイトにログインします。

    ●Tailscale公式:https://tailscale.com/

  ・ログイン後サイトのメニューの「Settings」をクリック

  

  ・「Settings」画面の左下「Keys」をクリック
  ・「Keys」画面の右下「API access tokens」の項目
  ・「Generate access token」ボタンをクリック

  

  ・API tokenのダイアログ表示される
  ・「Description」は適当なキーワードを入力
  ・「Expiration」でAPIの期限を入力

  

  ・「Generate access token」ボタンをクリック
  ・APIキーが表示されるので、無くさないようメモ

  ■ Tailnetネットワーク名取得

  ・TOPページのメニューから「DNS」をクリック
  ・「DNS」画面の「Tailnet name」にある、自身のネックワーク名をメモ

  

3.コード/端末情報一覧

TailscaleDeviceInfo.ps1
# ***** 設定値:APIキー/Tailnet名 *****
$tailscaleApiKey = "tskey-api-xxxxxxxxxxxxxxx" # Tailscale APIキー
$tailnetName     = "xxxxxxxxxxxxxxxxx.net"     # Tailnet名

# ***** APIエンドポイント *****
$apiUrl = "https://api.tailscale.com/api/v2/tailnet/$tailnetName/devices?fields=all"

# ***** APIリクエスト用ヘッダーの定義 *****
$headers = @{
    "Authorization" = "Bearer $tailscaleApiKey"
    "Accept"        = "application/json"
}

try {
    Write-Host "Tailscale APIからデバイス情報を取得中..." -ForegroundColor Cyan

    # APIへリクエスト送信/デバイス情報取得
    $response = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers -ContentType "application/json"
    $devices  = $response.devices

    # デバイス数が1以上で処理
    if ($devices.Count -gt 0) {
        
        # 各デバイスをループ処理/情報を整形して表示
        $devices | ForEach-Object {

            # 変数にデバイスオブジェクトを代入
            $device = $_

            # ******** 各デバイスのステータス表示 ********
            Write-Host "----------------------------------"

            #現在時刻から最終アクセス時刻の差分計算
            $timeSinceLastSeen = (Get-Date) - [DateTime]$device.lastseen
    
            # オンライン閾値を設定:5分以下=Online/5分以上=Offline
            # ※TailscaleAPIでオンライン判定が無いため5分以内をオンラインとみなす
            if ($timeSinceLastSeen.TotalMinutes -lt 5) {
                Write-Host "Active: ● Online (Last: $($timeSinceLastSeen.TotalSeconds -as [int])Sec ago)" -ForegroundColor Green
            } else {
                Write-Host "Active: ✖ Offline " -ForegroundColor Red
            }

            # 各種取得デバイス情報
            Write-Host "LAST SEEN (UTC): $($device.lastseen) +9:00(JST)"
            Write-Host "hostname: $($device.hostname)"
            Write-Host "User: $($device.user)"
            Write-Host "Name: $($device.name)"
            Write-Host "TailscaleIP: $($device.addresses -join ', ')"
            Write-Host "OS: $($device.os)"
            Write-Host "clientVersion: $($device.clientVersion)"

            # Exit Node 有無判定
            if ($device.enabledRoutes) {
                Write-Host "[*] Exit Node" -ForegroundColor Yellow
                Write-Host "EnabledRoutes: $($device.enabledRoutes -join ', ')"
            }

            # Subnet Router 有無判定
            if ($device.advertisedRoutes) {
                Write-Host "[*] Subnet Router" -ForegroundColor Yellow
                Write-Host "Advertised Subnets: $($device.advertisedRoutes -join ', ')"
            }

        }
        Write-Host "----------------------------------"
    } else {
        # 取得デバイス数がゼロの場合
        Write-Host "------ デバイス情報なし" -ForegroundColor Yellow
    }

}
catch {

    # エラー処理
    Write-Host "API呼び出し中/エラー発生" -ForegroundColor Red
    Write-Host "エラーメッセージ: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "ステータスコード: $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red
    if ($_.Exception.Response) {
        Write-Host "詳細: $($_.Exception.Response.Content)" -ForegroundColor Red
    }
}

Write-Host "スクリプト終了------処理完了日時[$(Get-Date)]" -ForegroundColor Cyan

3.結果/端末情報一覧

 

 上記のように登録したTailnetから各デバイスの情報一覧を取得できました。

4.コード/デバイス削除 (2025/06/09追記)

 改良案で挙げていた「一定期間使ってないデバイスがあったら削除」について
 実際にテスト接続して邪魔なデバイスを削除するためにコードを書いたので
 追記掲載いたします。
 1ヶ月以上Tailnetに接続していないデバイスを抽出し削除するか表示します。

TailscaleDeviceDel.ps1
# ***** 設定値:APIキー/Tailnet名 *****
$tailscaleApiKey = "tskey-api-xxxxxxxxxxxxxxx" # Tailscale APIキー
$tailnetName     = "xxxxxxxxxxxxxxxxx.net"     # Tailnet名

# ***** APIエンドポイント *****
$apiUrl = "https://api.tailscale.com/api/v2/tailnet/$tailnetName/devices?fields=all"

# ***** APIリクエスト用ヘッダーの定義 *****
$headers = @{
    "Authorization" = "Bearer $tailscaleApiKey"
    "Accept"        = "application/json"
}


try {
    Write-Host "Tailscale APIからデバイス情報を取得中..." -ForegroundColor Cyan

    # APIへリクエスト送信/デバイス情報取得
    $response = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers -ContentType "application/json"
    $devices  = $response.devices

    # デバイス数が1以上で処理
    if ($devices.Count -gt 0) {
        
        # 各デバイスをループ処理
        # 過去1ヶ月以上アクセスがないデバイスのみフィルタ
        $devices | Where-Object { [DateTime]$_.LastSeen -lt (Get-Date).AddMonths(-1) } | ForEach-Object {
        #$devices | Where-Object { [DateTime]$_.LastSeen -lt (Get-Date).AddDays(-7) } | ForEach-Object { #1ヶ月未満用

            # 情報を整形して表示

            # 変数にデバイスオブジェクトを代入
            $device = $_

            # ******** 各デバイスのステータス表示 ********
            Write-Host "----------------------------------"

            #現在時刻から最終アクセス時刻の差分計算(nullは無い前提)
            $timeSinceLastSeen = (Get-Date) - [DateTime]$device.LastSeen
    
            # オンライン閾値を設定:5分以下=Online/5分以上=Offline
            # ※TailscaleAPIでオンライン判定が無いため5分以内をオンラインとみなす
            if ($timeSinceLastSeen.TotalMinutes -lt 5) {
                Write-Host "Active: ● Online (Last: $($timeSinceLastSeen.TotalSeconds -as [int])Sec ago)" -ForegroundColor Green
            } else {
                Write-Host "Active: ✖ Offline " -ForegroundColor Red
            }

            # 各種取得デバイス情報
            Write-Host "LAST SEEN (UTC): $($device.LastSeen) +9:00(JST)"
            Write-Host "hostname: $($device.hostname)"
            Write-Host "User: $($device.user)"
            Write-Host "Name: $($device.name)"
            Write-Host "TailscaleIP: $($device.addresses -join ', ')"
            Write-Host "OS: $($device.os)"
            Write-Host "clientVersion: $($device.clientVersion)"

            # Exit Node 有無判定
            if ($device.enabledRoutes) {
                Write-Host "[*] Exit Node" -ForegroundColor Yellow
                Write-Host "enabledRoutes: $($device.enabledRoutes -join ', ')"
            }

            # Subnet Router 有無判定
            if ($device.advertisedRoutes) {
                Write-Host "[*] Subnet Router" -ForegroundColor Yellow
                Write-Host "Advertised Subnets: $($device.advertisedRoutes -join ', ')"
            }

            # tag付与用/デバイスID確認
            Write-Host "[#] IDs==========" -ForegroundColor Cyan
            Write-Host "ID: $($device.id)"
            Write-Host "NodeID: $($device.nodeid)"
            Write-Host "-----------------"

            # デバイス削除メッセージ/Y&N
            Write-Host "このクライアント[ $($device.hostname) ]を Tailscale から削除しますか? [y]=Delete , [n]=Cancel" -ForegroundColor Yellow
            $deleteYn = Read-Host 
                
                # キー判定/( nキーと明示しているが y以外は全部キャンセル)
                if ($deleteYn.ToString().ToUpper() -eq 'y') {
                    Write-Host "削除を実行します..." -ForegroundColor Red
                    $deleteUrl = "https://api.tailscale.com/api/v2/device/$($device.nodeId)"

                    try {
                        # デバイス削除/APIアクセス
                        Invoke-RestMethod -Uri $deleteUrl -Method Delete -Headers $headers -ContentType "application/json" -ErrorAction Stop # エラー捕捉用Stop
                        Write-Host "クライアント '$($device.hostname)' (NodeID: $($device.nodeId)) を正常に削除しました。" -ForegroundColor Red
                    }
                    catch {
                        Write-Host "クライアント '$($device.hostname)' の削除中にエラーが発生しました。" -ForegroundColor Red
                        Write-Host "エラーメッセージ: $($_.Exception.Message)" -ForegroundColor Red
                    }
                } else {
                    Write-Host "クライアント '$($device.hostname)' の削除をキャンセルしました。" -ForegroundColor Gray
                }
        }
        Write-Host "----------------------------------"


    } else {
        # 取得デバイス数がゼロの場合
        Write-Host "------ デバイス情報なし" -ForegroundColor Yellow
    }

}
catch {

    # エラー処理
    Write-Host "API呼び出し中にエラーが発生しました。" -ForegroundColor Red
    Write-Host "エラーメッセージ: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "ステータスコード: $($_.Exception.Response.StatusCode.value__)" -ForegroundColor Red

}

Write-Host "スクリプト終了------処理完了日時[$(Get-Date)]" -ForegroundColor Cyan

5.結果/デバイス削除

■キャンセル時処理■

■削除実行時処理■

・WEB上でも無事削除されているのを確認できました。

6.備考

・✅ 改良案としては、これを弄るよりこのコードを定期的に実行して
   ステータスを監視して、それに応じて設定を変更したり
   一定期間で残ったデバイスを自動で削除するなり
   監視操作として発展させた方が良いと思いました。
   →一定期間接続していないデバイスをフィルタして
    削除できるコードを追記しました。

・PowerShellでAPI接続して操作する方法は理解しました。

・全角文字を使ってますが、オンラインステータスを色分けしました。

・オンラインステータスは、本来WireGuardのセッションを考えると
 ほぼ数秒以内にアクセスできていればオンラインですが
 切れても接続を継続する仕組みを考えると、まあ5分ぐらいが妥当と思いました。

・情報は一つずつ取得できたので、必要に応じてCSVに変換できます。
 APIドキュメントにある情報は一通り取得可能です。

・WEB画面で出来る事は大抵APIで可能です。
 情報取得さえできてしまえば、NodeIDを利用して
 変更、削除、追加情報も可能です。

5.参考/参照

■ Tailscale:API公式ドキュメント
https://tailscale.com/api#description/overview

■ Github/tailscale-client-go-v2
https://github.com/tailscale/tailscale-client-go-v2/blob/main/README.md
※GO言語によるTailscale APIの操作を網羅したコード集。感謝。


    それでは
         どこかの誰かの何かの足しになれば幸いです。
                     01000010 01011001 01000101

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?