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 폼에 별도 제출합니다. debug-otp: required: false default: "false" description: "true면 seed 검증용으로 생성된 OTP를 로그에 출력합니다. 확인 후 즉시 false로 되돌리세요." 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 }} DEBUG_OTP: ${{ inputs.debug-otp }} 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" "${1:-0}" <<'PY' import base64 import hashlib import hmac import struct import sys import time seed, fmt, offset_windows = sys.argv[1], sys.argv[2], int(sys.argv[3]) 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) + offset_windows 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" "$@" } summarize_html() { page="$1" if [ ! -f "$page" ]; then return fi title="$(sed -n 's/.*