Files
commonDeploy/connect-vpn/action.yml
T

283 lines
9.7 KiB
YAML

name: VPN 연결 (F5 BIG-IP)
description: F5 BIG-IP VPN 클라이언트를 설치하고 웹 로그인/OTP를 자동화해 VPN에 연결
inputs:
deb-path:
required: false
default: ""
description: F5 VPN 클라이언트 .deb 경로. 비어있으면 deb-url에서 다운로드
deb-url:
required: false
default: "https://gitea.pitap.at/public/commonDeploy/releases/download/setup01/linux_f5vpn.x86_64.deb"
description: F5 VPN 클라이언트 .deb 다운로드 URL
vpn-host:
required: true
description: F5 VPN 호스트 (예 https://vpn.example.com)
vpn-user:
required: true
description: VPN 사용자 ID
vpn-password:
required: true
description: VPN 정적 비밀번호
otp-seed:
required: true
description: TOTP 시드 키 (이 값으로 매 실행 OTP 코드를 로컬 생성)
otp-seed-format:
required: false
default: "base64"
description: >
OTP 시드 포맷. base32=구글OTP류 문자열(대문자/공백 자동 정규화),
hex=16진수 문자열, base64=base64로 인코딩된 키.
otp-mode:
required: false
default: "form"
description: >
호환성용 입력. 현재 웹 로그인 방식은 password 단계 후 OTP를 ga_code_attempt 폼에 별도 제출합니다.
resource-name:
required: false
default: "/Common/dw_default_na"
description: F5 Network Access 리소스 이름
timeout:
required: false
default: "90"
description: 연결 완료 대기 최대 시간(초)
runs:
using: composite
steps:
- name: 의존성/F5 VPN 클라이언트 설치
shell: bash
env:
DEB_PATH: ${{ inputs.deb-path }}
DEB_URL: ${{ inputs.deb-url }}
run: |
set -eu
SUDO=""
[ "$(id -u)" -ne 0 ] && SUDO="sudo"
if ! command -v apt-get >/dev/null 2>&1; then
echo "apt 기반 Linux 러너가 필요합니다."
exit 1
fi
$SUDO apt-get update
$SUDO apt-get install -y --no-install-recommends \
ca-certificates curl python3 coreutils dbus-x11 xvfb xdotool x11-utils procps iproute2 \
libxslt1.1 libsqlite3-0 libgl1 libglib2.0-0 libx11-xcb1 libxcb1 \
libxcb-render0 libxcb-shape0 libxcb-xfixes0 libxcb-shm0 libxrender1 \
libxi6 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 \
libxrandr2 libxtst6 libfontconfig1 libfreetype6
DEB=""
if [ -n "$DEB_PATH" ]; then
DEB="$DEB_PATH"
elif [ -n "$DEB_URL" ]; then
DEB="/tmp/f5-vpn-client.deb"
curl -fsSL -o "$DEB" "$DEB_URL"
fi
if [ -z "$DEB" ] || [ ! -f "$DEB" ]; then
echo ".deb를 찾을 수 없습니다 (deb-path/deb-url 확인)"
exit 1
fi
echo "F5 VPN 클라이언트 설치: $DEB"
$SUDO dpkg -i "$DEB" || $SUDO apt-get install -f -y
if [ ! -x /opt/f5/vpn/f5vpn ]; then
echo "/opt/f5/vpn/f5vpn 이 설치되지 않았습니다."
exit 1
fi
- name: TUN 디바이스 확인
shell: bash
run: |
set -eu
if [ ! -c /dev/net/tun ]; then
echo "/dev/net/tun 이 없습니다. 러너에 --device /dev/net/tun --cap-add=NET_ADMIN 필요"
exit 1
fi
- name: VPN 연결
shell: bash
env:
VPN_HOST: ${{ inputs.vpn-host }}
VPN_USER: ${{ inputs.vpn-user }}
VPN_PASS: ${{ inputs.vpn-password }}
OTP_SEED: ${{ inputs.otp-seed }}
OTP_SEED_FORMAT: ${{ inputs.otp-seed-format }}
RESOURCE_NAME: ${{ inputs.resource-name }}
TIMEOUT: ${{ inputs.timeout }}
run: |
set -eu
WORKDIR="${RUNNER_TEMP:-/tmp}/f5-vpn-action"
COOKIE_JAR="$WORKDIR/cookies.txt"
LAUNCH_URL_FILE="$WORKDIR/f5-vpn-launch-url.txt"
UA="Mozilla/5.0 (X11; Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0"
mkdir -p "$WORKDIR"
rm -f "$COOKIE_JAR" "$LAUNCH_URL_FILE"
CONNECTED=0
cleanup() {
if [ "$CONNECTED" = "1" ]; then
return
fi
if [ -n "${F5VPN_PID:-}" ] && kill -0 "$F5VPN_PID" 2>/dev/null; then
kill "$F5VPN_PID" 2>/dev/null || true
fi
if [ -n "${XVFB_PID:-}" ] && kill -0 "$XVFB_PID" 2>/dev/null; then
kill "$XVFB_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
totp() {
python3 - "$OTP_SEED" "$OTP_SEED_FORMAT" <<'PY'
import base64
import hashlib
import hmac
import struct
import sys
import time
seed, fmt = sys.argv[1], sys.argv[2]
seed = "".join(seed.split())
def decode_base32(value):
normalized = value.replace(" ", "").upper().rstrip("=")
normalized += "=" * ((8 - len(normalized) % 8) % 8)
return base64.b32decode(normalized)
if fmt == "base64":
decoded = base64.b64decode(seed)
try:
maybe_base32 = decoded.decode("ascii").replace(" ", "").upper()
if maybe_base32 and all(ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=" for ch in maybe_base32):
raw = decode_base32(maybe_base32)
else:
raw = decoded
except UnicodeDecodeError:
raw = decoded
elif fmt == "base32":
raw = decode_base32(seed)
elif fmt == "hex":
raw = bytes.fromhex(seed)
else:
raise SystemExit(f"잘못된 otp-seed-format: {fmt}")
counter = int(time.time() // 30)
digest = hmac.new(raw, struct.pack(">Q", counter), hashlib.sha1).digest()
offset = digest[-1] & 0x0f
code = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7fffffff) % 1000000
print(f"{code:06d}")
PY
}
curl_common() {
curl -k -sS --http1.1 -A "$UA" "$@"
}
echo "F5 웹 로그인 시작: ${VPN_HOST} (user=${VPN_USER})"
curl_common -L -c "$COOKIE_JAR" "$VPN_HOST/" -o "$WORKDIR/01-login.html"
curl_common -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
-X POST "$VPN_HOST/my.policy" \
--data-urlencode "username=$VPN_USER" \
--data-urlencode "password=$VPN_PASS" \
--data-urlencode "vhost=standard" \
-o "$WORKDIR/02-otp.html"
if ! grep -q "ga_code_attempt" "$WORKDIR/02-otp.html"; then
echo "OTP 입력 폼에 도달하지 못했습니다. 사용자명/비밀번호 또는 APM 정책을 확인하세요."
exit 1
fi
while [ $(( $(date +%s) % 30 )) -gt 22 ]; do
sleep 1
done
curl_common -L -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
-X POST "$VPN_HOST/my.policy" \
--data-urlencode "ga_code_attempt=$(totp)" \
--data-urlencode "vhost=standard" \
-o "$WORKDIR/03-webtop.html"
if ! grep -q "F5 Dynamic Webtop\|Network Access" "$WORKDIR/03-webtop.html"; then
echo "OTP 인증 후 Webtop에 도달하지 못했습니다."
exit 1
fi
host_no_scheme="${VPN_HOST#https://}"
host_no_scheme="${host_no_scheme#http://}"
host_no_scheme="${host_no_scheme%%/*}"
webtop_path="/vdesk/webtop.eui?z=$RESOURCE_NAME&webtop=/Common/dw_webtop&webtop_type=webtop_na_only"
TOKEN_HEADERS="$WORKDIR/04-token.headers"
curl_common -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
-D "$TOKEN_HEADERS" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Accept: */*" \
-e "$VPN_HOST$webtop_path" \
"$VPN_HOST/vdesk/get_token_for_sessid.php3" \
-o "$WORKDIR/04-token.body"
otc="$(
awk 'BEGIN{IGNORECASE=1} /^X-ACCESS-Session-Token:/ { sub(/\r$/, ""); print substr($0, index($0, ":") + 2) }' "$TOKEN_HEADERS" |
tail -n 1
)"
if [ -z "$otc" ]; then
echo "F5 session token 획득 실패"
exit 1
fi
base_url="f5-vpn://$host_no_scheme?server=$host_no_scheme&resourcename=$RESOURCE_NAME&resourcetype=network_access&cmd=launch&protocol=https&port=443&sid=nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn"
token="$(printf '%s' "$base_url" | md5sum | awk '{print $1}')"
printf '%s\n' "$base_url&token=$token&otc=$otc" > "$LAUNCH_URL_FILE"
# F5 번들 Qt 5.5는 Xvfb RandR refresh rate에서 SIGFPE가 나므로 RandR 확장을 끈다.
Xvfb :99 -screen 0 1280x800x24 -extension RANDR >"$WORKDIR/xvfb.log" 2>&1 &
XVFB_PID=$!
sleep 1
export DISPLAY=:99
export HOME="$WORKDIR/home"
mkdir -p "$HOME"
echo "F5 VPN 클라이언트 실행"
/opt/f5/vpn/f5vpn "$(cat "$LAUNCH_URL_FILE")" >"$WORKDIR/f5vpn.out" 2>"$WORKDIR/f5vpn.err" &
F5VPN_PID=$!
for _ in $(seq 1 20); do
if xwininfo -root -tree 2>/dev/null | grep -q "F5 VPN - Security Warning"; then
xdotool mousemove 650 398 click 1
break
fi
sleep 1
done
echo "연결 완료 대기 중 (최대 ${TIMEOUT}초)..."
for _ in $(seq 1 "$TIMEOUT"); do
if ip link show tun0 >/dev/null 2>&1; then
CONNECTED=1
echo "VPN 연결 완료"
ip -br addr show tun0
exit 0
fi
if ! kill -0 "$F5VPN_PID" 2>/dev/null; then
echo "f5vpn이 tun0 생성 전에 종료되었습니다."
sed -n '1,120p' "$WORKDIR/f5vpn.err" || true
tail -120 "$HOME/.F5Networks/vpn.log" 2>/dev/null || true
exit 1
fi
sleep 1
done
echo "VPN 연결 실패 (${TIMEOUT}초 초과)."
sed -n '1,120p' "$WORKDIR/f5vpn.err" || true
tail -120 "$HOME/.F5Networks/vpn.log" 2>/dev/null || true
exit 1