Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openwrt/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ define Package/ua2f
+(PACKAGE_nftables-json||PACKAGE_nftables-nojson):kmod-nft-tproxy \
+PACKAGE_firewall:iptables-mod-conntrack-extra \
+PACKAGE_firewall:iptables-mod-filter \
+PACKAGE_firewall:iptables-mod-u32 \
+PACKAGE_firewall:iptables-mod-nfqueue \
+PACKAGE_firewall:iptables-mod-tproxy
endef
Expand Down Expand Up @@ -91,6 +92,8 @@ define Package/ua2f/install
$(INSTALL_DIR) $(1)/etc/config $(1)/etc/init.d
$(INSTALL_BIN) ./files/ua2f.config $(1)/etc/config/ua2f
$(INSTALL_BIN) ./files/ua2f.init $(1)/etc/init.d/ua2f
$(INSTALL_DIR) $(1)/usr/share/ua2f
$(INSTALL_DATA) ./files/ua2f.firewall $(1)/usr/share/ua2f/firewall.sh

$(if $(UA2F_COVERAGE_BUILD),rm -rf $(UA2F_COVERAGE_DIR))
$(if $(UA2F_COVERAGE_BUILD),$(INSTALL_DIR) $(UA2F_COVERAGE_DIR)/objects/CMakeFiles/ua2f.dir)
Expand Down
109 changes: 109 additions & 0 deletions openwrt/files/ua2f.firewall
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/bin/sh
# Pure rule generators: sourcing this file does not read or change the firewall.
# Experimental empty-ACK bypass. Keep SYN/FIN/RST queued, never change a mark,
# and leave IP options, fragments and IPv6 extension headers on the slow path.
# Doff is in 32-bit words. Enumerating it avoids dynamic-length arithmetic and
# nft >= 1.0.3 integer-concatenation requirements on older OpenWrt installations.

# Print one complete u32 expression for each legal TCP header length.
# The caller must select the IP family and TCP ORIGINAL direction externally.
ua2f_empty_ack_u32() {
case "${1:-}" in
4|6) ;;
*) return 1 ;;
esac
for ua2f_doff in 5 6 7 8 9 10 11 12 13 14 15; do
if [ "$1" = 4 ]; then
printf '%s\n' "0&0xFFFF=$((20 + 4 * ua2f_doff)) && 0>>24&0xF=5 && 4&0x3FFF=0 && 6&0xFF=6 && 32>>28=$ua2f_doff && 33>>24&0x17=0x10 && $((16 + 4 * ua2f_doff))&0=0"
else
printf '%s\n' "4>>16=$((4 * ua2f_doff)) && 4>>8&0xFF=6 && 52>>28=$ua2f_doff && 53>>24&0x17=0x10 && $((36 + 4 * ua2f_doff))&0=0"
fi
done
}

# Print rules for the existing inet postrouting base chain, immediately before
# its queue statement. Exact skb length also excludes truncated/GRO oddities.
ua2f_empty_ack_nft() {
for ua2f_doff in 5 6 7 8 9 10 11 12 13 14 15; do
printf '%s\n' "meta length $((20 + 4 * ua2f_doff)) ct direction original ip protocol tcp ip hdrlength 5 ip frag-off & 0x3fff == 0 tcp flags & 0x17 == 0x10 tcp doff $ua2f_doff ip length $((20 + 4 * ua2f_doff)) counter return comment \"!ua2f: empty ACK\";"
printf '%s\n' "meta length $((40 + 4 * ua2f_doff)) ct direction original ip6 nexthdr tcp tcp flags & 0x17 == 0x10 tcp doff $ua2f_doff ip6 length $((4 * ua2f_doff)) counter return comment \"!ua2f: empty ACK\";"
done
}

# Explicit command prefix, e.g. 4 iptables -t mangle -A ua2f. Never uses eval.
# No calls occur unless the init script's opt-in config enables this function.
ua2f_add_empty_ack_iptables() {
[ "$#" -ge 2 ] || return 1
ua2f_family="$1"
shift
ua2f_rules="$(ua2f_empty_ack_u32 "$ua2f_family")" || return 1
while IFS= read -r ua2f_expr; do
"$@" -p tcp -m conntrack --ctdir ORIGINAL -m u32 --u32 "$ua2f_expr" -j RETURN || return 1
done <<EOF_RULES
$ua2f_rules
EOF_RULES
}

# Every precise bypass predicate implies this coarse range. Its complement may
# therefore take the SAME NFQUEUE action without inspecting the eleven doffs.
# Even a truncated u32 read cannot bypass anything: negation can only QUEUE it.
ua2f_empty_ack_length_u32() {
case "${1:-}" in
4) printf '%s\n' '0&0xFFFF=40:80' ;;
6) printf '%s\n' '4>>16=20:60' ;;
*) return 1 ;;
esac
}

# Pure complete candidate-tail generator: one rule per line, TAB-separated argv.
# Consumers must split on TAB, not whitespace, so each u32 expression stays one
# argument. No shell evaluation is needed. Family, first queue and last queue
# are explicit; equal endpoints select --queue-num, otherwise --queue-balance.
# Install AFTER all existing bypass/mark rules. The only skipped work is failed,
# side-effect-free empty-ACK matching; neither queue changes the connection mark.
ua2f_empty_ack_queue_iptables() {
[ "$#" -eq 3 ] || return 1
ua2f_length="$(ua2f_empty_ack_length_u32 "$1")" || return 1
case "$2" in ""|*[!0-9]*) return 1 ;; esac
case "$3" in ""|*[!0-9]*) return 1 ;; esac
[ "$2" -le 65535 ] && [ "$3" -le 65535 ] && [ "$2" -le "$3" ] || return 1
if [ "$2" -eq "$3" ]; then
ua2f_queue="$(printf '%s\t%s\t%s\t%s\t%s' -j NFQUEUE --queue-num "$2" --queue-bypass)"
else
ua2f_queue="$(printf '%s\t%s\t%s\t%s\t%s' -j NFQUEUE --queue-balance "$2:$3" --queue-bypass)"
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' -m u32 '!' --u32 "$ua2f_length" "$ua2f_queue"
ua2f_rules="$(ua2f_empty_ack_u32 "$1")" || return 1
while IFS= read -r ua2f_expr; do
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
-p tcp -m conntrack --ctdir ORIGINAL -m u32 --u32 "$ua2f_expr" "$(printf '%s\t%s' -j RETURN)"
done <<EOF_RULES
$ua2f_rules
EOF_RULES
printf '%s\n' "$ua2f_queue"
}

# Split the pure generator's TSV in a subshell, preserving the caller's IFS and
# glob settings. Only the explicit command prefix is executed, never eval.
ua2f_append_iptables_argv() (
ua2f_argv="$1"
shift
IFS="$(printf '\t')"
set -f
# Intentional field splitting of generated TAB-separated arguments.
# shellcheck disable=SC2086
"$@" $ua2f_argv
)

# Install exactly the generated tail, including its final fallback. If any rule
# fails, the caller MUST append an unconditional NFQUEUE fallback as before.
ua2f_add_empty_ack_queue_iptables() {
[ "$#" -ge 4 ] || return 1
ua2f_tail="$(ua2f_empty_ack_queue_iptables "$1" "$2" "$3")" || return 1
shift 3
while IFS= read -r ua2f_rule; do
ua2f_append_iptables_argv "$ua2f_rule" "$@" || return 1
done <<EOF_RULES
$ua2f_tail
EOF_RULES
}
31 changes: 22 additions & 9 deletions openwrt/files/ua2f.init
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,12 @@ setup_firewall() {
return 1
fi

local handle_tls handle_intranet handle_mmtls disable_connmark mode listen_port nfqueue_workers
local handle_tls handle_intranet handle_mmtls disable_connmark mode listen_port nfqueue_workers bypass_empty_ack
config_get_bool handle_tls "firewall" "handle_tls" "0"
config_get_bool handle_intranet "firewall" "handle_intranet" "0"
config_get_bool handle_mmtls "firewall" "handle_mmtls" "0"
# Source-only experimental candidate; disabled until packet-path validation.
config_get_bool bypass_empty_ack "firewall" "bypass_empty_ack" "0"
config_get_bool disable_connmark "main" "disable_connmark" "0"
config_get mode "main" "mode" "NFQUEUE"
config_get listen_port "main" "listen_port" "10010"
Expand All @@ -379,6 +381,10 @@ setup_firewall() {
;;
esac

if [ "$bypass_empty_ack" -eq "1" ]; then
. /usr/share/ua2f/firewall.sh || return 1
fi

local nfqueue_end nfqueue_expr
nfqueue_end=$((10010 + nfqueue_workers - 1))
nfqueue_expr="queue num 10010 bypass"
Expand Down Expand Up @@ -425,6 +431,7 @@ setup_firewall() {
$([ "$handle_tls" -eq "1" ] || echo 'tcp dport 443 counter return comment "!ua2f: bypass HTTPS";')
$([ "$disable_connmark" -eq "1" ] || echo 'tcp dport 80 counter ct mark set 44;')
$([ "$disable_connmark" -eq "1" ] || echo 'ct mark 43 counter return comment "!ua2f: bypass non-http stream";')
$([ "$bypass_empty_ack" -eq "0" ] || ua2f_empty_ack_nft)
meta l4proto tcp ct direction original counter $nfqueue_expr;
}
}
Expand All @@ -450,10 +457,13 @@ setup_firewall() {
[ "$disable_connmark" -eq "1" ] || $IPT_M -A ua2f -p tcp --dport 80 -j CONNMARK --set-mark 44
[ "$disable_connmark" -eq "1" ] || $IPT_M -A ua2f -m connmark --mark 43 -j RETURN # 不处理标记为非 http 的流
[ "$handle_mmtls" -eq "1" ] || $IPT_M -A ua2f -p tcp --dport 80 -m string --string "/mmtls/" --algo bm -j RETURN # 不处理微信的mmtls
if [ "$nfqueue_workers" -gt 1 ]; then
$IPT_M -A ua2f -j NFQUEUE --queue-balance "10010:$nfqueue_end" --queue-bypass
else
$IPT_M -A ua2f -j NFQUEUE --queue-num 10010 --queue-bypass
if [ "$bypass_empty_ack" -eq "0" ] || ! ua2f_add_empty_ack_queue_iptables 4 10010 "$nfqueue_end" iptables -t mangle -A ua2f; then
[ "$bypass_empty_ack" -eq "0" ] || echo "UA2F: empty-ACK IPv4 optimization unavailable; keeping NFQUEUE fallback" >&2
if [ "$nfqueue_workers" -gt 1 ]; then
$IPT_M -A ua2f -j NFQUEUE --queue-balance "10010:$nfqueue_end" --queue-bypass
else
$IPT_M -A ua2f -j NFQUEUE --queue-num 10010 --queue-bypass
fi
fi
fi
$IPT_M -A POSTROUTING -p tcp -m conntrack --ctdir ORIGINAL -j ua2f
Expand Down Expand Up @@ -484,10 +494,13 @@ setup_firewall() {
[ "$disable_connmark" -eq "1" ] || $IPT6_M -A ua2f -p tcp --dport 80 -j CONNMARK --set-mark 44
[ "$disable_connmark" -eq "1" ] || $IPT6_M -A ua2f -m connmark --mark 43 -j RETURN # 不处理标记为非 http 的流
[ "$handle_mmtls" -eq "1" ] || $IPT6_M -A ua2f -p tcp --dport 80 -m string --string "/mmtls/" --algo bm -j RETURN # 不处理微信的mmtls
if [ "$nfqueue_workers" -gt 1 ]; then
$IPT6_M -A ua2f -j NFQUEUE --queue-balance "10010:$nfqueue_end" --queue-bypass
else
$IPT6_M -A ua2f -j NFQUEUE --queue-num 10010 --queue-bypass
if [ "$bypass_empty_ack" -eq "0" ] || ! ua2f_add_empty_ack_queue_iptables 6 10010 "$nfqueue_end" ip6tables -t mangle -A ua2f; then
[ "$bypass_empty_ack" -eq "0" ] || echo "UA2F: empty-ACK IPv6 optimization unavailable; keeping NFQUEUE fallback" >&2
if [ "$nfqueue_workers" -gt 1 ]; then
$IPT6_M -A ua2f -j NFQUEUE --queue-balance "10010:$nfqueue_end" --queue-bypass
else
$IPT6_M -A ua2f -j NFQUEUE --queue-num 10010 --queue-bypass
fi
fi
fi
$IPT6_M -A POSTROUTING -p tcp -m conntrack --ctdir ORIGINAL -j ua2f
Expand Down
Loading
Loading