1 왜 systemd-resolved stub resolver와 충돌하는가
systemd-resolved가 켜진 시스템에서 /etc/resolv.conf는 종종 스텁용 심벌릭 링크입니다. 사용자 공간 프로그램이 보통의 포트 53 UDP/TCP 조회를 할 때 목적지가 루프백 스텁(127.0.0.53)로 고정돼 있습니다. Resolved는 받은 요청을 내부에서 다시 업스트림 DNS로 중계하면서 로컬 캐시·분할 라우팅을 담당합니다.
Mihomo와 호환되는 TUN 모드를 쓰면 IP 레이어의 outbound는 종종 가상 터널을 따릅니다. 그러나 기본 설정 상태에서는 DNS만 별 트랙으로 남거나, 애플리케이션이 계속해서 127.0.0.53에게만 묻는 구조입니다. 이때 Mihomo 설정의 DNS hijack/redir-port와 시스템 스텁이 같은 홉을 두 번 들리게 되면 조회 결과가 브라우저·패키지 관리자·컨테이너 레지스트리에 일관되지 않거나, 업스트림이 다시 Mihomo 안으로 접히는 형태로 루프에 가까운 상태가 만들어집니다.
배포판이 다르더라도 공통 패턴은 «TUN만 켰다고 전체 이름해석 파이프가 자동 교체되지 않는다»는 점입니다. 따라서 우리가 할 일은 (A) 시스템이 어떤 호스트에게 질문하는지 명확히 하고 (B) 그 호스트를 Mihomo 리스너로 연결할지 아니면 upstream 직통으로 돌려놓을지 결정하는 것입니다. 기본 설치 절차는 Linux Mihomo·systemd 설치 튜토리얼과 이어 받아 읽으면 맥락이 더 잘 맞습니다.
2 흔하게 보이는 증상 목록
- TUN 활성 상태인데
resolvectl query나dig기본 타깃이 여전히127.0.0.53 - Firefox·Chrome 등 앱 간으로 같은 도메인인데 결과 IP가 서로 다름 FakeIP 또는 직통 혼합
- 가끔만 타임아웃처럼 보이지만 실제로는 짧은 루프로 인한 초과 지연
- 직접 모드 블록은 업데이트됐으나 패키지
apt·dnf만 느리거나 실패 같은 비대칭 패턴 - 도커 브리지나 가상머신 게스트가 호스트 스텁에만 물려 업스트림이 기대와 다름
3 먼저 실행하는 진단 순서
성급하게 설정을 바꾸기 전에 다음 세 줄을 통해 «지금 누가 DNS를 들고 있는가»부터 고정합니다. 출력은 사용자 환경에 따라 크게 달라질 수 있으니, 결과를 메모하고 단계별로 한 번만 바꾸는 방식으로 되돌리기 쉽게 유지합니다.
# Where resolv.conf points
readlink -f /etc/resolv.conf
# Resolved view of uplink DNS and routing
resolvectl status
# Listening UDP/TCP sockets on stub or custom port (example filter)
ss -lunp '( sport = :53 or sport = :1053 )'
일반적으로 Debian·Ubuntu 계열에서는 /etc/resolv.conf가 ../run/systemd/resolve/stub-resolv.conf을 가리킵니다. 이 파일에는 항상 또는 거의 항상 127.0.0.53 줄이 존재합니다. 반대로 스텁을 끈 뒤에는 resolv.conf가 /run/systemd/resolve/resolv.conf를 가리키도록 바뀌는 패턴을 보기도 하는데, 이 경우 실제 전달되는 서버 줄이 업스트림에 더 가까워지면서 우리가 의도하는 «Mihomo가 중심인 지역적 스택»을 깔아 넣기 쉽습니다.
WSL 또는 중첩 가상화
WSL·KVM 게스트에서는 호스트 브리지 설정에 따라 이름해석 교차가 발생합니다. Linux 호스트 레벨에서 같은 개념을 적용했다면 게스트에서는 다시 다른 스텁이 돌며 혼합될 수 있으므로, 별도 브리지별로 이 가이드를 복사해 적용하세요. QEMU·NAT 환경의 리스너 순서도 QEMU KVM과 Mihomo NAT 구성글과 병렬로 읽어두면 디버깅이 빠릅니다.
4 해결 전략 요약 — 한 축만 바꿉니다
실무에서는 A안과 B안 중 하나만 택해서 끝까지 밀어붙입니다. 둘을 동시에 흔들면 되돌리기가 어려워지고 회사 보안 장비와도 충돌합니다.
- A안(systemd 우선 재구성):
resolved.conf에서 스텁을 끄고, 전역 혹은 링크 단위 DNS를 Mihomo 포트 또는 공인 업스트림으로 정한다. - B안(NetworkManager 또는 netplan 우선): 접속별 DNS를 명시적으로 덮어씌워 resolved가 스텁이 아니라 선택한 업스트림을 그대로 쓰도록 맞춘다.
공통되는 전제는 Mihomo 쪽에는 반드시 dns.listen으로 고정된 리스너가 있어야 한다는 점입니다. 예시로 많이 쓰는 값은 로컬 루프백 특정 포트이며 실제 번호는 사용 중 포트 충돌 검사 후 결정하면 됩니다. FakeIP 계열 레이아웃·DoH 백업의 관계까지 손보고 싶다면 Meta Mihomo DNS·유출 방지 가이드와 연결해서 읽는 것이 안전합니다.
5 DNSStubListener=no와 링크 정리
Ubuntu·Fedora 사용자에게 가장 익숙한 트릭은 /etc/systemd/resolved.conf의 [Resolve] 블록에서 DNSStubListener=no를 활성화한 뒤 systemctl restart systemd-resolved를 하는 흐름입니다. 그러면 127.0.0.53에서 듣던 스텁이 사라져 애플리케이션이 더 상위 줄의 서버 문자열을 읽거나, 사용자가 교체해 둔 nameserver를 직접 사용하는 경향이 커집니다.
[Resolve]
DNS=127.0.0.1#1053
FallbackDNS=
DNSStubListener=no
표의 포트 포함 표기(#1053 형식)는 systemd 버전과 배포판에 따라 허용 범위가 다릅니다. 자신의 패키지가 구버전이라면 주석 처리된 매뉴얼 예문을 따라가며 테스트하거나, 대신 Mihomo 리스너를 53번으로 올린 뒤 이름만 127.0.0.1로 표기합니다. 특권 계정 필요·다른 레지던트 프로세스와의 충돌은 별도로 점검해야 합니다.
링크를 유지해야 하는 환경에서는 resolvectl dns INTERFACE 127.0.0.1와 같이 무선 혹은 이더넷 인터페이스 이름에 직접 태워 보는 접근법이 있습니다. 테스트 후 재부팅에도 남도록 NetworkManager 또는 netplan 레벨 저장을 추가하는 과정까지 한 세트입니다.
6 Mihomo dns 블록 정렬 노트
TUN 활성 상태에서도 이름해석이 코어 규칙과 어긋나면 패킷이 기대 노드와 다른 경로를 탑니다. 최소 변경으로는 Mihomo 설정의 DNS 섹션에 enable: true와 루프백 특정 포트의 listen을 넣습니다. 업스트림 nameserver와 fallback은 지역 회선 규모에 따라 다르지만, 절대 방금 우리가 끄려는 systemd 스텁으로만 닫히는 순환 참조를 만들지 않도록 합니다.
dns:
enable: true
listen: 127.0.0.1:1053
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
nameserver:
- 223.5.5.5
- tls://dns.google
fallback:
- https://1.1.1.1/dns-query
위처럼 작성해 둔 상태에서 systemd-resolved 설정의 DNS=127.0.0.1#1053를 맞추면 «애플리케이션 → resolved → Mihomo 로컬 리스너 → 업스트림 DoH/TCP」라는 명확한 한 줄입니다. 브라우저 자체 보안 DNS(HTTPS) 기능을 병행해서 쓰고 있다면 Chrome·Edge 안전 DNS와 FakeIP 호환글에서 이중 채널을 분리하는지 한 번 확인하세요.
7 NetworkManager를 쓸 때 현실적인 순서
데스크톱 GNOME·KDE는 nmcli 혹은 GUI에서 연결별 DNS 문자열을 덮습니다. 테스트 절차는 짧습니다. 연결 이름을 특정해서 추가 DNS에 루프백의 선택 포트 또는 공인 업스트림을 순서대로 넣습니다. 변경 후에는 resolvectl dns 출력이 새 값을 반영하는지 확인했고, 무선 접속 재연결 또는 nmcli networking off && nmcli networking on 수준까지 가면 됩니다.
nmcli conn show active
sudo nmcli con mod "PROFILE_NAME" ipv4.ignore-auto-dns yes
sudo nmcli con mod "PROFILE_NAME" ipv4.dns "127.0.0.1"
sudo nmcli con up "PROFILE_NAME"
IPv6 전용 링크가 동시에 켜진 환경이면 같은 방식으로 ipv6.dns도 정리해야 합니다. 테스트할 때에는 두 스택 각각 한 번에 한 변수만 변경하는 습관이 트래픽 누락을 줄입니다.
8 루프와 «보이지 않는» 유출을 피하기
가장 많이 보는 실패는 Mihomo의 업스트림이 다시 systemd-resolved에게만 재질문하게 만드는 순환입니다. 이 상황을 막으려면 외부 업스트림(공인 레지버나 제공자 DoH 라인)까지 한 번이라도 깨진 연결이 확실히 존재하도록 작성합니다.
다른 한편 일부 프로그램은 자체 레지버를 내장해서 OS 스택만 만든다면 남습니다. 따라서 사용자 가시 범위의 «DNS 유출 테스트 사이트» 결과 하나만 신뢰하지 말고, 패키지 설치 프로그램·persist된 컨테이너처럼 잘 안 보이는 경로까지 짧게 점검합니다.
9 변경 후 검증 체크리스트
resolvectl status에서 활성 업링크 이름과 DNS 줄이 교체 결과와 일치- Mihomo API 혹은 로그에서 새 질문이 들어오는 패턴 재확인
- 두 개 이상의 상이한 프로그램(브라우저와
curl등)에 동일 테스트 도메인 요청 후 IP 대역 일관성 검토 - 재부팅 후에도
/etc/resolv.conf레이아웃이 의도 유지 또는 문서화된 자동 교체 패턴 재현
# Example targeted query toward local listener after changes
dig @127.0.0.1 -p 1053 example.org +tcp +timeout=3
테스트가 장시간 간다면 패킷 캡처보다 우선 순위로 Mihomo 로그 기반의 짧은 패킷 카운팅을 활용하면 되돌림이 간단합니다. 관리 자동화는 앞선 systemd 장기 상주 설정과 연계해 한 번 더 점검합니다.
10 정리
127.0.0.53는 그 자체가 «나쁜 주소»가 아니라 systemd-resolved가 제공하는 현대적인 Linux 데스크톱의 기본 접점입니다. 문제는 같은 머신에서 Mihomo의 TUN·DNS 레이어와 의도 불일치된 채 두 스택을 얹었을 때 드러납니다. 순서만 맞추면 스텁 리스너를 끄고 로컬 리스너로 연결하거나, 접속별 DNS 문자열에서 직접 Mihomo 루프백을 지목하는 간단한 그림 한 장으로 줄어듭니다.
이 워크플로는 패키지 수준에서도 GUI 도구 없이 재현 가능하므로 장비마다 레시피를 재사용할 수 있습니다. 동일 패턴을 사용자 친화적 바이너리로 즐긴다 할지라도, 충돌 순간에는 이 글이 스텁 레이어·리스너·업스트림의 단어를 놓치지 않게 해 줄 것입니다. 일상에서는 웹 접속 속도 차이 하나가 전체 업무 속도 차이와 연결되기도 하니, 한 번 검증 패스를 통해 오래도록 안정되는 구조를 깔아 두는 편이 이득입니다.
데스크톱·노트북에서는 커널 기능 대신 패널 제공 TUN 편리함을 택하기도 하는데, 이 경우에도 설치 절차·업데이트 경로가 통합된 패키지가 있으면 패치 주기까지 같이 따라가기 편합니다. 여러 브라우저와 앱 간 경험이 한결같아지도록 Clash 베스트 프랙티스 생태계를 한 도구 세트 위에 놓아 비교해 보세요.