본문으로 건너뛰기

DNS 정책 모듈 (v2)

이 Linux C ABI v2 예제는 DNS 질의를 제어하고 DNS 응답을 교체하는 방법을 보여 줍니다. libresolv로 DNS 메시지를 파싱하고 모듈이 소유한 버퍼에 변경한 응답을 저장합니다.

예제 동작​

질의 이름결과
allow.test질의 훅 체인을 끝내고 일반 DNS 조회를 계속합니다.
block.test업스트림 서버에 조회하지 않고 REFUSED를 반환합니다.
drop.test업스트림 조회나 DNS 응답 없이 질의를 버립니다.
redirect.testA 질의에는 192.0.2.42, AAAA 질의에는 2001:db8::42를 TTL 60초로 반환합니다.
rewrite.test서명되지 않은 응답의 A 레코드 주소를 203.0.113.7로 바꾸고 TTL을 60초로 설정합니다.
그 외 이름다음 모듈과 일반 처리를 계속합니다.

allow.test와 rewrite.test는 테스트용 DNS 서버에서 해당 이름의 응답을 제공하도록 설정하세요. 예제가 업스트림 레코드를 만들지는 않습니다. rewrite.test의 응답에 해당하는 A 레코드가 없거나 서명 레코드가 있으면 응답을 그대로 반환합니다.

ALLOW는 현재 훅 체인만 끝냅니다. 도메인 규칙이나 통신 방식 검사를 우회하지 않으며 응답 훅에서 결과가 바뀔 수도 있습니다. DNS 동작 규칙을 참고하세요.

다운로드 및 빌드​

policy.c와 mudfish_dns_module.h를 같은 디렉터리에 다운로드하세요. 헤더 안내에서 전체 헤더를 브라우저로 읽을 수 있습니다.

Linux에서 C 컴파일러와 표준 C 라이브러리 및 libresolv의 개발용 헤더를 설치하세요. 다운로드한 디렉터리에서 다음 명령을 실행합니다.

cc -std=c11 -shared -fPIC -Wall -Wextra -Werror -I. \
policy.c -lresolv -o policy.so

모듈 로드에 따라 policy.so를 설치하고 서비스를 다시 시작하세요. 질의를 테스트하기 전에 앱에서 DNS를 시작하세요.

기본 DNS 수신 주소를 사용하는 로컬 테스트에서는 다음 질의로 차단과 주소 응답 생성을 확인할 수 있습니다. dig가 설치되어 있어야 합니다.

dig @127.0.0.1 block.test A
dig @127.0.0.1 redirect.test A
dig @127.0.0.1 redirect.test AAAA

첫 번째 질의는 REFUSED를, 나머지 두 질의는 표에 나온 주소를 반환해야 합니다. 수신 주소나 포트를 바꿨다면 명령도 그에 맞게 조정하세요. drop.test 질의는 의도적으로 응답을 보내지 않으므로 시간 초과가 발생합니다.

동작 방식​

질의와 응답 콜백은 성공 시 0을 반환하고 동작은 result->action에 설정합니다. 동작 상수를 직접 반환하지 않습니다.

초기화 과정에서 모듈 컨텍스트에 응답 버퍼를 할당합니다. 응답 훅은 빌린 응답을 이 버퍼에 복사한 뒤 수정하고, destroy()에서 컨텍스트를 해제합니다. 버퍼 소유권 규칙에 따라 콜백이 반환된 뒤에도 대체 버퍼는 유효한 상태를 유지합니다.

응답 훅은 서명된 메시지를 변경하지 않습니다. DNSSEC 데이터를 검증하거나 재서명하지도 않습니다. 생성하거나 교체한 응답은 호스트 캐시에 저장하지 않습니다.

전체 소스​

policy.c
#include "mudfish_dns_module.h"

#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

struct policy {
uint8_t response[65535];
};

static int
question(const struct mudfish_dns_event *event, ns_rr *rr)
{
ns_msg message;

if (event->query.len > 65535 ||
ns_initparse(event->query.data, (int)event->query.len, &message) < 0)
return (-1);
return (ns_parserr(&message, ns_s_qd, 0, rr));
}

static int32_t
dns_query(void *context, const struct mudfish_dns_event *event,
struct mudfish_dns_result *result)
{
ns_rr rr;

(void)context;
if (question(event, &rr) < 0)
return (-1);
if (strcasecmp(ns_rr_name(rr), "allow.test") == 0)
result->action = MUDFISH_DNS_ALLOW;
else if (strcasecmp(ns_rr_name(rr), "block.test") == 0)
result->action = MUDFISH_DNS_BLOCK;
else if (strcasecmp(ns_rr_name(rr), "drop.test") == 0)
result->action = MUDFISH_DNS_DROP;
else if (strcasecmp(ns_rr_name(rr), "redirect.test") == 0) {
result->action = MUDFISH_DNS_ANSWER;
result->ttl = 60;
if (ns_rr_type(rr) == ns_t_aaaa) {
result->address_family = MUDFISH_DNS_IPV6;
inet_pton(AF_INET6, "2001:db8::42", result->address);
} else {
result->address_family = MUDFISH_DNS_IPV4;
inet_pton(AF_INET, "192.0.2.42", result->address);
}
}
return (0);
}

static int32_t
dns_response(void *context, const struct mudfish_dns_event *event,
struct mudfish_dns_result *result)
{
struct policy *policy = context;
ns_msg message;
ns_rr rr;
uint8_t *rdata;
int section, i, changed = 0;

if (question(event, &rr) < 0)
return (-1);
if (strcasecmp(ns_rr_name(rr), "rewrite.test") != 0)
return (0);
if (event->response.len > sizeof(policy->response))
return (-1);
memcpy(policy->response, event->response.data, event->response.len);
if (ns_initparse(policy->response, (int)event->response.len, &message) < 0)
return (-1);
/* This example only edits unsigned answers. A policy that edits signed
* data must also rebuild/remove the affected signatures.
*/
for (section = ns_s_an; section <= ns_s_ar; section++) {
for (i = 0; i < ns_msg_count(message, section); i++) {
if (ns_parserr(&message, (ns_sect)section, i, &rr) < 0)
return (-1);
if (ns_rr_type(rr) == ns_t_rrsig || ns_rr_type(rr) == ns_t_sig ||
ns_rr_type(rr) == ns_t_tsig)
return (0);
}
}
for (i = 0; i < ns_msg_count(message, ns_s_an); i++) {
if (ns_parserr(&message, ns_s_an, i, &rr) < 0)
return (-1);
if (ns_rr_class(rr) != ns_c_in || ns_rr_type(rr) != ns_t_a ||
ns_rr_rdlen(rr) != 4)
continue;
/* rdata belongs to our copy. TTL precedes RDLENGTH (2 bytes). */
rdata = policy->response + (ns_rr_rdata(rr) - policy->response);
inet_pton(AF_INET, "203.0.113.7", rdata);
ns_put32(60, rdata - 6);
changed = 1;
}
if (changed) {
result->action = MUDFISH_DNS_REPLACE;
result->response.data = policy->response;
result->response.len = event->response.len;
}
return (0);
}

static void
destroy(void *context)
{

free(context);
}

int32_t
mudfish_dns_module_init_v2(uint32_t abi_version, size_t module_size,
struct mudfish_dns_module_v2 *module)
{

if (abi_version != MUDFISH_DNS_MODULE_ABI_V2 || module_size != sizeof(*module))
return (-1);
module->context = calloc(1, sizeof(struct policy));
if (module->context == NULL)
return (-1);
module->on_dns_query = dns_query;
module->on_dns_response = dns_response;
module->destroy = destroy;
return (0);
}