strongSwan (IKEv2/IPsec) の CGNAT 環境向け、ユーザー単位ポートブロック割当 (PBA: Port Block Allocation) マネージャ。
VPN ユーザーごとに一意な public_ip:port_start-port_end の範囲を割り当てることで、Abuse(不正利用)対応時に「ある時刻に特定の public_ip:port を使っていたのは誰か」を特定できるようにする。
Neutron 型のコントロールプレーン / データプレーン分離構成。pba-manager バイナリは 1 本で、サブコマンドによって役割が変わる。
VPN クライアント
↓ IKEv2 / EAP-MSCHAPv2
strongSwan (charon)
↓ VICI イベント (child-updown)
pba-agent ─────────────────HTTP(bearer)────────────────▶ pba-controller
│ nftables 再生成 │ 割当ロジック
│ (ip pba_nat テーブル, host netns) ├─ Redis — lease 状態・pool
↓ └─ PostgreSQL — pba_sessions (Abuse ログ)
ホストの nftables
- pba-controller: 割当ロジック本体。HTTP API(
/v1/allocate/v1/release/v1/reconcile/v1/leases)と/metricsを公開。特権不要で、単一 replica で動作する(複製・排他制御は今後の拡張)。 - pba-agent: strongSwan の VICI イベントを購読し、controller の API を呼び出してリース状態を同期。ホストの nftables(
ip pba_natテーブルのpbaチェーン)を再生成する。network_mode: host+NET_ADMINが必要。 - 互換性のため、単一プロセスで両方の役割を担う
daemonサブコマンドも残っている(非コンテナ環境向け)。
ポート割当・生成される nftables ルールの詳細は internal/allocator/・internal/nft/ を参照。設計判断の背景は docs/containerization-migration.md にまとめている。
公開 IP ごとにポート空間を 512 ポート単位のスロットに分割する。新規接続のたびに、現在の空き率に応じて連続する N 個のスロットを割り当てる。
| 空き率 | ブロックサイズ |
|---|---|
| 75% 以上 | 4096 ポート (8 スロット) |
| 50% 以上 | 2048 ポート (4 スロット) |
| 25% 以上 | 1024 ポート (2 スロット) |
| 0% 以上 | 512 ポート (1 スロット) |
空き率が下がるほど割当ブロックを小さくし、枯渇を遅らせる適応的な方式。
strongSwan・freeRADIUS・Redis・PostgreSQL・nginx・certbot・Prometheus/Grafana を含むスタック全体をコンテナで構成する。Let's Encrypt による証明書自動更新にも対応。
cd deploy/docker
cp .env.example .env # ACME_DOMAIN 等を編集
make secrets-init # 各種パスワード・トークンをランダム生成
make config-init # config.yaml.example → config.yaml(public_ips 等を編集)
make init-cert # 初回のみ: HTTP-only nginx → Let's Encrypt staging 発行
make up詳細な構成・ネットワーク設計・証明書更新の仕組みは docs/containerization-migration.md を参照。
# 1. ビルド
go build -o pba-manager ./cmd/pba-manager
# 2. インストール
install -m 755 pba-manager /usr/local/bin/
# 3. 設定
mkdir -p /etc/pba-manager
cp deploy/config.yaml.example /etc/pba-manager/config.yaml
# postgres の DSN と public_ips を編集(DSN は直書きせず PBA_PG_DSN_FILE 等の
# 環境変数/secret 経由での注入を推奨。docs/containerization-migration.md §8)
# 4. PostgreSQL スキーマ
psql -U pba -d pba -f schema/pba_sessions.sql
# 5. nftables(ip pba_nat という PBA 専用テーブルを使う。Docker の "ip nat" と衝突しないため)
cp deploy/nftables.conf.example /etc/nftables.conf
nft -f /etc/nftables.conf
# 6. Redis pool 初期化
pba-manager init-redis --config /etc/pba-manager/config.yaml
# 7. systemd(デーモン単体プロセス版。controller/agent 分離版を使う場合は
# deploy/docker のコンテナ構成、または pba-manager controller / agent を
# 別々の unit として登録する)
cp deploy/pba-manager.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now pba-managerpba-manager [--config PATH] <command>
| コマンド | 説明 |
|---|---|
init-redis |
設定から Redis のポートプールを初期化(既存 lease がある場合は --force なしでは拒否) |
allocate --username U --vpn-ip IP |
ポートブロックを手動割当 |
release --vpn-ip IP |
ポートブロックを手動解放 |
list |
現在の active lease とプール使用状況を表示 |
reconcile |
Redis ↔ VICI の状態を同期し nftables を再構築 |
daemon [--reconcile-interval N] |
イベント駆動デーモン(controller/agent 一体型。既定: 60 秒ごとに reconcile) |
controller |
割当ロジック + HTTP API サーバー(コンテナ構成向け) |
agent [--reconcile-interval N] |
VICI 購読 + nftables 再生成(コンテナ構成向け、controller の API を呼び出す) |
pba:pool:<public_ip> Sorted Set — 空き 512 ポートスロットの開始位置(score = port_start)
pba:lease:vpn:<vpn_ip> Hash — active lease の詳細(generation 含む)
pba:gen:<vpn_ip> String — VPN IP ごとの単調増加 generation カウンタ
pba:user:<username> String — ユーザー名に対応する vpn_ip
SELECT *
FROM pba_sessions
WHERE public_ip = '203.0.113.32'
AND 44321 BETWEEN port_start AND port_end
AND connected_at <= '2026-07-07 12:34:56+00'
AND (disconnected_at IS NULL
OR disconnected_at >= '2026-07-07 12:34:56+00');| コンポーネント | 失敗時の挙動 |
|---|---|
| Redis からのスロット取得 | エラーを返し、スロットは消費しない |
| PostgreSQL への INSERT | スロットをプールに返却してエラー |
| Redis への lease 保存 | PG セッションをクローズ、スロットを返却してエラー |
| nftables 再生成 | 警告ログのみ。次の reconcile で修復される |
| VICI 切断(agent/daemon) | reconnect_delay_sec 後に再接続。再接続直後は必ず full reconcile を先に実行 |
| Redis 利用不可(起動時) | 致命的エラーで終了。Redis 復旧後に再起動 |
| PostgreSQL セッションクローズ失敗 | 警告ログのみ。Abuse ログの disconnected_at が欠落する可能性 |
release の冪等性・排他制御(generation ベースの conditional release、request_id によるリトライ安全性)については internal/controller/ と docs/containerization-migration.md §5 を参照。
internal/strongswan/vici.go の実装状況:
uniqueid(IKE_SA 識別子)・remote-eap-id(EAP ユーザー名)・remote-host(クライアント実 IP)・remote-vips(割当仮想 IP の配列)は、swanctl --list-sas --rawの実出力で確認済み。- 一方、
child-updownイベント購読時のイベント名・フィールド値(特にupキーの有無で up/down を判定するロジック)は、実機のswanctl --listenでの検証がまだ TODO として残っている。本番投入前にテスト接続で確認すること。 agentはイベント判定に不確実性が残ることを見越し、child-down受信時はConfirmSAGone(list-sasでの生存確認)を経てから解放する設計になっている(rekey / re-authentication による誤解放を防止)。