OpenWrt Passwall에서 IPv6만 우회될 때 DNS·TProxy를 나누는 법

127.0.0.1 필터 문제로 시작하면 범위가 어긋납니다

Passwall에서 IPv4 연결은 프록시를 통과하지만 IPv6 연결만 직접 나가거나 실패할 때, 먼저 127.0.0.1을 IPv6 필터 대상으로 보면 안 됩니다. 127.0.0.1은 IPv4 loopback이고 IPv6 loopback은 ::1입니다. 실제 문제는 DNS가 AAAA를 돌려주는지, IPv6 투명 프록시가 켜졌는지, 클라이언트 IPv6가 Passwall의 nftables chain을 통과하는지로 나눠야 합니다.

또한 LuCI의 Filter Proxy Host IPv6는 DNS에서 프록시 대상의 IPv6 응답을 제한하는 기능이고, 모든 IPv6 트래픽을 프록시로 보내는 스위치와 같지 않습니다. 공식 코드에서 전자는 filter_proxy_ipv6, 후자는 ipv6_tproxy로 별도 처리됩니다.

이 글에서 직접 확인한 범위

이 절차는 2026-08-08 기준 Passwall 공식 저장소의 커밋 a2989df1868c517cc330aa007e1b9987683d0535를 내려받아 LuCI 설정과 실행 스크립트, nftables rule 생성 경로를 대조한 결과입니다. 실제 OpenWrt 라우터에서 장애를 재현한 결과는 아니므로, 장비별 패키지 버전과 실행 출력은 아래 명령으로 별도 확인해야 합니다.

SOURCE_COMMIT=a2989df1868c517cc330aa007e1b9987683d0535
FILTER_PROXY_IPV6=global.filter_proxy_ipv6
PROXY_IPV6=global_forwarding.ipv6_tproxy
NFT_TABLE='inet passwall'
IPV6_CHAINS='PSW_MANGLE_V6 PSW_OUTPUT_MANGLE_V6'

이 커밋의 global.lua는 filter_proxy_ipv6를 DNS 탭의 옵션으로 정의합니다. app.sh는 값이 켜졌을 때 remote DNS query strategy를 IPv4 쪽으로 제한할 수 있지만, 투명 프록시 활성 여부는 별도의 ipv6_tproxy에서 읽습니다. nftables.sh는 IPv6 proxy가 활성화된 조건에서 inet passwall 아래 IPv6 chain과 set을 만듭니다.

먼저 네 층을 한 번에 저장합니다

층 확인 값 실패를 좁히는 기준
주소와 route LAN/WAN IPv6 주소, 기본 route 기본 route가 없으면 우회가 아니라 IPv6 자체 실패
DNS 라우터 DNS의 A/AAAA 응답, DoH 사용 여부 AAAA가 없으면 IPv6 TProxy 규칙까지 도달하지 않음
Passwall UCI filter_proxy_ipv6, ipv6_tproxy, proxy mode DNS 필터와 트래픽 TProxy 의도를 따로 확인
firewall inet passwall, IPv6 chain, counter chain 부재와 packet 미도달을 구분
클라이언트 같은 목적지의 IPv4/IPv6 결과 주소 계열별 public egress와 오류 시각 비교
ubus call system board
opkg list-installed | grep -E 'passwall|firewall4|nftables|iptables'
uci -q show passwall | grep -E 'filter_proxy_ipv6|ipv6_tproxy|tcp_proxy_mode|udp_proxy_mode|dns_mode'
ip -6 addr show
ip -6 route show table all
logread -e passwall

출력에는 노드 주소와 자격 정보가 포함될 수 있으므로 원문 전체를 외부에 올리지 않습니다. uci -q show passwall 결과를 공유해야 한다면 server, UUID, password, token을 먼저 제거합니다.

DNS 필터와 IPv6 TProxy를 분리합니다

  • AAAA 응답을 막고 싶은 경우: filter_proxy_ipv6의 적용 대상은 Passwall이 관리하는 DNS 분기입니다. 브라우저나 애플리케이션이 자체 DoH를 사용하면 라우터 dnsmasq를 우회할 수 있으므로, 설정이 저장됐다는 사실만으로 AAAA가 사라진다고 단정하지 않습니다.
  • IPv6도 프록시하고 싶은 경우: ipv6_tproxy와 선택한 노드가 IPv6/TProxy를 지원해야 합니다. DNS가 AAAA를 주더라도 IPv6 proxy chain이 없으면 native IPv6 route로 직접 나갈 수 있습니다.
  • IPv6를 사용하지 않으려는 경우: Passwall 옵션 하나를 전체 LAN의 IPv6 차단 기능으로 취급하지 않습니다. OpenWrt의 WAN6, RA/DHCPv6, firewall 정책까지 의도를 일치시켜야 하며 ICMPv6를 통째로 막아 연결을 불안정하게 만들지 않습니다.
nslookup example.com 127.0.0.1
command -v nft
nft list tables
nft -a list table inet passwall
nft -a list chain inet passwall PSW_MANGLE_V6
nft -a list chain inet passwall PSW_OUTPUT_MANGLE_V6

inet passwall 자체가 없다면 먼저 Passwall 시작 실패와 선택된 firewall backend를 봅니다. table은 있는데 PSW_MANGLE_V6가 없다면 ipv6_tproxy, 노드 지원, 시작 로그를 확인합니다. chain과 rule이 있지만 counter가 늘지 않는다면 클라이언트 packet이 다른 interface, zone, 정책 경로를 타는지 확인합니다.

fw4 환경에서 ip6tables만 보면 놓칠 수 있습니다

최근 OpenWrt의 기본 firewall은 fw4/nftables 계열입니다. 호환 패키지에 따라 ip6tables 명령이 보이더라도 실제 Passwall rule이 inet passwall에 생성됐다면 ip6tables -L 출력만으로 rule 부재를 결론 내리면 안 됩니다. 먼저 패키지와 실제 table을 확인합니다.

반대로 오래된 빌드나 legacy backend에서는 Passwall의 iptables.sh 경로가 사용될 수 있습니다. 인터넷에서 찾은 nft 명령을 무조건 적용하지 말고 현재 설치 버전, firewall4 패키지, 시작 로그가 가리키는 backend를 기준으로 도구를 선택합니다.

안전한 수정 순서

  1. 현재 설정과 rule을 먼저 보관합니다. 재부팅 뒤에도 필요하면 /tmp가 아닌 관리 PC로 복사합니다.
  2. 목표를 하나 정합니다. 프록시 대상 AAAA를 억제할지, IPv6 트래픽을 TProxy로 보낼지, LAN에서 IPv6 자체를 사용하지 않을지는 서로 다른 변경입니다.
  3. LuCI에서 관련 옵션 하나만 바꾸고 Apply 전에 이전 값을 기록합니다. UCI를 직접 수정할 경우 정확한 section을 먼저 출력해 추측으로 index를 넣지 않습니다.
  4. Passwall만 재시작하고 최초 오류를 확인합니다. firewall 전체를 flush하거나 임의 rule을 영구 추가하지 않습니다.
  5. DNS 응답, IPv6 route, inet passwall counter, 클라이언트 IPv4/IPv6 요청을 같은 순서로 다시 측정합니다.
cp /etc/config/passwall /tmp/passwall.before
nft list ruleset > /tmp/nft.before
ip -6 route show table all > /tmp/route6.before
/etc/init.d/passwall restart
logread -e passwall
nft -a list table inet passwall

클라이언트에서 우회와 실패를 구분합니다

nslookup example.com <ROUTER_LAN_IP>
curl -4 -I https://example.com/
curl -6 -I https://example.com/
ip -6 route get 2001:4860:4860::8888

curl -6이 실패하고 IPv6 기본 route도 없다면 Passwall 우회 증거가 아닙니다. 반대로 curl -4와 curl -6이 모두 성공하지만 public egress 또는 정책 결과가 다르면 주소 계열별 경로를 비교할 근거가 생깁니다. 테스트 대상이 CDN이면 응답 IP만 다를 수 있으므로 source egress, Passwall counter, 로그를 함께 봅니다.

검증과 롤백

수정 성공 기준은 LuCI 토글이 저장된 상태가 아니라 DNS 응답, inet passwall IPv6 counter, 실제 클라이언트 경로가 같은 의도로 맞는 상태입니다. 재부팅 후에도 table과 route가 다시 만들어지는지 확인합니다.

문제가 넓어지면 저장한 파일을 복원하고 Passwall을 재시작합니다. 수동으로 추가한 nft rule은 service 재시작 때 사라질 수 있으므로 해결책으로 남기지 않습니다. 복원 뒤에도 IPv6가 실패하면 Passwall 밖의 WAN6, RA/DHCPv6, fw4 zone 문제를 별도 장애로 분리합니다.

cp /tmp/passwall.before /etc/config/passwall
/etc/init.d/passwall restart
logread -e passwall
nft -a list table inet passwall

공식 코드 및 확인 기준