diff --git a/binaries/aarch64/nfqws b/binaries/aarch64/nfqws index 0a105dc..c6de601 100755 Binary files a/binaries/aarch64/nfqws and b/binaries/aarch64/nfqws differ diff --git a/binaries/aarch64/tpws b/binaries/aarch64/tpws index 1a97ef9..a4f5be0 100755 Binary files a/binaries/aarch64/tpws and b/binaries/aarch64/tpws differ diff --git a/binaries/arm/nfqws b/binaries/arm/nfqws index a927193..6c0015d 100755 Binary files a/binaries/arm/nfqws and b/binaries/arm/nfqws differ diff --git a/binaries/arm/tpws b/binaries/arm/tpws index 9f7aaa7..ffe7dab 100755 Binary files a/binaries/arm/tpws and b/binaries/arm/tpws differ diff --git a/binaries/freebsd-x64/dvtws b/binaries/freebsd-x64/dvtws index 3e29ff1..0568c3a 100755 Binary files a/binaries/freebsd-x64/dvtws and b/binaries/freebsd-x64/dvtws differ diff --git a/binaries/freebsd-x64/tpws b/binaries/freebsd-x64/tpws index d2d10a4..702da69 100755 Binary files a/binaries/freebsd-x64/tpws and b/binaries/freebsd-x64/tpws differ diff --git a/binaries/mac64/tpws b/binaries/mac64/tpws index 5cc97a8..ad905fa 100755 Binary files a/binaries/mac64/tpws and b/binaries/mac64/tpws differ diff --git a/binaries/mips32r1-lsb/nfqws b/binaries/mips32r1-lsb/nfqws index b2d3d39..614b64c 100755 Binary files a/binaries/mips32r1-lsb/nfqws and b/binaries/mips32r1-lsb/nfqws differ diff --git a/binaries/mips32r1-lsb/tpws b/binaries/mips32r1-lsb/tpws index a285a1b..dd8bfb7 100755 Binary files a/binaries/mips32r1-lsb/tpws and b/binaries/mips32r1-lsb/tpws differ diff --git a/binaries/mips32r1-msb/nfqws b/binaries/mips32r1-msb/nfqws index 6d74bb4..1d3f798 100755 Binary files a/binaries/mips32r1-msb/nfqws and b/binaries/mips32r1-msb/nfqws differ diff --git a/binaries/mips32r1-msb/tpws b/binaries/mips32r1-msb/tpws index e4bf302..f46bb45 100755 Binary files a/binaries/mips32r1-msb/tpws and b/binaries/mips32r1-msb/tpws differ diff --git a/binaries/mips64r2-msb/nfqws b/binaries/mips64r2-msb/nfqws index ba7933d..1d1e609 100755 Binary files a/binaries/mips64r2-msb/nfqws and b/binaries/mips64r2-msb/nfqws differ diff --git a/binaries/mips64r2-msb/tpws b/binaries/mips64r2-msb/tpws index ed019db..cf3a82e 100755 Binary files a/binaries/mips64r2-msb/tpws and b/binaries/mips64r2-msb/tpws differ diff --git a/binaries/ppc/nfqws b/binaries/ppc/nfqws index 7c95730..02e67fa 100755 Binary files a/binaries/ppc/nfqws and b/binaries/ppc/nfqws differ diff --git a/binaries/ppc/tpws b/binaries/ppc/tpws index ed4df99..d64d972 100755 Binary files a/binaries/ppc/tpws and b/binaries/ppc/tpws differ diff --git a/binaries/x86/nfqws b/binaries/x86/nfqws index 8884950..3275323 100755 Binary files a/binaries/x86/nfqws and b/binaries/x86/nfqws differ diff --git a/binaries/x86/tpws b/binaries/x86/tpws index 1e70467..8c81538 100755 Binary files a/binaries/x86/tpws and b/binaries/x86/tpws differ diff --git a/binaries/x86_64/nfqws b/binaries/x86_64/nfqws index 83e2f97..e2325fe 100755 Binary files a/binaries/x86_64/nfqws and b/binaries/x86_64/nfqws differ diff --git a/binaries/x86_64/tpws b/binaries/x86_64/tpws index cf94648..d03720f 100755 Binary files a/binaries/x86_64/tpws and b/binaries/x86_64/tpws differ diff --git a/binaries/x86_64/tpws_wsl.tgz b/binaries/x86_64/tpws_wsl.tgz index 378a9bb..a76f6a0 100644 Binary files a/binaries/x86_64/tpws_wsl.tgz and b/binaries/x86_64/tpws_wsl.tgz differ diff --git a/common/ipt.sh b/common/ipt.sh index 0976795..99a8861 100644 --- a/common/ipt.sh +++ b/common/ipt.sh @@ -89,6 +89,14 @@ filter_apply_ipset_target() filter_apply_ipset_target6 $2 } +reverse_nfqws_rule_stream() +{ + sed -e 's/-o /-i /g' -e 's/--dport /--sport /g' -e 's/--dports /--sports /g' -e 's/ dst$/ src/' -e 's/ dst / src /g' -e 's/--connbytes-dir=original/--connbytes-dir=reply/g' -e "s/-m mark ! --mark $DESYNC_MARK\/$DESYNC_MARK//g" +} +reverse_nfqws_rule() +{ + echo "$@" | reverse_nfqws_rule_stream +} prepare_tpws_fw4() { @@ -250,14 +258,81 @@ fw_nfqws_post() fw_nfqws_post6 $1 "$3" $4 } +_fw_nfqws_pre4() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - queue number + # $4 - wan interface names space separated + [ "$DISABLE_IPV4" = "1" -o -z "$2" ] || { + local i + + ipt_print_op $1 "$2" "nfqws input+forward (qnum $3)" + + rule="$2 $IPSET_EXCLUDE src -j NFQUEUE --queue-num $3 --queue-bypass" + if [ -n "$4" ] ; then + for i in $4; do + # iptables PREROUTING chain is before NAT. not possible to have DNATed ip's there + ipt_add_del $1 INPUT -t mangle -i $i $rule + ipt_add_del $1 FORWARD -t mangle -i $i $rule + done + else + ipt_add_del $1 INPUT -t mangle $rule + ipt_add_del $1 FORWARD -t mangle $rule + fi + } +} +_fw_nfqws_pre6() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv6 + # $3 - queue number + # $4 - wan interface names space separated + [ "$DISABLE_IPV6" = "1" -o -z "$2" ] || { + local i + + ipt_print_op $1 "$2" "nfqws input+forward (qnum $3)" 6 + + rule="$2 $IPSET_EXCLUDE6 src -j NFQUEUE --queue-num $3 --queue-bypass" + if [ -n "$4" ] ; then + for i in $4; do + # iptables PREROUTING chain is before NAT. not possible to have DNATed ip's there + ipt6_add_del $1 INPUT -t mangle -i $i $rule + ipt6_add_del $1 FORWARD -t mangle -i $i $rule + done + else + ipt6_add_del $1 INPUT -t mangle $rule + ipt6_add_del $1 FORWARD -t mangle $rule + fi + } +} +fw_nfqws_pre() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - iptable filter for ipv6 + # $4 - queue number + fw_nfqws_pre4 $1 "$2" $4 + fw_nfqws_pre6 $1 "$3" $4 +} + zapret_do_firewall_rules_ipt() { local mode="${MODE_OVERRIDE:-$MODE}" - local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 1:4" + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 1:" local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" - local f4 f6 qn qns qn6 qns6 + local n f4 f6 ff qn qns qn6 qns6 + + # autohostlist mode requires incoming traffic sample + # always use conntrack packet limiter or nfqws will deal with gigabytes + if [ "$MODE_FILTER" = "autohostlist" ]; then + n=$((4+${AUTOHOSTLIST_RETRANS_THRESHOLD:-3})) + else + n=4 + fi + first_packet_only="${first_packet_only}$n" case "$mode" in tpws) @@ -279,17 +354,23 @@ zapret_do_firewall_rules_ipt() f4="$f4 $first_packet_only" filter_apply_ipset_target4 f4 fw_nfqws_post4 $1 "$f4 $desync" $qn + [ "$MODE_FILTER" = "autohostlist" ] && fw_nfqws_pre4 $1 "$(reverse_nfqws_rule $f4)" $qn else if [ -n "$qn" ]; then f4="-p tcp --dport 80" + ff="$f4" [ "$MODE_HTTP_KEEPALIVE" = "1" ] || f4="$f4 $first_packet_only" + ff="$ff $first_packet_only" filter_apply_ipset_target4 f4 + filter_apply_ipset_target4 ff fw_nfqws_post4 $1 "$f4 $desync" $qn + [ "$MODE_FILTER" = "autohostlist" ] && fw_nfqws_pre4 $1 "$(reverse_nfqws_rule $ff)" $qn fi if [ -n "$qns" ]; then f4="-p tcp --dport 443 $first_packet_only" filter_apply_ipset_target4 f4 fw_nfqws_post4 $1 "$f4 $desync" $qns + [ "$MODE_FILTER" = "autohostlist" ] && fw_nfqws_pre4 $1 "$(reverse_nfqws_rule $f4)" $qns fi fi if [ "$MODE_HTTP_KEEPALIVE" != "1" ] && [ -n "$qn6" ] && [ "$qn6" = "$qns6" ]; then @@ -297,17 +378,23 @@ zapret_do_firewall_rules_ipt() f6="$f6 $first_packet_only" filter_apply_ipset_target6 f6 fw_nfqws_post6 $1 "$f6 $desync" $qn6 + [ "$MODE_FILTER" = "autohostlist" ] && fw_nfqws_pre6 $1 "$(reverse_nfqws_rule $f6)" $qn else if [ -n "$qn6" ]; then f6="-p tcp --dport 80" + ff="$f6" [ "$MODE_HTTP_KEEPALIVE" = "1" ] || f6="$f6 $first_packet_only" + ff="$ff $first_packet_only" filter_apply_ipset_target6 f6 + filter_apply_ipset_target6 ff fw_nfqws_post6 $1 "$f6 $desync" $qn6 + [ "$MODE_FILTER" = "autohostlist" ] && fw_nfqws_pre6 $1 "$(reverse_nfqws_rule $ff)" $qn6 fi if [ -n "$qns6" ]; then f6="-p tcp --dport 443 $first_packet_only" filter_apply_ipset_target6 f6 fw_nfqws_post6 $1 "$f6 $desync" $qns6 + [ "$MODE_FILTER" = "autohostlist" ] && fw_nfqws_pre6 $1 "$(reverse_nfqws_rule $f6)" $qns6 fi fi diff --git a/common/linux_fw.sh b/common/linux_fw.sh index 607bb99..3b428c9 100644 --- a/common/linux_fw.sh +++ b/common/linux_fw.sh @@ -1,3 +1,7 @@ +set_conntrack_liberal_mode() +{ + [ -n "$SKIP_CONNTRACK_LIBERAL_MODE" ] || sysctl -w net.netfilter.nf_conntrack_tcp_be_liberal=$1 +} zapret_do_firewall() { linux_fwtype @@ -14,6 +18,13 @@ zapret_do_firewall() ;; esac + # russian DPI sends RST,ACK with wrong ACK. + # this is sometimes treated by conntrack as invalid and connbytes fw rules do not pass RST packet to nfqws. + # swith on liberal mode on zapret firewall start and switch off on zapret firewall stop + # this is only required for processing incoming bad RSTs. incoming rules are only applied in autohostlist mode + # calling this after firewall because conntrack module can be not loaded before applying conntrack firewall rules + [ "$MODE_FILTER" = "autohostlist" -a "$MODE" != tpws-socks ] && set_conntrack_liberal_mode $1 + [ "$1" = 1 -a -n "$INIT_FW_POST_UP_HOOK" ] && $INIT_FW_POST_UP_HOOK [ "$1" = 0 -a -n "$INIT_FW_POST_DOWN_HOOK" ] && $INIT_FW_POST_DOWN_HOOK diff --git a/common/list.sh b/common/list.sh index f8d7c4e..50363ff 100644 --- a/common/list.sh +++ b/common/list.sh @@ -13,13 +13,15 @@ find_hostlists() HOSTLIST_EXCLUDE="$HOSTLIST_BASE/zapret-hosts-user-exclude.txt.gz" [ -f "$HOSTLIST_EXCLUDE" ] || HOSTLIST_EXCLUDE="$HOSTLIST_BASE/zapret-hosts-user-exclude.txt" [ -f "$HOSTLIST_EXCLUDE" ] || HOSTLIST_EXCLUDE= + + HOSTLIST_AUTO="$HOSTLIST_BASE/zapret-hosts-auto.txt" } filter_apply_hostlist_target() { # $1 - var name of tpws or nfqws params - [ "$MODE_FILTER" = "hostlist" ] || return + [ "$MODE_FILTER" = "hostlist" -o "$MODE_FILTER" = "autohostlist" ] || return local HOSTLIST_BASE HOSTLIST HOSTLIST_USER HOSTLIST_EXCLUDE @@ -28,4 +30,11 @@ filter_apply_hostlist_target() [ -n "$HOSTLIST" ] && eval $1="\"\$$1 --hostlist=$HOSTLIST\"" [ -n "$HOSTLIST_USER" ] && eval $1="\"\$$1 --hostlist=$HOSTLIST_USER\"" [ -n "$HOSTLIST_EXCLUDE" ] && eval $1="\"\$$1 --hostlist-exclude=$HOSTLIST_EXCLUDE\"" + [ "$MODE_FILTER" = "autohostlist" ] && { + local parm1="${AUTOHOSTLIST_FAIL_THRESHOLD:+--hostlist-auto-fail-threshold=$AUTOHOSTLIST_FAIL_THRESHOLD}" + local parm2="${AUTOHOSTLIST_FAIL_TIME:+--hostlist-auto-fail-time=$AUTOHOSTLIST_FAIL_TIME}" + local parm3 + [ "$MODE" = "tpws" -o "$MODE" = "tpws-socks" ] || parm3="${AUTOHOSTLIST_RETRANS_THRESHOLD:+--hostlist-auto-retrans-threshold=$AUTOHOSTLIST_RETRANS_THRESHOLD}" + eval $1="\"\$$1 --hostlist-auto=$HOSTLIST_AUTO $parm1 $parm2 $parm3\"" + } } diff --git a/common/nft.sh b/common/nft.sh index f5df02b..d0cc35e 100644 --- a/common/nft.sh +++ b/common/nft.sh @@ -63,10 +63,8 @@ nft_del_all_chains_from_table() nft_create_chains() { - # NOTE : postrouting hook has priority 101 to hook packets already processed by nat - # NOTE : this is the only way to implement ipfrag for forwarded packets if nat is present - # NOTE : to avoid retracking and sport changing use notrack for nfqws generated packets - # NOTE : this also prevents defragmentation of ipv6 fragmented frames in kernels 4.16+ + # NOTE : postrouting hook has priority 99 to hook packets with original source but NATed destination + # NOTE : prerouting hook has priority -99 for the same reason cat << EOF | nft -f - add chain inet $ZAPRET_NFT_TABLE dnat_output { type nat hook output priority -101; } flush chain inet $ZAPRET_NFT_TABLE dnat_output @@ -84,6 +82,7 @@ cat << EOF | nft -f - add rule inet $ZAPRET_NFT_TABLE localnet_protect ip daddr 127.0.0.0/8 drop comment "route_localnet remote access protection" add rule inet $ZAPRET_NFT_TABLE input iif != lo jump localnet_protect add chain inet $ZAPRET_NFT_TABLE postrouting { type filter hook postrouting priority 99; } + add chain inet $ZAPRET_NFT_TABLE prerouting { type filter hook prerouting priority -99; } flush chain inet $ZAPRET_NFT_TABLE postrouting add set inet $ZAPRET_NFT_TABLE lanif { type ifname; } add set inet $ZAPRET_NFT_TABLE wanif { type ifname; } @@ -109,6 +108,7 @@ cat << EOF | nft -f - 2>/dev/null delete chain inet $ZAPRET_NFT_TABLE forward delete chain inet $ZAPRET_NFT_TABLE input delete chain inet $ZAPRET_NFT_TABLE postrouting + delete chain inet $ZAPRET_NFT_TABLE prerouting delete chain inet $ZAPRET_NFT_TABLE flow_offload delete chain inet $ZAPRET_NFT_TABLE localnet_protect EOF @@ -202,7 +202,7 @@ nft_reverse_nfqws_rule() } nft_clean_nfqws_rule() { - echo "$@" | sed -e "s/mark and $DESYNC_MARK == 0//g" + echo "$@" | sed -e "s/mark and $DESYNC_MARK == 0//g" -e "s/oifname @wanif6//g" -e "s/oifname @wanif//g" } nft_add_nfqws_flow_exempt_rule() { @@ -464,6 +464,64 @@ nft_fw_nfqws_post() nft_fw_nfqws_post6 "$2" $3 } +_nft_fw_nfqws_pre4() +{ + # $1 - filter ipv4 + # $2 - queue number + # $3 - not-empty if wan interface filtering required + + [ "$DISABLE_IPV4" = "1" -o -z "$1" ] || { + local filter="$1" port="$2" rule + nft_print_op "$filter" "nfqws prerouting (qnum $port)" 4 + rule="${3:+iifname @wanif }$filter ip saddr != @nozapret" + nft_add_rule prerouting $rule queue num $port bypass + } +} +_nft_fw_nfqws_pre6() +{ + # $1 - filter ipv6 + # $2 - queue number + # $3 - not-empty if wan interface filtering required + + [ "$DISABLE_IPV6" = "1" -o -z "$1" ] || { + local filter="$1" port="$2" rule + nft_print_op "$filter" "nfqws prerouting (qnum $port)" 6 + rule="${3:+iifname @wanif6 }$filter ip6 saddr != @nozapret6" + nft_add_rule prerouting $rule queue num $port bypass + } +} +nft_fw_nfqws_pre() +{ + # $1 - filter ipv4 + # $2 - filter ipv6 + # $3 - queue number + + nft_fw_nfqws_pre4 "$1" $3 + nft_fw_nfqws_pre6 "$2" $3 +} + +nft_fw_nfqws_both4() +{ + # $1 - filter ipv4 + # $2 - queue number + nft_fw_nfqws_post4 "$@" + nft_fw_nfqws_pre4 "$(nft_reverse_nfqws_rule $1)" $2 +} +nft_fw_nfqws_both6() +{ + # $1 - filter ipv6 + # $2 - queue number + nft_fw_nfqws_post6 "$@" + nft_fw_nfqws_pre6 "$(nft_reverse_nfqws_rule $1)" $2 +} +nft_fw_nfqws_both() +{ + # $1 - filter ipv4 + # $2 - filter ipv6 + # $3 - queue number + nft_fw_nfqws_both4 "$1" "$3" + nft_fw_nfqws_both6 "$2" "$3" +} zapret_reload_ifsets() { @@ -485,9 +543,18 @@ zapret_apply_firewall_rules_nft() { local mode="${MODE_OVERRIDE:-$MODE}" - local first_packet_only="ct original packets 1-4" + local first_packet_only local desync="mark and $DESYNC_MARK == 0" - local f4 f6 qn qns qn6 qns6 + local f4 f6 ff qn qns qn6 qns6 + + # autohostlist mode requires incoming traffic sample + # always use conntrack packet limiter or nfqws will deal with gigabytes + if [ "$MODE_FILTER" = "autohostlist" ]; then + first_packet_only=$((4+${AUTOHOSTLIST_RETRANS_THRESHOLD:-3})) + else + first_packet_only=4 + fi + first_packet_only="ct original packets 1-$first_packet_only" case "$mode" in tpws) @@ -508,17 +575,23 @@ zapret_apply_firewall_rules_nft() f4="$f4 $first_packet_only" nft_filter_apply_ipset_target4 f4 nft_fw_nfqws_post4 "$f4 $desync" $qn + [ "$MODE_FILTER" = "autohostlist" ] && nft_fw_nfqws_pre4 "$(nft_reverse_nfqws_rule $f4)" $qn else if [ -n "$qn" ]; then f4="tcp dport 80" + ff="$f4" [ "$MODE_HTTP_KEEPALIVE" = "1" ] || f4="$f4 $first_packet_only" + ff="$ff $first_packet_only" nft_filter_apply_ipset_target4 f4 + nft_filter_apply_ipset_target4 ff nft_fw_nfqws_post4 "$f4 $desync" $qn + [ "$MODE_FILTER" = "autohostlist" ] && nft_fw_nfqws_pre4 "$(nft_reverse_nfqws_rule $ff)" $qn fi if [ -n "$qns" ]; then f4="tcp dport 443 $first_packet_only" nft_filter_apply_ipset_target4 f4 nft_fw_nfqws_post4 "$f4 $desync" $qns + [ "$MODE_FILTER" = "autohostlist" ] && nft_fw_nfqws_pre4 "$(nft_reverse_nfqws_rule $f4)" $qns fi fi if [ "$MODE_HTTP_KEEPALIVE" != "1" ] && [ -n "$qn6" ] && [ "$qn6" = "$qns6" ]; then @@ -526,17 +599,23 @@ zapret_apply_firewall_rules_nft() f6="$f6 $first_packet_only" nft_filter_apply_ipset_target6 f6 nft_fw_nfqws_post6 "$f6 $desync" $qn6 + [ "$MODE_FILTER" = "autohostlist" ] && nft_fw_nfqws_pre6 "$(nft_reverse_nfqws_rule $f6)" $qn else if [ -n "$qn6" ]; then f6="tcp dport 80" + ff="$f6" [ "$MODE_HTTP_KEEPALIVE" = "1" ] || f6="$f6 $first_packet_only" + ff="$ff $first_packet_only" nft_filter_apply_ipset_target6 f6 + nft_filter_apply_ipset_target6 ff nft_fw_nfqws_post6 "$f6 $desync" $qn6 + [ "$MODE_FILTER" = "autohostlist" ] && nft_fw_nfqws_pre6 "$(nft_reverse_nfqws_rule $ff)" $qn6 fi if [ -n "$qns6" ]; then f6="tcp dport 443 $first_packet_only" nft_filter_apply_ipset_target6 f6 nft_fw_nfqws_post6 "$f6 $desync" $qns6 + [ "$MODE_FILTER" = "autohostlist" ] && nft_fw_nfqws_pre6 "$(nft_reverse_nfqws_rule $f6)" $qns6 fi fi diff --git a/common/queue.sh b/common/queue.sh index 7d7fc52..15badfe 100644 --- a/common/queue.sh +++ b/common/queue.sh @@ -1,3 +1,12 @@ +apply_unspecified_desync_modes() +{ + NFQWS_OPT_DESYNC_HTTP="${NFQWS_OPT_DESYNC_HTTP:-$NFQWS_OPT_DESYNC}" + NFQWS_OPT_DESYNC_HTTPS="${NFQWS_OPT_DESYNC_HTTPS:-$NFQWS_OPT_DESYNC}" + NFQWS_OPT_DESYNC_HTTP6="${NFQWS_OPT_DESYNC_HTTP6:-$NFQWS_OPT_DESYNC_HTTP}" + NFQWS_OPT_DESYNC_HTTPS6="${NFQWS_OPT_DESYNC_HTTPS6:-$NFQWS_OPT_DESYNC_HTTPS}" + NFQWS_OPT_DESYNC_QUIC6="${NFQWS_OPT_DESYNC_QUIC6:-$NFQWS_OPT_DESYNC_QUIC}" +} + get_nfqws_qnums() { # $1 - var name for ipv4 http diff --git a/docs/iptables.txt b/docs/iptables.txt index fac4ca2..e28bb6e 100644 --- a/docs/iptables.txt +++ b/docs/iptables.txt @@ -14,6 +14,11 @@ iptables -t mangle -I POSTROUTING -p tcp -m multiport --dports 80,443 -m connbyt iptables -t mangle -I POSTROUTING -p tcp --dport 443 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass iptables -t mangle -I POSTROUTING -p udp --dport 443 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass +# auto hostlist with avoiding wrong ACK numbers in RST,ACK packets sent by russian DPI +sysctl net.netfilter.nf_conntrack_tcp_be_liberal=1 +iptables -t mangle -I POSTROUTING -p tcp -m multiport --dports 80,443 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 1:12 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass +iptables -t mangle -I PREROUTING -p tcp -m multiport --sports 80,443 -m connbytes --connbytes-dir=reply --connbytes-mode=packets --connbytes 1:4 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass + For TPROXY : diff --git a/docs/nftables.txt b/docs/nftables.txt index 6fb387a..4acbb59 100644 --- a/docs/nftables.txt +++ b/docs/nftables.txt @@ -19,7 +19,13 @@ For dpi desync attack : nft delete table inet ztest nft create table inet ztest nft add chain inet ztest post "{type filter hook postrouting priority mangle;}" -nft add rule inet ztest post tcp dport "{80,443}" queue num 200 bypass +nft add rule inet ztest post tcp dport "{80,443}" ct original packets 1-12 queue num 200 bypass +nft add rule inet ztest post udp dport 443 ct original packets 1-4 queue num 200 bypass + +# auto hostlist with avoiding wrong ACK numbers in RST,ACK packets sent by russian DPI +sysctl net.netfilter.nf_conntrack_tcp_be_liberal=1 +nft add chain inet ztest pre "{type filter hook prerouting priority filter;}" +nft add rule inet ztest pre tcp sport "{80,443}" ct reply packets 1-4 queue num 200 bypass show rules : nft list table inet ztest diff --git a/docs/redsocks.txt b/docs/redsocks.txt index f0c9983..c3bc990 100644 --- a/docs/redsocks.txt +++ b/docs/redsocks.txt @@ -116,7 +116,7 @@ SOXIFIER_PORT=1099 create_ipset no-update -network_find_wan_all wan_iface +network_find_wan4_all wan_iface for ext_iface in $wan_iface; do network_get_device ext_device $ext_iface ipt OUTPUT -t nat -o $ext_device -p tcp --dport 443 -m set --match-set zapret dst -m set ! --match-set nozapret dst -j REDIRECT --to-port $SOXIFIER_PORT diff --git a/docs/wireguard/wireguard_iproute_openwrt.txt b/docs/wireguard/wireguard_iproute_openwrt.txt index c392baa..6d36f91 100644 --- a/docs/wireguard/wireguard_iproute_openwrt.txt +++ b/docs/wireguard/wireguard_iproute_openwrt.txt @@ -268,7 +268,7 @@ OUTPUT относится к исходящим с роутера пакетам create_ipset no-update -network_find_wan_all wan_iface +network_find_wan4_all wan_iface for ext_iface in $wan_iface; do network_get_device DEVICE $ext_iface ipt OUTPUT -t mangle -o $DEVICE -p tcp --dport 443 -m set --match-set zapret dst -m set ! --match-set nozapret dst -j MARK --set-mark 0x800/0x800 @@ -414,7 +414,7 @@ connmark относится к соединению, fwmark - к пакету. create_ipset no-update -network_find_wan_all wan_iface +network_find_wan4_all wan_iface for ext_iface in $wan_iface; do network_get_device DEVICE $ext_iface ipt OUTPUT -t mangle -o $DEVICE -p tcp --dport 443 -m set --match-set zapret dst -m set ! --match-set nozapret dst -j MARK --set-mark 0x800/0x800 diff --git a/init.d/openwrt/functions b/init.d/openwrt/functions index 2ce3529..1ced585 100644 --- a/init.d/openwrt/functions +++ b/init.d/openwrt/functions @@ -30,12 +30,7 @@ CUSTOM_SCRIPT="$ZAPRET_BASE/init.d/openwrt/custom" IPSET_EXCLUDE="-m set ! --match-set nozapret" IPSET_EXCLUDE6="-m set ! --match-set nozapret6" -NFQWS_OPT_DESYNC_HTTP="${NFQWS_OPT_DESYNC_HTTP:-$NFQWS_OPT_DESYNC}" -NFQWS_OPT_DESYNC_HTTPS="${NFQWS_OPT_DESYNC_HTTPS:-$NFQWS_OPT_DESYNC}" -NFQWS_OPT_DESYNC_HTTP6="${NFQWS_OPT_DESYNC_HTTP6:-$NFQWS_OPT_DESYNC_HTTP}" -NFQWS_OPT_DESYNC_HTTPS6="${NFQWS_OPT_DESYNC_HTTPS6:-$NFQWS_OPT_DESYNC_HTTPS}" -NFQWS_OPT_DESYNC_QUIC6="${NFQWS_OPT_DESYNC_QUIC6:-$NFQWS_OPT_DESYNC_QUIC}" - +apply_unspecified_desync_modes # can be multiple ipv6 outgoing interfaces @@ -60,6 +55,15 @@ network_find_wan6_all() __network_ifstatus "$1" "" "[@.route[@.target='::' && !@.table]].interface" "" 10 2>/dev/null && return network_find_wan6 $1 } +network_find_wanX_devices() +{ + # $1 - ip version: 4 or 6 + # $2 - variable to put result to + local ifaces + network_find_wan${1}_all ifaces + call_for_multiple_items network_get_device $2 "$ifaces" +} + dnat6_target() { @@ -87,25 +91,35 @@ set_route_localnet() } -fw_nfqws_post_x() +fw_nfqws_prepost_x() { # $1 - 1 - add, 0 - del # $2 - filter # $3 - queue number - # $4 - ip version : 4 or 6 + # $4 - 4/6 + # $5 - post/pre + local ifaces DWAN network_find_wan${4}_all ifaces call_for_multiple_items network_get_device DWAN "$ifaces" - [ -n "$DWAN" ] && _fw_nfqws_post${4} $1 "$2" $3 "$(unique $DWAN)" + [ -n "$DWAN" ] && _fw_nfqws_${5}${4} $1 "$2" $3 "$(unique $DWAN)" } fw_nfqws_post4() { - fw_nfqws_post_x $1 "$2" $3 4 + fw_nfqws_prepost_x $1 "$2" $3 4 post } fw_nfqws_post6() { - fw_nfqws_post_x $1 "$2" $3 6 + fw_nfqws_prepost_x $1 "$2" $3 6 post +} +fw_nfqws_pre4() +{ + fw_nfqws_prepost_x $1 "$2" $3 4 pre +} +fw_nfqws_pre6() +{ + fw_nfqws_prepost_x $1 "$2" $3 6 pre } fw_tpws_x() { @@ -146,10 +160,6 @@ list_nfqws_rules() grep -E "NFQUEUE --queue-num $QNUM --queue-bypass|NFQUEUE --queue-num $(($QNUM+1)) --queue-bypass|NFQUEUE --queue-num $(($QNUM+2)) --queue-bypass|NFQUEUE --queue-num $(($QNUM+3)) --queue-bypass|NFQUEUE --queue-num $(($QNUM+10)) --queue-bypass|NFQUEUE --queue-num $(($QNUM+11)) --queue-bypass" | \ sed -re 's/^-A POSTROUTING (.*) -j NFQUEUE.*$/\1/' -e "s/-m mark ! --mark $DESYNC_MARK\/$DESYNC_MARK//" } -reverse_nfqws_rule() -{ - sed -e 's/-o /-i /g' -e 's/--dport /--sport /g' -e 's/--dports /--sports /g' -e 's/ dst$/ src/' -e 's/ dst / src /g' -} apply_flow_offloading_enable_rule() { # $1 = '' for ipv4, '6' for ipv6 @@ -171,14 +181,16 @@ apply_flow_offloading_exempt_rule() } flow_offloading_unexempt_v() { - ipt$1_del FORWARD -j forwarding_rule_zapret + # $1 = '' for ipv4, '6' for ipv6 + local DWAN + network_find_wanX_devices ${1:-4} DWAN + for i in $DWAN; do ipt$1_del FORWARD -o $i -j forwarding_rule_zapret ; done ip$1tables -F forwarding_rule_zapret 2>/dev/null ip$1tables -X forwarding_rule_zapret 2>/dev/null } flow_offloading_exempt_v() { # $1 = '' for ipv4, '6' for ipv6 - is_ipt_flow_offload_avail $1 || return 0 flow_offloading_unexempt_v $1 @@ -186,21 +198,19 @@ flow_offloading_exempt_v() [ "$FLOWOFFLOAD" = 'software' -o "$FLOWOFFLOAD" = 'hardware' ] && { ip$1tables -N forwarding_rule_zapret - list_nfqws_rules $1 | - while read rule; do - apply_flow_offloading_exempt_rule "$1" $rule - done - - list_nfqws_rules $1 | grep -v "connbytes" | reverse_nfqws_rule | + # remove outgoing interface + list_nfqws_rules $1 | sed -re 's/-o +[^ ]+//g' | while read rule; do apply_flow_offloading_exempt_rule "$1" $rule done apply_flow_offloading_enable_rule $1 - ipt$1 FORWARD -j forwarding_rule_zapret + # only outgoing to WAN packets trigger flow offloading + local DWAN + network_find_wanX_devices ${1:-4} DWAN + for i in $DWAN; do ipt$1 FORWARD -o $i -j forwarding_rule_zapret; done } - return 0 } flow_offloading_exempt() @@ -252,3 +262,11 @@ nft_fw_nfqws_post6() { _nft_fw_nfqws_post6 "$1" $2 always_apply_wan_filter } +nft_fw_nfqws_pre4() +{ + _nft_fw_nfqws_pre4 "$1" $2 always_apply_wan_filter +} +nft_fw_nfqws_pre6() +{ + _nft_fw_nfqws_pre6 "$1" $2 always_apply_wan_filter +} diff --git a/init.d/sysv/functions b/init.d/sysv/functions index 9382c45..e67ae32 100644 --- a/init.d/sysv/functions +++ b/init.d/sysv/functions @@ -67,10 +67,7 @@ IPSET_CR="$ZAPRET_BASE/ipset/create_ipset.sh" [ -n "$QNUM" ] || QNUM=200 [ -n "$NFQWS" ] || NFQWS="$ZAPRET_BASE/nfq/nfqws" NFQWS_OPT_BASE="$USEROPT --dpi-desync-fwmark=$DESYNC_MARK" -NFQWS_OPT_DESYNC_HTTP="${NFQWS_OPT_DESYNC_HTTP:-$NFQWS_OPT_DESYNC}" -NFQWS_OPT_DESYNC_HTTPS="${NFQWS_OPT_DESYNC_HTTPS:-$NFQWS_OPT_DESYNC}" -NFQWS_OPT_DESYNC_HTTP6="${NFQWS_OPT_DESYNC_HTTP6:-$NFQWS_OPT_DESYNC_HTTP}" -NFQWS_OPT_DESYNC_HTTPS6="${NFQWS_OPT_DESYNC_HTTPS6:-$NFQWS_OPT_DESYNC_HTTPS}" +apply_unspecified_desync_modes [ -n "$TPPORT" ] || TPPORT=988 [ -n "$TPWS" ] || TPWS="$ZAPRET_BASE/tpws/tpws" @@ -113,6 +110,14 @@ fw_nfqws_post6() { _fw_nfqws_post6 $1 "$2" $3 "${IFACE_WAN6:-$IFACE_WAN}" } +fw_nfqws_pre4() +{ + _fw_nfqws_pre4 $1 "$2" $3 "$IFACE_WAN" +} +fw_nfqws_pre6() +{ + _fw_nfqws_pre6 $1 "$2" $3 "${IFACE_WAN6:-$IFACE_WAN}" +} fw_tpws4() { _fw_tpws4 $1 "$2" $3 "$IFACE_LAN" "$IFACE_WAN" @@ -137,6 +142,14 @@ nft_fw_nfqws_post6() { _nft_fw_nfqws_post6 "$1" $2 "${IFACE_WAN6:-$IFACE_WAN}" } +nft_fw_nfqws_pre4() +{ + _nft_fw_nfqws_pre4 "$1" $2 "$IFACE_WAN" +} +nft_fw_nfqws_pre6() +{ + _nft_fw_nfqws_pre6 "$1" $2 "${IFACE_WAN6:-$IFACE_WAN}" +} nft_fill_ifsets_overload() { nft_fill_ifsets "$IFACE_LAN" "$IFACE_WAN" "${IFACE_WAN6:-$IFACE_WAN}" diff --git a/install_easy.sh b/install_easy.sh index 22a5d58..efd3d85 100755 --- a/install_easy.sh +++ b/install_easy.sh @@ -225,8 +225,8 @@ select_mode_quic() } select_mode_filter() { - local filter="none ipset hostlist" - [ "$MODE" = "tpws-socks" ] && filter="none hostlist" + local filter="none ipset hostlist autohostlist" + [ "$MODE" = "tpws-socks" ] && filter="none hostlist autohostlist" echo echo select filtering : ask_list MODE_FILTER "$filter" none && write_config_var MODE_FILTER @@ -487,7 +487,7 @@ backup_restore_settings() { # $1 - 1 - backup, 0 - restore local mode=$1 - on_off_function _backup_settings _restore_settings $mode "config" "init.d/sysv/custom" "init.d/openwrt/custom" "init.d/macos/custom" "ipset/zapret-hosts-user.txt" "ipset/zapret-hosts-user-exclude.txt" "ipset/zapret-hosts-user-ipban.txt" + on_off_function _backup_settings _restore_settings $mode "config" "init.d/sysv/custom" "init.d/openwrt/custom" "init.d/macos/custom" "ipset/zapret-hosts-user.txt" "ipset/zapret-hosts-user-exclude.txt" "ipset/zapret-hosts-user-ipban.txt" "ipset/zapret-hosts-auto.txt" } check_location() diff --git a/nfq/conntrack.c b/nfq/conntrack.c index bd7e7f9..dbd07cd 100644 --- a/nfq/conntrack.c +++ b/nfq/conntrack.c @@ -25,11 +25,16 @@ static void connswap(const t_conn *c, t_conn *c2) c2->dport = c->sport; } +static void ConntrackFreeElem(t_conntrack_pool *elem) +{ + if (elem->track.hostname) free(elem->track.hostname); + free(elem); +} static void ConntrackPoolDestroyPool(t_conntrack_pool **pp) { t_conntrack_pool *elem, *tmp; - HASH_ITER(hh, *pp, elem, tmp) { HASH_DEL(*pp, elem); free(elem); } + HASH_ITER(hh, *pp, elem, tmp) { HASH_DEL(*pp, elem); ConntrackFreeElem(elem); } } void ConntrackPoolDestroy(t_conntrack *p) { @@ -226,7 +231,7 @@ static bool ConntrackPoolDropPool(t_conntrack_pool **pp, const struct ip *ip, co t=ConntrackPoolSearch(*pp,&connswp); } if (!t) return false; - HASH_DEL(*pp, t); free(t); + HASH_DEL(*pp, t); ConntrackFreeElem(t); return true; } bool ConntrackPoolDrop(t_conntrack *p, const struct ip *ip, const struct ip6_hdr *ip6, const struct tcphdr *tcphdr, const struct udphdr *udphdr) @@ -251,7 +256,7 @@ void ConntrackPoolPurge(t_conntrack *p) t->conn.l4proto==IPPROTO_UDP && tidle>=p->timeout_udp) { - HASH_DEL(p->pool, t); free(t); + HASH_DEL(p->pool, t); ConntrackFreeElem(t); } } p->t_last_purge = tnow; @@ -263,6 +268,18 @@ static void taddr2str(uint8_t l3proto, const t_addr *a, char *buf, size_t bufsiz if (!inet_ntop(family_from_proto(l3proto), a, buf, bufsize) && bufsize) *buf=0; } +static const char *ConntrackProtoName(t_l7proto proto) +{ + switch(proto) + { + case HTTP: return "HTTP"; + case TLS: return "TLS"; + case QUIC: return "QUIC"; + case WIREGUARD: return "WIREGUARD"; + case DHT: return "DHT"; + default: return "UNKNOWN"; + } +} void ConntrackPoolDump(const t_conntrack *p) { t_conntrack_pool *t, *tmp; @@ -271,29 +288,24 @@ void ConntrackPoolDump(const t_conntrack *p) HASH_ITER(hh, p->pool, t, tmp) { taddr2str(t->conn.l3proto, &t->conn.src, sa1, sizeof(sa1)); taddr2str(t->conn.l3proto, &t->conn.dst, sa2, sizeof(sa2)); + printf("%s [%s]:%u => [%s]:%u : %s : t0=%llu last=t0+%llu now=last+%llu packets_orig=d%llu/n%llu packets_reply=d%llu/n%llu ", + proto_name(t->conn.l4proto), + sa1, t->conn.sport, sa2, t->conn.dport, + t->conn.l4proto==IPPROTO_TCP ? connstate_s[t->track.state] : "-", + (unsigned long long)t->track.t_start, (unsigned long long)(t->track.t_last - t->track.t_start), (unsigned long long)(tnow - t->track.t_last), + (unsigned long long)t->track.pdcounter_orig, (unsigned long long)t->track.pcounter_orig, + (unsigned long long)t->track.pdcounter_reply, (unsigned long long)t->track.pcounter_reply); if (t->conn.l4proto==IPPROTO_TCP) - printf("%s [%s]:%u => [%s]:%u : %s : t0=%llu last=t0+%llu now=last+%llu packets_orig=d%llu/n%llu packets_reply=d%llu/n%llu seq0=%u rseq=%u pos_orig=%u ack0=%u rack=%u pos_reply=%u wsize_orig=%u:%d wsize_reply=%u:%d", - proto_name(t->conn.l4proto), - sa1, t->conn.sport, sa2, t->conn.dport, - t->conn.l4proto==IPPROTO_TCP ? connstate_s[t->track.state] : "-", - (unsigned long long)t->track.t_start, (unsigned long long)(t->track.t_last - t->track.t_start), (unsigned long long)(tnow - t->track.t_last), - (unsigned long long)t->track.pdcounter_orig, (unsigned long long)t->track.pcounter_orig, - (unsigned long long)t->track.pdcounter_reply, (unsigned long long)t->track.pcounter_reply, + printf("seq0=%u rseq=%u pos_orig=%u ack0=%u rack=%u pos_reply=%u wsize_orig=%u:%d wsize_reply=%u:%d", t->track.seq0, t->track.seq_last - t->track.seq0, t->track.pos_orig - t->track.seq0, t->track.ack0, t->track.ack_last - t->track.ack0, t->track.pos_reply - t->track.ack0, t->track.winsize_orig, t->track.scale_orig==SCALE_NONE ? -1 : t->track.scale_orig, t->track.winsize_reply, t->track.scale_reply==SCALE_NONE ? -1 : t->track.scale_reply); else - printf("%s [%s]:%u => [%s]:%u : %s : t0=%llu last=t0+%llu now=last+%llu packets_orig=d%llu/n%llu packets_reply=d%llu/n%llu rseq=%u pos_orig=%u rack=%u pos_reply=%u", - proto_name(t->conn.l4proto), - sa1, t->conn.sport, sa2, t->conn.dport, - t->conn.l4proto==IPPROTO_TCP ? connstate_s[t->track.state] : "-", - (unsigned long long)t->track.t_start, (unsigned long long)(t->track.t_last - t->track.t_start), (unsigned long long)(tnow - t->track.t_last), - (unsigned long long)t->track.pdcounter_orig, (unsigned long long)t->track.pcounter_orig, - (unsigned long long)t->track.pdcounter_reply, (unsigned long long)t->track.pcounter_reply, + printf("rseq=%u pos_orig=%u rack=%u pos_reply=%u", t->track.seq_last, t->track.pos_orig, t->track.ack_last, t->track.pos_reply); - printf(" cutoff=%u wss_cutoff=%u d_cutoff=%u\n", - t->track.b_cutoff, t->track.b_wssize_cutoff, t->track.b_desync_cutoff); + printf(" req_retrans=%u cutoff=%u wss_cutoff=%u d_cutoff=%u hostname=%s l7proto=%s\n", + t->track.req_retrans_counter, t->track.b_cutoff, t->track.b_wssize_cutoff, t->track.b_desync_cutoff, t->track.hostname, ConntrackProtoName(t->track.l7proto)); }; } diff --git a/nfq/conntrack.h b/nfq/conntrack.h index a9b220d..d2fce2b 100644 --- a/nfq/conntrack.h +++ b/nfq/conntrack.h @@ -20,6 +20,7 @@ #define HASH_FUNCTION HASH_BER #include "uthash.h" +#define RETRANS_COUNTER_STOP ((uint8_t)-1) typedef union { struct in_addr ip; @@ -37,6 +38,7 @@ typedef struct // ESTABLISHED - any except SYN or SYN/ACK received // FIN - FIN or RST received typedef enum {SYN=0, ESTABLISHED, FIN} t_connstate; +typedef enum {UNKNOWN=0, HTTP, TLS, QUIC, WIREGUARD, DHT} t_l7proto; typedef struct { // common state @@ -51,9 +53,15 @@ typedef struct uint32_t seq0, ack0; // starting seq and ack uint16_t winsize_orig, winsize_reply; // last seen window size uint8_t scale_orig, scale_reply; // last seen window scale factor. SCALE_NONE if none + + uint8_t req_retrans_counter; // number of request retransmissions + uint32_t req_seq; // sequence number of the request (to track retransmissions) bool b_cutoff; // mark for deletion bool b_wssize_cutoff, b_desync_cutoff; + + t_l7proto l7proto; + char *hostname; } t_ctrack; typedef struct diff --git a/nfq/desync.c b/nfq/desync.c index 0efc908..7739404 100644 --- a/nfq/desync.c +++ b/nfq/desync.c @@ -11,9 +11,9 @@ const char *fake_http_request_default = "GET / HTTP/1.1\r\nHost: www.w3.org\r\n" - "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0\r\n" - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" - "Accept-Encoding: gzip, deflate\r\n\r\n"; + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" + "Accept-Encoding: gzip, deflate, br\r\n\r\n"; const uint8_t fake_tls_clienthello_default[517] = { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0x9a, 0x8f, 0xa7, 0x6a, 0x5d, 0x57, 0xf3, 0x62, 0x19, 0xbe, 0x46, 0x82, 0x45, 0xe2, 0x59, 0x5c, 0xb4, 0x48, 0x31, 0x12, 0x15, @@ -158,7 +158,10 @@ static void maybe_cutoff(t_ctrack *ctrack, uint8_t proto) if (proto==IPPROTO_TCP) // we do not need conntrack entry anymore if all cutoff conditions are either not defined or reached // do not drop udp entry because it will be recreated when next packet arrives - ctrack->b_cutoff |= (!params.wssize || ctrack->b_wssize_cutoff) && (!params.desync_cutoff || ctrack->b_desync_cutoff); + ctrack->b_cutoff |= \ + (!params.wssize || ctrack->b_wssize_cutoff) && + (!params.desync_cutoff || ctrack->b_desync_cutoff) && + (!*params.hostlist_auto_filename || ctrack->req_retrans_counter==RETRANS_COUNTER_STOP); } } static void wssize_cutoff(t_ctrack *ctrack) @@ -169,7 +172,73 @@ static void wssize_cutoff(t_ctrack *ctrack) maybe_cutoff(ctrack, IPPROTO_TCP); } } -#define CONNTRACK_REQUIRED (params.wssize || params.desync_cutoff) + +static void ctrack_stop_req_counter(t_ctrack *ctrack) +{ + ctrack->req_retrans_counter = RETRANS_COUNTER_STOP; + maybe_cutoff(ctrack, IPPROTO_TCP); +} + +// return true if retrans trigger fires +static bool auto_hostlist_retrans(t_ctrack *ctrack, uint8_t l4proto, int threshold) +{ + if (*params.hostlist_auto_filename && ctrack && ctrack->req_retrans_counter!=RETRANS_COUNTER_STOP) + { + ctrack->req_retrans_counter++; + DLOG("req retrans counter : %u/%u\n",ctrack->req_retrans_counter, threshold); + if (ctrack->req_retrans_counter >= threshold) + { + DLOG("req retrans threshold reached\n"); + ctrack_stop_req_counter(ctrack); + return true; + } + if (l4proto==IPPROTO_TCP) + { + if (!ctrack->req_seq) ctrack->req_seq = ctrack->seq_last; + if (ctrack->seq_last != ctrack->req_seq) + { + DLOG("another request, not retransmission. stop tracking.\n"); + ctrack_stop_req_counter(ctrack); + return false; + } + } + } + return false; +} +static void auto_hostlist_failed(const char *hostname) +{ + hostfail_pool *fail_counter; + + fail_counter = HostFailPoolFind(params.hostlist_auto_fail_counters, hostname); + if (!fail_counter) + { + fail_counter = HostFailPoolAdd(¶ms.hostlist_auto_fail_counters, hostname, params.hostlist_auto_fail_time); + if (!fail_counter) + { + fprintf(stderr, "HostFailPoolAdd: out of memory\n"); + return; + } + } + fail_counter->counter++; + DLOG("auto hostlist : %s : fail counter %d/%d\n", hostname, fail_counter->counter, params.hostlist_auto_fail_threshold); + if (fail_counter->counter >= params.hostlist_auto_fail_threshold) + { + DLOG("auto hostlist : fail threshold reached. adding %s to auto hostlist\n", hostname); + HostFailPoolDel(¶ms.hostlist_auto_fail_counters, fail_counter); + if (!StrPoolAddStr(¶ms.hostlist, hostname)) + { + fprintf(stderr, "StrPoolAddStr out of memory\n"); + return; + } + if (!append_to_list_file(params.hostlist_auto_filename, hostname)) + { + perror("write to auto hostlist:"); + return; + } + } +} + +#define CONNTRACK_REQUIRED (params.wssize || params.desync_cutoff || *params.hostlist_auto_filename) // result : true - drop original packet, false = dont drop packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, uint8_t *data_pkt, size_t len_pkt, struct ip *ip, struct ip6_hdr *ip6hdr, struct tcphdr *tcphdr, size_t len_tcp, uint8_t *data_payload, size_t len_payload) { @@ -190,6 +259,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, ConntrackPoolPurge(¶ms.conntrack); if (ConntrackPoolFeed(¶ms.conntrack, ip, ip6hdr, tcphdr, NULL, len_payload, &ctrack, &bReverse)) maybe_cutoff(ctrack, IPPROTO_TCP); + HostFailPoolPurgeRateLimited(¶ms.hostlist_auto_fail_counters); } //ConntrackPoolDump(¶ms.conntrack); @@ -199,11 +269,48 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, tcp_rewrite_winsize(tcphdr, params.wsize, params.wscale); res=modify; } - - if (bReverse) return res; // nothing to do. do not waste cpu + + if (bReverse) + { + // process reply packets for auto hostlist mode + // by looking at RSTs or HTTP replies we decide whether original request looks to be blocked by DPI + if (*params.hostlist_auto_filename && ctrack && ctrack->hostname && ctrack->req_retrans_counter != RETRANS_COUNTER_STOP) + { + bool bFail=false, bStop=false; + if (tcphdr->th_flags & TH_RST) + { + DLOG("incoming RST detected for hostname %s\n", ctrack->hostname); + bFail = bStop = true; + } + else if (len_payload && ctrack->l7proto==HTTP) + { + if (IsHttpReply(data_payload,len_payload)) + { + DLOG("incoming HTTP reply detected for hostname %s\n", ctrack->hostname); + bFail = HttpReplyLooksLikeDPIRedirect(data_payload, len_payload, ctrack->hostname); + if (bFail) + DLOG("redirect to another domain detected. possibly DPI redirect.\n") + else + DLOG("local or in-domain redirect detected. it's not a DPI redirect.\n") + } + else + { + // received not http reply. do not monitor this connection anymore + DLOG("incoming unknown HTTP data detected for hostname %s\n", ctrack->hostname); + } + bStop = true; + } + if (bFail) + auto_hostlist_failed(ctrack->hostname); + if (bStop) + ctrack_stop_req_counter(ctrack); + } + + return res; // nothing to do. do not waste cpu + } uint32_t desync_fwmark = fwmark | params.desync_fwmark; - + if (params.wssize) { if (ctrack) @@ -269,7 +376,7 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, } } - if (!params.wssize && params.desync_mode==DESYNC_NONE && !params.hostcase && !params.hostnospace && !params.domcase) return res; // nothing to do. do not waste cpu + if (!params.wssize && params.desync_mode==DESYNC_NONE && !params.hostcase && !params.hostnospace && !params.domcase && !*params.hostlist_auto_filename) return res; // nothing to do. do not waste cpu if (!(tcphdr->th_flags & TH_SYN) && len_payload) { @@ -280,12 +387,15 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, bool bIsHttp; bool bKnownProtocol = false; uint8_t *p, *phost; - if ((bIsHttp = IsHttp(data_payload,len_payload))) { DLOG("packet contains HTTP request\n") - if (params.wssize) DLOG("forced wssize-cutoff\n"); - wssize_cutoff(ctrack); + if (ctrack && !ctrack->l7proto) ctrack->l7proto = HTTP; + if (params.wssize) + { + DLOG("forced wssize-cutoff\n"); + wssize_cutoff(ctrack); + } fake = params.fake_http; fake_size = params.fake_http_size; if (params.hostlist || params.debug) bHaveHost=HttpExtractHost(data_payload,len_payload,host,sizeof(host)); @@ -299,8 +409,12 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, else if (IsTLSClientHello(data_payload,len_payload)) { DLOG("packet contains TLS ClientHello\n") - if (params.wssize) DLOG("forced wssize-cutoff\n"); - wssize_cutoff(ctrack); + if (ctrack && !ctrack->l7proto) ctrack->l7proto = TLS; + if (params.wssize) + { + DLOG("forced wssize-cutoff\n"); + wssize_cutoff(ctrack); + } fake = params.fake_tls; fake_size = params.fake_tls_size; if (params.hostlist || params.desync_skip_nosni || params.debug) @@ -316,6 +430,10 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, } else { + if (ctrack && *params.hostlist_auto_filename) + // received unknown payload. it means we are out of the request retransmission phase. stop counter + ctrack_stop_req_counter(ctrack); + if (!params.desync_any_proto) return res; DLOG("applying tampering to unknown protocol\n") fake = params.fake_unknown; @@ -325,9 +443,17 @@ packet_process_result dpi_desync_tcp_packet(uint32_t fwmark, const char *ifout, if (bHaveHost) { DLOG("hostname: %s\n",host) - if ((params.hostlist || params.hostlist_exclude) && !HostlistCheck(params.hostlist, params.hostlist_exclude, host)) + bool bExcluded; + if ((params.hostlist || params.hostlist_exclude) && !HostlistCheck(params.hostlist, params.hostlist_exclude, host, &bExcluded)) { DLOG("not applying tampering to this request\n") + if (!bExcluded) + { + if (*params.hostlist_auto_filename && ctrack && !ctrack->hostname) + ctrack->hostname=strdup(host); + if (auto_hostlist_retrans(ctrack, IPPROTO_TCP, params.hostlist_auto_retrans_threshold)) + auto_hostlist_failed(host); + } return res; } } @@ -650,12 +776,13 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, ConntrackPoolPurge(¶ms.conntrack); if (ConntrackPoolFeed(¶ms.conntrack, ip, ip6hdr, NULL, udphdr, len_payload, &ctrack, &bReverse)) maybe_cutoff(ctrack, IPPROTO_UDP); + HostFailPoolPurgeRateLimited(¶ms.hostlist_auto_fail_counters); } //ConntrackPoolDump(¶ms.conntrack); if (bReverse) return res; // nothing to do. do not waste cpu - + if (params.desync_cutoff) { if (ctrack) @@ -674,7 +801,7 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, } } - if (params.desync_mode==DESYNC_NONE) return res; + if (params.desync_mode==DESYNC_NONE && !*params.hostlist_auto_filename) return res; // do not waste cpu if (len_payload) { @@ -688,6 +815,8 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, if (IsQUICInitial(data_payload,len_payload)) { DLOG("packet contains QUIC initial\n") + + if (ctrack && !ctrack->l7proto) ctrack->l7proto = QUIC; fake = params.fake_quic; fake_size = params.fake_quic_size; @@ -732,6 +861,7 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, else if (IsWireguardHandshakeInitiation(data_payload,len_payload)) { DLOG("packet contains wireguard handshake initiation\n") + if (ctrack && !ctrack->l7proto) ctrack->l7proto = WIREGUARD; fake = params.fake_wg; fake_size = params.fake_wg_size; bKnownProtocol = true; @@ -739,12 +869,17 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, else if (IsDhtD1(data_payload,len_payload)) { DLOG("packet contains DHT d1...e\n") + if (ctrack && !ctrack->l7proto) ctrack->l7proto = DHT; fake = params.fake_dht; fake_size = params.fake_dht_size; bKnownProtocol = true; } else { + if (ctrack && *params.hostlist_auto_filename) + // received unknown payload. it means we are out of the request retransmission phase. stop counter + ctrack_stop_req_counter(ctrack); + if (!params.desync_any_proto) return res; DLOG("applying tampering to unknown protocol\n") fake = params.fake_unknown_udp; @@ -754,9 +889,17 @@ packet_process_result dpi_desync_udp_packet(uint32_t fwmark, const char *ifout, if (bHaveHost) { DLOG("hostname: %s\n",host) - if ((params.hostlist || params.hostlist_exclude) && !HostlistCheck(params.hostlist, params.hostlist_exclude, host)) + bool bExcluded; + if ((params.hostlist || params.hostlist_exclude) && !HostlistCheck(params.hostlist, params.hostlist_exclude, host, &bExcluded)) { DLOG("not applying tampering to this request\n") + if (!bExcluded) + { + if (*params.hostlist_auto_filename && ctrack && !ctrack->hostname) + ctrack->hostname=strdup(host); + if (auto_hostlist_retrans(ctrack, IPPROTO_UDP, params.hostlist_auto_retrans_threshold)) + auto_hostlist_failed(host); + } return res; } } diff --git a/nfq/helpers.c b/nfq/helpers.c index 80cbdb0..95bcc88 100644 --- a/nfq/helpers.c +++ b/nfq/helpers.c @@ -82,6 +82,14 @@ bool save_file(const char *filename, const void *buffer, size_t buffer_size) fclose(F); return true; } +bool append_to_list_file(const char *filename, const char *s) +{ + FILE *F = fopen(filename,"at"); + if (!F) return false; + bool bOK = fprintf(F,"%s\n",s)>0; + fclose(F); + return bOK; +} void ntop46(const struct sockaddr *sa, char *str, size_t len) diff --git a/nfq/helpers.h b/nfq/helpers.h index 61043a1..2e474d4 100644 --- a/nfq/helpers.h +++ b/nfq/helpers.h @@ -13,6 +13,7 @@ char *strncasestr(const char *s,const char *find, size_t slen); bool load_file(const char *filename,void *buffer,size_t *buffer_size); bool load_file_nonempty(const char *filename,void *buffer,size_t *buffer_size); bool save_file(const char *filename, const void *buffer, size_t buffer_size); +bool append_to_list_file(const char *filename, const char *s); void print_sockaddr(const struct sockaddr *sa); void ntop46(const struct sockaddr *sa, char *str, size_t len); diff --git a/nfq/hostlist.c b/nfq/hostlist.c index e942d3a..9c81efb 100644 --- a/nfq/hostlist.c +++ b/nfq/hostlist.c @@ -4,6 +4,7 @@ #include "params.h" +// inplace tolower() and add to pool static bool addpool(strpool **hostlist, char **s, const char *end) { char *p; @@ -22,7 +23,6 @@ static bool addpool(strpool **hostlist, char **s, const char *end) return true; } - bool AppendHostList(strpool **hostlist, char *filename) { char *p, *e, s[256], *zbuf; @@ -106,6 +106,12 @@ bool LoadHostLists(strpool **hostlist, struct str_list_head *file_list) return true; } +bool NonEmptyHostlist(strpool **hostlist) +{ + // add impossible hostname if the list is empty + return *hostlist ? true : StrPoolAddStrLen(hostlist, "@&()", 4); +} + bool SearchHostList(strpool *hostlist, const char *host) { @@ -126,12 +132,17 @@ bool SearchHostList(strpool *hostlist, const char *host) } // return : true = apply fooling, false = do not apply -bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host) +bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host, bool *excluded) { + if (excluded) *excluded = false; if (hostlist_exclude) { if (params.debug) printf("Checking exclude hostlist\n"); - if (SearchHostList(hostlist_exclude, host)) return false; + if (SearchHostList(hostlist_exclude, host)) + { + if (excluded) *excluded = true; + return false; + } } if (hostlist) { diff --git a/nfq/hostlist.h b/nfq/hostlist.h index 91bce82..5940ac6 100644 --- a/nfq/hostlist.h +++ b/nfq/hostlist.h @@ -1,10 +1,11 @@ #pragma once #include -#include "strpool.h" +#include "pools.h" bool AppendHostList(strpool **hostlist, char *filename); bool LoadHostLists(strpool **hostlist, struct str_list_head *file_list); +bool NonEmptyHostlist(strpool **hostlist); bool SearchHostList(strpool *hostlist, const char *host); // return : true = apply fooling, false = do not apply -bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host); +bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host, bool *excluded); diff --git a/nfq/nfqws.c b/nfq/nfqws.c index ed8b7eb..5b0262b 100644 --- a/nfq/nfqws.c +++ b/nfq/nfqws.c @@ -8,6 +8,8 @@ #include "params.h" #include "protocol.h" #include "hostlist.h" +#include "gzip.h" +#include "pools.h" #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #include #ifdef __linux__ @@ -73,6 +76,12 @@ static void onusr1(int sig) ConntrackPoolDump(¶ms.conntrack); printf("\n"); } +static void onusr2(int sig) +{ + printf("\nHOSTFAIL POOL DUMP\n"); + HostFailPoolDump(params.hostlist_auto_fail_counters); + printf("\n"); +} @@ -284,6 +293,7 @@ static int nfq_main(void) signal(SIGHUP, onhup); signal(SIGUSR1, onusr1); + signal(SIGUSR2, onusr2); desync_init(); @@ -553,7 +563,11 @@ static void exithelp(void) " --dpi-desync-udplen-pattern=|0xHEX\t; udp tail fill pattern\n" " --dpi-desync-cutoff=[n|d|s]N\t\t\t; apply dpi desync only to packet numbers (n, default), data packet numbers (d), relative sequence (s) less than N\n" " --hostlist=\t\t\t\t; apply dpi desync only to the listed hosts (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n" - " --hostlist-exclude=\t\t\t; do not apply dpi desync to the listed hosts (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n", + " --hostlist-exclude=\t\t\t; do not apply dpi desync to the listed hosts (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n" + " --hostlist-auto=\t\t\t; detect DPI blocks and build hostlist automatically\n" + " --hostlist-auto-fail-threshold=\t\t; how many failed attempts cause hostname to be added to auto hostlist (default : %d)\n" + " --hostlist-auto-fail-time=\t\t; all failed attemps must be within these seconds (default : %d)\n" + " --hostlist-auto-retrans-threshold=\t; how many request retransmissions cause attempt to fail (default : %d)\n", CTRACK_T_SYN, CTRACK_T_EST, CTRACK_T_FIN, CTRACK_T_UDP, #if defined(__linux__) || defined(SO_USER_COOKIE) DPI_DESYNC_FWMARK_DEFAULT,DPI_DESYNC_FWMARK_DEFAULT, @@ -562,7 +576,8 @@ static void exithelp(void) DPI_DESYNC_MAX_FAKE_LEN, IPFRAG_UDP_DEFAULT, DPI_DESYNC_MAX_FAKE_LEN, IPFRAG_TCP_DEFAULT, BADSEQ_INCREMENT_DEFAULT, BADSEQ_ACK_INCREMENT_DEFAULT, - UDPLEN_INCREMENT_DEFAULT + UDPLEN_INCREMENT_DEFAULT, + HOSTLIST_AUTO_FAIL_THRESHOLD_DEFAULT, HOSTLIST_AUTO_FAIL_TIME_DEFAULT, HOSTLIST_AUTO_RETRANS_THRESHOLD_DEFAULT ); exit(1); } @@ -573,16 +588,9 @@ static void cleanup_params(void) strlist_destroy(¶ms.hostlist_files); strlist_destroy(¶ms.hostlist_exclude_files); - if (params.hostlist_exclude) - { - StrPoolDestroy(¶ms.hostlist_exclude); - params.hostlist_exclude = NULL; - } - if (params.hostlist) - { - StrPoolDestroy(¶ms.hostlist); - params.hostlist = NULL; - } + StrPoolDestroy(¶ms.hostlist_exclude); + StrPoolDestroy(¶ms.hostlist); + HostFailPoolDestroy(¶ms.hostlist_auto_fail_counters); } static void exithelp_clean(void) { @@ -641,7 +649,7 @@ int main(int argc, char **argv) int option_index = 0; bool daemon = false; char pidfile[256]; - + srandom(time(NULL)); memset(¶ms, 0, sizeof(params)); @@ -674,10 +682,13 @@ int main(int argc, char **argv) params.desync_badseq_ack_increment = BADSEQ_ACK_INCREMENT_DEFAULT; params.wssize_cutoff_mode = params.desync_cutoff_mode = 'n'; // packet number by default params.udplen_increment = UDPLEN_INCREMENT_DEFAULT; + params.hostlist_auto_fail_threshold = HOSTLIST_AUTO_FAIL_THRESHOLD_DEFAULT; + params.hostlist_auto_fail_time = HOSTLIST_AUTO_FAIL_TIME_DEFAULT; + params.hostlist_auto_retrans_threshold = HOSTLIST_AUTO_RETRANS_THRESHOLD_DEFAULT; LIST_INIT(¶ms.hostlist_files); LIST_INIT(¶ms.hostlist_exclude_files); - + if (can_drop_root()) // are we root ? { params.uid = params.gid = 0x7FFFFFFF; // default uid:gid @@ -737,9 +748,14 @@ int main(int argc, char **argv) {"dpi-desync-cutoff",required_argument,0,0},// optidx=37 {"hostlist",required_argument,0,0}, // optidx=38 {"hostlist-exclude",required_argument,0,0}, // optidx=39 + {"hostlist-auto",required_argument,0,0}, // optidx=40 + {"hostlist-auto-fail-threshold",required_argument,0,0}, // optidx=41 + {"hostlist-auto-fail-time",required_argument,0,0}, // optidx=42 + {"hostlist-auto-retrans-threshold",required_argument,0,0}, // optidx=43 + #ifdef __linux__ - {"bind-fix4",no_argument,0,0}, // optidx=40 - {"bind-fix6",no_argument,0,0}, // optidx=41 + {"bind-fix4",no_argument,0,0}, // optidx=44 + {"bind-fix6",no_argument,0,0}, // optidx=45 #endif {NULL,0,NULL,0} }; @@ -1069,11 +1085,66 @@ int main(int argc, char **argv) exit_clean(1); } break; + case 40: /* hostlist-auto */ + if (*params.hostlist_auto_filename) + { + fprintf(stderr, "only one auto hostlist is supported\n"); + exit_clean(1); + } + { + FILE *F = fopen(optarg,"a+t"); + if (!F) + { + fprintf(stderr, "cannot create %s\n", optarg); + exit_clean(1); + } + bool bGzip = is_gzip(F); + fclose(F); + if (bGzip) + { + fprintf(stderr, "gzipped auto hostlists are not supported\n"); + exit_clean(1); + } + if (params.droproot && chown(optarg, params.uid, -1)) + fprintf(stderr, "could not chown %s. auto hostlist file may not be writable after privilege drop\n", optarg); + } + if (!strlist_add(¶ms.hostlist_files, optarg)) + { + fprintf(stderr, "strlist_add failed\n"); + exit_clean(1); + } + strncpy(params.hostlist_auto_filename, optarg, sizeof(params.hostlist_auto_filename)); + params.hostlist_auto_filename[sizeof(params.hostlist_auto_filename) - 1] = '\0'; + break; + case 41: /* hostlist-auto-fail-threshold */ + params.hostlist_auto_fail_threshold = (uint8_t)atoi(optarg); + if (params.hostlist_auto_fail_threshold<1 || params.hostlist_auto_fail_threshold>20) + { + fprintf(stderr, "auto hostlist fail threshold must be within 1..20\n"); + exit_clean(1); + } + break; + case 42: /* hostlist-auto-fail-time */ + params.hostlist_auto_fail_time = (uint8_t)atoi(optarg); + if (params.hostlist_auto_fail_time<1) + { + fprintf(stderr, "auto hostlist fail time is not valid\n"); + exit_clean(1); + } + break; + case 43: /* hostlist-auto-retrans-threshold */ + params.hostlist_auto_retrans_threshold = (uint8_t)atoi(optarg); + if (params.hostlist_auto_retrans_threshold<2 || params.hostlist_auto_retrans_threshold>10) + { + fprintf(stderr, "auto hostlist fail threshold must be within 2..10\n"); + exit_clean(1); + } + break; #ifdef __linux__ - case 40: /* bind-fix4 */ + case 44: /* bind-fix4 */ params.bind_fix4 = true; break; - case 41: /* bind-fix6 */ + case 45: /* bind-fix6 */ params.bind_fix6 = true; break; #endif @@ -1094,6 +1165,7 @@ int main(int argc, char **argv) fprintf(stderr, "Include hostlist load failed\n"); exit_clean(1); } + if (*params.hostlist_auto_filename) NonEmptyHostlist(¶ms.hostlist); if (!LoadHostLists(¶ms.hostlist_exclude, ¶ms.hostlist_exclude_files)) { fprintf(stderr, "Exclude hostlist load failed\n"); diff --git a/nfq/params.h b/nfq/params.h index b12741d..ac0a7d7 100644 --- a/nfq/params.h +++ b/nfq/params.h @@ -1,7 +1,7 @@ #pragma once #include "params.h" -#include "strpool.h" +#include "pools.h" #include "conntrack.h" #include "desync.h" @@ -26,6 +26,11 @@ #define UDPLEN_INCREMENT_DEFAULT 2 +#define HOSTLIST_AUTO_FAIL_THRESHOLD_DEFAULT 2 +#define HOSTLIST_AUTO_FAIL_TIME_DEFAULT 60 +#define HOSTLIST_AUTO_RETRANS_THRESHOLD_DEFAULT 3 + + struct params_s { bool debug; @@ -59,6 +64,9 @@ struct params_s strpool *hostlist, *hostlist_exclude; struct str_list_head hostlist_files, hostlist_exclude_files; + char hostlist_auto_filename[PATH_MAX]; + int hostlist_auto_fail_threshold, hostlist_auto_fail_time, hostlist_auto_retrans_threshold; + hostfail_pool *hostlist_auto_fail_counters; unsigned int ctrack_t_syn, ctrack_t_est, ctrack_t_fin, ctrack_t_udp; t_conntrack conntrack; diff --git a/nfq/pools.c b/nfq/pools.c new file mode 100644 index 0000000..2b37810 --- /dev/null +++ b/nfq/pools.c @@ -0,0 +1,152 @@ +#define _GNU_SOURCE +#include "pools.h" +#include +#include +#include + +#define DESTROY_STR_POOL(etype, ppool) \ + etype *elem, *tmp; \ + HASH_ITER(hh, *ppool, elem, tmp) { \ + free(elem->str); \ + HASH_DEL(*ppool, elem); \ + free(elem); \ + } + +#define ADD_STR_POOL(etype, ppool, keystr, keystr_len) \ + etype *elem; \ + if (!(elem = (etype*)malloc(sizeof(etype)))) \ + return false; \ + if (!(elem->str = malloc(keystr_len + 1))) \ + { \ + free(elem); \ + return false; \ + } \ + memcpy(elem->str, keystr, keystr_len); \ + elem->str[keystr_len] = 0; \ + oom = false; \ + HASH_ADD_KEYPTR(hh, *ppool, elem->str, strlen(elem->str), elem); \ + if (oom) \ + { \ + free(elem->str); \ + free(elem); \ + return false; \ + } + + +#undef uthash_nonfatal_oom +#define uthash_nonfatal_oom(elt) ut_oom_recover(elt) +static bool oom = false; +static void ut_oom_recover(void *elem) +{ + oom = true; +} + +// for not zero terminated strings +bool StrPoolAddStrLen(strpool **pp, const char *s, size_t slen) +{ + ADD_STR_POOL(strpool, pp, s, slen) + return true; +} +// for zero terminated strings +bool StrPoolAddStr(strpool **pp, const char *s) +{ + return StrPoolAddStrLen(pp, s, strlen(s)); +} + +bool StrPoolCheckStr(strpool *p, const char *s) +{ + strpool *elem; + HASH_FIND_STR(p, s, elem); + return elem != NULL; +} + +void StrPoolDestroy(strpool **pp) +{ + DESTROY_STR_POOL(strpool, pp) +} + + + +void HostFailPoolDestroy(hostfail_pool **pp) +{ + DESTROY_STR_POOL(hostfail_pool, pp) +} +hostfail_pool * HostFailPoolAdd(hostfail_pool **pp,const char *s,int fail_time) +{ + size_t slen = strlen(s); + ADD_STR_POOL(hostfail_pool, pp, s, slen) + elem->expire = time(NULL) + fail_time; + return elem; +} +hostfail_pool *HostFailPoolFind(hostfail_pool *p,const char *s) +{ + hostfail_pool *elem; + HASH_FIND_STR(p, s, elem); + return elem; +} +void HostFailPoolDel(hostfail_pool **p, hostfail_pool *elem) +{ + HASH_DEL(*p, elem); + free(elem); +} +void HostFailPoolPurge(hostfail_pool **pp) +{ + hostfail_pool *elem, *tmp; + time_t now = time(NULL); + HASH_ITER(hh, *pp, elem, tmp) + { + if (now >= elem->expire) + { + free(elem->str); + HASH_DEL(*pp, elem); + free(elem); + } + } +} +static time_t host_fail_purge_prev=0; +void HostFailPoolPurgeRateLimited(hostfail_pool **pp) +{ + time_t now = time(NULL); + // do not purge too often to save resources + if (host_fail_purge_prev != now) + { + HostFailPoolPurge(pp); + host_fail_purge_prev = now; + } +} +void HostFailPoolDump(hostfail_pool *p) +{ + hostfail_pool *elem, *tmp; + time_t now = time(NULL); + HASH_ITER(hh, p, elem, tmp) + printf("host=%s counter=%d time_left=%lld\n",elem->str,elem->counter,(long long int)elem->expire-now); +} + + +bool strlist_add(struct str_list_head *head, const char *filename) +{ + struct str_list *entry = malloc(sizeof(struct str_list)); + if (!entry) return false; + entry->str = strdup(filename); + if (!entry->str) + { + free(entry); + return false; + } + LIST_INSERT_HEAD(head, entry, next); + return true; +} +static void strlist_entry_destroy(struct str_list *entry) +{ + if (entry->str) free(entry->str); + free(entry); +} +void strlist_destroy(struct str_list_head *head) +{ + struct str_list *entry; + while ((entry = LIST_FIRST(head))) + { + LIST_REMOVE(entry, next); + strlist_entry_destroy(entry); + } +} diff --git a/nfq/pools.h b/nfq/pools.h new file mode 100644 index 0000000..ab58968 --- /dev/null +++ b/nfq/pools.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +//#define HASH_BLOOM 20 +#define HASH_NONFATAL_OOM 1 +#define HASH_FUNCTION HASH_BER +#include "uthash.h" + +typedef struct strpool { + char *str; /* key */ + UT_hash_handle hh; /* makes this structure hashable */ +} strpool; + +void StrPoolDestroy(strpool **pp); +bool StrPoolAddStr(strpool **pp,const char *s); +bool StrPoolAddStrLen(strpool **pp,const char *s,size_t slen); +bool StrPoolCheckStr(strpool *p,const char *s); + +struct str_list { + char *str; + LIST_ENTRY(str_list) next; +}; +LIST_HEAD(str_list_head, str_list); + + +typedef struct hostfail_pool { + char *str; /* key */ + int counter; /* value */ + time_t expire; /* when to expire record (unixtime) */ + UT_hash_handle hh; /* makes this structure hashable */ +} hostfail_pool; + +void HostFailPoolDestroy(hostfail_pool **pp); +hostfail_pool *HostFailPoolAdd(hostfail_pool **pp,const char *s,int fail_time); +hostfail_pool *HostFailPoolFind(hostfail_pool *p,const char *s); +void HostFailPoolDel(hostfail_pool **pp, hostfail_pool *elem); +void HostFailPoolPurge(hostfail_pool **pp); +void HostFailPoolPurgeRateLimited(hostfail_pool **pp); +void HostFailPoolDump(hostfail_pool *p); + +bool strlist_add(struct str_list_head *head, const char *filename); +void strlist_destroy(struct str_list_head *head); diff --git a/nfq/protocol.c b/nfq/protocol.c index b8f2d69..f9a6793 100644 --- a/nfq/protocol.c +++ b/nfq/protocol.c @@ -20,69 +20,95 @@ bool IsHttp(const uint8_t *data, size_t len) } return false; } -bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host) +bool IsHttpReply(const uint8_t *data, size_t len) +{ + // HTTP/1.x 200\r\n + return len>14 && !memcmp(data,"HTTP/1.",7) && (data[7]=='0' || data[7]=='1') && data[8]==' ' && + data[9]>='0' && data[9]<='9' && + data[10]>='0' && data[10]<='9' && + data[11]>='0' && data[11]<='9'; +} +int HttpReplyCode(const uint8_t *data, size_t len) +{ + return (data[9]-'0')*100 + (data[10]-'0')*10 + (data[11]-'0'); +} +bool HttpExtractHeader(const uint8_t *data, size_t len, const char *header, char *buf, size_t len_buf) { const uint8_t *p, *s, *e = data + len; - p = (uint8_t*)strncasestr((char*)data, "\nHost:", len); + p = (uint8_t*)strncasestr((char*)data, header, len); if (!p) return false; - p += 6; + p += strlen(header); while (p < e && (*p == ' ' || *p == '\t')) p++; s = p; while (s < e && (*s != '\r' && *s != '\n' && *s != ' ' && *s != '\t')) s++; if (s > p) { size_t slen = s - p; - if (host && len_host) + if (buf && len_buf) { - if (slen >= len_host) slen = len_host - 1; - for (size_t i = 0; i < slen; i++) host[i] = tolower(p[i]); - host[slen] = 0; + if (slen >= len_buf) slen = len_buf - 1; + for (size_t i = 0; i < slen; i++) buf[i] = tolower(p[i]); + buf[slen] = 0; } return true; } return false; } - -static uint8_t tvb_get_varint(const uint8_t *tvb, uint64_t *value) +bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host) { - switch (*tvb >> 6) + return HttpExtractHeader(data, len, "\nHost:", host, len_host); +} +const char *HttpFind2ndLevelDomain(const char *host) +{ + const char *p=NULL; + if (*host) { - case 0: /* 0b00 => 1 byte length (6 bits Usable) */ - if (value) *value = *tvb & 0x3F; - return 1; - case 1: /* 0b01 => 2 bytes length (14 bits Usable) */ - if (value) *value = pntoh16(tvb) & 0x3FFF; - return 2; - case 2: /* 0b10 => 4 bytes length (30 bits Usable) */ - if (value) *value = pntoh32(tvb) & 0x3FFFFFFF; - return 4; - case 3: /* 0b11 => 8 bytes length (62 bits Usable) */ - if (value) *value = pntoh64(tvb) & 0x3FFFFFFFFFFFFFFF; - return 8; + for (p = host + strlen(host)-1; p>host && *p!='.'; p--); + if (*p=='.') for (p--; p>host && *p!='.'; p--); + if (*p=='.') p++; } - return 0; + return p; } -static uint8_t tvb_get_size(uint8_t tvb) +// DPI redirects are global redirects to another domain +bool HttpReplyLooksLikeDPIRedirect(const uint8_t *data, size_t len, const char *host) { - return 1 << (tvb >> 6); + char loc[256],*redirect_host, *p; + int code; + + if (!host || !*host) return false; + + code = HttpReplyCode(data,len); + + if (code!=302 && code!=307 || !HttpExtractHeader(data,len,"\nLocation:",loc,sizeof(loc))) return false; + + // something like : https://censor.net/badpage.php?reason=denied&source=RKN + + if (!strncmp(loc,"http://",7)) + redirect_host=loc+7; + else if (!strncmp(loc,"https://",8)) + redirect_host=loc+8; + else + return false; + + // somethinkg like : censor.net/badpage.php?reason=denied&source=RKN + + for(p=redirect_host; *p && *p!='/' ; p++); + *p=0; + if (!*redirect_host) return false; + + // somethinkg like : censor.net + + // extract 2nd level domains + + const char *dhost = HttpFind2ndLevelDomain(host); + const char *drhost = HttpFind2ndLevelDomain(redirect_host); + + return strcasecmp(dhost, drhost)!=0; } -bool IsQUICCryptoHello(const uint8_t *data, size_t len, size_t *hello_offset, size_t *hello_len) -{ - size_t offset = 1; - uint64_t coff, clen; - if (len < 3 || *data != 6) return false; - if ((offset+tvb_get_size(data[offset])) >= len) return false; - offset += tvb_get_varint(data + offset, &coff); - // offset must be 0 if it's a full segment, not just a chunk - if (coff || (offset+tvb_get_size(data[offset])) >= len) return false; - offset += tvb_get_varint(data + offset, &clen); - if (data[offset] != 0x01 || (offset + clen) > len) return false; - if (hello_offset) *hello_offset = offset; - if (hello_len) *hello_len = (size_t)clen; - return true; -} + + bool IsTLSClientHello(const uint8_t *data, size_t len) { return len >= 6 && data[0] == 0x16 && data[1] == 0x03 && data[2] >= 0x01 && data[2] <= 0x03 && data[5] == 0x01 && (pntoh16(data + 3) + 5) <= len; @@ -188,13 +214,45 @@ bool TLSHelloExtractHostFromHandshake(const uint8_t *data, size_t len, char *hos } -bool IsWireguardHandshakeInitiation(const uint8_t *data, size_t len) + +static uint8_t tvb_get_varint(const uint8_t *tvb, uint64_t *value) { - return len==148 && data[0]==1 && data[1]==0 && data[2]==0 && data[3]==0; + switch (*tvb >> 6) + { + case 0: /* 0b00 => 1 byte length (6 bits Usable) */ + if (value) *value = *tvb & 0x3F; + return 1; + case 1: /* 0b01 => 2 bytes length (14 bits Usable) */ + if (value) *value = pntoh16(tvb) & 0x3FFF; + return 2; + case 2: /* 0b10 => 4 bytes length (30 bits Usable) */ + if (value) *value = pntoh32(tvb) & 0x3FFFFFFF; + return 4; + case 3: /* 0b11 => 8 bytes length (62 bits Usable) */ + if (value) *value = pntoh64(tvb) & 0x3FFFFFFFFFFFFFFF; + return 8; + } + return 0; } -bool IsDhtD1(const uint8_t *data, size_t len) +static uint8_t tvb_get_size(uint8_t tvb) { - return len>=7 && data[0]=='d' && data[1]=='1' && data[len-1]=='e'; + return 1 << (tvb >> 6); +} + +bool IsQUICCryptoHello(const uint8_t *data, size_t len, size_t *hello_offset, size_t *hello_len) +{ + size_t offset = 1; + uint64_t coff, clen; + if (len < 3 || *data != 6) return false; + if ((offset+tvb_get_size(data[offset])) >= len) return false; + offset += tvb_get_varint(data + offset, &coff); + // offset must be 0 if it's a full segment, not just a chunk + if (coff || (offset+tvb_get_size(data[offset])) >= len) return false; + offset += tvb_get_varint(data + offset, &clen); + if (data[offset] != 0x01 || (offset + clen) > len) return false; + if (hello_offset) *hello_offset = offset; + if (hello_len) *hello_len = (size_t)clen; + return true; } /* Returns the QUIC draft version or 0 if not applicable. */ @@ -258,7 +316,6 @@ static bool is_quic_v2(uint32_t version) return version == 0x709A50C4; } - static bool quic_hkdf_expand_label(const uint8_t *secret, uint8_t secret_len, const char *label, uint8_t *out, size_t out_len) { uint8_t hkdflabel[64]; @@ -275,7 +332,6 @@ static bool quic_hkdf_expand_label(const uint8_t *secret, uint8_t secret_len, co return !hkdfExpand(SHA256, secret, secret_len, hkdflabel, hkdflabel_size, out, out_len); } - static bool quic_derive_initial_secret(const quic_cid_t *cid, uint8_t *client_initial_secret, uint32_t version) { /* @@ -505,7 +561,6 @@ bool QUICDefragCrypto(const uint8_t *clean,size_t clean_len, uint8_t *defrag,siz return found; } - bool QUICExtractHostFromInitial(const uint8_t *data, size_t data_len, char *host, size_t len_host, bool *bDecryptOK, bool *bIsCryptoHello) { if (bIsCryptoHello) *bIsCryptoHello=false; @@ -565,3 +620,14 @@ bool IsQUICInitial(const uint8_t *data, size_t len) // client hello cannot be too small. likely ACK return sz>=96; } + + + +bool IsWireguardHandshakeInitiation(const uint8_t *data, size_t len) +{ + return len==148 && data[0]==1 && data[1]==0 && data[2]==0 && data[3]==0; +} +bool IsDhtD1(const uint8_t *data, size_t len) +{ + return len>=7 && data[0]=='d' && data[1]=='1' && data[len-1]=='e'; +} diff --git a/nfq/protocol.h b/nfq/protocol.h index 808baa9..4c70def 100644 --- a/nfq/protocol.h +++ b/nfq/protocol.h @@ -7,7 +7,15 @@ #include "crypto/aes-gcm.h" bool IsHttp(const uint8_t *data, size_t len); +// header must be passed like this : "\nHost:" +bool HttpExtractHeader(const uint8_t *data, size_t len, const char *header, char *buf, size_t len_buf); bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host); +bool IsHttpReply(const uint8_t *data, size_t len); +const char *HttpFind2ndLevelDomain(const char *host); +// must be pre-checked by IsHttpReply +int HttpReplyCode(const uint8_t *data, size_t len); +// must be pre-checked by IsHttpReply +bool HttpReplyLooksLikeDPIRedirect(const uint8_t *data, size_t len, const char *host); bool IsTLSClientHello(const uint8_t *data, size_t len); bool TLSFindExt(const uint8_t *data, size_t len, uint16_t type, const uint8_t **ext, size_t *len_ext); diff --git a/nfq/strpool.c b/nfq/strpool.c deleted file mode 100644 index 183b17f..0000000 --- a/nfq/strpool.c +++ /dev/null @@ -1,107 +0,0 @@ -#define _GNU_SOURCE -#include "strpool.h" -#include -#include - -#undef uthash_nonfatal_oom -#define uthash_nonfatal_oom(elt) ut_oom_recover(elt) - -static bool oom = false; -static void ut_oom_recover(strpool *elem) -{ - oom = true; -} - -// for zero terminated strings -bool StrPoolAddStr(strpool **pp, const char *s) -{ - strpool *elem; - if (!(elem = (strpool*)malloc(sizeof(strpool)))) - return false; - if (!(elem->str = strdup(s))) - { - free(elem); - return false; - } - oom = false; - HASH_ADD_KEYPTR(hh, *pp, elem->str, strlen(elem->str), elem); - if (oom) - { - free(elem->str); - free(elem); - return false; - } - return true; -} -// for not zero terminated strings -bool StrPoolAddStrLen(strpool **pp, const char *s, size_t slen) -{ - strpool *elem; - if (!(elem = (strpool*)malloc(sizeof(strpool)))) - return false; - if (!(elem->str = malloc(slen + 1))) - { - free(elem); - return false; - } - memcpy(elem->str, s, slen); - elem->str[slen] = 0; - oom = false; - HASH_ADD_KEYPTR(hh, *pp, elem->str, strlen(elem->str), elem); - if (oom) - { - free(elem->str); - free(elem); - return false; - } - return true; -} - -bool StrPoolCheckStr(strpool *p, const char *s) -{ - strpool *elem; - HASH_FIND_STR(p, s, elem); - return elem != NULL; -} - -void StrPoolDestroy(strpool **p) -{ - strpool *elem, *tmp; - HASH_ITER(hh, *p, elem, tmp) { - free(elem->str); - HASH_DEL(*p, elem); - free(elem); - } - *p = NULL; -} - - - - -bool strlist_add(struct str_list_head *head, const char *filename) -{ - struct str_list *entry = malloc(sizeof(struct str_list)); - if (!entry) return false; - entry->str = strdup(filename); - if (!entry->str) - { - free(entry); - return false; - } - LIST_INSERT_HEAD(head, entry, next); - return true; -} -static void strlist_entry_destroy(struct str_list *entry) -{ - if (entry->str) free(entry->str); - free(entry); -} -void strlist_destroy(struct str_list_head *head) -{ - struct str_list *entry; - while ((entry = LIST_FIRST(head))) - { - LIST_REMOVE(entry, next); - strlist_entry_destroy(entry); - } -} diff --git a/nfq/strpool.h b/nfq/strpool.h deleted file mode 100644 index 36d484e..0000000 --- a/nfq/strpool.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include - -//#define HASH_BLOOM 20 -#define HASH_NONFATAL_OOM 1 -#define HASH_FUNCTION HASH_BER -#include "uthash.h" - -typedef struct strpool { - char *str; /* key */ - UT_hash_handle hh; /* makes this structure hashable */ -} strpool; - -void StrPoolDestroy(strpool **p); -bool StrPoolAddStr(strpool **pp,const char *s); -bool StrPoolAddStrLen(strpool **pp,const char *s,size_t slen); -bool StrPoolCheckStr(strpool *p,const char *s); - -struct str_list { - char *str; - LIST_ENTRY(str_list) next; -}; -LIST_HEAD(str_list_head, str_list); - -bool strlist_add(struct str_list_head *head, const char *filename); -void strlist_destroy(struct str_list_head *head); diff --git a/tpws/helpers.c b/tpws/helpers.c index 6cd4401..a4a7750 100644 --- a/tpws/helpers.c +++ b/tpws/helpers.c @@ -30,6 +30,15 @@ char *strncasestr(const char *s,const char *find, size_t slen) return (char *)s; } +bool append_to_list_file(const char *filename, const char *s) +{ + FILE *F = fopen(filename,"at"); + if (!F) return false; + bool bOK = fprintf(F,"%s\n",s)>0; + fclose(F); + return bOK; +} + void ntop46(const struct sockaddr *sa, char *str, size_t len) { if (!len) return; diff --git a/tpws/helpers.h b/tpws/helpers.h index 1a31765..a021baa 100644 --- a/tpws/helpers.h +++ b/tpws/helpers.h @@ -8,6 +8,8 @@ char *strncasestr(const char *s,const char *find, size_t slen); +bool append_to_list_file(const char *filename, const char *s); + void ntop46(const struct sockaddr *sa, char *str, size_t len); void ntop46_port(const struct sockaddr *sa, char *str, size_t len); void print_sockaddr(const struct sockaddr *sa); diff --git a/tpws/hostlist.c b/tpws/hostlist.c index e942d3a..9c81efb 100644 --- a/tpws/hostlist.c +++ b/tpws/hostlist.c @@ -4,6 +4,7 @@ #include "params.h" +// inplace tolower() and add to pool static bool addpool(strpool **hostlist, char **s, const char *end) { char *p; @@ -22,7 +23,6 @@ static bool addpool(strpool **hostlist, char **s, const char *end) return true; } - bool AppendHostList(strpool **hostlist, char *filename) { char *p, *e, s[256], *zbuf; @@ -106,6 +106,12 @@ bool LoadHostLists(strpool **hostlist, struct str_list_head *file_list) return true; } +bool NonEmptyHostlist(strpool **hostlist) +{ + // add impossible hostname if the list is empty + return *hostlist ? true : StrPoolAddStrLen(hostlist, "@&()", 4); +} + bool SearchHostList(strpool *hostlist, const char *host) { @@ -126,12 +132,17 @@ bool SearchHostList(strpool *hostlist, const char *host) } // return : true = apply fooling, false = do not apply -bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host) +bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host, bool *excluded) { + if (excluded) *excluded = false; if (hostlist_exclude) { if (params.debug) printf("Checking exclude hostlist\n"); - if (SearchHostList(hostlist_exclude, host)) return false; + if (SearchHostList(hostlist_exclude, host)) + { + if (excluded) *excluded = true; + return false; + } } if (hostlist) { diff --git a/tpws/hostlist.h b/tpws/hostlist.h index 91bce82..5940ac6 100644 --- a/tpws/hostlist.h +++ b/tpws/hostlist.h @@ -1,10 +1,11 @@ #pragma once #include -#include "strpool.h" +#include "pools.h" bool AppendHostList(strpool **hostlist, char *filename); bool LoadHostLists(strpool **hostlist, struct str_list_head *file_list); +bool NonEmptyHostlist(strpool **hostlist); bool SearchHostList(strpool *hostlist, const char *host); // return : true = apply fooling, false = do not apply -bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host); +bool HostlistCheck(strpool *hostlist, strpool *hostlist_exclude, const char *host, bool *excluded); diff --git a/tpws/params.h b/tpws/params.h index e646840..c7e4c54 100644 --- a/tpws/params.h +++ b/tpws/params.h @@ -5,7 +5,10 @@ #include #include #include -#include "strpool.h" +#include "pools.h" + +#define HOSTLIST_AUTO_FAIL_THRESHOLD_DEFAULT 2 +#define HOSTLIST_AUTO_FAIL_TIME_DEFAULT 60 enum splithttpreq { split_none = 0, split_method, split_host }; enum tlsrec { tlsrec_none = 0, tlsrec_sni, tlsrec_pos }; @@ -53,6 +56,9 @@ struct params_s strpool *hostlist, *hostlist_exclude; struct str_list_head hostlist_files, hostlist_exclude_files; + char hostlist_auto_filename[PATH_MAX]; + int hostlist_auto_fail_threshold, hostlist_auto_fail_time; + hostfail_pool *hostlist_auto_fail_counters; int debug; diff --git a/tpws/pools.c b/tpws/pools.c new file mode 100644 index 0000000..2b37810 --- /dev/null +++ b/tpws/pools.c @@ -0,0 +1,152 @@ +#define _GNU_SOURCE +#include "pools.h" +#include +#include +#include + +#define DESTROY_STR_POOL(etype, ppool) \ + etype *elem, *tmp; \ + HASH_ITER(hh, *ppool, elem, tmp) { \ + free(elem->str); \ + HASH_DEL(*ppool, elem); \ + free(elem); \ + } + +#define ADD_STR_POOL(etype, ppool, keystr, keystr_len) \ + etype *elem; \ + if (!(elem = (etype*)malloc(sizeof(etype)))) \ + return false; \ + if (!(elem->str = malloc(keystr_len + 1))) \ + { \ + free(elem); \ + return false; \ + } \ + memcpy(elem->str, keystr, keystr_len); \ + elem->str[keystr_len] = 0; \ + oom = false; \ + HASH_ADD_KEYPTR(hh, *ppool, elem->str, strlen(elem->str), elem); \ + if (oom) \ + { \ + free(elem->str); \ + free(elem); \ + return false; \ + } + + +#undef uthash_nonfatal_oom +#define uthash_nonfatal_oom(elt) ut_oom_recover(elt) +static bool oom = false; +static void ut_oom_recover(void *elem) +{ + oom = true; +} + +// for not zero terminated strings +bool StrPoolAddStrLen(strpool **pp, const char *s, size_t slen) +{ + ADD_STR_POOL(strpool, pp, s, slen) + return true; +} +// for zero terminated strings +bool StrPoolAddStr(strpool **pp, const char *s) +{ + return StrPoolAddStrLen(pp, s, strlen(s)); +} + +bool StrPoolCheckStr(strpool *p, const char *s) +{ + strpool *elem; + HASH_FIND_STR(p, s, elem); + return elem != NULL; +} + +void StrPoolDestroy(strpool **pp) +{ + DESTROY_STR_POOL(strpool, pp) +} + + + +void HostFailPoolDestroy(hostfail_pool **pp) +{ + DESTROY_STR_POOL(hostfail_pool, pp) +} +hostfail_pool * HostFailPoolAdd(hostfail_pool **pp,const char *s,int fail_time) +{ + size_t slen = strlen(s); + ADD_STR_POOL(hostfail_pool, pp, s, slen) + elem->expire = time(NULL) + fail_time; + return elem; +} +hostfail_pool *HostFailPoolFind(hostfail_pool *p,const char *s) +{ + hostfail_pool *elem; + HASH_FIND_STR(p, s, elem); + return elem; +} +void HostFailPoolDel(hostfail_pool **p, hostfail_pool *elem) +{ + HASH_DEL(*p, elem); + free(elem); +} +void HostFailPoolPurge(hostfail_pool **pp) +{ + hostfail_pool *elem, *tmp; + time_t now = time(NULL); + HASH_ITER(hh, *pp, elem, tmp) + { + if (now >= elem->expire) + { + free(elem->str); + HASH_DEL(*pp, elem); + free(elem); + } + } +} +static time_t host_fail_purge_prev=0; +void HostFailPoolPurgeRateLimited(hostfail_pool **pp) +{ + time_t now = time(NULL); + // do not purge too often to save resources + if (host_fail_purge_prev != now) + { + HostFailPoolPurge(pp); + host_fail_purge_prev = now; + } +} +void HostFailPoolDump(hostfail_pool *p) +{ + hostfail_pool *elem, *tmp; + time_t now = time(NULL); + HASH_ITER(hh, p, elem, tmp) + printf("host=%s counter=%d time_left=%lld\n",elem->str,elem->counter,(long long int)elem->expire-now); +} + + +bool strlist_add(struct str_list_head *head, const char *filename) +{ + struct str_list *entry = malloc(sizeof(struct str_list)); + if (!entry) return false; + entry->str = strdup(filename); + if (!entry->str) + { + free(entry); + return false; + } + LIST_INSERT_HEAD(head, entry, next); + return true; +} +static void strlist_entry_destroy(struct str_list *entry) +{ + if (entry->str) free(entry->str); + free(entry); +} +void strlist_destroy(struct str_list_head *head) +{ + struct str_list *entry; + while ((entry = LIST_FIRST(head))) + { + LIST_REMOVE(entry, next); + strlist_entry_destroy(entry); + } +} diff --git a/tpws/pools.h b/tpws/pools.h new file mode 100644 index 0000000..ab58968 --- /dev/null +++ b/tpws/pools.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +//#define HASH_BLOOM 20 +#define HASH_NONFATAL_OOM 1 +#define HASH_FUNCTION HASH_BER +#include "uthash.h" + +typedef struct strpool { + char *str; /* key */ + UT_hash_handle hh; /* makes this structure hashable */ +} strpool; + +void StrPoolDestroy(strpool **pp); +bool StrPoolAddStr(strpool **pp,const char *s); +bool StrPoolAddStrLen(strpool **pp,const char *s,size_t slen); +bool StrPoolCheckStr(strpool *p,const char *s); + +struct str_list { + char *str; + LIST_ENTRY(str_list) next; +}; +LIST_HEAD(str_list_head, str_list); + + +typedef struct hostfail_pool { + char *str; /* key */ + int counter; /* value */ + time_t expire; /* when to expire record (unixtime) */ + UT_hash_handle hh; /* makes this structure hashable */ +} hostfail_pool; + +void HostFailPoolDestroy(hostfail_pool **pp); +hostfail_pool *HostFailPoolAdd(hostfail_pool **pp,const char *s,int fail_time); +hostfail_pool *HostFailPoolFind(hostfail_pool *p,const char *s); +void HostFailPoolDel(hostfail_pool **pp, hostfail_pool *elem); +void HostFailPoolPurge(hostfail_pool **pp); +void HostFailPoolPurgeRateLimited(hostfail_pool **pp); +void HostFailPoolDump(hostfail_pool *p); + +bool strlist_add(struct str_list_head *head, const char *filename); +void strlist_destroy(struct str_list_head *head); diff --git a/tpws/protocol.c b/tpws/protocol.c index f2d7463..b5c80c5 100644 --- a/tpws/protocol.c +++ b/tpws/protocol.c @@ -21,29 +21,95 @@ bool IsHttp(const uint8_t *data, size_t len) } return false; } -bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host) +bool IsHttpReply(const uint8_t *data, size_t len) { - const uint8_t *p, *s, *e=data+len; + // HTTP/1.x 200\r\n + return len>14 && !memcmp(data,"HTTP/1.",7) && (data[7]=='0' || data[7]=='1') && data[8]==' ' && + data[9]>='0' && data[9]<='9' && + data[10]>='0' && data[10]<='9' && + data[11]>='0' && data[11]<='9'; +} +int HttpReplyCode(const uint8_t *data, size_t len) +{ + return (data[9]-'0')*100 + (data[10]-'0')*10 + (data[11]-'0'); +} +bool HttpExtractHeader(const uint8_t *data, size_t len, const char *header, char *buf, size_t len_buf) +{ + const uint8_t *p, *s, *e = data + len; - p = (uint8_t*)strncasestr((char*)data, "\nHost:", len); + p = (uint8_t*)strncasestr((char*)data, header, len); if (!p) return false; - p+=6; - while(pp) + p += strlen(header); + while (p < e && (*p == ' ' || *p == '\t')) p++; + s = p; + while (s < e && (*s != '\r' && *s != '\n' && *s != ' ' && *s != '\t')) s++; + if (s > p) { - size_t slen = s-p; - if (host && len_host) + size_t slen = s - p; + if (buf && len_buf) { - if (slen>=len_host) slen=len_host-1; - for(size_t i=0;i= len_buf) slen = len_buf - 1; + for (size_t i = 0; i < slen; i++) buf[i] = tolower(p[i]); + buf[slen] = 0; } return true; } return false; } +bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host) +{ + return HttpExtractHeader(data, len, "\nHost:", host, len_host); +} +const char *HttpFind2ndLevelDomain(const char *host) +{ + const char *p=NULL; + if (*host) + { + for (p = host + strlen(host)-1; p>host && *p!='.'; p--); + if (*p=='.') for (p--; p>host && *p!='.'; p--); + if (*p=='.') p++; + } + return p; +} +// DPI redirects are global redirects to another domain +bool HttpReplyLooksLikeDPIRedirect(const uint8_t *data, size_t len, const char *host) +{ + char loc[256],*redirect_host, *p; + int code; + + if (!host || !*host) return false; + + code = HttpReplyCode(data,len); + + if (code!=302 && code!=307 || !HttpExtractHeader(data,len,"\nLocation:",loc,sizeof(loc))) return false; + + // something like : https://censor.net/badpage.php?reason=denied&source=RKN + + if (!strncmp(loc,"http://",7)) + redirect_host=loc+7; + else if (!strncmp(loc,"https://",8)) + redirect_host=loc+8; + else + return false; + + // somethinkg like : censor.net/badpage.php?reason=denied&source=RKN + + for(p=redirect_host; *p && *p!='/' ; p++); + *p=0; + if (!*redirect_host) return false; + + // somethinkg like : censor.net + + // extract 2nd level domains + + const char *dhost = HttpFind2ndLevelDomain(host); + const char *drhost = HttpFind2ndLevelDomain(redirect_host); + + return strcasecmp(dhost, drhost)!=0; +} + + + bool IsTLSClientHello(const uint8_t *data, size_t len) { return len>=6 && data[0]==0x16 && data[1]==0x03 && data[2]>=0x01 && data[2]<=0x03 && data[5]==0x01 && (pntoh16(data+3)+5)<=len; diff --git a/tpws/protocol.h b/tpws/protocol.h index 188afbc..94be21f 100644 --- a/tpws/protocol.h +++ b/tpws/protocol.h @@ -5,7 +5,16 @@ #include bool IsHttp(const uint8_t *data, size_t len); +// header must be passed like this : "\nHost:" +bool HttpExtractHeader(const uint8_t *data, size_t len, const char *header, char *buf, size_t len_buf); bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host); +bool IsHttpReply(const uint8_t *data, size_t len); +const char *HttpFind2ndLevelDomain(const char *host); +// must be pre-checked by IsHttpReply +int HttpReplyCode(const uint8_t *data, size_t len); +// must be pre-checked by IsHttpReply +bool HttpReplyLooksLikeDPIRedirect(const uint8_t *data, size_t len, const char *host); + bool IsTLSClientHello(const uint8_t *data, size_t len); bool TLSFindExt(const uint8_t *data, size_t len, uint16_t type, const uint8_t **ext, size_t *len_ext); bool TLSHelloExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host); diff --git a/tpws/strpool.c b/tpws/strpool.c deleted file mode 100644 index 183b17f..0000000 --- a/tpws/strpool.c +++ /dev/null @@ -1,107 +0,0 @@ -#define _GNU_SOURCE -#include "strpool.h" -#include -#include - -#undef uthash_nonfatal_oom -#define uthash_nonfatal_oom(elt) ut_oom_recover(elt) - -static bool oom = false; -static void ut_oom_recover(strpool *elem) -{ - oom = true; -} - -// for zero terminated strings -bool StrPoolAddStr(strpool **pp, const char *s) -{ - strpool *elem; - if (!(elem = (strpool*)malloc(sizeof(strpool)))) - return false; - if (!(elem->str = strdup(s))) - { - free(elem); - return false; - } - oom = false; - HASH_ADD_KEYPTR(hh, *pp, elem->str, strlen(elem->str), elem); - if (oom) - { - free(elem->str); - free(elem); - return false; - } - return true; -} -// for not zero terminated strings -bool StrPoolAddStrLen(strpool **pp, const char *s, size_t slen) -{ - strpool *elem; - if (!(elem = (strpool*)malloc(sizeof(strpool)))) - return false; - if (!(elem->str = malloc(slen + 1))) - { - free(elem); - return false; - } - memcpy(elem->str, s, slen); - elem->str[slen] = 0; - oom = false; - HASH_ADD_KEYPTR(hh, *pp, elem->str, strlen(elem->str), elem); - if (oom) - { - free(elem->str); - free(elem); - return false; - } - return true; -} - -bool StrPoolCheckStr(strpool *p, const char *s) -{ - strpool *elem; - HASH_FIND_STR(p, s, elem); - return elem != NULL; -} - -void StrPoolDestroy(strpool **p) -{ - strpool *elem, *tmp; - HASH_ITER(hh, *p, elem, tmp) { - free(elem->str); - HASH_DEL(*p, elem); - free(elem); - } - *p = NULL; -} - - - - -bool strlist_add(struct str_list_head *head, const char *filename) -{ - struct str_list *entry = malloc(sizeof(struct str_list)); - if (!entry) return false; - entry->str = strdup(filename); - if (!entry->str) - { - free(entry); - return false; - } - LIST_INSERT_HEAD(head, entry, next); - return true; -} -static void strlist_entry_destroy(struct str_list *entry) -{ - if (entry->str) free(entry->str); - free(entry); -} -void strlist_destroy(struct str_list_head *head) -{ - struct str_list *entry; - while ((entry = LIST_FIRST(head))) - { - LIST_REMOVE(entry, next); - strlist_entry_destroy(entry); - } -} diff --git a/tpws/strpool.h b/tpws/strpool.h deleted file mode 100644 index 36d484e..0000000 --- a/tpws/strpool.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include - -//#define HASH_BLOOM 20 -#define HASH_NONFATAL_OOM 1 -#define HASH_FUNCTION HASH_BER -#include "uthash.h" - -typedef struct strpool { - char *str; /* key */ - UT_hash_handle hh; /* makes this structure hashable */ -} strpool; - -void StrPoolDestroy(strpool **p); -bool StrPoolAddStr(strpool **pp,const char *s); -bool StrPoolAddStrLen(strpool **pp,const char *s,size_t slen); -bool StrPoolCheckStr(strpool *p,const char *s); - -struct str_list { - char *str; - LIST_ENTRY(str_list) next; -}; -LIST_HEAD(str_list_head, str_list); - -bool strlist_add(struct str_list_head *head, const char *filename); -void strlist_destroy(struct str_list_head *head); diff --git a/tpws/tamper.c b/tpws/tamper.c index 3079244..3358604 100644 --- a/tpws/tamper.c +++ b/tpws/tamper.c @@ -25,15 +25,17 @@ bool find_host(uint8_t **pHost,uint8_t *buf,size_t bs) static const char *http_methods[] = { "GET /","POST /","HEAD /","OPTIONS /","PUT /","DELETE /","CONNECT /","TRACE /",NULL }; // segment buffer has at least 5 extra bytes to extend data block -void modify_tcp_segment(uint8_t *segment,size_t segment_buffer_size,size_t *size,size_t *split_pos) +void tamper_out(t_ctrack *ctrack, uint8_t *segment,size_t segment_buffer_size,size_t *size, size_t *split_pos) { uint8_t *p, *pp, *pHost = NULL; size_t method_len = 0, pos; const char **method; - bool bIsHttp = false, bBypass = false; + bool bIsHttp = false, bBypass = false, bHaveHost = false, bHostExcluded = false; char bRemovedHostSpace = 0; - char *pc, Host[128]; + char *pc, Host[256]; + DBGPRINT("tamper_out") + *split_pos=0; for (method = http_methods; *method; method++) @@ -49,6 +51,7 @@ void modify_tcp_segment(uint8_t *segment,size_t segment_buffer_size,size_t *size if (bIsHttp) { VPRINT("Data block looks like http request start : %s", *method) + if (!ctrack->l7proto) ctrack->l7proto=HTTP; // cpu saving : we search host only if and when required. we do not research host every time we need its position if ((params.hostlist || params.hostlist_exclude) && find_host(&pHost,segment,*size)) { @@ -58,9 +61,10 @@ void modify_tcp_segment(uint8_t *segment,size_t segment_buffer_size,size_t *size while (pp < (segment + *size) && (pp - p) < (sizeof(Host) - 1) && *pp != '\r' && *pp != '\n') pp++; memcpy(Host, p, pp - p); Host[pp - p] = '\0'; + bHaveHost = true; VPRINT("Requested Host is : %s", Host) for(pc = Host; *pc; pc++) *pc=tolower(*pc); - bBypass = !HostlistCheck(params.hostlist, params.hostlist_exclude, Host); + bBypass = !HostlistCheck(params.hostlist, params.hostlist_exclude, Host, &bHostExcluded); } if (!bBypass) { @@ -210,62 +214,161 @@ void modify_tcp_segment(uint8_t *segment,size_t segment_buffer_size,size_t *size { VPRINT("Not acting on this request") } - return; } - - if (IsTLSClientHello(segment,*size)) + else if (IsTLSClientHello(segment,*size)) { - char host[256]; size_t tpos=0,elen; const uint8_t *ext; + + if (!ctrack->l7proto) ctrack->l7proto=TLS; VPRINT("packet contains TLS ClientHello") // we need host only if hostlist is present - if ((params.hostlist || params.hostlist_exclude) && TLSHelloExtractHost((uint8_t*)segment,*size,host,sizeof(host))) + if ((params.hostlist || params.hostlist_exclude) && TLSHelloExtractHost((uint8_t*)segment,*size,Host,sizeof(Host))) { - VPRINT("hostname: %s",host) - if (!HostlistCheck(params.hostlist, params.hostlist_exclude, host)) + VPRINT("hostname: %s",Host) + bHaveHost = true; + bBypass = !HostlistCheck(params.hostlist, params.hostlist_exclude, Host, &bHostExcluded); + } + if (bBypass) + { + VPRINT("Not acting on this request") + } + else + { + switch(params.tlsrec) { - VPRINT("Not acting on this request") - return; + case tlsrec_sni: + if (TLSFindExt(segment,*size,0,&ext,&elen)) + tpos = ext-segment+1; // between typical 1st and 2nd char of hostname + break; + case tlsrec_pos: + tpos = params.tlsrec_pos; + break; + default: + break; } - } - switch(params.tlsrec) - { - case tlsrec_sni: - if (TLSFindExt(segment,*size,0,&ext,&elen)) - tpos = ext-segment+1; // between typical 1st and 2nd char of hostname - break; - case tlsrec_pos: - tpos = params.tlsrec_pos; - break; - default: - break; - } - if (tpos) - { - // construct 2 TLS records from one - uint16_t l = pntoh16(segment+3); // length - if (l>=2) + if (tpos) { - // length is checked in IsTLSClientHello and cannot exceed buffer size - if (tpos>=l) tpos=1; - VPRINT("making 2 TLS records at pos %zu",tpos) - memmove(segment+5+tpos+5,segment+5+tpos,*size-(5+tpos)); - segment[5+tpos] = segment[0]; - segment[5+tpos+1] = segment[1]; - segment[5+tpos+2] = segment[2]; - phton16(segment+5+tpos+3,l-tpos); - phton16(segment+3,tpos); - *size += 5; + // construct 2 TLS records from one + uint16_t l = pntoh16(segment+3); // length + if (l>=2) + { + // length is checked in IsTLSClientHello and cannot exceed buffer size + if (tpos>=l) tpos=1; + VPRINT("making 2 TLS records at pos %zu",tpos) + memmove(segment+5+tpos+5,segment+5+tpos,*size-(5+tpos)); + segment[5+tpos] = segment[0]; + segment[5+tpos+1] = segment[1]; + segment[5+tpos+2] = segment[2]; + phton16(segment+5+tpos+3,l-tpos); + phton16(segment+3,tpos); + *size += 5; + } } - } - if (params.split_pos < *size) - *split_pos = params.split_pos; - return; + if (params.split_pos < *size) + *split_pos = params.split_pos; + } } - - if (params.split_any_protocol && params.split_pos < *size) + else if (params.split_any_protocol && params.split_pos < *size) *split_pos = params.split_pos; + + if (bHaveHost && bBypass && !bHostExcluded && !ctrack->hostname && *params.hostlist_auto_filename) + { + DBGPRINT("tamper_out put hostname : %s", Host) + ctrack->hostname=strdup(Host); + } + +} + + +static void auto_hostlist_failed(const char *hostname) +{ + hostfail_pool *fail_counter; + + fail_counter = HostFailPoolFind(params.hostlist_auto_fail_counters, hostname); + if (!fail_counter) + { + fail_counter = HostFailPoolAdd(¶ms.hostlist_auto_fail_counters, hostname, params.hostlist_auto_fail_time); + if (!fail_counter) + { + fprintf(stderr, "HostFailPoolAdd: out of memory\n"); + return; + } + } + fail_counter->counter++; + VPRINT("auto hostlist : %s : fail counter %d/%d", hostname, fail_counter->counter, params.hostlist_auto_fail_threshold); + if (fail_counter->counter >= params.hostlist_auto_fail_threshold) + { + VPRINT("auto hostlist : fail threshold reached. adding %s to auto hostlist", hostname); + HostFailPoolDel(¶ms.hostlist_auto_fail_counters, fail_counter); + if (!StrPoolAddStr(¶ms.hostlist, hostname)) + { + fprintf(stderr, "StrPoolAddStr out of memory\n"); + return; + } + if (!append_to_list_file(params.hostlist_auto_filename, hostname)) + { + perror("write to auto hostlist:"); + return; + } + } +} + +void tamper_in(t_ctrack *ctrack, uint8_t *segment,size_t segment_buffer_size,size_t *size) +{ + bool bFail=false; + + DBGPRINT("tamper_in hostname=%s", ctrack->hostname) + + HostFailPoolPurgeRateLimited(¶ms.hostlist_auto_fail_counters); + + if (ctrack->l7proto==HTTP && ctrack->hostname) + { + if (IsHttpReply(segment,*size)) + { + VPRINT("incoming HTTP reply detected for hostname %s", ctrack->hostname); + bFail = HttpReplyLooksLikeDPIRedirect(segment, *size, ctrack->hostname); + if (bFail) + VPRINT("redirect to another domain detected. possibly DPI redirect.") + else + VPRINT("local or in-domain redirect detected. it's not a DPI redirect.") + } + else + { + // received not http reply. do not monitor this connection anymore + VPRINT("incoming unknown HTTP data detected for hostname %s", ctrack->hostname); + } + if (bFail) + auto_hostlist_failed(ctrack->hostname); + + } + ctrack->bTamperInCutoff = true; +} + +void rst_in(t_ctrack *ctrack) +{ + DBGPRINT("rst_in hostname=%s", ctrack->hostname) + + HostFailPoolPurgeRateLimited(¶ms.hostlist_auto_fail_counters); + + if (!ctrack->bTamperInCutoff && ctrack->hostname) + { + VPRINT("incoming RST detected for hostname %s", ctrack->hostname); + auto_hostlist_failed(ctrack->hostname); + } +} +void hup_out(t_ctrack *ctrack) +{ + DBGPRINT("hup_out hostname=%s", ctrack->hostname) + + HostFailPoolPurgeRateLimited(¶ms.hostlist_auto_fail_counters); + + if (!ctrack->bTamperInCutoff && ctrack->hostname) + { + // local leg dropped connection after first request. probably due to timeout. + VPRINT("local leg closed connection after first request (timeout ?). hostname: %s", ctrack->hostname); + auto_hostlist_failed(ctrack->hostname); + } } diff --git a/tpws/tamper.h b/tpws/tamper.h index 0c70f88..f2b2fde 100644 --- a/tpws/tamper.h +++ b/tpws/tamper.h @@ -4,5 +4,19 @@ #include #include -bool find_host(uint8_t **pHost,uint8_t *buf,size_t bs); -void modify_tcp_segment(uint8_t *segment,size_t segment_buffer_size,size_t *size,size_t *split_pos); +typedef enum {UNKNOWN=0, HTTP, TLS} t_l7proto; +typedef struct +{ + // common state + t_l7proto l7proto; + bool bFirstReplyChecked; + bool bTamperInCutoff; + char *hostname; +} t_ctrack; + +void tamper_out(t_ctrack *ctrack, uint8_t *segment,size_t segment_buffer_size,size_t *size, size_t *split_pos); +void tamper_in(t_ctrack *ctrack, uint8_t *segment,size_t segment_buffer_size,size_t *size); +// connection reset by remote leg +void rst_in(t_ctrack *ctrack); +// local leg closed connection (timeout waiting response ?) +void hup_out(t_ctrack *ctrack); diff --git a/tpws/tpws.c b/tpws/tpws.c index d7db1e7..f3abb69 100644 --- a/tpws/tpws.c +++ b/tpws/tpws.c @@ -36,6 +36,8 @@ #include "sec.h" #include "redirect.h" #include "helpers.h" +#include "gzip.h" +#include "pools.h" struct params_s params; @@ -62,6 +64,12 @@ void dohup(void) } } +static void onusr2(int sig) +{ + printf("\nHOSTFAIL POOL DUMP\n"); + HostFailPoolDump(params.hostlist_auto_fail_counters); + printf("\n"); +} static int8_t block_sigpipe(void) @@ -120,62 +128,66 @@ static int get_default_ttl(void) static void exithelp(void) { printf( - " --bind-addr=|; for v6 link locals append %%interface_name\n" - " --bind-iface4=\t; bind to the first ipv4 addr of interface\n" - " --bind-iface6=\t; bind to the first ipv6 addr of interface\n" - " --bind-linklocal=no|unwanted|prefer|force\n" - "\t\t\t\t; prohibit, accept, prefer or force ipv6 link local bind\n" - " --bind-wait-ifup=\t\t; wait for interface to appear and up\n" - " --bind-wait-ip=\t\t; after ifup wait for ip address to appear up to N seconds\n" - " --bind-wait-ip-linklocal=\t; (prefer) accept only LL first N seconds then any (unwanted) accept only globals first N seconds then LL\n" - " --bind-wait-only\t\t; wait for bind conditions satisfaction then exit. return code 0 if success.\n" + " --bind-addr=|\t; for v6 link locals append %%interface_name\n" + " --bind-iface4=\t\t; bind to the first ipv4 addr of interface\n" + " --bind-iface6=\t\t; bind to the first ipv6 addr of interface\n" + " --bind-linklocal=no|unwanted|prefer|force ; prohibit, accept, prefer or force ipv6 link local bind\n" + " --bind-wait-ifup=\t\t\t; wait for interface to appear and up\n" + " --bind-wait-ip=\t\t\t; after ifup wait for ip address to appear up to N seconds\n" + " --bind-wait-ip-linklocal=\t\t; (prefer) accept only LL first N seconds then any (unwanted) accept only globals first N seconds then LL\n" + " --bind-wait-only\t\t\t; wait for bind conditions satisfaction then exit. return code 0 if success.\n" " * multiple binds are supported. each bind-addr, bind-iface* start new bind\n" - " --port=\t\t\t; only one port number for all binds is supported\n" - " --socks\t\t\t; implement socks4/5 proxy instead of transparent proxy\n" - " --no-resolve\t\t\t; disable socks5 remote dns ability (resolves are not async, they block all activity)\n" + " --port=\t\t\t\t; only one port number for all binds is supported\n" + " --socks\t\t\t\t; implement socks4/5 proxy instead of transparent proxy\n" + " --no-resolve\t\t\t\t; disable socks5 remote dns ability (resolves are not async, they block all activity)\n" " --local-rcvbuf=\n" " --local-sndbuf=\n" " --remote-rcvbuf=\n" " --remote-sndbuf=\n" - " --skip-nodelay\t\t\t; do not set TCP_NODELAY option for outgoing connections (incompatible with split options)\n" + " --skip-nodelay\t\t\t\t; do not set TCP_NODELAY option for outgoing connections (incompatible with split options)\n" " --maxconn=\n" #ifdef SPLICE_PRESENT - " --maxfiles=\t; should be at least (X*connections+16), where X=6 in tcp proxy mode, X=4 in tampering mode\n" + " --maxfiles=\t\t; should be at least (X*connections+16), where X=6 in tcp proxy mode, X=4 in tampering mode\n" #else - " --maxfiles=\t; should be at least (connections*2+16)\n" + " --maxfiles=\t\t; should be at least (connections*2+16)\n" #endif - " --max-orphan-time=\t; if local leg sends something and closes and remote leg is still connecting then cancel connection attempt after N seconds\n" - " --daemon\t\t\t; daemonize\n" - " --pidfile=\t\t; write pid to file\n" - " --user=\t\t; drop root privs\n" - " --uid=uid[:gid]\t\t; drop root privs\n" + " --max-orphan-time=\t\t; if local leg sends something and closes and remote leg is still connecting then cancel connection attempt after N seconds\n" + " --daemon\t\t\t\t; daemonize\n" + " --pidfile=\t\t\t; write pid to file\n" + " --user=\t\t\t; drop root privs\n" + " --uid=uid[:gid]\t\t\t; drop root privs\n" #if defined(BSD) && !defined(__OpenBSD__) && !defined(__APPLE__) - " --enable-pf\t\t\t; enable PF redirector support. required in FreeBSD when used with PF firewall.\n" + " --enable-pf\t\t\t\t; enable PF redirector support. required in FreeBSD when used with PF firewall.\n" #endif - " --debug=0|1|2\t\t\t; 0(default)=silent 1=verbose 2=debug\n" - "\nTAMPERING:\n" - " --hostlist=\t\t; only act on hosts in the list (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n" - " --hostlist-exclude=\t; do not act on hosts in the list (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n" - " --split-http-req=method|host\t; split at specified logical part of plain http request\n" - " --split-pos=\t; split at specified pos. split-http-req takes precedence for http.\n" - " --split-any-protocol\t\t; split not only http and https\n" + " --debug=0|1|2\t\t\t\t; 0(default)=silent 1=verbose 2=debug\n" + "\nFILTER:\n" + " --hostlist=\t\t\t; only act on hosts in the list (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n" + " --hostlist-exclude=\t\t; do not act on hosts in the list (one host per line, subdomains auto apply, gzip supported, multiple hostlists allowed)\n" + " --hostlist-auto=\t\t; detect DPI blocks and build hostlist automatically\n" + " --hostlist-auto-fail-threshold=\t; how many failed attempts cause hostname to be added to auto hostlist (default : %d)\n" + " --hostlist-auto-fail-time=\t; all failed attemps must be within these seconds (default : %d)\n" + "\nTAMPER:\n" + " --split-http-req=method|host\t\t; split at specified logical part of plain http request\n" + " --split-pos=\t\t; split at specified pos. split-http-req takes precedence for http.\n" + " --split-any-protocol\t\t\t; split not only http and https\n" #if defined(BSD) && !defined(__APPLE__) - " --disorder\t\t\t; when splitting simulate sending second fragment first (BSD sends entire message instead of first fragment, this is not good)\n" + " --disorder\t\t\t\t; when splitting simulate sending second fragment first (BSD sends entire message instead of first fragment, this is not good)\n" #else - " --disorder\t\t\t; when splitting simulate sending second fragment first\n" + " --disorder\t\t\t\t; when splitting simulate sending second fragment first\n" #endif - " --hostcase\t\t\t; change Host: => host:\n" - " --hostspell\t\t\t; exact spelling of \"Host\" header. must be 4 chars. default is \"host\"\n" - " --hostdot\t\t\t; add \".\" after Host: name\n" - " --hosttab\t\t\t; add tab after Host: name\n" - " --hostnospace\t\t\t; remove space after Host:\n" - " --hostpad=\t\t; add dummy padding headers before Host:\n" - " --domcase\t\t\t; mix domain case : Host: TeSt.cOm\n" - " --methodspace\t\t\t; add extra space after method\n" - " --methodeol\t\t\t; add end-of-line before method\n" - " --unixeol\t\t\t; replace 0D0A to 0A\n" - " --tlsrec=sni\t\t\t; make 2 TLS records. split at SNI. don't split if SNI is not present\n" - " --tlsrec-pos=\t\t; make 2 TLS records. split at specified pos\n" + " --hostcase\t\t\t\t; change Host: => host:\n" + " --hostspell\t\t\t\t; exact spelling of \"Host\" header. must be 4 chars. default is \"host\"\n" + " --hostdot\t\t\t\t; add \".\" after Host: name\n" + " --hosttab\t\t\t\t; add tab after Host: name\n" + " --hostnospace\t\t\t\t; remove space after Host:\n" + " --hostpad=\t\t\t; add dummy padding headers before Host:\n" + " --domcase\t\t\t\t; mix domain case : Host: TeSt.cOm\n" + " --methodspace\t\t\t\t; add extra space after method\n" + " --methodeol\t\t\t\t; add end-of-line before method\n" + " --unixeol\t\t\t\t; replace 0D0A to 0A\n" + " --tlsrec=sni\t\t\t\t; make 2 TLS records. split at SNI. don't split if SNI is not present\n" + " --tlsrec-pos=\t\t\t; make 2 TLS records. split at specified pos\n", + HOSTLIST_AUTO_FAIL_THRESHOLD_DEFAULT, HOSTLIST_AUTO_FAIL_TIME_DEFAULT ); exit(1); } @@ -183,16 +195,9 @@ static void cleanup_params(void) { strlist_destroy(¶ms.hostlist_files); strlist_destroy(¶ms.hostlist_exclude_files); - if (params.hostlist_exclude) - { - StrPoolDestroy(¶ms.hostlist_exclude); - params.hostlist_exclude = NULL; - } - if (params.hostlist) - { - StrPoolDestroy(¶ms.hostlist); - params.hostlist = NULL; - } + StrPoolDestroy(¶ms.hostlist_exclude); + StrPoolDestroy(¶ms.hostlist); + HostFailPoolDestroy(¶ms.hostlist_auto_fail_counters); } static void exithelp_clean(void) { @@ -246,6 +251,8 @@ void parse_params(int argc, char *argv[]) params.maxconn = DEFAULT_MAX_CONN; params.max_orphan_time = DEFAULT_MAX_ORPHAN_TIME; params.binds_last = -1; + params.hostlist_auto_fail_threshold = HOSTLIST_AUTO_FAIL_THRESHOLD_DEFAULT; + params.hostlist_auto_fail_time = HOSTLIST_AUTO_FAIL_TIME_DEFAULT; LIST_INIT(¶ms.hostlist_files); LIST_INIT(¶ms.hostlist_exclude_files); @@ -294,18 +301,22 @@ void parse_params(int argc, char *argv[]) { "tlsrec-pos",required_argument,0,0 },// optidx=32 { "hostlist",required_argument,0,0 },// optidx=33 { "hostlist-exclude",required_argument,0,0 },// optidx=34 - { "pidfile",required_argument,0,0 },// optidx=35 - { "debug",optional_argument,0,0 },// optidx=36 - { "local-rcvbuf",required_argument,0,0 },// optidx=37 - { "local-sndbuf",required_argument,0,0 },// optidx=38 - { "remote-rcvbuf",required_argument,0,0 },// optidx=39 - { "remote-sndbuf",required_argument,0,0 },// optidx=40 - { "socks",no_argument,0,0 },// optidx=41 - { "no-resolve",no_argument,0,0 },// optidx=42 - { "skip-nodelay",no_argument,0,0 },// optidx=43 + { "hostlist-auto",required_argument,0,0}, // optidx=35 + { "hostlist-auto-fail-threshold",required_argument,0,0}, // optidx=36 + { "hostlist-auto-fail-time",required_argument,0,0}, // optidx=37 + { "pidfile",required_argument,0,0 },// optidx=38 + { "debug",optional_argument,0,0 },// optidx=39 + { "local-rcvbuf",required_argument,0,0 },// optidx=40 + { "local-sndbuf",required_argument,0,0 },// optidx=41 + { "remote-rcvbuf",required_argument,0,0 },// optidx=42 + { "remote-sndbuf",required_argument,0,0 },// optidx=43 + { "socks",no_argument,0,0 },// optidx=44 + { "no-resolve",no_argument,0,0 },// optidx=45 + { "skip-nodelay",no_argument,0,0 },// optidx=46 #if defined(BSD) && !defined(__OpenBSD__) && !defined(__APPLE__) - { "enable-pf",no_argument,0,0 },// optidx=44 + { "enable-pf",no_argument,0,0 },// optidx=47 #endif + { "hostlist-auto-retrans-threshold",optional_argument,0,0}, // ignored. for nfqws command line compatibility { NULL,0,NULL,0 } }; while ((v = getopt_long_only(argc, argv, "", long_options, &option_index)) != -1) @@ -544,36 +555,84 @@ void parse_params(int argc, char *argv[]) } params.tamper = true; break; - case 35: /* pidfile */ + case 35: /* hostlist-auto */ + if (*params.hostlist_auto_filename) + { + fprintf(stderr, "only one auto hostlist is supported\n"); + exit_clean(1); + } + { + FILE *F = fopen(optarg,"a+t"); + if (!F) + { + fprintf(stderr, "cannot create %s\n", optarg); + exit_clean(1); + } + bool bGzip = is_gzip(F); + fclose(F); + if (bGzip) + { + fprintf(stderr, "gzipped auto hostlists are not supported\n"); + exit_clean(1); + } + if (params.droproot && chown(optarg, params.uid, -1)) + fprintf(stderr, "could not chown %s. auto hostlist file may not be writable after privilege drop\n", optarg); + } + if (!strlist_add(¶ms.hostlist_files, optarg)) + { + fprintf(stderr, "strlist_add failed\n"); + exit_clean(1); + } + strncpy(params.hostlist_auto_filename, optarg, sizeof(params.hostlist_auto_filename)); + params.hostlist_auto_filename[sizeof(params.hostlist_auto_filename) - 1] = '\0'; + params.tamper = true; // need to detect blocks and update autohostlist. cannot just slice. + break; + case 36: /* hostlist-auto-fail-threshold */ + params.hostlist_auto_fail_threshold = (uint8_t)atoi(optarg); + if (params.hostlist_auto_fail_threshold<1 || params.hostlist_auto_fail_threshold>20) + { + fprintf(stderr, "auto hostlist fail threshold must be within 1..20\n"); + exit_clean(1); + } + break; + case 37: /* hostlist-auto-fail-time */ + params.hostlist_auto_fail_time = (uint8_t)atoi(optarg); + if (params.hostlist_auto_fail_time<1) + { + fprintf(stderr, "auto hostlist fail time is not valid\n"); + exit_clean(1); + } + break; + case 38: /* pidfile */ strncpy(params.pidfile,optarg,sizeof(params.pidfile)); params.pidfile[sizeof(params.pidfile)-1]='\0'; break; - case 36: + case 39: params.debug = optarg ? atoi(optarg) : 1; break; - case 37: /* local-rcvbuf */ + case 40: /* local-rcvbuf */ params.local_rcvbuf = atoi(optarg)/2; break; - case 38: /* local-sndbuf */ + case 41: /* local-sndbuf */ params.local_sndbuf = atoi(optarg)/2; break; - case 39: /* remote-rcvbuf */ + case 42: /* remote-rcvbuf */ params.remote_rcvbuf = atoi(optarg)/2; break; - case 40: /* remote-sndbuf */ + case 43: /* remote-sndbuf */ params.remote_sndbuf = atoi(optarg)/2; break; - case 41: /* socks */ + case 44: /* socks */ params.proxy_type = CONN_TYPE_SOCKS; break; - case 42: /* no-resolve */ + case 45: /* no-resolve */ params.no_resolve = true; break; - case 43: /* skip-nodelay */ + case 46: /* skip-nodelay */ params.skip_nodelay = true; break; #if defined(BSD) && !defined(__OpenBSD__) && !defined(__APPLE__) - case 44: /* enable-pf */ + case 47: /* enable-pf */ params.pf_enable = true; break; #endif @@ -599,6 +658,7 @@ void parse_params(int argc, char *argv[]) fprintf(stderr, "Include hostlist load failed\n"); exit_clean(1); } + if (*params.hostlist_auto_filename) NonEmptyHostlist(¶ms.hostlist); if (!LoadHostLists(¶ms.hostlist_exclude, ¶ms.hostlist_exclude_files)) { fprintf(stderr, "Exclude hostlist load failed\n"); @@ -1020,6 +1080,7 @@ int main(int argc, char *argv[]) if (!params.tamper) printf("TCP proxy mode (no tampering)\n"); signal(SIGHUP, onhup); + signal(SIGUSR2, onusr2); retval = event_loop(listen_fd,params.binds_last+1); exit_v = retval < 0 ? EXIT_FAILURE : EXIT_SUCCESS; diff --git a/tpws/tpws_conn.c b/tpws/tpws_conn.c index d119d05..239ed2d 100644 --- a/tpws/tpws_conn.c +++ b/tpws/tpws_conn.c @@ -24,6 +24,7 @@ #include "socks.h" #include "helpers.h" + // keep separate legs counter. counting every time thousands of legs can consume cpu static int legs_local, legs_remote; /* @@ -376,6 +377,7 @@ static void free_conn(tproxy_conn_t *conn) } conn_free_buffers(conn); if (conn->partner) conn->partner->partner=NULL; + if (conn->track.hostname) free(conn->track.hostname); free(conn); } static tproxy_conn_t *new_conn(int fd, bool remote) @@ -890,6 +892,25 @@ static bool handle_proxy_mode(tproxy_conn_t *conn, struct tailhead *conn_list) return false; } +static void tamper(tproxy_conn_t *conn, uint8_t *segment, size_t segment_buffer_size, size_t *segment_size, size_t *split_pos) +{ + *split_pos=0; + if (params.tamper) + { + if (conn->remote) + { + if (conn_partner_alive(conn) && !conn->partner->track.bTamperInCutoff) + { + tamper_in(&conn->partner->track,segment,segment_buffer_size,segment_size); + } + } + else + { + tamper_out(&conn->track,segment,segment_buffer_size,segment_size,split_pos); + } + } +} + #define RD_BLOCK_SIZE 65536 #define MAX_WASTE (1024*1024) static bool handle_epoll(tproxy_conn_t *conn, struct tailhead *conn_list, uint32_t evt) @@ -942,7 +963,7 @@ static bool handle_epoll(tproxy_conn_t *conn, struct tailhead *conn_list, uint32 if (numbytes>0) { #ifdef SPLICE_PRESENT - if (!params.tamper || conn->remote) + if (!params.tamper || conn->remote && conn->track.bTamperInCutoff) { // incoming data from remote leg we splice without touching // pipe is in the local leg, so its in conn->partner->splice_pipe @@ -978,13 +999,11 @@ static bool handle_epoll(tproxy_conn_t *conn, struct tailhead *conn_list, uint32 { conn->trd+=rd; - size_t split_pos=0; + size_t split_pos; bs = rd; -#ifndef SPLICE_PRESENT - if (!conn->remote && params.tamper) -#endif - modify_tcp_segment(buf,sizeof(buf),&bs,&split_pos); + + tamper(conn, buf, sizeof(buf), &bs, &split_pos); if (split_pos) { @@ -1070,8 +1089,13 @@ static bool read_all_and_buffer(tproxy_conn_t *conn, int buffer_number) conn->partner->wr_buf[buffer_number].len = rd; conn->partner->bFlowOut = true; + + size_t split_pos; + tamper(conn, conn->partner->wr_buf[buffer_number].data, numbytes, &conn->partner->wr_buf[buffer_number].len, &split_pos); + if (epoll_update_flow(conn->partner)) return true; + } send_buffer_free(conn->partner->wr_buf+buffer_number); } @@ -1254,6 +1278,8 @@ int event_loop(const int *listen_fd, size_t listen_fd_ct) VPRINT("Socket fd=%d (partner_fd=%d, remote=%d) %s so_error=%d (%s)",conn->fd,conn->partner ? conn->partner->fd : 0,conn->remote,se,errn,strerror(errn)); proxy_remote_conn_ack(conn,errn); read_all_and_buffer(conn,3); + if (errn==ECONNRESET && conn->remote && conn_partner_alive(conn)) rst_in(&conn->partner->track); + conn_close_with_partner_check(&conn_list,&close_list,conn); continue; } @@ -1270,6 +1296,7 @@ int event_loop(const int *listen_fd, size_t listen_fd_ct) { DBGPRINT("EPOLLRDHUP") read_all_and_buffer(conn,2); + if (!conn->remote) hup_out(&conn->track); if (conn_has_unsent(conn)) { diff --git a/tpws/tpws_conn.h b/tpws/tpws_conn.h index 70034b0..544b621 100644 --- a/tpws/tpws_conn.h +++ b/tpws/tpws_conn.h @@ -3,6 +3,7 @@ #include #include #include +#include "tamper.h" #define BACKLOG 10 #define MAX_EPOLL_EVENTS 64 @@ -27,7 +28,7 @@ typedef uint8_t conn_state_t; // when pos==len its time to free buffer struct send_buffer { - char *data; + uint8_t *data; size_t len,pos; int ttl; }; @@ -84,6 +85,8 @@ struct tproxy_conn // buffer cannot be sent if there is unsent data in a lower buffer struct send_buffer wr_buf[4]; + t_ctrack track; + //Create the struct which contains ptrs to next/prev element TAILQ_ENTRY(tproxy_conn) conn_ptrs; };