OpenWrt dnsmasq 개요
OpenWrt 환경에서 dnsmasq는 네트워크 관리의 핵심 데몬으로 작동합니다. 이 프로세스는 크게 두 가지 주요 기능을 수행합니다. 첫째, LAN 인터페이스를 위한 DHCP 서버 역할을 하여 클라이언트에 IP를 할당하고, 둘째, IPv4 및 IPv6를 모두 지원하는 DNS 리졸버 및 캐시 서버로 작동하여 라우터의 DNS 쿼리를 처리합니다.
1. 데몬 초기화 및 실행 흐름
dnsmasq는 /etc/init.d/dnsmasq 초기화 스크립트를 통해 시작됩니다. 시작 시 UCI(Unified Configuration Interface) 설정 파일을 파싱하여 실제 데몬이 사용할 런타임 설정 파일을 생성합니다.
UCI 설정 예시 (/etc/config/dhcp)
config dnsmasq
option domainneeded '1'
option boguspriv '1'
option localise_queries '1'
option rebind_protection '1'
option domain 'home.lan'
option expandhosts '1'
option authoritative '1'
option readethers '1'
option leasefile '/tmp/dhcp.leases'
option resolvfile '/tmp/resolv.conf.auto'
config dhcp 'lan'
option interface 'lan'
option start '50'
option limit '200'
option leasetime '24h'
start_service 함수는 위 UCI 설정과 네트워크 설정을 결합하여 /var/etc/dnsmasq.conf 파일을 동적으로 생성합니다.
생성된 런타임 설정 (/var/etc/dnsmasq.conf)
# /etc/config/dhcp 기반으로 자동 생성됨
conf-file=/etc/dnsmasq.conf
dhcp-authoritative
domain-needed
localise-queries
bogus-priv
expand-hosts
bind-interfaces
domain=home.lan
server=/home.lan/
dhcp-leasefile=/tmp/dhcp.leases
resolv-file=/tmp/resolv.conf.auto
addn-hosts=/tmp/hosts
conf-dir=/tmp/dnsmasq.d
dhcp-range=lan,192.168.50.50,192.168.50.249,255.255.255.0,24h
설정 파일이 준비되면 C 언어로 작성된 메인 로직이 실행됩니다. 초기화 과정 이후, 이벤트 루프에서 네트워크 패킷과 시스템 이벤트를 처리합니다.
메인 이벤트 루프 재구성 (C 코드)
while (daemon_running) {
int wait_time = -1;
int listener_delay;
reset_poll_descriptors();
listener_delay = configure_dns_listeners(current_time);
if (listener_delay > 0) {
wait_time = listener_delay * 1000;
}
if (ctx->tftp_active || (option_enabled(OPT_DBUS) && !ctx->dbus_conn)) {
wait_time = 250;
} else if (check_dad_status()) {
wait_time = 1000;
}
#ifdef HAVE_DHCP
if (ctx->dhcp_enabled || ctx->relay4_active) {
add_poll_fd(ctx->dhcp_sock, POLLIN);
if (ctx->pxe_sock != -1)
add_poll_fd(ctx->pxe_sock, POLLIN);
}
#endif
#if defined(HAVE_LINUX_NETWORK)
add_poll_fd(ctx->netlink_sock, POLLIN);
#endif
add_poll_fd(pipe_read_end, POLLIN);
prepare_log_writer();
if (execute_poll(wait_time) < 0)
continue;
current_time = get_monotonic_time();
process_log_writer();
refresh_interfaces(1);
if (check_dad_status()) {
refresh_interfaces(0);
bind_pending_listeners();
}
#if defined(HAVE_LINUX_NETWORK)
if (check_poll_event(ctx->netlink_sock, POLLIN))
handle_netlink_events();
#endif
if (check_poll_event(pipe_read_end, POLLIN))
handle_async_events(pipe_read_end, current_time);
process_dns_listeners(current_time);
#ifdef HAVE_DHCP
if (ctx->dhcp_enabled || ctx->relay4_active) {
if (check_poll_event(ctx->dhcp_sock, POLLIN))
handle_dhcp_packet(current_time, 0);
}
#endif
}
2. DNS 쿼리 처리 메커니즘
DNS 쿼리 로깅을 활성화하면 패킷 처리 흐름을 명확히 추적할 수 있습니다. 쿼리는 로컬 캐시를 먼저 확인하고, 캐시에 없는 경우 업스트림 DNS 서버로 전달됩니다.
query[A] github.com from 127.0.0.1
cached github.com is 140.82.121.3
query[A] naver.com from 192.168.50.105
forwarded naver.com to 168.126.63.1
reply naver.com is 223.130.200.104
첫 번째 요청은 로컬 캐시에 존재하여 즉시 응답합니다. 두 번째 요청은 캐시에 없으므로 /tmp/resolv.conf.auto에 기록된 업스트림 DNS(예: 168.126.63.1)로 포워딩됩니다. 이 업스트림 주소는 netifd가 WAN 인터페이스를 통해 획득한 값입니다.
내부적으로 process_dns_listeners() 함수가 요청을 수신하며, 캐시 미스 시 forward_query()를 호출하여 상위 서버로 패킷을 전송합니다.
포워딩 로직 디버깅 코드 (C 코드)
if (sys_errno == 0) {
ctx->saved_srv = current_start;
ctx->pkt_length = payload_len;
log_debug("Forwarding query: domain=%s, client=%s", ctx->query_buffer, ctx->addr_buffer);
if (!has_name) {
strncpy(ctx->query_buffer, "unknown_query", MAX_NAME_LEN);
}
if (current_start->sock_addr.sa.sa_family == AF_INET) {
record_query(F_SERVER | F_IPV4 | F_FORWARD, ctx->query_buffer,
(struct in_addr *)¤t_start->sock_addr.in.sin_addr, NULL);
}
#ifdef HAVE_IPV6
else {
record_query(F_SERVER | F_IPV6 | F_FORWARD, ctx->query_buffer,
(struct in6_addr *)¤t_start->sock_addr.in6.sin6_addr, NULL);
}
#endif
current_start->query_count++;
is_forwarded = 1;
fwd_record->target = current_start;
}
3. DHCP IP 할당 프로세스
DHCP 서버는 표준 DORA(Discover, Offer, Request, Acknowledge) 4단계 핸드셰이크를 통해 IP를 할당합니다.
dnsmasq-dhcp[4102]: DHCPDISCOVER(br-lan) 48:51:b7:c2:9a:1f
dnsmasq-dhcp[4102]: DHCPOFFER(br-lan) 192.168.50.120 48:51:b7:c2:9a:1f
dnsmasq-dhcp[4102]: DHCPREQUEST(br-lan) 192.168.50.120 48:51:b7:c2:9a:1f
dnsmasq-dhcp[4102]: DHCPACK(br-lan) 192.168.50.120 48:51:b7:c2:9a:1f
할당된 임대 정보는 /tmp/dhcp.leases 파일에 지속됩니다.
1685432100 48:51:b7:c2:9a:1f 192.168.50.120 user-laptop 01:48:51:b7:c2:9a:1f
4. DNS 리바인딩 보호 및 내부망 접근
OpenWrt는 기본적으로 DNS 리바인딩 보호(rebind_protection)가 활성화되어 있습니다. 이는 업스트림 DNS가 사설 IP 대역(예: 192.168.x.x, 10.x.x.x)을 응답으로 반환할 경우, 이를域名 하이재킹으로 간주하고 패킷을 드롭하는 보안 기능입니다.
사내망이나 특정 내부 도메인 접근 시 이 기능으로 인해 연결이 실패할 수 있으며, 이 경우 UCI 설정에서 보호 기능을 비활성화해야 합니다.
uci set dhcp.@dnsmasq[0].rebind_protection='0'
uci commit dhcp
/etc/init.d/dnsmasq restart
5. 로컬 호스트명 및 게이트웨이 도메인 매핑
라우터의 LAN IP가 변경되더라도 일관된 도메인으로 관리 페이지에 접속할 수 있도록 호스트 매핑을 구성할 수 있습니다. /etc/init.d/dnsmasq 스크립트는 시스템 호스트명을 읽어 /tmp/hosts/dhcp 파일에 자동으로 등록합니다.
시스템 호스트명을 변경하여 게이트웨이 도메인을 커스터마이즈할 수 있습니다.
uci set system.@system[0].hostname='myrouter.local'
uci commit system
/etc/init.d/dnsmasq restart
또는 리눅스 표준 방식인 /etc/hosts 파일을 직접 수정하여 정적 매핑을 추가할 수도 있습니다.
192.168.50.1 myrouter.local admin.home
6. 정적 IP 할당 및 MAC 바인딩
특정 디바이스에 항상 고정된 IP를 할당하려면 MAC 주소와 IP를 바인딩해야 합니다. 이는 UCI를 통해 간편하게 설정 가능합니다.
uci add dhcp host
uci set dhcp.@host[-1].name='server-node'
uci set dhcp.@host[-1].ip='192.168.50.200'
uci set dhcp.@host[-1].mac='00:1a:2b:3c:4d:5e'
uci commit dhcp
/etc/init.d/dnsmasq restart
위 설정은 런타임 시 dhcp-host=00:1a:2b:3c:4d:5e,192.168.50.200,server-node 형태로 변환되어 적용됩니다. 또한, name 필드를 설정하면 DHCP 리스 파일과 내부 DNS에서 해당 디바이스의 호스트명이 자동으로 덮어써져 관리가 용이해집니다.
7. 도메인 기반 DNS 라우팅
특정 도메인에 대해 지정된 DNS 서버를 사용하도록 트래픽을 분할할 수 있습니다. 이는 내부망 도메인 해석이나 특정 서비스 최적화에 유용합니다.
# 내부망 도메인은 사내 DNS로 라우팅
uci add_list dhcp.@dnsmasq[0].server='/corp.internal/10.0.0.53'
# 특정 외부 서비스는 Cloudflare DNS 사용
uci add_list dhcp.@dnsmasq[0].server='/apple.com/1.1.1.1'
uci commit dhcp
/etc/init.d/dnsmasq restart
8. 도메인 차단을 통한 광고 필터링
dnsmasq의 주소 재정의 기능을 활용하면 특정 도메인을 로컬호스트나 블락홀 IP로 리디렉션하여 광고나 트래커를 차단할 수 있습니다. 와일드카드를 사용한 서브도메인 전체 차단도 지원합니다.
# 특정 광고 도메인 차단
uci add_list dhcp.@dnsmasq[0].address='/ads.tracker.net/0.0.0.0'
uci add_list dhcp.@dnsmasq[0].address='/telemetry.service.com/0.0.0.0'
# 와일드카드를 활용한 서브도메인 전체 차단
uci add_list dhcp.@dnsmasq[0].address='/.*.adnetwork.com/0.0.0.0'
uci commit dhcp
/etc/init.d/dnsmasq restart