You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

133 lines
6.4 KiB

# 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` 참조.