# Wi-Fi AP 서브시스템 가이드 > 살아있는 문서(living guide) — 앱 **v1.11.10** 기준. 코드: `src/network/ap_*.py`, > `deploy/dpworld-ap-apply.sh`. 아키텍처 전체는 [`architecture.md`](architecture.md) §3.1.1. 장비를 소프트 AP(`ap0`)로 띄워 운영자가 무선으로 Web Configurator(:9090)에 접속하게 한다. STA(`wlan0`, 업스트림 Wi-Fi)는 **무접촉**으로 유지한다. --- ## 1. ap0 가상 인터페이스 (vif) - `wlan0` 위에 `iw dev wlan0 interface add ap0 type __ap` 로 **별도 vif** 생성. - **MAC**: `wlan0` MAC 첫 옥텟에 local-admin bit(`0x02`)를 OR 해서 별도 MAC 부여 (`ap_mac()` in `dpworld-ap-apply.sh`) — STA 와 충돌하지 않는 LA-bit 주소. - **채널 = SCC(Single-Channel Concurrency)**: 기본(`ap_band=auto`)은 STA 가 붙어 있는 채널을 **추종**한다(`ap_engine.resolve_channel`). 단일 라디오는 동시에 한 채널만 쓸 수 있어, STA 와 AP 가 같은 채널이어야 둘 다 동작한다. STA 미연결 시 2.4GHz 채널 6 으로 폴백. `2g`/`5g` 명시(MCC)는 옵트인이며 STA 와 채널이 갈리면 한쪽이 끊길 수 있다. --- ## 2. ap_engine 라이프사이클 `ApEngine`(`src/network/ap_engine.py`)이 적용을 오케스트레이션한다. 직렬화 lock 으로 동시 apply 의 tmp 파일 경합·서비스 중복 start 를 막는다. 1. `ap_config` → intent 정규화(`ap_model.ap_intent_from_db`). 2. `validate_ap()` hard-rule 검증 → 실패 시 상태 `REJECTED`(파일 미작성). 3. 활성화면: 채널 산정 → `hostapd-ap0.conf` / `udhcpd-ap0.conf` 렌더(0600, 디렉터리 0700) → **marker** `ap-enabled` 작성. 비활성화면 marker 제거. 4. `systemctl start dpworld-ap-apply.service` 트리거(oneshot, `dpworld-ap-apply.sh` 실행). 5. 결과 상태 persist: `APPLYING`→`COMMITTED` / `FAILED`. 상태값: `REJECTED`, `APPLYING`, `COMMITTED`, `FAILED`. ### marker = kill-switch `/home/root/network/ap/ap-enabled` 파일의 **존재 여부**가 enable/disable 판정 기준이다. `dpworld-ap-apply.sh` 는 marker 가 있으면 `ap_up`, 없으면 `ap_down`(멱등 정리)을 한다. --- ## 3. `ap_config` DB 키 + 필드 별도 `board_config` 키 — **Python 전용**(`device_config` 무관, Java/dpworldapp 미사용). `db_manager.ALLOWED_KEYS` 에 등록됨. 기본값(`ap_model.DEFAULT_AP_CONFIG`): | 필드 | 기본값 | 설명 | |------|--------|------| | `ap_enabled` | `false` | AP on/off | | `ap_ssid` | `""` | SSID (최대 32 byte, `"`·`\`·`#`·제어문자 금지) | | `ap_passphrase` | `""` | WPA2-PSK passphrase (8–63 byte, 동일 금지문자) | | `ap_band` | `"auto"` | `auto`(SCC) / `2g` / `5g` | | `ap_channel` | `0` | 0 = auto. band 별 유효 채널만 허용 | | `ap_hidden` | `false` | SSID 브로드캐스트 숨김 | | `ap_ip` | `192.168.50.1` | AP 게이트웨이 IP (/24) | | `dhcp_start` | `192.168.50.50` | DHCP 풀 시작 (AP 서브넷 내) | | `dhcp_end` | `192.168.50.150` | DHCP 풀 끝 | | `dhcp_lease` | `43200` | 리스 시간(초) | > 검증은 `ap_validator.validate_ap` — SSID/PSK 바이트 길이, hostapd-conf 인젝션 차단 > (`#`·따옴표·역슬래시·개행), 채널-밴드 정합, DHCP 범위가 AP /24 서브넷 안인지·AP IP 와 > 겹치지 않는지, 그리고 **유효한 country regdomain 이 있어야** 활성화 가능 > (`country_pending`이면 "reboot required"). --- ## 4. systemd 유닛 (`deploy/`) | 유닛 | 역할 | |------|------| | `dpworld-ap-apply.service` | oneshot — `dpworld-ap-apply.sh` 실행(엔진이 on-demand start, `[Install]` 없음). `--boot`(seed)/무인자(on-demand) | | `dpworld-ap-seed.service` | 부팅 시 marker 기준 AP 재구성(seed) | | `dpworld-hostapd-ap0.service` | `hostapd /home/root/network/ap/hostapd-ap0.conf` | | `dpworld-udhcpd-ap0.service` | `udhcpd` (ap0 DHCP 서버) | `dpworld-ap-apply.sh` 가 vif 생성 → MAC 설정 → IP 부여 → 방화벽 → hostapd/udhcpd restart 순으로 멱등 reconcile 한다(`ap_up`). 어느 단계든 실패하면 `ap_down` 으로 fail-closed. --- ## 5. 방화벽 (v1.11.7) `ap0` 전용 iptables chain(`DPWORLD_AP_IN` / `DPWORLD_AP_FWD`)을 쓴다 — STA/eth 무영향. - **INPUT(장치 자체)**: **전면 개방**(SSH 22 포함). 즉 **WPA2 PSK 가 유일한 접근 게이트**. (이전 default-deny[9090/DHCP/ICMP 만 허용]에서 v1.11.7 에 전환 — 현장 관리 접근성 우선, 비밀번호 강화 전제.) - **FORWARD(경유)**: **blanket DROP**. `ip_forward=1` 이어도 AP 클라이언트가 PLC(eth1)·업링크로 라우팅하는 것을 전면 차단 = **provisioning 격리**(PLC/내부망 보호). 이 격리는 유지된다. ★ 요약: AP 클라이언트는 **장치 자체엔 접근 가능**하나(PSK 통과 시), **내부망/PLC 로는 경유 불가**. --- ## 6. API | Method | Path | 비고 | |--------|------|------| | `GET` | `/api/network/ap/status` | 라이브 상태 — 응답에서 **`ap_passphrase` 제거**(미인증 API) | | `POST` | `/api/network/ap/config` | `ap_config` 저장(필드 화이트리스트 merge, 알 수 없는 키 400) | | `POST` | `/api/network/ap/apply` | bring-up/down 적용. `{"dry_run": true}` 면 적용 없이 config 회신(PSK 제거) | `status` 응답: `ap_enabled`·`ap0_up`·`hostapd_running`·`clients`·`country_pending` + 마스킹된 `config`. 서브시스템 import 실패(예: Windows dev box) 시 503 fail-soft. > **API 인증 없음**: :9090 은 현재 미인증(문서화된 threat boundary). AP 가 열리면 PSK 만이 > 게이트이므로 **강한 AP PSK** 가 사실상의 1차 방어선이다. 운영자 로그인은 설계 백로그 > ([`architecture.md`](architecture.md) §10). --- ## 7. kill-switch (수동 비활성화) 엔진/웹이 응답하지 않을 때 root 셸에서 즉시 AP 를 내릴 수 있다: ```sh rm /home/root/network/ap/ap-enabled && /usr/bin/dpworld-ap-apply.sh ``` marker 가 사라지면 스크립트가 `ap_down`(데몬 stop + 방화벽 flush + ap0 삭제)을 수행한다. --- ## 8. ⚠ flash 생존 caveat `/home/root/network/ap/*`(config·marker)와 `/opt` 상태는 펌웨어 flash 후에도 **생존**한다. 그러나 `deploy/` 의 systemd 유닛·`dpworld-ap-apply.sh` 는 rootfs(`/lib`, `/usr/bin`)에 설치되며 **flash 마다 wipe** 된다. 따라서 flash 후에는 AP 유닛/스크립트를 **재설치**해야 AP 가 다시 뜬다. 근본 해결(BSP 이미지 베이킹)은 백로그 — `firmware-ota-guide.md` §6 및 메모리 `web-survives-flash` 참조.