RKNnoVPN/module/scripts/dns.sh
loop-uh 76ffdb1966 Initial commit: PrivStack v1.0.0
VPN-invisible transparent proxy for rooted Android.
No VPN, no TUN, no icon — uses tproxy+iptables at kernel level.

Components:
- Magisk module: shell scripts + Go daemon (privd/privctl)
- Android APK: Kotlin + Jetpack Compose controller (no INTERNET permission)
- CI/CD: GitHub Actions auto-build + release

Architecture verified against RKNHardering → NOT_DETECTED.
Based on 31 research agents + 10 security audits + 10 verification passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:32:59 +03:00

173 lines
7.3 KiB
Shell

#!/system/bin/sh
# ============================================================================
# dns.sh — DNS interception for PrivStack
# ============================================================================
# Transparently hijacks DNS traffic so the proxy core resolves everything.
#
# Two interception layers:
# 1. Port 53 (classic DNS) — redirected via iptables nat REDIRECT
# 2. DoT (853) / DoH (443) — intercepted via mangle TPROXY
#
# IMPORTANT: We never touch Settings.Global (Private DNS). Apps can detect
# that change and refuse to work. Instead we intercept at the
# network layer, which is invisible to userspace.
#
# Environment (set by privd before calling this script):
# DNS_PORT — sing-box DNS listener port (default 10856)
# CORE_GID — GID used by the proxy core (default 23333)
# DNS_MODE — "uid" = per-UID interception; "all" = everything
# APP_UIDS — space-separated list of UIDs to intercept (uid mode)
# PRIVSTACK_DIR — base directory, e.g. /data/adb/privstack
# ============================================================================
# Fail on first error; treat unset variables as errors.
set -eu
TAG="privstack:dns"
# Sane defaults if the caller omitted something.
DNS_PORT="${DNS_PORT:-10856}"
CORE_GID="${CORE_GID:-23333}"
DNS_MODE="${DNS_MODE:-uid}"
APP_UIDS="${APP_UIDS:-}"
PRIVSTACK_DIR="${PRIVSTACK_DIR:-/data/adb/privstack}"
# Chain names — keep in sync with the rest of PrivStack.
NAT4_CHAIN="PRIVSTACK_DNS_NAT"
NAT6_CHAIN="PRIVSTACK_DNS_NAT6"
MAN4_CHAIN="PRIVSTACK_DNS_MAN"
MAN6_CHAIN="PRIVSTACK_DNS_MAN6"
log() { /system/bin/log -t "$TAG" -p i "$*"; }
# ── helpers ─────────────────────────────────────────────────────────────────
create_chains() {
# IPv4
iptables -t nat -N "$NAT4_CHAIN" 2>/dev/null || iptables -t nat -F "$NAT4_CHAIN"
iptables -t mangle -N "$MAN4_CHAIN" 2>/dev/null || iptables -t mangle -F "$MAN4_CHAIN"
# IPv6
ip6tables -t nat -N "$NAT6_CHAIN" 2>/dev/null || ip6tables -t nat -F "$NAT6_CHAIN"
ip6tables -t mangle -N "$MAN6_CHAIN" 2>/dev/null || ip6tables -t mangle -F "$MAN6_CHAIN"
}
# Populate a single nat chain ($1=iptables/ip6tables, $2=chain).
fill_nat_chain() {
_ipt="$1"; _chain="$2"
# Always skip the proxy core's own traffic to avoid loops.
$_ipt -t nat -A "$_chain" -m owner --gid-owner "$CORE_GID" -j RETURN
if [ "$DNS_MODE" = "all" ]; then
# Redirect every UDP/TCP 53 packet to the core's DNS listener.
$_ipt -t nat -A "$_chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT"
$_ipt -t nat -A "$_chain" -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT"
else
# Per-UID: only redirect listed UIDs.
for uid in $APP_UIDS; do
$_ipt -t nat -A "$_chain" -p udp --dport 53 -m owner --uid-owner "$uid" -j REDIRECT --to-ports "$DNS_PORT"
$_ipt -t nat -A "$_chain" -p tcp --dport 53 -m owner --uid-owner "$uid" -j REDIRECT --to-ports "$DNS_PORT"
done
fi
}
# Populate a single mangle chain ($1=iptables/ip6tables, $2=chain).
# TPROXY intercepts DoT (853) and DoH (443) so encrypted DNS goes through
# the proxy core as well.
fill_mangle_chain() {
_ipt="$1"; _chain="$2"
# Skip core's own traffic.
$_ipt -t mangle -A "$_chain" -m owner --gid-owner "$CORE_GID" -j RETURN
if [ "$DNS_MODE" = "all" ]; then
# DoT — TCP 853
$_ipt -t mangle -A "$_chain" -p tcp --dport 853 \
-j TPROXY --on-port "$DNS_PORT" --tproxy-mark 0x2023
# DoH — TCP 443 (only well-known resolvers; full 443 would break the web)
# We intentionally do NOT blanket-TPROXY 443 here. The core's sniff +
# override_destination handles DoH domains matched by routing rules.
# This chain is a safety net for stubborn apps that hard-code resolver IPs.
$_ipt -t mangle -A "$_chain" -p udp --dport 443 \
-j TPROXY --on-port "$DNS_PORT" --tproxy-mark 0x2023
else
for uid in $APP_UIDS; do
$_ipt -t mangle -A "$_chain" -p tcp --dport 853 \
-m owner --uid-owner "$uid" \
-j TPROXY --on-port "$DNS_PORT" --tproxy-mark 0x2023
$_ipt -t mangle -A "$_chain" -p udp --dport 443 \
-m owner --uid-owner "$uid" \
-j TPROXY --on-port "$DNS_PORT" --tproxy-mark 0x2023
done
fi
}
# Hook our chains into OUTPUT so locally-generated traffic is caught.
hook_chains() {
# nat OUTPUT — classic DNS redirect
iptables -t nat -C OUTPUT -j "$NAT4_CHAIN" 2>/dev/null || \
iptables -t nat -A OUTPUT -j "$NAT4_CHAIN"
ip6tables -t nat -C OUTPUT -j "$NAT6_CHAIN" 2>/dev/null || \
ip6tables -t nat -A OUTPUT -j "$NAT6_CHAIN"
# mangle OUTPUT — TPROXY for DoT/DoH
iptables -t mangle -C OUTPUT -j "$MAN4_CHAIN" 2>/dev/null || \
iptables -t mangle -A OUTPUT -j "$MAN4_CHAIN"
ip6tables -t mangle -C OUTPUT -j "$MAN6_CHAIN" 2>/dev/null || \
ip6tables -t mangle -A OUTPUT -j "$MAN6_CHAIN"
}
# ── start ───────────────────────────────────────────────────────────────────
start() {
log "starting DNS interception (mode=$DNS_MODE, port=$DNS_PORT)"
create_chains
# Fill nat chains (port 53 redirect).
fill_nat_chain iptables "$NAT4_CHAIN"
fill_nat_chain ip6tables "$NAT6_CHAIN"
# Fill mangle chains (DoT/DoH TPROXY).
fill_mangle_chain iptables "$MAN4_CHAIN"
fill_mangle_chain ip6tables "$MAN6_CHAIN"
# Attach to OUTPUT.
hook_chains
log "DNS interception started"
}
# ── stop ────────────────────────────────────────────────────────────────────
stop() {
log "stopping DNS interception"
# Unhook from OUTPUT (ignore errors if already removed).
iptables -t nat -D OUTPUT -j "$NAT4_CHAIN" 2>/dev/null || true
ip6tables -t nat -D OUTPUT -j "$NAT6_CHAIN" 2>/dev/null || true
iptables -t mangle -D OUTPUT -j "$MAN4_CHAIN" 2>/dev/null || true
ip6tables -t mangle -D OUTPUT -j "$MAN6_CHAIN" 2>/dev/null || true
# Flush and delete chains.
iptables -t nat -F "$NAT4_CHAIN" 2>/dev/null || true
iptables -t nat -X "$NAT4_CHAIN" 2>/dev/null || true
ip6tables -t nat -F "$NAT6_CHAIN" 2>/dev/null || true
ip6tables -t nat -X "$NAT6_CHAIN" 2>/dev/null || true
iptables -t mangle -F "$MAN4_CHAIN" 2>/dev/null || true
iptables -t mangle -X "$MAN4_CHAIN" 2>/dev/null || true
ip6tables -t mangle -F "$MAN6_CHAIN" 2>/dev/null || true
ip6tables -t mangle -X "$MAN6_CHAIN" 2>/dev/null || true
# NOTE: We never touched Private DNS, so there is nothing to restore.
log "DNS interception stopped"
}
# ── main dispatch ───────────────────────────────────────────────────────────
case "${1:-}" in
start) start ;;
stop) stop ;;
*) echo "Usage: $0 {start|stop}" >&2; exit 1 ;;
esac