0xFF
hostapd와 dnsmasq를 이용한 Wi-Fi AP 및 Captive Portal 테스트 본문

필자가 진행 중인 프로젝트는 캡티브 포털을 이용하여 사용자가 Wi-Fi 네트워크에 접속하자마자 웹 페이지를 띄우고, 여기서 서비스를 제공한다.
따라서 Wi-Fi 칩셋인 AR9271을 AP 모드(핫스팟)로 작동하게 해야 한다.
이를 위해서는 hostapd가 필요한데, hostapd에는 IP 할당 기능이 없어 dnsmasq등의 DHCP 서버를 함께 사용하여 연결된 station 장치들에게 IP를 할당해 주어야 한다.
$ apt install dnsmasq hostapd nginx
Debian/Ubuntu 환경에서는 위와 같이 간단하게 설치할 수 있다.
여기서 nginx는 captive portal 구현 단계에서 302 리다이렉트를 발생시키기 위해 설치하였다.
# ip addr
...
3: wlxc01c3043146b: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether c0:1c:30:43:14:6b brd ff:ff:ff:ff:ff:ff
...
설치가 끝났다면 ip addr 명령을 입력하여 AP로 사용할 인터페이스의 이름을 확인한다.
필자의 환경에서는 AR9271이 wlxc01c3043146b 이라는 이름으로 인식되었다.
interface=wlxc01c3043146b
hw_mode=g
channel=7
country_code=KR
ieee80211n=1
ssid=SafeCast
/etc/hostapd/hostapd.conf 파일을 만들고, 위와 같은 설정 사항을 입력하였다.
보안을 사용하지 않는 경우 설정할 것이 그렇게 많지 않다.
channel은 가장 혼잡도가 적은 채널을 선택하는 auto 옵션을 먼저 시도해 보았으나,
에러가 발생하여 일단 고정 채널을 사용하기로 했다.
$ service hostapd restart

여기까지 진행 후 hostapd 서비스를 재시작하면 SSID가 표시되는 것은 확인할 수 있지만,
IP 주소가 할당되지 않아 연결까지 이뤄지지는 않는 것을 확인할 수 있었다.
이제 dnsmasq를 설정하여 dhcp에 의한 IP 할당이 이뤄지도록 해보자.
dnsmasq 설정 파일의 경로는 /etc/dnsmasq.conf 이며, 필자가 입력한 내용은 아래와 같다.
no-resolv
dhcp-option=114,"<https://safecast.0xff.kr/captive_info>"
address=/#/10.0.0.1 # 모든 도메인에 대한 resolve 요청을 10.0.0.1을 반환
interface=wlxc01c3043146b
bind-interfaces
dhcp-range=wlxc01c3043146b,10.0.0.2,10.0.0.32,255.255.255.0
다음으로 부팅시 hostapd로 사용되는 네트워크 인터페이스에 게이트웨이 주소가 자동으로 할당되도록 systemd 서비스를 등록한다.
서비스 파일의 경로는 /etc/systemd/system/set-ip4hostapd.service 이다.(파일명은 임의로 지정하여도 무관)
[Unit]
Description=sets ip for hostapd iface
After=network-pre.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/ip addr add 10.0.0.1/24 dev wlxc01c3043146b
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
여기까지 진행했다면 hostapd를 위한 설정은 끝났다.
이제 네트워크에 접속한 기기가 captive portal을 자동으로 감지할 수 있도록 설정해 보자.
접속한 기기가 captive portal의 존재를 인식하는 방법은 크게 두 가지가 있다.
첫 번째는 302 리다이렉트 감지로, 주요 운영체제(iOS, Android 등)에서 와이파이 네트워크 연결 시 약속된 응답을 반환하여야 하는 인터넷상의 웹 주소에 접속해 보는 것이다.
대표적으로 Android 운영체제에서는 새로운 와이파이 네트워크 접속 시 아래와 같은 웹 주소에 접속하여 정해진 응답인 204 No Content가 반환되는지 확인한다.
http://connectivitycheck.gstatic.com/generate_204
디바이스는 제한 없는 인터넷 환경이라면 구글 서버로 접속되어 204를 수신할 것이고, 여기서 만약 captive portal을 띄우기를 원하는 네트워크 운영자는 이 요청을 가로채어 302 Redirect 응답의 Location 헤더를 통해 captive portal의 URL을 전달할 수 있다.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name safecast.0xff.kr;
if ($host != "safecast.0xff.kr") {
return 302 <http://safecast.0xff.kr>;
}
}
server {
listen 443 ssl;
server_name safecast.0xff.kr;
ssl_certificate /etc/letsencrypt/live/safecast.0xff.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/safecast.0xff.kr/privkey.pem;
root /var/www/html;
index index.html index.htm;
location /captive_info {
default_type application/captive+json;
return 200 '{"captive": true, "user-portal-url": "<https://safecast.0xff.kr>"}';
}
location / {
try_files $uri $uri/ =404;
}
}
위 내용은 /etc/nginx/sites-available/default 파일의 모습이다.
80 포트에서 실행되는 HTTP 서버 구문을 보면 captive portal의 도메인을 제외한 모든 호스트명에 대해 302를 이용해 무조건 captive portal의 URL로 전환되도록 구성하였다.
두 번째는 2020년부터 RFC 표준으로 제정된 DHCP option 114를 사용하여 captive portal의 URL을 명시적으로 전달하는 것이다.
위 파일 하단의 HTTPS 서버 구문을 보면 DHCP option 114를 통해 전달했던 captive portal API의 주소(dnsmasq 설정 부분 참고)로 접속했을 때 해당 JSON 응답을 반환함으로써
기기가 현재 인터넷 접근이 차단된 상태(captive)에 있으며, 주어진 포털 링크(user-portal-url)로 접속해야 한다는 내용을 전달할 수 있다.
$ chmod 644 /etc/systemd/system/set-ip4hostapd.service
$ systemctl daemon-reload
$ systemctl enable set-ip4hostapd.service
$ systemctl start set-ip4hostapd.service
여기까지 진행한 다음 서비스 권한부여, 데몬 리로드, 서비스를 활성화 및 시작시키고 나면 핫스팟에 접속할 수 있게 된다.


갤럭시 기준으로 네트워크 연결과 동시에 로그인 안내 및 알림이 표시되고, 알림을 누르면 웹뷰를 통해 포털에 접속이 가능한 것을 확인할 수 있었다.(페이지는 구현하지 않아 일단 포털 감지 동작만 확인)
참고로 갤럭시는 애플과 다르게 HTTP 환경에서의 302 리다이렉트만으로는 captive portal 감지 알림이 표시되지 않았고,
약 하루 정도의 삽질 끝에 DHCP 114를 통해 https로 시작하는 captive portal 링크를 전달하였을때만 감지되는 것을 확인할 수 있었다.
'LINUX' 카테고리의 다른 글
| 젯슨 나노 단독망 네트워크 구성 (0) | 2026.02.22 |
|---|---|
| Dragon Q6A Armbian에서 spidev 활성화 (0) | 2026.02.08 |
| QCS6490 Linux Host에서 LLM 구동 실험 (0) | 2026.02.05 |
| 터미널 폰트 추천 - Neo둥근모 (0) | 2020.01.05 |