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

![PowerShell_banner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/2fd4db5e-14e9-42b3-9e38-a3728ed83c5b.png)
<br>

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

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/ec08f703-4a5c-46d9-8c78-a7527b229e57.png">


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

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

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

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


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

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

#### 　　■APIキー発行/取得
　　・Tailscaleの公式サイトにログインします。
 
　　　　●Tailscale公式：https://tailscale.com/

　　・ログイン後サイトのメニューの「**Settings**」をクリック
  
　　<img width="400" target="_blank" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/5988994a-c0a9-4f98-8a78-989ebb8927a2.png">

　　・「**Settings**」画面の左下「**Keys**」をクリック
　　・「**Keys**」画面の右下「**API access tokens**」の項目
　　・「**Generate access token**」ボタンをクリック
  
　　<img width="400" target="_blank" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/f4fad896-1e01-47c8-972d-4cfe584ec924.png">
  
　　・API tokenのダイアログ表示される
　　・「**Description**」は適当なキーワードを入力
　　・「**Expiration**」でAPIの期限を入力

　　<img width="400" target="_blank" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/77741d03-8c25-4440-b04b-e2a25023cf12.png">

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

#### 　　■ Tailnetネットワーク名取得
　　・TOPページのメニューから「**DNS**」をクリック
　　・「**DNS**」画面の「**Tailnet name**」にある、自身のネックワーク名をメモ
  
　　<img width="400" target="_blank" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/8ab8babf-646a-4f08-ad03-1e932b46aecd.png">


# ３．コード/端末情報一覧
```powershell: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
```

# ３．結果/端末情報一覧
　<img width="450" target="_blank" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/d80ea5e4-78b6-42d2-9aab-a605c0e0bdbe.png">

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

# ４．コード/デバイス削除 (<font color="blue">2025/06/09追記</font>)
　改良案で挙げていた「**`一定期間使ってないデバイスがあったら削除`**」について
　実際にテスト接続して邪魔なデバイスを削除するためにコードを書いたので
　追記掲載いたします。
　1ヶ月以上Tailnetに接続していないデバイスを抽出し削除するか表示します。
 
```powershell: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

```

# ５．結果/デバイス削除
**■キャンセル時処理■**
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/8d098f07-afd4-43fb-bd7d-35d93d42fb62.png">

**■削除実行時処理■**
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/00790d21-7024-482c-850f-e0cccecf1193.png">

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

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

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

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

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

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

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

# ５．参考/参照
■ 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の操作を網羅したコード集。感謝。

<br>

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