Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 

Repository files navigation

DNS Single Proxy

1. 프로젝트 개요

항목 내용
목적 __check_pf() 호출 회피를 위해 DNS 응답을 타입당 1개로 제한
대체 대상 systemd-resolved
호환성 /etc/systemd/resolved.conf 설정 파일 재사용

2. 핵심 기능

[필수]

  • UDP/TCP DNS 서버 (포트 53)
  • Upstream DNS 쿼리 및 응답 필터링
  • 응답 레코드를 타입(A, AAAA, CNAME 등)당 1개로 제한
  • TTL 기반 캐시
  • resolved.conf 파싱 [선택]
  • DNS over TLS (DoT)
  • 헬스체크/메트릭

3. 설정 파일 스펙

3.1 파싱 대상

  1. /etc/systemd/resolved.conf
  2. /etc/systemd/resolved.conf.d/*.conf (알파벳 순, 나중 값이 override)

3.2 지원 항목

타입 기본값 설명
DNS List 8.8.8.8 주 DNS 서버 (공백 구분)
FallbackDNS List 1.1.1.1 폴백 DNS 서버
Cache boolean true 캐시 활성화 (yes/no)
DNSStubListener boolean true 127.0.0.53:53 리스닝
DNSStubListenerExtra List [] 추가 리스닝 주소

3.3 파싱 규칙

  • [Resolve] 섹션만 처리
  • '#', ';' 로 시작하는 줄은 주석
  • 빈 줄 무시
  • key=value 형식 (공백 trim)
  • 알 수 없는 키는 무시 (경고 로그)
  • DNS, FallbackDNS, DNSStubListenerExtra는 누적 방식
    • 같은 키를 여러 줄로 작성하면 모두 누적됨
    • 공백으로 구분된 여러 값도 지원

3.4 DNS 서버 우선순위

1. networkctl status의 DNS (DHCP로 받은 DNS) - 최우선
       ↓ (없으면)
2. resolved.conf의 DNS= 설정
       ↓ (없으면)
3. /run/systemd/resolve/resolv.conf의 nameserver 항목
       ↓ (없으면)
4. /etc/resolv.conf의 nameserver 항목
       ↓ (없으면)
5. FallbackDNS의 첫 번째 항목을 Primary로 승격 (경고 출력)
       ↓ (없으면)
6. 시작 실패 (에러)

참고:

  • networkctl status 출력에서 DNS: x.x.x.x 형식의 라인을 파싱 (DHCP 환경 지원)
  • DHCP로 받은 DNS가 실제 네트워크 환경을 반영하므로 최우선 적용
  • /run/systemd/resolve/resolv.conf: systemd-resolved가 관리하는 실제 upstream DNS 목록
    • /etc/resolv.conf(stub, 127.0.0.53)와 달리 실제 DNS 서버 IP를 포함
    • 서비스 시작 전 ExecStartPre로 systemd-resolved를 일시 기동하여 파일 생성 보장 (아래 8.4 참고)
  • localhost (127.0.0.1, 127.0.0.53, ::1) 주소는 무시
  • FallbackDNS=는 Primary DNS 전체 실패 시에만 사용
  • Primary DNS가 FallbackDNS에서 승격된 경우 시작 시 경고 로그 출력:
    WARN  - No DNS configured. Using first FallbackDNS (x.x.x.x) as primary DNS.
    

3.5 DNS 설정 예시

[Resolve]
# 방법 1: 공백으로 구분
DNS=1.1.1.1 8.8.8.8 9.9.9.9

# 방법 2: 여러 줄로 작성 (누적됨)
DNS=1.1.1.1
DNS=8.8.8.8
DNS=9.9.9.9

# 방법 3: 혼합 사용 가능
DNS=1.1.1.1 8.8.8.8
DNS=9.9.9.9
# 결과: [1.1.1.1, 8.8.8.8, 9.9.9.9]

FallbackDNS=1.0.0.1
FallbackDNS=8.8.4.4
# 결과: [1.0.0.1, 8.8.4.4]

4. DNS 프로토콜 스펙

4.1 서버

항목
프로토콜 UDP (필수), TCP (필수)
기본 바인드 127.0.0.53:53
메시지 크기 UDP 512 bytes (EDNS 미지원 시), TCP 65535 bytes

4.2 클라이언트 (Upstream 쿼리)

항목
타임아웃 2초
재시도 주 DNS 전체 시도 → FallbackDNS 전체 시도
포트 53

4.2.1 여러 DNS 서버 동작 방식

DNS 쿼리 요청
    │
    ▼
┌─────────────────────────────────────┐
│ Primary DNS 서버 순차 시도           │
│ (DNS= 에 설정된 순서대로)            │
│                                     │
│  DNS[0] → 실패 → DNS[1] → 실패 → ... │
└─────────────────────────────────────┘
    │ 모두 실패
    ▼
┌─────────────────────────────────────┐
│ Fallback DNS 서버 순차 시도          │
│ (FallbackDNS= 에 설정된 순서대로)     │
│                                     │
│  Fallback[0] → 실패 → Fallback[1]...│
└─────────────────────────────────────┘
    │ 모두 실패
    ▼
  SERVFAIL 반환

동작 규칙:

  • 각 서버에 2초 타임아웃 적용
  • 첫 번째 성공 응답 즉시 반환 (나머지 서버 시도 안 함)
  • UDP 응답이 truncated면 같은 서버에 TCP로 재시도
  • Primary 전체 실패 시에만 Fallback 시도

4.3 응답 필터링 로직

// Pseudocode
Map<Integer, ResourceRecord> seen = new HashMap<>();
List<ResourceRecord> filtered = new ArrayList<>();
for (ResourceRecord rr : response.getAnswers()) {
    int type = rr.getType();  // A=1, AAAA=28, CNAME=5, ...
    if (!seen.containsKey(type)) {
        seen.put(type, rr);
        filtered.add(rr);
    }
}
response.setAnswers(filtered);

참고: 타입당 첫 번째 레코드만 선택하므로, Upstream DNS가 응답 순서를 변경(Round-Robin)하면 캐시 만료 후 다른 IP가 반환될 수 있습니다. 로드밸런싱용 DNS는 일반적으로 TTL이 짧아(30~300초) 로드밸런싱 효과가 유지됩니다.

5. 캐시 스펙

항목
{qname}:{qtype}:{qclass}
TTL 응답의 최소 TTL 사용
최대 엔트리 10,000 (설정 가능)
Eviction TTL 만료 또는 LRU
네거티브 캐시 NXDOMAIN 30초

6. 클래스 구조

com.logpresso.dnsproxy
├── Main.java                    # 엔트리포인트
├── config/
│   ├── ResolvedConfig.java      # 설정 모델
│   └── ResolvedConfigParser.java
├── server/
│   ├── DnsServer.java           # UDP/TCP 서버
│   └── DnsHandler.java          # 요청 처리
├── client/
│   └── UpstreamResolver.java    # Upstream 쿼리
├── filter/
│   └── SingleRecordFilter.java  # 타입당 1개 필터
└── cache/
└── DnsCache.java            # TTL 캐시

7. 의존성

<!-- DNS 메시지 파싱 -->
<dependency>
    <groupId>dnsjava</groupId>
    <artifactId>dnsjava</artifactId>
    <version>3.5.3</version>
</dependency>

또는 직접 파싱 (외부 의존성 제거 시)

8. 실행/배포

8.1 빌드

mvn clean package -DskipTests
# 결과: target/dns-single-proxy.jar

8.2 실행

java -jar dns-single-proxy.jar
# 또는 설정 경로 지정
java -jar dns-single-proxy.jar --config /etc/systemd/resolved.conf

8.3 systemd 서비스 설치/제거

설치 (--install)

systemd-resolved를 대체하여 시스템 DNS 서비스로 설치합니다.

# 빌드 후 설치 (root 권한 필요)
mvn clean package -DskipTests
sudo java -jar target/dns-single-proxy.jar --install

# 특정 Java 경로 지정 (예: OpenJDK 17)
sudo java -jar target/dns-single-proxy.jar --java /usr/lib/jvm/java-17-openjdk/bin/java --install

# 또는 = 형식으로도 가능
sudo java -jar target/dns-single-proxy.jar --java=/opt/java/bin/java --install

--java 옵션:

  • systemd 서비스 파일의 ExecStart에 사용할 Java 실행 파일 경로 지정
  • 기본값: /usr/bin/java
  • 시스템에 여러 Java 버전이 설치된 경우 특정 버전 선택 가능
  • --install과 함께 사용할 때만 의미 있음

설치 과정:

  1. systemd-resolved 서비스 중지 및 비활성화
  2. /opt/dns-single-proxy/ 디렉토리 생성
  3. JAR 파일 복사
  4. systemd 서비스 파일 생성 (/etc/systemd/system/dns-single-proxy.service)
    • ExecStartPre에 systemd-resolved 일시 기동 포함 (DNS 설정 파일 생성 목적)
  5. 서비스 활성화 및 시작

설치 후 확인:

systemctl status dns-single-proxy
journalctl -u dns-single-proxy -f
제거 (--uninstall)

dns-single-proxy를 제거하고 systemd-resolved를 복원합니다.

sudo java -jar /opt/dns-single-proxy/dns-single-proxy.jar --uninstall

제거 과정:

  1. dns-single-proxy 서비스 중지 및 비활성화
  2. 서비스 파일 삭제
  3. /opt/dns-single-proxy/ 디렉토리 삭제
  4. systemd-resolved 재활성화 및 시작

8.4 수동 Systemd 서비스 설정 (참고용)

--install 옵션이 자동으로 생성하는 서비스 파일 내용:

[Unit]
Description=DNS Single Proxy
Documentation=https://github.com/logpresso/dns-single-proxy
After=network.target
Before=nss-lookup.target
Wants=nss-lookup.target

[Service]
Type=simple
ExecStartPre=-/bin/sh -c 'systemctl start systemd-resolved && echo "dns-single-proxy: started systemd-resolved for DNS bootstrap"'
ExecStartPre=-/bin/sh -c 'systemctl stop systemd-resolved; echo "dns-single-proxy: stopped systemd-resolved, taking over DNS"'
ExecStart=/usr/bin/java -jar /opt/dns-single-proxy/dns-single-proxy.jar
Restart=always
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

ExecStartPre 동작:

  • start 단계: systemd-resolved를 일시 기동하여 /run/systemd/resolve/resolv.conf 등 런타임 파일 생성. 성공 시 journal에 기록
  • stop 단계: 포트 53(127.0.0.53) 해제 후 dns-single-proxy가 해당 주소를 점유. 결과 항상 journal에 기록
  • - 접두사: 해당 명령 실패 시(systemd-resolved 미설치 등) 무시하고 계속 진행
  • journal 출력 예시 (journalctl -u dns-single-proxy):
    dns-single-proxy: started systemd-resolved for DNS bootstrap
    dns-single-proxy: stopped systemd-resolved, taking over DNS
    

참고: --java 옵션을 사용하면 ExecStart의 Java 경로가 지정된 경로로 변경됩니다.

9. 로깅

INFO  - 서버 시작/종료
INFO  - 설정 로드 완료
WARN  - Upstream 쿼리 실패, 폴백 사용
DEBUG - 개별 쿼리/응답 (기본 off)
ERROR - 바인드 실패, 설정 파싱 오류

10. 테스트 시나리오

케이스 검증
A 레코드 여러 개 응답 1개만 반환
A + AAAA 혼합 각 1개씩 반환
CNAME 체인 CNAME 1개 + A 1개
NXDOMAIN 그대로 전달, 캐시
Upstream 전체 실패 SERVFAIL 반환
캐시 히트 Upstream 쿼리 없음

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages