summaryrefslogtreecommitdiff
path: root/vpn
diff options
context:
space:
mode:
Diffstat (limited to 'vpn')
-rw-r--r--vpn/.gitignore2
-rw-r--r--vpn/Dockerfile7
-rw-r--r--vpn/deploy.sh11
-rw-r--r--vpn/docker-compose.yml14
-rw-r--r--vpn/entrypoint.sh54
-rw-r--r--vpn/install.sh59
-rw-r--r--vpn/renew-cert.sh18
-rw-r--r--vpn/strongswan.conf15
-rw-r--r--vpn/swanctl.conf.tpl41
9 files changed, 221 insertions, 0 deletions
diff --git a/vpn/.gitignore b/vpn/.gitignore
new file mode 100644
index 0000000..b68b01a
--- /dev/null
+++ b/vpn/.gitignore
@@ -0,0 +1,2 @@
+certs/
+users.conf
diff --git a/vpn/Dockerfile b/vpn/Dockerfile
new file mode 100644
index 0000000..86644be
--- /dev/null
+++ b/vpn/Dockerfile
@@ -0,0 +1,7 @@
+FROM alpine:3.21
+RUN apk add --no-cache strongswan iptables iproute2 gettext
+COPY swanctl.conf.tpl /etc/swanctl/swanctl.conf.tpl
+COPY strongswan.conf /etc/strongswan.conf
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/vpn/deploy.sh b/vpn/deploy.sh
new file mode 100644
index 0000000..0005d9a
--- /dev/null
+++ b/vpn/deploy.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")"
+
+# 初期化前はスキップ(install.sh を先に実行する必要がある)
+if [ ! -f users.conf ] || [ ! -f certs/server.crt ]; then
+ echo "VPN: 未初期化のためスキップ (install.sh を実行してください)"
+ exit 0
+fi
+
+docker compose up -d --build
diff --git a/vpn/docker-compose.yml b/vpn/docker-compose.yml
new file mode 100644
index 0000000..83ff270
--- /dev/null
+++ b/vpn/docker-compose.yml
@@ -0,0 +1,14 @@
+services:
+ vpn:
+ build: .
+ restart: unless-stopped
+ network_mode: host
+ cap_add:
+ - NET_ADMIN
+ - SYS_MODULE
+ volumes:
+ - ./certs/server.crt:/etc/swanctl/x509/server.crt:ro
+ - ./certs/server.key:/etc/swanctl/private/server.key:ro
+ - ./users.conf:/etc/strongswan/users.conf:ro
+ environment:
+ - VPN_DOMAIN=vpn.yyamashita.com
diff --git a/vpn/entrypoint.sh b/vpn/entrypoint.sh
new file mode 100644
index 0000000..4efe500
--- /dev/null
+++ b/vpn/entrypoint.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+set -e
+
+# IP フォワーディング有効化
+sysctl -w net.ipv4.ip_forward=1
+sysctl -w net.ipv6.conf.all.forwarding=1
+
+# VPN クライアントのトラフィックを MASQUERADE
+IFACE=$(ip route show default | awk '/default/ {print $5}' | head -1)
+iptables -t nat -C POSTROUTING -s 10.10.0.0/24 -o "${IFACE}" -j MASQUERADE 2>/dev/null || \
+ iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o "${IFACE}" -j MASQUERADE
+iptables -C FORWARD -s 10.10.0.0/24 -j ACCEPT 2>/dev/null || \
+ iptables -A FORWARD -s 10.10.0.0/24 -j ACCEPT
+iptables -C FORWARD -d 10.10.0.0/24 -j ACCEPT 2>/dev/null || \
+ iptables -A FORWARD -d 10.10.0.0/24 -j ACCEPT
+
+# users.conf から EAP シークレットセクションを生成
+VPN_SECRETS=""
+while IFS=' ' read -r user pass || [ -n "$user" ]; do
+ [ -z "$user" ] && continue
+ case "$user" in \#*) continue ;; esac
+ VPN_SECRETS="${VPN_SECRETS}
+ eap-${user} {
+ id = ${user}
+ secret = \"${pass}\"
+ }"
+done < /etc/strongswan/users.conf
+
+export VPN_SECRETS
+export VPN_DOMAIN="${VPN_DOMAIN:-vpn.yyamashita.com}"
+
+# テンプレートから swanctl.conf を生成
+envsubst < /etc/swanctl/swanctl.conf.tpl > /etc/swanctl/swanctl.conf
+
+# charon (IKE デーモン) 起動
+ipsec start
+
+# 初期化待ち
+sleep 2
+
+# 設定ロード
+swanctl --load-conns
+swanctl --load-creds --noprompt
+swanctl --load-pools
+
+echo "VPN ready. Domain: ${VPN_DOMAIN}"
+
+# シグナルハンドラ
+trap 'ipsec stop; exit 0' TERM INT
+
+# charon が生きている間ループ
+while ipsec status > /dev/null 2>&1; do
+ sleep 10
+done
diff --git a/vpn/install.sh b/vpn/install.sh
new file mode 100644
index 0000000..9a2f33d
--- /dev/null
+++ b/vpn/install.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")"
+
+DOMAIN="vpn.yyamashita.com"
+CADDY_DIR="/app/infra/caddy"
+CERT_DIR="./certs"
+CADDY_CERT_BASE="/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}/${DOMAIN}"
+
+echo "=== VPN Install: ${DOMAIN} ==="
+
+# users.conf チェック
+if [ ! -f users.conf ]; then
+ echo ""
+ echo "Error: users.conf が見つかりません。先に作成してください:"
+ echo " echo 'username password' > /app/infra/vpn/users.conf"
+ echo " chmod 600 /app/infra/vpn/users.conf"
+ echo ""
+ echo "複数ユーザーは1行ずつ追加:"
+ echo " user1 pass1"
+ echo " user2 pass2"
+ exit 1
+fi
+
+mkdir -p "$CERT_DIR"
+
+# Caddy コンテナから証明書を取得
+echo "Caddy から証明書を取得中..."
+if ! docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \
+ cat "${CADDY_CERT_BASE}.crt" > "$CERT_DIR/server.crt"; then
+ echo ""
+ echo "Error: 証明書の取得に失敗しました。"
+ echo "Caddyfile に ${DOMAIN} が追加されているか確認してください。"
+ echo "追加後 'git push origin master' してから再実行してください。"
+ exit 1
+fi
+
+docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \
+ cat "${CADDY_CERT_BASE}.key" > "$CERT_DIR/server.key"
+
+chmod 600 "$CERT_DIR/server.key"
+echo "証明書を $CERT_DIR/ に保存しました。"
+
+# VPN コンテナ起動
+echo "VPN サービスを起動中..."
+docker compose up -d --build
+
+echo ""
+echo "=== VPN 起動完了 ==="
+echo ""
+echo "クライアント接続情報:"
+echo " サーバー: ${DOMAIN}"
+echo " 種別: IKEv2"
+echo " 認証: EAP (ユーザー名/パスワード)"
+echo ""
+echo "iOS/macOS: 設定 > VPN > VPN 構成を追加 > IKEv2"
+echo "Android (Quest/FireTV): strongSwan VPN Client アプリをインストール"
+echo ""
+echo "証明書の更新: bash /app/infra/vpn/renew-cert.sh"
diff --git a/vpn/renew-cert.sh b/vpn/renew-cert.sh
new file mode 100644
index 0000000..50f8784
--- /dev/null
+++ b/vpn/renew-cert.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")"
+
+DOMAIN="vpn.yyamashita.com"
+CADDY_DIR="/app/infra/caddy"
+CADDY_CERT_BASE="/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}/${DOMAIN}"
+
+echo "証明書を更新中..."
+
+docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \
+ cat "${CADDY_CERT_BASE}.crt" > certs/server.crt
+docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \
+ cat "${CADDY_CERT_BASE}.key" > certs/server.key
+chmod 600 certs/server.key
+
+docker compose exec vpn swanctl --load-creds --noprompt
+echo "完了。証明書を更新してリロードしました。"
diff --git a/vpn/strongswan.conf b/vpn/strongswan.conf
new file mode 100644
index 0000000..b42f8c0
--- /dev/null
+++ b/vpn/strongswan.conf
@@ -0,0 +1,15 @@
+charon {
+ load_modular = yes
+ plugins {
+ include strongswan.d/charon/*.conf
+ }
+ filelog {
+ stderr {
+ default = 1
+ ike = 2
+ knl = 3
+ }
+ }
+ send_vendor_id = no
+}
+include strongswan.d/*.conf
diff --git a/vpn/swanctl.conf.tpl b/vpn/swanctl.conf.tpl
new file mode 100644
index 0000000..f3a9e2e
--- /dev/null
+++ b/vpn/swanctl.conf.tpl
@@ -0,0 +1,41 @@
+connections {
+ ikev2-eap {
+ version = 2
+ # iOS/macOS/Android 互換の暗号スイート
+ proposals = aes256gcm16-prfsha256-ecp256,aes256-sha256-ecp256,aes256gcm16-prfsha384-ecp384,aes256-sha256-modp2048,aes256-sha1-modp2048
+
+ local {
+ auth = pubkey
+ certs = server.crt
+ id = ${VPN_DOMAIN}
+ }
+
+ remote {
+ auth = eap-mschapv2
+ eap_id = %any
+ }
+
+ children {
+ vpn {
+ local_ts = 0.0.0.0/0
+ esp_proposals = aes256gcm16,aes256-sha256,aes256-sha1
+ dpd_action = clear
+ }
+ }
+
+ pools = pool4
+ send_certreq = no
+ dpd_delay = 30s
+ }
+}
+
+pools {
+ pool4 {
+ addrs = 10.10.0.0/24
+ dns = 1.1.1.1,8.8.8.8
+ }
+}
+
+secrets {
+${VPN_SECRETS}
+}