#!/bin/sh # dpworld-ap-apply.sh — WiFi AP bring-up/down (spec §8). STA(wlan0) 무접촉, ap0 전용. # 호출: --boot(seed) | (무인자, on-demand). ap-enabled 마커로 enable/disable 판정. set -u AP_DIR="/home/root/network/ap" HOSTAPD_CONF="${AP_DIR}/hostapd-ap0.conf" UDHCPD_CONF="${AP_DIR}/udhcpd-ap0.conf" ENABLED_MARK="${AP_DIR}/ap-enabled" STATE_DIR="/opt/dpworld-network" LOG="${STATE_DIR}/ap_apply.log" WLAN_MAC_FILE="/sys/class/net/wlan0/address" log() { echo "[$(date '+%F %T' 2>/dev/null) up=$(cut -d' ' -f1 /proc/uptime 2>/dev/null)] $*" >&2 [ -d "$STATE_DIR" ] && echo "$*" >> "$LOG" 2>/dev/null || true; } run() { "$@"; rc=$?; [ "$rc" -ne 0 ] && log " cmd rc=$rc: $*"; return "$rc"; } ap_mac() { # wlan0 MAC의 첫 옥텟에 local-admin bit(0x02) 세팅 → 별도 MAC base=$(cat "$WLAN_MAC_FILE" 2>/dev/null); [ -n "$base" ] || { echo ""; return; } o1=$(echo "$base" | cut -d: -f1); rest=$(echo "$base" | cut -d: -f2-) [ -n "$o1" ] || { echo ""; return; } # 드라이버 리로드 중 빈 옥텟 → 산술오류 방지 new=$(printf '%02x' $(( 0x$o1 | 0x02 ))) echo "${new}:${rest}" } # §8.1 ap0 방화벽 — 전용 chain(cleanup=flush+delete로 잔재/중복 원천 차단). # INPUT(장치 자체): v1.11.7 — ap0 클라이언트에 장치 서비스 전부 개방(SSH 22 포함). WPA2 PSK가 접근 게이트. # (이전 default-deny[9090/67/icmp만 허용, 8989·8990·SSH 차단]에서 전환 — 현장 관리 접근성 우선, 비번 강화 전제.) # FORWARD(경유): ip_forward=1일 때 AP 클라이언트가 PLC(eth1)·업링크로 라우팅하는 것을 전면 차단(provisioning 격리) — 유지. # ★ INPUT은 열되 FORWARD 격리는 유지 → AP 클라이언트는 장치 자체엔 접근 가능하나 내부망/PLC로는 경유 불가. AP_IN_CHAIN="DPWORLD_AP_IN" AP_FWD_CHAIN="DPWORLD_AP_FWD" fw_clear() { # unhook(점프 전부 drain) → flush → delete chain. 멱등 + 중복 점프(비정상/크래시 잔재)까지 제거. while iptables -D INPUT -i ap0 -j "$AP_IN_CHAIN" 2>/dev/null; do :; done while iptables -D FORWARD -i ap0 -j "$AP_FWD_CHAIN" 2>/dev/null; do :; done while iptables -D FORWARD -o ap0 -j "$AP_FWD_CHAIN" 2>/dev/null; do :; done iptables -F "$AP_IN_CHAIN" 2>/dev/null iptables -F "$AP_FWD_CHAIN" 2>/dev/null iptables -X "$AP_IN_CHAIN" 2>/dev/null iptables -X "$AP_FWD_CHAIN" 2>/dev/null } fw_apply() { # 전용 chain 재생성 + INPUT/FORWARD에서 ap0 트래픽을 chain으로 점프. ap0 한정 → STA/eth 무영향. fw_clear # INPUT(장치 자체): ap0 클라이언트에 전체 개방(SSH 포함) — WPA2 PSK가 접근 게이트(v1.11.7). run iptables -N "$AP_IN_CHAIN" || { fw_clear; return 1; } run iptables -A "$AP_IN_CHAIN" -j ACCEPT || { fw_clear; return 1; } run iptables -A INPUT -i ap0 -j "$AP_IN_CHAIN" || { fw_clear; return 1; } # FORWARD(경유): PLC(eth1)/업링크 라우팅 전면 차단 — provisioning 격리 유지(INPUT 개방과 독립). run iptables -N "$AP_FWD_CHAIN" || { fw_clear; return 1; } run iptables -A "$AP_FWD_CHAIN" -j DROP || { fw_clear; return 1; } run iptables -A FORWARD -i ap0 -j "$AP_FWD_CHAIN" || { fw_clear; return 1; } run iptables -A FORWARD -o ap0 -j "$AP_FWD_CHAIN" || { fw_clear; return 1; } } ap_down() { # 멱등 정리 (stop은 미기동 시 routine 실패 → run 미사용, ap_up과 일치) systemctl stop dpworld-udhcpd-ap0.service 2>/dev/null systemctl stop dpworld-hostapd-ap0.service 2>/dev/null fw_clear ip addr flush dev ap0 2>/dev/null iw dev ap0 del 2>/dev/null log "AP down (cleaned)" } ap_up() { [ -f "$HOSTAPD_CONF" ] || { log "no hostapd conf — skip"; return 0; } # RECONCILE: 기존 데몬/방화벽 정리, vif는 재사용(없을 때만 add) systemctl stop dpworld-udhcpd-ap0.service 2>/dev/null systemctl stop dpworld-hostapd-ap0.service 2>/dev/null fw_clear iw dev ap0 info >/dev/null 2>&1 || run iw dev wlan0 interface add ap0 type __ap || return 1 mac=$(ap_mac); [ -n "$mac" ] && run ip link set ap0 address "$mac" ap_ip=$(sed -n 's/^opt router \([0-9.]*\).*/\1/p' "$UDHCPD_CONF" 2>/dev/null) if [ -n "$ap_ip" ]; then run ip addr replace "${ap_ip}/24" dev ap0 || return 1 # replace=멱등 else log "no udhcpd conf/router — fail closed"; return 1; fi run ip link set ap0 up || return 1 if ! fw_apply; then log "AP firewall setup failed" ap_down return 1 fi touch /run/udhcpd-ap0.leases 2>/dev/null run systemctl restart dpworld-hostapd-ap0.service || return 1 run systemctl restart dpworld-udhcpd-ap0.service || return 1 log "AP up (ssid rendered, ap0 up)" } mkdir -p "$STATE_DIR" 2>/dev/null || true if [ -f "$ENABLED_MARK" ]; then ap_up; rc=$?; else ap_down; rc=$?; fi exit "$rc"