commit 3703918a4bb4476686056a8b30edba63de4c9aaf Author: bol-van Date: Thu Mar 4 14:30:38 2021 +0300 history purge diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68145e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +DIRS := nfq tpws ip2net mdig +DIRS_MAC := tpws ip2net mdig +TGT := binaries/my + +all: clean + @mkdir -p "$(TGT)"; \ + for dir in $(DIRS); do \ + find "$$dir" -type f \( -name "*.c" -o -name "*.h" -o -name "*akefile" \) -exec chmod -x {} \; ; \ + $(MAKE) -C "$$dir" || exit; \ + for exe in "$$dir/"*; do \ + if [ -f "$$exe" ] && [ -x "$$exe" ]; then \ + mv -f "$$exe" "${TGT}" ; \ + ln -fs "../${TGT}/$$(basename "$$exe")" "$$exe" ; \ + fi \ + done \ + done + +bsd: clean + @mkdir -p "$(TGT)"; \ + for dir in $(DIRS); do \ + find "$$dir" -type f \( -name "*.c" -o -name "*.h" -o -name "*akefile" \) -exec chmod -x {} \; ; \ + $(MAKE) -C "$$dir" bsd || exit; \ + for exe in "$$dir/"*; do \ + if [ -f "$$exe" ] && [ -x "$$exe" ]; then \ + mv -f "$$exe" "${TGT}" ; \ + ln -fs "../${TGT}/$$(basename "$$exe")" "$$exe" ; \ + fi \ + done \ + done + +mac: clean + @mkdir -p "$(TGT)"; \ + for dir in $(DIRS_MAC); do \ + find "$$dir" -type f \( -name "*.c" -o -name "*.h" -o -name "*akefile" \) -exec chmod -x {} \; ; \ + $(MAKE) -C "$$dir" mac || exit; \ + for exe in "$$dir/"*; do \ + if [ -f "$$exe" ] && [ -x "$$exe" ]; then \ + mv -f "$$exe" "${TGT}" ; \ + ln -fs "../${TGT}/$$(basename "$$exe")" "$$exe" ; \ + fi \ + done \ + done + +clean: + @[ -d "$(TGT)" ] && rm -rf "$(TGT)" ; \ + for dir in $(DIRS); do \ + $(MAKE) -C "$$dir" clean; \ + done diff --git a/binaries/aarch64/ip2net b/binaries/aarch64/ip2net new file mode 100755 index 0000000..ce09b8f Binary files /dev/null and b/binaries/aarch64/ip2net differ diff --git a/binaries/aarch64/mdig b/binaries/aarch64/mdig new file mode 100755 index 0000000..3a78f80 Binary files /dev/null and b/binaries/aarch64/mdig differ diff --git a/binaries/aarch64/nfqws b/binaries/aarch64/nfqws new file mode 100755 index 0000000..7b2a18e Binary files /dev/null and b/binaries/aarch64/nfqws differ diff --git a/binaries/aarch64/tpws b/binaries/aarch64/tpws new file mode 100755 index 0000000..e66f4ea Binary files /dev/null and b/binaries/aarch64/tpws differ diff --git a/binaries/arm/ip2net b/binaries/arm/ip2net new file mode 100755 index 0000000..0d4b02f Binary files /dev/null and b/binaries/arm/ip2net differ diff --git a/binaries/arm/mdig b/binaries/arm/mdig new file mode 100755 index 0000000..2a83351 Binary files /dev/null and b/binaries/arm/mdig differ diff --git a/binaries/arm/nfqws b/binaries/arm/nfqws new file mode 100755 index 0000000..d16777c Binary files /dev/null and b/binaries/arm/nfqws differ diff --git a/binaries/arm/tpws b/binaries/arm/tpws new file mode 100755 index 0000000..566026d Binary files /dev/null and b/binaries/arm/tpws differ diff --git a/binaries/mac64/ip2net b/binaries/mac64/ip2net new file mode 100755 index 0000000..75535df Binary files /dev/null and b/binaries/mac64/ip2net differ diff --git a/binaries/mac64/mdig b/binaries/mac64/mdig new file mode 100755 index 0000000..1c3393d Binary files /dev/null and b/binaries/mac64/mdig differ diff --git a/binaries/mac64/tpws b/binaries/mac64/tpws new file mode 100755 index 0000000..30c76dc Binary files /dev/null and b/binaries/mac64/tpws differ diff --git a/binaries/mips32r1-lsb/ip2net b/binaries/mips32r1-lsb/ip2net new file mode 100755 index 0000000..bc85e07 Binary files /dev/null and b/binaries/mips32r1-lsb/ip2net differ diff --git a/binaries/mips32r1-lsb/mdig b/binaries/mips32r1-lsb/mdig new file mode 100755 index 0000000..93df02e Binary files /dev/null and b/binaries/mips32r1-lsb/mdig differ diff --git a/binaries/mips32r1-lsb/nfqws b/binaries/mips32r1-lsb/nfqws new file mode 100755 index 0000000..81e4835 Binary files /dev/null and b/binaries/mips32r1-lsb/nfqws differ diff --git a/binaries/mips32r1-lsb/tpws b/binaries/mips32r1-lsb/tpws new file mode 100755 index 0000000..15bea27 Binary files /dev/null and b/binaries/mips32r1-lsb/tpws differ diff --git a/binaries/mips32r1-msb/ip2net b/binaries/mips32r1-msb/ip2net new file mode 100755 index 0000000..0c910a2 Binary files /dev/null and b/binaries/mips32r1-msb/ip2net differ diff --git a/binaries/mips32r1-msb/mdig b/binaries/mips32r1-msb/mdig new file mode 100755 index 0000000..d92dc56 Binary files /dev/null and b/binaries/mips32r1-msb/mdig differ diff --git a/binaries/mips32r1-msb/nfqws b/binaries/mips32r1-msb/nfqws new file mode 100755 index 0000000..56abf12 Binary files /dev/null and b/binaries/mips32r1-msb/nfqws differ diff --git a/binaries/mips32r1-msb/tpws b/binaries/mips32r1-msb/tpws new file mode 100755 index 0000000..6e62d69 Binary files /dev/null and b/binaries/mips32r1-msb/tpws differ diff --git a/binaries/mips64r2-msb/ip2net b/binaries/mips64r2-msb/ip2net new file mode 100755 index 0000000..bc032ba Binary files /dev/null and b/binaries/mips64r2-msb/ip2net differ diff --git a/binaries/mips64r2-msb/mdig b/binaries/mips64r2-msb/mdig new file mode 100755 index 0000000..98f9b20 Binary files /dev/null and b/binaries/mips64r2-msb/mdig differ diff --git a/binaries/mips64r2-msb/nfqws b/binaries/mips64r2-msb/nfqws new file mode 100755 index 0000000..5ca4ccc Binary files /dev/null and b/binaries/mips64r2-msb/nfqws differ diff --git a/binaries/mips64r2-msb/tpws b/binaries/mips64r2-msb/tpws new file mode 100755 index 0000000..086a9a5 Binary files /dev/null and b/binaries/mips64r2-msb/tpws differ diff --git a/binaries/ppc/ip2net b/binaries/ppc/ip2net new file mode 100755 index 0000000..cd07940 Binary files /dev/null and b/binaries/ppc/ip2net differ diff --git a/binaries/ppc/mdig b/binaries/ppc/mdig new file mode 100755 index 0000000..2c19623 Binary files /dev/null and b/binaries/ppc/mdig differ diff --git a/binaries/ppc/nfqws b/binaries/ppc/nfqws new file mode 100755 index 0000000..108002b Binary files /dev/null and b/binaries/ppc/nfqws differ diff --git a/binaries/ppc/tpws b/binaries/ppc/tpws new file mode 100755 index 0000000..828776f Binary files /dev/null and b/binaries/ppc/tpws differ diff --git a/binaries/x86/ip2net b/binaries/x86/ip2net new file mode 100755 index 0000000..9f89b04 Binary files /dev/null and b/binaries/x86/ip2net differ diff --git a/binaries/x86/mdig b/binaries/x86/mdig new file mode 100755 index 0000000..a057584 Binary files /dev/null and b/binaries/x86/mdig differ diff --git a/binaries/x86/nfqws b/binaries/x86/nfqws new file mode 100755 index 0000000..5643f0e Binary files /dev/null and b/binaries/x86/nfqws differ diff --git a/binaries/x86/tpws b/binaries/x86/tpws new file mode 100755 index 0000000..976c186 Binary files /dev/null and b/binaries/x86/tpws differ diff --git a/binaries/x86_64/ip2net b/binaries/x86_64/ip2net new file mode 100755 index 0000000..0ddba99 Binary files /dev/null and b/binaries/x86_64/ip2net differ diff --git a/binaries/x86_64/mdig b/binaries/x86_64/mdig new file mode 100755 index 0000000..510cb3c Binary files /dev/null and b/binaries/x86_64/mdig differ diff --git a/binaries/x86_64/nfqws b/binaries/x86_64/nfqws new file mode 100755 index 0000000..1d3983d Binary files /dev/null and b/binaries/x86_64/nfqws differ diff --git a/binaries/x86_64/tpws b/binaries/x86_64/tpws new file mode 100755 index 0000000..c04d412 Binary files /dev/null and b/binaries/x86_64/tpws differ diff --git a/binaries/x86_64/tpws_wsl.tgz b/binaries/x86_64/tpws_wsl.tgz new file mode 100644 index 0000000..407b567 Binary files /dev/null and b/binaries/x86_64/tpws_wsl.tgz differ diff --git a/config b/config new file mode 100644 index 0000000..25f4fca --- /dev/null +++ b/config @@ -0,0 +1,69 @@ +# this file is included from init scripts +# change values here + +# can help in case /tmp has not enough space +#TMPDIR=/opt/zapret/tmp + +# options for ipsets +# too low hashsize can cause memory allocation errors on low RAM systems , even if RAM is enough +# too large hashsize will waste lots of RAM +IPSET_OPT="hashsize 262144 maxelem 2097152" + +# options for ip2net. "-4" or "-6" auto added by ipset create script +IP2NET_OPT4="--prefix-length=22-30 --v4-threshold=3/4" +IP2NET_OPT6="--prefix-length=56-64 --v6-threshold=5" + +# ipset/*.sh can compress large lists +GZIP_LISTS=1 +# command to reload ip/host lists after update +# comment or leave empty for auto backend selection : ipset or ipfw if present +# on BSD systems with PF no auto reloading happens. you must provide your own command +# set to "-" to disable reload +#LISTS_RELOAD="pfctl -f /etc/pf.conf" + +# CHOOSE OPERATION MODE +# MODE : nfqws,tpws,filter,custom +# nfqws : use nfqws +# tpws : use tpws +# filter : no daemon, just create ipset or download hostlist +# custom : custom mode. should modify custom init script and add your own code +MODE=tpws +# apply fooling to http +MODE_HTTP=1 +# for nfqws only. support http keep alives. enable only if DPI checks for http request in any outgoing packet +MODE_HTTP_KEEPALIVE=0 +# apply fooling to https +MODE_HTTPS=1 +# none,ipset,hostlist +MODE_FILTER=none + +# CHOOSE NFQWS DAEMON OPTIONS for DPI desync mode. run "nfq/nfqws --help" for option list +DESYNC_MARK=0x40000000 +NFQWS_OPT_DESYNC="--dpi-desync=fake --dpi-desync-ttl=0 --dpi-desync-fooling=badsum --dpi-desync-fwmark=$DESYNC_MARK" + +# CHOOSE TPWS DAEMON OPTIONS. run "tpws/tpws --help" for option list +TPWS_OPT="--hostspell=HOST --split-http-req=method --split-pos=3" + +# openwrt only : donttouch,none,software,hardware +FLOWOFFLOAD=donttouch + +# for routers based on desktop linux and macos. has no effect in openwrt. +# CHOOSE LAN and optinally WAN NETWORK INTERFACES +# or leave them commented if its not router +#IFACE_LAN=eth0 +#IFACE_WAN=eth1 + +# should init scripts apply firewall rules ? +# set to 0 if firewall control system is present +# openwrt uses fw3 firewall , init never touch fw +INIT_APPLY_FW=1 + +# do not work with ipv4 +#DISABLE_IPV4=1 +# do not work with ipv6 +DISABLE_IPV6=1 + +# select which init script will be used to get ip or host list +# possible values : get_user.sh get_antizapret.sh get_combined.sh get_reestr.sh get_hostlist.sh +# comment if not required +GETLIST=get_antifilter_ipsmart.sh diff --git a/docs/bsd.eng.txt b/docs/bsd.eng.txt new file mode 100644 index 0000000..849785e --- /dev/null +++ b/docs/bsd.eng.txt @@ -0,0 +1,346 @@ +Supported versions +------------------ + +FreeBSD 11.x+ , OpenBSD 6.x+, partially MacOS Sierra+ + +Older versions may work or not. pfSense is not supported. + +BSD features +------------ + +BSD does not have NFQUEUE. Similar mechanism - divert sockets. +In BSD compiling the source from nfq directory result in dvtws binary instead of nfqws. +dvtws shares most of the code with nfqws and offers almost identical parameters. + +FreeBSD has 2 firewalls : IPFilter (ipfw) and Packet Filter (PF). OpenBSD has only PF. + +To compile sources in FreeBSD use 'make', in OpenBSD - use 'make bsd', in MacOS - use 'make mac'. +Compile all programs : make -C /opt/zapret +Compile all programs with PF support : make -C /opt/zapret CFLAGS=-DUSE_PF +In FreeBSD enable PF only if you use it. Its undesirable if you don't. +PF is enabled automatically in OpenBSD and MacOS. + +Divert sockets are internal type sockets in the BSD kernel. They have no relation to network addresses +or network packet exchange. They are identified by a port number 1..65535. Its like queue number in NFQUEUE. +Traffic can be diverted to a divert socket using firewall rule. +If nobody listens on the specified divert port packets are dropped. Its similar to NFQUEUE without --queue-bypass. + +ipset/*.sh scripts work with ipfw lookup tables if ipfw is present. +ipfw table is analog to linux ipset. Unlike ipsets ipfw tables share v4 an v6 addresses and subnets. +If ipfw is absent scripts check LISTS_RELOAD config variable. +If its present then scripts execute a command from LISTS_RELOAD. +If LISTS_RELOAD=- scripts do not load tables even if ipfw exists. + +PF can load ip tables from a file. To use this feature with ipset/*.sh scripts disable gzip file creation +using "GZIP_LISTS=0" directive in the /opt/zapret/config file. + +BSD kernel doesn't implement splice syscall. tpws uses regular recv/send operations with data copying to user space. +Its slower but not critical. +tpws uses nonblocking sockets with linux specific epoll feature. +In BSD systems epoll is emulated by epoll-shim library on top of kqueue. + +dvtws uses some programming HACKs, assumptions and knowledge of discovered bugs and limitations. +BSD systems have many limitations, version specific features and bugs in low level networking, especially for ipv6. +Many years have passed but BSD code still has 15-20 year artificial limiters in the code. +dvtws uses additinal divert socket(s) for layer 3 packet injection if raw sockets do not allow it. +It works for the moment but who knows. Such a usage is not very documented. + +mdig and ip2net are fully compatible with BSD. + +FreeBSD +------- + +Divert sockets require special kernel module 'ipdivert'. +Write the following to config files : +/boot/loader.conf (create if absent) : +----------- +ipdivert_load="YES" +net.inet.ip.fw.default_to_accept=1 +----------- +/etc/rc.conf : +----------- +firewall_enable="YES" +firewall_script="/etc/rc.firewall.my" +----------- +/etc/rc.firewall.my : +----------- +ipfw -q -f flush +----------- +Later you will add ipfw commands to /etc/rc.firewall.my to be reapplied after reboot. +You can also run zapret daemons from there. Start them with "--daemon" options, for example : +----------- +pkill ^dvtws$ +/opt/zapret/nfq/dvtws --port=989 --daemon --dpi-desync=split2 +----------- +To restart firewall and daemons run : /etc/rc.d/ipfw restart + + +Assume LAN='em1', WAN="em0". + +tpws transparent mode quick start. + +For all traffic: +ipfw delete 100 +ipfw add 100 fwd 127.0.0.1,988 tcp from me to any 80,443 proto ip4 xmit em0 not uid daemon +ipfw add 100 fwd ::1,988 tcp from me to any 80,443 proto ip6 xmit em0 not uid daemon +ipfw add 100 fwd 127.0.0.1,988 tcp from any to any 80,443 proto ip4 recv em1 +ipfw add 100 fwd ::1,988 tcp from any to any 80,443 proto ip6 recv em1 +/opt/zapret/tpws/tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +Process only table zapret with the exception of table nozapret : +ipfw delete 100 +ipfw add 100 allow tcp from me to table\(nozapret\) 80,443 +ipfw add 100 fwd 127.0.0.1,988 tcp from me to table\(zapret\) 80,443 proto ip4 xmit em0 not uid daemon +ipfw add 100 fwd ::1,988 tcp from me to table\(zapret\) 80,443 proto ip6 xmit em0 not uid daemon +ipfw add 100 allow tcp from any to table\(nozapret\) 80,443 recv em1 +ipfw add 100 fwd 127.0.0.1,988 tcp from any to any 80,443 proto ip4 recv em1 +ipfw add 100 fwd ::1,988 tcp from any to any 80,443 proto ip6 recv em1 +/opt/zapret/tpws/tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +Tables zapret, nozapret, ipban are created by ipset/*.sh scripts the same way as in Linux. +Its a good idea to update tables periodically : + crontab -e + write the line : 0 12 */2 * * /opt/zapret/ipset/get_config.sh + +When using ipfw tpws does not require special permissions for transparent mode. +However without root its not possible to bind to ports <1024 and change UID/GID. Without changing UID tpws +will run into recursive loop, and that's why its necessary to write ipfw rules with the right UID. +Redirecting to ports >=1024 is dangerous. If tpws is not running any unprivileged process can +listen to that port and intercept traffic. + + +dvtws quick start. + +For all traffic: +ipfw delete 100 +ipfw add 100 divert 989 tcp from any to any 80,443 out not diverted not sockarg xmit em0 +/opt/zapret/nfq/dvtws --port=989 --dpi-desync=split2 + +Process only table zapret with the exception of table nozapret : +ipfw delete 100 +ipfw add 100 allow tcp from me to table\(nozapret\) 80,443 +ipfw add 100 divert 989 tcp from any to table\(zapret\) 80,443 out not diverted not sockarg xmit em0 +/opt/zapret/nfq/dvtws --port=989 --dpi-desync=split2 + +Reinjection loop avoidance. +FreeBSD artificially ignores sockarg for ipv6 in the kernel. +This limitation is coming from the ipv6 early age. Code is still in "testing" state. 10-20 years. Everybody forgot about it. +dvtws sends ipv6 forged frames using another divert socket (HACK). they can be filtered out using 'diverted'. +ipv4 frames are filtered using 'sockarg'. + +PF in FreeBSD: +The setup is similar to OpenBSD, but there are important nuances. +1) Don't forget to build special PF-enabled version of tpws : make CFLAGS=-DUSE_PF +2) It's not possible to redirect to ::1. Need to redirect to the link-local address of the incoming interface. +Look for fe80:... address in ifconfig and use it for redirection target. +3) pf.conf syntax is a bit different from OpenBSD. +4) How to set maximum table size : sysctl net.pf.request_maxcount=2000000 +5) The word 'divert-packet' is absent in the pfctl binary, divert-packet rules are not working. +'divert-to' is not the same thing. Looks like its not possible to use dvtws with PF in FreeBSD. +/etc/pf.conf +----------- +rdr pass on em1 inet6 proto tcp to port {80,443} -> fe80::31c:29ff:dee2:1c4d port 988 +rdr pass on em1 inet proto tcp to port {80,443} -> 127.0.0.1 port 988 +----------- +/opt/zapret/tpws/tpws --port=988 --bind-addr=127.0.0.1 --bind-iface6=em1 --bind-linklocal=force + +Its not clear how to do rdr-to outgoing traffic. I could not make route-to scheme work. + + +OpenBSD +------- + +In OpenBSD default tpws bind is ipv6 only. to bind to ipv4 specify --bind-addr=0.0.0.0 +Use --bind-addr=0.0.0.0 --bind-addr=:: to achieve the same default bind as in others OSes. + +tpws for forwarded traffic only : + +/etc/pf.conf +------------ +pass in quick on em1 inet proto tcp to port {80,443} rdr-to 127.0.0.1 port 988 +pass in quick on em1 inet6 proto tcp to port {80,443} rdr-to ::1 port 988 +------------ +pfctl -f /etc/pf.conf +tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +Its not clear how to do rdr-to outgoing traffic. I could not make route-to scheme work. +rdr-to support is done using /dev/pf, that's why transparent mode requires root. + +dvtws for all traffic: + +/etc/pf.conf +------------ +pass out quick on em0 proto tcp to port {80,443} divert-packet port 989 +------------ +pfctl -f /etc/pf.conf +./dvtws --port=989 --dpi-desync=split2 + +dwtws only for table zapret with the exception of table nozapret : + +/etc/pf.conf +------------ +table file "/opt/zapret/ipset/zapret-ip.txt" +table file "/opt/zapret/ipset/zapret-ip-user.txt" +table file "/opt/zapret/ipset/zapret-ip-exclude.txt" +pass out quick on em0 inet proto tcp to port {80,443} +pass out quick on em0 inet proto tcp to port {80,443} divert-packet port 989 +pass out quick on em0 inet proto tcp to port {80,443} divert-packet port 989 +table file "/opt/zapret/ipset/zapret-ip6.txt" +table file "/opt/zapret/ipset/zapret-ip-user6.txt" +table file "/opt/zapret/ipset/zapret-ip-exclude6.txt" +pass out quick on em0 inet6 proto tcp to port {80,443} +pass out quick on em0 inet6 proto tcp to port {80,443} divert-packet port 989 +pass out quick on em0 inet6 proto tcp to port {80,443} divert-packet port 989 +------------ +pfctl -f /etc/pf.conf +./dvtws --port=989 --dpi-desync=split2 + + +dvtws in OpenBSD sends all fakes through a divert socket because raw sockets have critical artificial limitations. +Looks like pf automatically prevent reinsertion of diverted frames. Loop problem does not exist. + +Sadly PF auto applies return rule to divert-packet. +Not only outgoing packets go through dvtws but also incoming. +This adds great unneeded overhead that will be the most noticable on http/https downloads. +I could not figure out how to disable this feature. +Thats why you are encouraged to use table filters with your personal blocked site lists. + +OpenBSD forcibly recomputes tcp checksum after divert. Thats why most likely +dpi-desync-fooling=badsum will not work. dvtws will warn if you specify this parameter. + +ipset scripts do not reload PF by default. To enable reload specify command in /opt/zapret/config : +LISTS_RELOAD="pfctl -f /etc/pf.conf" +Newer pfctl versions can reload tables only : pfctl -Tl -f /etc/pf.conf +But OpenBSD 6.8 pfctl is old enough and does not support that. Newer FreeBSD do. +Don't forget to disable gzip compression : +GZIP_LISTS=0 +If some list files do not exist and have references in pf.conf it leads to error. +You need to exclude those tables from pf.conf and referencing them rules. +After configuration is done you can put ipset script : + crontab -e + write the line : 0 12 */2 * * /opt/zapret/ipset/get_config.sh + + +MacOS +----- + +Initially, the kernel of this OS was based on BSD. That's why it is still BSD but a lot was modified by Apple. +As usual a mass commercial project priorities differ from their free counterparts. +Apple guys do what they want. +What everyone have updated long ago they keep old like a mammoth. But who cares ? + +MacOS used to have ipfw but it was removed later and replaced by PF. +It looks like divert sockets are internally replaced with raw. Its possible to request a divert socket +but it behaves exactly as raw socket with all its BSD inherited + apple specific bugs and feature. +The fact is that divert-packet in /etc/pf.conf does not work. pfctl binary does not contain the word 'divert'. +dvtws does compile but is useless. + +After some efforts tpws works. Apple has removed some important stuff from their newer SDKs (DIOCNATLOOK) making +them undocumented and unsupported. With important definitions copied from an older SDK it was possible to make +transparent mode working again. But this is not guaranteed to work in the future versions. +Another MacOS unique feature is root requirement while polling /dev/pf. +By default tpws drops root. Its necessary to specify --user=root to stay with root. +In other aspects PF behaves very similar to FreeBSD and shares the same pf.conf syntax. + +In MacOS redirection works both for passthrough and outgoing traffic. Outgoing redirection requires route-to rule. +Because tpws is forced to run as root to avoid loop its necessary to exempt root from the redirection. +That's why DPI bypass will not work for local requests from root. + +If you do ipv6 routing you have to get rid of "secured" ipv6 address assignment. +"secured" addresses are designed to be permanent and not related to the MAC address. +And they really are. Except for link-locals. +If you just reboot the system link-locals will not change. But next day they will change. Not necessary to wait so long. +Just change the system time to tomorrow and reboot. Link-locals will change. (at least they change in vmware guest) +Looks like its a kernel bug. Link locals should not change. Its useless and can be harmful. Cant use LL as a gateway. +The easiest solution is to disable "secured" addresses. +Outgoing connections prefer randomly generated temporary addressesas like in other systems. +Put the string "net.inet6.send.opmode=0" to /etc/sysctl.conf. If not present - create it. +Then reboot the system. +If you dont like this solution you can assign an additional static ipv6 address from fd00::/8 range with /128 prefix +to your LAN interface and use it as the gateway address. + +tpws transparent mode only for outgoing connections. en0 - WAN. + +/etc/pf.conf +------------ +rdr pass on lo0 inet proto tcp from !127.0.0.0/8 to any port {80,443} -> 127.0.0.1 port 988 +rdr pass on lo0 inet6 proto tcp from !::1 to any port {80,443} -> fe80::1 port 988 +pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any port {80,443} user { >root } +pass out route-to (lo0 fe80::1) inet6 proto tcp from any to any port {80,443} user { >root } +------------ +pfctl -ef /etc/pf.conf +/opt/zapret/tpws/tpws --user=root --port=988 --bind-addr=127.0.0.1 --bind-iface6=en0 --bind-linklocal=force + + +tpws transparent mode for both passthrough and outgoing connections. en0 - WAN, en1 - WAN. + +ifconfig en1 | grep fe80 + inet6 fe80::bbbb:bbbb:bbbb:bbbb%en1 prefixlen 64 scopeid 0x8 +/etc/pf.conf +------------ +rdr pass on en1 inet proto tcp from any to any port {80,443} -> 127.0.0.1 port 988 +rdr pass on en1 inet6 proto tcp from any to any port {80,443} -> fe80::bbbb:bbbb:bbbb:bbbb port 988 +rdr pass on lo0 inet proto tcp from !127.0.0.0/8 to any port {80,443} -> 127.0.0.1 port 988 +rdr pass on lo0 inet6 proto tcp from !::1 to any port {80,443} -> fe80::1 port 988 +pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any port {80,443} user { >root } +pass out route-to (lo0 fe80::1) inet6 proto tcp from any to any port {80,443} user { >root } +------------ +pfctl -ef /etc/pf.conf +/opt/zapret/tpws/tpws --user=root --port=988 --bind-addr=127.0.0.1 --bind-iface6=en0 --bind-linklocal=force --bind-iface6=en1 --bind-linklocal=force + + +Build from source : make -C /opt/zapret mac + +ipset/*.sh scripts work. + + +MacOS easy install +------------------ + +install_easy.sh supports MacOS + +Shipped precompiled binaries are built for 64-bit MacOS with -mmacosx-version-min=10.8 option. +They should run on all supported MacOS versions. +If no - its easy to build your own. Running 'make' automatically installs developer tools. + +!! Internet sharing is not supported !! +Routing is supported but only manually configured through PF. +If you enable internet sharing tpws stops functioning. When you disable internet sharing you may lose web site access. +To fix : pfctl -f /etc/pf.conf +If you need internet sharing use tpws socks mode. + +launchd is used for autostart (/Library/LaunchDaemons/zapret.plist) +Control script : /opt/zapret/init.d/macos/zapret +The following commands fork with both tpws and firewall (if INIT_APPLY_FW=1 in config) +/opt/zapret/init.d/macos/zapret start +/opt/zapret/init.d/macos/zapret stop +/opt/zapret/init.d/macos/zapret restart +Work with tpws only : +/opt/zapret/init.d/macos/zapret start-daemons +/opt/zapret/init.d/macos/zapret stop-daemons +/opt/zapret/init.d/macos/zapret restart-daemons +Work with PF only : +/opt/zapret/init.d/macos/zapret start-fw +/opt/zapret/init.d/macos/zapret stop-fw +/opt/zapret/init.d/macos/zapret restart-fw +Reloading PF tables : +/opt/zapret/init.d/macos/zapret reload-fw-tables + +Installer configures LISTS_RELOAD in the config so ipset/*.sh scripts automatically reload PF tables. +Installer creates cron job for ipset/get_config.sh, as in OpenWRT. + +start-fw script automatically patches /etc/pf.conf inserting there "zapret" anchors. +Auto patching requires pf.conf with apple anchors preserved. +If your pf.conf is highly customized and patching fails you will see the warning. Do not ignore it. +In that case you need to manually insert "zapret" anchors to your pf.conf (keeping the right rule type ordering) : +rdr-anchor "zapret" +anchor "zapret" +unistall_easy.sh unpatches pf.conf + +start-fw creates 3 anchor files in /etc/pf.anchors : zapret,zapret-v4,zapret-v6. +Last 2 are referenced by anchor "zapret". +Tables nozapret,nozapret6 belong to anchor "zapret". +Tables zapret,zapret-user belong to anchor "zapret-v4". +Tables zapret6,zapret6-user belong to anchor "zapret-v6". +If an ip version is disabled then corresponding anchor is empty and is not referenced from the anchor "zapret". +Tables are only created for existing list files in the ipset directory. diff --git a/docs/bsd.txt b/docs/bsd.txt new file mode 100644 index 0000000..7140341 --- /dev/null +++ b/docs/bsd.txt @@ -0,0 +1,375 @@ +Поддерживаемые версии +--------------------- + +FreeBSD 11.x+ , OpenBSD 6.x+, частично MacOS Sierra+ + +На более старых может собираться, может не собираться, может работать или не работать. +На FreeBSD 10 собирается и работает dvtws. С tpws есть проблемы из-за слишком старой версии компилятора clang. +Вероятно, будет работать, если обновить компилятор. +На pfSense если и можно завести, то это не просто. Собранные на FreeBSD с той же версией ядра бинарики не работают. +Статические бинарики тоже. Модуль ipdivert отсутствует. + + +Особенности BSD систем +---------------------- + +В BSD нет nfqueue. Похожий механизм - divert sockets. +Из каталога "nfq" под BSD собирается dvtws вместо nfqws. +Он разделяет с nfqws большую часть кода и почти совпадает по параметрам командной строки. + +FreeBSD содержит 2 фаервола : IPFilter (ipfw) и Packet Filter (PF). OpenBSD содержит только PF. + +Под FreeBSD tpws и dvtws собираются через "make", под OpenBSD - "make bsd", под MacOS - "make mac". +FreeBSD make распознает BSDmakefile , OpenBSD и MacOS - нет. Поэтому там используется отдельный target в Makefile. +Сборка всех исходников : make -C /opt/zapret +Сборка всех исходников с поддержкой PF : make -C /opt/zapret CFLAGS=-DUSE_PF +В FreeBSD поддержку PF нужно включать только, если вы его используете. Иначе это нежелательно ! +В OpenBSD и MacOS PF при сборке включается автоматически. + +divert сокет - внутренний тип сокета ядра BSD. Он не привязывается ни к какому сетевому адресу, не участвует +в обмене данными через сеть и идентифицируется по номеру порта 1..65535. Аналогия с номером очереди NFQUEUE. +На divert сокеты заворачивается трафик посредством правил ipfw или PF. +Если в фаерволе есть правило divert, но на divert порту никто не слушает, то пакеты дропаются. +Это поведение аналогично правилам NFQUEUE без параметра --queue-bypass. +На FreeBSD divert сокеты могут быть только ipv4, хотя на них принимаются и ipv4, и ipv6 фреймы. +На OpenBSD divert сокеты создаются отдельно для ipv4 и ipv6 и работают только с одной версией ip каждый. +На MacOS похоже, что divert сокеты из ядра вырезаны. См подробнее раздел про MacOS. +Отсылка в divert сокет работает аналогично отсылке через raw socket на linux. Передается полностью IP фрейм, начиная +с ip загловка . Эти особенности учитываются в dvtws. + +Скрипты ipset/*.sh при наличии ipfw работают с ipfw lookup tables. +Это прямой аналог ipset. lookup tables не разделены на v4 и v6. Они могут содержать v4 и v6 адреса и подсети одновременно. +Если ipfw отсутствует, то действие зависит от переменной LISTS_RELOAD в config. +Если она задана, то выполняется команда из LISTS_RELOAD. В противном случае не делается ничего. +Если LISTS_RELOAD=-, то заполнение таблиц отключается даже при наличии ipfw. + +PF может загружать ip таблицы из файла. Чтобы использовать эту возможность следует отключить сжатие gzip для листов +через параметр файла config "GZIP_LISTS=0". + +BSD не содержит системного вызова splice. tpws работает через переброску данных в user mode в оба конца. +Это медленнее, но не критически. +Управление асинхронными сокетами в tpws основано на linux-specific механизме epoll. +В BSD для его эмуляции используется epoll-shim - прослойка для эмуляции epoll на базе kqueue. + +Некоторые функции dvtws пришлось реализовывать через хаки. +В BSD много ограничений, особенностей и багов при работе с низкоуровневой сетью, в особенности в области ipv6. +Казалось бы столько лет прошло, а в коде все еще сидят ограничители 15-20 летней давности. +Прямая отсылка ipv6 фреймов с измененным source address и вовсе невозможна через raw sockets. +OpenBSD не дает отсылать через raw sockets tcp фреймы. +Там, где функции нельзя было реализовать напрямую, либо их реализация привела бы к залезанию в низкоуровневые дебри, +используются те же divert сокеты. Оказывается через них можно скармливать ядру любые пакеты, обходя ограничения +raw sockets. Не знаю насколько это легально, но пока это работает. Однако, имейте в виду. Что-то может сломаться. + +mdig и ip2net полностью работоспособны в BSD. В них нет ничего системо-зависимого. + +FreeBSD +------- + +divert сокеты требуют специального модуля ядра ipdivert. +Поместите следующие строки в /boot/loader.conf (создать, если отсутствует) : +----------- +ipdivert_load="YES" +net.inet.ip.fw.default_to_accept=1 +----------- +В /etc/rc.conf : +----------- +firewall_enable="YES" +firewall_script="/etc/rc.firewall.my" +----------- +/etc/rc.firewall.my : +----------- +ipfw -q -f flush +----------- +В /etc/rc.firewall.my можно дописывать правила ipfw, чтобы они восстанавливались после перезагрузки. +Оттуда же можно запускать и демоны zapret, добавив в параметры "--daemon". Например так : +----------- +pkill ^dvtws$ +/opt/zapret/nfq/dvtws --port=989 --daemon --dpi-desync=split2 +----------- +Для перезапуска фаервола и демонов достаточно будет сделать : /etc/rc.d/ipfw restart + + +Краткая инструкция по запуску tpws в прозрачном режиме. +Предполагается, что интерфейс LAN называется em1, WAN - em0. + +Для всего трафика : +ipfw delete 100 +ipfw add 100 fwd 127.0.0.1,988 tcp from me to any 80,443 proto ip4 xmit em0 not uid daemon +ipfw add 100 fwd ::1,988 tcp from me to any 80,443 proto ip6 xmit em0 not uid daemon +ipfw add 100 fwd 127.0.0.1,988 tcp from any to any 80,443 proto ip4 recv em1 +ipfw add 100 fwd ::1,988 tcp from any to any 80,443 proto ip6 recv em1 +/opt/zapret/tpws/tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +Для трафика только на таблицу zapret, за исключением таблицы nozapret : +ipfw delete 100 +ipfw add 100 allow tcp from me to table\(nozapret\) 80,443 +ipfw add 100 fwd 127.0.0.1,988 tcp from me to table\(zapret\) 80,443 proto ip4 xmit em0 not uid daemon +ipfw add 100 fwd ::1,988 tcp from me to table\(zapret\) 80,443 proto ip6 xmit em0 not uid daemon +ipfw add 100 allow tcp from any to table\(nozapret\) 80,443 recv em1 +ipfw add 100 fwd 127.0.0.1,988 tcp from any to any 80,443 proto ip4 recv em1 +ipfw add 100 fwd ::1,988 tcp from any to any 80,443 proto ip6 recv em1 +/opt/zapret/tpws/tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +Таблицы zapret, nozapret, ipban создаются скриптами из ipset по аналогии с Linux. +Обновление скриптов можно забить в cron под root : + crontab -e + Создать строчку "0 12 */2 * * /opt/zapret/ipset/get_config.sh" + +При использовании ipfw tpws не требует повышенных привилегий для реализации прозрачного режима. +Однако, без рута невозможен бинд на порты <1024 и смена UID/GID. Без смены UID будет рекурсия, +поэтому правила ipfw нужно создавать с учетом UID, под которым работает tpws. +Переадресация на порты >=1024 может создать угрозу перехвата трафика непривилегированным +процессом, если вдруг tpws не запущен. + + +Краткая инструкция по запуску dvtws. + +Для всего трафика : +ipfw delete 100 +ipfw add 100 divert 989 tcp from any to any 80,443 out not diverted not sockarg xmit em0 +/opt/zapret/nfq/dvtws --port=989 ---dpi-desync=split2 + +Для трафика только на таблицу zapret, за исключением таблицы nozapret : +ipfw delete 100 +ipfw add 100 allow tcp from me to table\(nozapret\) 80,443 +ipfw add 100 divert 989 tcp from any to table\(zapret\) 80,443 out not diverted not sockarg xmit em0 +/opt/zapret/nfq/dvtws --port=989 --dpi-desync=split2 + +Недопущение зацикливания - повторного вхождения фейк пакетов на обработку. +FreeBSD игнорирует sockarg в ipv6. +Это искусственное ограничение в коде ядра, которое тянется уже лет 10-20. +Кто-то в свое время посчитал код сырым, и до сих пор никто не удосужился поправить. +dvtws в FreeBSD отсылает ipv4 фреймы через raw socket. Такие пакеты не 'diverted'. Они отсекаются по 'sockarg'. +Для отсылки ipv6 фейков используется divert socket, потому что ipv6 raw сокеты в BSD не дают самому +формировать IP заголовок и подменять source address. Фейки в ipv6 'diverted'. Они отсекаются по 'diverted'. +В linux nfqws для недопущения зацикливания используется fwmark. + + +PF в FreeBSD: +Настройка аналогична OpenBSD, но есть важные нюансы. +1) Не забыть собрать специальную версию под PF : make CFLAGS=-DUSE_PF +2) Нельзя сделать ipv6 rdr на ::1. Нужно делать на link-local адрес входящего интерфейса. +Смотрите через ifconfig адрес fe80:... и добавляете в правило +3) Синтаксис pf.conf немного отличается. Более новая версия PF. +4) Лимит на количество элементов таблиц задается так : sysctl net.pf.request_maxcount=2000000 +5) Слово 'divert-packet' отсутствует в бинарике pfctl, правила divert-packet выдают ошибку. +'divert-to' - это не то. Не похоже, что в FreeBSD можно завести dvtws через PF. +/etc/pf.conf +----------- +rdr pass on em1 inet6 proto tcp to port {80,443} -> fe80::31c:29ff:dee2:1c4d port 988 +rdr pass on em1 inet proto tcp to port {80,443} -> 127.0.0.1 port 988 +----------- +/opt/zapret/tpws/tpws --port=988 --bind-addr=127.0.0.1 --bind-iface6=em1 --bind-linklocal=force + +В PF непонятно как делать rdr-to с той же системы, где работает proxy. Вариант с route-to у меня не заработал. + + +OpenBSD +------- + +В tpws бинд по умолчанию только на ipv6. для бинда на ipv4 указать "--bind-addr=0.0.0.0" +Используйте --bind-addr=0.0.0.0 --bind-addr=:: для достижения того же результата, как в других ОС по умолчанию. +(лучше все же так не делать, а сажать на определенные внутренние адреса или интерфейсы) + +tpws для проходящего трафика : + +/etc/pf.conf +------------ +pass in quick on em1 inet proto tcp to port {80,443} rdr-to 127.0.0.1 port 988 +pass in quick on em1 inet6 proto tcp to port {80,443} rdr-to ::1 port 988 +------------ +pfctl -f /etc/pf.conf +tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +В PF непонятно как делать rdr-to с той же системы, где работает proxy. Вариант с route-to у меня не заработал. +Поддержка rdr-to реализована через /dev/pf, поэтому прозрачный режим требует root. + +dvtws для всего трафика : + +/etc/pf.conf +------------ +pass out quick on em0 proto tcp to port {80,443} divert-packet port 989 +------------ +pfctl -f /etc/pf.conf +./dvtws --port=989 --dpi-desync=split2 + +dvtws для трафика только на таблицу zapret, за исключением таблицы nozapret : + +/etc/pf.conf +------------ +set limit table-entries 2000000 +table file "/opt/zapret/ipset/zapret-ip.txt" +table file "/opt/zapret/ipset/zapret-ip-user.txt" +table file "/opt/zapret/ipset/zapret-ip-exclude.txt" +pass out quick on em0 inet proto tcp to port {80,443} +pass out quick on em0 inet proto tcp to port {80,443} divert-packet port 989 +pass out quick on em0 inet proto tcp to port {80,443} divert-packet port 989 +table file "/opt/zapret/ipset/zapret-ip6.txt" +table file "/opt/zapret/ipset/zapret-ip-user6.txt" +table file "/opt/zapret/ipset/zapret-ip-exclude6.txt" +pass out quick on em0 inet6 proto tcp to port {80,443} +pass out quick on em0 inet6 proto tcp to port {80,443} divert-packet port 989 +pass out quick on em0 inet6 proto tcp to port {80,443} divert-packet port 989 +------------ +pfctl -f /etc/pf.conf +./dvtws --port=989 --dpi-desync=split2 + + +В OpenBSD dvtws все фейки отсылает через divert socket, поскольку эта возможность через raw sockets заблокирована. +Видимо pf автоматически предотвращает повторный заворот diverted фреймов, поэтому проблемы зацикливания нет. + +К сожалению, в PF присутствует "удобная" функция, которая автоматически применяет к правилу divert-packet +обратный трафик. Через divert пойдет все соединение, а не только исходящие пакеты. +Это добавит огромный ненужный overhead по процессингу входящих пакетов в dvtws, который будет наиболее заметен +на скачивании по http/https. Мне не удалось понять как этого избежать. +Поэтому использование фильтр-таблиц крайне рекомендовано ! + +OpenBSD принудительно пересчитывает tcp checksum после divert, поэтому скорее всего +dpi-desync-fooling=badsum у вас не заработает. При использовании этого параметра +dvtws предупредит о возможной проблеме. + +Скрипты из ipset не перезагружают таблицы в PF по умолчанию. +Чтобы они это делали, добавьте параметр в /opt/zapret/config : +LISTS_RELOAD="pfctl -f /etc/pf.conf" +Более новые версии pfctl понимают команду перезагрузить только таблицы : pfctl -Tl -f /etc/pf.conf +Но это не относится к OpenBSD 6.8. В новых FreeBSD есть. +Не забудьте выключить сжатие gzip : +GZIP_LISTS=0 +Если в вашей конфигурации какого-то файла листа нет, то его необходимо исключить из правил PF. +Если вдруг листа нет, и он задан в pf.conf, будет ошибка перезагрузки фаервола. +После настройки обновление листов можно поместить в cron : + crontab -e + дописать строчку : 0 12 */2 * * /opt/zapret/ipset/get_config.sh + +Если будете пользоваться скриптом ipset/get_combined.sh, установите GNU grep : pkg_add ggrep. +Родной древний как мамонт, безумно медленный с опцией -f. + + +MacOS +----- + +Иначально ядро этой ОС "darwin" основывалось на BSD, потому в ней много похожего на другие версии BSD. +Однако, как и в других массовых коммерческих проектах, приоритеты смещаются в сторону от оригинала. +Яблочники что хотят, то и творят. Меняют, убирают, оставляют какие-то безумно старые версии API и утилит. +То, что уже давно везде обновили, может быть еще древним как мамонт в самой последней версии MacOS. +Но кого это волнует ? + +Раньше был ipfw, потом его убрали, заменили на PF. +Есть сомнения, что divert сокеты в ядре остались. Попытка создать divert socket не выдает ошибок, +но полученный сокет ведет себя точно так же, как raw, со всеми его унаследованными косяками + еще яблочно специфическими. +В PF divert-packet не работает. Простой grep бинарика pfctl показывает, что там нет слова "divert", +а в других версиях BSD оно есть. dvtws собирается, но совершенно бесполезен. + +tpws удалось адаптировать, он работоспособен. Получение адреса назначения для прозрачного прокси в PF (DIOCNATLOOK) +убрали из заголовков в новых SDK, сделав фактически недокументированным. +В tpws перенесены некоторые определения из более старых версий яблочных SDK. С ними удалось завести прозрачный режим. +Однако, что будет в следующих версиях угадать сложно. Гарантий нет. +Еще одной особенностью PF в MacOS является проверка на рута в момент обращения к /dev/pf, чего нет в остальных BSD. +tpws по умолчанию сбрасывает рутовые привилегии. Необходимо явно указать параметр --user=root. +В остальном PF себя ведет похоже на FreeBSD. Синтаксис pf.conf тот же. + +На MacOS работает редирект как с проходящего трафика, так и с локальной системы через route-to. +Поскольку tpws вынужден работать под root, для исключения рекурсии приходится пускать исходящий от root трафик напрямую. +Отсюда имеем недостаток : обход DPI для рута работать не будет. + +Если вы пользуетесь MaсOS в качестве ipv6 роутера, то нужно будет решить вопрос с регулярно изменяемым link-local адресом. +С некоторых версий MacOS использует по умолчанию постоянные "secured" ipv6 адреса вместо генерируемых на базе MAC адреса. +Все замечательно, но есть одна проблема. Постоянными остаются только global scope адреса. +Link locals периодически меняются. Смена завязана на системное время. Перезагрузки адрес не меняют, +Но если перевести время на день вперед и перезагрузиться - link local станет другим. (по крайней мере в vmware это так) +Информации по вопросу крайне мало, но тянет на баг. Не должен меняться link local. Скрывать link local не имеет смысла, +а динамический link local нельзя использовать в качестве адреса шлюза. +Проще всего отказаться от "secured" адресов. +Поместите строчку "net.inet6.send.opmode=0" в /etc/sysctl.conf. Затем перезагрузите систему. +Все равно для исходящих соединений будут использоваться temporary адреса, как и в других системах. +Или вам идея не по вкусу, можно прописать дополнительный статический ipv6 из диапазона fd00::/8 - +выберите любой с длиной префикса 128. Это можно сделать в системных настройках, создав дополнительный адаптер на базе +того же сетевого интерфейса, отключить в нем ipv4 и вписать статический ipv6. Он добавится к автоматически настраеваемым. + +Настройка tpws на macos в прозрачном режиме только для исходящих запросов, где en0 - WAN : + +/etc/pf.conf +------------ +rdr pass on lo0 inet proto tcp from !127.0.0.0/8 to any port {80,443} -> 127.0.0.1 port 988 +rdr pass on lo0 inet6 proto tcp from !::1 to any port {80,443} -> fe80::1 port 988 +pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any port {80,443} user { >root } +pass out route-to (lo0 fe80::1) inet6 proto tcp from any to any port {80,443} user { >root } +------------ +pfctl -ef /etc/pf.conf +/opt/zapret/tpws/tpws --user=root --port=988 --bind-addr=127.0.0.1 --bind-iface6=en0 --bind-linklocal=force + + +Настройка tpws на macos роутере в прозрачном режиме, где en0 - WAN, en1 - LAN. + +ifconfig en1 | grep fe80 + inet6 fe80::bbbb:bbbb:bbbb:bbbb%en1 prefixlen 64 scopeid 0x8 +/etc/pf.conf +------------ +rdr pass on en1 inet proto tcp from any to any port {80,443} -> 127.0.0.1 port 988 +rdr pass on en1 inet6 proto tcp from any to any port {80,443} -> fe80::bbbb:bbbb:bbbb:bbbb port 988 +rdr pass on lo0 inet proto tcp from !127.0.0.0/8 to any port {80,443} -> 127.0.0.1 port 988 +rdr pass on lo0 inet6 proto tcp from !::1 to any port {80,443} -> fe80::1 port 988 +pass out route-to (lo0 127.0.0.1) inet proto tcp from any to any port {80,443} user { >root } +pass out route-to (lo0 fe80::1) inet6 proto tcp from any to any port {80,443} user { >root } +------------ +pfctl -ef /etc/pf.conf +/opt/zapret/tpws/tpws --user=root --port=988 --bind-addr=127.0.0.1 --bind-iface6=en0 --bind-linklocal=force --bind-iface6=en1 --bind-linklocal=force + + +Сборка : make -C /opt/zapret mac + +Скрипты получения листов ipset/*.sh работают. +Если будете пользоваться ipset/get_combined.sh, нужно установить gnu grep через brew. +Имеющийся очень старый и безумно медленный с оцией -f. + + +MacOS простая установка +----------------------- + +В MacOS поддерживается install_easy.sh + +В комплекте идут бинарики, собраные под 64-bit с опцией -mmacosx-version-min=10.8. +Они должны работать на всех поддерживаемых версиях macos. +Если вдруг не работают - можно собрать свои. Developer tools ставятся автоматом при запуске make. + +!! Internet sharing средствами системы НЕ ПОДДЕРЖИВАЕТСЯ !! +Поддерживается только роутер, настроенный своими силами через PF. +Если вы вдруг включили шаринг, а потом выключили, то доступ к сайтам может пропасть совсем. +Лечение : pfctl -f /etc/pf.conf +Если вам нужен шаринг интернета, лучше отказаться от прозрачного режима и использовать socks. + +Для автостарта используется launchd (/Library/LaunchDaemons/zapret.plist) +Управляющий скрипт : /opt/zapret/init.d/macos/zapret +Следующие команды работают с tpws и фаерволом одновременно (если INIT_APPLY_FW=1 в config) +/opt/zapret/init.d/macos/zapret start +/opt/zapret/init.d/macos/zapret stop +/opt/zapret/init.d/macos/zapret restart +Работа только с tpws : +/opt/zapret/init.d/macos/zapret start-daemons +/opt/zapret/init.d/macos/zapret stop-daemons +/opt/zapret/init.d/macos/zapret restart-daemons +Работа только с PF : +/opt/zapret/init.d/macos/zapret start-fw +/opt/zapret/init.d/macos/zapret stop-fw +/opt/zapret/init.d/macos/zapret restart-fw +Перезагрузка всех IP таблиц из файлов : +/opt/zapret/init.d/macos/zapret reload-fw-tables + +Инсталятор настраивает LISTS_RELOAD в config, так что скрипты ipset/*.sh автоматически перезагружают IP таблицы в PF. +Автоматически создается cron job на ipset/get_config.sh, по аналогии с openwrt. + +При start-fw скрипт автоматически модицифирует /etc/pf.conf, вставляя туда anchors "zapret". +Модификация расчитана на pf.conf, в котором сохранены дефолтные anchors от apple. +Если у вас измененный pf.conf и модификация не удалась, об этом будет предупреждение. Не игнорируйте его. +В этом случае вам нужно вставить в свой pf.conf (в соответствии с порядком типов правил) : +rdr-anchor "zapret" +anchor "zapret" +При деинсталяции через uninstall_easy.sh модификации pf.conf убираются. + +start-fw создает 3 файла anchors в /etc/pf.anchors : zapret,zapret-v4,zapret-v6. +Последние 2 подключаются из anchor "zapret". +Таблицы nozapret,nozapret6 принадлежат anchor "zapret". +Таблицы zapret,zapret-user - в anchor "zapret-v4". +Таблицы zapret6,zapret6-user - в anchor "zapret-v6". +Если какая-то версия протокола отключена - соответствующий anchor пустой и не упоминается в anchor "zapret". +Таблицы и правила создаются только на те листы, которые фактически есть в директории ipset. diff --git a/docs/bsdfw.txt b/docs/bsdfw.txt new file mode 100644 index 0000000..39c21dc --- /dev/null +++ b/docs/bsdfw.txt @@ -0,0 +1,89 @@ +WAN=em0 LAN=em1 + +FreeBSD IPFW : + +ipfw delete 100 +ipfw add 100 fwd 127.0.0.1,988 tcp from me to any 80,443 proto ip4 xmit em0 not uid daemon +ipfw add 100 fwd ::1,988 tcp from me to any 80,443 proto ip6 xmit em0 not uid daemon +ipfw add 100 fwd 127.0.0.1,988 tcp from any to any 80,443 proto ip4 recv em1 +ipfw add 100 fwd ::1,988 tcp from any to any 80,443 proto ip6 recv em1 + +ipfw delete 100 +ipfw add 100 allow tcp from me to table\(nozapret\) 80,443 +ipfw add 100 fwd 127.0.0.1,988 tcp from me to table\(zapret\) 80,443 proto ip4 xmit em0 not uid daemon +ipfw add 100 fwd ::1,988 tcp from me to table\(zapret\) 80,443 proto ip6 xmit em0 not uid daemon +ipfw add 100 allow tcp from any to table\(nozapret\) 80,443 recv em1 +ipfw add 100 fwd 127.0.0.1,988 tcp from any to any 80,443 proto ip4 recv em1 +ipfw add 100 fwd ::1,988 tcp from any to any 80,443 proto ip6 recv em1 + +/opt/zapret/tpws/tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + + +; Loop avoidance. +; FreeBSD artificially ignores sockarg for ipv6 in the kernel. +; This limitation is coming from the ipv6 early age. Code is still in "testing" state. 10-20 years. Everybody forgot about it. +; dvtws sends ipv6 forged frames using another divert socket (HACK). they can be filtered out using 'diverted'. + + +ipfw delete 100 +ipfw add 100 divert 989 tcp from any to any 80,443 out not diverted not sockarg xmit em0 + +ipfw delete 100 +ipfw add 100 allow tcp from me to table\(nozapret\) 80,443 +ipfw add 100 divert 989 tcp from any to table\(zapret\) 80,443 out not diverted not sockarg xmit em0 + +/opt/zapret/nfq/dvtws --port=989 --debug --dpi-desync=split + + +sample ipfw NAT setup : + +WAN=em0 +LAN=em1 +ipfw -q flush +ipfw -q nat 1 config if $WAN unreg_only reset +ipfw -q add 10 allow ip from any to any via $LAN +ipfw -q add 20 allow ip from any to any via lo0 +ipfw -q add 300 nat 1 ip4 from any to any in recv $WAN +ipfw -q add 301 check-state +ipfw -q add 350 skipto 390 tcp from any to any out xmit $WAN setup keep-state +ipfw -q add 350 skipto 390 udp from any to any out xmit $WAN keep-state +ipfw -q add 360 allow all from any to me in recv $WAN +ipfw -q add 390 nat 1 ip4 from any to any out xmit $WAN +ipfw -q add 10000 allow ip from any to any + +Forwarding : +sysctl net.inet.ip.forwarding=1 +sysctl net.inet6.ip6.forwarding=1 + + +OpenBSD PF : + +; dont know how to rdr-to from local system. doesn't seem to work. only works for routed traffic. + +/etc/pf.conf +pass in quick on em1 inet proto tcp to port {80,443} rdr-to 127.0.0.1 port 988 +pass in quick on em1 inet6 proto tcp to port {80,443} rdr-to ::1 port 988 +pfctl -f /etc/pf.conf +/opt/zapret/tpws/tpws --port=988 --user=daemon --bind-addr=::1 --bind-addr=127.0.0.1 + +; dvtws works both for routed and local + +pass out quick on em0 proto tcp to port {80,443} divert-packet port 989 +pfctl -f /etc/pf.conf +./dvtws --port=989 --dpi-desync=split2 + +; dvtws with table limitations : to zapret,zapret6 but not to nozapret,nozapret6 +; reload tables : pfctl -f /etc/pf.conf +set limit table-entries 2000000 +table file "/opt/zapret/ipset/zapret-ip.txt" +table file "/opt/zapret/ipset/zapret-ip-user.txt" +table file "/opt/zapret/ipset/zapret-ip-exclude.txt" +pass out quick on em0 inet proto tcp to port {80,443} +pass out quick on em0 inet proto tcp to port {80,443} divert-packet port 989 +pass out quick on em0 inet proto tcp to port {80,443} divert-packet port 989 +table file "/opt/zapret/ipset/zapret-ip6.txt" +table file "/opt/zapret/ipset/zapret-ip-user6.txt" +table file "/opt/zapret/ipset/zapret-ip-exclude6.txt" +pass out quick on em0 inet6 proto tcp to port {80,443} +pass out quick on em0 inet6 proto tcp to port {80,443} divert-packet port 989 +pass out quick on em0 inet6 proto tcp to port {80,443} divert-packet port 989 diff --git a/docs/changes.txt b/docs/changes.txt new file mode 100644 index 0000000..3b4d211 --- /dev/null +++ b/docs/changes.txt @@ -0,0 +1,191 @@ +v1 + +Initial release + +v2 + +nfqws : command line options change. now using standard getopt. +nfqws : added options for window size changing and "Host:" case change +ISP support : tested on mns.ru and beeline (corbina) +init scripts : rewritten init scripts for simple choise of ISP +create_ipset : now using 'ipset restore', it works much faster +readme : updated. now using UTF-8 charset. + +v3 + +tpws : added transparent proxy (supports TPROXY and DNAT). + can help when ISP tracks whole HTTP session, not only the beginning +ipset : added zapret-hosts-user.txt which contain user defined host names to be resolved + and added to zapret ip list +ISP support : dom.ru support via TPROXY/DNAT +ISP support : successfully tested sknt.ru on 'domru' configuration + other configs will probably also work, but cannot test +compile : openwrt compile howto + +v4 + +tpws : added ability to insert extra space after http method : "GET /" => "GET /" +ISP support : TKT support + +v5 + +nfqws : ipv6 support in nfqws + +v6 + +ipset : added "get_antizapret.sh" + +v7 + +tpws : added ability to insert "." after Host: name + +v8 + +openwrt init : removed hotplug.d/firewall because of race conditions. now only use /etc/firewall.user + +v9 + +ipban : added ipban ipset. place domains banned by ip to zapret-hosts-user-ipban.txt + these IPs must be soxified for both http and https +ISP support : tiera support +ISP support : added DNS filtering to ubuntu and debian scripts + +v10 + +tpws : added split-pos option. split every message at specified position + +v11 + +ipset : scripts optimizations + +v12 + +nfqws : fix wrong tcp checksum calculation if packet length is odd and platform is big-endian + +v13 + +added binaries + +v14 + +change get_antizapret script to work with https://github.com/zapret-info/z-i/raw/master/dump.csv +filter out 192.168.*, 127.*, 10.* from blocked ips + +v15 + +added --hostspell option to nfqws and tpws +ISP support : beeline now catches "host" but other spellings still work +openwrt/LEDE : changed init script to work with procd +tpws, nfqws : minor cosmetic fixes + +v16 + +tpws: split-http-req=method : split inside method name, not after +ISP support : mns.ru changed split pos to 3 (got redirect page with HEAD req : curl -I ej.ru) + +v17 + +ISP support : athome moved from nfqws to tpws because of instability and http request hangs +tpws : added options unixeol,methodeol,hosttab + +v18 + +tpws,nfqws : added hostnospace option + +v19 + +tpws : added hostlist option + +v20 + +added ip2net. ip2net groups ips from iplist into subnets and reduces ipset size twice + +v21 + +added mdig. get_reestr.sh is *real* again + +v22 + +total review of init script logic +dropped support of older debian 7 and ubuntu 12/14 systems +install_bin.sh : auto binaries preparation +docs: readme review. some new topics added, others deleted +docs: VPN setup with policy based routing using wireguard +docs: wireguard modding guide + +v23 + +major init system rewrite +openwrt : separate firewall include /etc/firewall.zapret +install_easy.sh : easy setup on openwrt, debian, ubuntu, centos, fedora, opensuse + +v24 + +separate config from init scripts +gzip support in ipset/*.sh and tpws + +v25 + +init : move to native systemd units +use links to units, init scripts and firewall includes, no more copying + +v26 + +ipv6 support +tpws : advanced bind options + +v27 + +tpws : major connection code rewrite. originally it was derived from not top quality example , with many bugs and potential problems. +next generation connection code uses nonblocking sockets. now its in EXPERIMENTAL state. + +v28 + +tpws : added socks5 support +ipset : major RKN getlist rewrite. added antifilter.network support + +v29 + +nfqws : DPI desync attack +ip exclude system + +v30 + +nfqws : DPI desync attack modes : fake,rst + +v31 + +nfqws : DPI desync attack modes : disorder,disorder2,split,split2. +nfqws : DPI desync fooling mode : badseq. multiple modes supported + +v32 + +tpws : multiple binds +init scripts : run only one instance of tpws in any case + +v33 + +openwrt : flow offloading support +config : MODE refactoring + +v34 + +nfqws : dpi-desync 2 mode combos +nfqws : dpi-desync without parameter no more supported. previously it meant "fake" +nfqws : custom fake http request and tls client hello + +v35 + +limited FreeBSD and OpenBSD support + +v36 + +full FreeBSD and OpenBSD support + +v37 + +limited MacOS support + +v38 + +MacOS easy install diff --git a/docs/compile/build_howto_openwrt.txt b/docs/compile/build_howto_openwrt.txt new file mode 100644 index 0000000..46a65d8 --- /dev/null +++ b/docs/compile/build_howto_openwrt.txt @@ -0,0 +1,42 @@ +How to compile native programs for use in openwrt +------------------------------------------------- + +1) + + cd ~ + + + git clone git://git.openwrt.org/15.05/openwrt.git + + git clone git://git.openwrt.org/14.07/openwrt.git + + git clone git://git.openwrt.org/openwrt.git + + cd openwrt + +2) ./scripts/feeds update -a + ./scripts/feeds install -a + +3) #add zapret packages to build root + #copy package descriptions + copy compile/openwrt/* to ~/openwrt + #copy source code of tpws + copy tpws to ~/openwrt/package/zapret/tpws + #copy source code of nfq + copy nfq to ~/openwrt/package/zapret/nfq + #copy source code of ip2net + copy ip2net to ~/openwrt/package/zapret/ip2net + +4) make menuconfig + #select your target architecture + #select packages Network/Zapret/* as "M" + +5) make toolchain/compile + +6) make package/tpws/compile + make package/nfqws/compile + make package/ip2net/compile + make package/mdig/compile + +7) find bin -name tpws*.ipk + #take your tpws*.ipk , nfqws*.ipk , ip2net*.ipk, mdig*.ipk from there diff --git a/docs/compile/openwrt/package/zapret/ip2net/Makefile b/docs/compile/openwrt/package/zapret/ip2net/Makefile new file mode 100644 index 0000000..4564675 --- /dev/null +++ b/docs/compile/openwrt/package/zapret/ip2net/Makefile @@ -0,0 +1,32 @@ +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=ip2net +PKG_RELEASE:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/ip2net + SECTION:=net + CATEGORY:=Network + TITLE:=ip2net + SUBMENU:=Zapret +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./ip2net/* $(PKG_BUILD_DIR)/ +endef + +define Build/Compile + $(MAKE) -C $(PKG_BUILD_DIR) $(TARGET_CONFIGURE_OPTS) +endef + +define Package/ip2net/install + $(INSTALL_DIR) $(1)/opt/zapret/ip2net + $(INSTALL_BIN) $(PKG_BUILD_DIR)/ip2net $(1)/opt/zapret/ip2net +endef + +$(eval $(call BuildPackage,ip2net)) + diff --git a/docs/compile/openwrt/package/zapret/ip2net/readme.txt b/docs/compile/openwrt/package/zapret/ip2net/readme.txt new file mode 100644 index 0000000..abf7acd --- /dev/null +++ b/docs/compile/openwrt/package/zapret/ip2net/readme.txt @@ -0,0 +1 @@ +Copy "ip2net" folder here ! diff --git a/docs/compile/openwrt/package/zapret/mdig/Makefile b/docs/compile/openwrt/package/zapret/mdig/Makefile new file mode 100644 index 0000000..55d55d2 --- /dev/null +++ b/docs/compile/openwrt/package/zapret/mdig/Makefile @@ -0,0 +1,32 @@ +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=mdig +PKG_RELEASE:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/mdig + SECTION:=net + CATEGORY:=Network + TITLE:=mdig + SUBMENU:=Zapret +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./mdig/* $(PKG_BUILD_DIR)/ +endef + +define Build/Compile + $(MAKE) -C $(PKG_BUILD_DIR) $(TARGET_CONFIGURE_OPTS) +endef + +define Package/mdig/install + $(INSTALL_DIR) $(1)/opt/zapret/mdig + $(INSTALL_BIN) $(PKG_BUILD_DIR)/mdig $(1)/opt/zapret/mdig +endef + +$(eval $(call BuildPackage,mdig)) + diff --git a/docs/compile/openwrt/package/zapret/mdig/readme.txt b/docs/compile/openwrt/package/zapret/mdig/readme.txt new file mode 100644 index 0000000..14e5c14 --- /dev/null +++ b/docs/compile/openwrt/package/zapret/mdig/readme.txt @@ -0,0 +1 @@ +Copy "mdig" folder here ! diff --git a/docs/compile/openwrt/package/zapret/nfqws/Makefile b/docs/compile/openwrt/package/zapret/nfqws/Makefile new file mode 100644 index 0000000..48b562f --- /dev/null +++ b/docs/compile/openwrt/package/zapret/nfqws/Makefile @@ -0,0 +1,34 @@ +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=nfqws +PKG_RELEASE:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/nfqws + SECTION:=net + CATEGORY:=Network + TITLE:=nfqws + SUBMENU:=Zapret + DEPENDS:=+libnetfilter-queue +libcap +zlib +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./nfq/* $(PKG_BUILD_DIR)/ +endef + +define Build/Compile + $(MAKE) -C $(PKG_BUILD_DIR) $(TARGET_CONFIGURE_OPTS) +endef + +define Package/nfqws/install + $(INSTALL_DIR) $(1)/opt/zapret/nfq + $(INSTALL_BIN) $(PKG_BUILD_DIR)/nfqws $(1)/opt/zapret/nfq +endef + +$(eval $(call BuildPackage,nfqws)) + + diff --git a/docs/compile/openwrt/package/zapret/nfqws/readme.txt b/docs/compile/openwrt/package/zapret/nfqws/readme.txt new file mode 100644 index 0000000..daf8b84 --- /dev/null +++ b/docs/compile/openwrt/package/zapret/nfqws/readme.txt @@ -0,0 +1 @@ +Copy "nfq" folder here ! diff --git a/docs/compile/openwrt/package/zapret/tpws/Makefile b/docs/compile/openwrt/package/zapret/tpws/Makefile new file mode 100644 index 0000000..3f8dfc7 --- /dev/null +++ b/docs/compile/openwrt/package/zapret/tpws/Makefile @@ -0,0 +1,33 @@ +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=tpws +PKG_RELEASE:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/tpws + SECTION:=net + CATEGORY:=Network + TITLE:=tpws + SUBMENU:=Zapret + DEPENDS:=+zlib +libcap +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./tpws/* $(PKG_BUILD_DIR)/ +endef + +define Build/Compile + $(MAKE) -C $(PKG_BUILD_DIR) $(TARGET_CONFIGURE_OPTS) +endef + +define Package/tpws/install + $(INSTALL_DIR) $(1)/opt/zapret/tpws + $(INSTALL_BIN) $(PKG_BUILD_DIR)/tpws $(1)/opt/zapret/tpws +endef + +$(eval $(call BuildPackage,tpws)) + diff --git a/docs/compile/openwrt/package/zapret/tpws/readme.txt b/docs/compile/openwrt/package/zapret/tpws/readme.txt new file mode 100644 index 0000000..18fa3ed --- /dev/null +++ b/docs/compile/openwrt/package/zapret/tpws/readme.txt @@ -0,0 +1 @@ +Copy "tpws" folder here ! diff --git a/docs/https.txt b/docs/https.txt new file mode 100644 index 0000000..53a280d --- /dev/null +++ b/docs/https.txt @@ -0,0 +1,159 @@ +Расскажу как я решал вопрос с блокировкой https на роутере. +На тех провайдерах, что мне доступны, все, кроме одного либо банили https по IP (вообще нет конекта), либо захватывали TLS сессию и она намертво зависала - пакеты больше не приходили. На домру удалось выяснить, что DPI цепляется к SNI (Server Name Indication) в TLS, но сплит TLS запроса не помог. Я пришел к выводу, что https самым разумным будет прозрачно заворачивать в socks. +Tor поддерживает "из коробки" режим transparent proxy. Это можно использовать в теории, но практически - только на роутерах с 128 мб памяти и выше. Таких роутеров не так много. В основном объем памяти 32 или 64 мб. И тор еще и тормозной. +Другой вариант напрашивается, если у вас есть доступ к какой-нибудь unix системе с SSH, где сайты не блокируются. Например, у вас есть VPS вне России. Именно так и поступил. +Понятийно требуются следующие шаги : +1) Выделять IP, на которые надо проксировать трафик. У нас уже имеется ipset "zapret", технология создания которого отработана. +2) Сделать так, чтобы все время при загрузке системы на некотором порту возникал socks. +3) Установить transparent соксификатор. Redsocks прекрасно подошел на эту роль. +4) Завернуть через iptables трафик с порта назначения 443 и на ip адреса из ipset 'zapret' на соксификатор +Буду рассматривать систему на базе openwrt, где уже установлена система обхода dpi "zapret". +По крайней мере нужно иметь заполненный ipset 'zapret', устанавливать tpws или nfqws не обязательно. +Более того, если они на вашей системе не срабатывают, то можно соксифицировать не только https, но и http. + +* Сделать так, чтобы все время при загрузке системы на некотором порту возникал socks + +Т.к. дефолтный dropbear клиент не поддерживает создание socks, то для начала придется заменить dropbear ssh client на openssh : пакеты openssh-client и openssh-client-utils. +Устанавливать их нужно с опцией opkg --force-overwrite, поскольку они перепишут ssh клиент от dropbear. +После установки пакетов расслабим неоправданно жестокие права : chmod 755 /etc/ssh. +Следует создать пользователя, под которым будем крутить ssh client. Допустим, это будет 'proxy'. +Сначала установить пакет shadow-useradd. +------------------ +useradd -d /home/proxy proxy +mkdir -p /home/proxy +chown proxy:proxy /home/proxy +------------------ +Openssh ловит разные глюки, если у него нет доступа к /dev/tty. +Добавим в /etc/rc.local строчку : "chmod 666 /dev/tty" +Сгенерируем для него ключ RSA для доступа к ssh серверу. +------------------ +su proxy +cd +mkdir -m 700 .ssh +cd .ssh +ssh-keygen +ls +exit +------------------ +Должны получиться файлы id_rsa и id_rsa.pub. +Строчку из id_rsa.pub следует добавить на ssh сервер в файл $HOME/.ssh/authorized_keys. +Более подробно о доступе к ssh через авторизацию по ключам : https://beget.com/ru/articles/ssh_by_key +Предположим, ваш ssh сервер - vps.mydomain.com, пользователь называется 'proxy'. +Проверить подключение можно так : ssh -N -D 1098 -l proxy vps.mydomain.com. +Сделайте это под пользователем "proxy", поскольку при первом подключении ssh спросит о правильности hostkey. +Соединение может отвалиться в любой момент, поэтому нужно зациклить запуск ssh. +Для этого лучший вариант - использовать procd - упрощенная замена systemd на openwrt версий BB и выше. +--- /etc/init.d/socks_vps --- +#!/bin/sh /etc/rc.common +START=50 +STOP=50 +USE_PROCD=1 +USERNAME=proxy +COMMAND="ssh -N -D 1098 -l proxy vps.mydomain.com" +start_service() { + procd_open_instance + procd_set_param user $USERNAME + procd_set_param respawn 10 10 0 + procd_set_param command $COMMAND + procd_close_instance +} +----------------------------- +Этому файлу нужно дать права : chmod +x /etc/init.d/socks_vps +Запуск : /etc/init.d/socks_vps start +Останов : /etc/init.d/socks_vps stop +Включить автозагрузку : /etc/init.d/socks_vps enable +Проверка : curl -4 --socks5 127.0.0.1:1098 https://rutracker.org + +* Организовать прозрачную соксификацию + +Установить пакет redsocks. +Конфиг : +-- /etc/redsocks.conf : --- +base { + log_debug = off; + log_info = on; + log = "syslog:local7"; + daemon = on; + user = nobody; + group = nogroup; + redirector = iptables; +} +redsocks { + local_ip = 127.0.0.1; + local_port = 1099; + ip = 127.0.0.1; + port = 1098; + type = socks5; +} +--------------------------- +После чего перезапускаем : /etc/init.d/redsocks restart +Смотрим появился ли листенер : netstat -tnlp | grep 1099 +Автостарт redsocks при таком конфиге не работает, потому что на момент запуска сеть не инициализирована, и у нас даже нет 127.0.0.1. +Вместо штатного автостарта будем вешаться на события поднятия интерфейса. Разберем это позже. +Пока что отключим автостарт : /etc/init.d/redsocks disable + +* Завертывание соединений через iptables + +Будем завертывать любые tcp соединения на ip из ipset "ipban" и https на ip из ipset "zapret". + +--- /etc/firewall.user ----- +SOXIFIER_PORT=1099 + +. /opt/zapret/init.d/openwrt/functions + +create_ipset no-update + +network_find_wan_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 -j REDIRECT --to-port $SOXIFIER_PORT + ipt OUTPUT -t nat -o $ext_device -p tcp -m set --match-set ipban dst -j REDIRECT --to-port $SOXIFIER_PORT +done + +network_get_device DEVICE lan +sysctl -w net.ipv4.conf.$DEVICE.route_localnet=1 +ipt prerouting_lan_rule -t nat -p tcp --dport 443 -m set --match-set zapret dst -j DNAT --to 127.0.0.1:$SOXIFIER_PORT +ipt prerouting_lan_rule -t nat -p tcp -m set --match-set ipban dst -j DNAT --to 127.0.0.1:$SOXIFIER_PORT +---------------------------- + +Внести параметр "reload" в указанное место : +--- /etc/config/firewall --- +config include + option path '/etc/firewall.user' + option reload '1' +---------------------------- + +Перезапуск : /etc/init.d/firewall restart +Все, теперь можно проверять : +/etc/init.d/redsocks stop +curl -4 https://rutracker.org +# должно обломаться с надписью "Connection refused". если не обламывается - значит ip адрес rutracker.org не в ipset, +# либо не сработали правила фаервола. например, из-за не установленных модулей ipt +/etc/init.d/redsocks start +curl -4 https://rutracker.org +# должно выдать страницу + +* Автозапуск redsocks + +Я сделал для себя небольшой скриптик, вешающийся на события поднятия и опускания интерфейсов. + +--- /etc/hotplug.d/iface/99-exec-on-updown --- +#!/bin/sh +if [ "$ACTION" = ifup ]; then +cmd=$(uci get network.$INTERFACE.exec_on_up) +[ -n "$cmd" ] && $cmd +fi +if [ "$ACTION" = ifdown ]; then +cmd=$(uci get network.$INTERFACE.exec_on_down) +[ -n "$cmd" ] && $cmd +fi +---------------------------------------------- + +Теперь можно в описания интерфейсов внести в соответствующий раздел : +--- /etc/config/nework --- +config interface 'wan' + ........ + option exec_on_up '/etc/init.d/redsocks start' +-------------------------- +reboot. Заходим снова, смотрим, что есть redsocks, есть ssh, опять проверяем curl -4 https://rutracker.org. +Пробуем зайти на https://rutracker.org с компа внутри локалки. diff --git a/docs/iptables.txt b/docs/iptables.txt new file mode 100644 index 0000000..51b806c --- /dev/null +++ b/docs/iptables.txt @@ -0,0 +1,62 @@ +For window size changing : + +iptables -t mangle -I PREROUTING -p tcp --sport 80 --tcp-flags SYN,ACK SYN,ACK -j NFQUEUE --queue-num 200 --queue-bypass +iptables -t mangle -I PREROUTING -p tcp --sport 80 --tcp-flags SYN,ACK SYN,ACK -m set --match-set zapret src -j NFQUEUE --queue-num 200 --queue-bypass + +For outgoing data manipulation ("Host:" case changing) : + +iptables -t mangle -I POSTROUTING -p tcp --dport 80 -j NFQUEUE --queue-num 200 --queue-bypass +iptables -t mangle -I POSTROUTING -p tcp --dport 80 -m set --match-set zapret dst -j NFQUEUE --queue-num 200 --queue-bypass +iptables -t mangle -I POSTROUTING -p tcp --dport 80 -m set --match-set zapret dst -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 1:5 -j NFQUEUE --queue-num 200 --queue-bypass + +For dpi desync attack : + +iptables -t mangle -I POSTROUTING -p tcp -m multiport --dports 80,443 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass + + +For TPROXY : + +sysctl -w net.ipv4.ip_forward=1 +iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE + +ip -f inet rule add fwmark 1 lookup 100 +ip -f inet route add local default dev lo table 100 +# prevent loop +iptables -t filter -I INPUT -p tcp --dport 988 -j REJECT +iptables -t mangle -A PREROUTING -i eth1 -p tcp --dport 80 -j MARK --set-mark 1 +iptables -t mangle -A PREROUTING -i eth1 -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 988 + +iptables -t mangle -A PREROUTING -i eth1 -p tcp --dport 80 -m set --match-set zapret dst -j MARK --set-mark 1 +iptables -t mangle -A PREROUTING -i eth1 -p tcp --dport 80 -m mark --mark 0x1/0x1 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 988 + +For DNAT : + +# run tpws as user "tpws". its required to avoid loops. +sysctl -w net.ipv4.conf.eth1.route_localnet=1 +iptables -t nat -I PREROUTING -p tcp --dport 80 -j DNAT --to 127.0.0.127:988 +iptables -t nat -I OUTPUT -p tcp --dport 80 -m owner ! --uid-owner tpws -j DNAT --to 127.0.0.127:988 + + +Reset all iptable rules : + +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F +iptables -t mangle -X +iptables -t raw -F +iptables -t raw -X + +Reset iptable policies : + +iptables -P INPUT ACCEPT +iptables -P FORWARD ACCEPT +iptables -P OUTPUT ACCEPT +iptables -t mangle -P POSTROUTING ACCEPT +iptables -t mangle -P PREROUTING ACCEPT +iptables -t mangle -P INPUT ACCEPT +iptables -t mangle -P FORWARD ACCEPT +iptables -t mangle -P OUTPUT ACCEPT +iptables -t raw -P PREROUTING ACCEPT +iptables -t raw -P OUTPUT ACCEPT diff --git a/docs/readme.eng.txt b/docs/readme.eng.txt new file mode 100644 index 0000000..ef310b0 --- /dev/null +++ b/docs/readme.eng.txt @@ -0,0 +1,643 @@ +What is it for +-------------- + +Bypass the blocking of http/https web sites on DPI without the use of third-party servers. + +The project is mainly aimed at the Russian audience to fight russian regulator named "Roskomnadzor". +Some features of the project are russian reality specific (such as getting list of sites +blocked by Roskomnadzor), but most others are common. + +(EXPERIMENTAL) FreeBSD and OpenBSD are also supported. +(EXPERIMENTAL, PARTIAL) MacOS limited support. +see docs/bsd.eng.txt + +How it works +------------ + +In the simplest case you are dealing with passive DPI. Passive DPI can read passthrough traffic, +inject its own packets, but cannot drop packets. +If the request is prohibited the passive DPI will inject its own RST packet and optionally http redirect packet. +If fake packets from DPI are only sent to client, you can use iptables commands to drop them if you can write +correct filter rules. This requires manual in-deep traffic analysis and tuning for specific ISP. +This is how we bypass the consequences of a ban trigger. + +If the passive DPI sends an RST packet also to the server, there is nothing you can do about it. +Your task is to prevent ban trigger from firing up. Iptables alone will not work. +This project is aimed at preventing the ban rather than eliminating its consequences. + +To do that send what DPI does not expect and what breaks its algorithm of recognizing requests and blocking them. + +Some DPIs cannot recognize the http request if it is divided into TCP segments. +For example, a request of the form "GET / HTTP / 1.1 \ r \ nHost: kinozal.tv ......" +we send in 2 parts: first go "GET", then "/ HTTP / 1.1 \ r \ nHost: kinozal.tv .....". +Other DPIs stumble when the "Host:" header is written in another case: for example, "host:". +Sometimes work adding extra space after the method: "GET /" => "GET /" +or adding a dot at the end of the host name: "Host: kinozal.tv." + +There is also more advanced magic for bypassing DPI at the packet level. + + +How to put this into practice in the linux system +------------------------------------------------- + +In short, the options can be classified according to the following scheme: + +1) Passive DPI not sending RST to the server. ISP tuned iptables commands can help. +This option is out of the scope of the project. If you do not allow ban trigger to fire, then you won’t have to +deal with its consequences. +2) Modification of the TCP connection at the stream level. Implemented through a proxy or transparent proxy. +3) Modification of TCP connection at the packet level. Implemented through the NFQUEUE handler and raw sockets. + +For options 2 and 3, tpws and nfqws programs are implemented, respectively. +You need to run them with the necessary parameters and redirect certain traffic with iptables. + +To redirect a TCP connection to a transparent proxy, the following commands are used: + +forwarded fraffic : +iptables -t nat -I PREROUTING -i -p tcp --dport 80 -j DNAT --to 127.0.0.127:988 +outgoing traffic : +iptables -t nat -I OUTPUT -o -p tcp --dport 80 -m owner ! --uid-owner tpws -j DNAT --to 127.0.0.127:988 + +DNAT on localhost works in the OUTPUT chain, but does not work in the PREROUTING chain without enabling the route_localnet parameter: + +sysctl -w net.ipv4.conf..route_localnet=1 + +You can use "-j REDIRECT --to-port 988" instead of DNAT, but in this case the transparent proxy process +should listen on the ip address of the incoming interface or on all addresses. Listen all - not good +in terms of security. Listening one (local) is possible, but automated scripts will have to recognize it, +then dynamically enter it into the command. In any case, additional efforts are required. +Using route_localnet can also introduce some security risks. You make available from internal_interface everything +bound to 127.0.0.0/8. Services are usually bound to 127.0.0.1. Its possible to deny input to 127.0.0.1 from all interfaces except lo +or bind tpws to any other IP from 127.0.0.0/8 range, for example to 127.0.0.127, and allow incomings only to that IP : + +iptables -A INPUT ! -i lo -d 127.0.0.127 -j ACCEPT +iptables -A INPUT ! -i lo -d 127.0.0.0/8 -j DROP + +Owner filter is necessary to prevent recursive redirection of connections from tpws itself. +tpws must be started under OS user "tpws". + + +NFQUEUE redirection of the outgoing traffic and forwarded traffic going towards the external interface, +can be done with the following commands: + +iptables -t mangle -I POSTROUTING -o -p tcp --dport 80 -j NFQUEUE --queue-num 200 --queue-bypass + +In order not to touch the traffic to unblocked addresses, you can take a list of blocked hosts, resolve it +into IP addresses and put them to ipset 'zapret', then add a filter to the command: + +iptables -t mangle -I POSTROUTING -o -p tcp --dport 80 -m set --match-set zapret dst -j NFQUEUE --queue-num 200 --queue-bypass + +Some DPIs catch only the first http request, ignoring subsequent requests in a keep-alive session. +Then we can reduce CPU load, refusing to process unnecessary packets. + +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp --dport 80 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -m set --match-set zapret dst -j NFQUEUE --queue-num 200 --queue-bypass + +Mark filter does not allow nfqws-generated packets to enter the queue again. +Its necessary to use this filter when also using "connbytes 2:4". Without it packet ordering can be changed breaking the whole idea. + + +ip6tables +--------- + +ip6tables work almost exactly the same way as ipv4, but there are a number of important nuances. +In DNAT, you should take the address --to in square brackets. For example : + + ip6tables -t nat -I OUTPUT -o -p tcp --dport 80 -m owner ! --uid-owner tpws -j DNAT --to [::1]:988 + +The route_localnet parameter does not exist for ipv6. +DNAT to localhost (:: 1) is possible only in the OUTPUT chain. +In the PREROUTING DNAT chain, it is possible to any global address or to the link local address of the same interface +the packet came from. +NFQUEUE works without changes. + +When it will not work +---------------------- + +* If DNS server returns false responses. ISP can return false IP addresses or not return anything +when blocked domains are queried. If this is the case change DNS to public ones, such as 8.8.8.8 or 1.1.1.1. +Sometimes ISP hijacks queries to any DNS server. Dnscrypt or dns-over-tls help. +* If blocking is done by IP. +* If a connection passes through a filter capable of reconstructing a TCP connection, and which +follows all standards. For example, we are routed to squid. Connection goes through the full OS tcpip stack, +fragmentation disappears immediately as a means of circumvention. Squid is correct, it will find everything +as it should, it is useless to deceive him. +BUT. Only small providers can afford using squid, since it is very resource intensive. +Large companies usually use DPI, which is designed for much greater bandwidth. + +nfqws +----- + +This program is a packet modifier and a NFQUEUE queue handler. +For BSD systems there is dvtws. Its built from the same source and has almost the same parameters (see bsd.eng.txt). +nfqws takes the following parameters: + + --debug=0|1 ; 1=print debug info + --qnum= + --wsize= ; set window size. 0 = do not modify (obsolete !) + --hostcase ; change Host: => host: + --hostspell=HoSt ; exact spelling of the "Host" header. must be 4 chars. default is "host" + --hostnospace ; remove space after Host: and add it to User-Agent: to preserve packet size + --domcase ; mix domain case after Host: like this : TeSt.cOm + --daemon ; daemonize + --pidfile= ; write pid to file + --user= ; drop root privs + --uid=uid[:gid] ; drop root privs + --dpi-desync[=][,] ; try to desync dpi state. modes : fake rst rstack disorder disorder2 split split2 + --dpi-desync-fwmark= ; override fwmark for desync packet. default = 0x40000000 + --dpi-desync-ttl= ; set ttl for desync packet + --dpi-desync-fooling=none|md5sig|ts|badseq|badsum ; can take multiple comma separated values + --dpi-desync-retrans=0|1 ; (fake,rst,rstack only) 0(default)=reinject original data packet after fake 1=drop original data packet to force its retransmission + --dpi-desync-repeats= ; send every desync packet N times + --dpi-desync-skip-nosni=0|1 ; 1(default)=do not apply desync to requests without hostname in the SNI + --dpi-desync-split-pos=<1..1500> ; (for split* and disorder* only) split TCP packet at specified position + --dpi-desync-any-protocol=0|1 ; 0(default)=desync only http and tls 1=desync any nonempty data packet + --dpi-desync-fake-http= ; file containing fake http request. replacement for built-in + --dpi-desync-fake-tls= ; file containing fake TLS ClientHello (for https). replacement for built-in + --hostlist= ; apply fooling only to the listed hosts (one host per line, subdomains auto apply) + +The manipulation parameters can be combined in any way. + +WARNING. --wsize parameter is now not used anymore in scripts. TCP split can be achieved using DPI desync attack. + +DPI DESYNC ATTACK +After completion of the tcp 3-way handshake, the first data packet from the client goes. +It usually has "GET / ..." or TLS ClientHello. We drop this packet, replacing with something else. +It can be a fake version with another harmless but valid http or https request (fake), tcp reset packet (rst,rstack), +split into 2 segments original packet with fake segment in the middle (disorder). +In articles these attack have names "TCB desynchronization" and "TCB teardown". +Fake packet must reach DPI, but do not reach the destination server. +The following means are available: set a low TTL, send a packet with bad checksum, +add tcp option "MD5 signature". All of them have their own disadvantages : + +* md5sig does not work on all servers +* badsum doesn't work if your device is behind NAT which does not pass invalid packets. + Linux NAT by default does not pass them without special setting "sysctl -w net.netfilter.nf_conntrack_checksum=0" + Openwrt sets it from the box, other routers in most cases dont, and its not always possible to change it. + If nfqws is on the router, its not neccessary to switch of "net.netfilter.nf_conntrack_checksum". + Fake packet doesn't go through FORWARD chain, it goes through OUTPUT. But if your router is behind another NAT, for example ISP NAT, + and that NAT does not pass invalid packets, you cant do anything. +* badseq packets will be dropped by server, but DPI also can ignore them +* TTL looks like the best option, but it requires special tuning for earch ISP. If DPI is further than local ISP websites + you can cut access to them. Manual IP exclude list is required. Its possible to use md5sig with ttl. + This way you cant hurt anything, but good chances it will help to open local ISP websites. + If automatic solution cannot be found then use zapret-hosts-user-exclude.txt. + +--dpi-desync-fooling takes multiple comma separated values. + +For fake,rst,rstack modes original packet can be sent after the fake one or just dropped. +If its dropped OS will perform first retransmission after 0.2 sec, then the delay increases exponentially. +Delay can help to make sure fake and original packets are properly ordered and processed on DPI. +When dpi-desync-retrans=1 its mandatory to use connbytes in iptables rule. Otherwise loop happens. + +Disorder mode splits original packet and sends packets in the following order : +1. 2nd segment +2. fake 1st segment, data filled with zeroes +3. 1st segment +4. fake 1st segment, data filled with zeroes (2nd copy) +Original packet is always dropped. --dpi-desync-split-pos sets split position (default 3). +If position is higher than packet length, pos=1 is used. +This sequence is designed to make reconstruction of critical message as difficult as possible. +Fake segments may not be required to bypass some DPIs, but can potentially help if more sophisticated reconstruction +algorithms are used. +Mode 'disorder2' disables sending of fake segments. + +Split mode is very similar to disorder but without segment reordering : +1. fake 1st segment, data filled with zeroes +2. 1st segment +3. fake 1st segment, data filled with zeroes (2nd copy) +4. 2nd segment +Mode 'split2' disables sending of fake segments. It can be used as a faster alternative to --wsize. + +In disorder2 and split2 modes no fake packets are sent, so ttl and fooling options are not required. + +There are DPIs that analyze responses from the server, particularly the certificate from the ServerHello +that contain domain name(s). The ClientHello delivery confirmation is an ACK packet from the server +with ACK sequence number corresponding to the length of the ClientHello+1. +In the disorder variant, a selective acknowledgement (SACK) usually arrives first, then a full ACK. +If, instead of ACK or SACK, there is an RST packet with minimal delay, DPI cuts you off at the request stage. +If the RST is after a full ACK after a delay of about ping to the server, then probably DPI acts +on the server response. The DPI may be satisfied with good ClientHello and stop monitoring the TCP session +without checking ServerHello. Then you were lucky. 'fake' option could work. +If it does not stop monitoring and persistently checks the ServerHello, also performing reconstruction of TCP segments, +doing something about it is hardly possible without the help of the server. +The best solution is to enable TLS 1.3 support on the server. TLS 1.3 sends the server certificate in encrypted form. +This is recommendation to all admins of blocked sites. Enable TLS 1.3. You will give more opportunities to overcome DPI. + +Hosts are extracted from plain http request Host: header and SNI of ClientHelllo TLS message. +Subdomains are applied automatically. gzip lists are supported. + +iptables for performing the attack on the first packet : + +iptables -t mangle -I POSTROUTING -o -p tcp -m multiport --dports 80,443 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass + +This is good if DPI does not track all requests in http keep-alive session. +If it does, then pass all outgoing packets for http and only first data packet for https : + +iptables -t mangle -I POSTROUTING -o -p tcp --dport 443 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass +iptables -t mangle -I POSTROUTING -o -p tcp --dport 80 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass + +mark is needed to keep away generated packets from NFQUEUE. nfqws sets fwmark when it sends generated packets. +nfqws can internally filter marked packets. but when connbytes filter is used without mark filter +packet ordering can be changed breaking the whole idea of desync attack. + +DESYNC COMBOS +dpi-desync parameter can take 2 comma separated arguments. +1st phase mode can be fake,rst,rstack, 2nd phase mode - disorder,disorder2,split,split2. +Can be useful for ISPs with more than one DPI. + +VIRTUAL MACHINES +Most of nfqws packet magic does not work from VMs powered by virtualbox and vmware when network is NATed. +Hypervisor forcibly changes ttl and does not forward fake packets. +Set up bridge networking. + + +tpws +----- + +tpws is transparent proxy. + + --debug=0|1|2 ; 0(default)=silent 1=verbose 2=debug + --bind-addr=|; for v6 link locals append %interface_name : fe80::1%br-lan + --bind-iface4= ; bind to the first ipv4 addr of interface + --bind-iface6= ; bind to the first ipv6 addr of interface + --bind-linklocal=prefer|force ; prefer or force ipv6 link local + --bind-wait-ifup= ; wait for interface to appear and up + --bind-wait-ip= ; after ifup wait for ip address to appear up to N seconds + --bind-wait-ip-linklocal= ; accept only link locals first N seconds then any + --bind-wait-only ; wait for bind conditions satisfaction then exit. return code 0 if success. + --port= ; port number to listen on + --socks ; implement socks4/5 proxy instead of transparent proxy + --local-rcvbuf= ; SO_RCVBUF for local legs + --local-sndbuf= ; SO_SNDBUF for local legs + --remote-rcvbuf= ; SO_RCVBUF for remote legs + --remote-sndbuf= ; SO_SNDBUF for remote legs + --skip-nodelay ; do not set TCP_NODELAY for outgoing connections. incompatible with split. + --no-resolve ; disable socks5 remote dns + --maxconn= ; max number of local legs + --maxfiles= ; max file descriptors (setrlimit). min requirement is (X*connections+16), where X=6 in tcp proxy mode, X=4 in tampering mode. + ; its worth to make a reserve with 1.5 multiplier. by default maxfiles is (X*connections)*1.5+16 + --max-orphan-time= ; if local leg sends something and closes and remote leg is still connecting then cancel connection attempt after N seconds + + --hostlist= ; only act on host in the list (one host per line, subdomains auto apply, gzip lists supported) + --split-http-req=method|host ; split http request at specified logical position. + --split-pos= ; split at specified pos. split-http-req takes precedence over split-pos for http reqs. + --split-any-protocol ; split not only http and https + --hostcase ; change Host: => host: + --hostspell ; exact spelling of "Host" header. must be 4 chars. default is "host" + --hostdot ; add "." after Host: name + --hosttab ; add tab after Host: name + --hostnospace ; remove space after Host: + --hostpad= ; add dummy padding headers before Host: + --domcase ; mix domain case after Host: like this : TeSt.cOm + --methodspace ; add extra space after method + --methodeol ; add end-of-line before method + --unixeol ; replace 0D0A to 0A + --daemon ; daemonize + --pidfile= ; write pid to file + --user= ; drop root privs + --uid=uid[:gid] ; drop root privs + +The manipulation parameters can be combined in any way. + +split-http-req takes precedence over split-pos for http reqs. +split-pos works by default only on http and TLS ClientHello. use --split-any-protocol to act on any packet + +tpws can bind to multiple interfaces and IP addresses (up to 32). +Port number is always the same. +Parameters --bind-iface* и --bind-addr create new bind. +Other parameters --bind-* are related to the last bind. +To bind to all ipv4 specify --bind-addr "0.0.0.0", all ipv6 - "::". --bind-addr="" - mean bind to all ipv4 and ipv6. +If no binds are specified default bind to all ipv4 and ipv6 addresses is created. +The --bind-wait* parameters can help in situations where you need to get IP from the interface, but it is not there yet, it is not raised +or not configured. +In different systems, ifup events are caught in different ways and do not guarantee that the interface has already received an IP address of a certain type. +In the general case, there is no single mechanism to hang oneself on an event of the type "link local address appeared on the X interface." + +in socks proxy mode no additional system privileges are required +connection to local IPs of the system where tpws runs are prohibited +tpws supports remote dns resolving (curl : --socks5-hostname firefox : socks_remote_dns=true) , but does it in blocking mode. +tpws uses async sockets for all activity but resolving can break this model. +if tpws serves many clients it can cause trouble. also DoS attack is possible against tpws. +if remote resolving causes trouble configure clients to use local name resolution and use +--no-resolve option on tpws side. + +Ways to get a list of blocked IP +-------------------------------- + +1) Enter the blocked domains to ipset/zapret-hosts-user.txt and run ipset/get_user.sh +At the output, you get ipset/zapret-ip-user.txt with IP addresses. + +2) ipset/get_reestr_*.sh. Russian specific + +3) ipset/get_antifilter_*.sh. Russian specific + +4) ipset/get_config.sh. This script calls what is written into the GETLIST variable from the config file. +If the variable is not defined, then only lists for ipsets nozapret/nozapret6 are resolved. + +So, if you're not russian, the only way for you is to manually add blocked domains. +Or write your own ipset/get_iran_blocklist.sh , if you know where to download this one. + +On routers, it is not recommended to call these scripts more than once in 2 days to minimize flash memory writes. + +ipset/create_ipset.sh executes forced ipset update. +With "no-update" parameter create_ipset.sh creates ipset but populate it only if it was actually created. +It's useful when multiple subsequent calls are possible to avoid wasting of cpu time redoing the same job. +Ipset loading is resource consuming. Its a good idea to call create_ipset without "no-update" parameter +only once a several days. Use it with "no-update" option in other cases. + +ipset scripts automatically call ip2net utility. +ip2net helps to reduce ip list size by combining IPs to subnets. Also it cuts invalid IPs from the list. +Stored lists are already processed by ip2net. They are error free and ready for loading. + +create_ipset.sh supports loading ip lists from gzip files. First it looks for the filename with the ".gz" extension, +such as "zapret-ip.txt.gz", if not found it falls back to the original name "zapret-ip.txt". +So your own get_iran_blockslist.sh can use "zz" function to produce gz. Study how other russian get_XXX.sh work. +Gzipping helps saving a lot of precious flash space on embedded systems. +User lists are not gzipped because they are not expected to be very large. + +You can add a list of domains to ipset/zapret-hosts-user-ipban.txt. Their ip addresses will be placed +in a separate ipset "ipban". It can be used to route connections to transparent proxy "redsocks" or VPN. + +IPV6: if ipv6 is enabled, then additional txt's are created with the same name, but with a "6" at the end before the extension. +zapret-ip.txt => zapret-ip6.txt +The ipsets zapret6 and ipban6 are created. + +IP EXCLUSION SYSTEM. All scripts resolve zapret-hosts-user-exclude.txt file, creating zapret-ip-exclude.txt and zapret-ip-exclude6.txt. +They are the source for ipsets nozapret/nozapret6. All rules created by init scripts are created with these ipsets in mind. +The IPs placed in them are not involved in the process. +zapret-hosts-user-exclude.txt can contain domains, ipv4 and ipv6 addresses or subnets. + +FreeBSD. ipset/*.sh scripts also work in FreeBSD. Instead of ipset they create ipfw lookup tables with the same names as in Linux. +ipfw tables can store both ipv4 and ipv6 addresses and subnets. There's no 4 and 6 separation. + +LISTS_RELOAD config parameter defines a custom lists reloading command. +Its useful on BSD systems with PF. +LISTS_RELOAD=- disables reloading ip list backend. + + +Domain name filtering +--------------------- + +An alternative to ipset is to use tpws or nfqws with a list of domains. Only one list is supported. + +Enter the blocked domains to ipset/zapret-hosts-users.txt. Remove ipset/zapret-hosts.txt.gz. +Then the init script will run tpws with the zapret-hosts-users.txt list. + +Other option ( Roskomnadzor list - get_hostlist.sh ) is russian specific. +You can write your own replacement for get_hostlist.sh. + +When filtering by domain name, daemons should run without filtering by ipset. +When using large regulator lists estimate the amount of RAM on the router ! + + +Choosing parameters +------------------- + +The file /opt/zapret/config is used by various components of the system and contains basic settings. +It needs to be viewed and edited if necessary. + + +Main mode : +tpws - use tpws +tpws - use nfqws +filter - only fill ipset or load hostlist +custom - use custom script for running daemons and establishing firewall rules + +MODE=tpws + +Enable http fooling : + +MODE_HTTP=1 + +Apply fooling to keep alive http sessions. Only applicable to nfqws. Tpws always fool keepalives. +Not enabling this can save CPU time. + +MODE_HTTP_KEEPALIVE=0 + +Enable https fooling : + +MODE_HTTPS=1 + +Host filtering mode : +none - apply fooling to all hosts +ipset - limit fooling to hosts from ipset zapret/zapret6 +hostlist - limit fooling to hosts from hostlist + +MODE_FILTER=none + +Its possible to change manipulation options used by tpws : + +TPWS_OPT="--hostspell=HOST --split-http-req=method --split-pos=3" + +nfqws options for DPI desync attack: + +DESYNC_MARK=0x40000000 +NFQWS_OPT_DESYNC="--dpi-desync=fake --dpi-desync-ttl=0 --dpi-desync-fooling=badsum --dpi-desync-fwmark=$DESYNC_MARK" + +flow offloading control (openwrt only) +donttouch : disable system flow offloading setting if selected mode is incompatible with it, dont touch it otherwise and dont configure selective flow offloading +none : always disable system flow offloading setting and dont configure selective flow offloading +software : always disable system flow offloading setting and configure selective software flow offloading +hardware : always disable system flow offloading setting and configure selective hardware flow offloading + +FLOWOFFLOAD=donttouch + +The GETLIST parameter tells the install_easy.sh installer which script to call +to update the list of blocked ip or hosts. +Its called via get_config.sh from scheduled tasks (crontab or systemd timer). +Put here the name of the script that you will use to update the lists. +If not, then the parameter should be commented out. + +You can individually disable ipv4 or ipv6. If the parameter is commented out or not equal to "1", +use of the protocol is permitted. +#DISABLE_IPV4=1 +DISABLE_IPV6=1 + +The number of threads for mdig multithreaded DNS resolver (1..100). +The more of them, the faster, but will your DNS server be offended by hammering ? +MDIG_THREADS=30 + +temp directory. Used by ipset/*.sh scripts for large lists processing. +/tmp by default. Can be reassigned if /tmp is tmpfs and RAM is low. +TMPDIR=/opt/zapret/tmp + +ipset options : + +IPSET_OPT="hashsize 262144 maxelem 2097152" + +Kernel automatically increases hashsize if ipset is too large for the current hashsize. +This procedure requires internal reallocation and may require additional memory. +On low RAM systems it can cause errors. +Do not use too high hashsize. This way you waste your RAM. And dont use too low hashsize to avoid reallocs. + +ip2net options. separate for ipv4 and ipv6. +IP2NET_OPT4="--prefix-length=22-30 --v4-threshold=3/4" +IP2NET_OPT6="--prefix-length=56-64 --v6-threshold=5" + +Enable gzip compression for large lists. Used by ipset/*.sh scripts. +GZIP_LISTS=1 + +Command to reload ip/host lists after update. +Comment or leave empty for auto backend selection : ipset or ipfw if present. +On BSD systems with PF no auto reloading happens. You must provide your own command. +Newer FreeBSD versions support table only reloading : pfctl -Tl -f /etc/pf.conf +Set to "-" to disable reload. +LISTS_RELOAD="pfctl -f /etc/pf.conf" + +The following settings are not relevant for openwrt : + +If your system works as a router, then you need to enter the names of the internal and external interfaces: +IFACE_LAN = eth0 +IFACE_WAN = eth1 +IMPORTANT: configuring routing, masquerade, etc. not a zapret task. +Only modes that intercept transit traffic are enabled. + +The INIT_APPLY_FW=1 parameter enables the init script to independently apply iptables rules. +With other values or if the parameter is commented out, the rules will not be applied. +This is useful if you have a firewall management system, in the settings of which you should tie the rules. + + +Screwing to the firewall control system or your launch system +------------------------------------------------------------- + +If you use some kind of firewall management system, then it may conflict with an existing startup script. +When re-applying the rules, it could break the iptables settings from the zapret. +In this case, the rules for iptables should be screwed to your firewall separately from running tpws or nfqws. + +The following calls allow you to apply or remove iptables rules separately: + + /opt/zapret/init.d/sysv/zapret start-fw + /opt/zapret/init.d/sysv/zapret stop-fw + +And you can start or stop the demons separately from the firewall: + + /opt/zapret/init.d/sysv/zapret start-daemons + /opt/zapret/init.d/sysv/zapret stop-daemons + + +Simple install to desktop linux system +-------------------------------------- + +Simple install works on most modern linux distributions with systemd, OpenWRT and MacOS. +Run install_easy.sh and answer its questions. + +Simple install to openwrt +------------------------- + +install_easy.sh works on openwrt but there're additional challenges. +They are mainly about possibly low flash free space. +Simple install will not work if it has no space to install itself and required packages from the repo. + +Another challenge would be to bring zapret to the router. You can download zip from github and use it. +Do not repack zip contents in Windows, because this way you break chmod and links. +Install openssh-sftp-server and unzip to openwrt and use sftp to transfer the file. + +The best way to start is to put zapret dir to /tmp and run /tmp/zapret/install_easy.sh from there. +After installation remove /tmp/zapret to free RAM. + +The absolute minimum for openwrt is 64/8 system, 64/16 is comfortable, 128/extroot is recommended. + + +Android +------- + +Its not possible to use nfqws and tpws in transparent proxy mode without root privileges. +Without root tpws can run in --socks mode. + +I have no NFQUEUE presence statistics in stock android kernels, but its present on my MTK device. +If NFQUEUE is present nfqws works. + +There's no ipset support unless you run custom kernel. In common case task of bringing up ipset +on android is ranging from "not easy" to "almost impossible", unless you find working kernel +image for your device. + +Android does not use /etc/passwd, tpws --user won't work. There's replacement. +Use numeric uids in --uid option. +Its recommended to use gid 3003 (AID_INET), otherwise tpws will not have inet access. +Example : --uid 1:3003 +In iptables use : "! --uid-owner 1" instead of "! --uid-owner tpws". + +Write your own shell script with iptables and tpws, run it using your root manager. +Autorun scripts are here : +magisk : /data/adb/service.d +supersu : /system/su.d + +I haven't checked whether android can kill iptable rules at its own will during wifi connection/disconnection, +mobile data on/off, ... + +How to run tpws on root-less android. +You can't write to /system, /data, can't run from sd card. +Selinux prevents running executables in /data/local/tmp from apps. +Use adb and adb shell. +mkdir /data/local/tmp/zapret +adb push tpws /data/local/tmp/zapret +chmod 755 /data/local/tmp/zapret /data/local/tmp/zapret/tpws +chcon u:object_r:system_file:s0 /data/local/tmp/zapret/tpws +Now its possible to run /data/local/tmp/zapret/tpws from any app such as tasker. + + +FreeBSD, OpenBSD, MacOS +----------------------- + +see docs/bsd.eng.txt + + +Windows (WSL) +------------- + +Using WSL (Windows subsystem for Linux) it's possible to run tpws in socks mode under rather new builds of +windows 10 and windows server. +Its not required to install any linux distributions as suggested in most articles. +tpws is static binary. It doesn't need a distribution. + +Install WSL : dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all +Copy binaries/x86_64/tpws_wsl.tgz to the target system. +Run : wsl --import tpws "%USERPROFILE%\tpws" tpws_wsl.tgz +Run tpws : wsl --exec /tpws --uid=1 --no-resolve --socks --bind-addr=127.0.0.1 --port=1080 +Configure socks as 127.0.0.1:1080 in a browser or another program. + +Cleanup : wsl --unregister tpws + +Tested in windows 10 build 19041 (20.04). + +NOTICE. There is native windows solution GoodByeDPI. It works on packet level like nfqws. + + +Other devices +------------- + +Author's goal does not include easy supporting as much devices as possibles. +Please do not ask for easy supporting firmwares. It requires a lot of work and owning lots of devices. Its counterproductive. +As a devices owner its easier for you and should not be too hard if firmware is open. +Most closed stock firmwares are not designed for custom usage and sometimes actively prevent it. +In the latter case you have to hack into it and reverse engineer. Its not easy. +Binaries are universal. They can run on almost all firmwares. +You will need : + * root shell access. true sh shell, not microtik-like console + * startup hook + * r/w partition to store binaries and startup script with executable permission (+x) + * tpws can be run almost anywhere but nfqws require kernel support for NFQUEUE. Its missing in most firmwares. + * too old 2.6 kernels are unsupported and can cause errors +If binaries crash with segfault (rare but happens on some kernels) try to unpack upx like this : upx -d tpws. +First manually debug your scenario. Run iptables + daemon and check if its what you want. +Write your own script with iptables magic and run required daemon from there. Put it to startup. +Dont ask me how to do it. Its different for all firmwares and requires studying. +Find manual or reverse engineer yourself. +Check for race conditions. Firmware can clear or modify iptables after your startup script. +If this is the case then run another script in background and add some delay there. + + +Https blocking bypass +---------------------- + +SOMETIMES (but not often) a tls handshake split trick works. +Try MODE=..._https +May be you're lucky. + +MORE OFTEN DPI desync attack work, but it may require some manual tuning. + +OTHERWISE you have to redirect traffic through a third-party host. +It is proposed to use transparent redirect through socks5 using iptables + redsocks, or iptables + iproute + vpn. +Redsocks variant is described in https.txt. +iproute + wireguard - in wireguard_iproute_openwrt.txt. +(they are russian) diff --git a/docs/readme.txt b/docs/readme.txt new file mode 100644 index 0000000..beb16da --- /dev/null +++ b/docs/readme.txt @@ -0,0 +1,1314 @@ +zapret v.38 + +English +------- + +For english version refer to docs/readme.eng.txt + +Для чего это надо +----------------- + +Автономно, без задействования сторонних серверов, обойти блокировки веб сайтов http и https на DPI. +Проект нацелен прежде всего на маломощные embedded устройства - роутеры, работающие под openwrt, +но так же поддерживается и большинство классических дистрибутивов linux. +В некоторых случаях возможна самостоятельная прикрутка решения к различным прошивкам. + +Так же поддерживаются в экспериментальном режиме системы FreeBSD и OpenBSD. +Есть частичная поддержка MacOS. См docs/bsd.txt + +Как это работает +---------------- + +В самом простейшем случае вы имеете дело с пассивным DPI. Пассивный DPI может читать трафик +из потока, может инжектить свои пакеты, но не может блокировать проходящие пакеты. +Если запрос "плохой", пассивный DPI инжектит пакет RST, опционально дополняя его пакетом http redirect. +Если фейк пакет инжектится только для клиента, в этом случае можно обойтись командами iptables +для дропа RST и/или редиректа на заглушку по определенным условиям, которые нужно подбирать +для каждого провайдера индивидуально. Так мы обходим последствия срабатывания триггера запрета. +Если пассивный DPI направляет пакет RST в том числе и серверу, то вы ничего с этим не сможете сделать. +Ваша задача - не допустить срабатывания триггера запрета. Одними iptables уже не обойдетесь. +Этот проект нацелен именно на предотвращение срабатывания запрета, а не ликвидацию его последствий. + +Активный DPI ставится в разрез провода и может дропать пакеты по любым критериям, +в том числе распознавать TCP потоки и блокировать любые пакеты, принадлежащие потоку. + +Как не допустить срабатывания триггера запрета ? Послать то, на что DPI не расчитывает +и что ломает ему алгоритм распознавания запросов и их блокировки. + +Некоторые DPI не могут распознать http запрос, если он разделен на TCP сегменты. +Например, запрос вида "GET / HTTP/1.1\r\nHost: kinozal.tv......" +мы посылаем 2 частями : сначала идет "GET ", затем "/ HTTP/1.1\r\nHost: kinozal.tv.....". +Другие DPI спотыкаются, когда заголовок "Host:" пишется в другом регистре : например, "host:". +Кое-где работает добавление дополнительного пробела после метода : "GET /" => "GET /" +или добавление точки в конце имени хоста : "Host: kinozal.tv." + +Существует и более продвинутая магия, направленная на преодоление DPI на пакетном уровне. + +Подробнее про DPI : + https://habr.com/ru/post/335436 + https://geneva.cs.umd.edu/papers/geneva_ccs19.pdf + +Как это реализовать на практике в системе linux +----------------------------------------------- + +Если кратко, то варианты можно классифицировать по следующей схеме : + +1) Пассивный DPI, не отправляющий RST серверу. Помогут индивидуально настраиваемые под провайдера команды iptables. +На rutracker в разделе "обход блокировок - другие способы" по этому вопросу существует отдельная тема. +В данном проекте не рассматривается. Если вы не допустите срабатывание триггера запрета, то и не придется +бороться с его последствиями. +2) Модификация TCP соединения на уровне потока. Реализуется через proxy или transparent proxy. +3) Модификация TCP соединения на уровне пакетов. Реализуется через обработчик очереди NFQUEUE и raw сокеты. + +Для вариантов 2 и 3 реализованы программы tpws и nfqws соответственно. +Чтобы они работали, необходимо их запустить с нужными параметрами и перенаправить на них определенный трафик +средствами iptables. + + +Для перенаправления tcp соединения на transparent proxy используются команды следующего вида : + +проходящий трафик : +iptables -t nat -I PREROUTING -i <внутренний_интерфейс> -p tcp --dport 80 -j DNAT --to 127.0.0.127:988 +исходящий трафик : +iptables -t nat -I OUTPUT -o <внешний_интерфейс> -p tcp --dport 80 -m owner ! --uid-owner tpws -j DNAT --to 127.0.0.127:988 + +DNAT на localhost работает в цепочке OUTPUT, но не работает в цепочке PREROUTING без включения параметра route_localnet : + +sysctl -w net.ipv4.conf.<внутренний_интерфейс>.route_localnet=1 + +Можно использовать "-j REDIRECT --to-port 988" вместо DNAT , однако в этом случае процесс transparent proxy +должен слушать на ip адресе входящего интерфейса или на всех адресах. Слушать на всех - не есть хорошо +с точки зрения безопасности. Слушать на одном (локальном) можно, но в случае автоматизированного +скрипта придется его узнавать, потом динамически вписывать в команду. В любом случае требуются дополнительные усилия. +Использование route_localnet тоже имеет потенциальные проблемы с безопасностью. Вы делаете доступным все, что висит +на 127.0.0.0/8 для локальной подсети <внутренний_интерфейс>. Службы обычно привязываются к 127.0.0.1, поэтому можно +средствами iptables запретить входящие на 127.0.0.1 не с интерфейса lo, либо повесить tpws на любой другой IP из +из 127.0.0.0/8, например на 127.0.0.127, и разрешить входящие не с lo только на этот IP. + +iptables -A INPUT ! -i lo -d 127.0.0.127 -j ACCEPT +iptables -A INPUT ! -i lo -d 127.0.0.0/8 -j DROP + +Фильтр по owner необходим для исключения рекурсивного перенаправления соединений от самого tpws. +tpws запускается под пользователем "tpws", для него задается исключающее правило. + +tpws может использоваться в режиме socks proxy. В этом случае iptables не нужны, а нужно прописать socks +в настройки программы (например, броузера), с которой будем обходить блокировки. +transparent proxy отличается от socks именно тем, что в варианте transparent настраивать клиентские программы не нужно. + + +Для перенаправления на очередь NFQUEUE исходящего и проходящего в сторону внешнего интерфейса трафика используются +команды следующего вида : + +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp --dport 80 -j NFQUEUE --queue-num 200 --queue-bypass + + +Чтобы не трогать трафик на незаблокированные адреса, можно взять список заблокированных хостов, заресолвить его +в IP адреса и загнать в ipset zapret, затем добавить фильтр в команду : + +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp --dport 80 -m set --match-set zapret dst -j NFQUEUE --queue-num 200 --queue-bypass + +DPI может ловить только первый http запрос, игнорируя последующие запросы в keep-alive сессии. +Тогда можем уменьшить нагрузку на проц, отказавшись от процессинга ненужных пакетов. + +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp --dport 80 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -m set --match-set zapret dst -j NFQUEUE --queue-num 200 --queue-bypass + +Фильтр по mark нужен для отсечения от очереди пакетов, сгенерированных внутри nfqws. +Если применяется фильтр по connbytes 2:4, то обязательно добавлять в iptables и фильтр по mark. Иначе возможно +перепутывания порядка следования пакетов, что приведет к неработоспособности метода. + + +Если ваше устройство поддерживает аппаратное ускорение (flow offloading, hardware nat, hardware acceleration), то iptables могут не работать. +При включенном offloading пакет не проходит по обычному пути netfilter. +Необходимо или его отключить, или выборочно им управлять. + +В новых ядрах (и в более старых, openwrt портировал изменение на 4.14) присутствует software flow offloading (SFO). +Пакеты, проходящие через SFO, так же проходят мимо большей части механизмов iptables. +При включенном SFO работает DNAT/REDIRECT (tpws). Эти соединения исключаются из offloading. +Однако, остальные соединения идут через SFO, потому NFQUEUE будет срабатывать только до помещения +соединения в flowtable. Практически это означает, что nfqws будет работать на window size changing, +но не будут работать опции по модификации содержимого пакетов. +Offload включается через специальный target в iptables "FLOWOFFLOAD". Не обязательно пропускать весь трафик через offload. +Можно исключить из offload соединения, которые должны попасть на tpws или nfqws. +openwrt не предусматривает выборочного управления offload. +Поэтому скрипты zapret поддерживают свою систему выборочного управления offload в openwrt. + + +Особенности применения ip6tables +-------------------------------- + +ip6tables работают почти точно так же, как и ipv4, но есть ряд важных нюансов. +В DNAT следует брать адрес --to в квадратные скобки. Например : + + ip6tables -t nat -I OUTPUT -o <внешний_интерфейс> -p tcp --dport 80 -m owner ! --uid-owner tpws -j DNAT --to [::1]:988 + +Параметра route_localnet не существует для ipv6. +DNAT на localhost (::1) возможен только в цепочке OUTPUT. +В цепочке PREROUTING DNAT возможен на любой global address или на link local address того же интерфейса, +откуда пришел пакет. +NFQUEUE работает без изменений. + + +Когда это работать не будет +--------------------------- + +* Если подменяется DNS. С этой проблемой легко справиться. +* Если блокировка осуществляется по IP. +* Если соединение проходит через фильтр, способный реконструировать TCP соединение, и который +следует всем стандартам. Например, нас заворачивают на squid. Соединение идет через полноценный стек tcpip +операционной системы, фрагментация отпадает сразу как средство обхода. Squid правильный, он все найдет +как надо, обманывать его бесполезно. +НО. Заворачивать на squid могут позволить себе лишь небольшие провайдеры, поскольку это очень ресурсоемко. +Большие компании обычно используют DPI, который расчитан на гораздо большую пропускную способность. +Может применяться комбинированный подход, когда на DPI заворачивают только IP из "плохого" списка, +и дальше уже DPI решает пропускать или нет. Так можно снизить нагрузку на DPI в десятки, если не сотни раз, +а следовательно не покупать очень дорогие решения, обойдясь чем-то существенно более дешевым. +Мелкие провайдеры могут покупать услугу фильтрации у вышестоящих, чтобы самим не морочиться, и +они уже будут применять DPI. + + +nfqws +----- + +Эта программа - модификатор пакетов и обработчик очереди NFQUEUE. +Для BSD систем существует адаптированный вариант - dvtws, собираемый из тех же исходников (см. bsd.txt). + + --debug=0|1 ; 1=выводить отладочные сообщения + --daemon ; демонизировать прогу + --pidfile= ; сохранить PID в файл + --user= ; менять uid процесса + --uid=uid[:gid] ; менять uid процесса + --qnum=200 ; номер очереди + --wsize=4 ; менять tcp window size на указанный размер (устарело !) + --hostcase ; менять регистр заголовка "Host:" по умолчанию на "host:". + --hostnospace ; убрать пробел после "Host:" и переместить его в конец значения "User-Agent:" для сохранения длины пакета + --hostspell=HoST ; точное написание заголовка Host (можно "HOST" или "HoSt"). автоматом включает --hostcase + --domcase ; домен после Host: сделать таким : TeSt.cOm + --dpi-desync[=][, ; бит fwmark для пометки десинхронизирующих пакетов, чтобы они повторно не падали в очередь. default = 0x40000000 + --dpi-desync-ttl= ; установить ttl для десинхронизирующих пакетов + --dpi-desync-fooling=none|md5sig|ts|badseq|badsum ; дополнительные методики как сделать, чтобы фейковый пакет не дошел до сервера + --dpi-desync-retrans=0|1 ; (только для fake,rst,rstack) 0(default)=отправлять оригинал следом за фейком 1=дропать оригинал, заставляя ОС выполнять ретрансмиссию через 0.2 сек + --dpi-desync-repeats= ; посылать каждый генерируемый в nfqws пакет N раз (не влияет на остальные пакеты) + --dpi-desync-skip-nosni=0|1 ; 1(default)=не применять dpi desync для запросов без hostname в SNI, в частности для ESNI + --dpi-desync-split-pos=<1..1500> ; (только для split*, disorder*) разбивать пакет на указанной позиции + --dpi-desync-any-protocol=0|1 ; 0(default)=работать только по http request и tls clienthello 1=по всем непустым пакетам данных + --dpi-desync-fake-http= ; файл, содержащий фейковый http запрос для dpi-desync=fake, на замену стандартному w3.org + --dpi-desync-fake-tls= ; файл, содержащий фейковый tls clienthello для dpi-desync=fake, на замену стандартному w3.org + --hostlist= ; применять дурение только к хостам из листа + +Параметры манипуляции могут сочетаться в любых комбинациях. + +ЗАМЕЧАНИЕ. Параметр --wsize считается устаревшим и более не поддерживается в скриптах. +Функции сплита выполняются в рамках атаки десинхронизации. Это быстрее и избавляет от целого ряда недостатков wsize. + +АТАКА ДЕСИНХРОНИЗАЦИИ DPI +Суть ее в следующем. После выполнения tcp 3-way handshake идет первый пакет с данными от клиента. +Там обычно "GET / ..." или TLS ClientHello. Мы дропаем этот пакет, заменяя чем-то другим. +Это может быть поддельная версия с безобидным, но валидным запросом http или https (вариант fake), +пакет сброса соединения (варианты rst, rstack), разбитый на части оригинальный пакет с перепутанным +порядком следования сегментов + обрамление первого сегмента фейками (disorder), +то же самое без перепутывания порядка сегментов (split). +В литературе такие атаки еще называют TCB desynchronization и TCB teardown. +Надо, чтобы фейковые пакеты дошли до DPI, но не дошли до сервера. +На вооружении есть следующие возможности : установить низкий TTL, посылать пакет с инвалидной чексуммой, +добавлять tcp option "MD5 signature", испортить sequence numbers. Все они не лишены недостатков. + +* md5sig работает не на всех серверах. Пакеты с md5 обычно отбрасывают только linux. +* badsum не сработает, если ваше устройство за NAT, который не пропускает пакеты с инвалидной суммой. + Linux NAT по умолчанию их не пропускает без особой настройки "sysctl -w net.netfilter.nf_conntrack_checksum=0". + В openwrt она сделана из коробки, в других роутерах как правило нет, и не всегда это можно изменить. + Если nfqws работает на роутере, то не обязательно выключать nf_conntrack_checksum. Фейковый пакет не проходит FORWARD, он идет через OUTPUT. + Но если роутер за другим NAT, например провайдерским, и он не пропускает invalid packets, вы ничего не сможете с этим сделать. +* пакеты с badseq будут наверняка отброшены принимающим узлом, но так же и DPI, если он ориентируется на sequence numbers +* TTL казалось бы - лучший вариант, но он требует индивидуальной настройки под каждого провайдера. Если DPI находится дальше локальных + сайтов провайдера, то вы можете отрезать себе доступ к ним. Необходим ip exclude list, заполняемый вручную. + Вместе с ttl можно применять md5sig. Это ничего не испортит, зато дает неплохой шанс работы сайтов, до которых "плохой" пакет дойдет по TTL. + Если не удается найти автоматическое решение, воспользуйтесь файлом zapret-hosts-user-exclude.txt. + КАКИМ СТОИТ ВЫБИРАТЬ TTL : найдите минимальное значение, при котором обход еще работает. Это и будет номер хопа вашего DPI. + +Режимы дурения могут сочетаться в любых комбинациях. --dpi-desync-fooling берет множество значений через запятую. + +Для режимов fake, rst, rstack после фейка отправляем оригинальный пакет. Можно его отправить сразу следом за фейком, а можно его просто дропнуть. +Если его дропнуть, ОС выполнит ретрансмиссию. Первая ретрансмиссия случается через 0.2 сек, потом задержка увеличивается экспоненциально. +Задержка может дать надежную гарантию, что пакеты пойдут именно в нужном порядке и будут именно в нем обработаны на DPI. +По умолчанию используется первый вариант, т.к. он быстрее. +При использовании dpi-desync-retrans=1 обязательно вставлять ограничитель connbytes в iptables, иначе получим зацикливание. + +Режим disorder делит оригинальный пакет на 2 части и отправляет следующую комбинацию в указанном порядке : +1. 2-я часть пакета +2. поддельная 1-я часть пакета, поле данных заполнено нулями +3. 1-я часть пакета +4. поддельная 1-я часть пакета, поле данных заполнено нулями. отсылка 2-й раз. +Оригинальный пакет дропается всегда. Параметр --dpi-desync-split-pos позволяет указать байтовую позицию, на которой +происходит разбивка. По умолчанию - 3. Если позиция больше длины пакета, позиция выбирается 1. +Этой последовательностью для DPI максимально усложняется задача реконструкции начального сообщения, +по которому принимается решение о блокировке. Некоторым DPI хватит и tcp сегментов в неправильном порядке, +поддельные части сделаны для дополнительной надежности и более сложных алгоритмов реконструкции. +Режим disorder2 отключает отправку поддельных частей. + +Режим split очень похож на disorder, только нет изменения порядка следования сегментов : +1. поддельная 1-я часть пакета, поле данных заполнено нулями +2. 1-я часть пакета +3. поддельная 1-я часть пакета, поле данных заполнено нулями. отсылка 2-й раз. +4. 2-я часть пакета +Режим split2 отключает отправку поддельных частей. +Он может быть использован как более быстрая альтернатива --wsize. + +disorder2 и split2 не предполагают отсылку фейк пакетов, поэтому опции ttl и fooling неактуальны. + +Есть DPI, которые анализируют ответы от сервера, в частности сертификат из ServerHello, где прописаны домены. +Подтверждением доставки ClientHello является ACK пакет от сервера с номером ACK sequence, соответствующим длине ClientHello+1. +В варианте disorder обычно приходит сперва частичное подтверждение (SACK), потом полный ACK. +Если вместо ACK или SACK идет RST пакет с минимальной задержкой, то DPI вас отсекает еще на этапе вашего запроса. +Если RST идет после полного ACK спустя задержку, равную примерно пингу до сервера, +тогда вероятно DPI реагирует на ответ сервера. +DPI может отстать от потока, если ClientHello его удовлетворил и не проверять ServerHello. +Тогда вам повезло. Вариант fake может сработать. +Если же он не отстает и упорно проверяет ServerHello, еще и выполняя реконструкцию сегментов TCP, +то сделать с этим что-либо вряд ли возможно без помощи со стороны сервера. +Лучшее решение - включить на сервере поддержку TLS 1.3. В нем сертификат сервера передается в зашифрованном виде. +Это рекомендация ко всем админам блокируемых сайтов. Включайте TLS 1.3. Так вы дадите больше возможностей преодолеть DPI. + +Хосты извлекаются из Host: хедера обычных http запросов и из SNI в TLS ClientHello. +Субдомены учитываются автоматически. Поддерживаются листы gzip. + +iptables для задействования атаки на первый пакет данных : + +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp -m multiport --dports 80,443 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass + +Этот вариант применяем, когда DPI не следит за всеми запросами http внутри keep-alive сессии. +Если следит, направляем только первый пакет от https и все пакеты от http : + +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp --dport 443 -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass +iptables -t mangle -I POSTROUTING -o <внешний_интерфейс> -p tcp --dport 80 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass + +mark нужен, чтобы сгенерированный поддельный пакет не попал опять к нам на обработку. nfqws выставляет fwmark при его отсылке. +хотя nfqws способен самостоятельно различать помеченные пакеты, фильтр в iptables по mark нужен при использовании connbytes, +чтобы не допустить изменения порядка следования пакетов. Процессинг очереди - процесс отложенный. +Если ядро имеет пакеты на отсылку вне очереди - оно их отправляет незамедлительно. +Изменение правильного порядка следования пакетов при десинхронизации ломает всю идею. +При отсутствии ограничения на connbytes, атака будет работать и без фильтра по mark. +Но лучше его все же оставить для увеличения скорости. + +Почему --connbytes 2:4 : 2 - иногда данные идут в 3-м пакете 3-way handshake. 3 - стандартная ситуация. 4 - для надежности. на случай, если выполнялась одна ретрансмиссия + +КОМБИНИРОВАНИЕ МЕТОДОВ ДЕСИНХРОНИЗАЦИИ +В параметре dpi-desync можно указать 2 режима через запятую. +Режим 1-й фазы может быть fake,rst,rstack. Режим 2-й фазы может быть disorder,disorder2,split,split2. +Может быть полезно, когда у провайдера стоит не один DPI. + +ВИРТУАЛЬНЫЕ МАШИНЫ +Изнутри VM от virtualbox и vmware в режиме NAT не работают многие техники пакетной магии nfqws. +Принудительно заменяется ttl, не проходят фейк пакеты. Необходимо настроить сеть в режиме bridge. + + +tpws +----- + +tpws - это transparent proxy. + --debug=0|1|2 ; Количество буковок в output : 0(default)=тихо, 1=подробно, 2=отладка + --daemon ; демонизировать прогу + --pidfile= ; сохранить PID в файл + --user= ; менять uid процесса + --uid=uid[:gid] ; менять uid процесса + --bind-addr ; на каком адресе слушать. может быть ipv4 или ipv6 адрес + ; если указан ipv6 link local, то требуется указать с какого он интерфейса : fe80::1%br-lan + --bind-linklocal=prefer|force ; если prefer, то найти link local от iface6. если не найдено - использовать первый адрес любого типа. + ; если force и link local не найден - выход по ошибке. + --bind-iface4= ; слушать на первом ipv4 интерфейса iface + --bind-iface6= ; слушать на первом ipv6 интерфейса iface + --bind-wait-ifup= ; ждать до N секунд появления и поднятия интерфейса + --bind-wait-ip= ; ждать до N секунд получения IP адреса (если задан --bind-wait-ifup - время идет после поднятия интерфейса) + --bind-wait-ip-linklocal= ; (только если заданы --bind-wait-ip и --bind-linklocal=prefer) согласиться на global address после N секунд + --bind-wait-only ; подождать все бинды и выйти. результат 0 в случае успеха, иначе не 0. + --socks ; вместо прозрачного прокси реализовать socks4/5 proxy + --no-resolve ; запретить ресолвинг имен через socks5 + --port= ; на каком порту слушать + --maxconn= ; максимальное количество соединений от клиентов к прокси + --maxfiles= ; макс количество файловых дескрипторов (setrlimit). мин требование (X*connections+16), где X=6 в tcp proxy mode, X=4 в режиме тамперинга. + ; стоит сделать запас с коэффициентом как минимум 1.5. по умолчанию maxfiles (X*connections)*1.5+16 + --max-orphan-time=; если вы запускаете через tpws торрент-клиент с множеством раздач, он пытается установить очень много исходящих соединений, + ; большая часть из которых отваливается по таймату (юзера сидят за NAT, firewall, ...) + ; установление соединения в linux может длиться очень долго. локальный конец отвалился, перед этим послав блок данных, + ; tpws ждет подключения удаленного конца, чтобы отослать ему этот блок, и зависает надолго. + ; настройка позволяет сбрасывать такие подключения через N секунд, теряя блок данных. по умолчанию 5 сек. 0 означает отключить функцию + ; эта функция не действует на успешно подключенные ранее соединения + + --local-rcvbuf= ; SO_RCVBUF для соединений client-proxy + --local-sndbuf= ; SO_SNDBUF для соединений client-proxy + --remote-rcvbuf= ; SO_RCVBUF для соединений proxy-target + --remote-sndbuf= ; SO_SNDBUF для соединений proxy-target + --skip-nodelay ; не устанавливать в исходящих соединения TCP_NODELAY. несовместимо со split. + + --split-http-req=method|host ; способ разделения http запросов на сегменты : около метода (GET,POST) или около заголовка Host + --split-pos= ; делить все посылы на сегменты в указанной позиции. единственная опция, работающая на не-http. при указании split-http-req он имеет преимущество на http. + --split-any-protocol ; применять split-pos к любым пакетам. по умолчанию - только к http и TLS ClientHello + --hostcase ; менять регистр заголовка "Host:". по умолчанию на "host:". + --hostspell=HoST ; точное написание заголовка Host (можно "HOST" или "HoSt"). автоматом включает --hostcase + --hostdot ; добавление точки после имени хоста : "Host: kinozal.tv." + --hosttab ; добавление табуляции после имени хоста : "Host: kinozal.tv\t" + --hostnospace ; убрать пробел после "Host:" + --hostpad= ; добавить паддинг-хедеров общей длиной перед Host: + --domcase ; домен после Host: сделать таким : TeSt.cOm + --methodspace ; добавить пробел после метода : "GET /" => "GET /" + --methodeol ; добавить перевод строки перед методом : "GET /" => "\r\nGET /" + --unixeol ; конвертировать 0D0A в 0A и использовать везде 0A + --hostlist= ; действовать только над доменами, входящими в список из filename. поддомены автоматически учитываются. + ; в файле должен быть хост на каждой строке. + ; список читается 1 раз при старте и хранится в памяти в виде иерархической структуры для быстрого поиска. + ; для списка РКН может потребоваться система с 128 Mb памяти ! + ; расчитывайте требование RAM для процесса как 3-5 кратный размер файла списка. + ; по сигналу HUP список будет перечитан при следующем принятом соединении + ; список может быть запакован в gzip. формат автоматически распознается и разжимается + ; хосты извлекаются из Host: хедера обычных http запросов и из SNI в TLS ClientHello. + +Параметры манипуляции могут сочетаться в любых комбинациях. + +В случае http запроса split-http-req имеет преимущество над split-pos. +split-pos по умолчанию работает только на http и TLS ClientHello. +Чтобы он работал на любых пакетах, укажите --split-any-protocol. + +На прикладном уровне в общем случае нет гарантированного средства заставить ядро выплюнуть +блок данных, порезанным в определенном месте. ОС держит буфер отсылки (SNDBUF) у каждого сокета. +Если у сокета включена опция TCP_NODELAY и буфер пуст, то каждый send приводит к отсылке +отдельного ip пакета или группы пакетов, если блок не вмещается в один ip пакет. +Однако, если в момент send уже имеется неотосланный буфер, то ОС присоединит данные к нему, +никакой отсылки отдельным пакетом не будет. Но в этом случае и так нет никакой гарантии, +что какой-то блок сообщения пойдет в начале пакета, на что собственно и заточены DPI. +Разбиение будет производится согласно MSS, который зависит от MTU исходящего интерфейса. +Таким образом DPI, смотрящие в начало поля данных TCP пакета, будут поломаны в любом случае. +Протокол http относится к запрос-ответным протоколам. Новое сообщение посылается только тогда, +когда сервер получил запрос и полностью вернул ответ. Значит запрос фактически был не только отослан, +но и принят другой стороной, а следовательно буфер отсылки пуст, и следующие 2 send приведут +к отсылке сегментов данных разными ip пакетами. +Резюме : tpws гарантирует сплит только за счет раздельных вызовов send, что на практике +вполне достаточно для протоколов http(s). + +tpws может биндаться на множество интерфейсов и IP адресов (до 32 шт). +Порт всегда только один. +Параметры --bind-iface* и --bind-addr создают новый бинд. +Остальные паремтры --bind-* относятся к последнему бинду. +Для бинда на все ipv4 укажите --bind-addr "0.0.0.0", на все ipv6 - "::". --bind-addr="" - биндаемся на все ipv4 и ipv6. +Если не указано ни одного бинда, то создается бинд по умолчанию на все адреса всех интерфейсов. +Параметры --bind-wait* могут помочь в ситуациях, когда нужно взять IP с интерфейса, но его еще нет, он не поднят +или не сконфигурирован. +В разных системах события ifup ловятся по-разному и не гарантируют, что интерфейс уже получил IP адрес определенного типа. +В общем случае не существует единого механизма повеситься на событие типа "на интерфейсе X появился link local address". + +Параметры rcvbuf и sndbuf позволяют установить setsockopt SO_RCVBUF SO_SNDBUF для локального и удаленного соединения. + +Если не указан ни один из параметров модификации содержимого, tpws работает в режиме "tcp proxy mode". +Он отличается тем, что в оба конца применяется splice для переброски данных из одного сокета в другой +без копирования в память процесса. Практически - это то же самое, но может быть чуть побыстрее. +TCP проксирование может быть полезно для обхода блокировок, когда DPI спотыкается на экзотических +хедерах IP или TCP. Вы вряд ли сможете поправить хедеры, исходящие от айфончиков и гаджетиков, +но на linux сможете влиять на них в какой-то степени через sysctl. +Когда соединение проходит через tpws, фактически прокси-сервер сам устанавливает подключение к удаленному +узлу от своего имени, и на это распространяются настройки системы, на которой работает прокси. +tpws можно использовать на мобильном устройстве, раздающем интернет на тарифе сотового оператора, +где раздача запрещена, в socks режиме даже без рута. Соединения от tpws неотличимы от соединений +с самого раздающего устройства. Отличить можно только по содержанию (типа обновлений windows). +Заодно можно и обойти блокировки. 2 зайца одним выстрелом. +Более подробную информацию по вопросу обхода ограничений операторов гуглите на 4pda.ru. + +Режим "--socks" не требует повышенных привилегий (кроме бинда на привилегированные порты 1..1023). +Поддерживаются версии socks 4 и 5 без авторизации. Версия протокола распознается автоматически. +Подключения к IP того же устройства, на котором работает tpws, включая localhost, запрещены. +socks5 позволяет удаленно ресолвить хосты (curl : --socks5-hostname firefox : socks_remote_dns=true). +tpws поддерживает эту возможность, однако используется блокирующий ресолвинг. Пока система +ресолвит хост (это может занять секунды), вся активность останавливается. +tpws полностью работает на асинхронных сокетах, но ресолвинг может попортить эту модель. +С ним возможны атаки DoS на tpws. Если tpws обслуживает множество клиентов, то из-за частого +ресолвинга качество обслуживания может существенно ухудшиться. +Если удаленный ресолвинг создает проблемы, настройте клиенты на локальный ресолвинг, включите опцию +--no-resolve на стороне tpws. + +Параметр --hostpad= добавляет паддинг-хедеров перед Host: на указанное количество байтов. +Если размер слишком большой, то идет разбивка на разные хедеры по 2K. +Общий буфер приема http запроса - 64K, больший паддинг не поддерживается, да и http сервера +такое уже не принимают. +Полезно против DPI, выполняющих реассемблинг TCP с ограниченным буфером. +Если техника работает, то после некоторого количества bytes http запрос начнет проходить до сайта. +Если при этом критический размер padding около MTU, значит скорее всего DPI не выполняет реассемблинг пакетов, и лучше будет использовать обычные опции --split-… +Если все же реассемблинг выполняется, то критический размер будет около размера буфера DPI. Он может быть 4K или 8K, возможны и другие значения. + +--skip-nodelay может быть полезен, чтобы привести MTU к MTU системы, на которой работает tpws. +Это может быть полезно для скрытия факта использования VPN. Пониженный MTU - 1 из способов обнаружения +подозрительного подключения. С tcp proxy ваши соединения неотличимы от тех, что сделал бы сам шлюз. + + +Способы получения списка заблокированных IP +------------------------------------------- + +1) Внесите заблокированные домены в ipset/zapret-hosts-user.txt и запустите ipset/get_user.sh +На выходе получите ipset/zapret-ip-user.txt с IP адресами. + +Cкрипты с названием get_reestr_* оперируют дампом реестра заблокированных сайтов : + +2) ipset/get_reestr_resolve.sh получает список доменов от rublacklist и дальше их ресолвит в ip адреса +в файл ipset/zapret-ip.txt.gz. В этом списке есть готовые IP адреса, но судя во всему они там в точности в том виде, +что вносит в реестр РосКомПозор. Адреса могут меняться, позор не успевает их обновлять, а провайдеры редко +банят по IP : вместо этого они банят http запросы с "нехорошим" заголовком "Host:" вне зависимости +от IP адреса. Поэтому скрипт ресолвит все сам, хотя это и занимает много времени. +Используется мультипоточный ресолвер mdig (собственная разработка). +Реестр РКН уже настолько огромен, что однопоточный ресолв займет вечность, а многопоточный хоть и тоже много времени, +но хотя бы оно конечно. +На роутерах с небольшим объемом RAM может сработать только с TMPDIR на внешнем носителе + +3) ipset/get_reestr_ip.txt +взять все IP адреса из реестра и загнать в ipset zapret/zapret6 +На роутерах с небольшим объемом RAM может сработать только с TMPDIR на внешнем носителе + +4) ipset/get_reestr_combined.sh. для провайдеров, которые блокируют по IP https, а остальное по DPI. +IP https и IP без домена заносятся в ipset ipban, остальные в ipset zapret. +На роутерах с небольшим объемом RAM может сработать только с TMPDIR на внешнем носителе + +Cкрипты с названием get_antifilter_* оперируют списками адресов и масок подсетей с сайтов antifilter.network и antifilter.download : + +5) ipset/get_antifilter_ip.sh. получает лист https://antifilter.network/download/ip.lst. + +7) ipset/get_antifilter_ipsmart.sh. получает лист https://antifilter.network/download/ipsmart.lst. +это умная суммаризация отдельных адресов из ip.lst по маскам от /32 до /22 +количество префиксов измеряется всего лишь десятками тысяч, потому это лучшее решение для роутера с 64 Mb RAM + +7) ipset/get_antifilter_ipsum.sh. получает лист https://antifilter.network/download/ipsum.lst. +это суммаризация отдельных адресов из ip.lst по маске /24 +количество префиксов измеряется всего лишь десятками тысяч, потому можно использовать на роутерах с 64 Mb RAM + +Все варианты рассмотренных скриптов автоматически создают и заполняют ipset. +Варианты 2-7 дополнительно вызывают вариант 1. + +8) ipset/get_config.sh. этот скрипт вызывает то, что прописано в переменной GETLIST из файла config +Если переменная не определена, то ресолвятся лишь листы для ipset nozapret/nozapret6. + +Листы РКН все время изменяются. Возникают новые тенденции. Требования к RAM могут меняться. +Поэтому необходима нечастая, но все же регулярная ревизия что же вообще у вас происходит на роутере. +Или вы можете узнать о проблеме лишь когда у вас начнет постоянно пропадать wifi, и вам придется +его перезагружать каждые 2 часа (метод кувалды). + +Листы zapret-ip.txt и zapret-ipban.txt сохраняются в сжатом виде в файлы .gz. +Это позволяет снизить их размер во много раз и сэкономить место на роутере. +Отключить сжатие листов можно параметром конфига GZIP_LISTS=0. + +На роутерах не рекомендуется вызывать эти скрипты чаще раза за 2 суток, поскольку сохранение идет +либо во внутреннюю флэш память роутера, либо в случае extroot - на флэшку. +В обоих случаях слишком частая запись может убить флэшку, но если это произойдет с внутренней +флэш памятью, то вы просто убьете роутер. + +Принудительное обновление ipset выполняет скрипт ipset/create_ipset.sh. +Если передан параметр "no-update", скрипт не обновляет ipset, а только создает его при его отсутствии и заполняет. +Это полезно, когда могут случиться несколько последовательных вызовов скрипта. Нет смысла несколько раз перезаполнять +ipset, это длительная операция на больших листах. Листы можно обновлять раз в несколько суток, и только тогда +вызывать create_ipset без параметра "no-update". Во всех остальных случаях стоит применять "no-update". + +Список РКН уже достиг внушительных размеров в сотни тысяч IP адресов. Поэтому для оптимизации ipset +применяется утилита ip2net. Она берет список отдельных IP адресов и пытается интеллектуально создать из него подсети для сокращения +количества адресов. ip2net отсекает неправильные записи в листах, гарантируя осутствие ошибок при их загрузке. +ip2net написан на языке C, поскольку операция ресурсоемкая. Иные способы роутер может не потянуть. + +Можно внести список доменов в ipset/zapret-hosts-user-ipban.txt. Их ip адреса будут помещены +в отдельный ipset "ipban". Он может использоваться для принудительного завертывания всех +соединений на прозрачный proxy "redsocks" или на VPN. + +IPV6 : если включен ipv6, то дополнительно создаются листы с таким же именем, но с "6" на конце перед расширением. +zapret-ip.txt => zapret-ip6.txt +Создаются ipset-ы zapret6 и ipban6. +Листы с antifilter не содержат список ipv6 адресов. + +СИСТЕМА ИСКЛЮЧЕНИЯ IP. Все скрипты ресолвят файл zapret-hosts-user-exclude.txt, создавая zapret-ip-exclude.txt и zapret-ip-exclude6.txt. +Они загоняются в ipset-ы nozapret и nozapret6. Все правила, создаваемые init скриптами, создаются с учетом этих ipset. +Помещенные в них IP не участвуют в процессе. +zapret-hosts-user-exclude.txt может содержать домены, ipv4 и ipv6 адреса или подсети. + +FreeBSD. Скрипты ipset/*.sh работают так же на FreeBSD. Вместо ipset они создают lookup таблицы ipfw с аналогичными именами. +ipfw таблицы в отличие от ipset могут содержать как ipv4, так и ipv6 адреса и подсети в одной таблице, поэтому разделения нет. + +Параметр конфига LISTS_RELOAD задает произвольную команду для перезагрузки листов. +Это особенно полезно на BSD системах с PF. +LISTS_RELOAD=- отключает перезагрузку листов. + + +ip2net +------ + +Утилита ip2net предназначена для преобразования ipv4 или ipv6 списка ip в список подсетей +с целью сокращения размера списка. Входные данные берутся из stdin, выходные выдаются в stdout. + + -4 ; лист - ipv4 (по умолчанию) + -6 ; лист - ipv6 + --prefix-length=min[-max] ; диапазон рассматриваемых длин префиксов. например : 22-30 (ipv4), 56-64 (ipv6) + --v4-threshold=mul/div ; ipv4 : включать подсети, в которых заполнено по крайней мере mul/div адресов. например : 3/4 + --v6-threshold=N ; ipv6 : минимальное количество ip для создания подсети + +В списке могут присутствовать записи вида ip/prefix и ip1-ip2. Такие записи выкидываются в stdout без изменений. +Они принимаются командой ipset. ipset умеет для листов hash:net из ip1-ip2 делать оптимальное покрытие ip/prefix. +ipfw из FreeBSD понимает ip/prefix, но не понимает ip1-ip2. +ip2net фильтрует входные данные, выкидывая неправильные IP адреса. + +Выбирается подсеть, в которой присутствует указанный минимум адресов. +Для ipv4 минимум задается как процент от размера подсети (mul/div. например, 3/4), для ipv6 минимум задается напрямую. + +Размер подсети выбирается следующим алгоритмом : +Сначала в указанном диапазоне длин префиксов ищутся подсети, в которых количество адресов - максимально. +Если таких сетей найдено несколько, берется наименьшая сеть (префикс больше). +Например, заданы параметры v6_threshold=2 prefix_length=32-64, имеются следующие ipv6 : +1234:5678:aaaa::5 +1234:5678:aaaa::6 +1234:5678:aaac::5 +Результат будет : +1234:5678:aaa8::/45 +Эти адреса так же входят в подсеть /32. Однако, нет смысла проходиться ковровой бомбардировкой, +когда те же самые адреса вполне влезают в /45 и их ровно столько же. +Если изменить v6_threshold=4, то результат будет : +1234:5678:aaaa::5 +1234:5678:aaaa::6 +1234:5678:aaac::5 +То есть ip не объединятся в подсеть, потому что их слишком мало. +Если изменить prefix_length=56-64, результат будет : +1234:5678:aaaa::/64 +1234:5678:aaac::5 + +Требуемое процессорное время для вычислений сильно зависит от ширины диапазона длин префиксов, размера искомых подсетей и длины листа. +Если ip2net думает слишком долго, не используйте слишком большие подсети и уменьшите диапазон длин префиксов. +Учтите, что арифметика mul/div - целочисленная. При превышении разрядной сетки 32 bit результат непредсказуем. +Не надо делать такое : 5000000/10000000. 1/2 - гораздо лучше. + + +Фильтрация по именам доменов +---------------------------- + +Альтернативой ipset является использование tpws или nfqws со списком доменов. +Может быть только один hostlist. + +Поддерживаются 2 варианта : +1) Внесите домены для дурения в ipset/zapret-hosts-users.txt. Удалите ipset/zapret-hosts.txt.gz. +Тогда init скрипт будет запускать tpws с листом zapret-hosts-users.txt. + +2) Список доменов РКН может быть получен скриптом ipset/get_reestr_hostlist.sh - кладется в ipset/zapret-hosts.txt.gz. +Этот скрипт автоматически добавляет к списку РКН домены из zapret-hosts-user.txt. +init скрипт будет запускать tpws с листом zapret-hosts.txt.gz. + +При фильтрации по именам доменов демон должен запускаться без фильтрации по ipset. +tpws и nfqws решают нужно ли применять дурение в зависимости от поля Host: в http запросе или SNI в TLS ClientHello. +При использовании больших списков, в том числе списка РКН, оцените объем RAM на роутере ! +Если после запуска демона RAM под завязку или случаются oom, значит нужно отказаться от таких больших списков. + + +Проверка провайдера +------------------- + +Перед настройкой нужно провести исследование какую бяку устроил вам ваш провайдер. + +Нужно выяснить не подменяет ли он DNS и какой метод обхода DPI работает. +В этом вам поможет скрипт https://github.com/ValdikSS/blockcheck. + +Если DNS подменяется, но провайдер не перехватывает обращения к сторонним DNS, поменяйте DNS на публичный. +Например : 8.8.8.8, 8.8.4.4, 1.1.1.1, 1.0.0.1, 9.9.9.9 +Если DNS подменяется и провайдер перехватывает обращения к сторонним DNS, настройте dnscrypt. + +Если blockcheck не определил рабочие методы обхода, попробуйте атаку десинхронизации с различными параметрами. + +Проанализируйте какие методы дурения DPI работают, в соответствии с ними настройте /opt/zapret/config. + + +Выбор параметров +---------------- + +Файл /opt/zapret/config используется различными компонентами системы и содержит основные настройки. +Его нужно просмотреть и при необходимости отредактировать. + + +Основной режим : +tpws - использовать tpws +tpws - использовать nfqws +filter - только заполнить ipset или загрузить hostlist +custom - нужно самому запрограммировать запуск демонов в init скрипте и правила iptables + +MODE=tpws + +Применять ли дурение к HTTP : + +MODE_HTTP=1 + +Применять ли дурение к последовательным http запросам в одном tcp соединении (http keeaplive). +Относится только к nfqws. Выключение данной функции способно сэкономить загрузку процессора. +tpws всегда работает с http keepalive + +MODE_HTTP_KEEPALIVE=0 + +Применять ли дурение к HTTPS : + +MODE_HTTPS=1 + +Режим фильтрации хостов : +none - применять дурение ко всем хостам +ipset - ограничить дурение ipset-ом zapret/zapret6 +hostlist - ограничить дурение списком хостов из файла + +MODE_FILTER=none + +Опции tpws : + +TPWS_OPT="--hostspell=HOST --split-http-req=method --split-pos=3" + +Опции nfqws для атаки десинхронизации DPI : + +DESYNC_MARK=0x40000000 +NFQWS_OPT_DESYNC="--dpi-desync=fake --dpi-desync-ttl=0 --dpi-desync-fooling=badsum --dpi-desync-fwmark=$DESYNC_MARK" + +Настройка системы управления выборочным traffic offload (только openwrt) +donttouch : выборочное управление отключено, используется системная настройка, простой инсталятор выключает системную настройку, если она не совместима с выбранным режимом +none : выборочное управление отключено, простой инсталятор выключает системную настройку +software : выборочное управление включено в режиме software, простой инсталятор выключает системную настройку +hardware : выборочное управление включено в режиме hardware, простой инсталятор выключает системную настройку + +FLOWOFFLOAD=donttouch + +Параметр GETLIST указывает инсталятору install_easy.sh какой скрипт дергать +для обновления списка заблокированных ip или хостов. +Он же вызывается через get_config.sh из запланированных заданий (crontab или systemd timer). +Поместите сюда название скрипта, который будете использовать для обновления листов. +Если не нужно, то параметр следует закомментировать. + +Можно индивидуально отключить ipv4 или ipv6. Если параметр закомментирован или не равен "1", +использование протокола разрешено. +#DISABLE_IPV4=1 +DISABLE_IPV6=1 + +Количество потоков для многопоточного DNS ресолвера mdig (1..100). +Чем их больше, тем быстрее, но не обидится ли на долбежку ваш DNS сервер ? +MDIG_THREADS=30 + +Место для хранения временных файлов. При скачивании огромных реестров в /tmp места может не хватить. +Если файловая система на нормальном носителе (не встроенная память роутера), то можно +указать место на флэшке или диске. +TMPDIR=/opt/zapret/tmp + +Опции для создания ipset-ов +IPSET_OPT="hashsize 262144 maxelem 2097152" +ПРО РУГАНЬ в dmesg по поводу нехватки памяти. +Может так случиться, что памяти в системе достаточно, но при попытке заполнить огромный ipset +ядро начинает громко ругаться, ipset заполняется не полностью. +Вероятная причина в том, что превышается hashsize, заданный при создании ipset (create_ipset.sh). +Происходит переаллокация списка, не находится непрерывных фрагментов памяти нужной длины. +Это лечится увеличением hashsize. Но чем больше hashsize, тем больше занимает ipset в памяти. +Задавать слишком большой hashsize для недостаточно больших списков нецелесообразно. + +Опции для вызова ip2net. Отдельно для листов ipv4 и ipv6. +IP2NET_OPT4="--prefix-length=22-30 --v4-threshold=3/4" +IP2NET_OPT6="--prefix-length=56-64 --v6-threshold=5" + +Включить или выключить сжатие больших листов в скриптах ipset/*.sh. По умолчанию включено. +GZIP_LISTS=1 + +Команда для перезагрузки ip таблиц фаервола. +Если не указано или пустое, выбирается автоматически ipset или ipfw при их наличии. +На BSD системах с PF нет автоматической загрузки. Там нужно указать команду явно : pfctl -f /etc/pf.conf +На более новых pfctl (есть в новых FreeBSD, нет в OpenBSD 6.8) можно дать команду загрузки только таблиц : pfctl -Tl -f /etc/pf.conf +"-" означает отключение загрузки листов даже при наличии поддерживаемого backend. +#LISTS_RELOAD="pfctl -f /etc/pf.conf" +#LISTS_RELOAD=- + + +Следующие настройки не актуальны для openwrt : + +Если ваша система работает как роутер, то нужно вписать названия внутреннего и внешнего интерфейсов : +IFACE_LAN=eth0 +IFACE_WAN=eth1 +ВАЖНО : настройка маршрутизации , маскарада и т.д. не входит в задачу zapret. +Включаются только режимы, обеспечивающие перехват транзитного трафика. + +Параметр INIT_APPLY_FW=1 разрешает init скрипту самостоятельно применять правила iptables. +При иных значениях или если параметр закомментирован, правила применены не будут. +Это полезно, если у вас есть система управления фаерволом, в настройки которой и следует прикрутить правила. + +Прикручивание к системе управления фаерволом или своей системе запуска +---------------------------------------------------------------------- + +Если вы используете какую-то систему управления фаерволом, то она может вступать в конфликт +с имеющимся скриптом запуска. При повторном применении правил она могла бы поломать настройки iptables от zapret. +В этом случае правила для iptables должны быть прикручены к вашему фаерволу отдельно от запуска tpws или nfqws. + +Следующие вызовы позволяют применить или убрать правила iptables отдельно : + + /opt/zapret/init.d/sysv/zapret start-fw + /opt/zapret/init.d/sysv/zapret stop-fw + +А так можно запустить или остановить демоны отдельно от фаервола : + + /opt/zapret/init.d/sysv/zapret start-daemons + /opt/zapret/init.d/sysv/zapret stop-daemons + +Вариант custom +-------------- + +custom код вынесен в отдельный shell include +/opt/zapret/init.d/sysv/custom +или +/opt/zapret/init.d/openwrt/custom + +Нужно свой код вписать в функции : +zapret_custom_daemons +zapret_custom_firewall + +В файле custom пишите ваш код, пользуясь хелперами из "functions" или "zapret". +Смотрите как там сделано добавление iptables или запуск демонов. +Используя хелпер функции, вы избавитесь от необходимости учитывать все возможные случаи +типа наличия/отсутствия ipv6, является ли система роутером, имена интерфейсов, ... +Хелперы это учитывают , вам нужно сосредоточиться лишь на фильтрах iptables и +параметрах демонов. + +Код для openwrt и sysv немного отличается. В sysv нужно обрабатывать и запуск, и остановку. +Запуск это или остановка передается в параметре $1 (0 или 1). +В openwrt за остановку демонов отвечает procd, а firewall вычищается при "fw3 restart", +потому нет необходимости реализовывать логику останова. + +При апгрейде нужно сохранить лишь custom, другие файлы править не надо. + +Готовый custom скрипт custom-tpws4http-nfqws4https позволяет применить дурение +tpws к http и nfqws к https. При этом поддерживаются установки из config. +Его можно использовать как стартовую точку для написания своих скриптов. + +Пример ручной установки на debian-подобную систему +-------------------------------------------------- + +На debian основано большое количество дистрибутивов linux, включая ubuntu. +Здесь рассматриваются прежде всего Debian 8+ и Ubuntu 16+. +Но с большой вероятностью может сработать и на производных от них. +Главное условие - наличие systemd, apt и нескольких стандартных пакетов в репозитории. + +Установить пакеты : + apt-get update + apt-get install ipset curl dnsutils git + +Скопировать директорию zapret в /opt или скачать через git : + cd /opt + git clone --depth 1 https://github.com/bol-van/zapret + +Запустить автоинсталятор бинариков. Он сам определит рабочую архитектуру и настроит все бинарики. + /opt/zapret/install_bin.sh +АЛЬТЕРНАТИВА : make -C /opt/zapret. Получите динамические бинарики под вашу ось. +Для сборки требуются dev пакеты : zlib1g-dev libcap-dev libnetfilter-queue-dev + +Настроить параметры согласно разделу "Выбор параметров". + +Создать ссылку на service unit в systemd : + ln -fs /opt/zapret/init.d/systemd/zapret.service /lib/systemd/system + +Удалить старые листы, если они были созданы ранее : + /opt/zapret/ipset/clear_lists.sh +По желанию прописать в /opt/zapret/ipset/zapret-hosts-user.txt свои домены. +Выполнить скрипт обновления листа : + /opt/zapret/ipset/get_config.sh +Настроить таймер systemd для обновления листа : + ln -fs /opt/zapret/init.d/systemd/zapret-list-update.service /lib/systemd/system + ln -fs /opt/zapret/init.d/systemd/zapret-list-update.timer /lib/systemd/system + +Принять изменения в systemd : + systemctl daemon-reload + +Включить автозапуск службы : + systemctl enable zapret + +Включить таймер обновления листа : + systemctl enable zapret-list-update.timer + +Запустить службу : + systemctl start zapret + +Шпаргалка по управлению службой и таймером : + +enable auto start : systemctl enable zapret +disable auto start : systemctl disable zapret +start : sytemctl start zapret +stop : systemctl stop zapret +status, output messages : systemctl status zapret +timer info : systemctl list-timer +delete service : systemctl disable zapret ; rm /lib/systemd/system/zapret.service +delete timer : systemctl disable zapret-list-update.timer ; rm /lib/systemd/system/zapret-list-update.* + +Centos 7+, Fedora +----------------- + +Centos с 7 версии и более-менее новые федоры построены на systemd. +В качестве пакетного менеджера используется yum. + +Установить пакеты : + yum install -y curl ipset dnsutils git + +Далее все аналогично debian. + +OpenSUSE +-------- + +Новые OpenSUSE основаны на systemd и менеджере пакетов zypper. + +Установить пакеты : + zypper --non-interactive install curl ipset + +Далее все аналогично debian, кроме расположения systemd. +В opensuse он находится не в /lib/systemd, а в /usr/lib/systemd. +Правильные команды будут : + + ln -fs /opt/zapret/init.d/systemd/zapret.service /usr/lib/systemd/system + ln -fs /opt/zapret/init.d/systemd/zapret-list-update.service /usr/lib/systemd/system + ln -fs /opt/zapret/init.d/systemd/zapret-list-update.timer /usr/lib/systemd/system + +Arch linux +---------- + +Построен на базе systemd. + +Установить пакеты : + pacman -Syy + pacman --noconfirm -S ipset curl + +Далее все аналогично debian. + +Gentoo +------ + +Эта система использует OpenRC - улучшенную версию sysvinit. +Установка пакетов производится командой : emerge +Пакеты собираются из исходников. + +Требуются все те же ipset, curl, git для скачивания с github. +git и curl по умолчанию могут присутствовать, ipset отсутствует. + + emerge ipset + +Настроить параметры согласно разделу "Выбор параметров". + +Запустить автоинсталятор бинариков. Он сам определит рабочую архитектуру и настроит все бинарики. + /opt/zapret/install_bin.sh +АЛЬТЕРНАТИВА : make -C /opt/zapret. Получите динамические бинарики под вашу ось. + +Удалить старые листы, если они были созданы ранее : + /opt/zapret/ipset/clear_lists.sh +По желанию прописать в /opt/zapret/ipset/zapret-hosts-user.txt свои домены. +Выполнить скрипт обновления листа : + /opt/zapret/ipset/get_config.sh +Зашедулить обновление листа : + crontab -e + Создать строчку "0 12 */2 * * /opt/zapret/ipset/get_config.sh" + +Подключить init скрипт : + + ln -fs /opt/zapret/init.d/sysv/zapret /etc/init.d + rc-update add zapret + +Запустить службу : + + rc-service zapret start + +Шпаргалка по управлению службой : + +enable auto start : rc-update add zapret +disable auto start : rc-update del zapret +start : rc-service zapret start +stop : rc-service zapret stop + + +Простая установка +----------------- + +install_easy.sh автоматизирует описанные выше ручные варианты процедур установки. +Он поддерживает OpenWRT, linux системы на базе systemd и MacOS. + +Для более гибкой настройки перед запуском инсталятора следует выполнить раздел "Выбор параметров". + +Если система на базе systemd, но используется не поддерживаемый инсталятором менеджер пакетов +или названия пакетов не соответствуют прописанным в инсталятор, пакеты нужно установить вручную. +Требуется : ipset curl + +ВАЖНО : Хоть инсталятор и спрашивает является ли система роутером, +настройка маршрутизации , маскарада и т.д. не входит в задачу zapret. +Роутер вдруг сам волшебно не поднимется. Предполагается, что роутер вы уже настроили сами. + +В комплекте идут статические бинарики для большинства архитектур. Какой-то из них подойдет +с вероятностью 99%. Но если у вас экзотическая система, инсталятор попробует собрать бинарики сам +через make. Для этого нужны gcc, make и необходимые -dev пакеты. Можно форсировать режим +компиляции следующим вызовом : + + install_easy.sh make + +Для MacOS готовые бинарики не поставляются, поэтому всегда выбирается вариант установки через make. +Запуск make на чистой системе вызывает автоматическую установку developer tools. В них есть все необходимое. + +Деинсталяция выполняется через uninstall_easy.sh + + +Ручная установка на openwrt/LEDE +-------------------------------- + +Установить дополнительные пакеты : +opkg update +opkg install iptables-mod-extra iptables-mod-nfqueue iptables-mod-filter iptables-mod-ipopt iptables-mod-conntrack-extra ipset curl +(ipv6) opkg install ip6tables-mod-nat +(опционально) opkg install gzip +(опционально) opkg install grep +(опционально) opkg install coreutils-sort + +ЭКОНОМИЯ МЕСТА : + +gzip от busybox в разы медленней полноценного варианта. gzip используется скриптами получения листов. +sort от busybox медленней полноценного варианта и жрет намного болше памяти. sort используется скриптами получения листов. +grep от busybox катастрофически медленный с опцией -f. она применяется в get_reestr_combined.sh. если вы не собираетесь +пользоваться этим скриптом, gnu grep можно не устанавливать +iptables-mod-nfqueue можно выкинуть, если не будем пользоваться nfqws +curl можно выкинуть, если для получения ip листа будет использоваться только get_user.sh + +Самая главная трудность - скомпилировать программы на C. Это можно сделать на linux x64 при помощи SDK, который +можно скачать с официального сайта openwrt или LEDE. Но процесс кросс компиляции - это всегда сложности. +Недостаточно запустить make как на традиционной linux системе. +Поэтому в binaries имеются готовые статические бинарики для всех самых распространенных архитектур. +Статическая сборка означает, что бинарик не зависит от типа libc (glibc, uclibc или musl) и наличия установленных so. +Его можно использовать сразу. Лишь бы подходил тип CPU. У ARM и MIPS есть несколько версий. +Скорее всего найдется рабочий вариант. Если нет - вам придется собирать самостоятельно. +Для всех поддерживаемых архитектур бинарики запакованы upx. На текущий момент все, кроме mips64. + +Скопировать директорию "zapret" в /opt на роутер. + +Если места достаточно, самый простой способ : + opkg update + opkg install git-http + mkdir /opt + cd /opt + git clone --depth 1 https://github.com/bol-van/zapret + +Если места немного : + opkg update + opkg install openssh-sftp-server unzip + ifconfig br-lan +Скачать на комп с github zip архив кнопкой "Clone or download"->Download ZIP +Скопировать средствами sftp zip архив на роутер в /tmp. + mkdir /opt + cd /opt + unzip /tmp/zapret-master.zip + mv zapret-master zapret + rm /tmp/zapret-master.zip + +Если места совсем мало : + cd /tmp + nc -l -p 1111 >zapret.tar.gz +На linux системе скачать и распаковать zapret. Оставить необходимый минимум файлов. +Запаковать в архив zapret.tar.gz. + md5sum zapret.tar.gz + nc 1111 /{tpws,nfqws,ip2net,mdig} + +После успешной установки можно удалить zapret из tmp для освобождения RAM : + rm -r /tmp/zapret + +Для более гибкой настройки перед запуском инсталятора следует выполнить раздел "Выбор параметров". + + +Android +------- + +Без рута забудьте про nfqws и tpws в режиме transparent proxy. tpws будет работать только в режиме --socks. + +Статистики наличия NFQUEUE в стоковых ядрах android у меня нет, но на первом попавшемся устройстве на базе MTK он есть. +Если NFQUEUE есть, то nfqws проверен - он работает. + +В стоковых ядрах нет поддержки ipset. В общем случае сложность задачи по поднятию ipset варьируется от +"не просто" до "почти невозможно". Если только вы не найдете готовое собранное ядро под ваш девайс. + +tpws будет работать в любом случае, он не требует чего-либо особенного. +В android нет /etc/passwd, потому опция --user не будет работать. Вместо нее можно +пользоваться числовыми user id и опцией --uid. +Рекомендую использовать gid 3003 (AID_INET). Иначе можете получить permission denied на создание сокета. +Например : --uid 1:3003 +В iptables укажите : "! --uid-owner 1" вместо "! --uid-owner tpws". +Напишите шелл скрипт с iptables и tpws, запускайте его средствами вашего рут менеджера. +Скрипты автозапуска лежат тут : +magisk : /data/adb/service.d +supersu : /system/su.d + +Я не проверял не прибивают ли новые андроиды iptables по своей прихоти в процессе работы +или при подключении/отключении wifi, mobile data, ... + +Ответ на вопрос куда поместить tpws на android без рута, чтобы потом его запускать из приложений. +Файл заливаем через adb shell в /data/local/tmp/, лучше всего в субфолдер. +mkdir /data/local/tmp/zapret +adb push tpws /data/local/tmp/zapret +chmod 755 /data/local/tmp/zapret /data/local/tmp/zapret/tpws +chcon u:object_r:system_file:s0 /data/local/tmp/zapret/tpws + +Мобильные модемы и роутеры huawei +--------------------------------- + +Устройства типа E3372, E8372, E5770 разделяют общую идеологию построения системы. +Имеются 2 вычислительных ядра. Одно ядро выполняет vxworks, другое - linux. +На 4pda имеются модицифированные прошивки с telnet и adb. Их и нужно использовать. + +Дальнейшие утверждения проверены на E8372. На других может быть аналогично или похоже. +Присутствуют дополнительные аппаратные блоки для offload-а сетевых функций. +Не весь трафик идет через linux. Исходящий трафик с самого модема проходит +цепочку OUTPUT нормально, на FORWARD =>wan часть пакетов выпадает из tcpdump. + +tpws работает обычным образом. + +nfqueue поломан. можно собрать фиксящий модуль https://github.com/im-0/unfuck-nfqueue-on-e3372h, +используя исходники с huawei open source. Исходники содержат тулчейн и полусобирающееся, +неактуальное ядро. Конфиг можно взять с рабочего модема из /proc/config.gz. +С помощью этих исходников умельцы могут собрать модуль unfuck_nfqueue.ko. +После его применения NFQUEUE и nfqws для arm работают нормально. + +Чтобы избежать проблемы с offload-ом при использвании nfqws, следует комбинировать tpws в режиме tcp proxy и nfqws. +Правила NFQUEUE пишутся для цепочки OUTPUT. +connbytes придется опускать, поскольку модуля в ядре нет. Но это не смертельно. + +Скрипт автозапуска - /system/etc/autorun.sh. Создайте свой скрипт настройки zapret, +запускайте из конца autorun.sh через "&". Скрипт должен в начале делать sleep 5, чтобы дождаться +поднятия сети и iptables от huawei. + +ПРЕДУПРЕЖДЕНИЕ. +На этом модеме происходят хаотические сбросы соединений tcp по непонятным причинам. +Выглядит это так, если запускать curl с самого модема : + curl www.ru + curl: (7) Failed to connect to www.ru port 80: Host is unreachable +Возникает ошибка сокета EHOSTUNREACH (errno -113). То же самое видно в tpws. +В броузере не подгружаются части веб страниц, картинки, стили. +В tcpdump на внешнем интерфейсе eth_x виден только единственный и безответный SYN пакет, без сообщений ICMP. +ОС каким-то образом узнает о невозможности установить TCP соединение и выдает ошибку. +Если выполнять подключение с клиента, то SYN пропадают, соединение не устанавливается. +ОС клиента проводит ретрансмиссию, и с какого-то раза подключение удается. +Поэтому без tcp проксирования в этой ситуации сайты тупят, но загружаются, а с проксированием +подключение выполняется, но вскоре сбрасывается без каких-либо данных, и броузеры не пытаются установить +его заново. Поэтому качество броузинга с tpws может быть хуже, но дело не в tpws. +Частота сбросов заметно возрастает, если запущен торент клиент, имеется много tcp соединений. +Однако, причина не в переполнении таблицы conntrack. Увеличение лимитов и очистка conntrack не помогают. +Предположительно эта особенность связана с обработкой пакетов сброса соединения в hardware offload. +Точного ответа на вопрос у меня нет. Если вы знаете - поделитесь, пожалуйста. +Чтобы не ухудшать качество броузинга, можно фильтровать заворот на tpws по ip фильтру. +Поддержка ipset отсутствует. Значит, все, что можно сделать - создать индивидуальные правила +на небольшое количество хостов. + +Некоторые наброски скриптов присутствуют в files/huawei. Не готовое решение ! Смотрите, изучайте, приспосабливайте. +Здесь можно скачать готовые полезные статические бинарики для arm, включая curl : https://github.com/bol-van/bins + + +FreeBSD, OpenBSD, MacOS +----------------------- + +Описано в docs/bsd.txt + + +Windows (WSL) +------------- + +tpws в режиме socks можно запускать и под более-менее современными билдами windows 10 и windows server +с установленным WSL. Совсем не обязательно устанавливать дистрибутив убунту, как вам напишут почти в каждой +статье про WSL, которую вы найдете в сети. tpws - статический бинарик, ему дистрибутив не нужен. + +Установить WSL : dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all +Скопировать на целевую систему binaries/x86_64/tpws_wsl.tgz. +Выполнить : wsl --import tpws "%USERPROFILE%\tpws" tpws_wsl.tgz +Запустить : wsl --exec /tpws --uid=1 --no-resolve --socks --bind-addr=127.0.0.1 --port=1080 <параметры_дурения> +Прописать socks 127.0.0.1:1080 в броузер или другую программу. + +Удаление : wsl --unregister tpws + +Проверено на windows 10 build 19041 (20.04). + +ЗАМЕЧАНИЕ. Под Windows существует нативное решение GoodByeDPI, выполняющее дурение на пакетном уровне (по типу nfqws). + + +Другие прошивки +--------------- + +Для статических бинариков не имеет значения на чем они запущены : PC, android, приставка, роутер, любой другой девайс. +Подойдет любая прошивка, дистрибутив linux. Статические бинарики запустятся на всем. +Им нужно только ядро с необходимыми опциями сборки или модулями. +Но кроме бинариков в проекте используются еще и скрипты, в которых задействуются некоторые +стандартные программы. + +ЗАМЕЧАНИЕ. Как показала практика, на некоторых ядрах бинарики с upx падают в segfault. +Если это ваш случай, скачайте upx и распакуйте бинарики. Распаковать можно на любой системе и любой архитектуре. + +Основные причины почему нельзя просто так взять и установить эту систему на что угодно : + * отсутствие доступа к девайсу через shell + * отсутствие рута + * отсутствие раздела r/w для записи и энергонезависимого хранения файлов + * отсутствие возможности поставить что-то в автозапуск + * отсутствие cron + * недостаток модулей ядра или опций его сборки + * недостаток модулей iptables (/usr/lib/iptables/lib*.so) + * недостаток стандартных программ (типа ipset, curl) или их кастрированность (облегченная замена) + * кастрированный или нестандартный шелл sh + +Если в вашей прошивке есть все необходимое, то вы можете адаптировать zapret под ваш девайс в той или иной степени. +Может быть у вас не получится поднять все части системы, однако вы можете хотя бы попытаться +поднять tpws и завернуть на него через -j REDIRECT весь трафик на порт 80. +Если вам есть куда записать tpws, есть возможность выполнять команды при старте, то как минимум +это вы сделать сможете. Скорее всего поддержка REDIRECT в ядре есть. Она точно есть на любом роутере, +на других устройствах под вопросом. NFQUEUE, ipset на большинстве прошивок отсутствуют из-за ненужности. + +Пересобрать ядро или модули для него будет скорее всего достаточно трудно. +Для этого вам необходимо будет по крайней мере получить исходники вашей прошивки. +User mode компоненты могут быть привнесены относительно безболезненно, если есть место куда их записать. +Специально для девайсов, имеющих область r/w, существует проект entware. +Некоторые прошивки даже имеют возможность его облегченной установки через веб интерфейс. +entware содержит репозиторий user-mode компонент, которые устанавливаются в /opt. +С их помощью можно компенсировать недостаток ПО основной прошивки, за исключением ядра. + +Подробное описание настроек для других прошивок выходит за рамки данного проекта. + +Openwrt является одной из немногих относительно полноценных linux систем для embedded devices. +Она характеризуется следующими вещами, которые и послужили основой выбора именно этой прошивки : + * полный root доступ к девайсу через shell. на заводских прошивках чаще всего отсутствует, на многих альтернативных есть + * корень r/w. это практически уникальная особенность openwrt. заводские и большинство альтернативных прошивок + построены на базе squashfs root (r/o), а конфигурация хранится в специально отформатированной области + встроенной памяти, называемой nvram. не имеющие r/w корня системы сильно кастрированы. они не имеют + возможности доустановки ПО из репозитория без специальных вывертов и заточены в основном + на чуть более продвинутого, чем обычно, пользователя и управление имеющимся функционалом через веб интерфейс, + но функционал фиксированно ограничен. альтернативные прошивки как правило могут монтировать r/w раздел + в какую-то область файловой системы, заводские обычно могут монтировать лишь флэшки, подключенные к USB, + и не факт, что есть поддержка unix файловых системы. может быть поддержка только fat и ntfs. + * возможность выноса корневой файловой системы на внешний носитель (extroot) или создания на нем оверлея (overlay) + * наличие менеджера пакетов opkg и репозитория софта + * в репозитории есть все модули ядра, их можно доустановить через opkg. ядро пересобирать не нужно. + * в репозитории есть все модули iptables, их можно доустановить через opkg + * в репозитории есть огромное количество стандартных программ и дополнительного софта + * наличие SDK, позволяющего собрать недостающее + + +Обход блокировки https +---------------------- + +ИНОГДА (но нечасто) работает трюк со сплитом tls handshake на 2 части. +Это можно сделать все теми же средствами. nfqws --dpi-desync=split2 или tpws --split-pos. +У tpws --split-pos - единственный параметр, который работает на не-HTTP трафике, все остальное работать не будет. + +ЧАЩЕ на https работает атака DPI desync, но у нее есть свои нюансы настройки. + +Если ничего не работает, приходится перенаправлять трафик через сторонний хост. +Предлагается использовать прозрачный редирект через socks5 посредством iptables+redsocks, либо iptables+iproute+vpn. +Настройка варианта с redsocks на openwrt описана в https.txt. +Настройка варианта с iproute+wireguard - в wireguard_iproute_openwrt.txt. + + +Почему стоит вложиться в покупку VPS +------------------------------------ + +VPS - это виртуальный сервер. Существует огромное множество датацентров, предлагающих данную услугу. +На VPS могут выполняться какие угодно задачи. От простого веб сайта до навороченной системы собственной разработки. +Можно использовать VPS и для поднятия собственного vpn или прокси. +Сама широта возможных способов применения , распространенность услуги сводят к минимуму возможности +регуляторов по бану сервисов такого типа. Да, если введут белые списки, то решение загнется, но это будет уже другая +реальность, в которой придется изобретать иные решения. +Пока этого не сделали, никто не будет банить хостинги просто потому , что они предоставляют хостинг услуги. +Вы как индивидуум скорее всего никому не нужны. Подумайте чем вы отличаетесь от известного VPN провайдера. +VPN провайдер предоставляет _простую_ и _доступную_ услугу по обходу блокировок для масс. +Этот факт делает его первоочередной целью блокировки. РКН направит уведомление, после отказа сотрудничать +заблокирует VPN. Предоплаченная сумма пропадет. +У регуляторов нет и никогда не будет ресурсов для тотальной проверки каждого сервера в сети. +Возможен китайский расклад, при котором DPI выявляет vpn протоколы и динамически банит IP серверов, +предоставляющих нелицензированный VPN. Но имея знания, голову, вы всегда можете обфусцировать +vpn трафик или применить другие типы VPN, более устойчивые к анализу на DPI или просто менее широкоизвестные, +а следовательно с меньшей вероятностью обнаруживамые регулятором. +У вас есть свобода делать на вашем VPS все что вы захотите, адаптируясь к новым условиям. +Да, это потребует знаний. Вам выбирать учиться и держать ситуацию под контролем, когда вам ничего запретить +не могут, или покориться системе. + +VPS можно прибрести в множестве мест. Существуют специализированные на поиске предложений VPS порталы. +Например, вот этот : https://vps.today/ +Для персонального VPN сервера обычно достаточно самой минимальной конфигурации, но с безлимитным трафиком или +с большим лимитом по трафику (терабайты). Важен и тип VPS. Openvz подойдет для openvpn, но +вы не поднимете на нем wireguard, ipsec, то есть все, что требует kernel mode. +Для kernel mode требуется тип виртуализации, предполагающий запуск полноценного экземпляра ОС linux +вместе с ядром. Подойдут kvm, xen, hyper-v, vmware. + +По цене можно найти предложения, которые будут дешевле готовой VPN услуги, но при этом вы сам хозяин в своей лавке +и не рискуете попасть под бан регулятора, разве что "заодно" под ковровую бомбардировку с баном миллионов IP. +Кроме того, если вам совсем все кажется сложным, прочитанное вызывает ступор, и вы точно знаете, что ничего +из описанного сделать не сможете, то вы сможете хотя бы использовать динамическое перенаправление портов ssh +для получения шифрованного socks proxy и прописать его в броузер. Знания linux не нужны совсем. +Это вариант наименее напряжный для чайников, хотя и не самый удобный в использовании. diff --git a/docs/wireguard/010-wg-mod.patch b/docs/wireguard/010-wg-mod.patch new file mode 100644 index 0000000..1577da6 --- /dev/null +++ b/docs/wireguard/010-wg-mod.patch @@ -0,0 +1,133 @@ +Index: WireGuard-0.0.20190123/src/cookie.c +=================================================================== +--- WireGuard-0.0.20190123.orig/src/cookie.c ++++ WireGuard-0.0.20190123/src/cookie.c +@@ -193,6 +193,8 @@ void wg_cookie_message_create(struct mes + xchacha20poly1305_encrypt(dst->encrypted_cookie, cookie, COOKIE_LEN, + macs->mac1, COOKIE_LEN, dst->nonce, + checker->cookie_encryption_key); ++ // MOD : randomize trash ++ dst->header.trash = gen_trash(); + } + + void wg_cookie_message_consume(struct message_handshake_cookie *src, +Index: WireGuard-0.0.20190123/src/messages.h +=================================================================== +--- WireGuard-0.0.20190123.orig/src/messages.h ++++ WireGuard-0.0.20190123/src/messages.h +@@ -53,23 +53,41 @@ enum limits { + MAX_QUEUED_PACKETS = 1024 /* TODO: replace this with DQL */ + }; + ++/* + enum message_type { +- MESSAGE_INVALID = 0, +- MESSAGE_HANDSHAKE_INITIATION = 1, +- MESSAGE_HANDSHAKE_RESPONSE = 2, +- MESSAGE_HANDSHAKE_COOKIE = 3, +- MESSAGE_DATA = 4 ++ MESSAGE_INVALID = 0, ++ MESSAGE_HANDSHAKE_INITIATION = 1, ++ MESSAGE_HANDSHAKE_RESPONSE = 2, ++ MESSAGE_HANDSHAKE_COOKIE = 3, ++ MESSAGE_DATA = 4 + }; ++*/ ++ ++// MOD : message type ++enum message_type { ++ MESSAGE_INVALID = 0xE319CCD0, ++ MESSAGE_HANDSHAKE_INITIATION = 0x48ADE198, ++ MESSAGE_HANDSHAKE_RESPONSE = 0xFCA6A8F3, ++ MESSAGE_HANDSHAKE_COOKIE = 0x64A3BB18, ++ MESSAGE_DATA = 0x391820AA ++}; ++ ++// MOD : generate fast trash without true RNG ++__le32 gen_trash(void); + + struct message_header { +- /* The actual layout of this that we want is: +- * u8 type +- * u8 reserved_zero[3] +- * +- * But it turns out that by encoding this as little endian, +- * we achieve the same thing, and it makes checking faster. +- */ +- __le32 type; ++ /* The actual layout of this that we want is: ++ * u8 type ++ * u8 reserved_zero[3] ++ * ++ * But it turns out that by encoding this as little endian, ++ * we achieve the same thing, and it makes checking faster. ++ */ ++ ++ // MOD : trash field to change message size and add 4 byte offset to all fields ++ __le32 trash; ++ ++ __le32 type; + }; + + struct message_macs { +Index: WireGuard-0.0.20190123/src/noise.c +=================================================================== +--- WireGuard-0.0.20190123.orig/src/noise.c ++++ WireGuard-0.0.20190123/src/noise.c +@@ -17,6 +17,24 @@ + #include + #include + ++ ++// MOD : trash generator ++__le32 gtrash = 0; ++__le32 gen_trash(void) ++{ ++ if (gtrash) ++ gtrash = gtrash*1103515243 + 12345; ++ else ++ // first value is true random ++ get_random_bytes_wait(>rash, sizeof(gtrash)); ++ return gtrash; ++} ++ + /* This implements Noise_IKpsk2: + * + * <- s +@@ -515,6 +533,10 @@ wg_noise_handshake_create_initiation(str + &handshake->entry); + + handshake->state = HANDSHAKE_CREATED_INITIATION; ++ ++ // MOD : randomize trash ++ dst->header.trash = gen_trash(); ++ + ret = true; + + out: +@@ -655,6 +677,10 @@ bool wg_noise_handshake_create_response( + &handshake->entry); + + handshake->state = HANDSHAKE_CREATED_RESPONSE; ++ ++ // MOD : randomize trash ++ dst->header.trash = gen_trash(); ++ + ret = true; + + out: +Index: WireGuard-0.0.20190123/src/send.c +=================================================================== +--- WireGuard-0.0.20190123.orig/src/send.c ++++ WireGuard-0.0.20190123/src/send.c +@@ -200,6 +200,10 @@ static bool encrypt_packet(struct sk_buf + header->header.type = cpu_to_le32(MESSAGE_DATA); + header->key_idx = keypair->remote_index; + header->counter = cpu_to_le64(PACKET_CB(skb)->nonce); ++ ++ // MOD : randomize trash ++ header->header.trash = gen_trash(); ++ + pskb_put(skb, trailer, trailer_len); + + /* Now we can encrypt the scattergather segments */ diff --git a/docs/wireguard/wireguard-mod.txt b/docs/wireguard/wireguard-mod.txt new file mode 100644 index 0000000..5cb7e3f --- /dev/null +++ b/docs/wireguard/wireguard-mod.txt @@ -0,0 +1,244 @@ +Посвящено возможной блокировке в РФ VPN протоколов через DPI. +Предпосылками являются последние законодательные акты и во всю сочащиеся "секретные" записки. +В РФ разрабатываются и готовятся к применению более продвинутые решения по блокировке трафика. +Вполне вероятно будут резать стандартные VPN протоколы. Нам надо быть к этому готовыми. + +Один из возможных и перспективных путей решения данного вопроса - кустомная модификация +исходников VPN с целью незначительного изменения протокола, ломающего стандартные модули обнаружения в DPI. +Это относительно сложно, доступно только для гиков. +Никто не будет разрабатывать специальные модули обнаружения в DPI, если только кто-то не сделает простое и +удобное решение для всех, и его станут широко применять. Но это маловероятно, и даже если и так, +то всегда можно модифицировать протокол чуток по другому. Делать моды для DPI несравненно дольше +и дороже, чем клепать на коленке изменения протокола для wireguard. + + +ЗАМЕЧЕНИЕ : альтернативой модификации конечного софта для VPN является использование "навесных" +обфускаторов. см : https://github.com/bol-van/ipobfs + + +Рассмотрю что нам надо пропатчить в wireguard. Модифицированный wireguard проверен на виртуалках +с десктопным linux, он работает, сообщения в wireshark действительно не вписываются в стандартный +протокол и не опознаются. + +Wireguard протокол очень простой. Все сообщения описаны в messages.h +Поставим себе целью сделать 2 простые модификации : +1) Добавим в начало всех сообщений немного мусора, чтобы изменить размер сообщений и смещения полей +2) Изменим коды типов сообщений +Этого может быть вполне достаточно для обмана DPI + +--messages.h-------------------------- +/* +enum message_type { + MESSAGE_INVALID = 0, + MESSAGE_HANDSHAKE_INITIATION = 1, + MESSAGE_HANDSHAKE_RESPONSE = 2, + MESSAGE_HANDSHAKE_COOKIE = 3, + MESSAGE_DATA = 4 +}; +*/ + +// MOD : message type +enum message_type { + MESSAGE_INVALID = 0xE319CCD0, + MESSAGE_HANDSHAKE_INITIATION = 0x48ADE198, + MESSAGE_HANDSHAKE_RESPONSE = 0xFCA6A8F3, + MESSAGE_HANDSHAKE_COOKIE = 0x64A3BB18, + MESSAGE_DATA = 0x391820AA +}; + +// MOD : generate fast trash without true RNG +__le32 gen_trash(void); + +struct message_header { + /* The actual layout of this that we want is: + * u8 type + * u8 reserved_zero[3] + * + * But it turns out that by encoding this as little endian, + * we achieve the same thing, and it makes checking faster. + */ + + // MOD : trash field to change message size and add 4 byte offset to all fields + __le32 trash; + + __le32 type; +}; +-------------------------------------- + +Напишем функцию для генерации trash. Функция должна быть быстрая, важно не замедлить скорость. +Мы не расчитываем, что нас будут специально ловить, иначе бы пришлось делать полноценный обфускатор. +Задача лишь сломать стандартный модуль обнаружения протокола wireguard. Потому истинная рандомность +trash не важна. +Но все же немного "трэша" не повредит. Гонки между тредами так же пофигистичны. Это же трэш. + +--noise.c----------------------------- +// MOD : trash generator +__le32 gtrash = 0; +__le32 gen_trash(void) +{ + if (gtrash) + gtrash = gtrash*1103515243 + 12345; + else + // first value is true random + get_random_bytes_wait(>rash, sizeof(gtrash)); + return gtrash; +} +-------------------------------------- + +Теперь осталось найти все места, где создаются сообщения и внести туда заполнение поля trash. +Сообщений всего 4. Их можно найти по присваиванию полю type одного из значений enum message_type. + +2 места в noise.c в функциях wg_noise_handshake_create_initiation и wg_noise_handshake_create_response, +1 место в cookie.c в функции wg_cookie_message_create +Дописываем в конец инициализации структуры сообщения : + +-------------------------------------- + // MOD : randomize trash + dst->header.trash = gen_trash(); +-------------------------------------- + +и 1 место в send.c в функции encrypt_packet + +-------------------------------------- + // MOD : randomize trash + header->header.trash = gen_trash(); +-------------------------------------- + + +Вот и весь патчинг. Полный patch (версия wireguard 0.0.20190123) лежит в 010-wg-mod.patch. +Патчинг кода - самое простое. Для десктопного linux дальше все просто. +Пересобираем через make, устанавливаем через make install, перегружаем +модуль wireguard, перезапускаем интерфейсы, и все готово. + +Настоящий геморой начнется когда вы это попытаетесь засунуть на роутер под openwrt. +Одна из больших проблем linux - отсутствие совместимости драйверов на уровне бинариков. +Поэтому собирать необходимо в точности под вашу версию ядра и в точности под его .config. +Вам придется либо полностью самостоятельно собирать всю прошивку, либо найти SDK в точности +от вашей версии прошивки для вашей архитектуры и собрать модуль с помощью этого SDK. +Последний вариант более легкий. +Для сборки вам понадобится система на linux x86_64. Ее можно установить в виртуалке. +Теоретически можно пользоваться WSL из win10, но на практике там очень медленное I/O, +по крайней мере на старых версиях win10. Безумно медленное. Будете собирать вечность. +Может в новых win10 что-то и улучшили, но я бы сразу расчитывал на полноценный linux. + +Находим здесь вашу версию : https://downloads.openwrt.org/ +Скачиваем файл openwrt-sdk-*.tar.xz или lede-sdk-*.tar.xz +Например : https://downloads.openwrt.org/releases/18.06.2/targets/ar71xx/generic/openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64.tar.xz +Если ваша версия непонятна или стара, то проще будет найти последнюю прошивку и перешить роутер. +Распаковываем SDK. Следующими командами можно собрать оригинальный вариант wireguard : + +# scripts/feeds update -a +# scripts/feeds install -a +# make defconfig +# make -j 4 package/wireguard/compile + +Сборка будет довольно долгой. Ведь придется подтащить ядро, собрать его, собрать зависимости. +"-j 4" означает использовать 4 потока. Впишите вместо 4 количество доступных cpu cores. + +Получим следующие файлы : + +openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/bin/targets/ar71xx/generic/packages/kmod-wireguard_4.9.152+0.0.20190123-1_mips_24kc.ipk +openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/bin/packages/mips_24kc/base/wireguard-tools_0.0.20190123-1_mips_24kc.ipk + +Но это будет оригинальный wireguard. Нам нужен патченый. +Установим quilt и mc для нормального редактора вместо vim : + +# sudo apt-get update +# sudo apt-get install quilt mc + +# make package/wireguard/clean +# make package/wireguard/prepare V=s QUILT=1 + + +Сорцы приготовлены для сборки в : + openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/build_dir/target-mips_24kc_musl/linux-ar71xx_generic/WireGuard-0.0.20190123/src + +# cd build_dir/target-mips_24kc_musl/linux-ar71xx_generic/WireGuard-0.0.20190123/src +# quilt push -a +# quilt new 010-wg-mod.patch +# export EDITOR=mcedit + +Далее будет открываться редактор mcedit, в который нужно вносить изменения в каждый файл : + +# quilt edit messages.h +# quilt edit cookie.c +# quilt edit noise.c +# quilt edit send.c +# quilt diff +# quilt refresh + +Получили файл патча в : +openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/build_dir/target-mips_24kc_musl/linux-ar71xx_generic/WireGuard-0.0.20190123/patches/010-wg-mod.patch + +Выходим в корень SDK. + +# make package/wireguard/compile V=99 + +Если не было ошибок, то получили измененные ipk. +Патч можно зафиксировать в описании пакета : + +# make package/wireguard/update + +Получим : +openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/feeds/base/package/network/services/wireguard/patches/010-wg-mod.patch +При последующей очистке и пересборке он будет автоматом применяться. + + +АЛЬТЕРНАТИВА : можно не возиться с quilt. +сделайте +# make package/wireguard/clean +# make package/wireguard/prepare +и напрямую модифицируйте или копируйте файлы в + openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/build_dir/target-mips_24kc_musl/linux-ar71xx_generic/WireGuard-0.0.20190123/src +затем +# make package/wireguard/compile + +Если нужно поменять версию wireguard, то идите в +openwrt-sdk-18.06.2-ar71xx-generic_gcc-7.3.0_musl.Linux-x86_64/feeds/base/package/network/services/wireguard/Makefile +поменяйте там версию в PKG_VERSION на последнюю из : https://git.zx2c4.com/WireGuard +скачайте tar.xz с этой версией , вычислите его sha256sum, впишите в PKG_HASH + +1 раз где-нибудь пропатчите файлы последней версии wireguard в текстовом редакторе, скопируйте в build_dir, +сделайте версию для openwrt. эти же файлы скопируйте на ваш сервер с десктопным linux, сделайте там make / make install + +Но имейте в виду, что build_dir - локация для временных файлов. +make clean оттуда все снесет, включая ваши модификации. Модифицированные файлы лучше сохранить отдельно, +чтобы потом было легко скопировать обратно. + +Полученные ipk копируем на роутер в /tmp, устанавливаем через +# cd /tmp +# rm -r /tmp/opkg-lists +# opkg install *.ipk +Если требует зависимостей, то +# opkg update +# opkg install .... <зависимости> +# rm -r /tmp/opkg-lists +# opkg install *.ipk + +В /tmp/opkg-lists opkg хранит кэш списка пакетов. Если попытаться установить файл ipk, и такой же пакет +найдется в репозитории, opkg будет устанавливать из репозитория. А нам это не надо. + +# rmmod wireguard +# kmodloader +# dmesg | tail +должны увидеть что-то вроде : +[8985.415490] wireguard: WireGuard 0.0.20190123 loaded. See www.wireguard.com for information. +[8985.424178] wireguard: Copyright (C) 2015-2019 Jason A. Donenfeld . All Rights Reserved. +значит модуль загрузился + +Могут понадобиться ключи opkg --force-reinstall, --force-depends. +--force-depends поможет при несоответствии hash версии ядра. То есть версия x.x.x та же самая, но hash конфигурации разный. +При несоответствии x.x.x вы что-то делаете не так, работать это не будет. +Например : 4.14.56-1-b1186491495127cc6ff81d29c00a91fc, 4.14.56-1-3f8a21a63974cfb7ee67e41f2d4b805d +Это свидетельствует о несоответствии .config ядра при сборке прошивки и в SDK. +Если несоответствие легкое, то может все прокатить, но при более серьезной разнице в .config модуль может не загрузиться +или вызвать стабильные или хаотические падения ядра и перезагрузки (включая вариант беcконечной перезагрузки - bootloop). +Так что перед --force-depends убедитесь, что знаете как лечится такая ситуация, и не стоит это делать при отсутствии физического +доступа к девайсу. + +Когда поднимите линк, и вдруг ничего не будет работать, то посмотрите в wireshark udp пакеты +на порт endpoint. Они не должны начинаться с 0,1,2,3,4. В первых 4 байтах должен быть рандом, +в следующих 4 байтах - значения из измененного enum message_type. Если пакет все еще начинается с 0..4, +значит модуль wireguard оригинальный, что-то не собралось, не скопировалось, не перезапустилось. +В противном случае должен подняться линк, пинги ходить. Значит вы победили, поздравляю. +Регулятору будет намного сложнее поймать ваш VPN. diff --git a/docs/wireguard/wireguard_iproute_openwrt.txt b/docs/wireguard/wireguard_iproute_openwrt.txt new file mode 100644 index 0000000..9e7aa5a --- /dev/null +++ b/docs/wireguard/wireguard_iproute_openwrt.txt @@ -0,0 +1,519 @@ +Есть возможность поднять свой VPN сервер ? Не хотим использовать redsocks ? +Хотим завертывать на VPN только часть трафика ? +Например, из ipset zapret только порт tcp:443, из ipban - весь трафик, не только tcp ? +Да, с VPN такое возможно. +Опишу понятийно как настраивается policy based routing в openwrt на примере wireguard. +Вместо wireguard можно использовать openvpn или любой другой. Но wireguard прекрасен сразу несколькими вещами. +Главная из которых - в разы большая скорость, даже немного превышающая ipsec. +Ведь openvpn основан на tun, а tun - всегда в разы медленнее решения в kernel mode, +и если для PC оно может быть не так актуально, для soho роутеров - более чем. +Wireguard может дать 50 mbps там, где openvpn еле тащит 10. +Но есть и дополнительное требование. Wireguard работает в ядре, значит ядро должно +быть под вашим контролем. vps на базе openvz не подойдет ! Нужен xen, kvm, +любой другой вариант, где загружается ваше собственное ядро, а не используется +общее, разделяемое на множество vps. В openvz вам никто не даст лезть в ядро. + +Если вдруг окажется, что основные VPN протоколы блокируется DPI, включая wireguard, +то стоит смотреть в сторону либо обфускации трафика до состояния нераспознаваемого +мусора, либо маскировки под TLS (лучше на порт 443). Скорость, конечно, вы потеряете, но это +та самая ситуация, которая описывается словами "медленно или никак". +Маскированные под TLS протоколы DPI может распознать двумя действиями : +пассивно через анализ статистических характеристик пакетов (время, размер, периодичность, ..) +или активно через подключение к вашему серверу от себя и попытку поговорить с сервером по +известным протоколам (называется active probing). Если вы подключаетесь к серверу +с фиксированных IP, то активный пробинг можно надежно заблокировать через ограничение +диапазонов IP адресов, с которых можно подключаться к серверу. В ином случае можно использовать +технику "port knocking". +Перспективным направлением так же считаю легкую собственную модификацию исходников +существующих VPN с целью незначительного изменения протокола, которая ломает стандартные +модули обнаружения в DPI. В wireguard можно добавить в начало пакета handshake лишнее поле, +заполненное случайным мусором. Разумеется, в таком случае требуется держать измененную версию +как на сервере, так и на клиенте. Если затея срабатывает, то вы получаете максимальную +скорость, при этом полностью нагибая регулятора. +Полезная инфа по теме : https://habr.com/ru/post/415977/ + +Понятийно необходимо выполнить следующие шаги : +1) Поднять vpn сервер. +2) Настроить vpn клиент. Результат этого шага - получение поднятого интерфейса vpn. +Будь то wireguard, openvpn или любой другой тип vpn. +3) Создать такую схему маршрутизации, при которой пакеты, помечаемые особым mark, +попадают на vpn, а остальные идут обычным способом. +4) Создать правила, выставляющие mark для всего трафика, который необходимо рулить на vpn. +Критерии могут быть любые, ограниченные лишь возможностями iptables и вашим воображением. + +Будем считать наш vpn сервер находится на ip 91.15.68.202. +Вешать его будем на udp порт 12345. На этот же порт будем вешать и клиентов. +Сервер работает под debian 9. Клиент работает под openwrt. +Для vpn отведем подсеть 192.168.254.0/24. + +--- Поднятие сервера --- + +На сервере должны быть установлены заголовки ядра (linux-headers-...) и компилятор gcc. +Качаем последний tar.xz с wireguard отсюда : https://git.zx2c4.com/WireGuard/ + +# tar xf WireGuard*.tar.xz +# cd WireGuard-*/src +# make +# strip --strip-debug wireguard.ko +# sudo make install + +wireguard основан на понятии криптороутинга. Каждый пир (сервер - тоже пир) +имеет пару открытый/закрытый ключ. Закрытый ключ остается у пира, +открытый прописывается у его партнера. Каждый пир авторизует другого +по знанию приватного ключа, соответствующего прописанному у него публичному ключу. +Протокол построен таким образом, что на все неправильные udp пакеты не следует ответа. +Не знаешь приватный ключ ? Не смог послать правильный запрос ? Долбись сколько влезет, +я тебе ничего не отвечу. Это защищает от активного пробинга со стороны DPI и просто +экономит ресурсы. +Значит первым делом нужно создать 2 пары ключей : для сервера и для клиента. +wg genkey генерит приватный ключ, wg pubkey получает из него публичный ключ. + +# wg genkey +oAUkmhoREtFQ5D5yZmeHEgYaSWCcLYlKe2jBP7EAGV0= +# echo oAUkmhoREtFQ5D5yZmeHEgYaSWCcLYlKe2jBP7EAGV0= | wg pubkey +bCdDaPYSTBZVO1HTmKD+Tztuf3PbOWGDWfz7Lb1E6C4= +# wg genkey +OKXX0TSlyjJmGt3/yHlHxi0AqjJ0vh+Msne3qEHk0VM= +# echo OKXX0TSlyjJmGt3/yHlHxi0AqjJ0vh+Msne3qEHk0VM= | wg pubkey +EELdA2XzjcKxtriOCPBXMOgxlkgpbRdIyjtc3aIpkxg= + +Пишем конфиг +--/etc/wireguard/wgvps.conf------------------- +[Interface] +PrivateKey = OKXX0TSlyjJmGt3/yHlHxi0AqjJ0vh+Msne3qEHk0VM= +ListenPort = 12345 + +[Peer] +#Endpoint = +PublicKey = bCdDaPYSTBZVO1HTmKD+Tztuf3PbOWGDWfz7Lb1E6C4= +AllowedIPs = 192.168.254.3 +PersistentKeepalive=20 +---------------------------------------------- + +Wireguard - минималистичный vpn. В нем нет никаких средств для автоконфигурации ip. +Все придется прописывать руками. +В wgvps.conf должны быть перечислены все пиры с их публичными ключами, +а так же прописаны допустимые для них ip адреса. +Назначим нашему клиенту 192.168.254.3. Сервер будет иметь ip 192.168.254.1. +Endpoint должен быть прописан хотя бы на одном пире. +Если endpoint настроен для пира, то wireguard будет периодически пытаться к нему подключиться. +В схеме клиент/сервер у сервера можно не прописывать endpoint-ы пиров, что позволит +менять ip и быть за nat. Endpoint пира настраивается динамически после успешной фазы +проверки ключа. + +Включаем маршрутизцию : +# echo net.ipv4.ip_forward = 1 >>/etc/sysctl.conf +# sysctl -p + +Интерфейс конфигурится стандартно для дебианоподобных систем : + +--/etc/network/interfaces.d/wgvps------------- +auto wgvps +iface wgvps inet static + address 192.168.254.1 + netmask 255.255.255.0 + pre-up ip link add $IFACE type wireguard + pre-up wg setconf $IFACE /etc/wireguard/$IFACE.conf + post-up iptables -t nat -A POSTROUTING -o eth0 -s 192.168.254.0/24 -j MASQUERADE + post-up iptables -A FORWARD -o eth0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + post-down iptables -D FORWARD -o eth0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + post-down iptables -t nat -D POSTROUTING -o eth0 -s 192.168.254.0/24 -j MASQUERADE + post-down ip link del $IFACE +---------------------------------------------- + +Поднятие через ifup wgvps, опускание через ifdown wgvps. +При поднятии интерфейса заодно настраивается nat. eth0 здесь означает интерфейс vpn сервера с инетовским ip адресом. +Если у вас какая-то система управления фаерволом, то надо настройку nat прикручивать туда. +Пример написан для простейшего случая, когда никаких ограничений нет, таблицы iptables пустые. +Чтобы посмотреть текущие настройки wireguard, запустите 'wg' без параметров. + + +--- Поднятие клиента --- + +# opkg update +# opkg install wireguard + +Добавляем записи в конфиги. + +--/etc/config/network-------------------------- +config interface 'wgvps' + option proto 'wireguard' + option auto '1' + option private_key 'oAUkmhoREtFQ5D5yZmeHEgYaSWCcLYlKe2jBP7EAGV0=' + option listen_port '12345' + option metric '9' + option mtu '1420' + +config wireguard_wgvps + option public_key 'EELdA2XzjcKxtriOCPBXMOgxlkgpbRdIyjtc3aIpkxg= + list allowed_ips '0.0.0.0/0' + option endpoint_host '91.15.68.202' + option endpoint_port '12345' + option route_allowed_ips '0' + option persistent_keepalive '20' + +config interface 'wgvps_ip' + option proto 'static' + option ifname '@wgvps' + list ipaddr '192.168.254.3/24' + +config route + option interface 'wgvps' + option target '0.0.0.0/0' + option table '100' + +config rule + option mark '0x800/0x800' + option priority '100' + option lookup '100' +------------------------------------------------ + +--/etc/config/firewall-------------------------- +config zone + option name 'tunvps' + option output 'ACCEPT' + option input 'REJECT' + option masq '1' + option mtu_fix '1' + option forward 'REJECT' + option network 'wgvps wgvps_ip' + +config forwarding + option dest 'tunvps' + option src 'lan' + +config rule + option name 'Allow-ICMP-tunvps' + option src 'tunvps' + option proto 'icmp' + option target 'ACCEPT' + +config rule + option target 'ACCEPT' + option src 'wan' + option proto 'udp' + option family 'ipv4' + option src_port '12345' + option src_ip '91.15.68.202' + option name 'WG-VPS' +------------------------------------------------ + +Что тут было сделано : +*) Настроен интерфейс wireguard. Указан собственный приватный ключ. +*) Настроен пир-партнер с указанием его публичнго ключа и endpoint (ip:port нашего сервера) + такая настройка заставит периодически долбиться на сервер по указанному ip + route_allowed_ip '0' запрещает автоматическое создание маршрута + allowed_ips '0.0.0.0/0' разрешает пакеты с любым адресом источника. + ведь мы собираемся подключаться к любым ip в инете + persistent_keepalive '20' помогает исключить дропание mapping на nat-е, если мы сидим за ним, + да и вообще полезная вещь, чтобы не было подвисших пиров +*) Статическая конфигурация ip интерфейса wgvps. +*) Маршрут default route на wgvps в отдельной таблице маршрутизации с номером 100. Аналог команды ip route add .. table 100 +*) Правило использовать таблицу 100 при выставлении в mark бита 0x800. Аналог команды ip rule. +*) Отдельная зона фаервола для VPN - 'tunvps'. В принципе ее можно не создавать, можете приписать интерфейс к зоне wan. + Но в случае с отдельной зоной можно настроить особые правила на подключения с vpn сервера в сторону клиента. +*) Разрешение форвардинга между локалкой за роутером и wgvps. +*) Разрешение принимать icmp от vpn сервера, включая пинги. ICMP жизненно важны для правильного функционирования ip сети ! +*) И обязательно проткнуть дырку в фаерволе, чтобы принимать пакеты wireguard со стороны инетовского ip vpn сервера. + +# fw3 restart +# ifup wgvps +# ifconfig wgvps +# ping 192.168.254.1 + +Если все хорошо, должны ходить пинги. +С сервера не помешает : +# ping 192.168.254.3 + + +--- Маркировка трафика --- + +Завернем на vpn все из ipset zapret на tcp:443 и все из ipban. +OUTPUT относится к исходящим с роутера пакетам, PREROUTING - ко всем остальным. +Если с самого роутера ничего заруливать не надо, можно опустить все до команд с PREROUTING. + +--/etc/firewall.user---------------------------- +. /opt/zapret/init.d/openwrt/functions + +create_ipset no-update + +network_find_wan_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 -j MARK --set-mark 0x800/0x800 + ipt OUTPUT -t mangle -o $DEVICE -m set --match-set ipban dst -j MARK --set-mark 0x800/0x800 +done + +network_get_device DEVICE lan +ipt PREROUTING -t mangle -i $DEVICE -p tcp --dport 443 -m set --match-set zapret dst -j MARK --set-mark 0x800/0x800 +ipt PREROUTING -t mangle -i $DEVICE -m set --match-set ipban dst -j MARK --set-mark 0x800/0x800 +------------------------------------------------ + +# fw3 restart + + +--- По поводу двойного NAT --- + +В описанной конфигурации nat выполняется дважды : на роутере-клиенте происходит замена адреса источника из LAN +на 192.168.254.3 и на сервере замена 192.168.254.3 на внешний адрес сервера в инете. +Зачем так делать ? Исключительно для простоты настройки. Но если вы готовы чуток еще поднапрячься и не хотите двойного nat, +то можете вписать в /etc/config/firewall "masq '0'", на сервер дописать маршрут до вашей подсети lan. +Чтобы не делать это для каждого клиента, можно отвести под всех клиентов диапазон 192.168.0.0-192.168.127.255 +и прописать его одним маршрутом. + +--/etc/network/interfaces.d/wgvps------------- + post-up ip route add dev $IFACE 192.168.0.0/17 + post-down ip route del dev $IFACE 192.168.0.0/17 +---------------------------------------------- + +Так же необходимо указать wireguard дополнительные разрешенные ip для peer : + +--/etc/wireguard/wgvps.conf------------------- +[Peer] +PublicKey = bCdDaPYSTBZVO1HTmKD+Tztuf3PbOWGDWfz7Lb1E6C4= +AllowedIPs = 192.168.254.3, 192.168.2.0/24 +---------------------------------------------- + +Всем клиентам придется назначать различные диапазоны адресов в lan и индивидуально прописывать AllowedIPs +для каждого peer. + +# ifdown wgvps ; ifup wgvps + +На клиенте разрешим форвард icmp, чтобы работал пинг и корректно определялось mtu. + +--/etc/config/firewall-------------------------- +config rule + option name 'Allow-ICMP-tunvps' + option src 'tunvps' + option dest 'lan' + option proto 'icmp' + option target 'ACCEPT' +------------------------------------------------ + +Существуют еще два неочевидных нюанса. + +Первый из них касается пакетов с самого роутера (цепочка OUTPUT). +Адрес источника выбирается по особому алгоритму, если программа явно его не задала, еще до этапа iptables. +Он берется с интерфейса, куда бы пошел пакет при нормальном раскладе. +Обратная маршрутизация с VPN станет невозможной, да и wireguard такие пакеты порежет, поскольку они не вписываются в AllowedIPs. +Никаким мистическим образом автоматом source address не поменяется. +В прошлом варианте настройки проблема решалось через маскарад. Сейчас же маскарада нет. +Потому все же придется его делать в случае, когда пакет изначально направился бы через wan, +а мы его завертываем на VPN. Помечаем такие пакеты марком 0x1000. +Если вам не актуальны исходящие с самого роутера, то можно ничего не менять. + +Другой нюанс связан с обработкой проброшенных на vps портов, соединения по которым приходят как входящие с интерфейса wgvps. +Представьте себе, что вы пробросили порт 2222. Кто-то подключается с адреса 1.2.3.4. Вам приходит пакет SYN 1.2.3.4:51723=>192.168.2.2:2222. +По правилам маршрутизации он пойдет в локалку. 192.168.2.2 его обработает, ответит пакетом ACK 192.168.2.2:2222=>1.2.3.4:51723. +Этот пакет придет на роутер. И куда он дальше пойдет ? Если он не занесен в ipban, то согласно правилам машрутизации +он пойдет по WAN интерфейсу, а не по исходному wgvps. +Чтобы решить эту проблему, необходимо воспользоваться CONNMARK. Существуют 2 отдельных марка : fwmark и connmark. +connmark относится к соединению, fwmark - к пакету. Трэкингом соединений занимается conntrack. +Посмотреть его таблицу можно командой "conntrack -L". Там же найдете connmark : mark=xxxx. +Как только видим приходящий с wgvps пакет с новым соединением, отмечаем его connmark как 0x800/0x800. +При этом fwmark не меняется, иначе бы пакет тут же бы завернулся обратно на wgvps согласно ip rule. +Если к нам приходит пакет с какого-то другого интерфейса, то восстанавливаем его connmark в fwmark по маске 0x800. +И теперь он подпадает под правило ip rule, заворачиваясь на wgvps, что и требовалось. + +--/etc/firewall.user---------------------------- +. /opt/zapret/init.d/openwrt/functions + +create_ipset no-update + +network_find_wan_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 -j MARK --set-mark 0x800/0x800 + ipt OUTPUT -t mangle -o $DEVICE -m set --match-set ipban dst -j MARK --set-mark 0x800/0x800 + ipt OUTPUT -t mangle -o $DEVICE -j MARK --set-mark 0x1000/0x1000 +done + +network_get_device DEVICE lan +ipt PREROUTING -t mangle -i $DEVICE -p tcp --dport 443 -m set --match-set zapret dst -j MARK --set-mark 0x800/0x800 +ipt PREROUTING -t mangle -i $DEVICE -m set --match-set ipban dst -j MARK --set-mark 0x800/0x800 + +# do masquerade for OUTPUT to ensure correct outgoing address +ipt postrouting_tunvps_rule -t nat -m mark --mark 0x1000/0x1000 -j MASQUERADE + +# incoming from wgvps +network_get_device DEVICE wgvps +ipt PREROUTING -t mangle ! -i $DEVICE -j CONNMARK --restore-mark --nfmask 0x800 --ctmask 0x800 +ipt PREROUTING -t mangle -i $DEVICE -m conntrack --ctstate NEW -j CONNMARK --set-xmark 0x800/0x800 +------------------------------------------------ + + +# fw3 restart + +Сейчас уже можно с vpn сервера пингануть ip адрес внутри локалки клиента. Пинги должны ходить. + +Отсутствие двойного NAT значительно облегчает проброс портов с внешнего IP vpn сервера в локалку какого-либо клиента. +Для этого надо выполнить 2 действия : добавить разрешение в фаервол на клиенте и сделать dnat на сервере. +Пример форварда портов 5001 и 5201 на 192.168.2.2 : + +--/etc/config/firewall-------------------------- +config rule + option target 'ACCEPT' + option src 'tunvps' + option dest 'lan' + option proto 'tcp udp' + option dest_port '5001 5201' + option dest_ip '192.168.2.2' + option name 'IPERF' +------------------------------------------------ + +# fw3 restart + +--/etc/network/interfaces.d/wgvps------------- + post-up iptables -t nat -A PREROUTING -i eth0 -p tcp -m multiport --dports 5001,5201 -j DNAT --to-destination 192.168.2.2 + post-up iptables -t nat -A POSTROUTING -o $IFACE -d 192.168.2.2 -p tcp -m multiport --dports 5001,5201 -j MASQUERADE + post-up iptables -t nat -A PREROUTING -i eth0 -p udp -m multiport --dports 5001,5201 -j DNAT --to-destination 192.168.2.2 + post-up iptables -t nat -A POSTROUTING -o $IFACE -d 192.168.2.2 -p udp -m multiport --dports 5001,5201 -j MASQUERADE + post-down iptables -t nat -D PREROUTING -i eth0 -p tcp -m multiport --dports 5001,5201 -j DNAT --to-destination 192.168.2.2 + post-down iptables -t nat -D POSTROUTING -o $IFACE -d 192.168.2.2 -p tcp -m multiport --dports 5001,5201 -j MASQUERADE + post-down iptables -t nat -D PREROUTING -i eth0 -p udp -m multiport --dports 5001,5201 -j DNAT --to-destination 192.168.2.2 + post-down iptables -t nat -D POSTROUTING -o $IFACE -d 192.168.2.2 -p udp -m multiport --dports 5001,5201 -j MASQUERADE +---------------------------------------------- + +# ifdown wgvps ; ifup wgvps + +Пример приведен для iperf и iperf3, чтобы показать как пробрасывать несколько портов tcp+udp с минимальным количеством команд. +Проброс tcp и udp порта так же необходим для полноценной работы bittorrent клиента, чтобы работали входящие. + +--- Как мне отправлять на vpn весь трафик с bittorrent ? --- + +Можно поступить так : посмотрите порт в настройках torrent клиента, убедитесь, что не поставлено "случайный порт", +добавьте на роутер правило маркировки по порту источника. +Но мне предпочтительно иное решение. На windows есть замечательная возможность +прописать правило установки поля качества обслуживания в заголовках ip пакетов в зависимости от процесса-источника. +Для windows 7/2008R2 необходимо будет установить ключик реестра и перезагрузить комп : +# reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Tcpip\QoS /v "Do not use NLA" /t REG_SZ /d "1" +Редактировать политику можно в : gpedit.msc -> Computer Configuration -> Windows Settings -> Policy-based QoS +На win 10 ключик реестра больше не работает, правила qos в gpedit применяются только для профиля домена. +Необходимо пользоваться командой powershell New-NetQosPolicy. Гуглите хелп по ней. Пример : +# powershell New-NetQosPolicy -Name "torrent" -AppPathNameMatchCondition "qbittorrent.exe" -DSCPAction 1 +Однозначно требуется проверка в wireshark или netmon успешности установки поля dscp. Если там по-прежнему 0x00, +значит что-то не сработало. 0x04 означает DSCP=1 (dscp находится в старших 6 битах). + +На роутере в фаер прописываем правило : + +--/etc/config/firewall-------------------------- +config rule + option target 'MARK' + option src 'lan' + option proto 'all' + option extra '-m dscp --dscp 1' + option name 'route-dscp-1' + option set_mark '0x0800/0x0800' +------------------------------------------------ + +# fw3 restart + +Теперь все с полем dscp "1" идет на vpn. Клиент сам решает какой трафик ему нужно забрасывать +на vpn, перенастраивать роутер не нужно. +На linux клиенте проще всего будет выставлять dscp в iptables по номеру порта источника : + +--/etc/rc.local--------------------------------- +iptables -A OUTPUT -t mangle -p tcp --sport 23444 -j DSCP --set-dscp 1 +iptables -A OUTPUT -t mangle -p udp --sport 23444 -j DSCP --set-dscp 1 +------------------------------------------------ + +можно привязываться к pid процесса, но тогда нужно перенастраивать iptables при каждом перезапуске +торент клиента, это требует рута, и все становится очень неудобно. + + +--- Автоматизация проброса портов через miniupnd --- + +Да, его тоже можно использовать на vps. Только как всегда есть нюансы. + +miniupnpd поддерживает 3 протокола IGD : upnp,nat-pmp и pcp. +upnp и pcp работают через мультикаст, который не пройдет через wgvps. +nat-pmp работает через посылку специальных сообщений на udp:5351 на default gateway. +Обычно их обслуживает miniupnpd на роутере. При создании lease miniupnpd добавляет +правила для проброса портов в цепочку iptables MINIUPNPD, при потери lease - убирает. + +udp:5351 можно перенаправить на vpn сервер через DNAT, чтобы их обрабатывал miniupnpd там. +Но вы должны иметь однозначный критерий перенаправления. +Если вы решили завернуть на vpn все, то проблем нет. Пробрасываем udp:5351 безусловно. +Если у вас идет перенаправление только с торрент, то необходимо к условию перенаправления +добавить условия, выделяющие torrent трафик из прочего. Или по dscp, или по sport. +Чтобы запросы от остальных программ обрабатывались miniupnpd на роутере. +Если какая-то программа создаст lease не там, где нужно, то входящий трафик до нее не дойдет. + +На роутере стоит запретить протокол upnp, чтобы торрент клиент не удовлетворился запросом, +обслуженным по upnp на роутере, и пытался использовать nat-pmp. + +--/etc/config/upnp-------------------------- +config upnpd 'config' + ..... + option enable_upnp '0' +------------------------------------------------ + +/etc/init.d/miniupnpd restart + +Делаем проброс порта на роутере. +Для простоты изложения будем считать, что на vpn у нас завернут весь трафик. +Если это не так, то следует добавить фильтр в "config redirect". +Заодно выделяем диапазон портов для торрент клиентов. +Порт в торент клиенте следует прописать какой-то из этого диапазона. + +------------------------------------------------ +config redirect + option enabled '1' + option target 'DNAT' + option src 'lan' + option dest 'tunvps' + option proto 'udp' + option src_dport '5351' + option dest_ip '192.168.254.1' + option dest_port '5351' + option name 'NAT-PMP' + option reflection '0' +config rule + option enabled '1' + option target 'ACCEPT' + option src 'tunvps' + option dest 'lan' + option name 'tunvps-torrent' + option dest_port '28000-28009' +------------------------------------------------ + +fw3 reload + + +На сервере : + +apt install miniupnpd + +--- /etc/miniupnpd/miniupnpd.conf -------- +enable_natpmp=yes +enable_upnp=no +lease_file=/var/log/upnp.leases +system_uptime=yes +clean_ruleset_threshold=10 +clean_ruleset_interval=600 +force_igd_desc_v1=no +listening_ip=192.168.254.1/16 +ext_ifname=eth0 +------------------------------------------ + +systemctl restart miniupnpd + +listening_ip прописан именно таким образом, чтобы обозначить диапазон разрешенных IP. +С других IP он не будет обрабатывать запросы на редирект. +В ext_ifname впишите название inet интерфейса на сервере. + +Запускаем торрент клиент. Попутно смотрим в tcpdump весь путь udp:5351 до сервера и обратно. +Смотрим syslog сервера на ругань от miniupnpd. +Если все ок, то можем проверить редиректы : iptables -t nat -nL MINIUPNPD +С какого-нибудь другого хоста (не vpn сервер, не ваше подключение) можно попробовать telnet-нуться на проброшенный порт. +Должно установиться соединение. Или качайте торент и смотрите в пирах флаг "I" (incoming). +Если "I" есть и по ним идет закачка, значит все в порядке. + +ОСОБЕННОСТЬ НОВЫХ DEBIAN : по умолчанию используются iptables-nft. miniupnpd работает с iptables-legacy. +ЛЕЧЕНИЕ : update-alternatives --set iptables /usr/sbin/iptables-legacy + + +--- А если не заработало ? --- + +Мануал пишется не как копипастная инструкция, а как помощь уже соображающему. +В руки вам ifconfig, ip, iptables, tcpdump, ping. В умелых руках творят чудеса. diff --git a/files/fake/fake_http_req_example.bin b/files/fake/fake_http_req_example.bin new file mode 100644 index 0000000..ce1d420 --- /dev/null +++ b/files/fake/fake_http_req_example.bin @@ -0,0 +1,9 @@ +GET / HTTP/1.1 +Host: www.iana.org +Connection: keep-alive +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4300.0 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 +Accept-Encoding: gzip, deflate +Accept-Language: en-US,en;q=0.9,ru;q=0.8 + diff --git a/files/fake/fake_tls13_clienthello_example.bin b/files/fake/fake_tls13_clienthello_example.bin new file mode 100644 index 0000000..e641d73 Binary files /dev/null and b/files/fake/fake_tls13_clienthello_example.bin differ diff --git a/files/huawei/E8372/run-zapret-hostlist b/files/huawei/E8372/run-zapret-hostlist new file mode 100755 index 0000000..7f37d58 --- /dev/null +++ b/files/huawei/E8372/run-zapret-hostlist @@ -0,0 +1,35 @@ +#!/system/bin/busybox sh + +# download hostlist from http(s) (need curl, its absent by default), +# feed it to zapret. save flash write cycles + +u="https://your.host.com/censorship/hoslist.txt" + +SCRIPT=$(readlink -f "$0") +EXEDIR=$(dirname "$SCRIPT") + +d=/data/censorship +[ -d $d ] || mkdir $d +f=$d/hostlist.txt +t=/hostlist.txt + +curl -k --fail --max-time 10 -o "$t" "$u" && { + if [ -s "$t" ]; then + m1=$(md5sum "$t" | cut -d ' ' -f 1) + m2=$(md5sum "$f" | cut -d ' ' -f 1) + echo $m1 $m2 + if [ -z "$m2" ] || [ "$m1" != "$m2" ]; then + echo updating hostlist + cp -f "$t" "$f" + else + echo hostlist was not changed. keeping old copy + fi + else + echo downloaded hostlist is empty. disabling zapret + rm "$f" + fi +} + +rm -f "$t" +"$EXEDIR/unzapret" +[ -s "$f" ] && exec "$EXEDIR/zapret" "--hostlist=$f" diff --git a/files/huawei/E8372/run-zapret-ip b/files/huawei/E8372/run-zapret-ip new file mode 100755 index 0000000..803e984 --- /dev/null +++ b/files/huawei/E8372/run-zapret-ip @@ -0,0 +1,39 @@ +#!/system/bin/busybox sh + +# download hostlist from http(s) (need curl, its absent by default), +# resolve to ip list, feed to zapret-ip. save flash write cycles + +u="https://your.host.com/censorship/hoslist.txt" + +SCRIPT=$(readlink -f "$0") +EXEDIR=$(dirname "$SCRIPT") + +d=/data/censorship +[ -d $d ] || mkdir $d +f=$d/hostlist.txt +t=/hostlist.txt +i=/iplist.txt + +curl -k --fail --max-time 10 -o "$t" "$u" && { + if [ -s "$t" ]; then + m1=$(md5sum "$t" | cut -d ' ' -f 1) + m2=$(md5sum "$f" | cut -d ' ' -f 1) + echo $m1 $m2 + if [ -z "$m2" ] || [ "$m1" != "$m2" ]; then + echo updating hostlist + cp -f "$t" "$f" + else + echo hostlist was not changed. keeping old copy + fi + else + echo downloaded hostlist is empty. disabling zapret + rm "$f" + fi +} + +rm -f "$t" +"$EXEDIR/unzapret-ip" +[ -s "$f" ] && { + mdig --threads=10 --family=4 <"$f" >"$i" + [ -s "$i" ] && exec "$EXEDIR/zapret-ip" "$i" +} diff --git a/files/huawei/E8372/unfuck_nfqueue.ko b/files/huawei/E8372/unfuck_nfqueue.ko new file mode 100644 index 0000000..c24ce5e Binary files /dev/null and b/files/huawei/E8372/unfuck_nfqueue.ko differ diff --git a/files/huawei/E8372/unzapret b/files/huawei/E8372/unzapret new file mode 100755 index 0000000..f040dfc --- /dev/null +++ b/files/huawei/E8372/unzapret @@ -0,0 +1,9 @@ +#!/system/bin/busybox sh + +rule="PREROUTING -t nat -i br0 ! -d 192.168.0.0/16 -p tcp -m multiport --dports 80,443 -j REDIRECT --to-port 1" +iptables -C $rule 2>/dev/null && iptables -D $rule +killall tpws + +rule="OUTPUT -t mangle -o wan0 -p tcp -m multiport --dports 80,443 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass" +iptables -C $rule 2>/dev/null && iptables -D $rule +killall nfqws diff --git a/files/huawei/E8372/unzapret-ip b/files/huawei/E8372/unzapret-ip new file mode 100755 index 0000000..ccb7425 --- /dev/null +++ b/files/huawei/E8372/unzapret-ip @@ -0,0 +1,11 @@ +#!/system/bin/busybox sh + +rule="PREROUTING -t nat -i br0 -p tcp -m multiport --dports 80,443 -j tpws" +iptables -C $rule 2>/dev/null && iptables -D $rule +iptables -F tpws -t nat +iptables -X tpws -t nat +killall tpws + +rule="OUTPUT -t mangle -o wan0 -p tcp -m multiport --dports 80,443 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass" +iptables -C $rule 2>/dev/null && iptables -D $rule +killall nfqws diff --git a/files/huawei/E8372/zapret b/files/huawei/E8372/zapret new file mode 100755 index 0000000..f19eed3 --- /dev/null +++ b/files/huawei/E8372/zapret @@ -0,0 +1,15 @@ +#!/system/bin/busybox sh + +# $1 - additional parameters for nfqws + +insmod /online/modules/unfuck_nfqueue.ko 2>/dev/null + +rule="PREROUTING -t nat -i br0 ! -d 192.168.0.0/16 -p tcp -m multiport --dports 80,443 -j REDIRECT --to-port 1" +iptables -C $rule 2>/dev/null || iptables -I $rule + +tpws --uid 1:3003 --port=1 --daemon + +rule="OUTPUT -t mangle -o wan0 -p tcp -m multiport --dports 80,443 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass" +iptables -C $rule 2>/dev/null || iptables -I $rule + +nfqws --uid 2 --qnum=200 --dpi-desync=disorder --dpi-desync-ttl=8 --dpi-desync-fooling=md5sig --daemon $1 diff --git a/files/huawei/E8372/zapret-ip b/files/huawei/E8372/zapret-ip new file mode 100755 index 0000000..9e70fac --- /dev/null +++ b/files/huawei/E8372/zapret-ip @@ -0,0 +1,34 @@ +#!/system/bin/busybox sh + +# $1 - ip list file. create individual rules for tpws redirection. ipset is not available + +[ -z "$1" ] && { + echo need iplist file as parameter + exit 1 +} + +insmod /online/modules/unfuck_nfqueue.ko 2>/dev/null + +tpws --maxconn=1024 --uid 1:3003 --port=1 --daemon + + +REDIR="-j REDIRECT --to-port 1" + +iptables -F tpws -t nat +iptables -X tpws -t nat +iptables -N tpws -t nat +iptables -A tpws -t nat -d 192.168.0.0/16 -j RETURN + +while read ip; do + echo redirecting $ip + iptables -A tpws -t nat -d $ip -p tcp $REDIR +done <"$1" + + +rule="PREROUTING -t nat -i br0 -p tcp -m multiport --dports 80,443 -j tpws" +iptables -C $rule 2>/dev/null || iptables -I $rule + +nfqws --uid 2 --qnum=200 --dpi-desync=disorder --dpi-desync-ttl=8 --dpi-desync-fooling=md5sig --daemon + +rule="OUTPUT -t mangle -o wan0 -p tcp -m multiport --dports 80,443 -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass" +iptables -C $rule 2>/dev/null || iptables -I $rule diff --git a/init.d/macos/functions b/init.d/macos/functions new file mode 100644 index 0000000..4d033c9 --- /dev/null +++ b/init.d/macos/functions @@ -0,0 +1,401 @@ +[ -n "$ZAPRET_BASE" ] || ZAPRET_BASE=/opt/zapret + +IPSET_DIR=$ZAPRET_BASE/ipset +. "$IPSET_DIR/def.sh" + +HOSTLIST="$ZHOSTLIST.gz" +[ -f "$HOSTLIST" ] || HOSTLIST="$ZHOSTLIST" +[ -f "$HOSTLIST" ] || HOSTLIST="$ZUSERLIST" + +PIDDIR=/var/run +TPPORT=988 +TPWS_WAIT="--bind-wait-ip=60" +TPWS="$ZAPRET_BASE/tpws/tpws" + +PF_MAIN="/etc/pf.conf" +PF_ANCHOR_DIR=/etc/pf.anchors +PF_ANCHOR_ZAPRET="$PF_ANCHOR_DIR/zapret" +PF_ANCHOR_ZAPRET_V4="$PF_ANCHOR_DIR/zapret-v4" +PF_ANCHOR_ZAPRET_V6="$PF_ANCHOR_DIR/zapret-v6" + +[ -n "$IFACE_WAN" ] && OWAN=" on $IFACE_WAN" + +on_off_function() +{ + # $1 : function name on + # $2 : function name off + # $3 : 0 - off, 1 - on + local F="$1" + [ "$3" = "1" ] || F="$2" + shift + shift + shift + "$F" "$@" +} + +run_daemon() +{ + # $1 - daemon number : 1,2,3,... + # $2 - daemon + # $3 - daemon args + # use $PIDDIR/$DAEMONBASE$1.pid as pidfile + local DAEMONBASE="$(basename $2)" + local PIDFILE="$PIDDIR/$DAEMONBASE$1.pid" + local ARGS="--daemon --pidfile=$PIDFILE $3" + [ -f "$PIDFILE" ] && pgrep -qF "$PIDFILE" && { + echo Already running $1: $2 + return 0 + } + echo "Starting daemon $1: $2 $ARGS" + "$2" $ARGS +} +stop_daemon() +{ + # $1 - daemon number : 1,2,3,... + # $2 - daemon + # use $PIDDIR/$DAEMONBASE$1.pid as pidfile + + local PID + local DAEMONBASE="$(basename $2)" + local PIDFILE="$PIDDIR/$DAEMONBASE$1.pid" + [ -f "$PIDFILE" ] && read PID <"$PIDFILE" + [ -n "$PID" ] && { + echo "Stopping daemon $1: $2 (PID=$PID)" + kill $PID + rm -f "$PIDFILE" + } + return 0 +} +do_daemon() +{ + # $1 - 1 - run, 0 - stop + on_off_function run_daemon stop_daemon "$@" +} + +filter_apply_hostlist_target() +{ + # $1 - var name of tpws or nfqws params + [ "$MODE_FILTER" = "hostlist" ] && eval $1="\"\$$1 --hostlist=$HOSTLIST\"" +} +tpws_apply_binds() +{ + local o + [ "$DISABLE_IPV4" = "1" ] || o="--bind-addr=127.0.0.1" + [ "$DISABLE_IPV6" = "1" ] || { + for i in lo0 $IFACE_LAN; do + o="$o --bind-iface6=$i --bind-linklocal=force $TPWS_WAIT" + done + } + eval $1="\"\$$1 $o\"" +} + +wait_interface_ll() +{ + echo waiting for an ipv6 link local address on $1 ... + "$TPWS" --bind-wait-only --bind-iface6=$1 --bind-linklocal=force $TPWS_WAIT +} +wait_lan_ll() +{ + [ "$DISABLE_IPV6" != "1" ] && [ -n "$IFACE_LAN" ] && { + wait_interface_ll $IFACE_LAN >&2 || { + echo "wait interface failed" + return 1 + } + } + return 0 +} +get_ipv6_linklocal() +{ + ifconfig $1 | sed -nEe 's/^.*inet6 (fe80:[a-f0-9:]+).*/\1/p' +} + + +pf_anchor_root_reload() +{ + echo reloading PF root anchor + pfctl -qf "$PF_MAIN" +} + +pf_anchor_root() +{ + local patch + [ -f "$PF_MAIN" ] && { + grep -q '^rdr-anchor "zapret"$' "$PF_MAIN" || { + echo patching rdr-anchor in $PF_MAIN + patch=1 + sed -i '' -e '/^rdr-anchor "com\.apple\/\*"$/i \ +rdr-anchor "zapret" +' $PF_MAIN + } + grep -q '^anchor "zapret"$' "$PF_MAIN" || { + echo patching anchor in $PF_MAIN + patch=1 + sed -i '' -e '/^anchor "com\.apple\/\*"$/i \ +anchor "zapret" +' $PF_MAIN + } + grep -q "^set limit table-entries" "$PF_MAIN" || { + echo patching table-entries limit + patch=1 + sed -i '' -e '/^scrub-anchor "com\.apple\/\*"$/i \ +set limit table-entries 5000000 +' $PF_MAIN + } + + grep -q '^anchor "zapret"$' "$PF_MAIN" && + grep -q '^rdr-anchor "zapret"$' "$PF_MAIN" && + grep -q '^set limit table-entries' "$PF_MAIN" && { + if [ -n "$patch" ]; then + echo successfully patched $PF_MAIN + pf_anchor_root_reload + else + echo successfully checked zapret anchors in $PF_MAIN + fi + return 0 + } + } + echo ---------------------------------- + echo Automatic $PF_MAIN patching failed. You must apply root anchors manually in your PF config. + echo rdr-anchor \"zapret\" + echo anchor \"zapret\" + echo ---------------------------------- + return 1 +} +pf_anchor_root_del() +{ + sed -i '' -e '/^anchor "zapret"$/d' -e '/^rdr-anchor "zapret"$/d' -e '/^set limit table-entries/d' "$PF_MAIN" +} + +pf_anchor_zapret() +{ + [ "$DISABLE_IPV4" = "1" ] || { + if [ -f "$ZIPLIST_EXCLUDE" ]; then + echo "table persist file \"$ZIPLIST_EXCLUDE\"" + else + echo "table persist" + fi + } + [ "$DISABLE_IPV4" = "1" ] || { + if [ -f "$ZIPLIST_EXCLUDE6" ]; then + echo "table persist file \"$ZIPLIST_EXCLUDE6\"" + else + echo "table persist" + fi + } + echo + [ "$DISABLE_IPV4" = "1" ] || echo "rdr-anchor \"/zapret-v4\" inet to !" + [ "$DISABLE_IPV6" = "1" ] || echo "rdr-anchor \"/zapret-v6\" inet6 to !" + [ "$DISABLE_IPV4" = "1" ] || echo "anchor \"/zapret-v4\" inet to !" + [ "$DISABLE_IPV6" = "1" ] || echo "anchor \"/zapret-v6\" inet6 to !" +} +pf_anchor_zapret_tables() +{ + # $1 - variable to receive applied table names + # $2/$3 $4/$5 ... table_name/table_file + local tblv=$1 + local _tbl + + shift + [ "$MODE_FILTER" = "ipset" ] && + { + while [ -n "$1" ] && [ -n "$2" ] ; do + [ -f "$2" ] && { + echo "table <$1> file \"$2\"" + _tbl="$_tbl<$1> " + } + shift + shift + done + } + [ -n "$_tbl" ] || _tbl="any" + + eval $tblv="\"\$_tbl\"" +} +pf_anchor_port_target() +{ + if [ "$MODE_HTTP" = "1" ] && [ "$MODE_HTTPS" = "1" ]; then + echo "{80,443}" + elif [ "$MODE_HTTPS" = "1" ]; then + echo "443" + elif [ "$MODE_HTTP" = "1" ]; then + echo "80" + fi +} +pf_anchor_zapret_v4() +{ + local tbl port + + [ "$DISABLE_IPV4" = "1" ] || { + [ ! "$MODE_HTTP" = "1" ] && [ ! "$MODE_HTTPS" = "1" ] && return + pf_anchor_zapret_tables tbl zapret-user "$ZIPLIST_USER" zapret "$ZIPLIST" + port=$(pf_anchor_port_target) + for t in $tbl; do + [ -n "$IFACE_LAN" ] && echo "rdr on $IFACE_LAN inet proto tcp from any to $t port $port -> 127.0.0.1 port $TPPORT" + done + echo "rdr on lo0 inet proto tcp from !127.0.0.0/8 to any port $port -> 127.0.0.1 port $TPPORT" + for t in $tbl; do + echo "pass out$OWAN route-to (lo0 127.0.0.1) inet proto tcp from !127.0.0.0/8 to $t port $port user { >root }" + done + } +} +pf_anchor_zapret_v6() +{ + local tbl port LL_LAN + + [ "$DISABLE_IPV6" = "1" ] || { + [ ! "$MODE_HTTP" = "1" ] && [ ! "$MODE_HTTPS" = "1" ] && return + + # LAN link local is only for router + [ -n "$IFACE_LAN" ] && LL_LAN=$(get_ipv6_linklocal $IFACE_LAN) + + pf_anchor_zapret_tables tbl zapret6-user "$ZIPLIST_USER6" zapret6 "$ZIPLIST6" + port=$(pf_anchor_port_target) + for t in $tbl; do + [ -n "$LL_LAN" ] && echo "rdr on $IFACE_LAN inet6 proto tcp from any to $t port $port -> $LL_LAN port $TPPORT" + done + echo "rdr on lo0 inet6 proto tcp from !::1 to any port $port -> fe80::1 port $TPPORT" + for t in $tbl; do + echo "pass out$OWAN route-to (lo0 fe80::1) inet6 proto tcp from !::1 to $t port $port user { >root }" + done + } +} +pf_anchors_create() +{ + wait_lan_ll + pf_anchor_zapret >"$PF_ANCHOR_ZAPRET" + pf_anchor_zapret_v4 >"$PF_ANCHOR_ZAPRET_V4" + pf_anchor_zapret_v6 >"$PF_ANCHOR_ZAPRET_V6" +} +pf_anchors_del() +{ + rm -f "$PF_ANCHOR_ZAPRET" "$PF_ANCHOR_ZAPRET_V4" "$PF_ANCHOR_ZAPRET_V6" +} +pf_anchors_load() +{ + echo loading zapret anchor from "$PF_ANCHOR_ZAPRET" + pfctl -qa zapret -f "$PF_ANCHOR_ZAPRET" || { + echo error loading zapret anchor + return 1 + } + if [ "$DISABLE_IPV4" = "1" ]; then + echo clearing zapret-v4 anchor + pfctl -qa zapret-v4 -F all 2>/dev/null + else + echo loading zapret-v4 anchor from "$PF_ANCHOR_ZAPRET_V4" + pfctl -qa zapret-v4 -f "$PF_ANCHOR_ZAPRET_V4" || { + echo error loading zapret-v4 anchor + return 1 + } + fi + if [ "$DISABLE_IPV6" = "1" ]; then + echo clearing zapret-v6 anchor + pfctl -qa zapret-v6 -F all 2>/dev/null + else + echo loading zapret-v6 anchor from "$PF_ANCHOR_ZAPRET_V6" + pfctl -qa zapret-v6 -f "$PF_ANCHOR_ZAPRET_V6" || { + echo error loading zapret-v6 anchor + return 1 + } + fi + echo successfully loaded PF anchors + return 0 +} +pf_anchors_clear() +{ + echo clearing zapret anchors + pfctl -qa zapret-v4 -F all 2>/dev/null + pfctl -qa zapret-v6 -F all 2>/dev/null + pfctl -qa zapret -F all 2>/dev/null +} +pf_enable() +{ + echo enabling PF + pfctl -qe +} +pf_table_reload() +{ + echo reloading zapret tables + [ "$DISABLE_IPV4" = "1" ] || pfctl -qTl -a zapret-v4 -f "$PF_ANCHOR_ZAPRET_V4" + [ "$DISABLE_IPV6" = "1" ] || pfctl -qTl -a zapret-v6 -f "$PF_ANCHOR_ZAPRET_V6" + pfctl -qTl -a zapret -f "$PF_ANCHOR_ZAPRET" +} +zapret_do_firewall() +{ + # $1 - 1 - add, 0 - del + + case "${MODE}" in + tpws) + if [ "$1" = "1" ] ; then + pf_anchor_root || return 1 + pf_anchors_create + pf_anchors_load || return 1 + pf_enable + else + pf_anchors_clear + fi + ;; + filter) + ;; + *) + echo "unsupported MODE=$MODE" + return 1 + ;; + esac + return 0 +} +zapret_apply_firewall() +{ + zapret_do_firewall 1 "$@" +} +zapret_unapply_firewall() +{ + zapret_do_firewall 0 "$@" +} +zapret_restart_firewall() +{ + zapret_unapply_firewall "$@" + zapret_apply_firewall "$@" +} + + + +zapret_do_daemons() +{ + # $1 - 1 - run, 0 - stop + + local opt + + case "${MODE}" in + tpws) + [ "$1" = "1" ] && [ "$DISABLE_IPV4" = "1" ] && [ "$DISABLE_IPV6" = "1" ] && { + echo "both ipv4 and ipv6 are disabled. nothing to do" + return 0 + } + # MacOS requires root. kernel hardcoded requirement for /dev/pf ioctls + opt="--user=root --port=$TPPORT" + filter_apply_hostlist_target opt + tpws_apply_binds opt + opt="$opt $TPWS_OPT" + do_daemon $1 1 "$TPWS" "$opt" + ;; + filter) + ;; + *) + echo "unsupported MODE=$MODE" + return 1 + ;; + esac +} +zapret_run_daemons() +{ + zapret_do_daemons 1 "$@" +} +zapret_stop_daemons() +{ + zapret_do_daemons 0 "$@" +} +zapret_restart_daemons() +{ + zapret_stop_daemons "$@" + zapret_run_daemons "$@" +} diff --git a/init.d/macos/zapret b/init.d/macos/zapret new file mode 100755 index 0000000..c563926 --- /dev/null +++ b/init.d/macos/zapret @@ -0,0 +1,51 @@ +#!/bin/sh + +EXEDIR="$(dirname "$0")" +ZAPRET_BASE="$EXEDIR/../.." +ZAPRET_BASE="$(cd "$ZAPRET_BASE"; pwd)" + +. "$EXEDIR/functions" + +case "$1" in + start) + zapret_run_daemons + [ "$INIT_APPLY_FW" != "1" ] || zapret_apply_firewall + ;; + stop) + [ "$INIT_APPLY_FW" != "1" ] || zapret_unapply_firewall + zapret_stop_daemons + ;; + restart) + "$0" stop + "$0" start + ;; + + start-fw) + zapret_apply_firewall + ;; + stop-fw) + zapret_unapply_firewall + ;; + restart-fw) + zapret_restart_firewall + ;; + reload-fw-tables) + pf_table_reload + ;; + + start-daemons) + zapret_run_daemons + ;; + stop-daemons) + zapret_stop_daemons + ;; + restart-daemons) + zapret_restart_daemons + ;; + + *) + N="$SCRIPT/$NAME" + echo "Usage: $N {start|stop|start-fw|stop-fw|restart-fw|reload-fw-tables|start-daemons|stop-daemons|restart-daemons}" >&2 + exit 1 + ;; +esac diff --git a/init.d/macos/zapret.plist b/init.d/macos/zapret.plist new file mode 100644 index 0000000..747d69b --- /dev/null +++ b/init.d/macos/zapret.plist @@ -0,0 +1,17 @@ + + + + + Label + zapret + LaunchOnlyOnce + + ProgramArguments + + /opt/zapret/init.d/macos/zapret + start + + RunAtLoad + + + diff --git a/init.d/openwrt/90-zapret b/init.d/openwrt/90-zapret new file mode 100644 index 0000000..a2d6324 --- /dev/null +++ b/init.d/openwrt/90-zapret @@ -0,0 +1,8 @@ +#!/bin/sh + +ZAPRET=/etc/init.d/zapret +[ -x "$ZAPRET" ] && [ "$INTERFACE" = "lan" ] && { + [ "$ACTION" = "ifup" ] && { + $ZAPRET enabled && $ZAPRET restart + } +} diff --git a/init.d/openwrt/custom b/init.d/openwrt/custom new file mode 100644 index 0000000..2136b74 --- /dev/null +++ b/init.d/openwrt/custom @@ -0,0 +1,20 @@ +# this script contain your special code to launch daemons and configure firewall +# use helpers from "functions" file and "zapret" init script +# in case of upgrade keep this file only, do not modify others + +zapret_custom_daemons() +{ + # PLACEHOLDER + echo !!! NEED ATTENTION !!! + echo Start daemon\(s\) + echo Study how other sections work + + run_daemon 1 /bin/sleep 20 +} +zapret_custom_firewall() +{ + # PLACEHOLDER + echo !!! NEED ATTENTION !!! + echo Configure iptables for required actions + echo Study how other sections work +} diff --git a/init.d/openwrt/custom-2nfqws b/init.d/openwrt/custom-2nfqws new file mode 100644 index 0000000..b875c7b --- /dev/null +++ b/init.d/openwrt/custom-2nfqws @@ -0,0 +1,44 @@ +# this custom script demonstrates how to use 2 copies of nfqws +# it preserves config settings : MODE_HTTP, MODE_HTTP_KEEPALIVE, MODE_HTTPS, MODE_FILTER, NFQWS_OPT_DESYNC +# NFQWS_OPT_DESYNC - parameters for http +# NFQWS_OPT_DESYNC2 - parameters for https. you should add this variable to config file, its absent there + +QNUM2=$(($QNUM+1)) + +zapret_custom_daemons() +{ + local opt + + [ "$MODE_HTTP" = "1" ] && { + opt="$NFQWS_OPT_BASE $NFQWS_OPT_DESYNC" + filter_apply_hostlist_target opt + run_daemon 1 $NFQWS "$opt" + } + + [ "$MODE_HTTPS" = "1" ] && { + opt="$NFQWS_OPT_BASE $NFQWS_OPT_DESYNC2 --qnum=$QNUM2" + filter_apply_hostlist_target opt + run_daemon 2 $NFQWS "$opt" + } +} +zapret_custom_firewall() +{ + local f4 f6 + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4" + local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" + + [ "$MODE_HTTP" = "1" ] && { + f4="--dport 80" + [ "$MODE_HTTP_KEEPALIVE" = "1" ] || f4="$f4 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post "$f4 $desync" "$f6 $desync" $QNUM + } + + [ "$MODE_HTTPS" = "1" ] && { + f4="--dport 443 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post "$f4 $desync" "$f6 $desync" $QNUM2 + } +} diff --git a/init.d/openwrt/custom-tpws4http-nfqws4https b/init.d/openwrt/custom-tpws4http-nfqws4https new file mode 100644 index 0000000..8526840 --- /dev/null +++ b/init.d/openwrt/custom-tpws4http-nfqws4https @@ -0,0 +1,39 @@ +# this custom script demonstrates how to apply tpws to http and nfqws to https +# it preserves config settings : MODE_HTTP, MODE_HTTPS, MODE_FILTER, TPWS_OPT, NFQWS_OPT_DESYNC + +zapret_custom_daemons() +{ + local opt + + [ "$MODE_HTTP" = "1" ] && { + opt="$TPWS_OPT" + filter_apply_hostlist_target opt + run_tpws 1 "$opt" + } + + [ "$MODE_HTTPS" = "1" ] && { + opt="$NFQWS_OPT_BASE $NFQWS_OPT_DESYNC" + filter_apply_hostlist_target opt + run_daemon 2 $NFQWS "$opt" + } +} +zapret_custom_firewall() +{ + local f4 f6 + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4" + local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" + + [ "$MODE_HTTP" = "1" ] && { + f4="--dport 80" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_tpws "$f4" "$f6" $TPPORT + } + + [ "$MODE_HTTPS" = "1" ] && { + f4="--dport 443 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post "$f4 $desync" "$f6 $desync" $QNUM + } +} diff --git a/init.d/openwrt/custom.default b/init.d/openwrt/custom.default new file mode 100644 index 0000000..2136b74 --- /dev/null +++ b/init.d/openwrt/custom.default @@ -0,0 +1,20 @@ +# this script contain your special code to launch daemons and configure firewall +# use helpers from "functions" file and "zapret" init script +# in case of upgrade keep this file only, do not modify others + +zapret_custom_daemons() +{ + # PLACEHOLDER + echo !!! NEED ATTENTION !!! + echo Start daemon\(s\) + echo Study how other sections work + + run_daemon 1 /bin/sleep 20 +} +zapret_custom_firewall() +{ + # PLACEHOLDER + echo !!! NEED ATTENTION !!! + echo Configure iptables for required actions + echo Study how other sections work +} diff --git a/init.d/openwrt/firewall.zapret b/init.d/openwrt/firewall.zapret new file mode 100644 index 0000000..a09d74d --- /dev/null +++ b/init.d/openwrt/firewall.zapret @@ -0,0 +1,11 @@ +SCRIPT=$(readlink /etc/init.d/zapret) +if [ -n "$SCRIPT" ]; then + EXEDIR=$(dirname "$SCRIPT") + ZAPRET_BASE=$(readlink -f "$EXEDIR/../..") +else + ZAPRET_BASE=/opt/zapret +fi + +. "$ZAPRET_BASE/init.d/openwrt/functions" + +zapret_apply_firewall diff --git a/init.d/openwrt/functions b/init.d/openwrt/functions new file mode 100644 index 0000000..a7b3d0d --- /dev/null +++ b/init.d/openwrt/functions @@ -0,0 +1,425 @@ +. /lib/functions/network.sh + +[ -n "$ZAPRET_BASE" ] || ZAPRET_BASE=/opt/zapret +. "$ZAPRET_BASE/config" + +QNUM=200 +TPPORT=988 +TPWS_USER=daemon +TPWS_LOCALHOST4=127.0.0.127 +[ -n "$DESYNC_MARK" ] || DESYNC_MARK=0x40000000 + +# max wait time for the link local ipv6 on the LAN interface +LINKLOCAL_WAIT_SEC=5 + +IPSET_CR="$ZAPRET_BASE/ipset/create_ipset.sh" + +CUSTOM_SCRIPT="$ZAPRET_BASE/init.d/openwrt/custom" +[ -f "$CUSTOM_SCRIPT" ] && . "$CUSTOM_SCRIPT" + +IPSET_EXCLUDE="-m set ! --match-set nozapret" +IPSET_EXCLUDE6="-m set ! --match-set nozapret6" + +exists() +{ + which "$1" >/dev/null 2>/dev/null +} +existf() +{ + type "$1" >/dev/null 2>/dev/null +} + + +# can be multiple ipv6 outgoing interfaces +# uplink from isp, tunnelbroker, vpn, ... +# want them all. who knows what's the real one that blocks sites +# dont want any manual configuration - want to do it automatically +# standard network_find_wan[6] return only the first +# we use low level function from network.sh to avoid this limitation +# it can change theoretically and stop working + +network_find_wan_all() +{ + __network_ifstatus "$1" "" "[@.route[@.target='0.0.0.0' && !@.table]].interface" "" 10 2>/dev/null && return + network_find_wan $1 +} +network_find_wan6_all() +{ + __network_ifstatus "$1" "" "[@.route[@.target='::' && !@.table]].interface" "" 10 2>/dev/null && return + network_find_wan6 $1 +} + +ipt() +{ + iptables -C "$@" 2>/dev/null || iptables -I "$@" +} +ipt_del() +{ + iptables -C "$@" 2>/dev/null && iptables -D "$@" +} +ipt6() +{ + ip6tables -C "$@" 2>/dev/null || ip6tables -I "$@" +} +ipt6_del() +{ + ip6tables -C "$@" 2>/dev/null && ip6tables -D "$@" +} + +# there's no route_localnet for ipv6 +# the best we can is to route to link local of the incoming interface +# OUTPUT - can DNAT to ::1 +# PREROUTING - can't DNAT to ::1. can DNAT to link local of -i interface or to any global addr +# not a good idea to expose tpws to the world (bind to ::) + +get_ipv6_linklocal() +{ + # $1 - interface name. if empty - any interface + if exists ip ; then + local dev + [ -n "$1" ] && dev="dev $1" + ip addr show $dev | sed -e 's/^.*inet6 \([^ ]*\)\/[0-9]* scope link.*$/\1/;t;d' | head -n 1 + else + ifconfig $1 | sed -re 's/^.*inet6 addr: ([^ ]*)\/[0-9]* Scope:Link.*$/\1/;t;d' | head -n 1 + fi +} +get_ipv6_global() +{ + # $1 - interface name. if empty - any interface + if exists ip ; then + local dev + [ -n "$1" ] && dev="dev $1" + ip addr show $dev | sed -e 's/^.*inet6 \([^ ]*\)\/[0-9]* scope global.*$/\1/;t;d' | head -n 1 + else + ifconfig $1 | sed -re 's/^.*inet6 addr: ([^ ]*)\/[0-9]* Scope:Global.*$/\1/;t;d' | head -n 1 + fi +} + +dnat6_target() +{ + # get target ip address for DNAT. prefer link locals + # tpws should be as inaccessible from outside as possible + # link local address can appear not immediately after ifup + + # DNAT6_TARGET=- means attempt was made but address was not found (to avoid multiple re-attempts) + + [ -n "$DNAT6_TARGET" ] || { + # no reason to query if its down + network_is_up lan || return + + local DEVICE + network_get_device DEVICE lan + + local ct=0 + while + DNAT6_TARGET=$(get_ipv6_linklocal $DEVICE) + [ -n "$DNAT6_TARGET" ] && break + [ "$ct" -ge "$LINKLOCAL_WAIT_SEC" ] && break + echo waiting for the link local for another $(($LINKLOCAL_WAIT_SEC - $ct)) seconds ... + ct=$(($ct+1)) + sleep 1 + do :; done + + [ -n "$DNAT6_TARGET" ] || { + echo no link local. getting global + DNAT6_TARGET=$(get_ipv6_global $DEVICE) + [ -n "$DNAT6_TARGET" ] || { + echo could not get any address + DNAT6_TARGET=- + } + } + } +} + + +fw_nfqws_pre4() +{ + # $1 - filter ipv4 + # $2 - queue number + + local DEVICE wan_iface + + [ "$DISABLE_IPV4" = "1" ] || { + network_find_wan_all wan_iface + for ext_iface in $wan_iface; do + network_get_device DEVICE $ext_iface + ipt PREROUTING -t mangle -i $DEVICE -p tcp $1 $IPSET_EXCLUDE src -j NFQUEUE --queue-num $2 --queue-bypass + done + } +} +fw_nfqws_pre6() +{ + # $1 - filter ipv6 + # $2 - queue number + + local DEVICE wan_iface + + [ "$DISABLE_IPV6" = "1" ] || { + network_find_wan6_all wan_iface + for ext_iface in $wan_iface; do + network_get_device DEVICE $ext_iface + ipt6 PREROUTING -t mangle -i $DEVICE -p tcp $1 $IPSET_EXCLUDE6 src -j NFQUEUE --queue-num $2 --queue-bypass + done + } +} +fw_nfqws_pre() +{ + # $1 - filter ipv4 + # $2 - filter ipv6 + # $3 - queue number + + fw_nfqws_pre4 "$1" $3 + fw_nfqws_pre6 "$2" $3 +} +fw_nfqws_post4() +{ + # $1 - filter ipv4 + # $2 - queue number + + local DEVICE wan_iface + + [ "$DISABLE_IPV4" = "1" ] || { + network_find_wan_all wan_iface + for ext_iface in $wan_iface; do + network_get_device DEVICE $ext_iface + ipt POSTROUTING -t mangle -o $DEVICE -p tcp $1 $IPSET_EXCLUDE dst -j NFQUEUE --queue-num $2 --queue-bypass + done + } +} +fw_nfqws_post6() +{ + # $1 - filter ipv6 + # $2 - queue number + + local DEVICE wan_iface + + [ "$DISABLE_IPV6" = "1" ] || { + network_find_wan6_all wan_iface + for ext_iface in $wan_iface; do + network_get_device DEVICE $ext_iface + ipt6 POSTROUTING -t mangle -o $DEVICE -p tcp $1 $IPSET_EXCLUDE6 dst -j NFQUEUE --queue-num $2 --queue-bypass + done + } +} +fw_nfqws_post() +{ + # $1 - filter ipv4 + # $2 - filter ipv6 + # $3 - queue number + + fw_nfqws_post4 "$1" $3 + fw_nfqws_post6 "$2" $3 +} + + +IPT_OWNER="-m owner ! --uid-owner $TPWS_USER" +fw_tpws4() +{ + # $1 - filter ipv6 + # $2 - tpws port + + local DEVICE wan_iface + + [ "$DISABLE_IPV4" = "1" ] || { + network_find_wan_all wan_iface + for ext_iface in $wan_iface; do + network_get_device DEVICE $ext_iface + ipt OUTPUT -t nat -o $DEVICE $IPT_OWNER -p tcp $1 $IPSET_EXCLUDE dst -j DNAT --to $TPWS_LOCALHOST4:$2 + done + ipt prerouting_lan_rule -t nat -p tcp $1 $IPSET_EXCLUDE dst -j DNAT --to $TPWS_LOCALHOST4:$2 + network_get_device DEVICE lan + [ -n "$DEVICE" ] && { + # allow localnet route only to special tpws IP + iptables -N input_lan_rule_zapret 2>/dev/null + ipt input_lan_rule_zapret -d 127.0.0.0/8 -j DROP + ipt input_lan_rule_zapret -d $TPWS_LOCALHOST4 -j RETURN + ipt input_lan_rule -j input_lan_rule_zapret + sysctl -qw net.ipv4.conf.$DEVICE.route_localnet=1 + } + } +} +fw_tpws6() +{ + # $1 - filter ipv6 + # $2 - tpws port + + local DEVICE wan_iface + + [ "$DISABLE_IPV6" = "1" ] || { + network_find_wan6_all wan_iface + for ext_iface in $wan_iface; do + network_get_device DEVICE $ext_iface + ipt6 OUTPUT -t nat -o $DEVICE $IPT_OWNER -p tcp $1 $IPSET_EXCLUDE6 dst -j DNAT --to [::1]:$2 + done + network_get_device DEVICE lan + dnat6_target + [ "$DNAT6_TARGET" != "-" ] && ipt6 PREROUTING -t nat -i $DEVICE -p tcp $1 $IPSET_EXCLUDE6 dst -j DNAT --to [$DNAT6_TARGET]:$2 + } +} +fw_tpws() +{ + # $1 - filter ipv4 + # $2 - filter ipv6 + # $3 - tpws port + + fw_tpws4 "$1" $3 + fw_tpws6 "$2" $3 +} + +filter_apply_port_target() +{ + # $1 - var name of iptables filter + local f + if [ "$MODE_HTTP" = "1" ] && [ "$MODE_HTTPS" = "1" ]; then + f="-m multiport --dports 80,443" + elif [ "$MODE_HTTPS" = "1" ]; then + f="--dport 443" + elif [ "$MODE_HTTP" = "1" ]; then + f="--dport 80" + else + echo WARNING !!! HTTP and HTTPS are both disabled + fi + eval $1="\"\$$1 $f\"" +} +filter_apply_ipset_target() +{ + # $1 - var name of ipv4 iptables filter + # $2 - var name of ipv6 iptables filter + if [ "$MODE_FILTER" = "ipset" ]; then + eval $1="\"\$$1 -m set --match-set zapret dst\"" + eval $2="\"\$$2 -m set --match-set zapret6 dst\"" + fi +} + + +create_ipset() +{ + echo "Creating ipset" + "$IPSET_CR" "$@" +} + + +is_flow_offload_avail() +{ + # $1 = '' for ipv4, '6' for ipv6 + grep -q FLOWOFFLOAD /proc/net/ip$1_tables_targets +} +list_nfqws_rules() +{ + # $1 = '' for ipv4, '6' for ipv6 + ip$1tables -S POSTROUTING -t mangle | grep "NFQUEUE --queue-num $QNUM --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 /' -e 's/--dport /--sport /' -e 's/--dports /--sports /' -e 's/ dst$/ src/' -e 's/ dst / src /' +} +apply_flow_offloading_enable_rule() +{ + # $1 = '' for ipv4, '6' for ipv6 + local i off='-j FLOWOFFLOAD' + [ "$FLOWOFFLOAD" = "hardware" ] && off="$off --hw" + i="forwarding_rule_zapret -m comment --comment zapret_traffic_offloading_enable -m conntrack --ctstate RELATED,ESTABLISHED $off" + echo enabling ipv${1:-4} flow offloading : $i + ip$1tables -A $i +} +apply_flow_offloading_exempt_rule() +{ + # $1 = '' for ipv4, '6' for ipv6 + local i v + v=$1 + shift + i="forwarding_rule_zapret $@ -m comment --comment zapret_traffic_offloading_exemption -j RETURN" + echo applying ipv${v:-4} flow offloading exemption : $i + ip${v}tables -A $i +} +flow_offloading_exempt_v() +{ + # $1 = '' for ipv4, '6' for ipv6 + + is_flow_offload_avail $1 || return 0 + + ipt$1_del forwarding_rule -j forwarding_rule_zapret + ip$1tables -F forwarding_rule_zapret 2>/dev/null + ip$1tables -X forwarding_rule_zapret 2>/dev/null + + [ "$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 | + while read rule; do + apply_flow_offloading_exempt_rule "$1" $rule + done + + apply_flow_offloading_enable_rule $1 + + ipt$1 forwarding_rule -j forwarding_rule_zapret + } + + return 0 +} +flow_offloading_exempt() +{ + [ "$DISABLE_IPV4" = "1" ] || flow_offloading_exempt_v + [ "$DISABLE_IPV6" = "1" ] || flow_offloading_exempt_v 6 +} + + +zapret_apply_firewall() +{ + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4" + local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" + local f4 f6 + + # always create ipsets. ip_exclude ipset is required + create_ipset no-update + + case "${MODE}" in + tpws) + if [ ! "$MODE_HTTP" = "1" ] && [ ! "$MODE_HTTPS" = "1" ]; then + echo both http and https are disabled. not applying redirection. + else + filter_apply_port_target f4 + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_tpws "$f4" "$f6" $TPPORT + fi + ;; + + nfqws) + if [ ! "$MODE_HTTP" = "1" ] && [ ! "$MODE_HTTPS" = "1" ]; then + echo both http and https are disabled. not applying redirection. + else + if [ "$MODE_HTTP_KEEPALIVE" = "1" ]; then + if [ "$MODE_HTTP" = "1" ]; then + f4="--dport 80" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post "$f4 $desync" "$f6 $desync" $QNUM + fi + if [ "$MODE_HTTPS" = "1" ]; then + f4="--dport 443 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post "$f4 $desync" "$f6 $desync" $QNUM + fi + else + filter_apply_port_target f4 + f4="$f4 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post "$f4 $desync" "$f6 $desync" $QNUM + fi + fi + ;; + custom) + existf zapret_custom_firewall && zapret_custom_firewall + ;; + esac + + flow_offloading_exempt +} diff --git a/init.d/openwrt/zapret b/init.d/openwrt/zapret new file mode 100755 index 0000000..805a776 --- /dev/null +++ b/init.d/openwrt/zapret @@ -0,0 +1,100 @@ +#!/bin/sh /etc/rc.common + +USE_PROCD=1 +# after network +START=21 + + +SCRIPT=$(readlink /etc/init.d/zapret) +if [ -n "$SCRIPT" ]; then + EXEDIR=$(dirname "$SCRIPT") + ZAPRET_BASE=$(readlink -f "$EXEDIR/../..") +else + ZAPRET_BASE=/opt/zapret +fi +. "$ZAPRET_BASE/init.d/openwrt/functions" + + +# !!!!! in openwrt firewall rules are configured separately + +PIDDIR=/var/run + +QNUM=200 +NFQWS_USER=daemon +NFQWS="$ZAPRET_BASE/nfq/nfqws" +NFQWS_OPT_BASE="--qnum=$QNUM --user=$NFQWS_USER" + +TPWS_USER=daemon +TPPORT=988 +TPWS="$ZAPRET_BASE/tpws/tpws" +TPWS_LOCALHOST4=127.0.0.127 +HOSTLIST="$ZAPRET_BASE/ipset/zapret-hosts.txt.gz" +[ -f "$HOSTLIST" ] || HOSTLIST="$ZAPRET_BASE/ipset/zapret-hosts.txt" +[ -f "$HOSTLIST" ] || HOSTLIST="$ZAPRET_BASE/ipset/zapret-hosts-user.txt" +TPWS_OPT_BASE="--user=$TPWS_USER --port=$TPPORT" +TPWS_OPT_BASE4="--bind-addr=$TPWS_LOCALHOST4" +TPWS_OPT_BASE6="--bind-addr=::1" +# first wait for lan to ifup, then wait for bind-wait-ip-linklocal seconds for link local address and bind-wait-ip for any ipv6 as the worst case +TPWS_OPT_BASE6_PRE="--bind-linklocal=prefer --bind-wait-ifup=30 --bind-wait-ip=30 --bind-wait-ip-linklocal=3" + +run_daemon() +{ + # $1 - daemon string id or number. can use 1,2,3,... + # $2 - daemon + # $3 - daemon args + # use $PIDDIR/$DAEMONBASE$1.pid as pidfile + local DAEMONBASE=$(basename $2) + echo "Starting daemon $1: $2 $3" + procd_open_instance + procd_set_param command $2 $3 + procd_set_param pidfile $PIDDIR/$DAEMONBASE$1.pid + procd_close_instance +} + +run_tpws() +{ + [ "$DISABLE_IPV4" = "1" ] && [ "$DISABLE_IPV6" = "1" ] && return 0 + + local OPT="$TPWS_OPT_BASE" + local DEVICE + + [ "$DISABLE_IPV4" = "1" ] || OPT="$OPT $TPWS_OPT_BASE4" + [ "$DISABLE_IPV6" = "1" ] || { + OPT="$OPT $TPWS_OPT_BASE6" + network_get_device DEVICE lan + [ -n "$DEVICE" ] && OPT="$OPT --bind-iface6=$DEVICE $TPWS_OPT_BASE6_PRE" + } + run_daemon $1 $TPWS "$OPT $2" +} +stop_tpws() +{ + stop_daemon $1 $TPWS +} + + +filter_apply_hostlist_target() +{ + # $1 - var name of tpws or nfqws params + [ "$MODE_FILTER" = "hostlist" ] && eval $1="\"\$$1 --hostlist=$HOSTLIST\"" +} + + +start_service() { + local opt + + case "${MODE}" in + tpws) + opt="$TPWS_OPT" + filter_apply_hostlist_target opt + run_tpws 1 "$opt" + ;; + nfqws) + opt="$NFQWS_OPT_BASE $NFQWS_OPT_DESYNC" + filter_apply_hostlist_target opt + run_daemon 1 $NFQWS "$opt" + ;; + custom) + existf zapret_custom_daemons && zapret_custom_daemons $1 + ;; + esac +} diff --git a/init.d/systemd/zapret-list-update.service b/init.d/systemd/zapret-list-update.service new file mode 100644 index 0000000..eeee1b0 --- /dev/null +++ b/init.d/systemd/zapret-list-update.service @@ -0,0 +1,13 @@ +[Unit] +Description=zapret ip/host list update + +[Service] +Restart=no +IgnoreSIGPIPE=no +KillMode=control-group +GuessMainPID=no +RemainAfterExit=no +ExecStart=/opt/zapret/ipset/get_config.sh + +[Install] +WantedBy=multi-user.target diff --git a/init.d/systemd/zapret-list-update.timer b/init.d/systemd/zapret-list-update.timer new file mode 100644 index 0000000..29379bd --- /dev/null +++ b/init.d/systemd/zapret-list-update.timer @@ -0,0 +1,11 @@ +[Unit] +Description=zapret ip/host list update timer + +[Timer] +OnCalendar=*-*-2,4,6,8,10,12,14,16,18,20,22,24,26,28,30 00:00:00 +RandomizedDelaySec=86400 +Persistent=true +Unit=zapret-list-update.service + +[Install] +WantedBy=timers.target diff --git a/init.d/systemd/zapret.service b/init.d/systemd/zapret.service new file mode 100644 index 0000000..9d3bf41 --- /dev/null +++ b/init.d/systemd/zapret.service @@ -0,0 +1,17 @@ +[Unit] +After=network-online.target +Wants=network-online.target + +[Service] +Type=forking +Restart=no +TimeoutSec=30sec +IgnoreSIGPIPE=no +KillMode=none +GuessMainPID=no +RemainAfterExit=no +ExecStart=/opt/zapret/init.d/sysv/zapret start +ExecStop=/opt/zapret/init.d/sysv/zapret stop + +[Install] +WantedBy=multi-user.target diff --git a/init.d/sysv/custom b/init.d/sysv/custom new file mode 100644 index 0000000..ed6183b --- /dev/null +++ b/init.d/sysv/custom @@ -0,0 +1,24 @@ +# this script contain your special code to launch daemons and configure firewall +# use helpers from "functions" file +# in case of upgrade keep this file only, do not modify others + +zapret_custom_daemons() +{ + # $1 - 1 - run, 0 - stop + + # PLACEHOLDER + echo !!! NEED ATTENTION !!! + echo Start daemon\(s\) + echo Study how other sections work + + do_daemon $1 1 /bin/sleep 20 +} +zapret_custom_firewall() +{ + # $1 - 1 - run, 0 - stop + + # PLACEHOLDER + echo !!! NEED ATTENTION !!! + echo Configure iptables for required actions + echo Study how other sections work +} diff --git a/init.d/sysv/custom-2nfqws b/init.d/sysv/custom-2nfqws new file mode 100644 index 0000000..2f942a4 --- /dev/null +++ b/init.d/sysv/custom-2nfqws @@ -0,0 +1,44 @@ +# this custom script demonstrates how to use 2 copies of nfqws +# it preserves config settings : MODE_HTTP, MODE_HTTP_KEEPALIVE, MODE_HTTPS, MODE_FILTER, NFQWS_OPT_DESYNC +# NFQWS_OPT_DESYNC - parameters for http +# NFQWS_OPT_DESYNC2 - parameters for https. you should add this variable to config file, its absent there + +QNUM2=$(($QNUM+1)) + +zapret_custom_daemons() +{ + local opt + + [ "$MODE_HTTP" = "1" ] && { + opt="$NFQWS_OPT_DESYNC" + filter_apply_hostlist_target opt + do_nfqws $1 1 "$opt" + } + + [ "$MODE_HTTPS" = "1" ] && { + opt="$NFQWS_OPT_DESYNC2 --qnum=$QNUM2" + filter_apply_hostlist_target opt + do_nfqws $1 2 "$opt" + } +} +zapret_custom_firewall() +{ + local f4 f6 + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4" + local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" + + [ "$MODE_HTTP" = "1" ] && { + f4="--dport 80" + [ "$MODE_HTTP_KEEPALIVE" = "1" ] || f4="$f4 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post $1 "$f4 $desync" "$f6 $desync" $QNUM + } + + [ "$MODE_HTTPS" = "1" ] && { + f4="--dport 443 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post $1 "$f4 $desync" "$f6 $desync" $QNUM2 + } +} diff --git a/init.d/sysv/custom-tpws4http-nfqws4https b/init.d/sysv/custom-tpws4http-nfqws4https new file mode 100644 index 0000000..411b8fe --- /dev/null +++ b/init.d/sysv/custom-tpws4http-nfqws4https @@ -0,0 +1,39 @@ +# this custom script demonstrates how to apply tpws to http and nfqws to https +# it preserves config settings : MODE_HTTP, MODE_HTTPS, MODE_FILTER, TPWS_OPT, NFQWS_OPT_DESYNC + +zapret_custom_daemons() +{ + local opt + + [ "$MODE_HTTP" = "1" ] && { + opt="$TPWS_OPT" + filter_apply_hostlist_target opt + do_tpws $1 1 "$opt" + } + + [ "$MODE_HTTPS" = "1" ] && { + opt="$NFQWS_OPT_DESYNC" + filter_apply_hostlist_target opt + do_nfqws $1 2 "$opt" + } +} +zapret_custom_firewall() +{ + local f4 f6 + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4" + local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" + + [ "$MODE_HTTP" = "1" ] && { + f4="--dport 80" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_tpws $1 "$f4" "$f6" $TPPORT + } + + [ "$MODE_HTTPS" = "1" ] && { + f4="--dport 443 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post $1 "$f4 $desync" "$f6 $desync" $QNUM + } +} diff --git a/init.d/sysv/functions b/init.d/sysv/functions new file mode 100644 index 0000000..1c5535a --- /dev/null +++ b/init.d/sysv/functions @@ -0,0 +1,532 @@ +# init script functions library for desktop linux systems + +[ -n "$ZAPRET_BASE" ] || ZAPRET_BASE=/opt/zapret +# SHOULD EDIT config +. "$ZAPRET_BASE/config" + +PIDDIR=/var/run + +IPSET_CR="$ZAPRET_BASE/ipset/create_ipset.sh" + +WS_USER=tpws + +QNUM=200 +NFQWS="$ZAPRET_BASE/nfq/nfqws" +NFQWS_OPT_BASE="--qnum=$QNUM --user=$WS_USER" + +TPPORT=988 +TPWS="$ZAPRET_BASE/tpws/tpws" +TPWS_LOCALHOST4=127.0.0.127 +HOSTLIST="$ZAPRET_BASE/ipset/zapret-hosts.txt.gz" +[ -f "$HOSTLIST" ] || HOSTLIST="$ZAPRET_BASE/ipset/zapret-hosts.txt" +[ -f "$HOSTLIST" ] || HOSTLIST="$ZAPRET_BASE/ipset/zapret-hosts-user.txt" + +TPWS_OPT_BASE="--user=$WS_USER --port=$TPPORT" +TPWS_OPT_BASE4="--bind-addr=$TPWS_LOCALHOST4" +TPWS_OPT_BASE6="--bind-addr=::1" +# first wait for lan to ifup, then wait for bind-wait-ip-linklocal seconds for link local address and bind-wait-ip for any ipv6 as the worst case +TPWS_OPT_BASE6_PRE="--bind-linklocal=prefer --bind-wait-ifup=30 --bind-wait-ip=30 --bind-wait-ip-linklocal=3" + +[ -n "$IFACE_WAN" ] && IPT_OWAN="-o $IFACE_WAN" +[ -n "$IFACE_WAN" ] && IPT_IWAN="-i $IFACE_WAN" +[ -n "$IFACE_LAN" ] && IPT_ILAN="-i $IFACE_LAN" + +[ -n "$DESYNC_MARK" ] || DESYNC_MARK=0x40000000 + +# max wait time for the link local ipv6 on the LAN interface +LINKLOCAL_WAIT_SEC=5 + +CUSTOM_SCRIPT="$ZAPRET_BASE/init.d/sysv/custom" +[ -f "$CUSTOM_SCRIPT" ] && . "$CUSTOM_SCRIPT" + +IPSET_EXCLUDE="-m set ! --match-set nozapret" +IPSET_EXCLUDE6="-m set ! --match-set nozapret6" + +exists() +{ + which "$1" >/dev/null 2>/dev/null +} +existf() +{ + type "$1" >/dev/null 2>/dev/null +} + +on_off_function() +{ + # $1 : function name on + # $2 : function name off + # $3 : 0 - off, 1 - on + local F="$1" + [ "$3" = "1" ] || F="$2" + shift + shift + shift + "$F" "$@" +} + + +ipt() +{ + iptables -C "$@" 2>/dev/null || iptables -I "$@" +} +ipt_del() +{ + iptables -C "$@" 2>/dev/null && iptables -D "$@" +} +ipt_add_del() +{ + on_off_function ipt ipt_del "$@" +} +ipt6() +{ + ip6tables -C "$@" 2>/dev/null || ip6tables -I "$@" +} +ipt6_del() +{ + ip6tables -C "$@" 2>/dev/null && ip6tables -D "$@" +} +ipt6_add_del() +{ + on_off_function ipt6 ipt6_del "$@" +} + +# there's no route_localnet for ipv6 +# the best we can is to route to link local of the incoming interface +# OUTPUT - can DNAT to ::1 +# PREROUTING - can't DNAT to ::1. can DNAT to link local of -i interface or to any global addr +# not a good idea to expose tpws to the world (bind to ::) + +get_ipv6_linklocal() +{ + # $1 - interface name. if empty - any interface + local dev + [ -n "$1" ] && dev="dev $1" + ip addr show $dev | sed -e 's/^.*inet6 \([^ ]*\)\/[0-9]* scope link.*$/\1/;t;d' | head -n 1 +} +get_ipv6_global() +{ + # $1 - interface name. if empty - any interface + local dev + [ -n "$1" ] && dev="dev $1" + ip addr show $dev | sed -e 's/^.*inet6 \([^ ]*\)\/[0-9]* scope global.*$/\1/;t;d' | head -n 1 +} + +iface_is_up() +{ + # $1 - interface name + [ -f /sys/class/net/$1/operstate ] || return + local state + read state /dev/null || { + # allow localnet route only to special tpws IP + iptables -N input_lan_rule_zapret 2>/dev/null + iptables -F input_lan_rule_zapret + iptables -A input_lan_rule_zapret -d $TPWS_LOCALHOST4 -j RETURN + iptables -A input_lan_rule_zapret -d 127.0.0.0/8 -j DROP + iptables -I INPUT -i $IFACE_LAN -j input_lan_rule_zapret + } + sysctl -qw net.ipv4.conf.$IFACE_LAN.route_localnet=1 + } +} +unprepare_tpws_fw4() +{ + [ -n "$IFACE_LAN" ] && { + iptables -C INPUT -i $IFACE_LAN -j input_lan_rule_zapret 2>/dev/null && { + sysctl -qw net.ipv4.conf.$IFACE_LAN.route_localnet=0 + iptables -D INPUT -i $IFACE_LAN -j input_lan_rule_zapret + iptables -F input_lan_rule_zapret + iptables -X input_lan_rule_zapret + } + } +} +unprepare_tpws_fw() +{ + unprepare_tpws_fw4 +} + + +print_op() +{ + if [ "$1" = "1" ]; then + echo "Adding ip$4tables rule for $3 : $2" + else + echo "Deleting ip$4tables rule for $3 : $2" + fi +} + +fw_tpws4() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - tpws port + [ "$DISABLE_IPV4" = "1" ] || { + [ "$1" = 1 ] && prepare_tpws_fw4 + print_op $1 "$2" "tpws" + [ -n "$IFACE_LAN" ] && { + ipt_add_del $1 PREROUTING -t nat $IPT_ILAN -p tcp $2 $IPSET_EXCLUDE dst -j DNAT --to $TPWS_LOCALHOST4:$3 + } + ipt_add_del $1 OUTPUT -t nat $IPT_OWAN -m owner ! --uid-owner $WS_USER -p tcp $2 $IPSET_EXCLUDE dst -j DNAT --to $TPWS_LOCALHOST4:$3 + } +} +fw_tpws6() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv6 + # $3 - tpws port + [ "$DISABLE_IPV6" = "1" ] || { + print_op $1 "$2" "tpws" 6 + [ -n "$IFACE_LAN" ] && { + dnat6_target + [ "$DNAT6_TARGET" != "-" ] && ipt6_add_del $1 PREROUTING -t nat $IPT_ILAN -p tcp $2 $IPSET_EXCLUDE6 dst -j DNAT --to [$DNAT6_TARGET]:$3 + } + ipt6_add_del $1 OUTPUT -t nat $IPT_OWAN -m owner ! --uid-owner $WS_USER -p tcp $2 $IPSET_EXCLUDE6 dst -j DNAT --to [::1]:$3 + } +} +fw_tpws() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - iptable filter for ipv6 + # $4 - tpws port + fw_tpws4 $1 "$2" $4 + fw_tpws6 $1 "$3" $4 +} + + +fw_nfqws_pre4() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - queue number + [ "$DISABLE_IPV4" = "1" ] || { + print_op $1 "$2" "nfqws prerouting" + ipt_add_del $1 PREROUTING -t mangle $IPT_IWAN -p tcp $2 $IPSET_EXCLUDE src -j NFQUEUE --queue-num $3 --queue-bypass + } +} +fw_nfqws_pre6() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv6 + # $3 - queue number + [ "$DISABLE_IPV6" = "1" ] || { + print_op $1 "$2" "nfqws prerouting" 6 + ipt6_add_del $1 PREROUTING -t mangle $IPT_IWAN -p tcp $2 $IPSET_EXCLUDE6 src -j NFQUEUE --queue-num $3 --queue-bypass + } +} +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 +} +fw_nfqws_post4() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - queue number + [ "$DISABLE_IPV4" = "1" ] || { + print_op $1 "$2" "nfqws postrouting" + ipt_add_del $1 POSTROUTING -t mangle $IPT_OWAN -p tcp $2 $IPSET_EXCLUDE dst -j NFQUEUE --queue-num $3 --queue-bypass + } +} +fw_nfqws_post6() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv6 + # $3 - queue number + [ "$DISABLE_IPV6" = "1" ] || { + print_op $1 "$2" "nfqws postrouting" 6 + ipt6_add_del $1 POSTROUTING -t mangle $IPT_OWAN -p tcp $2 $IPSET_EXCLUDE6 dst -j NFQUEUE --queue-num $3 --queue-bypass + } +} +fw_nfqws_post() +{ + # $1 - 1 - add, 0 - del + # $2 - iptable filter for ipv4 + # $3 - iptable filter for ipv6 + # $4 - queue number + fw_nfqws_post4 $1 "$2" $4 + fw_nfqws_post6 $1 "$3" $4 +} + + +run_daemon() +{ + # $1 - daemon number : 1,2,3,... + # $2 - daemon + # $3 - daemon args + # use $PIDDIR/$DAEMONBASE$1.pid as pidfile + + local DAEMONBASE=$(basename $2) + local PIDFILE=$PIDDIR/$DAEMONBASE$1.pid + echo "Starting daemon $1: $2 $3" + if exists start-stop-daemon ; then + start-stop-daemon --start --pidfile "$PIDFILE" --background --make-pidfile --exec "$2" -- $3 + else + if [ -f "$PIDFILE" ] && pgrep -F "$PIDFILE" "$DAEMONBASE" >/dev/null; then + echo already running + else + "$2" $3 >/dev/null 2>/dev/null & + PID=$! + if [ -n "$PID" ]; then + echo $PID >$PIDFILE + else + echo could not start daemon $1 : $2 $3 + false + fi + fi + fi +} +stop_daemon() +{ + # $1 - daemon number : 1,2,3,... + # $2 - daemon + # use $PIDDIR/$DAEMONBASE$1.pid as pidfile + local DAEMONBASE=$(basename $2) + local PIDFILE=$PIDDIR/$DAEMONBASE$1.pid + echo "Stopping daemon $1: $2" + if exists start-stop-daemon ; then + start-stop-daemon --stop --pidfile "$PIDFILE" --exec "$2" + else + if [ -f "$PIDFILE" ]; then + read PID <"$PIDFILE" + kill $PID + rm -f "$PIDFILE" + else + echo no pidfile : $PIDFILE + fi + fi +} +do_daemon() +{ + # $1 - 1 - run, 0 - stop + on_off_function run_daemon stop_daemon "$@" +} + + +prepare_user() +{ + # $WS_USER is required to prevent redirection of the traffic originating from TPWS itself + # otherwise infinite loop will occur + # also its good idea not to run tpws as root + id -u $WS_USER >/dev/null 2>/dev/null || useradd --no-create-home --system --shell /bin/false $WS_USER +} +do_tpws() +{ + # $1 : 1 - run, 0 - stop + # $2 : daemon number + # $3 : daemon args + + [ "$1" = "1" ] && prepare_user + + [ "$DISABLE_IPV4" = "1" ] && [ "$DISABLE_IPV6" = "1" ] && return 0 + + local OPT="$TPWS_OPT_BASE" + + [ "$DISABLE_IPV4" = "1" ] || OPT="$OPT $TPWS_OPT_BASE4" + [ "$DISABLE_IPV6" = "1" ] || { + OPT="$OPT $TPWS_OPT_BASE6" + [ -n "$IFACE_LAN" ] && OPT="$OPT --bind-iface6=$IFACE_LAN $TPWS_OPT_BASE6_PRE" + } + + do_daemon $1 $2 $TPWS "$OPT $3" +} +do_nfqws() +{ + # $1 : 1 - run, 0 - stop + # $2 : daemon number + # $3 : daemon args + + [ "$1" = "1" ] && prepare_user + do_daemon $1 $2 $NFQWS "$NFQWS_OPT_BASE $3" +} + + +filter_apply_port_target() +{ + # $1 - var name of iptables filter + local f + if [ "$MODE_HTTP" = "1" ] && [ "$MODE_HTTPS" = "1" ]; then + f="-m multiport --dports 80,443" + elif [ "$MODE_HTTPS" = "1" ]; then + f="--dport 443" + elif [ "$MODE_HTTP" = "1" ]; then + f="--dport 80" + else + echo WARNING !!! HTTP and HTTPS are both disabled + fi + eval $1="\"\$$1 $f\"" +} +filter_apply_ipset_target() +{ + # $1 - var name of ipv4 iptables filter + # $2 - var name of ipv6 iptables filter + if [ "$MODE_FILTER" = "ipset" ]; then + eval $1="\"\$$1 -m set --match-set zapret dst\"" + eval $2="\"\$$2 -m set --match-set zapret6 dst\"" + fi +} +filter_apply_hostlist_target() +{ + # $1 - var name of tpws or nfqws params + [ "$MODE_FILTER" = "hostlist" ] && eval $1="\"\$$1 --hostlist=$HOSTLIST\"" +} + + +create_ipset() +{ + echo "Creating ipset" + "$IPSET_CR" "$@" +} + + + +zapret_do_firewall() +{ + # $1 - 1 - add, 0 - del + + local first_packet_only="-m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:4" + local desync="-m mark ! --mark $DESYNC_MARK/$DESYNC_MARK" + local f4 f6 + + # always create ipsets. ip_exclude ipset is required + [ "$1" != "1" ] || create_ipset no-update + + case "${MODE}" in + tpws) + if [ ! "$MODE_HTTP" = "1" ] && [ ! "$MODE_HTTPS" = "1" ]; then + echo both http and https are disabled. not applying redirection. + else + filter_apply_port_target f4 + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_tpws $1 "$f4" "$f6" $TPPORT + fi + ;; + + nfqws) + if [ ! "$MODE_HTTP" = "1" ] && [ ! "$MODE_HTTPS" = "1" ]; then + echo both http and https are disabled. not applying redirection. + else + if [ "$MODE_HTTP_KEEPALIVE" = "1" ]; then + if [ "$MODE_HTTP" = "1" ]; then + f4="--dport 80" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post $1 "$f4 $desync" "$f6 $desync" $QNUM + fi + if [ "$MODE_HTTPS" = "1" ]; then + f4="--dport 443 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post $1 "$f4 $desync" "$f6 $desync" $QNUM + fi + else + filter_apply_port_target f4 + f4="$f4 $first_packet_only" + f6=$f4 + filter_apply_ipset_target f4 f6 + fw_nfqws_post $1 "$f4 $desync" "$f6 $desync" $QNUM + fi + fi + ;; + custom) + existf zapret_custom_firewall && zapret_custom_firewall $1 + ;; + esac + [ "$1" = 0 ] && unprepare_tpws_fw +} +zapret_apply_firewall() +{ + zapret_do_firewall 1 "$@" +} +zapret_unapply_firewall() +{ + zapret_do_firewall 0 "$@" +} + +zapret_do_daemons() +{ + # $1 - 1 - run, 0 - stop + + local opt + + case "${MODE}" in + tpws) + opt="$TPWS_OPT" + filter_apply_hostlist_target opt + do_tpws $1 1 "$opt" + ;; + nfqws) + opt="$NFQWS_OPT_DESYNC" + filter_apply_hostlist_target opt + do_nfqws $1 1 "$opt" + ;; + custom) + existf zapret_custom_daemons && zapret_custom_daemons $1 + ;; + esac +} + +zapret_run_daemons() +{ + zapret_do_daemons 1 "$@" +} +zapret_stop_daemons() +{ + zapret_do_daemons 0 "$@" +} diff --git a/init.d/sysv/zapret b/init.d/sysv/zapret new file mode 100755 index 0000000..0691bba --- /dev/null +++ b/init.d/sysv/zapret @@ -0,0 +1,50 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: zapret +# Required-Start: $local_fs $network +# Required-Stop: $local_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +### END INIT INFO + +SCRIPT=$(readlink -f "$0") +EXEDIR=$(dirname "$SCRIPT") +ZAPRET_BASE=$(readlink -f "$EXEDIR/../..") +. "$EXEDIR/functions" + +NAME=zapret +DESC=anti-zapret + +case "$1" in + start) + zapret_run_daemons + [ "$INIT_APPLY_FW" != "1" ] || zapret_apply_firewall + ;; + + stop) + zapret_stop_daemons + [ "$INIT_APPLY_FW" != "1" ] || zapret_unapply_firewall + ;; + + start-fw) + zapret_apply_firewall + ;; + stop-fw) + zapret_unapply_firewall + ;; + + start-daemons) + zapret_run_daemons + ;; + stop-daemons) + zapret_stop_daemons + ;; + + *) + N=/etc/init.d/$NAME + echo "Usage: $N {start|stop|start-fw|stop-fw|start-daemons|stop-daemons}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/install_bin.sh b/install_bin.sh new file mode 100755 index 0000000..60d5951 --- /dev/null +++ b/install_bin.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +EXEDIR="$(dirname "$0")" +EXEDIR="$(cd "$EXEDIR"; pwd)" +BINS=binaries +BINDIR="$EXEDIR/$BINS" + +check_dir() +{ + local exe=$BINDIR/$1/ip2net + if [ -f "$exe" ]; then + if [ -x "$exe" ]; then + echo 0.0.0.0 | "$exe" 1>/dev/null 2>/dev/null + else + echo "$exe is not executable. set proper chmod." + return 1 + fi + else + echo "$exe is absent" + return 2 + fi +} + +# link or copy executables. uncomment either ln or cp, comment other +ccp() +{ + local F=$(basename $1) + [ -d "$EXEDIR/$2" ] || mkdir "$EXEDIR/$2" + [ -f "$EXEDIR/$2/$F" ] && rm -f "$EXEDIR/$2/$F" + ln -fs "../$BINS/$1" "$EXEDIR/$2" && echo linking : "../$BINS/$1" =\> "$EXEDIR/$2" + #cp -f "$BINDIR/$1" "$EXEDIR/$2" && echo copying : "$BINDIR/$1" =\> "$EXEDIR/$2" +} + +UNAME=$(uname) +if [ "$UNAME" = "Linux" ]; then + ARCHLIST="my x86_64 x86 aarch64 arm mips64r2-msb mips32r1-lsb mips32r1-msb ppc" +elif [ "$UNAME" = "Darwin" ]; then + ARCHLIST="my mac64" +else + ARCHLIST="my" +fi + +if [ "$1" = "getarch" ]; then + for arch in $ARCHLIST + do + [ -d "$BINDIR/$arch" ] || continue + if check_dir $arch; then + echo $arch + exit 0 + fi + done +else + for arch in $ARCHLIST + do + [ -d "$BINDIR/$arch" ] || continue + if check_dir $arch; then + echo $arch is OK + echo installing binaries ... + ccp $arch/ip2net ip2net + ccp $arch/mdig mdig + if [ "$(uname)" = "Linux" ]; then + ccp $arch/nfqws nfq + else + ccp $arch/dvtws nfq + fi + ccp $arch/tpws tpws + exit 0 + else + echo $arch is NOT OK + fi + done + echo no compatible binaries found +fi + +exit 1 diff --git a/install_easy.sh b/install_easy.sh new file mode 100755 index 0000000..8323669 --- /dev/null +++ b/install_easy.sh @@ -0,0 +1,1235 @@ +#!/bin/sh + +# automated script for easy installing zapret + +EXEDIR="$(dirname "$0")" +EXEDIR="$(cd "$EXEDIR"; pwd)" +IPSET_DIR="$EXEDIR/ipset" +ZAPRET_CONFIG="$EXEDIR/config" +ZAPRET_BASE="$EXEDIR" + +. "$ZAPRET_CONFIG" + +# install target +ZAPRET_TARGET=/opt/zapret + +GET_LIST="$IPSET_DIR/get_config.sh" +GET_LIST_PREFIX=/ipset/get_ +INIT_SCRIPT=/etc/init.d/zapret + +DNSCHECK_DNS="8.8.8.8 1.1.1.1 77.88.8.8" +DNSCHECK_DOM="pornhub.com putinhuylo.com rutracker.org nnmclub.to kinozal.tv" +DNSCHECK_DIG1=/tmp/dig1.txt +DNSCHECK_DIG2=/tmp/dig2.txt +DNSCHECK_DIGS=/tmp/digs.txt + +SYSTEMD_SYSTEM_DIR=/lib/systemd/system +[ -d "$SYSTEMD_SYSTEM_DIR" ] || SYSTEMD_SYSTEM_DIR=/usr/lib/systemd/system + +ECHON="echo -n" + +exists() +{ + which $1 >/dev/null 2>/dev/null +} +whichq() +{ + which $1 2>/dev/null +} + +MD5=md5sum +exists $MD5 || MD5=md5 + +contains() +{ + # check if substring $2 contains in $1 + [ "${1#*$2}" != "$1" ] +} + +exitp() +{ + local A + + echo + echo press enter to continue + read A + exit $1 +} + +require_root() +{ + [ $(id -u) -ne "0" ] && { + echo root is required + exists sudo && exec sudo "$0" + exists su && exec su -c "$0" + echo su or sudo not found + exitp 2 + } +} + +sedi() +{ + # MacOS doesnt support -i without parameter. busybox doesnt support -i with parameter. + # its not possible to put "sed -i ''" to a variable and then use it + if [ "$SYSTEM" = "macos" ]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +read_yes_no() +{ + # $1 - default (Y/N) + local A + read A + [ -z "$A" ] || ([ "$A" != "Y" ] && [ "$A" != "y" ] && [ "$A" != "N" ] && [ "$A" != "n" ]) && A=$1 + [ "$A" = "Y" ] || [ "$A" = "y" ] || [ "$A" = "1" ] +} +ask_yes_no() +{ + # $1 - default (Y/N or 0/1) + # $2 - text + local DEFAULT=$1 + [ "$1" = "1" ] && DEFAULT=Y + [ "$1" = "0" ] && DEFAULT=N + [ -z "$DEFAULT" ] && DEFAULT=N + $ECHON "$2 (default : $DEFAULT) (Y/N) ? " + read_yes_no $DEFAULT +} +ask_yes_no_var() +{ + # $1 - variable name for answer : 0/1 + # $2 - text + local DEFAULT + eval DEFAULT="\$$1" + if ask_yes_no "$DEFAULT" "$2"; then + eval $1=1 + else + eval $1=0 + fi +} + +on_off_function() +{ + # $1 : function name on + # $2 : function name off + # $3 : 0 - off, 1 - on + local F="$1" + [ "$3" = "1" ] || F="$2" + shift + shift + shift + "$F" "$@" +} + +get_dir_inode() +{ + local dir="$1" + [ -L "$dir" ] && dir=$(readlink "$dir") + ls -id "$dir" | awk '{print $1}' +} + +random() +{ + # $1 - min, $2 - max + local r rs + if [ -c /dev/urandom ]; then + read rs /dev/null) + [ -z "$M" ] && M="$M_DEFAULT" + echo selected : $M + eval $1="$M" + + [ "$M" != "$M_OLD" ] +} +write_config_var() +{ + # $1 - mode var + local M + eval M="\$$1" + + if grep -q "^$1=\|^#$1=" "$ZAPRET_CONFIG"; then + # replace / => \/ + #M=${M//\//\\\/} + M=$(echo $M | sed 's/\//\\\//g') + if [ -n "$M" ]; then + if contains "$M" " "; then + sedi -Ee "s/^#?$1=.*$/$1=\"$M\"/" "$ZAPRET_CONFIG" + else + sedi -Ee "s/^#?$1=.*$/$1=$M/" "$ZAPRET_CONFIG" + fi + else + # write with comment at the beginning + sedi -Ee "s/^#?$1=.*$/#$1=/" "$ZAPRET_CONFIG" + fi + else + # var does not exist in config. add it + if [ -n "$M" ]; then + echo "$1=$M" >>"$ZAPRET_CONFIG" + else + echo "#$1=$M" >>"$ZAPRET_CONFIG" + fi + fi +} + +select_mode_mode() +{ + local MODES="tpws nfqws filter custom" + [ "$SYSTEM" = "macos" ] && MODES="tpws filter" + echo + echo select MODE : + ask_list MODE "$MODES" tpws && write_config_var MODE + case $MODE in + tpws) + echo + echo tpws options : $TPWS_OPT + echo to change : edit TPWS_OPT in $ZAPRET_CONFIG + ;; + nfqws) + echo + echo nfqws options : $NFQWS_OPT_DESYNC + echo to change : edit NFQWS_OPT_DESYNC in $ZAPRET_CONFIG + ;; + esac +} +select_mode_http() +{ + [ "$MODE" != "filter" ] && { + echo + ask_yes_no_var MODE_HTTP "enable http support" + write_config_var MODE_HTTP + } +} +select_mode_keepalive() +{ + [ "$MODE" = "nfqws" ] && [ "$MODE_HTTP" = "1" ] && { + echo + echo enable keep alive support only if DPI checks every outgoing packet for http signature + echo dont enable otherwise because it consumes more cpu resources + ask_yes_no_var MODE_HTTP_KEEPALIVE "enable http keep alive support" + write_config_var MODE_HTTP_KEEPALIVE + } +} +select_mode_https() +{ + [ "$MODE" != "filter" ] && { + echo + ask_yes_no_var MODE_HTTPS "enable https support" + write_config_var MODE_HTTPS + } +} +select_mode_filter() +{ + echo + echo select filtering : + ask_list MODE_FILTER "none ipset hostlist" none && write_config_var MODE_FILTER +} +select_mode() +{ + select_mode_mode + select_mode_http + select_mode_keepalive + select_mode_https + select_mode_filter +} + +select_getlist() +{ + if [ "$MODE_FILTER" = "ipset" -o "$MODE_FILTER" = "hostlist" ]; then + local D=N + [ -n "$GETLIST" ] && D=Y + echo + if ask_yes_no $D "do you want to auto download ip/host list"; then + if [ "$MODE_FILTER" = "hostlist" ] ; then + local GL_OLD=$GETLIST + GETLIST="get_reestr_hostlist.sh" + [ "$GL_OLD" != "$GET_LIST" ] && write_config_var GETLIST + else + GETLISTS="get_user.sh get_antifilter_ip.sh get_antifilter_ipsmart.sh get_antifilter_ipsum.sh get_reestr_ip.sh get_reestr_combined.sh get_reestr_resolve.sh" + GETLIST_DEF="get_antifilter_ipsmart.sh" + ask_list GETLIST "$GETLISTS" "$GETLIST_DEF" && write_config_var GETLIST + fi + return + fi + fi + GETLIST="" + write_config_var GETLIST +} +select_ipv6() +{ + local T=N + + [ "$DISABLE_IPV6" != '1' ] && T=Y + local old6=$DISABLE_IPV6 + echo + if ask_yes_no $T "enable ipv6 support"; then + DISABLE_IPV6=0 + else + DISABLE_IPV6=1 + fi + [ "$old6" != "$DISABLE_IPV6" ] && write_config_var DISABLE_IPV6 +} + +ask_config() +{ + select_mode + select_getlist +} + +ask_iface() +{ + # $1 - var to ask + # $2 - additional name () + + local ifs i0 + [ -n "$2" ] && i0="$2 " + case $SYSTEM in + macos) + ifs="$(ifconfig -l)" + ;; + *) + ifs="$(ls /sys/class/net)" + ;; + esac + ask_list $1 "$i0$ifs" && write_config_var $1 +} + +ask_config_offload() +{ + is_flow_offload_avail && { + echo + echo flow offloading can greatly increase speed on slow devices and high speed links \(usually 150+ mbits\) + echo unfortuantely its not compatible with most nfqws options. nfqws traffic must be exempted from flow offloading. + echo donttouch = disable system flow offloading setting if nfqws mode was selected, dont touch it otherwise and dont configure selective flow offloading + echo none = always disable system flow offloading setting and dont configure selective flow offloading + echo software = always disable system flow offloading setting and configure selective software flow offloading + echo hardware = always disable system flow offloading setting and configure selective hardware flow offloading + echo select flow offloading : + ask_list FLOWOFFLOAD "donttouch none software hardware" donttouch && write_config_var FLOWOFFLOAD + } +} + +get_free_space_mb() +{ + df -m $PWD | awk '/[0-9]%/{print $(NF-2)}' +} +get_ram_kb() +{ + grep MemTotal /proc/meminfo | awk '{print $2}' +} +get_ram_mb() +{ + local R=$(get_ram_kb) + echo $(($R/1024)) +} + +ask_config_tmpdir() +{ + # ask tmpdir change for low ram systems with enough free disk space + [ -n "$GETLIST" ] && [ $(get_free_space_mb "$EXEDIR/tmp") -ge 128 ] && [ $(get_ram_mb) -le 400 ] && { + echo + echo /tmp in openwrt is tmpfs. on low RAM systems there may be not enough RAM to store downloaded files + echo default tmpfs has size of 50% RAM + echo "RAM : $(get_ram_mb) Mb" + echo "DISK : $(get_free_space_mb) Mb" + echo select temp file location + [ -z "$TMPDIR" ] && TMPDIR=/tmp + ask_list TMPDIR "/tmp $EXEDIR/tmp" && { + [ "$TMPDIR" = "/tmp" ] && TMPDIR= + write_config_var TMPDIR + } + } +} + +select_router_iface() +{ + # $1 - ask iface function name + + local T=N + [ -n "$IFACE_LAN" ] && T=Y + local old_lan=$IFACE_LAN + local old_wan=$IFACE_WAN + + echo + if [ "$SYSTEM" = "macos" ]; then + echo "WARNING ! OS feature \"internet sharing\" is not supported." + echo "Only manually configured PF router is supported." + else + echo "WARNING ! This installer will not configure routing, NAT, ... for you. Its your responsibility." + fi + if ask_yes_no $T "is this system a router"; then + echo LAN interface : + ask_iface IFACE_LAN + echo WAN interface : + [ -n "$IFACE_WAN" ] || IFACE_WAN="ANY" + ask_iface IFACE_WAN "ANY" + [ "$IFACE_WAN" = "ANY" ] && { + # any = not defined + IFACE_WAN= + write_config_var IFACE_WAN + } + else + [ -n "$old_lan" ] && { + IFACE_LAN="" + write_config_var IFACE_LAN + } + [ -n "$old_wan" ] && { + IFACE_WAN="" + write_config_var IFACE_WAN + } + fi +} +ask_config_desktop() +{ + select_router_iface +} + +copy_all() +{ + cp -R "$1" "$2" + [ -d "$2/tmp" ] || mkdir "$2/tmp" +} +copy_openwrt() +{ + local ARCH=$(get_bin_arch) + local BINDIR="$1/binaries/$ARCH" + + [ -d "$2" ] || mkdir -p "$2" + + mkdir "$2/tpws" "$2/nfq" "$2/ip2net" "$2/mdig" "$2/binaries" "$2/binaries/$ARCH" "$2/init.d" "$2/tmp" + cp -R "$1/ipset" "$2" + cp -R "$1/init.d/openwrt" "$2/init.d" + cp "$1/config" "$1/install_easy.sh" "$1/uninstall_easy.sh" "$1/install_bin.sh" "$2" + cp "$BINDIR/tpws" "$BINDIR/nfqws" "$BINDIR/ip2net" "$BINDIR/mdig" "$2/binaries/$ARCH" +} + +_backup_settings() +{ + local i=0 + for f in "$@"; do + [ -f "$ZAPRET_TARGET/$f" ] && cp -f "$ZAPRET_TARGET/$f" "/tmp/zapret-bkp-$i" + i=$(($i+1)) + done +} +_restore_settings() +{ + local i=0 + for f in "$@"; do + [ -f "/tmp/zapret-bkp-$i" ] && mv -f "/tmp/zapret-bkp-$i" "$ZAPRET_TARGET/$f" || rm -f "/tmp/zapret-bkp-$i" + i=$(($i+1)) + done +} +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" "ipset/zapret-hosts-user.txt" "ipset/zapret-hosts-user-exclude.txt" "ipset/zapret-hosts-user-ipban.txt" +} + +check_location() +{ + # $1 - copy function + + echo \* checking location + + # use inodes in case something is linked + [ -d "$ZAPRET_TARGET" ] && [ $(get_dir_inode "$EXEDIR") = $(get_dir_inode "$ZAPRET_TARGET") ] || { + echo + echo easy install is supported only from default location : $ZAPRET_TARGET + echo currently its run from $EXEDIR + if ask_yes_no N "do you want the installer to copy it for you"; then + local keep=N + if [ -d "$ZAPRET_TARGET" ]; then + echo + echo installer found existing $ZAPRET_TARGET + echo directory needs to be replaced. config and custom scripts can be kept or replaced with clean version + if ask_yes_no N "do you want to delete all files there and copy this version"; then + echo + ask_yes_no Y "keep config, custom scripts and user lists" && keep=Y + [ "$keep" = "Y" ] && backup_restore_settings 1 + rm -r "$ZAPRET_TARGET" + else + echo refused to overwrite $ZAPRET_TARGET. exiting + exitp 3 + fi + fi + local B=$(dirname "$ZAPRET_TARGET") + [ -d "$B" ] || mkdir -p "$B" + $1 "$EXEDIR" "$ZAPRET_TARGET" + [ "$keep" = "Y" ] && backup_restore_settings 0 + echo relaunching itself from $ZAPRET_TARGET + exec $ZAPRET_TARGET/$(basename $0) + else + echo copying aborted. exiting + exitp 3 + fi + } + echo running from $EXEDIR +} + + +check_prerequisites_linux() +{ + echo \* checking prerequisites + + if exists ipset && exists curl ; then + echo everything is present + else + echo \* installing prerequisites + + APTGET=$(whichq apt-get) + YUM=$(whichq yum) + PACMAN=$(whichq pacman) + ZYPPER=$(whichq zypper) + EOPKG=$(whichq eopkg) + if [ -x "$APTGET" ] ; then + "$APTGET" update + "$APTGET" install -y --no-install-recommends ipset curl dnsutils || { + echo could not install prerequisites + exitp 6 + } + elif [ -x "$YUM" ] ; then + "$YUM" -y install curl ipset || { + echo could not install prerequisites + exitp 6 + } + elif [ -x "$PACMAN" ] ; then + "$PACMAN" -Syy + "$PACMAN" --noconfirm -S ipset curl || { + echo could not install prerequisites + exitp 6 + } + elif [ -x "$ZYPPER" ] ; then + "$ZYPPER" --non-interactive install ipset curl || { + echo could not install prerequisites + exitp 6 + } + elif [ -x "$EOPKG" ] ; then + "$EOPKG" -y install ipset curl || { + echo could not install prerequisites + exitp 6 + } + else + echo supported package manager not found + echo you must manually install : ipset curl + exitp 5 + fi + fi +} + + +service_install_systemd() +{ + echo \* installing zapret service + + rm -f "$INIT_SCRIPT" + ln -fs "$EXEDIR/init.d/systemd/zapret.service" "$SYSTEMD_SYSTEM_DIR" + "$SYSTEMCTL" daemon-reload + "$SYSTEMCTL" enable zapret || { + echo could not enable systemd service + exitp 20 + } +} + +service_stop_systemd() +{ + echo \* stopping zapret service + + "$SYSTEMCTL" daemon-reload + "$SYSTEMCTL" disable zapret + "$SYSTEMCTL" stop zapret +} + +service_start_systemd() +{ + echo \* starting zapret service + + "$SYSTEMCTL" start zapret || { + echo could not start zapret service + exitp 30 + } +} + +timer_install_systemd() +{ + echo \* installing zapret-list-update timer + + "$SYSTEMCTL" disable zapret-list-update.timer + "$SYSTEMCTL" stop zapret-list-update.timer + ln -fs "$EXEDIR/init.d/systemd/zapret-list-update.service" "$SYSTEMD_SYSTEM_DIR" + ln -fs "$EXEDIR/init.d/systemd/zapret-list-update.timer" "$SYSTEMD_SYSTEM_DIR" + "$SYSTEMCTL" daemon-reload + "$SYSTEMCTL" enable zapret-list-update.timer || { + echo could not enable zapret-list-update.timer + exitp 20 + } + "$SYSTEMCTL" start zapret-list-update.timer || { + echo could not start zapret-list-update.timer + exitp 30 + } +} + +download_list() +{ + [ -x "$GET_LIST" ] && { + echo \* downloading blocked ip/host list + + # can be txt or txt.gz + "$IPSET_DIR/clear_lists.sh" + "$GET_LIST" || { + echo could not download ip list + exitp 25 + } + } +} + +end_with_newline() +{ + local c=$(tail -c 1) + [ "$c" = "" ] +} + +crontab_del_quiet() +{ + exists crontab || return + + CRONTMP=/tmp/cron.tmp + crontab -l >$CRONTMP 2>/dev/null + if grep -q "$GET_LIST_PREFIX" $CRONTMP; then + grep -v "$GET_LIST_PREFIX" $CRONTMP >$CRONTMP.2 + crontab $CRONTMP.2 + rm -f $CRONTMP.2 + fi + rm -f $CRONTMP +} + +crontab_add() +{ + # $1 - hour min + # $2 - hour max + [ -x "$GET_LIST" ] && { + echo \* adding crontab entry + + CRONTMP=/tmp/cron.tmp + crontab -l >$CRONTMP 2>/dev/null + if grep -q "$GET_LIST_PREFIX" $CRONTMP; then + echo some entries already exist in crontab. check if this is corrent : + grep "$GET_LIST_PREFIX" $CRONTMP + else + end_with_newline <"$CRONTMP" || echo >>"$CRONTMP" + echo "$(random 0 59) $(random $1 $2) */2 * * $GET_LIST" >>$CRONTMP + crontab $CRONTMP + fi + + rm -f $CRONTMP + } +} +cron_ensure_running() +{ + # if no crontabs present in /etc/cron openwrt init script does not launch crond. this is default + [ "$SYSTEM" = "openwrt" ] && { + /etc/init.d/cron enable + /etc/init.d/cron start + } +} + + +pingtest() +{ + ping -c 1 -W 1 $1 >/dev/null +} +find_working_public_dns() +{ + for dns in $DNSCHECK_DNS; do + pingtest $dns && nslookup w3.org $dns >/dev/null 2>/dev/null && { + PUBDNS=$dns + return 0 + } + done + return 1 +} +check_dns_spoof() +{ + # $1 - domain + # $2 - public DNS + echo $1 | "$EXEDIR/mdig/mdig" --family=4 >"$DNSCHECK_DIG1" + nslookup $1 $2 | sed -n '/Name:/,$p' | grep ^Address | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' >"$DNSCHECK_DIG2" + # check whether system resolver returns anything other than public DNS + grep -qvFf "$DNSCHECK_DIG2" "$DNSCHECK_DIG1" +} +check_dns_cleanup() +{ + rm -f "$DNSCHECK_DIG1" "$DNSCHECK_DIG2" "$DNSCHECK_DIGS" 2>/dev/null +} +check_dns() +{ + local C1 C2 + + echo \* checking DNS + + [ -f "$DNSCHECK_DIGS" ] && rm -f "$DNSCHECK_DIGS" + if find_working_public_dns ; then + echo comparing system resolver to public DNS : $PUBDNS + for dom in $DNSCHECK_DOM; do + if check_dns_spoof $dom $PUBDNS ; then + echo $dom : MISMATCH + echo -- system resolver : + cat "$DNSCHECK_DIG1" + echo -- $PUBDNS : + cat "$DNSCHECK_DIG2" + check_dns_cleanup + echo -- POSSIBLE DNS HIJACK DETECTED. ZAPRET WILL NOT HELP YOU IN CASE DNS IS SPOOFED !!! + echo -- DNS CHANGE OR DNSCRYPT MAY BE REQUIRED + return 1 + else + echo $dom : OK + cat "$DNSCHECK_DIG1" >>"$DNSCHECK_DIGS" + fi + done + else + echo no working public DNS was found. looks like public DNS blocked. + for dom in $DNSCHECK_DOM; do echo $dom; done | "$EXEDIR/mdig/mdig" --family=4 >"$DNSCHECK_DIGS" + fi + + echo checking resolved IP uniqueness for : $DNSCHECK_DOM + echo censor\'s DNS can return equal result for multiple blocked domains. + C1=$(wc -l <"$DNSCHECK_DIGS") + C2=$(sort -u "$DNSCHECK_DIGS" | wc -l) + [ "$C1" = "$C2" ] || + { + echo system dns resolver has returned equal IPs for some domains checked above \($C1 total, $C2 unique\) + echo non-unique IPs : + sort "$DNSCHECK_DIGS" | uniq -d + echo -- POSSIBLE DNS HIJACK DETECTED. ZAPRET WILL NOT HELP YOU IN CASE DNS IS SPOOFED !!! + echo -- DNSCRYPT MAY BE REQUIRED + check_dns_cleanup + return 1 + } + echo all resolved IPs are unique + echo -- DNS looks good + echo -- NOTE this check is Russia targeted. In your country other domains can be blocked. + check_dns_cleanup + return 0 +} + +install_systemd() +{ + INIT_SCRIPT_SRC="$EXEDIR/init.d/sysv/zapret" + + check_bins + require_root + check_location copy_all + check_prerequisites_linux + service_stop_systemd + install_binaries + check_dns + select_ipv6 + ask_config_desktop + ask_config + service_install_systemd + download_list + # in case its left from old version of zapret + crontab_del_quiet + # now we use systemd timers + timer_install_systemd + service_start_systemd +} + + + + +check_kmod() +{ + [ -f "/lib/modules/$(uname -r)/$1.ko" ] +} +check_package_exists_openwrt() +{ + [ -n "$(opkg list $1)" ] +} +check_package_openwrt() +{ + [ -n "$(opkg list-installed $1)" ] +} +check_packages_openwrt() +{ + for pkg in $@; do + check_package_openwrt $pkg || return + done +} + +is_linked_to_busybox() +{ + local F P + F=/usr/bin/$1 + P="$(readlink $F)" + if [ -z "$P" ] && [ -x $F ] && [ ! -L $F ]; then return 1; fi + [ "${P%busybox*}" != "$P" ] && return + F=/bin/$1 + P="$(readlink $F)" + if [ -z "$P" ] && [ -x $F ] && [ ! -L $F ]; then return 1; fi + [ "${P%busybox*}" != "$P" ] +} + +check_prerequisites_openwrt() +{ + echo \* checking prerequisites + + local PKGS="iptables-mod-extra iptables-mod-nfqueue iptables-mod-filter iptables-mod-ipopt iptables-mod-conntrack-extra ipset curl" + [ "$DISABLE_IPV6" != "1" ] && PKGS="$PKGS ip6tables-mod-nat" + local UPD=0 + + if check_packages_openwrt $PKGS ; then + echo everything is present + else + echo \* installing prerequisites + + opkg update + UPD=1 + opkg install $PKGS || { + echo could not install prerequisites + exitp 6 + } + fi + + is_linked_to_busybox gzip && { + echo + echo your system uses default busybox gzip. its several times slower than GNU gzip. + echo ip/host list scripts will run much faster with GNU gzip + echo installer can install GNU gzip but it requires about 100 Kb space + if ask_yes_no N "do you want to install GNU gzip"; then + [ "$UPD" = "0" ] && { + opkg update + UPD=1 + } + opkg install --force-overwrite gzip + fi + } + is_linked_to_busybox sort && { + echo + echo your system uses default busybox sort. its much slower and consumes much more RAM than GNU sort + echo ip/host list scripts will run much faster with GNU sort + echo installer can install GNU sort but it requires about 100 Kb space + if ask_yes_no N "do you want to install GNU sort"; then + [ "$UPD" = "0" ] && { + opkg update + UPD=1 + } + opkg install --force-overwrite coreutils-sort + fi + } + is_linked_to_busybox grep && { + echo + echo your system uses default busybox grep. its damn infinite slow with -f option + echo get_combined.sh will be severely impacted + echo installer can install GNU grep but it requires about 0.5 Mb space + if ask_yes_no N "do you want to install GNU grep"; then + [ "$UPD" = "0" ] && { + opkg update + UPD=1 + } + opkg install --force-overwrite grep + + # someone reported device partially fail if /bin/grep is absent + # grep package deletes /bin/grep + [ -f /bin/grep ] || ln -s busybox /bin/grep + fi + } +} + +openwrt_fw_section_find() +{ + # $1 - fw include postfix + # echoes section number + + i=0 + while true + do + path=$(uci -q get firewall.@include[$i].path) + [ -n "$path" ] || break + [ "$path" = "$OPENWRT_FW_INCLUDE$1" ] && { + echo $i + return 0 + } + i=$(($i+1)) + done + return 1 +} +openwrt_fw_section_del() +{ + # $1 - fw include postfix + + local id=$(openwrt_fw_section_find $1) + [ -n "$id" ] && { + uci delete firewall.@include[$id] && uci commit firewall + rm -f "$OPENWRT_FW_INCLUDE$1" + } +} +openwrt_fw_section_add() +{ + openwrt_fw_section_find || + { + uci add firewall include >/dev/null || return + echo -1 + } +} +openwrt_fw_section_configure() +{ + local id=$(openwrt_fw_section_add $1) + [ -z "$id" ] || + ! uci set firewall.@include[$id].path="$OPENWRT_FW_INCLUDE" || + ! uci set firewall.@include[$id].reload="1" || + ! uci commit firewall && + { + echo could not add firewall include + exitp 50 + } +} + +install_openwrt_firewall() +{ + echo \* installing firewall script $1 + + [ -n "MODE" ] || { + echo should specify MODE in $ZAPRET_CONFIG + exitp 7 + } + + echo "linking : $FW_SCRIPT_SRC => $OPENWRT_FW_INCLUDE" + ln -fs "$FW_SCRIPT_SRC" "$OPENWRT_FW_INCLUDE" + + openwrt_fw_section_configure $1 +} + + +restart_openwrt_firewall() +{ + echo \* restarting firewall + + fw3 -q restart || { + echo could not restart firewall + exitp 30 + } +} + +remove_openwrt_firewall() +{ + echo \* removing firewall script + + openwrt_fw_section_del + # from old zapret versions. now we use single include + openwrt_fw_section_del 6 + + # free some RAM + "$IPSET_DIR/create_ipset.sh" clear +} + +install_openwrt_iface_hook() +{ + echo \* installing ifup hook + + ln -fs "$OPENWRT_IFACE_HOOK" /etc/hotplug.d/iface +} + +is_flow_offload_avail() +{ + # $1 = '' for ipv4, '6' for ipv6 + grep -q FLOWOFFLOAD /proc/net/ip$1_tables_targets +} + +deoffload_openwrt_firewall() +{ + echo \* checking flow offloading + + is_flow_offload_avail || { + echo unavailable + return + } + + local fo=$(uci -q get firewall.@defaults[0].flow_offloading) + + if [ "$fo" = "1" ] ; then + local mod=0 + $ECHON "system wide flow offloading detected. " + case $FLOWOFFLOAD in + donttouch) + if [ "$MODE" = "nfqws" ]; then + echo its incompatible with nfqws tcp data tampering. disabling + uci set firewall.@defaults[0].flow_offloading=0 + mod=1 + else + if [ "$MODE" = "custom" ] ; then + echo custom mode selected !!! only you can decide whether flow offloading is compatible + else + echo its compatible with selected options. not disabling + fi + fi + ;; + *) + echo zapret will disable system wide offloading setting and add selective rules if required + uci set firewall.@defaults[0].flow_offloading=0 + mod=1 + esac + [ "$mod" = "1" ] && uci commit firewall + else + echo system wide software flow offloading disabled. ok + fi + +} + +install_sysv_init() +{ + # $1 - "0"=disable + echo \* installing init script + + [ -x "$INIT_SCRIPT" ] && { + "$INIT_SCRIPT" stop + "$INIT_SCRIPT" disable + } + ln -fs "$INIT_SCRIPT_SRC" "$INIT_SCRIPT" + [ "$1" != "0" ] && "$INIT_SCRIPT" enable +} + +service_start_sysv() +{ + echo \* starting zapret service + + "$INIT_SCRIPT" start || { + echo could not start zapret service + exitp 30 + } +} + + + +install_openwrt() +{ + INIT_SCRIPT_SRC="$EXEDIR/init.d/openwrt/zapret" + FW_SCRIPT_SRC="$EXEDIR/init.d/openwrt/firewall.zapret" + OPENWRT_FW_INCLUDE=/etc/firewall.zapret + OPENWRT_IFACE_HOOK="$EXEDIR/init.d/openwrt/90-zapret" + + check_bins + require_root + check_location copy_openwrt + install_binaries + check_dns + select_ipv6 + check_prerequisites_openwrt + ask_config + ask_config_tmpdir + ask_config_offload + install_sysv_init + # can be previous firewall preventing access + remove_openwrt_firewall + restart_openwrt_firewall + download_list + crontab_del_quiet + # router system : works 24/7. night is the best time + crontab_add 0 6 + cron_ensure_running + service_start_sysv + install_openwrt_iface_hook + install_openwrt_firewall + deoffload_openwrt_firewall + restart_openwrt_firewall +} + + + +remove_macos_firewall() +{ + echo \* removing zapret PF hooks + + pf_anchors_clear + pf_anchor_root_del + pf_anchor_root_reload +} +service_install_macos() +{ + echo \* installing zapret service + + ln -fs /opt/zapret/init.d/macos/zapret.plist /Library/LaunchDaemons +} +service_start_macos() +{ + echo \* starting zapret service + + ln -fs /opt/zapret/init.d/macos/zapret.plist /Library/LaunchDaemons + "$INIT_SCRIPT_SRC" start +} +macos_fw_reload_trigger_clear() +{ + LISTS_RELOAD= + write_config_var LISTS_RELOAD +} +macos_fw_reload_trigger_set() +{ + LISTS_RELOAD="$INIT_SCRIPT_SRC reload-fw-tables" + write_config_var LISTS_RELOAD +} + +install_macos() +{ + INIT_SCRIPT_SRC="$EXEDIR/init.d/macos/zapret" + + # compile before root + check_bins + require_root + check_location copy_all + install_binaries + check_dns + select_ipv6 + ask_config_desktop + ask_config + service_install_macos + remove_macos_firewall + macos_fw_reload_trigger_clear + # gzip lists are incompatible with PF + GZIP_LISTS=0 write_config_var GZIP_LISTS + download_list + macos_fw_reload_trigger_set + crontab_del_quiet + # desktop system. more likely up at daytime + crontab_add 10 22 + service_start_macos +} + + +# build binaries, do not use precompiled +[ "$1" = "make" ] && FORCE_BUILD=1 + +check_system + +[ "$SYSTEM" = "macos" ] && . "$EXEDIR/init.d/macos/functions" + +case $SYSTEM in + systemd) + install_systemd + ;; + openwrt) + install_openwrt + ;; + macos) + install_macos + ;; +esac + + +exitp 0 diff --git a/ip2net/Makefile b/ip2net/Makefile new file mode 100644 index 0000000..782954e --- /dev/null +++ b/ip2net/Makefile @@ -0,0 +1,17 @@ +CC ?= gcc +CFLAGS += -std=gnu99 -s -O3 +CFLAGS_BSD = -Wno-address-of-packed-member -Wno-logical-op-parentheses -Wno-switch +LIBS = +SRC_FILES = *.c + +all: ip2net +mac: bsd + +ip2net: $(SRC_FILES) + $(CC) $(CFLAGS) -o $@ $(SRC_FILES) $(LDFLAGS) $(LIBS) + +bsd: $(SRC_FILES) + $(CC) $(CFLAGS) $(CFLAGS_BSD) -o ip2net $(SRC_FILES) $(LDFLAGS) $(LIBS) + +clean: + rm -f ip2net *.o diff --git a/ip2net/ip2net b/ip2net/ip2net new file mode 120000 index 0000000..315335f --- /dev/null +++ b/ip2net/ip2net @@ -0,0 +1 @@ +../binaries/x86_64/ip2net \ No newline at end of file diff --git a/ip2net/ip2net.c b/ip2net/ip2net.c new file mode 100644 index 0000000..b24ec78 --- /dev/null +++ b/ip2net/ip2net.c @@ -0,0 +1,397 @@ +// group ipv4/ipv6 list from stdout into subnets +// each line must contain either ip or ip/bitcount +// valid ip/bitcount and ip1-ip2 are passed through without modification +// ips are groupped into subnets + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "qsort.h" + +#define ALLOC_STEP 16384 + +// minimum subnet fill percent is PCTMULT/PCTDIV (for example 3/4) +#define DEFAULT_PCTMULT 3 +#define DEFAULT_PCTDIV 4 +// subnet search range in "zero bit count" +// means search start from /(32-ZCT_MAX) to /(32-ZCT_MIN) +#define DEFAULT_V4_ZCT_MAX 10 // /22 +#define DEFAULT_V4_ZCT_MIN 2 // /30 +#define DEFAULT_V6_ZCT_MAX 72 // /56 +#define DEFAULT_V6_ZCT_MIN 64 // /64 +// must be no less than N ipv6 in subnet +#define DEFAULT_V6_THRESHOLD 5 + +static int ucmp(const void * a, const void * b, void *arg) +{ + if (*(uint32_t*)a < *(uint32_t*)b) + return -1; + else if (*(uint32_t*)a > *(uint32_t*)b) + return 1; + else + return 0; +} +static uint32_t mask_from_bitcount(uint32_t zct) +{ + return ~((1 << zct) - 1); +} +// make presorted array unique. return number of unique items. +// 1,1,2,3,3,0,0,0 (ct=8) => 1,2,3,0 (ct=4) +static uint32_t unique(uint32_t *pu, uint32_t ct) +{ + uint32_t i, j, u; + for (i = j = 0; j < ct; i++) + { + u = pu[j++]; + for (; j < ct && pu[j] == u; j++); + pu[i] = u; + } + return i; +} + + + +static int cmp6(const void * a, const void * b, void *arg) +{ + for (uint8_t i = 0; i < sizeof(((struct in6_addr *)0)->s6_addr); i++) + { + if (((struct in6_addr *)a)->s6_addr[i] < ((struct in6_addr *)b)->s6_addr[i]) + return -1; + else if (((struct in6_addr *)a)->s6_addr[i] > ((struct in6_addr *)b)->s6_addr[i]) + return 1; + } + return 0; +} +// make presorted array unique. return number of unique items. +static uint32_t unique6(struct in6_addr *pu, uint32_t ct) +{ + uint32_t i, j, k; + for (i = j = 0; j < ct; i++) + { + for (k = j++; j < ct && !memcmp(pu + j, pu + k, sizeof(struct in6_addr)); j++); + pu[i] = pu[k]; + } + return i; +} +static void mask_from_bitcount6(uint32_t zct, struct in6_addr *a) +{ + if (zct >= 128) + memset(a->s6_addr,0x00,16); + else + { + int32_t n = (127 - zct) >> 3; + memset(a->s6_addr,0xFF,n); + memset(a->s6_addr+n,0x00,16-n); + a->s6_addr[n] = ~((1 << (zct & 7)) - 1); + } +} +// result = a & b +static void ip6_and(const struct in6_addr *a, const struct in6_addr *b, struct in6_addr *result) +{ + ((uint64_t*)result->s6_addr)[0] = ((uint64_t*)a->s6_addr)[0] & ((uint64_t*)b->s6_addr)[0]; + ((uint64_t*)result->s6_addr)[1] = ((uint64_t*)a->s6_addr)[1] & ((uint64_t*)b->s6_addr)[1]; +} + +static void rtrim(char *s) +{ + if (s) + for (char *p = s + strlen(s) - 1; p >= s && (*p == '\n' || *p == '\r'); p--) *p = '\0'; +} + + +static struct params_s +{ + bool ipv6; + uint32_t pctmult, pctdiv; // for v4 + uint32_t zct_min, zct_max; // for v4 and v6 + uint32_t v6_threshold; // for v6 +} params; + + +static void exithelp() +{ + printf( + " -4\t\t\t\t; ipv4 list (default)\n" + " -6\t\t\t\t; ipv6 list\n" + " --prefix-length=min[-max]\t; consider prefix lengths from 'min' to 'max'. examples : 22-30 (ipv4), 56-64 (ipv6)\n" + " --v4-threshold=mul/div\t\t; ipv4 only : include subnets with more than mul/div ips. example : 3/4\n" + " --v6-threshold=N\t\t; ipv6 only : include subnets with more than N v6 ips. example : 5\n" + ); + exit(1); +} + +static void parse_params(int argc, char *argv[]) +{ + int option_index = 0; + int v, i; + uint32_t plen1=-1, plen2=-1; + + memset(¶ms, 0, sizeof(params)); + params.pctmult = DEFAULT_PCTMULT; + params.pctdiv = DEFAULT_PCTDIV; + params.v6_threshold = DEFAULT_V6_THRESHOLD; + + const struct option long_options[] = { + { "help",no_argument,0,0 },// optidx=0 + { "h",no_argument,0,0 },// optidx=1 + { "4",no_argument,0,0 },// optidx=2 + { "6",no_argument,0,0 },// optidx=3 + { "prefix-length",required_argument,0,0 },// optidx=4 + { "v4-threshold",required_argument,0,0 },// optidx=5 + { "v6-threshold",required_argument,0,0 },// optidx=6 + { NULL,0,NULL,0 } + }; + while ((v = getopt_long_only(argc, argv, "", long_options, &option_index)) != -1) + { + if (v) exithelp(); + switch (option_index) + { + case 0: + case 1: + exithelp(); + break; + case 2: + params.ipv6 = false; + break; + case 3: + params.ipv6 = true; + break; + case 4: + i = sscanf(optarg,"%u-%u",&plen1,&plen2); + if (i == 1) plen2 = plen1; + if (!i || plen2=params.pctdiv) + { + fprintf(stderr, "invalid parameter for v4-threshold : %s\n", optarg); + exit(1); + } + break; + case 6: + i = sscanf(optarg, "%u", ¶ms.v6_threshold); + if (i != 1 || params.v6_threshold<1) + { + fprintf(stderr, "invalid parameter for v6-threshold : %s\n", optarg); + exit(1); + } + break; + } + } + if (plen1 != -1 && (!params.ipv6 && (plen1>31 || plen2>31) || params.ipv6 && (plen1>127 || plen2>127))) + { + fprintf(stderr, "invalid parameter for prefix-length\n"); + exit(1); + } + params.zct_min = params.ipv6 ? plen2==-1 ? DEFAULT_V6_ZCT_MIN : 128-plen2 : plen2==-1 ? DEFAULT_V4_ZCT_MIN : 32-plen2; + params.zct_max = params.ipv6 ? plen1==-1 ? DEFAULT_V6_ZCT_MAX : 128-plen1 : plen1==-1 ? DEFAULT_V4_ZCT_MAX : 32-plen1; +} + + +int main(int argc, char **argv) +{ + char str[256],d; + uint32_t ipct = 0, iplist_size = 0, pos = 0, p, zct, ip_ct, pos_end; + + parse_params(argc, argv); + + if (params.ipv6) // ipv6 + { + char *s; + struct in6_addr a, *iplist = NULL, *iplist_new; + + while (fgets(str, sizeof(str), stdin)) + { + rtrim(str); + d = 0; + if ((s = strchr(str, '/')) || (s = strchr(str, '-'))) + { + d = *s; + *s = '\0'; + } + if (inet_pton(AF_INET6, str, &a)) + { + if (d=='/') + { + // we have subnet ip6/y + // output it as is + *s = d; + if (sscanf(s + 1, "%u", &zct) && zct!=128) + { + if (zct<128) printf("%s\n", str); + continue; + } + } + else if (d=='-') + { + *s = d; + if (inet_pton(AF_INET6, s+1, &a)) printf("%s\n", str); + continue; + } + if (ipct >= iplist_size) + { + iplist_size += ALLOC_STEP; + iplist_new = (struct in6_addr*)(iplist ? realloc(iplist, sizeof(*iplist)*iplist_size) : malloc(sizeof(*iplist)*iplist_size)); + if (!iplist_new) + { + free(iplist); + fprintf(stderr, "out of memory\n"); + return 100; + } + iplist = iplist_new; + } + iplist[ipct++] = a; + } + } + gnu_quicksort(iplist, ipct, sizeof(*iplist), cmp6, NULL); + ipct = unique6(iplist, ipct); + + /* + for(uint32_t i=0;i= params.zct_min; zct--) + { + mask_from_bitcount6(zct, &mask); + ip6_and(iplist + pos, &mask, &ip_start); + for (p = pos + 1, ip_ct = 1; p < ipct; p++, ip_ct++) + { + ip6_and(iplist + p, &mask, &ip); + if (memcmp(&ip_start, &ip, sizeof(ip))) + break; + } + if (ip_ct == 1) break; + if (ip_ct >= params.v6_threshold) + { + // network found. but is there smaller network with the same ip_ct ? dont do carpet bombing if possible, use smaller subnets + if (!ip_ct_best || ip_ct == ip_ct_best) + { + ip_ct_best = ip_ct; + zct_best = zct; + pos_end = p; + } + else + break; + } + } + if (!zct_best) ip_start = iplist[pos], pos_end = pos + 1; // network not found, use single ip + inet_ntop(AF_INET6, &ip_start, str, sizeof(str)); + printf(zct_best ? "%s/%u\n" : "%s\n", str, 128 - zct_best); + + pos = pos_end; + } + + free(iplist); + } + else // ipv4 + { + uint32_t u1,u2,u3,u4, u11,u22,u33,u44, ip; + uint32_t *iplist = NULL, *iplist_new, i; + + while (fgets(str, sizeof(str), stdin)) + { + if ((i = sscanf(str, "%u.%u.%u.%u-%u.%u.%u.%u", &u1, &u2, &u3, &u4, &u11, &u22, &u33, &u44)) >= 8 && + !(u1 & 0xFFFFFF00) && !(u2 & 0xFFFFFF00) && !(u3 & 0xFFFFFF00) && !(u4 & 0xFFFFFF00) && + !(u11 & 0xFFFFFF00) && !(u22 & 0xFFFFFF00) && !(u33 & 0xFFFFFF00) && !(u44 & 0xFFFFFF00)) + { + printf("%u.%u.%u.%u-%u.%u.%u.%u\n", u1, u2, u3, u4, u11, u22, u33, u44); + } + else + if ((i = sscanf(str, "%u.%u.%u.%u/%u", &u1, &u2, &u3, &u4, &zct)) >= 4 && + !(u1 & 0xFFFFFF00) && !(u2 & 0xFFFFFF00) && !(u3 & 0xFFFFFF00) && !(u4 & 0xFFFFFF00)) + { + if (i == 5 && zct != 32) + { + // we have subnet x.x.x.x/y + // output it as is if valid, ignore otherwise + if (zct < 32) + printf("%u.%u.%u.%u/%u\n", u1, u2, u3, u4, zct); + } + else + { + ip = u1 << 24 | u2 << 16 | u3 << 8 | u4; + if (ipct >= iplist_size) + { + iplist_size += ALLOC_STEP; + iplist_new = (uint32_t*)(iplist ? realloc(iplist, sizeof(*iplist)*iplist_size) : malloc(sizeof(*iplist)*iplist_size)); + if (!iplist_new) + { + free(iplist); + fprintf(stderr, "out of memory\n"); + return 100; + } + iplist = iplist_new; + } + iplist[ipct++] = ip; + } + } + } + + gnu_quicksort(iplist, ipct, sizeof(*iplist), ucmp, NULL); + ipct = unique(iplist, ipct); + + while (pos < ipct) + { + uint32_t mask, ip_start, ip_end, subnet_ct; + uint32_t ip_ct_best = 0, zct_best = 0; + + // find smallest network with maximum ip coverage with no less than mul/div percent addresses + for (zct = params.zct_max; zct >= params.zct_min; zct--) + { + mask = mask_from_bitcount(zct); + ip_start = iplist[pos] & mask; + subnet_ct = ~mask + 1; + if (iplist[pos] > (ip_start + subnet_ct*(params.pctdiv - params.pctmult) / params.pctdiv)) + continue; // ip is higher than (1-PCT). definitely coverage is not enough. skip searching + ip_end = ip_start | ~mask; + for (p=pos+1, ip_ct=1; p < ipct && iplist[p] <= ip_end; p++) ip_ct++; // count ips within subnet range + if (ip_ct == 1) break; + if (ip_ct >= (subnet_ct*params.pctmult / params.pctdiv)) + { + // network found. but is there smaller network with the same ip_ct ? dont do carpet bombing if possible, use smaller subnets + if (!ip_ct_best || ip_ct == ip_ct_best) + { + ip_ct_best = ip_ct; + zct_best = zct; + pos_end = p; + } + else + break; + } + } + if (!zct_best) ip_start = iplist[pos], pos_end = pos + 1; // network not found, use single ip + + u1 = ip_start >> 24; + u2 = (ip_start >> 16) & 0xFF; + u3 = (ip_start >> 8) & 0xFF; + u4 = ip_start & 0xFF; + printf(zct_best ? "%u.%u.%u.%u/%u\n" : "%u.%u.%u.%u\n", u1, u2, u3, u4, 32 - zct_best); + + pos = pos_end; + } + + free(iplist); + } + + return 0; +} diff --git a/ip2net/qsort.c b/ip2net/qsort.c new file mode 100644 index 0000000..2ee1185 --- /dev/null +++ b/ip2net/qsort.c @@ -0,0 +1,250 @@ +/* Copyright (C) 1991-2018 Free Software Foundation, Inc. + This file is part of the GNU C Library. + Written by Douglas C. Schmidt (schmidt@ics.uci.edu). + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +/* If you consider tuning this algorithm, you should consult first: + Engineering a sort function; Jon Bentley and M. Douglas McIlroy; + Software - Practice and Experience; Vol. 23 (11), 1249-1265, 1993. */ + +//#include +#include +#include +//#include +#include "qsort.h" + +/* Byte-wise swap two items of size SIZE. */ +#define SWAP(a, b, size) \ + do \ + { \ + size_t __size = (size); \ + char *__a = (a), *__b = (b); \ + do \ + { \ + char __tmp = *__a; \ + *__a++ = *__b; \ + *__b++ = __tmp; \ + } while (--__size > 0); \ + } while (0) + +/* Discontinue quicksort algorithm when partition gets below this size. + This particular magic number was chosen to work best on a Sun 4/260. */ +#define MAX_THRESH 4 + +/* Stack node declarations used to store unfulfilled partition obligations. */ +typedef struct + { + char *lo; + char *hi; + } stack_node; + +/* The next 4 #defines implement a very fast in-line stack abstraction. */ +/* The stack needs log (total_elements) entries (we could even subtract + log(MAX_THRESH)). Since total_elements has type size_t, we get as + upper bound for log (total_elements): + bits per byte (CHAR_BIT) * sizeof(size_t). */ +#define STACK_SIZE (CHAR_BIT * sizeof(size_t)) +#define PUSH(low, high) ((void) ((top->lo = (low)), (top->hi = (high)), ++top)) +#define POP(low, high) ((void) (--top, (low = top->lo), (high = top->hi))) +#define STACK_NOT_EMPTY (stack < top) + + +/* Order size using quicksort. This implementation incorporates + four optimizations discussed in Sedgewick: + + 1. Non-recursive, using an explicit stack of pointer that store the + next array partition to sort. To save time, this maximum amount + of space required to store an array of SIZE_MAX is allocated on the + stack. Assuming a 32-bit (64 bit) integer for size_t, this needs + only 32 * sizeof(stack_node) == 256 bytes (for 64 bit: 1024 bytes). + Pretty cheap, actually. + + 2. Chose the pivot element using a median-of-three decision tree. + This reduces the probability of selecting a bad pivot value and + eliminates certain extraneous comparisons. + + 3. Only quicksorts TOTAL_ELEMS / MAX_THRESH partitions, leaving + insertion sort to order the MAX_THRESH items within each partition. + This is a big win, since insertion sort is faster for small, mostly + sorted array segments. + + 4. The larger of the two sub-partitions is always pushed onto the + stack first, with the algorithm then concentrating on the + smaller partition. This *guarantees* no more than log (total_elems) + stack size is needed (actually O(1) in this case)! */ + +void +gnu_quicksort (void *const pbase, size_t total_elems, size_t size, + __gnu_compar_d_fn_t cmp, void *arg) +{ + char *base_ptr = (char *) pbase; + + const size_t max_thresh = MAX_THRESH * size; + + if (total_elems == 0) + /* Avoid lossage with unsigned arithmetic below. */ + return; + + if (total_elems > MAX_THRESH) + { + char *lo = base_ptr; + char *hi = &lo[size * (total_elems - 1)]; + stack_node stack[STACK_SIZE]; + stack_node *top = stack; + + PUSH (NULL, NULL); + + while (STACK_NOT_EMPTY) + { + char *left_ptr; + char *right_ptr; + + /* Select median value from among LO, MID, and HI. Rearrange + LO and HI so the three values are sorted. This lowers the + probability of picking a pathological pivot value and + skips a comparison for both the LEFT_PTR and RIGHT_PTR in + the while loops. */ + + char *mid = lo + size * ((hi - lo) / size >> 1); + + if ((*cmp) ((void *) mid, (void *) lo, arg) < 0) + SWAP (mid, lo, size); + if ((*cmp) ((void *) hi, (void *) mid, arg) < 0) + SWAP (mid, hi, size); + else + goto jump_over; + if ((*cmp) ((void *) mid, (void *) lo, arg) < 0) + SWAP (mid, lo, size); + jump_over:; + + left_ptr = lo + size; + right_ptr = hi - size; + + /* Here's the famous ``collapse the walls'' section of quicksort. + Gotta like those tight inner loops! They are the main reason + that this algorithm runs much faster than others. */ + do + { + while ((*cmp) ((void *) left_ptr, (void *) mid, arg) < 0) + left_ptr += size; + + while ((*cmp) ((void *) mid, (void *) right_ptr, arg) < 0) + right_ptr -= size; + + if (left_ptr < right_ptr) + { + SWAP (left_ptr, right_ptr, size); + if (mid == left_ptr) + mid = right_ptr; + else if (mid == right_ptr) + mid = left_ptr; + left_ptr += size; + right_ptr -= size; + } + else if (left_ptr == right_ptr) + { + left_ptr += size; + right_ptr -= size; + break; + } + } + while (left_ptr <= right_ptr); + + /* Set up pointers for next iteration. First determine whether + left and right partitions are below the threshold size. If so, + ignore one or both. Otherwise, push the larger partition's + bounds on the stack and continue sorting the smaller one. */ + + if ((size_t) (right_ptr - lo) <= max_thresh) + { + if ((size_t) (hi - left_ptr) <= max_thresh) + /* Ignore both small partitions. */ + POP (lo, hi); + else + /* Ignore small left partition. */ + lo = left_ptr; + } + else if ((size_t) (hi - left_ptr) <= max_thresh) + /* Ignore small right partition. */ + hi = right_ptr; + else if ((right_ptr - lo) > (hi - left_ptr)) + { + /* Push larger left partition indices. */ + PUSH (lo, right_ptr); + lo = left_ptr; + } + else + { + /* Push larger right partition indices. */ + PUSH (left_ptr, hi); + hi = right_ptr; + } + } + } + + /* Once the BASE_PTR array is partially sorted by quicksort the rest + is completely sorted using insertion sort, since this is efficient + for partitions below MAX_THRESH size. BASE_PTR points to the beginning + of the array to sort, and END_PTR points at the very last element in + the array (*not* one beyond it!). */ + +#define min(x, y) ((x) < (y) ? (x) : (y)) + + { + char *const end_ptr = &base_ptr[size * (total_elems - 1)]; + char *tmp_ptr = base_ptr; + char *thresh = min(end_ptr, base_ptr + max_thresh); + char *run_ptr; + + /* Find smallest element in first threshold and place it at the + array's beginning. This is the smallest array element, + and the operation speeds up insertion sort's inner loop. */ + + for (run_ptr = tmp_ptr + size; run_ptr <= thresh; run_ptr += size) + if ((*cmp) ((void *) run_ptr, (void *) tmp_ptr, arg) < 0) + tmp_ptr = run_ptr; + + if (tmp_ptr != base_ptr) + SWAP (tmp_ptr, base_ptr, size); + + /* Insertion sort, running from left-hand-side up to right-hand-side. */ + + run_ptr = base_ptr + size; + while ((run_ptr += size) <= end_ptr) + { + tmp_ptr = run_ptr - size; + while ((*cmp) ((void *) run_ptr, (void *) tmp_ptr, arg) < 0) + tmp_ptr -= size; + + tmp_ptr += size; + if (tmp_ptr != run_ptr) + { + char *trav; + + trav = run_ptr + size; + while (--trav >= run_ptr) + { + char c = *trav; + char *hi, *lo; + + for (hi = lo = trav; (lo -= size) >= tmp_ptr; hi = lo) + *hi = *lo; + *hi = c; + } + } + } + } +} diff --git a/ip2net/qsort.h b/ip2net/qsort.h new file mode 100644 index 0000000..f537ab7 --- /dev/null +++ b/ip2net/qsort.h @@ -0,0 +1,6 @@ +#pragma once + +// GNU qsort is 2x faster than musl + +typedef int (*__gnu_compar_d_fn_t) (const void *, const void *, void *); +void gnu_quicksort (void *const pbase, size_t total_elems, size_t size, __gnu_compar_d_fn_t cmp, void *arg); diff --git a/ipset/antifilter.helper b/ipset/antifilter.helper new file mode 100644 index 0000000..aec3cae --- /dev/null +++ b/ipset/antifilter.helper @@ -0,0 +1,19 @@ +get_antifilter() +{ + # $1 - list url + # $2 - target file + local ZIPLISTTMP="$TMPDIR/zapret-ip.txt" + + [ "$DISABLE_IPV4" != "1" ] && { + curl --fail --max-time 150 --connect-timeout 20 --max-filesize 41943040 -k -L "$1" | cut_local >"$ZIPLISTTMP" && + { + dlsize=$(LANG=C wc -c "$ZIPLISTTMP" | xargs | cut -f 1 -d ' ') + if [ $dlsize -lt 204800 ]; then + echo list file is too small. can be bad. + exit 2 + fi + ip2net4 <"$ZIPLISTTMP" | zz "$2" + rm -f "$ZIPLISTTMP" + } + } +} diff --git a/ipset/clear_lists.sh b/ipset/clear_lists.sh new file mode 100755 index 0000000..80c1531 --- /dev/null +++ b/ipset/clear_lists.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +rm -f "$ZIPLIST"* "$ZIPLIST6"* "$ZIPLIST_USER" "$ZIPLIST_USER6" "$ZIPLIST_IPBAN"* "$ZIPLIST_IPBAN6"* "$ZIPLIST_USER_IPBAN" "$ZIPLIST_USER_IPBAN6" "$ZIPLIST_EXCLUDE" "$ZIPLIST_EXCLUDE6" "$ZHOSTLIST"* diff --git a/ipset/create_ipset.sh b/ipset/create_ipset.sh new file mode 100755 index 0000000..83701d6 --- /dev/null +++ b/ipset/create_ipset.sh @@ -0,0 +1,201 @@ +#!/bin/sh + +# create ipset or ipfw table from resolved ip's +# $1=no-update - do not update ipset, only create if its absent + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +IPSET_CMD="$TMPDIR/ipset_cmd.txt" +IPSET_SAVERAM_CHUNK_SIZE=20000 +IPSET_SAVERAM_MIN_FILESIZE=131072 + + +while [ -n "$1" ]; do + [ "$1" = "no-update" ] && NO_UPDATE=1 + [ "$1" = "clear" ] && DO_CLEAR=1 + shift +done + + +file_extract_lines() +{ + # $1 - filename + # $2 - from line (starting with 0) + # $3 - line count + # awk "{ err=1 } NR < $(($2+1)) { next } { print; err=0 } NR == $(($2+$3)) { exit err } END {exit err}" "$1" + awk "NR < $(($2+1)) { next } { print } NR == $(($2+$3)) { exit }" "$1" +} +ipset_restore_chunked() +{ + # $1 - filename + # $2 - chunk size + local pos lines + [ -f "$1" ] || return + lines=$(wc -l <"$1") + pos=$lines + while [ "$pos" -gt "0" ]; do + pos=$((pos-$2)) + [ "$pos" -lt "0" ] && pos=0 + file_extract_lines "$1" $pos $2 | ipset -! restore + sed -i "$(($pos+1)),$ d" "$1" + done +} + + +ipset_get_script() +{ + # $1 - filename + # $2 - ipset name + zzcat "$1" | sort -u | sed -nEe "s/^.+$/add $2 &/p" +} + +ipset_restore() +{ + # $1 - filename + # $2 - ipset name + # $3 - "6" = ipv6 + zzexist "$1" || return + local fsize=$(zzsize "$1") + local svram=0 + # do not saveram small files. file can also be gzipped + [ "$SAVERAM" = "1" ] && [ "$fsize" -ge "$IPSET_SAVERAM_MIN_FILESIZE" ] && svram=1 + + local T="Adding to ipset $2 ($IPSTYPE" + [ "$svram" = "1" ] && T="$T, saveram" + T="$T) : $f" + echo $T + + if [ "$svram" = "1" ]; then + ipset_get_script "$1" "$2" >"$IPSET_CMD" + ipset_restore_chunked "$IPSET_CMD" $IPSET_SAVERAM_CHUNK_SIZE + rm -f "$IPSET_CMD" + else + ipset_get_script "$1" "$2" | ipset -! restore + fi +} + +create_ipset() +{ + if [ "$1" -eq "6" ]; then + FAMILY=inet6 + else + FAMILY=inet + fi + ipset create $2 $3 $4 family $FAMILY 2>/dev/null || { + [ "$NO_UPDATE" = "1" ] && return + } + ipset flush $2 + [ "$DO_CLEAR" = "1" ] || { + for f in "$5" "$6" ; do + ipset_restore "$f" "$2" $1 + done + } + return 0 +} + + +add_ipfw_table() +{ + # $1 - table name + sed -nEe "s/^.+$/table $1 add &/p" | ipfw -q /dev/stdin +} +populate_ipfw_table() +{ + # $1 - table name + # $2 - ip list file + zzexist "$2" || return + zzcat "$2" | sort -u | add_ipfw_table $1 +} +create_ipfw_table() +{ + # $1 - table name + # $2 - table options + # $3,$4, ... - ip list files. can be v4,v6 or mixed + + local name=$1 + ipfw table "$name" create $2 2>/dev/null || { + [ "$NO_UPDATE" = "1" ] && return + } + ipfw -q table $1 flush + shift + shift + [ "$DO_CLEAR" = "1" ] || { + while [ -n "$1" ]; do + populate_ipfw_table $name "$1" + shift + done + } +} + +print_reloading_backend() +{ + # $1 - backend name + local s="reloading $1 backend" + if [ "$NO_UPDATE" = 1 ]; then + s="$s (no-update)" + else + s="$s (forced-update)" + fi + echo $s +} + + +oom_adjust_high + +if [ -n "$LISTS_RELOAD" ] ; then + if [ "$LISTS_RELOAD" = "-" ] ; then + echo not reloading ip list backend + true + else + echo executing custom ip list reload command : $LISTS_RELOAD + $LISTS_RELOAD + fi +elif exists ipset; then + # ipset seem to buffer the whole script to memory + # on low RAM system this can cause oom errors + # in SAVERAM mode we feed script lines in portions starting from the end, while truncating source file to free /tmp space + # only /tmp is considered tmpfs. other locations mean tmpdir was redirected to a disk + SAVERAM=0 + [ "$TMPDIR" = "/tmp" ] && { + RAMSIZE=$($GREP MemTotal /proc/meminfo | awk '{print $2}') + [ "$RAMSIZE" -lt "110000" ] && SAVERAM=1 + } + print_reloading_backend ipset + [ "$DISABLE_IPV4" != "1" ] && { + create_ipset 4 $ZIPSET hash:net "$IPSET_OPT" "$ZIPLIST" "$ZIPLIST_USER" + create_ipset 4 $ZIPSET_IPBAN hash:net "$IPSET_OPT" "$ZIPLIST_IPBAN" "$ZIPLIST_USER_IPBAN" + create_ipset 4 $ZIPSET_EXCLUDE hash:net "$IPSET_OPT_EXCLUDE" "$ZIPLIST_EXCLUDE" + } + [ "$DISABLE_IPV6" != "1" ] && { + create_ipset 6 $ZIPSET6 hash:net "$IPSET_OPT" "$ZIPLIST6" "$ZIPLIST_USER6" + create_ipset 6 $ZIPSET_IPBAN6 hash:net "$IPSET_OPT" "$ZIPLIST_IPBAN6" "$ZIPLIST_USER_IPBAN6" + create_ipset 6 $ZIPSET_EXCLUDE6 hash:net "$IPSET_OPT_EXCLUDE" "$ZIPLIST_EXCLUDE6" + } + true +elif exists ipfw; then + print_reloading_backend "ipfw table" + if [ "$DISABLE_IPV4" != "1" ] && [ "$DISABLE_IPV6" != "1" ]; then + create_ipfw_table $ZIPSET "$IPFW_TABLE_OPT" "$ZIPLIST" "$ZIPLIST_USER" "$ZIPLIST6" "$ZIPLIST_USER6" + create_ipfw_table $ZIPSET_IPBAN "$IPFW_TABLE_OPT" "$ZIPLIST_IPBAN" "$ZIPLIST_USER_IPBAN" "$ZIPLIST_IPBAN6" "$ZIPLIST_USER_IPBAN6" + create_ipfw_table $ZIPSET_EXCLUDE "$IPFW_TABLE_OPT_EXCLUDE" "$ZIPLIST_EXCLUDE" "$ZIPLIST_EXCLUDE6" + elif [ "$DISABLE_IPV4" != "1" ]; then + create_ipfw_table $ZIPSET "$IPFW_TABLE_OPT" "$ZIPLIST" "$ZIPLIST_USER" + create_ipfw_table $ZIPSET_IPBAN "$IPFW_TABLE_OPT" "$ZIPLIST_IPBAN" "$ZIPLIST_USER_IPBAN" + create_ipfw_table $ZIPSET_EXCLUDE "$IPFW_TABLE_OPT_EXCLUDE" "$ZIPLIST_EXCLUDE" + elif [ "$DISABLE_IPV6" != "1" ]; then + create_ipfw_table $ZIPSET "$IPFW_TABLE_OPT" "$ZIPLIST6" "$ZIPLIST_USER6" + create_ipfw_table $ZIPSET_IPBAN "$IPFW_TABLE_OPT" "$ZIPLIST_IPBAN6" "$ZIPLIST_USER_IPBAN6" + create_ipfw_table $ZIPSET_EXCLUDE "$IPFW_TABLE_OPT_EXCLUDE" "$ZIPLIST_EXCLUDE6" + else + create_ipfw_table $ZIPSET "$IPFW_TABLE_OPT" + create_ipfw_table $ZIPSET_IPBAN "$IPFW_TABLE_OPT" + create_ipfw_table $ZIPSET_EXCLUDE "$IPFW_TABLE_OPT_EXCLUDE" + fi + true +else + echo no supported ip list backend found + true +fi diff --git a/ipset/def.sh b/ipset/def.sh new file mode 100644 index 0000000..a9240f6 --- /dev/null +++ b/ipset/def.sh @@ -0,0 +1,186 @@ +. "$IPSET_DIR/../config" + +[ -z "$TMPDIR" ] && TMPDIR=/tmp +[ -z "$GZIP_LISTS" ] && GZIP_LISTS=1 + +[ -z "$IPSET_OPT" ] && IPSET_OPT="hashsize 262144 maxelem 2097152" +[ -z "$IPSET_OPT_EXCLUDE" ] && IPSET_OPT_EXCLUDE="hashsize 1024 maxelem 65536" + +[ -z "$IPFW_TABLE_OPT" ] && IPFW_TABLE_OPT="algo addr:radix" +[ -z "$IPFW_TABLE_OPT_EXCLUDE" ] && IPFW_TABLE_OPT_EXCLUDE="algo addr:radix" + + +ZIPSET=zapret +ZIPSET6=zapret6 +ZIPSET_EXCLUDE=nozapret +ZIPSET_EXCLUDE6=nozapret6 +ZIPLIST="$IPSET_DIR/zapret-ip.txt" +ZIPLIST6="$IPSET_DIR/zapret-ip6.txt" +ZIPLIST_EXCLUDE="$IPSET_DIR/zapret-ip-exclude.txt" +ZIPLIST_EXCLUDE6="$IPSET_DIR/zapret-ip-exclude6.txt" +ZIPLIST_USER="$IPSET_DIR/zapret-ip-user.txt" +ZIPLIST_USER6="$IPSET_DIR/zapret-ip-user6.txt" +ZUSERLIST="$IPSET_DIR/zapret-hosts-user.txt" +ZHOSTLIST="$IPSET_DIR/zapret-hosts.txt" + +ZIPSET_IPBAN=ipban +ZIPSET_IPBAN6=ipban6 +ZIPLIST_IPBAN="$IPSET_DIR/zapret-ip-ipban.txt" +ZIPLIST_IPBAN6="$IPSET_DIR/zapret-ip-ipban6.txt" +ZIPLIST_USER_IPBAN="$IPSET_DIR/zapret-ip-user-ipban.txt" +ZIPLIST_USER_IPBAN6="$IPSET_DIR/zapret-ip-user-ipban6.txt" +ZUSERLIST_IPBAN="$IPSET_DIR/zapret-hosts-user-ipban.txt" +ZUSERLIST_EXCLUDE="$IPSET_DIR/zapret-hosts-user-exclude.txt" + + +IP2NET="$IPSET_DIR/../ip2net/ip2net" +MDIG="$IPSET_DIR/../mdig/mdig" +[ -z "$MDIG_THREADS" ] && MDIG_THREADS=30 + + +exists() +{ + which "$1" >/dev/null 2>/dev/null +} + +# BSD grep is damn slow with -f option. prefer GNU grep (ggrep) if present +# MacoS in cron does not include /usr/local/bin to PATH +if [ -x /usr/local/bin/ggrep ] ; then + GREP=/usr/local/bin/ggrep +elif exists ggrep; then + GREP=$(which ggrep) +else + GREP=$(which grep) +fi + +grep_supports_b() +{ + # \b does not work with BSD grep + $GREP --version 2>&1 | $GREP -qE "BusyBox|GNU" +} +get_ip_regex() +{ + REG_IPV4='((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/([0-9]|[12][0-9]|3[012]))?' + REG_IPV6='[0-9a-fA-F]{1,4}:([0-9a-fA-F]{1,4}|:)+(/([0-9][0-9]?|1[01][0-9]|12[0-8]))?' + # good but too slow + # REG_IPV6='([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}(/[0-9]+)?|([0-9a-fA-F]{1,4}:){1,7}:(/[0-9]+)?|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}(/[0-9]+)?|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}(/[0-9]+)?|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}(/[0-9]+)?|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}(/[0-9]+)?|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}(/[0-9]+)?|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})(/[0-9]+)?|:((:[0-9a-fA-F]{1,4}){1,7}|:)(/([0-9][0-9]?|1[01][0-9]|12[0-8]))?' + grep_supports_b && { + REG_IPV4="\b$REG_IPV4\b" + REG_IPV6="\b$REG_IPV6\b" + } +} + + +ip2net4() +{ + if [ -x "$IP2NET" ]; then + "$IP2NET" -4 $IP2NET_OPT4 + else + sort -u + fi +} +ip2net6() +{ + if [ -x "$IP2NET" ]; then + "$IP2NET" -6 $IP2NET_OPT6 + else + sort -u + fi +} + +zzexist() +{ + [ -f "$1.gz" ] || [ -f "$1" ] +} +zzcat() +{ + if [ -f "$1.gz" ]; then + gunzip -c "$1.gz" + else + cat "$1" + fi +} +zz() +{ + if [ "$GZIP_LISTS" = "1" ]; then + gzip -c >"$1.gz" + rm -f "$1" + else + cat >"$1" + rm -f "$1.gz" + fi +} +zzsize() +{ + local f="$1" + [ -f "$1.gz" ] && f="$1.gz" + wc -c <"$f" | xargs +} + +digger() +{ + # $1 - hostlist + # $2 - family (4|6) + >&2 echo digging $(wc -l <"$1" | xargs) ipv$2 domains : "$1" + + if [ -x "$MDIG" ]; then + zzcat "$1" | "$MDIG" --family=$2 --threads=$MDIG_THREADS --stats=1000 + else + local A=A + [ "$2" = "6" ] && A=AAAA + zzcat "$1" | dig $A +short +time=8 +tries=2 -f - | $GREP -E '^[^;].*[^\.]$' + fi +} + +cut_local() +{ + $GREP -vE '^192\.168\.|^127\.|^10\.' +} +cut_local6() +{ + $GREP -vE '^::|^fc..:|^fd..:' +} + +oom_adjust_high() +{ + [ -f /proc/$$/oom_score_adj ] && { + echo setting high oom kill priority + echo -n 100 >/proc/$$/oom_score_adj + } +} + +getexclude() +{ + oom_adjust_high + + [ -f "$ZUSERLIST_EXCLUDE" ] && { + [ "$DISABLE_IPV4" != "1" ] && digger "$ZUSERLIST_EXCLUDE" 4 | sort -u > "$ZIPLIST_EXCLUDE" + [ "$DISABLE_IPV6" != "1" ] && digger "$ZUSERLIST_EXCLUDE" 6 | sort -u > "$ZIPLIST_EXCLUDE6" + } +} + +getuser() +{ + getexclude + [ -f "$ZUSERLIST" ] && { + [ "$DISABLE_IPV4" != "1" ] && digger "$ZUSERLIST" 4 | cut_local | sort -u > "$ZIPLIST_USER" + [ "$DISABLE_IPV6" != "1" ] && digger "$ZUSERLIST" 6 | cut_local6 | sort -u > "$ZIPLIST_USER6" + } + [ -f "$ZUSERLIST_IPBAN" ] && { + [ "$DISABLE_IPV4" != "1" ] && digger "$ZUSERLIST_IPBAN" 4 | cut_local | sort -u > "$ZIPLIST_USER_IPBAN" + [ "$DISABLE_IPV6" != "1" ] && digger "$ZUSERLIST_IPBAN" 6 | cut_local6 | sort -u > "$ZIPLIST_USER_IPBAN6" + } +} + +hup_zapret_daemons() +{ + echo forcing zapret daemons to reload their hostlist + if exists killall; then + kcmd=killall + killall -HUP tpws nfqws dvtws 2>/dev/null + elif exists pkill; then + pkill -HUP ^tpws$ ^nfqws$ ^dvtws$ + else + echo no mass killer available ! cant HUP zapret daemons + fi +} diff --git a/ipset/get_antifilter_ip.sh b/ipset/get_antifilter_ip.sh new file mode 100755 index 0000000..1628890 --- /dev/null +++ b/ipset/get_antifilter_ip.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +getuser + +. "$IPSET_DIR/antifilter.helper" + +get_antifilter https://antifilter.network/download/ip.lst "$ZIPLIST" + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_antifilter_ipsmart.sh b/ipset/get_antifilter_ipsmart.sh new file mode 100755 index 0000000..b71298f --- /dev/null +++ b/ipset/get_antifilter_ipsmart.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +getuser + +. "$IPSET_DIR/antifilter.helper" + +get_antifilter https://antifilter.network/download/ipsmart.lst "$ZIPLIST" + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_antifilter_ipsum.sh b/ipset/get_antifilter_ipsum.sh new file mode 100755 index 0000000..3fc2673 --- /dev/null +++ b/ipset/get_antifilter_ipsum.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +getuser + +. "$IPSET_DIR/antifilter.helper" + +get_antifilter https://antifilter.network/download/ipsum.lst "$ZIPLIST" + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_config.sh b/ipset/get_config.sh new file mode 100755 index 0000000..9d73133 --- /dev/null +++ b/ipset/get_config.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# run script specified in config + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/../config" + +[ -z "$GETLIST" ] && GETLIST=get_exclude.sh +[ -x "$IPSET_DIR/$GETLIST" ] && exec "$IPSET_DIR/$GETLIST" diff --git a/ipset/get_exclude.sh b/ipset/get_exclude.sh new file mode 100755 index 0000000..ab5dd38 --- /dev/null +++ b/ipset/get_exclude.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# resolve user host list + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +getexclude + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_reestr_combined.sh b/ipset/get_reestr_combined.sh new file mode 100755 index 0000000..675d37d --- /dev/null +++ b/ipset/get_reestr_combined.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +ZREESTR="$TMPDIR/reestr.txt" +#ZURL_REESTR=https://reestr.rublacklist.net/api/current +ZURL_REESTR=https://raw.githubusercontent.com/zapret-info/z-i/master/dump.csv + +getuser + +dig_reestr() +{ + # $1 - grep ipmask + # $2 - iplist + # $3 - ipban list + # $4 - ip version : 4,6 + + local DOMMASK='^.*;[^ ;:/]+\.[^ ;:/]+;' + local TMP="$TMPDIR/tmp.txt" + + echo processing reestr lists $2 $3 + + # find entries with https or without domain name - they should be banned by IP + # 2971-18 is TELEGRAM. lots of proxy IPs banned, list grows very large + (nice -n 5 $GREP -avE "$DOMMASK" "$ZREESTR" ; $GREP -a "https://" "$ZREESTR") | + nice -n 5 $GREP -oE "$1" | cut_local | sort -u >$TMP + + ip2net$4 <"$TMP" | zz "$3" + + # other IPs go to regular zapret list + tail -n +2 "$ZREESTR" | nice -n 5 $GREP -oE "$1" | cut_local | nice -n 5 $GREP -xvFf "$TMP" | ip2net$4 | zz "$2" + + rm -f "$TMP" +} + + +curl -k --fail --max-time 600 --connect-timeout 5 --retry 3 --max-filesize 251658240 "$ZURL_REESTR" -o "$ZREESTR" || +{ + echo reestr list download failed + exit 2 +} +dlsize=$(LANG=C wc -c "$ZREESTR" | xargs | cut -f 1 -d ' ') +if test $dlsize -lt 1048576; then + echo reestr ip list is too small. can be bad. + exit 2 +fi +#sed -i 's/\\n/\r\n/g' $ZREESTR + +get_ip_regex + +[ "$DISABLE_IPV4" != "1" ] && { + dig_reestr "$REG_IPV4" "$ZIPLIST" "$ZIPLIST_IPBAN" 4 +} + +[ "$DISABLE_IPV6" != "1" ] && { + dig_reestr "$REG_IPV6" "$ZIPLIST6" "$ZIPLIST_IPBAN6" 6 +} + +rm -f "$ZREESTR" + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_reestr_hostlist.sh b/ipset/get_reestr_hostlist.sh new file mode 100755 index 0000000..d204d27 --- /dev/null +++ b/ipset/get_reestr_hostlist.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +# useful in case ipban set is used in custom scripts +getuser +"$IPSET_DIR/create_ipset.sh" + +ZREESTR="$TMPDIR/zapret.txt" +#ZURL=https://reestr.rublacklist.net/api/current +ZURL=https://raw.githubusercontent.com/zapret-info/z-i/master/dump.csv + +curl -k --fail --max-time 600 --connect-timeout 5 --retry 3 --max-filesize 251658240 "$ZURL" >"$ZREESTR" || +{ + echo reestr list download failed + exit 2 +} +dlsize=$(LANG=C wc -c "$ZREESTR" | xargs | cut -f 1 -d ' ') +if test $dlsize -lt 204800; then + echo list file is too small. can be bad. + exit 2 +fi +(LANG=C cut -s -f2 -d';' "$ZREESTR" | LANG=C sed -Ee 's/^\*\.(.+)$/\1/' -ne 's/^[a-z0-9A-Z._-]+$/&/p' | awk '{ print tolower($0) }' ; cat "$ZUSERLIST" ) | sort -u | zz "$ZHOSTLIST" +rm -f "$ZREESTR" + +hup_zapret_daemons + +exit 0 diff --git a/ipset/get_reestr_ip.sh b/ipset/get_reestr_ip.sh new file mode 100755 index 0000000..0eeec80 --- /dev/null +++ b/ipset/get_reestr_ip.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +ZREESTR="$TMPDIR/reestr.txt" +#ZURL_REESTR=https://reestr.rublacklist.net/api/current +ZURL_REESTR=https://raw.githubusercontent.com/zapret-info/z-i/master/dump.csv + +getuser + +dig_reestr() +{ + # $1 - grep ipmask + # $2 - iplist + # $3 - ip version : 4,6 + + echo processing reestr list $2 + + tail -n +2 "$ZREESTR" | nice -n 5 $GREP -oE "$1" | cut_local | ip2net$3 | zz "$2" +} + + +# assume all https banned by ip +curl -k --fail --max-time 600 --connect-timeout 5 --retry 3 --max-filesize 251658240 "$ZURL_REESTR" -o "$ZREESTR" || +{ + echo reestr list download failed + exit 2 +} +dlsize=$(LANG=C wc -c "$ZREESTR" | xargs | cut -f 1 -d ' ') +if test $dlsize -lt 1048576; then + echo reestr ip list is too small. can be bad. + exit 2 +fi +#sed -i 's/\\n/\r\n/g' $ZREESTR + +get_ip_regex + +[ "$DISABLE_IPV4" != "1" ] && { + dig_reestr "$REG_IPV4" "$ZIPLIST" 4 +} + +[ "$DISABLE_IPV6" != "1" ] && { + dig_reestr "$REG_IPV6" "$ZIPLIST6" 6 +} + +rm -f "$ZREESTR" + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_reestr_resolve.sh b/ipset/get_reestr_resolve.sh new file mode 100755 index 0000000..c6913ad --- /dev/null +++ b/ipset/get_reestr_resolve.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +ZREESTR="$TMPDIR/zapret.txt" +ZDIG="$TMPDIR/zapret-dig.txt" +ZIPLISTTMP="$TMPDIR/zapret-ip.txt" +#ZURL=https://reestr.rublacklist.net/api/current +ZURL=https://raw.githubusercontent.com/zapret-info/z-i/master/dump.csv + +getuser + +# both disabled +[ "$DISABLE_IPV4" = "1" ] && [ "$DISABLE_IPV6" = "1" ] && exit 0 + +curl -k --fail --max-time 600 --connect-timeout 5 --retry 3 --max-filesize 251658240 "$ZURL" >"$ZREESTR" || +{ + echo reestr list download failed + exit 2 +} + +dlsize=$(LANG=C wc -c "$ZREESTR" | xargs | cut -f 1 -d ' ') +if test $dlsize -lt 204800; then + echo list file is too small. can be bad. + exit 2 +fi + +echo preparing dig list .. +LANG=C cut -f2 -d ';' "$ZREESTR" | LANG=C sed -Ee 's/^\*\.(.+)$/\1/' -ne 's/^[a-z0-9A-Z._-]+$/&/p' >"$ZDIG" +rm -f "$ZREESTR" + +echo digging started. this can take long ... + +[ "$DISABLE_IPV4" != "1" ] && { + digger "$ZDIG" 4 | cut_local >"$ZIPLISTTMP" || { + rm -f "$ZDIG" + exit 1 + } + ip2net4 <"$ZIPLISTTMP" | zz "$ZIPLIST" + rm -f "$ZIPLISTTMP" +} +[ "$DISABLE_IPV6" != "1" ] && { + digger "$ZDIG" 6 | cut_local6 >"$ZIPLISTTMP" || { + rm -f "$ZDIG" + exit 1 + } + ip2net6 <"$ZIPLISTTMP" | zz "$ZIPLIST6" + rm -f "$ZIPLISTTMP" +} +rm -f "$ZDIG" +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/get_user.sh b/ipset/get_user.sh new file mode 100755 index 0000000..2d98981 --- /dev/null +++ b/ipset/get_user.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# resolve user host list + +IPSET_DIR="$(dirname "$0")" +IPSET_DIR="$(cd "$IPSET_DIR"; pwd)" + +. "$IPSET_DIR/def.sh" + +getuser + +"$IPSET_DIR/create_ipset.sh" diff --git a/ipset/zapret-hosts-user-exclude.txt b/ipset/zapret-hosts-user-exclude.txt new file mode 100644 index 0000000..999ccdd --- /dev/null +++ b/ipset/zapret-hosts-user-exclude.txt @@ -0,0 +1,6 @@ +10.0.0.0/8 +172.16.0.0/12 +192.168.0.0/16 +169.254.0.0/16 +fc00::/7 +fe80::/10 diff --git a/ipset/zapret-hosts-user-ipban.txt b/ipset/zapret-hosts-user-ipban.txt new file mode 100644 index 0000000..6073d92 --- /dev/null +++ b/ipset/zapret-hosts-user-ipban.txt @@ -0,0 +1 @@ +pornhub.com diff --git a/ipset/zapret-hosts-user.txt b/ipset/zapret-hosts-user.txt new file mode 100644 index 0000000..ce093c1 --- /dev/null +++ b/ipset/zapret-hosts-user.txt @@ -0,0 +1 @@ +st.kinozal.tv diff --git a/mdig/Makefile b/mdig/Makefile new file mode 100644 index 0000000..83b93bb --- /dev/null +++ b/mdig/Makefile @@ -0,0 +1,21 @@ +CC ?= gcc +CFLAGS += -std=gnu99 -O3 +CFLAGS_BSD = -Wno-address-of-packed-member -Wno-logical-op-parentheses -Wno-switch +CFLAGS_MAC = -mmacosx-version-min=10.8 +STRIP = -s +LIBS = -lpthread +SRC_FILES = *.c + +all: mdig + +mdig: $(SRC_FILES) + $(CC) $(STRIP) $(CFLAGS) -o $@ $(SRC_FILES) $(LDFLAGS) $(LIBS) + +bsd: $(SRC_FILES) + $(CC) $(STRIP) $(CFLAGS) $(CFLAGS_BSD) -o mdig $(SRC_FILES) $(LDFLAGS) $(LIBS) + +mac: $(SRC_FILES) + $(CC) $(CFLAGS) $(CFLAGS_BSD) $(CFLAGS_MAC) -o mdig $(SRC_FILES) $(LDFLAGS) $(LIBS) + +clean: + rm -f mdig *.o diff --git a/mdig/mdig b/mdig/mdig new file mode 120000 index 0000000..276b0e8 --- /dev/null +++ b/mdig/mdig @@ -0,0 +1 @@ +../binaries/x86_64/mdig \ No newline at end of file diff --git a/mdig/mdig.c b/mdig/mdig.c new file mode 100644 index 0000000..f91cb9b --- /dev/null +++ b/mdig/mdig.c @@ -0,0 +1,367 @@ +// multi thread dns resolver +// domain list stdout +// errors, verbose >stderr +// transparent for valid ip or ip/subnet of allowed address family + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RESOLVER_EAGAIN_ATTEMPTS 2 + +static void trimstr(char *s) +{ + char *p; + for (p = s + strlen(s) - 1; p >= s && (*p == '\n' || *p == '\r'); p--) *p = '\0'; +} + +static const char* eai_str(int r) +{ + switch (r) + { + case EAI_NONAME: + return "EAI_NONAME"; + case EAI_AGAIN: + return "EAI_AGAIN"; +#ifdef EAI_ADDRFAMILY + case EAI_ADDRFAMILY: + return "EAI_ADDRFAMILY"; +#endif +#ifdef EAI_NODATA + case EAI_NODATA: + return "EAI_NODATA"; +#endif + case EAI_BADFLAGS: + return "EAI_BADFLAGS"; + case EAI_FAIL: + return "EAI_FAIL"; + case EAI_MEMORY: + return "EAI_MEMORY"; + case EAI_FAMILY: + return "EAI_FAMILY"; + case EAI_SERVICE: + return "EAI_SERVICE"; + case EAI_SOCKTYPE: + return "EAI_SOCKTYPE"; + case EAI_SYSTEM: + return "EAI_SYSTEM"; + default: + return "UNKNOWN"; + } +} + +bool dom_valid(char *dom) +{ + if (!dom || *dom=='.') return false; + for (; *dom; dom++) + if (*dom < 0x20 || *dom>0x7F || !(*dom == '.' || *dom == '-' || *dom == '_' || *dom >= '0' && *dom <= '9' || *dom >= 'a' && *dom <= 'z' || *dom >= 'A' && *dom <= 'Z')) + return false; + return true; +} +void invalid_domain_beautify(char *dom) +{ + for (int i = 0; *dom && i < 64; i++, dom++) + if (*dom < 0x20 || *dom>0x7F) *dom = '?'; + if (*dom) *dom = 0; +} + +#define FAMILY4 1 +#define FAMILY6 2 +static struct +{ + char verbose; + char family; + int threads; + time_t start_time; + pthread_mutex_t flock; + pthread_mutex_t slock; // stats lock + int stats_every, stats_ct, stats_ct_ok; // stats +} glob; + +// get next domain. return 0 if failure +static char interlocked_get_dom(char *dom, size_t size) +{ + char *s; + pthread_mutex_lock(&glob.flock); + s = fgets(dom, size, stdin); + pthread_mutex_unlock(&glob.flock); + if (!s) return 0; + trimstr(s); + return 1; +} +static void interlocked_fprintf(FILE *stream, const char * format, ...) +{ + va_list args; + va_start(args, format); + pthread_mutex_lock(&glob.flock); + vfprintf(stream, format, args); + pthread_mutex_unlock(&glob.flock); + va_end(args); +} + +#define ELOG(format, ...) interlocked_fprintf(stderr, "[%d] " format "\n", tid, ##__VA_ARGS__) +#define VLOG(format, ...) {if (glob.verbose) ELOG(format, ##__VA_ARGS__);} + +static void print_addrinfo(struct addrinfo *ai) +{ + char str[64]; + while (ai) + { + switch (ai->ai_family) + { + case AF_INET: + if (inet_ntop(ai->ai_family, &((struct sockaddr_in*)ai->ai_addr)->sin_addr, str, sizeof(str))) + interlocked_fprintf(stdout, "%s\n", str); + break; + case AF_INET6: + if (inet_ntop(ai->ai_family, &((struct sockaddr_in6*)ai->ai_addr)->sin6_addr, str, sizeof(str))) + interlocked_fprintf(stdout, "%s\n", str); + break; + } + ai = ai->ai_next; + } +} + +static void stat_print(int ct, int ct_ok) +{ + if (glob.stats_every > 0) + { + time_t tm = time(NULL)-glob.start_time; + interlocked_fprintf(stderr, "mdig stats : %02u:%02u:%02u : domains=%d success=%d error=%d\n", tm/3600, (tm/60)%60, tm%60, ct, ct_ok, ct - ct_ok); + } +} + +static void stat_plus(char is_ok) +{ + int ct, ct_ok; + if (glob.stats_every > 0) + { + pthread_mutex_lock(&glob.slock); + ct = ++glob.stats_ct; + ct_ok = glob.stats_ct_ok += !!is_ok; + pthread_mutex_unlock(&glob.slock); + + if (!(ct % glob.stats_every)) stat_print(ct, ct_ok); + } +} + +static uint16_t GetAddrFamily(const char *saddr) +{ + struct in_addr a4; + struct in6_addr a6; + + if (inet_pton(AF_INET, saddr, &a4)) + return AF_INET; + else if (inet_pton(AF_INET6, saddr, &a6)) + return AF_INET6; + return 0; +} + +static void *t_resolver(void *arg) +{ + int tid = (int)(size_t)arg; + int i, r; + char dom[256], is_ok; + struct addrinfo hints; + struct addrinfo *result; + + VLOG("started"); + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = (glob.family == FAMILY4) ? AF_INET : (glob.family == FAMILY6) ? AF_INET6 : AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + + while (interlocked_get_dom(dom, sizeof(dom))) + { + if (*dom) + { + is_ok = 0; + uint16_t family; + char *s_mask, s_ip[sizeof(dom)]; + + strncpy(s_ip, dom, sizeof(s_ip)); + s_mask = strchr(s_ip, '/'); + if (s_mask) *s_mask++ = 0; + family = GetAddrFamily(s_ip); + if (family) + { + if (family == AF_INET && (glob.family & FAMILY4) || family == AF_INET6 && (glob.family & FAMILY6)) + { + unsigned int mask; + bool mask_needed = false; + if (s_mask) + { + if (sscanf(s_mask, "%u", &mask)) + { + switch (family) + { + case AF_INET: is_ok = mask <= 32; mask_needed = mask < 32; break; + case AF_INET6: is_ok = mask <= 128; mask_needed = mask < 128; break; + } + } + } + else + is_ok = 1; + if (is_ok) + interlocked_fprintf(stdout, mask_needed ? "%s/%u\n" : "%s\n", s_ip, mask); + else + VLOG("bad ip/subnet %s", dom); + } + else + VLOG("wrong address family %s", s_ip); + } + else if (dom_valid(dom)) + { + VLOG("resolving %s", dom); + for (i = 0; i < RESOLVER_EAGAIN_ATTEMPTS; i++) + { + if ((r = getaddrinfo(dom, NULL, &hints, &result))) + { + VLOG("failed to resolve %s : result %d (%s)", dom, r, eai_str(r)); + if (r == EAI_AGAIN) continue; // temporary failure. should retry + } + else + { + print_addrinfo(result); + freeaddrinfo(result); + is_ok = 1; + } + break; + } + } + else + { + invalid_domain_beautify(dom); + VLOG("invalid domain : %s", dom); + } + } + stat_plus(is_ok); + } + VLOG("ended"); + return NULL; +} + +static int run_threads() +{ + int i, thread; + pthread_t *t; + + glob.stats_ct = glob.stats_ct_ok = 0; + time(&glob.start_time); + if (pthread_mutex_init(&glob.flock, NULL) != 0) + { + fprintf(stderr, "mutex init failed\n"); + return 10; + } + if (pthread_mutex_init(&glob.slock, NULL) != 0) + { + fprintf(stderr, "mutex init failed\n"); + pthread_mutex_destroy(&glob.flock); + return 10; + } + t = (pthread_t*)malloc(sizeof(pthread_t)*glob.threads); + if (!t) + { + fprintf(stderr, "out of memory\n"); + pthread_mutex_destroy(&glob.slock); + pthread_mutex_destroy(&glob.flock); + return 11; + } + for (thread = 0; thread < glob.threads; thread++) + { + if (pthread_create(t + thread, NULL, t_resolver, (void*)(size_t)thread)) + { + interlocked_fprintf(stderr, "failed to create thread #%d\n", thread); + break; + } + } + for (i = 0; i < thread; i++) + { + pthread_join(t[i], NULL); + } + free(t); + stat_print(glob.stats_ct, glob.stats_ct_ok); + pthread_mutex_destroy(&glob.slock); + pthread_mutex_destroy(&glob.flock); + return thread ? 0 : 12; +} + +static void exithelp() +{ + printf( + " --threads=\n" + " --family=<4|6|46>\t; ipv4, ipv6, ipv4+ipv6\n" + " --verbose\t\t; print query progress to stderr\n" + " --stats=N\t\t; print resolve stats to stderr every N domains\n" + ); + exit(1); +} +int main(int argc, char **argv) +{ + int v, option_index = 0; + + static const struct option long_options[] = { + {"threads",required_argument,0,0}, // optidx=0 + {"family",required_argument,0,0}, // optidx=1 + {"verbose",no_argument,0,0}, // optidx=2 + {"stats",required_argument,0,0}, // optidx=3 + {"help",no_argument,0,0}, // optidx=4 + {NULL,0,NULL,0} + }; + + memset(&glob, 0, sizeof(glob)); + glob.family = FAMILY4; + glob.threads = 1; + while ((v = getopt_long_only(argc, argv, "", long_options, &option_index)) != -1) + { + if (v) exithelp(); + switch (option_index) + { + case 0: /* threads */ + glob.threads = optarg ? atoi(optarg) : 0; + if (glob.threads <= 0 || glob.threads > 100) + { + fprintf(stderr, "thread number must be within 1..100\n"); + return 1; + } + break; + case 1: /* family */ + if (!strcmp(optarg, "4")) + glob.family = FAMILY4; + else if (!strcmp(optarg, "6")) + glob.family = FAMILY6; + else if (!strcmp(optarg, "46")) + glob.family = FAMILY4 | FAMILY6; + else + { + fprintf(stderr, "ip family must be 4,6 or 46\n"); + return 1;; + } + break; + case 2: /* verbose */ + glob.verbose = '\1'; + break; + glob.threads = optarg ? atoi(optarg) : 0; + case 3: /* stats */ + glob.stats_every = optarg ? atoi(optarg) : 0; + break; + case 4: /* help */ + exithelp(); + break; + } + } + return run_threads(); +} diff --git a/nfq/BSDmakefile b/nfq/BSDmakefile new file mode 100644 index 0000000..219154f --- /dev/null +++ b/nfq/BSDmakefile @@ -0,0 +1,12 @@ +CC ?= cc +CFLAGS += -std=gnu99 -s -O3 -Wno-address-of-packed-member -Wno-logical-op-parentheses -Wno-switch +LIBS = -lz +SRC_FILES = *.c + +all: dvtws + +dvtws: $(SRC_FILES) + $(CC) $(CFLAGS) -o $@ $(SRC_FILES) $(LDFLAGS) $(LIBS) + +clean: + rm -f dvtws *.o diff --git a/nfq/Makefile b/nfq/Makefile new file mode 100644 index 0000000..e5f382b --- /dev/null +++ b/nfq/Makefile @@ -0,0 +1,22 @@ +CC ?= gcc +CFLAGS += -std=gnu99 -O3 +CFLAGS_BSD = -Wno-address-of-packed-member -Wno-logical-op-parentheses -Wno-switch +CFLAGS_MAC = -mmacosx-version-min=10.8 +STRIP = -s +LIBS = -lnetfilter_queue -lnfnetlink -lz +LIBS_BSD = -lz +SRC_FILES = *.c + +all: nfqws + +nfqws: $(SRC_FILES) + $(CC) $(STRIP) $(CFLAGS) -o $@ $(SRC_FILES) $(LDFLAGS) $(LIBS) + +bsd: $(SRC_FILES) + $(CC) $(STRIP) $(CFLAGS) $(CFLAGS_BSD) -o dvtws $(SRC_FILES) $(LDFLAGS) $(LIBS_BSD) + +mac: $(SRC_FILES) + $(CC) $(CFLAGS) $(CFLAGS_BSD) $(CFLAGS_MAC) -o dvtws $(SRC_FILES) $(LDFLAGS) $(LIBS_BSD) + +clean: + rm -f nfqws dvtws *.o diff --git a/nfq/checksum.c b/nfq/checksum.c new file mode 100644 index 0000000..e29b521 --- /dev/null +++ b/nfq/checksum.c @@ -0,0 +1,138 @@ +#define _GNU_SOURCE +#include "checksum.h" +#include + +//#define htonll(x) ((1==htonl(1)) ? (x) : ((uint64_t)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32)) +//#define ntohll(x) ((1==ntohl(1)) ? (x) : ((uint64_t)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32)) + +static uint16_t from64to16(uint64_t x) +{ + uint32_t u = (uint32_t)(uint16_t)x + (uint16_t)(x>>16) + (uint16_t)(x>>32) + (uint16_t)(x>>48); + return (uint16_t)u + (uint16_t)(u>>16); +} + +static uint16_t do_csum(const uint8_t * buff, size_t len) +{ + uint8_t odd; + size_t count; + uint64_t result,w,carry=0; + uint16_t u16; + + if (len <= 0) return 0; + odd = (uint8_t)(1 & (size_t)buff); + if (odd) + { + // any endian compatible + u16 = 0; + *((uint8_t*)&u16+1) = *buff; + result = u16; + len--; + buff++; + } + else + result = 0; + count = len >> 1; /* nr of 16-bit words.. */ + if (count) + { + if (2 & (size_t) buff) + { + result += *(uint16_t *) buff; + count--; + len -= 2; + buff += 2; + } + count >>= 1; /* nr of 32-bit words.. */ + if (count) + { + if (4 & (size_t) buff) + { + result += *(uint32_t *) buff; + count--; + len -= 4; + buff += 4; + } + count >>= 1; /* nr of 64-bit words.. */ + if (count) + { + do + { + w = *(uint64_t *) buff; + count--; + buff += 8; + result += carry; + result += w; + carry = (w > result); + } while (count); + result += carry; + result = (result & 0xffffffff) + (result >> 32); + } + if (len & 4) + { + result += *(uint32_t *) buff; + buff += 4; + } + } + if (len & 2) + { + result += *(uint16_t *) buff; + buff += 2; + } + } + if (len & 1) + { + // any endian compatible + u16 = 0; + *(uint8_t*)&u16 = *buff; + result += u16; + } + u16 = from64to16(result); + if (odd) u16 = ((u16 >> 8) & 0xff) | ((u16 & 0xff) << 8); + return u16; +} + +uint16_t csum_partial(const void *buff, size_t len) +{ + return do_csum(buff,len); +} + +uint16_t csum_tcpudp_magic(uint32_t saddr, uint32_t daddr, size_t len, uint8_t proto, uint16_t sum) +{ + return ~from64to16((uint64_t)saddr + daddr + sum + htonl(len+proto)); +} + +uint16_t ip4_compute_csum(const void *buff, size_t len) +{ + return ~from64to16(do_csum(buff,len)); +} +void ip4_fix_checksum(struct ip *ip) +{ + ip->ip_sum = 0; + ip->ip_sum = ip4_compute_csum(ip, ip->ip_hl<<2); +} + +uint16_t csum_ipv6_magic(const void *saddr, const void *daddr, size_t len, uint8_t proto, uint16_t sum) +{ + uint64_t a = (uint64_t)sum + htonl(len+proto) + + *(uint32_t*)saddr + *((uint32_t*)saddr+1) + *((uint32_t*)saddr+2) + *((uint32_t*)saddr+3) + + *(uint32_t*)daddr + *((uint32_t*)daddr+1) + *((uint32_t*)daddr+2) + *((uint32_t*)daddr+3); + return ~from64to16(a); +} + + +void tcp4_fix_checksum(struct tcphdr *tcp,size_t len, const struct in_addr *src_addr, const struct in_addr *dest_addr) +{ + tcp->th_sum = 0; + tcp->th_sum = csum_tcpudp_magic(src_addr->s_addr,dest_addr->s_addr,len,IPPROTO_TCP,csum_partial(tcp, len)); +} +void tcp6_fix_checksum(struct tcphdr *tcp,size_t len, const struct in6_addr *src_addr, const struct in6_addr *dest_addr) +{ + tcp->th_sum = 0; + tcp->th_sum = csum_ipv6_magic(src_addr,dest_addr,len,IPPROTO_TCP,csum_partial(tcp, len)); +} +void tcp_fix_checksum(struct tcphdr *tcp,size_t len,const struct ip *ip,const struct ip6_hdr *ip6hdr) +{ + if (ip) + tcp4_fix_checksum(tcp, len, &ip->ip_src, &ip->ip_dst); + else if (ip6hdr) + tcp6_fix_checksum(tcp, len, &ip6hdr->ip6_src, &ip6hdr->ip6_dst); +} diff --git a/nfq/checksum.h b/nfq/checksum.h new file mode 100644 index 0000000..6776739 --- /dev/null +++ b/nfq/checksum.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +uint16_t csum_partial(const void *buff, size_t len); +uint16_t csum_tcpudp_magic(uint32_t saddr, uint32_t daddr, size_t len, uint8_t proto, uint16_t sum); +uint16_t csum_ipv6_magic(const void *saddr, const void *daddr, size_t len, uint8_t proto, uint16_t sum); +uint16_t ip4_compute_csum(const void *buff, size_t len); +void ip4_fix_checksum(struct ip *ip); + +void tcp4_fix_checksum(struct tcphdr *tcp,size_t len, const struct in_addr *src_addr, const struct in_addr *dest_addr); +void tcp6_fix_checksum(struct tcphdr *tcp,size_t len, const struct in6_addr *src_addr, const struct in6_addr *dest_addr); +void tcp_fix_checksum(struct tcphdr *tcp,size_t len,const struct ip *ip,const struct ip6_hdr *ip6hdr); diff --git a/nfq/darkmagic.c b/nfq/darkmagic.c new file mode 100644 index 0000000..2a1170f --- /dev/null +++ b/nfq/darkmagic.c @@ -0,0 +1,650 @@ +#define _GNU_SOURCE + +#include "darkmagic.h" +#include +#include +#include +#include +#include +#include +#include + +#include "helpers.h" + + +uint32_t net32_add(uint32_t netorder_value, uint32_t cpuorder_increment) +{ + return htonl(ntohl(netorder_value)+cpuorder_increment); +} + +uint8_t *tcp_find_option(struct tcphdr *tcp, uint8_t kind) +{ + uint8_t *t = (uint8_t*)(tcp+1); + uint8_t *end = (uint8_t*)tcp + (tcp->th_off<<2); + while(t=end || t[1]<2 || (t+t[1])>end) + return NULL; + if (*t==kind) + return t; + t+=t[1]; + break; + } + } + return NULL; +} +uint32_t *tcp_find_timestamps(struct tcphdr *tcp) +{ + uint8_t *t = tcp_find_option(tcp,8); + return (t && t[1]==10) ? (uint32_t*)(t+2) : NULL; +} + +static void fill_tcphdr(struct tcphdr *tcp, uint8_t tcp_flags, uint32_t seq, uint32_t ack_seq, uint8_t fooling, uint16_t nsport, uint16_t ndport, uint16_t nwsize, uint32_t *timestamps) +{ + char *tcpopt = (char*)(tcp+1); + uint8_t t=0; + + memset(tcp,0,sizeof(*tcp)); + tcp->th_sport = nsport; + tcp->th_dport = ndport; + if (fooling & TCP_FOOL_BADSEQ) + { + tcp->th_seq = net32_add(seq,0x80000000); + tcp->th_ack = net32_add(ack_seq,0x80000000); + } + else + { + tcp->th_seq = seq; + tcp->th_ack = ack_seq; + } + tcp->th_off = 5; + *((uint8_t*)tcp+13)= tcp_flags; + tcp->th_win = nwsize; + if (fooling & TCP_FOOL_MD5SIG) + { + tcpopt[0] = 19; // kind + tcpopt[1] = 18; // len + *(uint32_t*)(tcpopt+2)=random(); + *(uint32_t*)(tcpopt+6)=random(); + *(uint32_t*)(tcpopt+10)=random(); + *(uint32_t*)(tcpopt+14)=random(); + t=18; + } + if (timestamps || (fooling & TCP_FOOL_TS)) + { + tcpopt[t] = 8; // kind + tcpopt[t+1] = 10; // len + // forge only TSecr if orig timestamp is present + *(uint32_t*)(tcpopt+t+2) = timestamps ? timestamps[0] : -1; + *(uint32_t*)(tcpopt+t+6) = (timestamps && !(fooling & TCP_FOOL_TS)) ? timestamps[1] : -1; + t+=10; + } + while (t&3) tcpopt[t++]=1; // noop + tcp->th_off += t>>2; +} +static uint16_t tcpopt_len(uint8_t fooling, uint32_t *timestamps) +{ + uint16_t t=0; + if (fooling & TCP_FOOL_MD5SIG) t=18; + if ((fooling & TCP_FOOL_TS) || timestamps) t+=10; + return (t+3)&~3; +} + +static int rawsend_sock4=-1, rawsend_sock6=-1; +static void rawsend_clean_sock(int *sock) +{ + if (sock && *sock!=-1) + { + close(*sock); + *sock=-1; + } +} +void rawsend_cleanup() +{ + rawsend_clean_sock(&rawsend_sock4); + rawsend_clean_sock(&rawsend_sock6); +} +static int *rawsend_family_sock(sa_family_t family) +{ + switch(family) + { + case AF_INET: return &rawsend_sock4; + case AF_INET6: return &rawsend_sock6; + default: return NULL; + } +} + +#ifdef BSD +static int rawsend_socket_divert(sa_family_t family) +{ + // HACK HACK HACK HACK HACK HACK HACK HACK + // FreeBSD doesnt allow IP_HDRINCL for IPV6 + // OpenBSD doesnt allow rawsending tcp frames + // we either have to go to the link layer (its hard, possible problems arise, compat testing, ...) or use some HACKING + // from my point of view disabling direct ability to send ip frames is not security. its SHIT + + int fd = socket(family, SOCK_RAW, IPPROTO_DIVERT); + if (!set_socket_buffers(fd,4096,RAW_SNDBUF)) + { + close(fd); + return -1; + } + return fd; +} +static int rawsend_sendto_divert(sa_family_t family, int sock, const void *buf, size_t len) +{ + struct sockaddr_storage sa; + socklen_t slen; + + memset(&sa,0,sizeof(sa)); + sa.ss_family = family; + switch(family) + { + case AF_INET: + slen = sizeof(struct sockaddr_in); + break; + case AF_INET6: + slen = sizeof(struct sockaddr_in6); + break; + default: + return -1; + } + return sendto(sock, buf, len, 0, (struct sockaddr*)&sa, slen); +} +#endif + +static int rawsend_socket_raw(int domain, int proto) +{ + int fd = socket(domain, SOCK_RAW, proto); + if (fd!=-1) + { + #ifdef __linux__ + int s=RAW_SNDBUF/2; + int r=2048; + #else + int s=RAW_SNDBUF; + int r=4096; + #endif + if (!set_socket_buffers(fd,r,s)) + { + close(fd); + return -1; + } + } + return fd; +} + +static int rawsend_socket(sa_family_t family,uint32_t fwmark) +{ + int yes=1; + int *sock = rawsend_family_sock(family); + if (!sock) return -1; + + if (*sock==-1) + { + int yes=1,pri=6; + //printf("rawsend_socket: family %d",family); + +#ifdef __FreeBSD__ + // IPPROTO_RAW with ipv6 in FreeBSD always returns EACCES on sendto. + // must use IPPROTO_TCP for ipv6. IPPROTO_RAW works for ipv4 + // divert sockets are always v4 but accept both v4 and v6 + *sock = (family==AF_INET) ? rawsend_socket_raw(family, IPPROTO_TCP) : rawsend_socket_divert(AF_INET); +#elif defined(__OpenBSD__) || defined (__APPLE__) + // OpenBSD does not allow sending TCP frames through raw sockets + // I dont know about macos. They have dropped ipfw in recent versions and their PF does not support divert-packet + *sock = rawsend_socket_divert(family); +#else + *sock = rawsend_socket_raw(family, IPPROTO_RAW); +#endif + if (*sock==-1) + { + perror("rawsend: socket()"); + return -1; + } +#ifdef BSD +#if !(defined(__OpenBSD__) || defined (__APPLE__)) + // HDRINCL not supported for ipv6 in any BSD + if (family==AF_INET && setsockopt(*sock,IPPROTO_IP,IP_HDRINCL,&yes,sizeof(yes)) == -1) + { + perror("rawsend: setsockopt(IP_HDRINCL)"); + goto exiterr; + } +#endif +#ifdef SO_USER_COOKIE + if (setsockopt(*sock, SOL_SOCKET, SO_USER_COOKIE, &fwmark, sizeof(fwmark)) == -1) + { + perror("rawsend: setsockopt(SO_MARK)"); + goto exiterr; + } +#endif +#endif +#ifdef __linux__ + if (setsockopt(*sock, SOL_SOCKET, SO_MARK, &fwmark, sizeof(fwmark)) == -1) + { + perror("rawsend: setsockopt(SO_MARK)"); + goto exiterr; + } + if (setsockopt(*sock, SOL_SOCKET, SO_PRIORITY, &pri, sizeof(pri)) == -1) + { + perror("rawsend: setsockopt(SO_PRIORITY)"); + goto exiterr; + } +#endif + } + return *sock; +exiterr: + rawsend_clean_sock(sock); + return -1; +} +bool rawsend_preinit(uint32_t fwmark) +{ + return rawsend_socket(AF_INET,fwmark)!=-1 && rawsend_socket(AF_INET6,fwmark)!=-1; +} +bool rawsend(const struct sockaddr* dst,uint32_t fwmark,const void *data,size_t len) +{ + int sock=rawsend_socket(dst->sa_family,fwmark); + if (sock==-1) return false; + int salen = dst->sa_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6); + struct sockaddr_storage dst2; + memcpy(&dst2,dst,salen); + if (dst->sa_family==AF_INET6) + ((struct sockaddr_in6 *)&dst2)->sin6_port = 0; // or will be EINVAL in linux +#ifdef BSD +/* + // this works only for local connections and not working for transit : cant spoof source addr + if (len>=sizeof(struct ip6_hdr)) + { + // BSD ipv6 raw socks are limited. cannot pass the whole packet with ip6 header. + struct sockaddr_storage sa_src; + int v; + extract_endpoints(NULL,(struct ip6_hdr *)data,NULL, &sa_src, NULL); + v = ((struct ip6_hdr *)data)->ip6_ctlun.ip6_un1.ip6_un1_hlim; + if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &v, sizeof(v)) == -1) + perror("rawsend: setsockopt(IPV6_HOPLIMIT)"); + // the only way to control source address is bind. make it equal to ip6_hdr + if (bind(sock, (struct sockaddr*)&sa_src, salen) < 0) + perror("rawsend bind: "); + //printf("BSD v6 RAWSEND "); print_sockaddr((struct sockaddr*)&sa_src); printf(" -> "); print_sockaddr((struct sockaddr*)&dst2); printf("\n"); + proto_skip_ipv6((uint8_t**)&data, &len, NULL); + } +*/ + +#if !(defined(__OpenBSD__) || defined (__APPLE__)) + // OpenBSD doesnt allow rawsending tcp frames. always use divert socket + if (dst->sa_family==AF_INET6) +#endif + { + ssize_t bytes = rawsend_sendto_divert(dst->sa_family,sock,data,len); + if (bytes==-1) + { + perror("rawsend: sendto_divert"); + return false; + } + return true; + } +#endif + +#if defined(__FreeBSD__) && __FreeBSD__<=10 + // old FreeBSD requires some fields in the host byte order + if (dst->sa_family==AF_INET && len>=sizeof(struct ip)) + { + ((struct ip*)data)->ip_len = htons(((struct ip*)data)->ip_len); + ((struct ip*)data)->ip_off = htons(((struct ip*)data)->ip_off); + } +#endif + // normal raw socket sendto + ssize_t bytes = sendto(sock, data, len, 0, (struct sockaddr*)&dst2, salen); +#if defined(__FreeBSD) && __FreeBSD__<=10 + // restore byte order + if (dst->sa_family==AF_INET && len>=sizeof(struct ip)) + { + ((struct ip*)data)->ip_len = htons(((struct ip*)data)->ip_len); + ((struct ip*)data)->ip_off = htons(((struct ip*)data)->ip_off); + } +#endif + if (bytes==-1) + { + perror("rawsend: sendto"); + return false; + } + return true; +} +bool prepare_tcp_segment4( + const struct sockaddr_in *src, const struct sockaddr_in *dst, + uint8_t tcp_flags, + uint32_t seq, uint32_t ack_seq, + uint16_t wsize, + uint32_t *timestamps, + uint8_t ttl, + uint8_t fooling, + const void *data, uint16_t len, + uint8_t *buf, size_t *buflen) +{ + uint16_t tcpoptlen = tcpopt_len(fooling,timestamps); + uint16_t pktlen = sizeof(struct ip) + sizeof(struct tcphdr) + tcpoptlen + len; + if (pktlen>*buflen) + { + fprintf(stderr,"prepare_tcp_segment : packet len cannot exceed %zu\n",*buflen); + return false; + } + + struct ip *ip = (struct ip*) buf; + struct tcphdr *tcp = (struct tcphdr*) (ip+1); + + ip->ip_off = 0; + ip->ip_v = 4; + ip->ip_hl = 5; + ip->ip_len = htons(pktlen); + ip->ip_id = 0; + ip->ip_ttl = ttl; + ip->ip_p = IPPROTO_TCP; + ip->ip_src = src->sin_addr; + ip->ip_dst = dst->sin_addr; + + fill_tcphdr(tcp,tcp_flags,seq,ack_seq,fooling,src->sin_port,dst->sin_port,wsize,timestamps); + + memcpy((char*)tcp+sizeof(struct tcphdr)+tcpoptlen,data,len); + tcp4_fix_checksum(tcp,sizeof(struct tcphdr)+tcpoptlen+len,&ip->ip_src,&ip->ip_dst); + if (fooling & TCP_FOOL_BADSUM) tcp->th_sum^=0xBEAF; + + *buflen = pktlen; + return true; +} + + +bool prepare_tcp_segment6( + const struct sockaddr_in6 *src, const struct sockaddr_in6 *dst, + uint8_t tcp_flags, + uint32_t seq, uint32_t ack_seq, + uint16_t wsize, + uint32_t *timestamps, + uint8_t ttl, + uint8_t fooling, + const void *data, uint16_t len, + uint8_t *buf, size_t *buflen) +{ + uint16_t tcpoptlen = tcpopt_len(fooling,timestamps); + uint16_t payloadlen = sizeof(struct tcphdr) + tcpoptlen + len; + uint16_t pktlen = sizeof(struct ip6_hdr) + payloadlen; + if (pktlen>*buflen) + { + fprintf(stderr,"prepare_tcp_segment : packet len cannot exceed %zu\n",*buflen); + return false; + } + + struct ip6_hdr *ip6 = (struct ip6_hdr*) buf; + struct tcphdr *tcp = (struct tcphdr*) (ip6+1); + + ip6->ip6_ctlun.ip6_un1.ip6_un1_flow = htonl(0x60000000); + ip6->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(payloadlen); + ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt = IPPROTO_TCP; + ip6->ip6_ctlun.ip6_un1.ip6_un1_hlim = ttl; + ip6->ip6_src = src->sin6_addr; + ip6->ip6_dst = dst->sin6_addr; + + fill_tcphdr(tcp,tcp_flags,seq,ack_seq,fooling,src->sin6_port,dst->sin6_port,wsize,timestamps); + + memcpy((char*)tcp+sizeof(struct tcphdr)+tcpoptlen,data,len); + tcp6_fix_checksum(tcp,sizeof(struct tcphdr)+tcpoptlen+len,&ip6->ip6_src,&ip6->ip6_dst); + if (fooling & TCP_FOOL_BADSUM) tcp->th_sum^=0xBEAF; + + *buflen = pktlen; + return true; +} + +bool prepare_tcp_segment( + const struct sockaddr *src, const struct sockaddr *dst, + uint8_t tcp_flags, + uint32_t seq, uint32_t ack_seq, + uint16_t wsize, + uint32_t *timestamps, + uint8_t ttl, + uint8_t fooling, + const void *data, uint16_t len, + uint8_t *buf, size_t *buflen) +{ + return (src->sa_family==AF_INET && dst->sa_family==AF_INET) ? + prepare_tcp_segment4((struct sockaddr_in *)src,(struct sockaddr_in *)dst,tcp_flags,seq,ack_seq,wsize,timestamps,ttl,fooling,data,len,buf,buflen) : + (src->sa_family==AF_INET6 && dst->sa_family==AF_INET6) ? + prepare_tcp_segment6((struct sockaddr_in6 *)src,(struct sockaddr_in6 *)dst,tcp_flags,seq,ack_seq,wsize,timestamps,ttl,fooling,data,len,buf,buflen) : + false; +} + + +void extract_endpoints(const struct ip *ip,const struct ip6_hdr *ip6hdr,const struct tcphdr *tcphdr, struct sockaddr_storage *src, struct sockaddr_storage *dst) +{ + if (ip) + { + struct sockaddr_in *si; + + if (dst) + { + si = (struct sockaddr_in*)dst; + si->sin_family = AF_INET; + si->sin_port = tcphdr ? tcphdr->th_dport : 0; + si->sin_addr = ip->ip_dst; + } + + if (src) + { + si = (struct sockaddr_in*)src; + si->sin_family = AF_INET; + si->sin_port = tcphdr ? tcphdr->th_sport : 0; + si->sin_addr = ip->ip_src; + } + } + else if (ip6hdr) + { + struct sockaddr_in6 *si; + + if (dst) + { + si = (struct sockaddr_in6*)dst; + si->sin6_family = AF_INET6; + si->sin6_port = tcphdr ? tcphdr->th_dport : 0; + si->sin6_addr = ip6hdr->ip6_dst; + si->sin6_flowinfo = 0; + si->sin6_scope_id = 0; + } + + if (src) + { + si = (struct sockaddr_in6*)src; + si->sin6_family = AF_INET6; + si->sin6_port = tcphdr ? tcphdr->th_sport : 0; + si->sin6_addr = ip6hdr->ip6_src; + si->sin6_flowinfo = 0; + si->sin6_scope_id = 0; + } + } +} + +static const char *proto_name(uint8_t proto) +{ + switch(proto) + { + case IPPROTO_TCP: + return "tcp"; + case IPPROTO_UDP: + return "udp"; + case IPPROTO_ICMP: + return "icmp"; + case IPPROTO_IGMP: + return "igmp"; + case IPPROTO_ESP: + return "esp"; + case IPPROTO_AH: + return "ah"; + case IPPROTO_IPV6: + return "6in4"; +#ifdef IPPROTO_SCTP + case IPPROTO_SCTP: + return "sctp"; +#endif + default: + return NULL; + } +} +static void str_proto_name(char *s, size_t s_len, uint8_t proto) +{ + const char *name = proto_name(proto); + if (name) + snprintf(s,s_len,"%s",name); + else + snprintf(s,s_len,"%u",proto); +} + +static void str_srcdst_ip(char *s, size_t s_len, const void *saddr,const void *daddr) +{ + char s_ip[16],d_ip[16]; + *s_ip=*d_ip=0; + inet_ntop(AF_INET, saddr, s_ip, sizeof(s_ip)); + inet_ntop(AF_INET, daddr, d_ip, sizeof(d_ip)); + snprintf(s,s_len,"%s => %s",s_ip,d_ip); +} +static void str_ip(char *s, size_t s_len, const struct ip *ip) +{ + char ss[64],s_proto[16]; + str_srcdst_ip(ss,sizeof(ss),&ip->ip_src,&ip->ip_dst); + str_proto_name(s_proto,sizeof(s_proto),ip->ip_p); + snprintf(s,s_len,"%s proto=%s",ss,s_proto); +} +void print_ip(const struct ip *ip) +{ + char s[64]; + str_ip(s,sizeof(s),ip); + printf("%s",s); +} +static void str_srcdst_ip6(char *s, size_t s_len, const void *saddr,const void *daddr) +{ + char s_ip[40],d_ip[40]; + *s_ip=*d_ip=0; + inet_ntop(AF_INET6, saddr, s_ip, sizeof(s_ip)); + inet_ntop(AF_INET6, daddr, d_ip, sizeof(d_ip)); + snprintf(s,s_len,"%s => %s",s_ip,d_ip); +} +static void str_ip6hdr(char *s, size_t s_len, const struct ip6_hdr *ip6hdr, uint8_t proto) +{ + char ss[128],s_proto[16]; + str_srcdst_ip6(ss,sizeof(ss),&ip6hdr->ip6_src,&ip6hdr->ip6_dst); + str_proto_name(s_proto,sizeof(s_proto),proto); + snprintf(s,s_len,"%s proto=%s",ss,s_proto); +} +void print_ip6hdr(const struct ip6_hdr *ip6hdr, uint8_t proto) +{ + char s[128]; + str_ip6hdr(s,sizeof(s),ip6hdr,proto); + printf("%s",s); +} + +static void str_tcphdr(char *s, size_t s_len, const struct tcphdr *tcphdr) +{ + char flags[7],*f=flags; + if (tcphdr->th_flags & TH_SYN) *f++='S'; + if (tcphdr->th_flags & TH_ACK) *f++='A'; + if (tcphdr->th_flags & TH_RST) *f++='R'; + if (tcphdr->th_flags & TH_FIN) *f++='F'; + if (tcphdr->th_flags & TH_PUSH) *f++='P'; + if (tcphdr->th_flags & TH_URG) *f++='U'; + *f=0; + snprintf(s,s_len,"sport=%u dport=%u flags=%s seq=%u ack_seq=%u",htons(tcphdr->th_sport),htons(tcphdr->th_dport),flags,htonl(tcphdr->th_seq),htonl(tcphdr->th_ack)); +} +void print_tcphdr(const struct tcphdr *tcphdr) +{ + char s[80]; + str_tcphdr(s,sizeof(s),tcphdr); + printf("%s",s); +} + + + + +bool proto_check_ipv4(uint8_t *data, size_t len) +{ + return len >= 20 && (data[0] & 0xF0) == 0x40 && + len >= ((data[0] & 0x0F) << 2); +} +// move to transport protocol +void proto_skip_ipv4(uint8_t **data, size_t *len) +{ + size_t l; + + l = (**data & 0x0F) << 2; + *data += l; + *len -= l; +} +bool proto_check_tcp(uint8_t *data, size_t len) +{ + return len >= 20 && len >= ((data[12] & 0xF0) >> 2); +} +void proto_skip_tcp(uint8_t **data, size_t *len) +{ + size_t l; + l = ((*data)[12] & 0xF0) >> 2; + *data += l; + *len -= l; +} + +bool proto_check_ipv6(uint8_t *data, size_t len) +{ + return len >= 40 && (data[0] & 0xF0) == 0x60 && + (len - 40) >= htons(*(uint16_t*)(data + 4)); // payload length +} +// move to transport protocol +// proto_type = 0 => error +void proto_skip_ipv6(uint8_t **data, size_t *len, uint8_t *proto_type) +{ + size_t hdrlen; + uint8_t HeaderType; + + if (proto_type) *proto_type = 0; // put error in advance + + HeaderType = (*data)[6]; // NextHeader field + *data += 40; *len -= 40; // skip ipv6 base header + while (*len > 0) // need at least one byte for NextHeader field + { + switch (HeaderType) + { + case 0: // Hop-by-Hop Options + case 43: // routing + case 51: // authentication + case 60: // Destination Options + case 135: // mobility + case 139: // Host Identity Protocol Version v2 + case 140: // Shim6 + if (*len < 2) return; // error + hdrlen = 8 + ((*data)[1] << 3); + break; + case 44: // fragment. length fixed to 8, hdrlen field defined as reserved + hdrlen = 8; + break; + case 59: // no next header + return; // error + default: + // we found some meaningful payload. it can be tcp, udp, icmp or some another exotic shit + if (proto_type) *proto_type = HeaderType; + return; + } + if (*len < hdrlen) return; // error + HeaderType = **data; + // advance to the next header location + *len -= hdrlen; + *data += hdrlen; + } + // we have garbage +} + +bool tcp_synack_segment(const struct tcphdr *tcphdr) +{ + /* check for set bits in TCP hdr */ + return ((tcphdr->th_flags & (TH_URG|TH_ACK|TH_PUSH|TH_RST|TH_SYN|TH_FIN)) == (TH_ACK|TH_SYN)); +} diff --git a/nfq/darkmagic.h b/nfq/darkmagic.h new file mode 100644 index 0000000..2a39b93 --- /dev/null +++ b/nfq/darkmagic.h @@ -0,0 +1,76 @@ +#pragma once + +#include "checksum.h" + +#include +#include +#include +#include +#include +#include +#include + +// returns netorder value +uint32_t net32_add(uint32_t netorder_value, uint32_t cpuorder_increment); + +#define TCP_FOOL_NONE 0 +#define TCP_FOOL_MD5SIG 1 +#define TCP_FOOL_BADSUM 2 +#define TCP_FOOL_TS 4 +#define TCP_FOOL_BADSEQ 8 + +// seq and wsize have network byte order +bool prepare_tcp_segment4( + const struct sockaddr_in *src, const struct sockaddr_in *dst, + uint8_t tcp_flags, + uint32_t seq, uint32_t ack_seq, + uint16_t wsize, + uint32_t *timestamps, + uint8_t ttl, + uint8_t fooling, + const void *data, uint16_t len, + uint8_t *buf, size_t *buflen); +bool prepare_tcp_segment6( + const struct sockaddr_in6 *src, const struct sockaddr_in6 *dst, + uint8_t tcp_flags, + uint32_t seq, uint32_t ack_seq, + uint16_t wsize, + uint32_t *timestamps, + uint8_t ttl, + uint8_t fooling, + const void *data, uint16_t len, + uint8_t *buf, size_t *buflen); +bool prepare_tcp_segment( + const struct sockaddr *src, const struct sockaddr *dst, + uint8_t tcp_flags, + uint32_t seq, uint32_t ack_seq, + uint16_t wsize, + uint32_t *timestamps, + uint8_t ttl, + uint8_t fooling, + const void *data, uint16_t len, + uint8_t *buf, size_t *buflen); + +void extract_endpoints(const struct ip *ip,const struct ip6_hdr *ip6hdr,const struct tcphdr *tcphdr, struct sockaddr_storage *src, struct sockaddr_storage *dst); +uint8_t *tcp_find_option(struct tcphdr *tcp, uint8_t kind); +uint32_t *tcp_find_timestamps(struct tcphdr *tcp); + +// auto creates internal socket and uses it for subsequent calls +bool rawsend(const struct sockaddr* dst,uint32_t fwmark,const void *data,size_t len); +// should pre-do it if dropping privileges. otherwise its not necessary +bool rawsend_preinit(uint32_t fwmark); +// cleans up socket autocreated by rawsend +void rawsend_cleanup(); + +void print_ip(const struct ip *ip); +void print_ip6hdr(const struct ip6_hdr *ip6hdr, uint8_t proto); +void print_tcphdr(const struct tcphdr *tcphdr); + + +bool proto_check_ipv4(uint8_t *data, size_t len); +void proto_skip_ipv4(uint8_t **data, size_t *len); +bool proto_check_tcp(uint8_t *data, size_t len); +void proto_skip_tcp(uint8_t **data, size_t *len); +bool proto_check_ipv6(uint8_t *data, size_t len); +void proto_skip_ipv6(uint8_t **data, size_t *len, uint8_t *proto_type); +bool tcp_synack_segment(const struct tcphdr *tcphdr); diff --git a/nfq/desync.c b/nfq/desync.c new file mode 100644 index 0000000..d6c6f59 --- /dev/null +++ b/nfq/desync.c @@ -0,0 +1,396 @@ +#define _GNU_SOURCE + +#include "desync.h" +#include "protocol.h" +#include "params.h" +#include "helpers.h" +#include "hostlist.h" + +#include + + +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"; +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, + 0x14, 0x79, 0x2c, 0xaa, 0xcd, 0xea, 0xda, 0xf0, 0xe1, 0xfd, 0xbb, 0x20, 0xf4, 0x83, 0x2a, 0x94, + 0xf1, 0x48, 0x3b, 0x9d, 0xb6, 0x74, 0xba, 0x3c, 0x81, 0x63, 0xbc, 0x18, 0xcc, 0x14, 0x45, 0x57, + 0x6c, 0x80, 0xf9, 0x25, 0xcf, 0x9c, 0x86, 0x60, 0x50, 0x31, 0x2e, 0xe9, 0x00, 0x22, 0x13, 0x01, + 0x13, 0x03, 0x13, 0x02, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x2c, 0xc0, 0x30, + 0xc0, 0x0a, 0xc0, 0x09, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x33, 0x00, 0x39, 0x00, 0x2f, 0x00, 0x35, + 0x01, 0x00, 0x01, 0x91, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x0d, 0x00, 0x00, 0x0a, 0x77, 0x77, 0x77, + 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x0a, 0x00, 0x0e, 0x00, 0x0c, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x01, 0x00, + 0x01, 0x01, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, + 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x05, + 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x6b, 0x00, 0x69, 0x00, 0x1d, 0x00, + 0x20, 0xb0, 0xe4, 0xda, 0x34, 0xb4, 0x29, 0x8d, 0xd3, 0x5c, 0x70, 0xd3, 0xbe, 0xe8, 0xa7, 0x2a, + 0x6b, 0xe4, 0x11, 0x19, 0x8b, 0x18, 0x9d, 0x83, 0x9a, 0x49, 0x7c, 0x83, 0x7f, 0xa9, 0x03, 0x8c, + 0x3c, 0x00, 0x17, 0x00, 0x41, 0x04, 0x4c, 0x04, 0xa4, 0x71, 0x4c, 0x49, 0x75, 0x55, 0xd1, 0x18, + 0x1e, 0x22, 0x62, 0x19, 0x53, 0x00, 0xde, 0x74, 0x2f, 0xb3, 0xde, 0x13, 0x54, 0xe6, 0x78, 0x07, + 0x94, 0x55, 0x0e, 0xb2, 0x6c, 0xb0, 0x03, 0xee, 0x79, 0xa9, 0x96, 0x1e, 0x0e, 0x98, 0x17, 0x78, + 0x24, 0x44, 0x0c, 0x88, 0x80, 0x06, 0x8b, 0xd4, 0x80, 0xbf, 0x67, 0x7c, 0x37, 0x6a, 0x5b, 0x46, + 0x4c, 0xa7, 0x98, 0x6f, 0xb9, 0x22, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03, 0x03, 0x03, + 0x02, 0x03, 0x01, 0x00, 0x0d, 0x00, 0x18, 0x00, 0x16, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, + 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, 0x02, 0x01, 0x00, + 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x1c, 0x00, 0x02, 0x40, 0x01, 0x00, 0x15, 0x00, 0x96, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +#define PKT_MAXDUMP 32 + +static uint8_t zeropkt[DPI_DESYNC_MAX_FAKE_LEN]; + +void desync_init() +{ + memset(zeropkt, 0, sizeof(zeropkt)); +} + + +bool desync_valid_first_stage(enum dpi_desync_mode mode) +{ + return mode==DESYNC_FAKE || mode==DESYNC_RST || mode==DESYNC_RSTACK; +} +bool desync_valid_second_stage(enum dpi_desync_mode mode) +{ + return mode==DESYNC_NONE || mode==DESYNC_DISORDER || mode==DESYNC_DISORDER2 || mode==DESYNC_SPLIT || mode==DESYNC_SPLIT2; +} +enum dpi_desync_mode desync_mode_from_string(const char *s) +{ + if (!s) + return DESYNC_NONE; + else if (!strcmp(s,"fake")) + return DESYNC_FAKE; + else if (!strcmp(s,"rst")) + return DESYNC_RST; + else if (!strcmp(s,"rstack")) + return DESYNC_RSTACK; + else if (!strcmp(s,"disorder")) + return DESYNC_DISORDER; + else if (!strcmp(s,"disorder2")) + return DESYNC_DISORDER2; + else if (!strcmp(s,"split")) + return DESYNC_SPLIT; + else if (!strcmp(s,"split2")) + return DESYNC_SPLIT2; + return DESYNC_INVALID; +} + + +// auto creates internal socket and uses it for subsequent calls +static bool rawsend_rep(const struct sockaddr* dst,uint32_t fwmark,const void *data,size_t len) +{ + for (int i=0;ith_flags & TH_SYN) && len_payload) + { + struct sockaddr_storage src, dst; + const uint8_t *fake; + size_t fake_size; + char host[256]; + bool bHaveHost=false; + bool bIsHttp; + uint8_t *p, *phost; + + if ((bIsHttp = IsHttp(data_payload,len_payload))) + { + DLOG("packet contains HTTP request\n") + fake = params.fake_http; + fake_size = params.fake_http_size; + if (params.hostlist || params.debug) bHaveHost=HttpExtractHost(data_payload,len_payload,host,sizeof(host)); + if (params.hostlist && !bHaveHost) + { + DLOG("not applying tampering to HTTP without Host:\n") + return res; + } + } + else if (IsTLSClientHello(data_payload,len_payload)) + { + DLOG("packet contains TLS ClientHello\n") + fake = params.fake_tls; + fake_size = params.fake_tls_size; + if (params.hostlist || params.desync_skip_nosni || params.debug) + { + bHaveHost=TLSHelloExtractHost(data_payload,len_payload,host,sizeof(host)); + if (params.desync_skip_nosni && !bHaveHost) + { + DLOG("not applying tampering to TLS ClientHello without hostname in the SNI\n") + return res; + } + } + } + else + { + if (!params.desync_any_proto) return res; + DLOG("applying tampering to unknown protocol\n") + fake = zeropkt; + fake_size = 256; + } + + if (bHaveHost) + { + DLOG("hostname: %s\n",host) + if (params.hostlist && !SearchHostList(params.hostlist,host,params.debug)) + { + DLOG("not applying tampering to this request\n") + return res; + } + } + + if (bIsHttp && (params.hostcase || params.hostnospace || params.domcase) && (phost = (uint8_t*)memmem(data_payload, len_payload, "\r\nHost: ", 8))) + { + if (params.hostcase) + { + DLOG("modifying Host: => %c%c%c%c:\n", params.hostspell[0], params.hostspell[1], params.hostspell[2], params.hostspell[3]) + memcpy(phost + 2, params.hostspell, 4); + res=modify; + } + if (params.domcase) + { + DLOG("mixing domain case\n"); + for (p = phost+7; p < (data_payload + len_payload) && *p != '\r' && *p != '\n'; p++) + *p = (((size_t)p) & 1) ? tolower(*p) : toupper(*p); + res=modify; + } + uint8_t *pua; + if (params.hostnospace && + (pua = (uint8_t*)memmem(data_payload, len_payload, "\r\nUser-Agent: ", 14)) && + (pua = (uint8_t*)memmem(pua + 1, len_payload - (pua - data_payload) - 1, "\r\n", 2))) + { + DLOG("removing space after Host: and adding it to User-Agent:\n") + if (pua > phost) + { + memmove(phost + 7, phost + 8, pua - phost - 8); + phost[pua - phost - 1] = ' '; + } + else + { + memmove(pua + 1, pua, phost - pua + 7); + *pua = ' '; + } + res=modify; + } + } + + if (params.desync_mode==DESYNC_NONE) return res; + + extract_endpoints(ip, ip6hdr, tcphdr, &src, &dst); + if (params.debug) + { + printf("dpi desync src="); + print_sockaddr((struct sockaddr *)&src); + printf(" dst="); + print_sockaddr((struct sockaddr *)&dst); + printf("\n"); + } + + uint8_t newdata[DPI_DESYNC_MAX_FAKE_LEN+100]; + size_t newlen; + uint8_t ttl_orig = ip ? ip->ip_ttl : ip6hdr->ip6_ctlun.ip6_un1.ip6_un1_hlim; + uint8_t ttl_fake = params.desync_ttl ? params.desync_ttl : ttl_orig; + uint8_t flags_orig = *((uint8_t*)tcphdr+13); + uint32_t *timestamps = tcp_find_timestamps(tcphdr); + enum dpi_desync_mode desync_mode = params.desync_mode; + bool b; + + newlen = sizeof(newdata); + b = false; + switch(desync_mode) + { + case DESYNC_FAKE: + if (!prepare_tcp_segment((struct sockaddr *)&src, (struct sockaddr *)&dst, flags_orig, tcphdr->th_seq, tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_fake,params.desync_tcp_fooling_mode, + fake, fake_size, newdata, &newlen)) + { + return res; + } + DLOG("sending fake request : "); + hexdump_limited_dlog(fake,fake_size,PKT_MAXDUMP); DLOG("\n") + b = true; + break; + case DESYNC_RST: + case DESYNC_RSTACK: + if (!prepare_tcp_segment((struct sockaddr *)&src, (struct sockaddr *)&dst, TH_RST | (desync_mode==DESYNC_RSTACK ? TH_ACK:0), tcphdr->th_seq, tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_fake,params.desync_tcp_fooling_mode, + NULL, 0, newdata, &newlen)) + { + return res; + } + DLOG("sending fake RST/RSTACK\n"); + b = true; + break; + } + + if (b) + { + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, newdata, newlen)) + return res; + if (params.desync_mode2==DESYNC_NONE) + { + if (params.desync_retrans) + { + DLOG("dropping original packet to force retransmission. len=%zu len_payload=%zu\n", len_pkt, len_payload) + } + else + { + DLOG("reinjecting original packet. len=%zu len_payload=%zu\n", len_pkt, len_payload) + #ifdef __FreeBSD__ + // FreeBSD tend to pass ipv6 frames with wrong checksum + if (res==modify || ip6hdr) + #else + // if original packet was tampered earlier it needs checksum fixed + if (res==modify) + #endif + tcp_fix_checksum(tcphdr,len_tcp,ip,ip6hdr); + if (!rawsend((struct sockaddr *)&dst, params.desync_fwmark, data_pkt, len_pkt)) + return res; + } + return drop; + } + desync_mode = params.desync_mode2; + } + + newlen = sizeof(newdata); + switch(desync_mode) + { + case DESYNC_DISORDER: + case DESYNC_DISORDER2: + { + size_t split_pos=len_payload>params.desync_split_pos ? params.desync_split_pos : 1; + uint8_t fakeseg[DPI_DESYNC_MAX_FAKE_LEN+100]; + size_t fakeseg_len; + + if (split_posth_seq,split_pos), tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_orig,TCP_FOOL_NONE, + data_payload+split_pos, len_payload-split_pos, newdata, &newlen)) + return res; + DLOG("sending 2nd out-of-order tcp segment %zu-%zu len=%zu : ",split_pos,len_payload-1, len_payload-split_pos) + hexdump_limited_dlog(data_payload+split_pos,len_payload-split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, newdata, newlen)) + return res; + } + + + if (desync_mode==DESYNC_DISORDER) + { + fakeseg_len = sizeof(fakeseg); + if (!prepare_tcp_segment((struct sockaddr *)&src, (struct sockaddr *)&dst, flags_orig, tcphdr->th_seq, tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_fake,params.desync_tcp_fooling_mode, + zeropkt, split_pos, fakeseg, &fakeseg_len)) + return res; + DLOG("sending fake(1) 1st out-of-order tcp segment 0-%zu len=%zu : ",split_pos-1, split_pos) + hexdump_limited_dlog(zeropkt,split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, fakeseg, fakeseg_len)) + return res; + } + + + newlen = sizeof(newdata); + if (!prepare_tcp_segment((struct sockaddr *)&src, (struct sockaddr *)&dst, flags_orig, tcphdr->th_seq, tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_orig,TCP_FOOL_NONE, + data_payload, split_pos, newdata, &newlen)) + return res; + DLOG("sending 1st out-of-order tcp segment 0-%zu len=%zu : ",split_pos-1, split_pos) + hexdump_limited_dlog(data_payload,split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, newdata, newlen)) + return res; + + if (desync_mode==DESYNC_DISORDER) + { + DLOG("sending fake(2) 1st out-of-order tcp segment 0-%zu len=%zu : ",split_pos-1, split_pos) + hexdump_limited_dlog(zeropkt,split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, fakeseg, fakeseg_len)) + return res; + } + + return drop; + } + break; + case DESYNC_SPLIT: + case DESYNC_SPLIT2: + { + size_t split_pos=len_payload>params.desync_split_pos ? params.desync_split_pos : 1; + uint8_t fakeseg[DPI_DESYNC_MAX_FAKE_LEN+100]; + size_t fakeseg_len; + + if (desync_mode==DESYNC_SPLIT) + { + fakeseg_len = sizeof(fakeseg); + if (!prepare_tcp_segment((struct sockaddr *)&src, (struct sockaddr *)&dst, flags_orig, tcphdr->th_seq, tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_fake,params.desync_tcp_fooling_mode, + zeropkt, split_pos, fakeseg, &fakeseg_len)) + return res; + DLOG("sending fake(1) 1st tcp segment 0-%zu len=%zu : ",split_pos-1, split_pos) + hexdump_limited_dlog(zeropkt,split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, fakeseg, fakeseg_len)) + return res; + } + + newlen = sizeof(newdata); + if (!prepare_tcp_segment((struct sockaddr *)&src, (struct sockaddr *)&dst, flags_orig, tcphdr->th_seq, tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_orig,TCP_FOOL_NONE, + data_payload, split_pos, newdata, &newlen)) + return res; + DLOG("sending 1st tcp segment 0-%zu len=%zu : ",split_pos-1, split_pos) + hexdump_limited_dlog(data_payload,split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, newdata, newlen)) + return res; + + if (desync_mode==DESYNC_SPLIT) + { + DLOG("sending fake(2) 1st tcp segment 0-%zu len=%zu : ",split_pos-1, split_pos) + hexdump_limited_dlog(zeropkt,split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, fakeseg, fakeseg_len)) + return res; + } + + if (split_posth_seq,split_pos), tcphdr->th_ack, tcphdr->th_win, timestamps, + ttl_orig,TCP_FOOL_NONE, + data_payload+split_pos, len_payload-split_pos, newdata, &newlen)) + return res; + DLOG("sending 2nd tcp segment %zu-%zu len=%zu : ",split_pos,len_payload-1, len_payload-split_pos) + hexdump_limited_dlog(data_payload+split_pos,len_payload-split_pos,PKT_MAXDUMP); DLOG("\n") + if (!rawsend_rep((struct sockaddr *)&dst, params.desync_fwmark, newdata, newlen)) + return res; + } + + return drop; + } + break; + } + + return res; + } + + return res; +} diff --git a/nfq/desync.h b/nfq/desync.h new file mode 100644 index 0000000..912739a --- /dev/null +++ b/nfq/desync.h @@ -0,0 +1,41 @@ +#pragma once + +#include "darkmagic.h" +#include "nfqws.h" + +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#define DPI_DESYNC_FWMARK_DEFAULT 0x40000000 +#else +#define DPI_DESYNC_FWMARK_DEFAULT 512 +#endif + +#define DPI_DESYNC_MAX_FAKE_LEN 1500 + +enum dpi_desync_mode { + DESYNC_NONE=0, + DESYNC_INVALID, + DESYNC_FAKE, + DESYNC_RST, + DESYNC_RSTACK, + DESYNC_DISORDER, + DESYNC_DISORDER2, + DESYNC_SPLIT, + DESYNC_SPLIT2 +}; + +extern const char *fake_http_request_default; +extern const uint8_t fake_tls_clienthello_default[517]; + +enum dpi_desync_mode desync_mode_from_string(const char *s); +bool desync_valid_first_stage(enum dpi_desync_mode mode); +bool desync_valid_second_stage(enum dpi_desync_mode mode); + +void desync_init(); +packet_process_result dpi_desync_packet(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); diff --git a/nfq/gzip.c b/nfq/gzip.c new file mode 100644 index 0000000..cb46670 --- /dev/null +++ b/nfq/gzip.c @@ -0,0 +1,82 @@ +#include "gzip.h" +#include +#include +#include + +#define ZCHUNK 16384 +#define BUFMIN 128 +#define BUFCHUNK (1024*128) + +int z_readfile(FILE *F, char **buf, size_t *size) +{ + z_stream zs; + int r; + unsigned char in[ZCHUNK]; + size_t bufsize; + void *newbuf; + + memset(&zs, 0, sizeof(zs)); + + *buf = NULL; + bufsize = *size = 0; + + r = inflateInit2(&zs, 47); + if (r != Z_OK) return r; + + do + { + zs.avail_in = fread(in, 1, sizeof(in), F); + if (ferror(F)) + { + r = Z_ERRNO; + goto zerr; + } + if (!zs.avail_in) break; + zs.next_in = in; + do + { + if ((bufsize - *size) < BUFMIN) + { + bufsize += BUFCHUNK; + newbuf = *buf ? realloc(*buf, bufsize) : malloc(bufsize); + if (!newbuf) + { + r = Z_MEM_ERROR; + goto zerr; + } + *buf = newbuf; + } + zs.avail_out = bufsize - *size; + zs.next_out = (unsigned char*)(*buf + *size); + r = inflate(&zs, Z_NO_FLUSH); + if (r != Z_OK && r != Z_STREAM_END) goto zerr; + *size = bufsize - zs.avail_out; + } while (r == Z_OK && zs.avail_in); + } while (r == Z_OK); + + if (*size < bufsize) + { + // free extra space + if ((newbuf = realloc(*buf, *size))) *buf = newbuf; + } + + inflateEnd(&zs); + return Z_OK; + +zerr: + inflateEnd(&zs); + if (*buf) + { + free(*buf); + *buf = NULL; + } + return r; +} + +bool is_gzip(FILE* F) +{ + unsigned char magic[2]; + bool b = !fseek(F, 0, SEEK_SET) && fread(magic, 1, 2, F) == 2 && magic[0] == 0x1F && magic[1] == 0x8B; + fseek(F, 0, SEEK_SET); + return b; +} diff --git a/nfq/gzip.h b/nfq/gzip.h new file mode 100644 index 0000000..15e30d2 --- /dev/null +++ b/nfq/gzip.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include +#include + +int z_readfile(FILE *F,char **buf,size_t *size); +bool is_gzip(FILE* F); diff --git a/nfq/helpers.c b/nfq/helpers.c new file mode 100644 index 0000000..b2443cc --- /dev/null +++ b/nfq/helpers.c @@ -0,0 +1,120 @@ +#define _GNU_SOURCE + +#include "helpers.h" +#include +#include +#include +#include + +void hexdump_limited_dlog(const uint8_t *data, size_t size, size_t limit) +{ + size_t k; + bool bcut=false; + if (size>limit) + { + size=limit; + bcut = true; + } + if (!size) return; + for (k=0;k=0x20 && data[k]<=0x7F ? (char)data[k] : '.'); + if (bcut) DLOG(" ..."); +} + +char *strncasestr(const char *s,const char *find, size_t slen) +{ + char c, sc; + size_t len; + + if ((c = *find++) != '\0') + { + len = strlen(find); + do + { + do + { + if (slen-- < 1 || (sc = *s++) == '\0') return NULL; + } while (toupper(c) != toupper(sc)); + if (len > slen) return NULL; + } while (strncasecmp(s, find, len) != 0); + s--; + } + return (char *)s; +} + +bool load_file(const char *filename,void *buffer,size_t *buffer_size) +{ + FILE *F; + + F = fopen(filename,"rb"); + if (!F) return false; + + *buffer_size = fread(buffer,1,*buffer_size,F); + if (ferror(F)) + { + fclose(F); + return false; + } + + fclose(F); + return true; +} +bool load_file_nonempty(const char *filename,void *buffer,size_t *buffer_size) +{ + bool b = load_file(filename,buffer,buffer_size); + return b && *buffer_size; +} + + + +void print_sockaddr(const struct sockaddr *sa) +{ + char str[64]; + switch (sa->sa_family) + { + case AF_INET: + if (inet_ntop(sa->sa_family, &((struct sockaddr_in*)sa)->sin_addr, str, sizeof(str))) + printf("%s:%d", str, ntohs(((struct sockaddr_in*)sa)->sin_port)); + break; + case AF_INET6: + if (inet_ntop(sa->sa_family, &((struct sockaddr_in6*)sa)->sin6_addr, str, sizeof(str))) + printf("[%s]:%d", str, ntohs(((struct sockaddr_in6*)sa)->sin6_port)); + break; + default: + printf("UNKNOWN_FAMILY_%d", sa->sa_family); + } +} + +void dbgprint_socket_buffers(int fd) +{ + if (params.debug) + { + int v; + socklen_t sz; + sz=sizeof(int); + if (!getsockopt(fd,SOL_SOCKET,SO_RCVBUF,&v,&sz)) + DLOG("fd=%d SO_RCVBUF=%d\n",fd,v) + sz=sizeof(int); + if (!getsockopt(fd,SOL_SOCKET,SO_SNDBUF,&v,&sz)) + DLOG("fd=%d SO_SNDBUF=%d\n",fd,v) + } +} +bool set_socket_buffers(int fd, int rcvbuf, int sndbuf) +{ + DLOG("set_socket_buffers fd=%d rcvbuf=%d sndbuf=%d\n",fd,rcvbuf,sndbuf) + if (rcvbuf && setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(int)) <0) + { + perror("setsockopt (SO_RCVBUF): "); + close(fd); + return false; + } + if (sndbuf && setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(int)) <0) + { + perror("setsockopt (SO_SNDBUF): "); + close(fd); + return false; + } + dbgprint_socket_buffers(fd); + return true; +} diff --git a/nfq/helpers.h b/nfq/helpers.h new file mode 100644 index 0000000..fac7c24 --- /dev/null +++ b/nfq/helpers.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "params.h" + +void hexdump_limited_dlog(const uint8_t *data, size_t size, size_t limit); +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); + +void print_sockaddr(const struct sockaddr *sa); +void dbgprint_socket_buffers(int fd); +bool set_socket_buffers(int fd, int rcvbuf, int sndbuf); diff --git a/nfq/hostlist.c b/nfq/hostlist.c new file mode 100644 index 0000000..b4136d2 --- /dev/null +++ b/nfq/hostlist.c @@ -0,0 +1,112 @@ +#include +#include "hostlist.h" +#include "gzip.h" + + +static bool addpool(strpool **hostlist, char **s, char *end) +{ + char *p; + + // advance until eol lowering all chars + for (p = *s; p +#include "strpool.h" + +bool LoadHostList(strpool **hostlist, char *filename); +bool SearchHostList(strpool *hostlist, const char *host,bool debug); diff --git a/nfq/nfqws b/nfq/nfqws new file mode 120000 index 0000000..2c1b593 --- /dev/null +++ b/nfq/nfqws @@ -0,0 +1 @@ +../binaries/x86_64/nfqws \ No newline at end of file diff --git a/nfq/nfqws.c b/nfq/nfqws.c new file mode 100644 index 0000000..9564303 --- /dev/null +++ b/nfq/nfqws.c @@ -0,0 +1,809 @@ +#define _GNU_SOURCE + +#include "nfqws.h" +#include "sec.h" +#include "desync.h" +#include "helpers.h" +#include "checksum.h" +#include "params.h" +#include "protocol.h" +#include "hostlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#define NF_DROP 0 +#define NF_ACCEPT 1 +#endif + +#ifndef IPPROTO_DIVERT +#define IPPROTO_DIVERT 258 +#endif + + +struct params_s params; + + +static bool bHup = false; +static void onhup(int sig) +{ + printf("HUP received !\n"); + if (params.hostlist) + printf("Will reload hostlist on next request\n"); + bHup = true; +} +// should be called in normal execution +static void dohup() +{ + if (bHup) + { + if (params.hostlist) + { + if (!LoadHostList(¶ms.hostlist, params.hostfile)) + { + // what will we do without hostlist ?? sure, gonna die + exit(1); + } + } + bHup = false; + } +} + + + +static void tcp_rewrite_winsize(struct tcphdr *tcp, uint16_t winsize) +{ + uint16_t winsize_old; + winsize_old = htons(tcp->th_win); // << scale_factor; + tcp->th_win = htons(winsize); + DLOG("Window size change %u => %u\n", winsize_old, winsize) +} + +// data/len points to data payload +static bool modify_tcp_packet(uint8_t *data, size_t len, struct tcphdr *tcphdr) +{ + if (tcp_synack_segment(tcphdr) && params.wsize) + { + tcp_rewrite_winsize(tcphdr, (uint16_t)params.wsize); + return true; + } + return false; +} + + + +#ifdef __linux__ +static packet_process_result processPacketData(uint8_t *data_pkt, size_t len_pkt, uint32_t *mark) +#else +static packet_process_result processPacketData(uint8_t *data_pkt, size_t len_pkt) +#endif +{ + struct ip *ip = NULL; + struct ip6_hdr *ip6hdr = NULL; + struct tcphdr *tcphdr = NULL; + size_t len = len_pkt, len_tcp; + uint8_t *data = data_pkt; + packet_process_result res = pass, res2; + uint8_t proto; + +#ifdef __linux__ + if (*mark & params.desync_fwmark) + { + DLOG("ignoring generated packet\n") + return res; + } +#endif + + if (proto_check_ipv4(data, len)) + { + ip = (struct ip *) data; + proto = ip->ip_p; + proto_skip_ipv4(&data, &len); + if (params.debug) + { + printf("IP4: "); + print_ip(ip); + } + } + else if (proto_check_ipv6(data, len)) + { + ip6hdr = (struct ip6_hdr *) data; + proto_skip_ipv6(&data, &len, &proto); + if (params.debug) + { + printf("IP6: "); + print_ip6hdr(ip6hdr, proto); + } + } + else + { + // not ipv6 and not ipv4 + return res; + } + + if (proto==IPPROTO_TCP && proto_check_tcp(data, len)) + { + tcphdr = (struct tcphdr *) data; + len_tcp = len; + proto_skip_tcp(&data, &len); + + if (params.debug) + { + printf(" "); + print_tcphdr(tcphdr); + printf("\n"); + } + + if (len) { DLOG("TCP: ") hexdump_limited_dlog(data, len, 32); DLOG("\n") } + + if (modify_tcp_packet(data, len, tcphdr)) + res = modify; + + res2 = dpi_desync_packet(data_pkt, len_pkt, ip, ip6hdr, tcphdr, len_tcp, data, len); + res = (res2==pass && res==modify) ? modify : res2; + // in my FreeBSD divert tests only ipv4 packets were reinjected with correct checksum + // ipv6 packets were with incorrect checksum +#ifdef __FreeBSD__ + // FreeBSD tend to pass ipv6 frames with wrong checksum + if (res==modify || ip6hdr) +#else + if (res==modify) +#endif + tcp_fix_checksum(tcphdr,len_tcp,ip,ip6hdr); + } + else + { + if (params.debug) printf("\n"); + } + + return res; +} + + +#ifdef __linux__ +static int nfq_cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, + struct nfq_data *nfa, void *cookie) +{ + int id; + size_t len; + struct nfqnl_msg_packet_hdr *ph; + uint8_t *data; + + ph = nfq_get_msg_packet_hdr(nfa); + id = ph ? ntohl(ph->packet_id) : 0; + + uint32_t mark = nfq_get_nfmark(nfa); + len = nfq_get_payload(nfa, &data); + DLOG("packet: id=%d len=%zu\n", id, len) + if (len >= 0) + { + switch (processPacketData(data, len, &mark)) + { + case modify: + DLOG("packet: id=%d pass modified\n", id); + return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, len, data); + case drop: + DLOG("packet: id=%d drop\n", id); + return nfq_set_verdict2(qh, id, NF_DROP, mark, 0, NULL); + } + } + DLOG("packet: id=%d pass unmodified\n", id); + return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, 0, NULL); +} +static int nfq_main() +{ + struct nfq_handle *h = NULL; + struct nfq_q_handle *qh = NULL; + int fd,rv; + uint8_t buf[16384] __attribute__((aligned)); + + printf("opening library handle\n"); + h = nfq_open(); + if (!h) { + perror("nfq_open() :"); + goto exiterr; + } + + printf("unbinding existing nf_queue handler for AF_INET (if any)\n"); + if (nfq_unbind_pf(h, AF_INET) < 0) { + perror("nfq_unbind_pf() :"); + goto exiterr; + } + + printf("binding nfnetlink_queue as nf_queue handler for AF_INET\n"); + if (nfq_bind_pf(h, AF_INET) < 0) { + perror("nfq_bind_pf() :"); + goto exiterr; + } + + printf("binding this socket to queue '%u'\n", params.qnum); + qh = nfq_create_queue(h, params.qnum, &nfq_cb, ¶ms); + if (!qh) { + perror("nfq_create_queue() :"); + goto exiterr; + } + + printf("setting copy_packet mode\n"); + if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) < 0) { + perror("can't set packet_copy mode :"); + goto exiterr; + } + if (nfq_set_queue_maxlen(qh, Q_MAXLEN) < 0) { + perror("can't set queue maxlen : "); + goto exiterr; + } + // accept packets if they cant be handled + if (nfq_set_queue_flags(qh, NFQA_CFG_F_FAIL_OPEN , NFQA_CFG_F_FAIL_OPEN)) + { + fprintf(stderr, "can't set queue flags. its OK on linux <3.6\n"); + // dot not fail. not supported on old linuxes <3.6 + } + + if (params.droproot && !droproot(params.uid, params.gid)) + goto exiterr; + print_id(); + + signal(SIGHUP, onhup); + + desync_init(); + + fd = nfq_fd(h); + + // increase socket buffer size. on slow systems reloading hostlist can take a while. + // if too many unhandled packets are received its possible to get "no buffer space available" error + if (!set_socket_buffers(fd,Q_RCVBUF/2,Q_SNDBUF/2)) + goto exiterr; + do + { + while ((rv = recv(fd, buf, sizeof(buf), 0)) && rv >= 0) + { + dohup(); + int r = nfq_handle_packet(h, buf, rv); + if (r) fprintf(stderr, "nfq_handle_packet error %d\n", r); + } + fprintf(stderr, "recv: errno %d\n",errno); + perror("recv"); + // do not fail on ENOBUFS + } while(errno==ENOBUFS); + + printf("unbinding from queue 0\n"); + nfq_destroy_queue(qh); + +#ifdef INSANE + /* normally, applications SHOULD NOT issue this command, since + * it detaches other programs/sockets from AF_INET, too ! */ + printf("unbinding from AF_INET\n"); + nfq_unbind_pf(h, AF_INET); +#endif + + printf("closing library handle\n"); + nfq_close(h); + return 0; + +exiterr: + if (qh) nfq_destroy_queue(qh); + if (h) nfq_close(h); + return 1; +} + +#elif defined(BSD) + +static int dvt_main() +{ + uint8_t buf[16384] __attribute__((aligned)); + struct sockaddr_storage sa_from; + int fd[2] = {-1,-1}; // 4,6 + int i,r,res=1,fdct=1,fdmax; + unsigned int id=0; + socklen_t socklen; + ssize_t rd,wr; + packet_process_result ppr; + fd_set fdset; + + { + struct sockaddr_in bp4; + bp4.sin_family = AF_INET; + bp4.sin_port = htons(params.port); + bp4.sin_addr.s_addr = INADDR_ANY; + + printf("creating divert4 socket\n"); + fd[0] = socket(AF_INET, SOCK_RAW, IPPROTO_DIVERT); + if (fd[0] == -1) { + perror("socket (DIVERT4): "); + goto exiterr; + } + printf("binding divert4 socket\n"); + if (bind(fd[0], (struct sockaddr*)&bp4, sizeof(bp4)) < 0) + { + perror("bind (DIVERT4): "); + goto exiterr; + } + if (!set_socket_buffers(fd[0],Q_RCVBUF,Q_SNDBUF)) + goto exiterr; + } + + +#ifdef __OpenBSD__ + { + // in OpenBSD must use separate divert sockets for ipv4 and ipv6 + struct sockaddr_in6 bp6; + memset(&bp6,0,sizeof(bp6)); + bp6.sin6_family = AF_INET6; + bp6.sin6_port = htons(params.port); + + printf("creating divert6 socket\n"); + fd[1] = socket(AF_INET6, SOCK_RAW, IPPROTO_DIVERT); + if (fd[1] == -1) { + perror("socket (DIVERT6): "); + goto exiterr; + } + printf("binding divert6 socket\n"); + if (bind(fd[1], (struct sockaddr*)&bp6, sizeof(bp6)) < 0) + { + perror("bind (DIVERT6): "); + goto exiterr; + } + fdct++; + if (!set_socket_buffers(fd[1],Q_RCVBUF,Q_SNDBUF)) + goto exiterr; + } +#endif + fdmax = (fd[0]>fd[1] ? fd[0] : fd[1]) + 1; + + printf("initializing raw sockets with sockarg 0x%08X (%u)\n", params.desync_fwmark, params.desync_fwmark); + if (!rawsend_preinit(params.desync_fwmark)) + goto exiterr; + + if (params.droproot && !droproot(params.uid, params.gid)) + goto exiterr; + print_id(); + + signal(SIGHUP, onhup); + + desync_init(); + + for(;;) + { + FD_ZERO(&fdset); + for(i=0;i0) + { + DLOG("packet: id=%u len=%zd\n", id, rd) + ppr = processPacketData(buf, rd); + switch (ppr) + { + case pass: + case modify: + DLOG(ppr==pass ? "packet: id=%u reinject unmodified\n" : "packet: id=%u reinject modified\n", id); + wr = sendto(fd[i], buf, rd, 0, (struct sockaddr*)&sa_from, socklen); + if (wr<0) + perror("reinject sendto: "); + else if (wr!=rd) + fprintf(stderr,"reinject sendto: not all data was reinjected. received %zd, sent %zd\n", rd, wr); + break; + default: + DLOG("packet: id=%u drop\n", id); + } + id++; + } + else + { + DLOG("unexpected zero size recvfrom\n") + } + } + } + } + + res=0; +exiterr: + if (fd[0]!=-1) close(fd[0]); + if (fd[1]!=-1) close(fd[1]); + return res; +} + +#endif + + + +static void exithelp() +{ + printf( + " --debug=0|1\n" +#ifdef __linux__ + " --qnum=\n" +#elif defined(BSD) + " --port=\t\t\t\t; divert port\n" +#endif + " --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" + " --wsize=\t\t\t; set window size. 0 = do not modify. OBSOLETE !\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" + " --hostnospace\t\t\t\t; remove space after Host: and add it to User-Agent: to preserve packet size\n" + " --domcase\t\t\t\t; mix domain case : Host: TeSt.cOm\n" + " --dpi-desync=[,]\t\t; try to desync dpi state. modes : fake rst rstack disorder disorder2 split split2\n" +#ifdef __linux__ + " --dpi-desync-fwmark=\t; override fwmark for desync packet. default = 0x%08X (%u)\n" +#elif defined(SO_USER_COOKIE) + " --dpi-desync-sockarg=\t; override sockarg (SO_USER_COOKIE) for desync packet. default = 0x%08X (%u)\n" +#endif + " --dpi-desync-ttl=\t\t\t; set ttl for desync packet\n" + " --dpi-desync-fooling=[,]\t; can use multiple comma separated values. modes : none md5sig ts badseq badsum\n" +#ifdef __linux__ + " --dpi-desync-retrans=0|1\t\t; 0(default)=reinject original data packet after fake 1=drop original data packet to force its retransmission\n" +#endif + " --dpi-desync-repeats=\t\t; send every desync packet N times\n" + " --dpi-desync-skip-nosni=0|1\t\t; 1(default)=do not act on ClientHello without SNI (ESNI ?)\n" + " --dpi-desync-split-pos=<1..%u>\t; (for disorder only) split TCP packet at specified position\n" + " --dpi-desync-any-protocol=0|1\t\t; 0(default)=desync only http and tls 1=desync any nonempty data packet\n" + " --dpi-desync-fake-http=\t; file containing fake http request\n" + " --dpi-desync-fake-tls=\t; file containing fake TLS ClientHello (for https)\n" + " --hostlist=\t\t\t; apply dpi desync only to the listed hosts (one host per line, subdomains auto apply)\n", +#if defined(__linux__) || defined(SO_USER_COOKIE) + DPI_DESYNC_FWMARK_DEFAULT,DPI_DESYNC_FWMARK_DEFAULT, +#endif + DPI_DESYNC_MAX_FAKE_LEN + ); + exit(1); +} + +static void cleanup_params() +{ + if (params.hostlist) + { + StrPoolDestroy(¶ms.hostlist); + params.hostlist = NULL; + } +} +static void exithelp_clean() +{ + cleanup_params(); + exithelp(); +} +static void exit_clean(int code) +{ + cleanup_params(); + exit(code); +} + +int main(int argc, char **argv) +{ + int result, v; + int option_index = 0; + bool daemon = false; + char pidfile[256]; + + srandom(time(NULL)); + + memset(¶ms, 0, sizeof(params)); + memcpy(params.hostspell, "host", 4); // default hostspell + *pidfile = 0; + + params.desync_fwmark = DPI_DESYNC_FWMARK_DEFAULT; + params.desync_skip_nosni = true; + params.desync_split_pos = 3; + params.desync_repeats = 1; + params.fake_tls_size = sizeof(fake_tls_clienthello_default); + memcpy(params.fake_tls,fake_tls_clienthello_default,params.fake_tls_size); + params.fake_http_size = strlen(fake_http_request_default); + memcpy(params.fake_http,fake_http_request_default,params.fake_http_size); + if (can_drop_root()) // are we root ? + { + params.uid = params.gid = 0x7FFFFFFF; // default uid:gid + params.droproot = true; + } + + const struct option long_options[] = { + {"debug",optional_argument,0,0}, // optidx=0 +#ifdef __linux__ + {"qnum",required_argument,0,0}, // optidx=1 +#elif defined(BSD) + {"port",required_argument,0,0}, // optidx=1 +#else + {"disabled_argument_1",no_argument,0,0},// optidx=1 +#endif + {"daemon",no_argument,0,0}, // optidx=2 + {"pidfile",required_argument,0,0}, // optidx=3 + {"user",required_argument,0,0 }, // optidx=4 + {"uid",required_argument,0,0 }, // optidx=5 + {"wsize",required_argument,0,0}, // optidx=6 + {"hostcase",no_argument,0,0}, // optidx=7 + {"hostspell",required_argument,0,0}, // optidx=8 + {"hostnospace",no_argument,0,0}, // optidx=9 + {"domcase",no_argument,0,0 }, // optidx=10 + {"dpi-desync",required_argument,0,0}, // optidx=11 +#ifdef __linux__ + {"dpi-desync-fwmark",required_argument,0,0}, // optidx=12 +#elif defined(SO_USER_COOKIE) + {"dpi-desync-sockarg",required_argument,0,0}, // optidx=12 +#else + {"disabled_argument_2",no_argument,0,0}, // optidx=12 +#endif + {"dpi-desync-ttl",required_argument,0,0}, // optidx=13 + {"dpi-desync-fooling",required_argument,0,0}, // optidx=14 + {"dpi-desync-retrans",optional_argument,0,0}, // optidx=15 + {"dpi-desync-repeats",required_argument,0,0}, // optidx=16 + {"dpi-desync-skip-nosni",optional_argument,0,0},// optidx=17 + {"dpi-desync-split-pos",required_argument,0,0},// optidx=18 + {"dpi-desync-any-protocol",optional_argument,0,0},// optidx=19 + {"dpi-desync-fake-http",required_argument,0,0},// optidx=20 + {"dpi-desync-fake-tls",required_argument,0,0},// optidx=21 + {"hostlist",required_argument,0,0}, // optidx=22 + {NULL,0,NULL,0} + }; + if (argc < 2) exithelp(); + while ((v = getopt_long_only(argc, argv, "", long_options, &option_index)) != -1) + { + if (v) exithelp(); + switch (option_index) + { + case 0: /* debug */ + params.debug = !optarg || atoi(optarg); + break; + case 1: /* qnum or port */ +#ifdef __linux__ + params.qnum = atoi(optarg); + if (params.qnum < 0 || params.qnum>65535) + { + fprintf(stderr, "bad qnum\n"); + exit_clean(1); + } +#elif defined(BSD) + { + int i = atoi(optarg); + if (i <= 0 || i > 65535) + { + fprintf(stderr, "bad port number\n"); + exit_clean(1); + } + params.port = (uint16_t)i; + } +#endif + break; + case 2: /* daemon */ + daemon = true; + break; + case 3: /* pidfile */ + strncpy(pidfile, optarg, sizeof(pidfile)); + pidfile[sizeof(pidfile) - 1] = '\0'; + break; + case 4: /* user */ + { + struct passwd *pwd = getpwnam(optarg); + if (!pwd) + { + fprintf(stderr, "non-existent username supplied\n"); + exit_clean(1); + } + params.uid = pwd->pw_uid; + params.gid = pwd->pw_gid; + params.droproot = true; + break; + } + case 5: /* uid */ + params.gid = 0x7FFFFFFF; // default gid. drop gid=0 + params.droproot = true; + if (!sscanf(optarg, "%u:%u", ¶ms.uid, ¶ms.gid)) + { + fprintf(stderr, "--uid should be : uid[:gid]\n"); + exit_clean(1); + } + break; + case 6: /* wsize */ + params.wsize = atoi(optarg); + if (params.wsize < 0 || params.wsize>65535) + { + fprintf(stderr, "bad wsize\n"); + exit_clean(1); + } + break; + case 7: /* hostcase */ + params.hostcase = true; + break; + case 8: /* hostspell */ + if (strlen(optarg) != 4) + { + fprintf(stderr, "hostspell must be exactly 4 chars long\n"); + exit_clean(1); + } + params.hostcase = true; + memcpy(params.hostspell, optarg, 4); + break; + case 9: /* hostnospace */ + params.hostnospace = true; + break; + case 10: /* domcase */ + params.domcase = true; + break; + case 11: /* dpi-desync */ + { + char *mode2; + mode2 = optarg ? strchr(optarg,',') : NULL; + if (mode2) *mode2++=0; + + params.desync_mode = desync_mode_from_string(optarg); + params.desync_mode2 = desync_mode_from_string(mode2); + if (params.desync_mode==DESYNC_NONE || params.desync_mode==DESYNC_INVALID || params.desync_mode2==DESYNC_INVALID) + { + fprintf(stderr, "invalid dpi-desync mode\n"); + exit_clean(1); + } + if (params.desync_mode2 && !(desync_valid_first_stage(params.desync_mode) && desync_valid_second_stage(params.desync_mode2))) + { + fprintf(stderr, "invalid desync combo : %s+%s\n", optarg,mode2); + exit_clean(1); + } + } + break; + case 12: /* dpi-desync-fwmark/dpi-desync-sockarg */ +#if defined(__linux__) || defined(SO_USER_COOKIE) + params.desync_fwmark = 0; + if (!sscanf(optarg, "0x%X", ¶ms.desync_fwmark)) sscanf(optarg, "%u", ¶ms.desync_fwmark); + if (!params.desync_fwmark) + { + fprintf(stderr, "fwmark/sockarg should be decimal or 0xHEX and should not be zero\n"); + exit_clean(1); + } +#else + fprintf(stderr, "fmwark/sockarg not supported in this OS\n"); + exit_clean(1); +#endif + break; + case 13: /* dpi-desync-ttl */ + params.desync_ttl = (uint8_t)atoi(optarg); + break; + case 14: /* dpi-desync-fooling */ + { + char *e,*p = optarg; + while (p) + { + e = strchr(p,','); + if (e) *e++=0; + if (!strcmp(p,"md5sig")) + params.desync_tcp_fooling_mode |= TCP_FOOL_MD5SIG; + else if (!strcmp(p,"ts")) + params.desync_tcp_fooling_mode |= TCP_FOOL_TS; + else if (!strcmp(p,"badsum")) + { + #ifdef __OpenBSD__ + printf("\nWARNING !!! OpenBSD may forcibly recompute tcp checksums !!! In this case badsum fooling will not work.\nYou should check tcp checksum correctness in tcpdump manually before using badsum.\n\n"); + #endif + params.desync_tcp_fooling_mode |= TCP_FOOL_BADSUM; + } + else if (!strcmp(p,"badseq")) + params.desync_tcp_fooling_mode |= TCP_FOOL_BADSEQ; + else if (strcmp(p,"none")) + { + fprintf(stderr, "dpi-desync-fooling allowed values : none,md5sig,ts,badseq,badsum\n"); + exit_clean(1); + } + p = e; + } + } + break; + case 15: /* dpi-desync-retrans */ +#ifdef __linux__ + params.desync_retrans = !optarg || atoi(optarg); +#else + fprintf(stderr, "dpi-desync-retrans is only supported in linux\n"); + exit_clean(1); +#endif + break; + case 16: /* dpi-desync-repeats */ + params.desync_repeats = atoi(optarg); + if (params.desync_repeats<=0 || params.desync_repeats>20) + { + fprintf(stderr, "dpi-desync-repeats must be within 1..20\n"); + exit_clean(1); + } + break; + case 17: /* dpi-desync-skip-nosni */ + params.desync_skip_nosni = !optarg || atoi(optarg); + break; + case 18: /* dpi-desync-split-pos */ + params.desync_split_pos = atoi(optarg); + if (params.desync_split_pos<1 || params.desync_split_pos>DPI_DESYNC_MAX_FAKE_LEN) + { + fprintf(stderr, "dpi-desync-split-pos must be within 1..%u range\n",DPI_DESYNC_MAX_FAKE_LEN); + exit_clean(1); + } + break; + case 19: /* dpi-desync-any-protocol */ + params.desync_any_proto = !optarg || atoi(optarg); + break; + case 20: /* dpi-desync-fake-http */ + params.fake_http_size = sizeof(params.fake_http); + if (!load_file_nonempty(optarg,params.fake_http,¶ms.fake_http_size)) + { + fprintf(stderr, "could not read %s\n",optarg); + exit_clean(1); + } + break; + case 21: /* dpi-desync-fake-tls */ + params.fake_tls_size = sizeof(params.fake_tls); + if (!load_file_nonempty(optarg,params.fake_tls,¶ms.fake_tls_size)) + { + fprintf(stderr, "could not read %s\n",optarg); + exit_clean(1); + } + break; + case 22: /* hostlist */ + if (!LoadHostList(¶ms.hostlist, optarg)) + exit_clean(1); + strncpy(params.hostfile,optarg,sizeof(params.hostfile)); + params.hostfile[sizeof(params.hostfile)-1]='\0'; + break; + } + } +#ifdef BSD + if (!params.port) + { + fprintf(stderr, "Need port number\n"); + exit_clean(1); + } +#endif + + if (daemon) daemonize(); + + if (*pidfile && !writepid(pidfile)) + { + fprintf(stderr, "could not write pidfile\n"); + goto exiterr; + } + +#ifdef __linux__ + result = nfq_main(); +#elif defined(BSD) + result = dvt_main(); +#else + #error unsupported OS +#endif +ex: + rawsend_cleanup(); + cleanup_params(); + return result; +exiterr: + result = 1; + goto ex; +} diff --git a/nfq/nfqws.h b/nfq/nfqws.h new file mode 100644 index 0000000..216bb9e --- /dev/null +++ b/nfq/nfqws.h @@ -0,0 +1,6 @@ +#pragma once + +typedef enum +{ + pass = 0, modify, drop +} packet_process_result; diff --git a/nfq/params.h b/nfq/params.h new file mode 100644 index 0000000..688c5ac --- /dev/null +++ b/nfq/params.h @@ -0,0 +1,54 @@ +#pragma once + +#include "params.h" +#include "strpool.h" +#include "desync.h" + +#include +#include +#include +#include +#include + +#if defined(__OpenBSD__) || defined (__APPLE__) +// divert-packet also diverts return traffic. sockets will experience high load +#define Q_RCVBUF (256*1024) // in bytes +#define Q_SNDBUF (256*1024) // in bytes +#define RAW_SNDBUF (64*1024) // in bytes +#else +#define Q_RCVBUF (128*1024) // in bytes +#define Q_SNDBUF (64*1024) // in bytes +#define RAW_SNDBUF (64*1024) // in bytes +#endif + +#define Q_MAXLEN 1024 // in packets + +struct params_s +{ + bool debug; + int wsize; +#ifdef __linux__ + int qnum; +#elif defined(BSD) + uint16_t port; // divert port +#endif + bool hostcase, hostnospace, domcase; + char hostspell[4]; + enum dpi_desync_mode desync_mode,desync_mode2; + bool desync_retrans,desync_skip_nosni,desync_any_proto; + int desync_repeats,desync_split_pos; + uint8_t desync_ttl; + uint8_t desync_tcp_fooling_mode; + uint32_t desync_fwmark; // unused in BSD + char hostfile[256]; + strpool *hostlist; + uint8_t fake_http[1460],fake_tls[1460]; + size_t fake_http_size,fake_tls_size; + bool droproot; + uid_t uid; + gid_t gid; +}; + +extern struct params_s params; + +#define DLOG(format, ...) {if (params.debug) printf(format, ##__VA_ARGS__);} diff --git a/nfq/protocol.c b/nfq/protocol.c new file mode 100644 index 0000000..b5b1404 --- /dev/null +++ b/nfq/protocol.c @@ -0,0 +1,132 @@ +#define _GNU_SOURCE + +#include "protocol.h" +#include "helpers.h" +#include +#include +#include +#include + +const char *http_methods[] = { "GET /","POST /","HEAD /","OPTIONS /","PUT /","DELETE /","CONNECT /","TRACE /",NULL }; +bool IsHttp(const uint8_t *data, size_t len) +{ + const char **method; + size_t method_len; + for (method = http_methods; *method; method++) + { + method_len = strlen(*method); + if (method_len <= len && !memcmp(data, *method, method_len)) + return true; + } + return false; +} +bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host) +{ + const uint8_t *p, *s, *e=data+len; + + p = (uint8_t*)strncasestr((char*)data, "\nHost:", len); + if (!p) return false; + p+=6; + while(pp) + { + size_t slen = s-p; + if (host && len_host) + { + if (slen>=len_host) slen=len_host-1; + for(size_t i=0;i=6 && data[0]==0x16 && data[1]==0x03 && data[2]==0x01 && data[5]==0x01 && (ntohs(*(uint16_t*)(data+3))+5)<=len; +} +bool TLSFindExt(const uint8_t *data, size_t len, uint16_t type, const uint8_t **ext, size_t *len_ext) +{ + // +0 + // u8 ContentType: Handshake + // u16 Version: TLS1.0 + // u16 Length + // +5 + // u8 HandshakeType: ClientHello + // u24 Length + // u16 Version + // c[32] random + // u8 SessionIDLength + // + // u16 CipherSuitesLength + // + // u8 CompressionMethodsLength + // + // u16 ExtensionsLength + + size_t l,ll; + + l = 1+2+2+1+3+2+32; + // SessionIDLength + if (len<(l+1)) return false; + ll = data[6]<<16 | data[7]<<8 | data[8]; // HandshakeProtocol length + if (len<(ll+9)) return false; + l += data[l]+1; + // CipherSuitesLength + if (len<(l+2)) return false; + l += ntohs(*(uint16_t*)(data+l))+2; + // CompressionMethodsLength + if (len<(l+1)) return false; + l += data[l]+1; + // ExtensionsLength + if (len<(l+2)) return false; + + data+=l; len-=l; + l=ntohs(*(uint16_t*)data); + data+=2; len-=2; + if (l=4) + { + uint16_t etype=*(uint16_t*)data; + size_t elen=ntohs(*(uint16_t*)(data+2)); + data+=4; l-=4; + if (l=len_host) slen=len_host-1; + for(size_t i=0;i +#include +#include + +bool IsHttp(const uint8_t *data, size_t len); +bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_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/nfq/sec.c b/nfq/sec.c new file mode 100644 index 0000000..6733b92 --- /dev/null +++ b/nfq/sec.c @@ -0,0 +1,171 @@ +#define _GNU_SOURCE + +#include +#include +#include "sec.h" +#include +#include +#include + +#ifdef __linux__ +#include + +bool checkpcap(uint64_t caps) +{ + if (!caps) return true; // no special caps reqd + + struct __user_cap_header_struct ch = {_LINUX_CAPABILITY_VERSION_3, getpid()}; + struct __user_cap_data_struct cd[2]; + uint32_t c0 = (uint32_t)caps; + uint32_t c1 = (uint32_t)(caps>>32); + + return !capget(&ch,cd) && (cd[0].effective & c0)==c0 && (cd[1].effective & c1)==c1; +} +bool setpcap(uint64_t caps) +{ + struct __user_cap_header_struct ch = {_LINUX_CAPABILITY_VERSION_3, getpid()}; + struct __user_cap_data_struct cd[2]; + + cd[0].effective = cd[0].permitted = (uint32_t)caps; + cd[0].inheritable = 0; + cd[1].effective = cd[1].permitted = (uint32_t)(caps>>32); + cd[1].inheritable = 0; + + return !capset(&ch,cd); +} +int getmaxcap() +{ + int maxcap = CAP_LAST_CAP; + FILE *F = fopen("/proc/sys/kernel/cap_last_cap", "r"); + if (F) + { + int n = fscanf(F, "%d", &maxcap); + fclose(F); + } + return maxcap; + +} +bool dropcaps() +{ + uint64_t caps = (1< +#include + +#ifdef __linux__ + +#include + +bool checkpcap(uint64_t caps); +bool setpcap(uint64_t caps); +int getmaxcap(); +bool dropcaps(); +#endif + +bool can_drop_root(); +bool droproot(uid_t uid, gid_t gid); +void print_id(); +void daemonize(); +bool writepid(const char *filename); diff --git a/nfq/strpool.c b/nfq/strpool.c new file mode 100644 index 0000000..6649763 --- /dev/null +++ b/nfq/strpool.c @@ -0,0 +1,76 @@ +#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; +} diff --git a/nfq/strpool.h b/nfq/strpool.h new file mode 100644 index 0000000..5932ba3 --- /dev/null +++ b/nfq/strpool.h @@ -0,0 +1,19 @@ +#pragma once + +#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); diff --git a/nfq/uthash.h b/nfq/uthash.h new file mode 100644 index 0000000..f34c1f9 --- /dev/null +++ b/nfq/uthash.h @@ -0,0 +1,1217 @@ +/* +Copyright (c) 2003-2018, Troy D. Hanson http://troydhanson.github.com/uthash/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef UTHASH_H +#define UTHASH_H + +#define UTHASH_VERSION 2.0.2 + +#include /* memcmp, memset, strlen */ +#include /* ptrdiff_t */ +#include /* exit */ + +/* These macros use decltype or the earlier __typeof GNU extension. + As decltype is only available in newer compilers (VS2010 or gcc 4.3+ + when compiling c++ source) this code uses whatever method is needed + or, for VS2008 where neither is available, uses casting workarounds. */ +#if !defined(DECLTYPE) && !defined(NO_DECLTYPE) +#if defined(_MSC_VER) /* MS compiler */ +#if _MSC_VER >= 1600 && defined(__cplusplus) /* VS2010 or newer in C++ mode */ +#define DECLTYPE(x) (decltype(x)) +#else /* VS2008 or older (or VS2010 in C mode) */ +#define NO_DECLTYPE +#endif +#elif defined(__BORLANDC__) || defined(__ICCARM__) || defined(__LCC__) || defined(__WATCOMC__) +#define NO_DECLTYPE +#else /* GNU, Sun and other compilers */ +#define DECLTYPE(x) (__typeof(x)) +#endif +#endif + +#ifdef NO_DECLTYPE +#define DECLTYPE(x) +#define DECLTYPE_ASSIGN(dst,src) \ +do { \ + char **_da_dst = (char**)(&(dst)); \ + *_da_dst = (char*)(src); \ +} while (0) +#else +#define DECLTYPE_ASSIGN(dst,src) \ +do { \ + (dst) = DECLTYPE(dst)(src); \ +} while (0) +#endif + +/* a number of the hash function use uint32_t which isn't defined on Pre VS2010 */ +#if defined(_WIN32) +#if defined(_MSC_VER) && _MSC_VER >= 1600 +#include +#elif defined(__WATCOMC__) || defined(__MINGW32__) || defined(__CYGWIN__) +#include +#else +typedef unsigned int uint32_t; +typedef unsigned char uint8_t; +#endif +#elif defined(__GNUC__) && !defined(__VXWORKS__) +#include +#else +typedef unsigned int uint32_t; +typedef unsigned char uint8_t; +#endif + +#ifndef uthash_malloc +#define uthash_malloc(sz) malloc(sz) /* malloc fcn */ +#endif +#ifndef uthash_free +#define uthash_free(ptr,sz) free(ptr) /* free fcn */ +#endif +#ifndef uthash_bzero +#define uthash_bzero(a,n) memset(a,'\0',n) +#endif +#ifndef uthash_memcmp +#define uthash_memcmp(a,b,n) memcmp(a,b,n) +#endif +#ifndef uthash_strlen +#define uthash_strlen(s) strlen(s) +#endif + +#ifndef uthash_noexpand_fyi +#define uthash_noexpand_fyi(tbl) /* can be defined to log noexpand */ +#endif +#ifndef uthash_expand_fyi +#define uthash_expand_fyi(tbl) /* can be defined to log expands */ +#endif + +#ifndef HASH_NONFATAL_OOM +#define HASH_NONFATAL_OOM 0 +#endif + +#if HASH_NONFATAL_OOM +/* malloc failures can be recovered from */ + +#ifndef uthash_nonfatal_oom +#define uthash_nonfatal_oom(obj) do {} while (0) /* non-fatal OOM error */ +#endif + +#define HASH_RECORD_OOM(oomed) do { (oomed) = 1; } while (0) +#define IF_HASH_NONFATAL_OOM(x) x + +#else +/* malloc failures result in lost memory, hash tables are unusable */ + +#ifndef uthash_fatal +#define uthash_fatal(msg) exit(-1) /* fatal OOM error */ +#endif + +#define HASH_RECORD_OOM(oomed) uthash_fatal("out of memory") +#define IF_HASH_NONFATAL_OOM(x) + +#endif + +/* initial number of buckets */ +#define HASH_INITIAL_NUM_BUCKETS 32U /* initial number of buckets */ +#define HASH_INITIAL_NUM_BUCKETS_LOG2 5U /* lg2 of initial number of buckets */ +#define HASH_BKT_CAPACITY_THRESH 10U /* expand when bucket count reaches */ + +/* calculate the element whose hash handle address is hhp */ +#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho))) +/* calculate the hash handle from element address elp */ +#define HH_FROM_ELMT(tbl,elp) ((UT_hash_handle *)(((char*)(elp)) + ((tbl)->hho))) + +#define HASH_ROLLBACK_BKT(hh, head, itemptrhh) \ +do { \ + struct UT_hash_handle *_hd_hh_item = (itemptrhh); \ + unsigned _hd_bkt; \ + HASH_TO_BKT(_hd_hh_item->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \ + (head)->hh.tbl->buckets[_hd_bkt].count++; \ + _hd_hh_item->hh_next = NULL; \ + _hd_hh_item->hh_prev = NULL; \ +} while (0) + +#define HASH_VALUE(keyptr,keylen,hashv) \ +do { \ + HASH_FCN(keyptr, keylen, hashv); \ +} while (0) + +#define HASH_FIND_BYHASHVALUE(hh,head,keyptr,keylen,hashval,out) \ +do { \ + (out) = NULL; \ + if (head) { \ + unsigned _hf_bkt; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _hf_bkt); \ + if (HASH_BLOOM_TEST((head)->hh.tbl, hashval) != 0) { \ + HASH_FIND_IN_BKT((head)->hh.tbl, hh, (head)->hh.tbl->buckets[ _hf_bkt ], keyptr, keylen, hashval, out); \ + } \ + } \ +} while (0) + +#define HASH_FIND(hh,head,keyptr,keylen,out) \ +do { \ + unsigned _hf_hashv; \ + HASH_VALUE(keyptr, keylen, _hf_hashv); \ + HASH_FIND_BYHASHVALUE(hh, head, keyptr, keylen, _hf_hashv, out); \ +} while (0) + +#ifdef HASH_BLOOM +#define HASH_BLOOM_BITLEN (1UL << HASH_BLOOM) +#define HASH_BLOOM_BYTELEN (HASH_BLOOM_BITLEN/8UL) + (((HASH_BLOOM_BITLEN%8UL)!=0UL) ? 1UL : 0UL) +#define HASH_BLOOM_MAKE(tbl,oomed) \ +do { \ + (tbl)->bloom_nbits = HASH_BLOOM; \ + (tbl)->bloom_bv = (uint8_t*)uthash_malloc(HASH_BLOOM_BYTELEN); \ + if (!(tbl)->bloom_bv) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \ + (tbl)->bloom_sig = HASH_BLOOM_SIGNATURE; \ + } \ +} while (0) + +#define HASH_BLOOM_FREE(tbl) \ +do { \ + uthash_free((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \ +} while (0) + +#define HASH_BLOOM_BITSET(bv,idx) (bv[(idx)/8U] |= (1U << ((idx)%8U))) +#define HASH_BLOOM_BITTEST(bv,idx) (bv[(idx)/8U] & (1U << ((idx)%8U))) + +#define HASH_BLOOM_ADD(tbl,hashv) \ + HASH_BLOOM_BITSET((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U))) + +#define HASH_BLOOM_TEST(tbl,hashv) \ + HASH_BLOOM_BITTEST((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U))) + +#else +#define HASH_BLOOM_MAKE(tbl,oomed) +#define HASH_BLOOM_FREE(tbl) +#define HASH_BLOOM_ADD(tbl,hashv) +#define HASH_BLOOM_TEST(tbl,hashv) (1) +#define HASH_BLOOM_BYTELEN 0U +#endif + +#define HASH_MAKE_TABLE(hh,head,oomed) \ +do { \ + (head)->hh.tbl = (UT_hash_table*)uthash_malloc(sizeof(UT_hash_table)); \ + if (!(head)->hh.tbl) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head)->hh.tbl->tail = &((head)->hh); \ + (head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS; \ + (head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2; \ + (head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head); \ + (head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc( \ + HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \ + (head)->hh.tbl->signature = HASH_SIGNATURE; \ + if (!(head)->hh.tbl->buckets) { \ + HASH_RECORD_OOM(oomed); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + } else { \ + uthash_bzero((head)->hh.tbl->buckets, \ + HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \ + HASH_BLOOM_MAKE((head)->hh.tbl, oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (oomed) { \ + uthash_free((head)->hh.tbl->buckets, \ + HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + } \ + ) \ + } \ + } \ +} while (0) + +#define HASH_REPLACE_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,replaced,cmpfcn) \ +do { \ + (replaced) = NULL; \ + HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \ + if (replaced) { \ + HASH_DELETE(hh, head, replaced); \ + } \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn); \ +} while (0) + +#define HASH_REPLACE_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add,replaced) \ +do { \ + (replaced) = NULL; \ + HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \ + if (replaced) { \ + HASH_DELETE(hh, head, replaced); \ + } \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add); \ +} while (0) + +#define HASH_REPLACE(hh,head,fieldname,keylen_in,add,replaced) \ +do { \ + unsigned _hr_hashv; \ + HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \ + HASH_REPLACE_BYHASHVALUE(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced); \ +} while (0) + +#define HASH_REPLACE_INORDER(hh,head,fieldname,keylen_in,add,replaced,cmpfcn) \ +do { \ + unsigned _hr_hashv; \ + HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \ + HASH_REPLACE_BYHASHVALUE_INORDER(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced, cmpfcn); \ +} while (0) + +#define HASH_APPEND_LIST(hh, head, add) \ +do { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail); \ + (head)->hh.tbl->tail->next = (add); \ + (head)->hh.tbl->tail = &((add)->hh); \ +} while (0) + +#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \ +do { \ + do { \ + if (cmpfcn(DECLTYPE(head)(_hs_iter), add) > 0) { \ + break; \ + } \ + } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \ +} while (0) + +#ifdef NO_DECLTYPE +#undef HASH_AKBI_INNER_LOOP +#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \ +do { \ + char *_hs_saved_head = (char*)(head); \ + do { \ + DECLTYPE_ASSIGN(head, _hs_iter); \ + if (cmpfcn(head, add) > 0) { \ + DECLTYPE_ASSIGN(head, _hs_saved_head); \ + break; \ + } \ + DECLTYPE_ASSIGN(head, _hs_saved_head); \ + } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \ +} while (0) +#endif + +#if HASH_NONFATAL_OOM + +#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \ +do { \ + if (!(oomed)) { \ + unsigned _ha_bkt; \ + (head)->hh.tbl->num_items++; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \ + HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \ + if (oomed) { \ + HASH_ROLLBACK_BKT(hh, head, &(add)->hh); \ + HASH_DELETE_HH(hh, head, &(add)->hh); \ + (add)->hh.tbl = NULL; \ + uthash_nonfatal_oom(add); \ + } else { \ + HASH_BLOOM_ADD((head)->hh.tbl, hashval); \ + HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \ + } \ + } else { \ + (add)->hh.tbl = NULL; \ + uthash_nonfatal_oom(add); \ + } \ +} while (0) + +#else + +#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \ +do { \ + unsigned _ha_bkt; \ + (head)->hh.tbl->num_items++; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \ + HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \ + HASH_BLOOM_ADD((head)->hh.tbl, hashval); \ + HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \ +} while (0) + +#endif + + +#define HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh,head,keyptr,keylen_in,hashval,add,cmpfcn) \ +do { \ + IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \ + (add)->hh.hashv = (hashval); \ + (add)->hh.key = (char*) (keyptr); \ + (add)->hh.keylen = (unsigned) (keylen_in); \ + if (!(head)) { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = NULL; \ + HASH_MAKE_TABLE(hh, add, _ha_oomed); \ + IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \ + (head) = (add); \ + IF_HASH_NONFATAL_OOM( } ) \ + } else { \ + void *_hs_iter = (head); \ + (add)->hh.tbl = (head)->hh.tbl; \ + HASH_AKBI_INNER_LOOP(hh, head, add, cmpfcn); \ + if (_hs_iter) { \ + (add)->hh.next = _hs_iter; \ + if (((add)->hh.prev = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev)) { \ + HH_FROM_ELMT((head)->hh.tbl, (add)->hh.prev)->next = (add); \ + } else { \ + (head) = (add); \ + } \ + HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev = (add); \ + } else { \ + HASH_APPEND_LIST(hh, head, add); \ + } \ + } \ + HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \ + HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE_INORDER"); \ +} while (0) + +#define HASH_ADD_KEYPTR_INORDER(hh,head,keyptr,keylen_in,add,cmpfcn) \ +do { \ + unsigned _hs_hashv; \ + HASH_VALUE(keyptr, keylen_in, _hs_hashv); \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, keyptr, keylen_in, _hs_hashv, add, cmpfcn); \ +} while (0) + +#define HASH_ADD_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,cmpfcn) \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn) + +#define HASH_ADD_INORDER(hh,head,fieldname,keylen_in,add,cmpfcn) \ + HASH_ADD_KEYPTR_INORDER(hh, head, &((add)->fieldname), keylen_in, add, cmpfcn) + +#define HASH_ADD_KEYPTR_BYHASHVALUE(hh,head,keyptr,keylen_in,hashval,add) \ +do { \ + IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \ + (add)->hh.hashv = (hashval); \ + (add)->hh.key = (char*) (keyptr); \ + (add)->hh.keylen = (unsigned) (keylen_in); \ + if (!(head)) { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = NULL; \ + HASH_MAKE_TABLE(hh, add, _ha_oomed); \ + IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \ + (head) = (add); \ + IF_HASH_NONFATAL_OOM( } ) \ + } else { \ + (add)->hh.tbl = (head)->hh.tbl; \ + HASH_APPEND_LIST(hh, head, add); \ + } \ + HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \ + HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE"); \ +} while (0) + +#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add) \ +do { \ + unsigned _ha_hashv; \ + HASH_VALUE(keyptr, keylen_in, _ha_hashv); \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, keyptr, keylen_in, _ha_hashv, add); \ +} while (0) + +#define HASH_ADD_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add) \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add) + +#define HASH_ADD(hh,head,fieldname,keylen_in,add) \ + HASH_ADD_KEYPTR(hh, head, &((add)->fieldname), keylen_in, add) + +#define HASH_TO_BKT(hashv,num_bkts,bkt) \ +do { \ + bkt = ((hashv) & ((num_bkts) - 1U)); \ +} while (0) + +/* delete "delptr" from the hash table. + * "the usual" patch-up process for the app-order doubly-linked-list. + * The use of _hd_hh_del below deserves special explanation. + * These used to be expressed using (delptr) but that led to a bug + * if someone used the same symbol for the head and deletee, like + * HASH_DELETE(hh,users,users); + * We want that to work, but by changing the head (users) below + * we were forfeiting our ability to further refer to the deletee (users) + * in the patch-up process. Solution: use scratch space to + * copy the deletee pointer, then the latter references are via that + * scratch pointer rather than through the repointed (users) symbol. + */ +#define HASH_DELETE(hh,head,delptr) \ + HASH_DELETE_HH(hh, head, &(delptr)->hh) + +#define HASH_DELETE_HH(hh,head,delptrhh) \ +do { \ + struct UT_hash_handle *_hd_hh_del = (delptrhh); \ + if ((_hd_hh_del->prev == NULL) && (_hd_hh_del->next == NULL)) { \ + HASH_BLOOM_FREE((head)->hh.tbl); \ + uthash_free((head)->hh.tbl->buckets, \ + (head)->hh.tbl->num_buckets * sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head) = NULL; \ + } else { \ + unsigned _hd_bkt; \ + if (_hd_hh_del == (head)->hh.tbl->tail) { \ + (head)->hh.tbl->tail = HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev); \ + } \ + if (_hd_hh_del->prev != NULL) { \ + HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev)->next = _hd_hh_del->next; \ + } else { \ + DECLTYPE_ASSIGN(head, _hd_hh_del->next); \ + } \ + if (_hd_hh_del->next != NULL) { \ + HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->next)->prev = _hd_hh_del->prev; \ + } \ + HASH_TO_BKT(_hd_hh_del->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \ + HASH_DEL_IN_BKT((head)->hh.tbl->buckets[_hd_bkt], _hd_hh_del); \ + (head)->hh.tbl->num_items--; \ + } \ + HASH_FSCK(hh, head, "HASH_DELETE_HH"); \ +} while (0) + +/* convenience forms of HASH_FIND/HASH_ADD/HASH_DEL */ +#define HASH_FIND_STR(head,findstr,out) \ +do { \ + unsigned _uthash_hfstr_keylen = (unsigned)uthash_strlen(findstr); \ + HASH_FIND(hh, head, findstr, _uthash_hfstr_keylen, out); \ +} while (0) +#define HASH_ADD_STR(head,strfield,add) \ +do { \ + unsigned _uthash_hastr_keylen = (unsigned)uthash_strlen((add)->strfield); \ + HASH_ADD(hh, head, strfield[0], _uthash_hastr_keylen, add); \ +} while (0) +#define HASH_REPLACE_STR(head,strfield,add,replaced) \ +do { \ + unsigned _uthash_hrstr_keylen = (unsigned)uthash_strlen((add)->strfield); \ + HASH_REPLACE(hh, head, strfield[0], _uthash_hrstr_keylen, add, replaced); \ +} while (0) +#define HASH_FIND_INT(head,findint,out) \ + HASH_FIND(hh,head,findint,sizeof(int),out) +#define HASH_ADD_INT(head,intfield,add) \ + HASH_ADD(hh,head,intfield,sizeof(int),add) +#define HASH_REPLACE_INT(head,intfield,add,replaced) \ + HASH_REPLACE(hh,head,intfield,sizeof(int),add,replaced) +#define HASH_FIND_PTR(head,findptr,out) \ + HASH_FIND(hh,head,findptr,sizeof(void *),out) +#define HASH_ADD_PTR(head,ptrfield,add) \ + HASH_ADD(hh,head,ptrfield,sizeof(void *),add) +#define HASH_REPLACE_PTR(head,ptrfield,add,replaced) \ + HASH_REPLACE(hh,head,ptrfield,sizeof(void *),add,replaced) +#define HASH_DEL(head,delptr) \ + HASH_DELETE(hh,head,delptr) + +/* HASH_FSCK checks hash integrity on every add/delete when HASH_DEBUG is defined. + * This is for uthash developer only; it compiles away if HASH_DEBUG isn't defined. + */ +#ifdef HASH_DEBUG +#define HASH_OOPS(...) do { fprintf(stderr,__VA_ARGS__); exit(-1); } while (0) +#define HASH_FSCK(hh,head,where) \ +do { \ + struct UT_hash_handle *_thh; \ + if (head) { \ + unsigned _bkt_i; \ + unsigned _count = 0; \ + char *_prev; \ + for (_bkt_i = 0; _bkt_i < (head)->hh.tbl->num_buckets; ++_bkt_i) { \ + unsigned _bkt_count = 0; \ + _thh = (head)->hh.tbl->buckets[_bkt_i].hh_head; \ + _prev = NULL; \ + while (_thh) { \ + if (_prev != (char*)(_thh->hh_prev)) { \ + HASH_OOPS("%s: invalid hh_prev %p, actual %p\n", \ + (where), (void*)_thh->hh_prev, (void*)_prev); \ + } \ + _bkt_count++; \ + _prev = (char*)(_thh); \ + _thh = _thh->hh_next; \ + } \ + _count += _bkt_count; \ + if ((head)->hh.tbl->buckets[_bkt_i].count != _bkt_count) { \ + HASH_OOPS("%s: invalid bucket count %u, actual %u\n", \ + (where), (head)->hh.tbl->buckets[_bkt_i].count, _bkt_count); \ + } \ + } \ + if (_count != (head)->hh.tbl->num_items) { \ + HASH_OOPS("%s: invalid hh item count %u, actual %u\n", \ + (where), (head)->hh.tbl->num_items, _count); \ + } \ + _count = 0; \ + _prev = NULL; \ + _thh = &(head)->hh; \ + while (_thh) { \ + _count++; \ + if (_prev != (char*)_thh->prev) { \ + HASH_OOPS("%s: invalid prev %p, actual %p\n", \ + (where), (void*)_thh->prev, (void*)_prev); \ + } \ + _prev = (char*)ELMT_FROM_HH((head)->hh.tbl, _thh); \ + _thh = (_thh->next ? HH_FROM_ELMT((head)->hh.tbl, _thh->next) : NULL); \ + } \ + if (_count != (head)->hh.tbl->num_items) { \ + HASH_OOPS("%s: invalid app item count %u, actual %u\n", \ + (where), (head)->hh.tbl->num_items, _count); \ + } \ + } \ +} while (0) +#else +#define HASH_FSCK(hh,head,where) +#endif + +/* When compiled with -DHASH_EMIT_KEYS, length-prefixed keys are emitted to + * the descriptor to which this macro is defined for tuning the hash function. + * The app can #include to get the prototype for write(2). */ +#ifdef HASH_EMIT_KEYS +#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) \ +do { \ + unsigned _klen = fieldlen; \ + write(HASH_EMIT_KEYS, &_klen, sizeof(_klen)); \ + write(HASH_EMIT_KEYS, keyptr, (unsigned long)fieldlen); \ +} while (0) +#else +#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) +#endif + +/* default to Jenkin's hash unless overridden e.g. DHASH_FUNCTION=HASH_SAX */ +#ifdef HASH_FUNCTION +#define HASH_FCN HASH_FUNCTION +#else +#define HASH_FCN HASH_JEN +#endif + +/* The Bernstein hash function, used in Perl prior to v5.6. Note (x<<5+x)=x*33. */ +#define HASH_BER(key,keylen,hashv) \ +do { \ + unsigned _hb_keylen = (unsigned)keylen; \ + const unsigned char *_hb_key = (const unsigned char*)(key); \ + (hashv) = 0; \ + while (_hb_keylen-- != 0U) { \ + (hashv) = (((hashv) << 5) + (hashv)) + *_hb_key++; \ + } \ +} while (0) + + +/* SAX/FNV/OAT/JEN hash functions are macro variants of those listed at + * http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx */ +#define HASH_SAX(key,keylen,hashv) \ +do { \ + unsigned _sx_i; \ + const unsigned char *_hs_key = (const unsigned char*)(key); \ + hashv = 0; \ + for (_sx_i=0; _sx_i < keylen; _sx_i++) { \ + hashv ^= (hashv << 5) + (hashv >> 2) + _hs_key[_sx_i]; \ + } \ +} while (0) +/* FNV-1a variation */ +#define HASH_FNV(key,keylen,hashv) \ +do { \ + unsigned _fn_i; \ + const unsigned char *_hf_key = (const unsigned char*)(key); \ + (hashv) = 2166136261U; \ + for (_fn_i=0; _fn_i < keylen; _fn_i++) { \ + hashv = hashv ^ _hf_key[_fn_i]; \ + hashv = hashv * 16777619U; \ + } \ +} while (0) + +#define HASH_OAT(key,keylen,hashv) \ +do { \ + unsigned _ho_i; \ + const unsigned char *_ho_key=(const unsigned char*)(key); \ + hashv = 0; \ + for(_ho_i=0; _ho_i < keylen; _ho_i++) { \ + hashv += _ho_key[_ho_i]; \ + hashv += (hashv << 10); \ + hashv ^= (hashv >> 6); \ + } \ + hashv += (hashv << 3); \ + hashv ^= (hashv >> 11); \ + hashv += (hashv << 15); \ +} while (0) + +#define HASH_JEN_MIX(a,b,c) \ +do { \ + a -= b; a -= c; a ^= ( c >> 13 ); \ + b -= c; b -= a; b ^= ( a << 8 ); \ + c -= a; c -= b; c ^= ( b >> 13 ); \ + a -= b; a -= c; a ^= ( c >> 12 ); \ + b -= c; b -= a; b ^= ( a << 16 ); \ + c -= a; c -= b; c ^= ( b >> 5 ); \ + a -= b; a -= c; a ^= ( c >> 3 ); \ + b -= c; b -= a; b ^= ( a << 10 ); \ + c -= a; c -= b; c ^= ( b >> 15 ); \ +} while (0) + +#define HASH_JEN(key,keylen,hashv) \ +do { \ + unsigned _hj_i,_hj_j,_hj_k; \ + unsigned const char *_hj_key=(unsigned const char*)(key); \ + hashv = 0xfeedbeefu; \ + _hj_i = _hj_j = 0x9e3779b9u; \ + _hj_k = (unsigned)(keylen); \ + while (_hj_k >= 12U) { \ + _hj_i += (_hj_key[0] + ( (unsigned)_hj_key[1] << 8 ) \ + + ( (unsigned)_hj_key[2] << 16 ) \ + + ( (unsigned)_hj_key[3] << 24 ) ); \ + _hj_j += (_hj_key[4] + ( (unsigned)_hj_key[5] << 8 ) \ + + ( (unsigned)_hj_key[6] << 16 ) \ + + ( (unsigned)_hj_key[7] << 24 ) ); \ + hashv += (_hj_key[8] + ( (unsigned)_hj_key[9] << 8 ) \ + + ( (unsigned)_hj_key[10] << 16 ) \ + + ( (unsigned)_hj_key[11] << 24 ) ); \ + \ + HASH_JEN_MIX(_hj_i, _hj_j, hashv); \ + \ + _hj_key += 12; \ + _hj_k -= 12U; \ + } \ + hashv += (unsigned)(keylen); \ + switch ( _hj_k ) { \ + case 11: hashv += ( (unsigned)_hj_key[10] << 24 ); /* FALLTHROUGH */ \ + case 10: hashv += ( (unsigned)_hj_key[9] << 16 ); /* FALLTHROUGH */ \ + case 9: hashv += ( (unsigned)_hj_key[8] << 8 ); /* FALLTHROUGH */ \ + case 8: _hj_j += ( (unsigned)_hj_key[7] << 24 ); /* FALLTHROUGH */ \ + case 7: _hj_j += ( (unsigned)_hj_key[6] << 16 ); /* FALLTHROUGH */ \ + case 6: _hj_j += ( (unsigned)_hj_key[5] << 8 ); /* FALLTHROUGH */ \ + case 5: _hj_j += _hj_key[4]; /* FALLTHROUGH */ \ + case 4: _hj_i += ( (unsigned)_hj_key[3] << 24 ); /* FALLTHROUGH */ \ + case 3: _hj_i += ( (unsigned)_hj_key[2] << 16 ); /* FALLTHROUGH */ \ + case 2: _hj_i += ( (unsigned)_hj_key[1] << 8 ); /* FALLTHROUGH */ \ + case 1: _hj_i += _hj_key[0]; \ + } \ + HASH_JEN_MIX(_hj_i, _hj_j, hashv); \ +} while (0) + +/* The Paul Hsieh hash function */ +#undef get16bits +#if (defined(__GNUC__) && defined(__i386__)) || defined(__WATCOMC__) \ + || defined(_MSC_VER) || defined (__BORLANDC__) || defined (__TURBOC__) +#define get16bits(d) (*((const uint16_t *) (d))) +#endif + +#if !defined (get16bits) +#define get16bits(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8) \ + +(uint32_t)(((const uint8_t *)(d))[0]) ) +#endif +#define HASH_SFH(key,keylen,hashv) \ +do { \ + unsigned const char *_sfh_key=(unsigned const char*)(key); \ + uint32_t _sfh_tmp, _sfh_len = (uint32_t)keylen; \ + \ + unsigned _sfh_rem = _sfh_len & 3U; \ + _sfh_len >>= 2; \ + hashv = 0xcafebabeu; \ + \ + /* Main loop */ \ + for (;_sfh_len > 0U; _sfh_len--) { \ + hashv += get16bits (_sfh_key); \ + _sfh_tmp = ((uint32_t)(get16bits (_sfh_key+2)) << 11) ^ hashv; \ + hashv = (hashv << 16) ^ _sfh_tmp; \ + _sfh_key += 2U*sizeof (uint16_t); \ + hashv += hashv >> 11; \ + } \ + \ + /* Handle end cases */ \ + switch (_sfh_rem) { \ + case 3: hashv += get16bits (_sfh_key); \ + hashv ^= hashv << 16; \ + hashv ^= (uint32_t)(_sfh_key[sizeof (uint16_t)]) << 18; \ + hashv += hashv >> 11; \ + break; \ + case 2: hashv += get16bits (_sfh_key); \ + hashv ^= hashv << 11; \ + hashv += hashv >> 17; \ + break; \ + case 1: hashv += *_sfh_key; \ + hashv ^= hashv << 10; \ + hashv += hashv >> 1; \ + } \ + \ + /* Force "avalanching" of final 127 bits */ \ + hashv ^= hashv << 3; \ + hashv += hashv >> 5; \ + hashv ^= hashv << 4; \ + hashv += hashv >> 17; \ + hashv ^= hashv << 25; \ + hashv += hashv >> 6; \ +} while (0) + +#ifdef HASH_USING_NO_STRICT_ALIASING +/* The MurmurHash exploits some CPU's (x86,x86_64) tolerance for unaligned reads. + * For other types of CPU's (e.g. Sparc) an unaligned read causes a bus error. + * MurmurHash uses the faster approach only on CPU's where we know it's safe. + * + * Note the preprocessor built-in defines can be emitted using: + * + * gcc -m64 -dM -E - < /dev/null (on gcc) + * cc -## a.c (where a.c is a simple test file) (Sun Studio) + */ +#if (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86)) +#define MUR_GETBLOCK(p,i) p[i] +#else /* non intel */ +#define MUR_PLUS0_ALIGNED(p) (((unsigned long)p & 3UL) == 0UL) +#define MUR_PLUS1_ALIGNED(p) (((unsigned long)p & 3UL) == 1UL) +#define MUR_PLUS2_ALIGNED(p) (((unsigned long)p & 3UL) == 2UL) +#define MUR_PLUS3_ALIGNED(p) (((unsigned long)p & 3UL) == 3UL) +#define WP(p) ((uint32_t*)((unsigned long)(p) & ~3UL)) +#if (defined(__BIG_ENDIAN__) || defined(SPARC) || defined(__ppc__) || defined(__ppc64__)) +#define MUR_THREE_ONE(p) ((((*WP(p))&0x00ffffff) << 8) | (((*(WP(p)+1))&0xff000000) >> 24)) +#define MUR_TWO_TWO(p) ((((*WP(p))&0x0000ffff) <<16) | (((*(WP(p)+1))&0xffff0000) >> 16)) +#define MUR_ONE_THREE(p) ((((*WP(p))&0x000000ff) <<24) | (((*(WP(p)+1))&0xffffff00) >> 8)) +#else /* assume little endian non-intel */ +#define MUR_THREE_ONE(p) ((((*WP(p))&0xffffff00) >> 8) | (((*(WP(p)+1))&0x000000ff) << 24)) +#define MUR_TWO_TWO(p) ((((*WP(p))&0xffff0000) >>16) | (((*(WP(p)+1))&0x0000ffff) << 16)) +#define MUR_ONE_THREE(p) ((((*WP(p))&0xff000000) >>24) | (((*(WP(p)+1))&0x00ffffff) << 8)) +#endif +#define MUR_GETBLOCK(p,i) (MUR_PLUS0_ALIGNED(p) ? ((p)[i]) : \ + (MUR_PLUS1_ALIGNED(p) ? MUR_THREE_ONE(p) : \ + (MUR_PLUS2_ALIGNED(p) ? MUR_TWO_TWO(p) : \ + MUR_ONE_THREE(p)))) +#endif +#define MUR_ROTL32(x,r) (((x) << (r)) | ((x) >> (32 - (r)))) +#define MUR_FMIX(_h) \ +do { \ + _h ^= _h >> 16; \ + _h *= 0x85ebca6bu; \ + _h ^= _h >> 13; \ + _h *= 0xc2b2ae35u; \ + _h ^= _h >> 16; \ +} while (0) + +#define HASH_MUR(key,keylen,hashv) \ +do { \ + const uint8_t *_mur_data = (const uint8_t*)(key); \ + const int _mur_nblocks = (int)(keylen) / 4; \ + uint32_t _mur_h1 = 0xf88D5353u; \ + uint32_t _mur_c1 = 0xcc9e2d51u; \ + uint32_t _mur_c2 = 0x1b873593u; \ + uint32_t _mur_k1 = 0; \ + const uint8_t *_mur_tail; \ + const uint32_t *_mur_blocks = (const uint32_t*)(_mur_data+(_mur_nblocks*4)); \ + int _mur_i; \ + for (_mur_i = -_mur_nblocks; _mur_i != 0; _mur_i++) { \ + _mur_k1 = MUR_GETBLOCK(_mur_blocks,_mur_i); \ + _mur_k1 *= _mur_c1; \ + _mur_k1 = MUR_ROTL32(_mur_k1,15); \ + _mur_k1 *= _mur_c2; \ + \ + _mur_h1 ^= _mur_k1; \ + _mur_h1 = MUR_ROTL32(_mur_h1,13); \ + _mur_h1 = (_mur_h1*5U) + 0xe6546b64u; \ + } \ + _mur_tail = (const uint8_t*)(_mur_data + (_mur_nblocks*4)); \ + _mur_k1=0; \ + switch ((keylen) & 3U) { \ + case 0: break; \ + case 3: _mur_k1 ^= (uint32_t)_mur_tail[2] << 16; /* FALLTHROUGH */ \ + case 2: _mur_k1 ^= (uint32_t)_mur_tail[1] << 8; /* FALLTHROUGH */ \ + case 1: _mur_k1 ^= (uint32_t)_mur_tail[0]; \ + _mur_k1 *= _mur_c1; \ + _mur_k1 = MUR_ROTL32(_mur_k1,15); \ + _mur_k1 *= _mur_c2; \ + _mur_h1 ^= _mur_k1; \ + } \ + _mur_h1 ^= (uint32_t)(keylen); \ + MUR_FMIX(_mur_h1); \ + hashv = _mur_h1; \ +} while (0) +#endif /* HASH_USING_NO_STRICT_ALIASING */ + +/* iterate over items in a known bucket to find desired item */ +#define HASH_FIND_IN_BKT(tbl,hh,head,keyptr,keylen_in,hashval,out) \ +do { \ + if ((head).hh_head != NULL) { \ + DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (head).hh_head)); \ + } else { \ + (out) = NULL; \ + } \ + while ((out) != NULL) { \ + if ((out)->hh.hashv == (hashval) && (out)->hh.keylen == (keylen_in)) { \ + if (uthash_memcmp((out)->hh.key, keyptr, keylen_in) == 0) { \ + break; \ + } \ + } \ + if ((out)->hh.hh_next != NULL) { \ + DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (out)->hh.hh_next)); \ + } else { \ + (out) = NULL; \ + } \ + } \ +} while (0) + +/* add an item to a bucket */ +#define HASH_ADD_TO_BKT(head,hh,addhh,oomed) \ +do { \ + UT_hash_bucket *_ha_head = &(head); \ + _ha_head->count++; \ + (addhh)->hh_next = _ha_head->hh_head; \ + (addhh)->hh_prev = NULL; \ + if (_ha_head->hh_head != NULL) { \ + _ha_head->hh_head->hh_prev = (addhh); \ + } \ + _ha_head->hh_head = (addhh); \ + if ((_ha_head->count >= ((_ha_head->expand_mult + 1U) * HASH_BKT_CAPACITY_THRESH)) \ + && !(addhh)->tbl->noexpand) { \ + HASH_EXPAND_BUCKETS(addhh,(addhh)->tbl, oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (oomed) { \ + HASH_DEL_IN_BKT(head,addhh); \ + } \ + ) \ + } \ +} while (0) + +/* remove an item from a given bucket */ +#define HASH_DEL_IN_BKT(head,delhh) \ +do { \ + UT_hash_bucket *_hd_head = &(head); \ + _hd_head->count--; \ + if (_hd_head->hh_head == (delhh)) { \ + _hd_head->hh_head = (delhh)->hh_next; \ + } \ + if ((delhh)->hh_prev) { \ + (delhh)->hh_prev->hh_next = (delhh)->hh_next; \ + } \ + if ((delhh)->hh_next) { \ + (delhh)->hh_next->hh_prev = (delhh)->hh_prev; \ + } \ +} while (0) + +/* Bucket expansion has the effect of doubling the number of buckets + * and redistributing the items into the new buckets. Ideally the + * items will distribute more or less evenly into the new buckets + * (the extent to which this is true is a measure of the quality of + * the hash function as it applies to the key domain). + * + * With the items distributed into more buckets, the chain length + * (item count) in each bucket is reduced. Thus by expanding buckets + * the hash keeps a bound on the chain length. This bounded chain + * length is the essence of how a hash provides constant time lookup. + * + * The calculation of tbl->ideal_chain_maxlen below deserves some + * explanation. First, keep in mind that we're calculating the ideal + * maximum chain length based on the *new* (doubled) bucket count. + * In fractions this is just n/b (n=number of items,b=new num buckets). + * Since the ideal chain length is an integer, we want to calculate + * ceil(n/b). We don't depend on floating point arithmetic in this + * hash, so to calculate ceil(n/b) with integers we could write + * + * ceil(n/b) = (n/b) + ((n%b)?1:0) + * + * and in fact a previous version of this hash did just that. + * But now we have improved things a bit by recognizing that b is + * always a power of two. We keep its base 2 log handy (call it lb), + * so now we can write this with a bit shift and logical AND: + * + * ceil(n/b) = (n>>lb) + ( (n & (b-1)) ? 1:0) + * + */ +#define HASH_EXPAND_BUCKETS(hh,tbl,oomed) \ +do { \ + unsigned _he_bkt; \ + unsigned _he_bkt_i; \ + struct UT_hash_handle *_he_thh, *_he_hh_nxt; \ + UT_hash_bucket *_he_new_buckets, *_he_newbkt; \ + _he_new_buckets = (UT_hash_bucket*)uthash_malloc( \ + 2UL * (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + if (!_he_new_buckets) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero(_he_new_buckets, \ + 2UL * (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + (tbl)->ideal_chain_maxlen = \ + ((tbl)->num_items >> ((tbl)->log2_num_buckets+1U)) + \ + ((((tbl)->num_items & (((tbl)->num_buckets*2U)-1U)) != 0U) ? 1U : 0U); \ + (tbl)->nonideal_items = 0; \ + for (_he_bkt_i = 0; _he_bkt_i < (tbl)->num_buckets; _he_bkt_i++) { \ + _he_thh = (tbl)->buckets[ _he_bkt_i ].hh_head; \ + while (_he_thh != NULL) { \ + _he_hh_nxt = _he_thh->hh_next; \ + HASH_TO_BKT(_he_thh->hashv, (tbl)->num_buckets * 2U, _he_bkt); \ + _he_newbkt = &(_he_new_buckets[_he_bkt]); \ + if (++(_he_newbkt->count) > (tbl)->ideal_chain_maxlen) { \ + (tbl)->nonideal_items++; \ + _he_newbkt->expand_mult = _he_newbkt->count / (tbl)->ideal_chain_maxlen; \ + } \ + _he_thh->hh_prev = NULL; \ + _he_thh->hh_next = _he_newbkt->hh_head; \ + if (_he_newbkt->hh_head != NULL) { \ + _he_newbkt->hh_head->hh_prev = _he_thh; \ + } \ + _he_newbkt->hh_head = _he_thh; \ + _he_thh = _he_hh_nxt; \ + } \ + } \ + uthash_free((tbl)->buckets, (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + (tbl)->num_buckets *= 2U; \ + (tbl)->log2_num_buckets++; \ + (tbl)->buckets = _he_new_buckets; \ + (tbl)->ineff_expands = ((tbl)->nonideal_items > ((tbl)->num_items >> 1)) ? \ + ((tbl)->ineff_expands+1U) : 0U; \ + if ((tbl)->ineff_expands > 1U) { \ + (tbl)->noexpand = 1; \ + uthash_noexpand_fyi(tbl); \ + } \ + uthash_expand_fyi(tbl); \ + } \ +} while (0) + + +/* This is an adaptation of Simon Tatham's O(n log(n)) mergesort */ +/* Note that HASH_SORT assumes the hash handle name to be hh. + * HASH_SRT was added to allow the hash handle name to be passed in. */ +#define HASH_SORT(head,cmpfcn) HASH_SRT(hh,head,cmpfcn) +#define HASH_SRT(hh,head,cmpfcn) \ +do { \ + unsigned _hs_i; \ + unsigned _hs_looping,_hs_nmerges,_hs_insize,_hs_psize,_hs_qsize; \ + struct UT_hash_handle *_hs_p, *_hs_q, *_hs_e, *_hs_list, *_hs_tail; \ + if (head != NULL) { \ + _hs_insize = 1; \ + _hs_looping = 1; \ + _hs_list = &((head)->hh); \ + while (_hs_looping != 0U) { \ + _hs_p = _hs_list; \ + _hs_list = NULL; \ + _hs_tail = NULL; \ + _hs_nmerges = 0; \ + while (_hs_p != NULL) { \ + _hs_nmerges++; \ + _hs_q = _hs_p; \ + _hs_psize = 0; \ + for (_hs_i = 0; _hs_i < _hs_insize; ++_hs_i) { \ + _hs_psize++; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + if (_hs_q == NULL) { \ + break; \ + } \ + } \ + _hs_qsize = _hs_insize; \ + while ((_hs_psize != 0U) || ((_hs_qsize != 0U) && (_hs_q != NULL))) { \ + if (_hs_psize == 0U) { \ + _hs_e = _hs_q; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + _hs_qsize--; \ + } else if ((_hs_qsize == 0U) || (_hs_q == NULL)) { \ + _hs_e = _hs_p; \ + if (_hs_p != NULL) { \ + _hs_p = ((_hs_p->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \ + } \ + _hs_psize--; \ + } else if ((cmpfcn( \ + DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_p)), \ + DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_q)) \ + )) <= 0) { \ + _hs_e = _hs_p; \ + if (_hs_p != NULL) { \ + _hs_p = ((_hs_p->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \ + } \ + _hs_psize--; \ + } else { \ + _hs_e = _hs_q; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + _hs_qsize--; \ + } \ + if ( _hs_tail != NULL ) { \ + _hs_tail->next = ((_hs_e != NULL) ? \ + ELMT_FROM_HH((head)->hh.tbl, _hs_e) : NULL); \ + } else { \ + _hs_list = _hs_e; \ + } \ + if (_hs_e != NULL) { \ + _hs_e->prev = ((_hs_tail != NULL) ? \ + ELMT_FROM_HH((head)->hh.tbl, _hs_tail) : NULL); \ + } \ + _hs_tail = _hs_e; \ + } \ + _hs_p = _hs_q; \ + } \ + if (_hs_tail != NULL) { \ + _hs_tail->next = NULL; \ + } \ + if (_hs_nmerges <= 1U) { \ + _hs_looping = 0; \ + (head)->hh.tbl->tail = _hs_tail; \ + DECLTYPE_ASSIGN(head, ELMT_FROM_HH((head)->hh.tbl, _hs_list)); \ + } \ + _hs_insize *= 2U; \ + } \ + HASH_FSCK(hh, head, "HASH_SRT"); \ + } \ +} while (0) + +/* This function selects items from one hash into another hash. + * The end result is that the selected items have dual presence + * in both hashes. There is no copy of the items made; rather + * they are added into the new hash through a secondary hash + * hash handle that must be present in the structure. */ +#define HASH_SELECT(hh_dst, dst, hh_src, src, cond) \ +do { \ + unsigned _src_bkt, _dst_bkt; \ + void *_last_elt = NULL, *_elt; \ + UT_hash_handle *_src_hh, *_dst_hh, *_last_elt_hh=NULL; \ + ptrdiff_t _dst_hho = ((char*)(&(dst)->hh_dst) - (char*)(dst)); \ + if ((src) != NULL) { \ + for (_src_bkt=0; _src_bkt < (src)->hh_src.tbl->num_buckets; _src_bkt++) { \ + for (_src_hh = (src)->hh_src.tbl->buckets[_src_bkt].hh_head; \ + _src_hh != NULL; \ + _src_hh = _src_hh->hh_next) { \ + _elt = ELMT_FROM_HH((src)->hh_src.tbl, _src_hh); \ + if (cond(_elt)) { \ + IF_HASH_NONFATAL_OOM( int _hs_oomed = 0; ) \ + _dst_hh = (UT_hash_handle*)(((char*)_elt) + _dst_hho); \ + _dst_hh->key = _src_hh->key; \ + _dst_hh->keylen = _src_hh->keylen; \ + _dst_hh->hashv = _src_hh->hashv; \ + _dst_hh->prev = _last_elt; \ + _dst_hh->next = NULL; \ + if (_last_elt_hh != NULL) { \ + _last_elt_hh->next = _elt; \ + } \ + if ((dst) == NULL) { \ + DECLTYPE_ASSIGN(dst, _elt); \ + HASH_MAKE_TABLE(hh_dst, dst, _hs_oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (_hs_oomed) { \ + uthash_nonfatal_oom(_elt); \ + (dst) = NULL; \ + continue; \ + } \ + ) \ + } else { \ + _dst_hh->tbl = (dst)->hh_dst.tbl; \ + } \ + HASH_TO_BKT(_dst_hh->hashv, _dst_hh->tbl->num_buckets, _dst_bkt); \ + HASH_ADD_TO_BKT(_dst_hh->tbl->buckets[_dst_bkt], hh_dst, _dst_hh, _hs_oomed); \ + (dst)->hh_dst.tbl->num_items++; \ + IF_HASH_NONFATAL_OOM( \ + if (_hs_oomed) { \ + HASH_ROLLBACK_BKT(hh_dst, dst, _dst_hh); \ + HASH_DELETE_HH(hh_dst, dst, _dst_hh); \ + _dst_hh->tbl = NULL; \ + uthash_nonfatal_oom(_elt); \ + continue; \ + } \ + ) \ + HASH_BLOOM_ADD(_dst_hh->tbl, _dst_hh->hashv); \ + _last_elt = _elt; \ + _last_elt_hh = _dst_hh; \ + } \ + } \ + } \ + } \ + HASH_FSCK(hh_dst, dst, "HASH_SELECT"); \ +} while (0) + +#define HASH_CLEAR(hh,head) \ +do { \ + if ((head) != NULL) { \ + HASH_BLOOM_FREE((head)->hh.tbl); \ + uthash_free((head)->hh.tbl->buckets, \ + (head)->hh.tbl->num_buckets*sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head) = NULL; \ + } \ +} while (0) + +#define HASH_OVERHEAD(hh,head) \ + (((head) != NULL) ? ( \ + (size_t)(((head)->hh.tbl->num_items * sizeof(UT_hash_handle)) + \ + ((head)->hh.tbl->num_buckets * sizeof(UT_hash_bucket)) + \ + sizeof(UT_hash_table) + \ + (HASH_BLOOM_BYTELEN))) : 0U) + +#ifdef NO_DECLTYPE +#define HASH_ITER(hh,head,el,tmp) \ +for(((el)=(head)), ((*(char**)(&(tmp)))=(char*)((head!=NULL)?(head)->hh.next:NULL)); \ + (el) != NULL; ((el)=(tmp)), ((*(char**)(&(tmp)))=(char*)((tmp!=NULL)?(tmp)->hh.next:NULL))) +#else +#define HASH_ITER(hh,head,el,tmp) \ +for(((el)=(head)), ((tmp)=DECLTYPE(el)((head!=NULL)?(head)->hh.next:NULL)); \ + (el) != NULL; ((el)=(tmp)), ((tmp)=DECLTYPE(el)((tmp!=NULL)?(tmp)->hh.next:NULL))) +#endif + +/* obtain a count of items in the hash */ +#define HASH_COUNT(head) HASH_CNT(hh,head) +#define HASH_CNT(hh,head) ((head != NULL)?((head)->hh.tbl->num_items):0U) + +typedef struct UT_hash_bucket { + struct UT_hash_handle *hh_head; + unsigned count; + + /* expand_mult is normally set to 0. In this situation, the max chain length + * threshold is enforced at its default value, HASH_BKT_CAPACITY_THRESH. (If + * the bucket's chain exceeds this length, bucket expansion is triggered). + * However, setting expand_mult to a non-zero value delays bucket expansion + * (that would be triggered by additions to this particular bucket) + * until its chain length reaches a *multiple* of HASH_BKT_CAPACITY_THRESH. + * (The multiplier is simply expand_mult+1). The whole idea of this + * multiplier is to reduce bucket expansions, since they are expensive, in + * situations where we know that a particular bucket tends to be overused. + * It is better to let its chain length grow to a longer yet-still-bounded + * value, than to do an O(n) bucket expansion too often. + */ + unsigned expand_mult; + +} UT_hash_bucket; + +/* random signature used only to find hash tables in external analysis */ +#define HASH_SIGNATURE 0xa0111fe1u +#define HASH_BLOOM_SIGNATURE 0xb12220f2u + +typedef struct UT_hash_table { + UT_hash_bucket *buckets; + unsigned num_buckets, log2_num_buckets; + unsigned num_items; + struct UT_hash_handle *tail; /* tail hh in app order, for fast append */ + ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */ + + /* in an ideal situation (all buckets used equally), no bucket would have + * more than ceil(#items/#buckets) items. that's the ideal chain length. */ + unsigned ideal_chain_maxlen; + + /* nonideal_items is the number of items in the hash whose chain position + * exceeds the ideal chain maxlen. these items pay the penalty for an uneven + * hash distribution; reaching them in a chain traversal takes >ideal steps */ + unsigned nonideal_items; + + /* ineffective expands occur when a bucket doubling was performed, but + * afterward, more than half the items in the hash had nonideal chain + * positions. If this happens on two consecutive expansions we inhibit any + * further expansion, as it's not helping; this happens when the hash + * function isn't a good fit for the key domain. When expansion is inhibited + * the hash will still work, albeit no longer in constant time. */ + unsigned ineff_expands, noexpand; + + uint32_t signature; /* used only to find hash tables in external analysis */ +#ifdef HASH_BLOOM + uint32_t bloom_sig; /* used only to test bloom exists in external analysis */ + uint8_t *bloom_bv; + uint8_t bloom_nbits; +#endif + +} UT_hash_table; + +typedef struct UT_hash_handle { + struct UT_hash_table *tbl; + void *prev; /* prev element in app order */ + void *next; /* next element in app order */ + struct UT_hash_handle *hh_prev; /* previous hh in bucket order */ + struct UT_hash_handle *hh_next; /* next hh in bucket order */ + void *key; /* ptr to enclosing struct's key */ + unsigned keylen; /* enclosing struct's key len */ + unsigned hashv; /* result of hash-fcn(key) */ +} UT_hash_handle; + +#endif /* UTHASH_H */ diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tpws/BSDmakefile b/tpws/BSDmakefile new file mode 100644 index 0000000..cc108cb --- /dev/null +++ b/tpws/BSDmakefile @@ -0,0 +1,12 @@ +CC ?= cc +CFLAGS += -std=gnu99 -s -O3 -Wno-logical-op-parentheses +LIBS = -lz +SRC_FILES = *.c + +all: tpws + +tpws: $(SRC_FILES) + $(CC) $(CFLAGS) -Iepoll-shim/include -o $@ $(SRC_FILES) epoll-shim/src/*.c $(LDFLAGS) $(LIBS) + +clean: + rm -f tpws *.o diff --git a/tpws/Makefile b/tpws/Makefile new file mode 100644 index 0000000..b3cf3c8 --- /dev/null +++ b/tpws/Makefile @@ -0,0 +1,21 @@ +CC ?= gcc +CFLAGS += -std=gnu99 -O3 +CFLAGS_BSD = -Wno-address-of-packed-member -Wno-logical-op-parentheses -Wno-switch +CFLAGS_MAC = -mmacosx-version-min=10.8 +STRIP = -s +LIBS = -lz +SRC_FILES = *.c + +all: tpws + +tpws: $(SRC_FILES) + $(CC) $(STRIP) $(CFLAGS) -o $@ $(SRC_FILES) $(LDFLAGS) $(LIBS) + +bsd: $(SRC_FILES) + $(CC) $(STRIP) $(CFLAGS) $(CFLAGS_BSD) -Iepoll-shim/include -o tpws $(SRC_FILES) epoll-shim/src/*.c $(LDFLAGS) $(LIBS) + +mac: $(SRC_FILES) + $(CC) $(CFLAGS) $(CFLAGS_BSD) $(CFLAGS_MAC) -Iepoll-shim/include -Imacos -o tpws $(SRC_FILES) epoll-shim/src/*.c $(LDFLAGS) $(LIBS) + +clean: + rm -f tpws *.o diff --git a/tpws/epoll-shim/include/sys/epoll.h b/tpws/epoll-shim/include/sys/epoll.h new file mode 100644 index 0000000..f96c0f1 --- /dev/null +++ b/tpws/epoll-shim/include/sys/epoll.h @@ -0,0 +1,80 @@ +#ifndef SHIM_SYS_EPOLL_H +#define SHIM_SYS_EPOLL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#if defined(__NetBSD__) +#include +#elif defined(__OpenBSD__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__APPLE__) +#include +#endif + +#define EPOLL_CLOEXEC O_CLOEXEC +#define EPOLL_NONBLOCK O_NONBLOCK + +enum EPOLL_EVENTS { __EPOLL_DUMMY }; +#define EPOLLIN 0x001 +#define EPOLLPRI 0x002 +#define EPOLLOUT 0x004 +#define EPOLLRDNORM 0x040 +#define EPOLLNVAL 0x020 +#define EPOLLRDBAND 0x080 +#define EPOLLWRNORM 0x100 +#define EPOLLWRBAND 0x200 +#define EPOLLMSG 0x400 +#define EPOLLERR 0x008 +#define EPOLLHUP 0x010 +#define EPOLLRDHUP 0x2000 +#define EPOLLEXCLUSIVE (1U<<28) +#define EPOLLWAKEUP (1U<<29) +#define EPOLLONESHOT (1U<<30) +#define EPOLLET (1U<<31) + +#define EPOLL_CTL_ADD 1 +#define EPOLL_CTL_DEL 2 +#define EPOLL_CTL_MOD 3 + +typedef union epoll_data { + void *ptr; + int fd; + uint32_t u32; + uint64_t u64; +} epoll_data_t; + +struct epoll_event { + uint32_t events; + epoll_data_t data; +} +#ifdef __x86_64__ +__attribute__ ((__packed__)) +#endif +; + + +int epoll_create(int); +int epoll_create1(int); +int epoll_ctl(int, int, int, struct epoll_event *); +int epoll_wait(int, struct epoll_event *, int, int); +int epoll_pwait(int, struct epoll_event *, int, int, const sigset_t *); + + +#ifndef SHIM_SYS_SHIM_HELPERS +#define SHIM_SYS_SHIM_HELPERS +#include /* IWYU pragma: keep */ + +extern int epoll_shim_close(int); +#define close epoll_shim_close +#endif + + +#ifdef __cplusplus +} +#endif + +#endif /* sys/epoll.h */ diff --git a/tpws/epoll-shim/src/epoll.c b/tpws/epoll-shim/src/epoll.c new file mode 100644 index 0000000..7b11653 --- /dev/null +++ b/tpws/epoll-shim/src/epoll.c @@ -0,0 +1,305 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "epoll_shim_ctx.h" + +#ifdef __NetBSD__ +#define ppoll pollts +#endif + +// TODO(jan): Remove this once the definition is exposed in in +// all supported FreeBSD versions. +#ifndef timespecsub +#define timespecsub(tsp, usp, vsp) \ + do { \ + (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \ + (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \ + if ((vsp)->tv_nsec < 0) { \ + (vsp)->tv_sec--; \ + (vsp)->tv_nsec += 1000000000L; \ + } \ + } while (0) +#endif + +static errno_t +epollfd_close(FDContextMapNode *node) +{ + return epollfd_ctx_terminate(&node->ctx.epollfd); +} + +static FDContextVTable const epollfd_vtable = { + .read_fun = fd_context_default_read, + .write_fun = fd_context_default_write, + .close_fun = epollfd_close, +}; + +static FDContextMapNode * +epoll_create_impl(errno_t *ec) +{ + FDContextMapNode *node; + + node = epoll_shim_ctx_create_node(&epoll_shim_ctx, ec); + if (!node) { + return NULL; + } + + node->flags = 0; + + if ((*ec = epollfd_ctx_init(&node->ctx.epollfd, /**/ + node->fd)) != 0) { + goto fail; + } + + node->vtable = &epollfd_vtable; + return node; + +fail: + epoll_shim_ctx_remove_node_explicit(&epoll_shim_ctx, node); + (void)fd_context_map_node_destroy(node); + return NULL; +} + +static int +epoll_create_common(void) +{ + FDContextMapNode *node; + errno_t ec; + + node = epoll_create_impl(&ec); + if (!node) { + errno = ec; + return -1; + } + + return node->fd; +} + +int +epoll_create(int size) +{ + if (size <= 0) { + errno = EINVAL; + return -1; + } + + return epoll_create_common(); +} + +int +epoll_create1(int flags) +{ + if (flags & ~EPOLL_CLOEXEC) { + errno = EINVAL; + return -1; + } + + return epoll_create_common(); +} + +static errno_t +epoll_ctl_impl(int fd, int op, int fd2, struct epoll_event *ev) +{ + if (!ev && op != EPOLL_CTL_DEL) { + return EFAULT; + } + + FDContextMapNode *node = epoll_shim_ctx_find_node(&epoll_shim_ctx, fd); + if (!node || node->vtable != &epollfd_vtable) { + struct stat sb; + return (fd < 0 || fstat(fd, &sb) < 0) ? EBADF : EINVAL; + } + + return epollfd_ctx_ctl(&node->ctx.epollfd, op, fd2, ev); +} + +int +epoll_ctl(int fd, int op, int fd2, struct epoll_event *ev) +{ + errno_t ec = epoll_ctl_impl(fd, op, fd2, ev); + if (ec != 0) { + errno = ec; + return -1; + } + + return 0; +} + +static bool +is_no_wait_deadline(struct timespec const *deadline) +{ + return (deadline && deadline->tv_sec == 0 && deadline->tv_nsec == 0); +} + +static errno_t +epollfd_ctx_wait_or_block(EpollFDCtx *epollfd, struct epoll_event *ev, int cnt, + int *actual_cnt, struct timespec const *deadline, sigset_t const *sigs) +{ + errno_t ec; + + for (;;) { + if ((ec = epollfd_ctx_wait(epollfd, /**/ + ev, cnt, actual_cnt)) != 0) { + return ec; + } + + if (*actual_cnt || is_no_wait_deadline(deadline)) { + return 0; + } + + struct timespec timeout; + + if (deadline) { + struct timespec current_time; + + if (clock_gettime(CLOCK_MONOTONIC, /**/ + ¤t_time) < 0) { + return errno; + } + + timespecsub(deadline, ¤t_time, &timeout); + if (timeout.tv_sec < 0 || + is_no_wait_deadline(&timeout)) { + return 0; + } + } + + (void)pthread_mutex_lock(&epollfd->mutex); + + nfds_t nfds = (nfds_t)(1 + epollfd->poll_fds_size); + + size_t size; + if (__builtin_mul_overflow(nfds, sizeof(struct pollfd), + &size)) { + ec = ENOMEM; + (void)pthread_mutex_unlock(&epollfd->mutex); + return ec; + } + + struct pollfd *pfds = malloc(size); + if (!pfds) { + ec = errno; + (void)pthread_mutex_unlock(&epollfd->mutex); + return ec; + } + + epollfd_ctx_fill_pollfds(epollfd, pfds); + + (void)pthread_mutex_lock(&epollfd->nr_polling_threads_mutex); + ++epollfd->nr_polling_threads; + (void)pthread_mutex_unlock(&epollfd->nr_polling_threads_mutex); + + (void)pthread_mutex_unlock(&epollfd->mutex); + + /* + * This surfaced a race condition when + * registering/unregistering poll-only fds. The tests should + * still succeed if this is enabled. + */ +#if 0 + usleep(500000); +#endif + + int n = ppoll(pfds, nfds, deadline ? &timeout : NULL, sigs); + if (n < 0) { + ec = errno; + } + + free(pfds); + + (void)pthread_mutex_lock(&epollfd->nr_polling_threads_mutex); + --epollfd->nr_polling_threads; + if (epollfd->nr_polling_threads == 0) { + (void)pthread_cond_signal( + &epollfd->nr_polling_threads_cond); + } + (void)pthread_mutex_unlock(&epollfd->nr_polling_threads_mutex); + + if (n < 0) { + return ec; + } + } +} + +static errno_t +timeout_to_deadline(struct timespec *deadline, int to) +{ + assert(to >= 0); + + if (to == 0) { + *deadline = (struct timespec){0, 0}; + } else if (to > 0) { + if (clock_gettime(CLOCK_MONOTONIC, deadline) < 0) { + return errno; + } + + if (__builtin_add_overflow(deadline->tv_sec, to / 1000 + 1, + &deadline->tv_sec)) { + return EINVAL; + } + deadline->tv_sec -= 1; + + deadline->tv_nsec += (to % 1000) * 1000000L; + if (deadline->tv_nsec >= 1000000000) { + deadline->tv_nsec -= 1000000000; + deadline->tv_sec += 1; + } + } + + return 0; +} + +static errno_t +epoll_pwait_impl(int fd, struct epoll_event *ev, int cnt, int to, + sigset_t const *sigs, int *actual_cnt) +{ + if (cnt < 1 || cnt > (int)(INT_MAX / sizeof(struct epoll_event))) { + return EINVAL; + } + + FDContextMapNode *node = epoll_shim_ctx_find_node(&epoll_shim_ctx, fd); + if (!node || node->vtable != &epollfd_vtable) { + struct stat sb; + return (fd < 0 || fstat(fd, &sb) < 0) ? EBADF : EINVAL; + } + + struct timespec deadline; + errno_t ec; + if (to >= 0 && (ec = timeout_to_deadline(&deadline, to)) != 0) { + return ec; + } + + return epollfd_ctx_wait_or_block(&node->ctx.epollfd, ev, cnt, + actual_cnt, (to >= 0) ? &deadline : NULL, sigs); +} + +int +epoll_pwait(int fd, struct epoll_event *ev, int cnt, int to, + sigset_t const *sigs) +{ + int actual_cnt; + + errno_t ec = epoll_pwait_impl(fd, ev, cnt, to, sigs, &actual_cnt); + if (ec != 0) { + errno = ec; + return -1; + } + + return actual_cnt; +} + +int +epoll_wait(int fd, struct epoll_event *ev, int cnt, int to) +{ + return epoll_pwait(fd, ev, cnt, to, NULL); +} diff --git a/tpws/epoll-shim/src/epoll_shim_ctx.c b/tpws/epoll-shim/src/epoll_shim_ctx.c new file mode 100644 index 0000000..ac89f5f --- /dev/null +++ b/tpws/epoll-shim/src/epoll_shim_ctx.c @@ -0,0 +1,281 @@ +#include "epoll_shim_ctx.h" + +#include + +#include +#include +#include +#include + +static void +fd_context_map_node_init(FDContextMapNode *node, int kq) +{ + node->fd = kq; + node->vtable = NULL; +} + +static FDContextMapNode * +fd_context_map_node_create(int kq, errno_t *ec) +{ + FDContextMapNode *node; + + node = malloc(sizeof(FDContextMapNode)); + if (!node) { + *ec = errno; + return NULL; + } + + fd_context_map_node_init(node, kq); + return node; +} + +static errno_t +fd_context_map_node_terminate(FDContextMapNode *node, bool close_fd) +{ + errno_t ec = node->vtable ? node->vtable->close_fun(node) : 0; + + if (close_fd && close(node->fd) < 0) { + ec = ec ? ec : errno; + } + + return ec; +} + +errno_t +fd_context_map_node_destroy(FDContextMapNode *node) +{ + errno_t ec = fd_context_map_node_terminate(node, true); + free(node); + return ec; +} + +/**/ + +errno_t +fd_context_default_read(FDContextMapNode *node, /**/ + void *buf, size_t nbytes, size_t *bytes_transferred) +{ + (void)node; + (void)buf; + (void)nbytes; + (void)bytes_transferred; + + return EINVAL; +} + +errno_t +fd_context_default_write(FDContextMapNode *node, /**/ + void const *buf, size_t nbytes, size_t *bytes_transferred) +{ + (void)node; + (void)buf; + (void)nbytes; + (void)bytes_transferred; + + return EINVAL; +} + +/**/ + +static int +fd_context_map_node_cmp(FDContextMapNode *e1, FDContextMapNode *e2) +{ + return (e1->fd < e2->fd) ? -1 : (e1->fd > e2->fd); +} + +RB_PROTOTYPE_STATIC(fd_context_map_, fd_context_map_node_, entry, + fd_context_map_node_cmp); +RB_GENERATE_STATIC(fd_context_map_, fd_context_map_node_, entry, + fd_context_map_node_cmp); + +EpollShimCtx epoll_shim_ctx = { + .fd_context_map = RB_INITIALIZER(&fd_context_map), + .mutex = PTHREAD_MUTEX_INITIALIZER, +}; + +static FDContextMapNode * +epoll_shim_ctx_create_node_impl(EpollShimCtx *epoll_shim_ctx, int kq, + errno_t *ec) +{ + FDContextMapNode *node; + + { + FDContextMapNode find; + find.fd = kq; + + node = RB_FIND(fd_context_map_, /**/ + &epoll_shim_ctx->fd_context_map, &find); + } + + if (node) { + /* + * If we get here, someone must have already closed the old fd + * with a normal 'close()' call, i.e. not with our + * 'epoll_shim_close()' wrapper. The fd inside the node + * refers now to the new kq we are currently creating. We + * must not close it, but we must clean up the old context + * object! + */ + (void)fd_context_map_node_terminate(node, false); + fd_context_map_node_init(node, kq); + } else { + node = fd_context_map_node_create(kq, ec); + if (!node) { + return NULL; + } + + void *colliding_node = RB_INSERT(fd_context_map_, + &epoll_shim_ctx->fd_context_map, node); + (void)colliding_node; + assert(colliding_node == NULL); + } + + return node; +} + +FDContextMapNode * +epoll_shim_ctx_create_node(EpollShimCtx *epoll_shim_ctx, errno_t *ec) +{ + FDContextMapNode *node; + + int kq = kqueue(); + if (kq < 0) { + *ec = errno; + return NULL; + } + + (void)pthread_mutex_lock(&epoll_shim_ctx->mutex); + node = epoll_shim_ctx_create_node_impl(epoll_shim_ctx, kq, ec); + (void)pthread_mutex_unlock(&epoll_shim_ctx->mutex); + + if (!node) { + close(kq); + } + + return node; +} + +static FDContextMapNode * +epoll_shim_ctx_find_node_impl(EpollShimCtx *epoll_shim_ctx, int fd) +{ + FDContextMapNode *node; + + FDContextMapNode find; + find.fd = fd; + + node = RB_FIND(fd_context_map_, /**/ + &epoll_shim_ctx->fd_context_map, &find); + + return node; +} + +FDContextMapNode * +epoll_shim_ctx_find_node(EpollShimCtx *epoll_shim_ctx, int fd) +{ + FDContextMapNode *node; + + (void)pthread_mutex_lock(&epoll_shim_ctx->mutex); + node = epoll_shim_ctx_find_node_impl(epoll_shim_ctx, fd); + (void)pthread_mutex_unlock(&epoll_shim_ctx->mutex); + + return node; +} + +FDContextMapNode * +epoll_shim_ctx_remove_node(EpollShimCtx *epoll_shim_ctx, int fd) +{ + FDContextMapNode *node; + + (void)pthread_mutex_lock(&epoll_shim_ctx->mutex); + node = epoll_shim_ctx_find_node_impl(epoll_shim_ctx, fd); + if (node) { + RB_REMOVE(fd_context_map_, /**/ + &epoll_shim_ctx->fd_context_map, node); + } + (void)pthread_mutex_unlock(&epoll_shim_ctx->mutex); + + return node; +} + +void +epoll_shim_ctx_remove_node_explicit(EpollShimCtx *epoll_shim_ctx, + FDContextMapNode *node) +{ + (void)pthread_mutex_lock(&epoll_shim_ctx->mutex); + RB_REMOVE(fd_context_map_, /**/ + &epoll_shim_ctx->fd_context_map, node); + (void)pthread_mutex_unlock(&epoll_shim_ctx->mutex); +} + +/**/ + +int +epoll_shim_close(int fd) +{ + FDContextMapNode *node; + + node = epoll_shim_ctx_remove_node(&epoll_shim_ctx, fd); + if (!node) { + return close(fd); + } + + errno_t ec = fd_context_map_node_destroy(node); + if (ec != 0) { + errno = ec; + return -1; + } + + return 0; +} + +ssize_t +epoll_shim_read(int fd, void *buf, size_t nbytes) +{ + FDContextMapNode *node; + + node = epoll_shim_ctx_find_node(&epoll_shim_ctx, fd); + if (!node) { + return read(fd, buf, nbytes); + } + + if (nbytes > SSIZE_MAX) { + errno = EINVAL; + return -1; + } + + size_t bytes_transferred; + errno_t ec = node->vtable->read_fun(node, /**/ + buf, nbytes, &bytes_transferred); + if (ec != 0) { + errno = ec; + return -1; + } + + return (ssize_t)bytes_transferred; +} + +ssize_t +epoll_shim_write(int fd, void const *buf, size_t nbytes) +{ + FDContextMapNode *node; + + node = epoll_shim_ctx_find_node(&epoll_shim_ctx, fd); + if (!node) { + return write(fd, buf, nbytes); + } + + if (nbytes > SSIZE_MAX) { + errno = EINVAL; + return -1; + } + + size_t bytes_transferred; + errno_t ec = node->vtable->write_fun(node, /**/ + buf, nbytes, &bytes_transferred); + if (ec != 0) { + errno = ec; + return -1; + } + + return (ssize_t)bytes_transferred; +} diff --git a/tpws/epoll-shim/src/epoll_shim_ctx.h b/tpws/epoll-shim/src/epoll_shim_ctx.h new file mode 100644 index 0000000..01ae19a --- /dev/null +++ b/tpws/epoll-shim/src/epoll_shim_ctx.h @@ -0,0 +1,76 @@ +#ifndef EPOLL_SHIM_CTX_H_ +#define EPOLL_SHIM_CTX_H_ + +#include "fix.h" + +#include + +#include + +#include "epollfd_ctx.h" +#include "eventfd_ctx.h" +#include "signalfd_ctx.h" +#include "timerfd_ctx.h" + +struct fd_context_map_node_; +typedef struct fd_context_map_node_ FDContextMapNode; + +typedef errno_t (*fd_context_read_fun)(FDContextMapNode *node, /**/ + void *buf, size_t nbytes, size_t *bytes_transferred); +typedef errno_t (*fd_context_write_fun)(FDContextMapNode *node, /**/ + const void *buf, size_t nbytes, size_t *bytes_transferred); +typedef errno_t (*fd_context_close_fun)(FDContextMapNode *node); + +typedef struct { + fd_context_read_fun read_fun; + fd_context_write_fun write_fun; + fd_context_close_fun close_fun; +} FDContextVTable; + +errno_t fd_context_default_read(FDContextMapNode *node, /**/ + void *buf, size_t nbytes, size_t *bytes_transferred); +errno_t fd_context_default_write(FDContextMapNode *node, /**/ + void const *buf, size_t nbytes, size_t *bytes_transferred); + +struct fd_context_map_node_ { + RB_ENTRY(fd_context_map_node_) entry; + int fd; + int flags; + union { + EpollFDCtx epollfd; + EventFDCtx eventfd; + TimerFDCtx timerfd; + SignalFDCtx signalfd; + } ctx; + FDContextVTable const *vtable; +}; + +errno_t fd_context_map_node_destroy(FDContextMapNode *node); + +/**/ + +typedef RB_HEAD(fd_context_map_, fd_context_map_node_) FDContextMap; + +typedef struct { + FDContextMap fd_context_map; + pthread_mutex_t mutex; +} EpollShimCtx; + +extern EpollShimCtx epoll_shim_ctx; + +FDContextMapNode *epoll_shim_ctx_create_node(EpollShimCtx *epoll_shim_ctx, + errno_t *ec); +FDContextMapNode *epoll_shim_ctx_find_node(EpollShimCtx *epoll_shim_ctx, + int fd); +FDContextMapNode *epoll_shim_ctx_remove_node(EpollShimCtx *epoll_shim_ctx, + int fd); +void epoll_shim_ctx_remove_node_explicit(EpollShimCtx *epoll_shim_ctx, + FDContextMapNode *node); + +/**/ + +int epoll_shim_close(int fd); +ssize_t epoll_shim_read(int fd, void *buf, size_t nbytes); +ssize_t epoll_shim_write(int fd, void const *buf, size_t nbytes); + +#endif diff --git a/tpws/epoll-shim/src/epollfd_ctx.c b/tpws/epoll-shim/src/epollfd_ctx.c new file mode 100644 index 0000000..baf3dc2 --- /dev/null +++ b/tpws/epoll-shim/src/epollfd_ctx.c @@ -0,0 +1,1386 @@ +#include "epollfd_ctx.h" + +#include + +#if defined(__FreeBSD__) +#include +#endif +#include +#include +#include +#include +#include + +#if defined(__DragonFly__) +/* For TAILQ_FOREACH_SAFE. */ +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include + +static RegisteredFDsNode * +registered_fds_node_create(int fd) +{ + RegisteredFDsNode *node; + + node = malloc(sizeof(*node)); + if (!node) { + return NULL; + } + + *node = (RegisteredFDsNode){.fd = fd, .self_pipe = {-1, -1}}; + + return node; +} + +static void +registered_fds_node_destroy(RegisteredFDsNode *node) +{ + if (node->self_pipe[0] >= 0 && node->self_pipe[1] >= 0) { + (void)close(node->self_pipe[0]); + (void)close(node->self_pipe[1]); + } + + free(node); +} + +typedef struct { + int evfilt_read; + int evfilt_write; + int evfilt_except; +} NeededFilters; + +static NeededFilters +get_needed_filters(RegisteredFDsNode *fd2_node) +{ + NeededFilters needed_filters; + + needed_filters.evfilt_except = 0; + + if (fd2_node->node_type == NODE_TYPE_FIFO) { + if (fd2_node->node_data.fifo.readable && + fd2_node->node_data.fifo.writable) { + needed_filters.evfilt_read = !!( + fd2_node->events & EPOLLIN); + needed_filters.evfilt_write = !!( + fd2_node->events & EPOLLOUT); + + if (fd2_node->events == 0) { + needed_filters.evfilt_read = + fd2_node->eof_state ? 1 : EV_CLEAR; + } + + } else if (fd2_node->node_data.fifo.readable) { + needed_filters.evfilt_read = !!( + fd2_node->events & EPOLLIN); + needed_filters.evfilt_write = 0; + + if (needed_filters.evfilt_read == 0) { + needed_filters.evfilt_read = + fd2_node->eof_state ? 1 : EV_CLEAR; + } + } else if (fd2_node->node_data.fifo.writable) { + needed_filters.evfilt_read = 0; + needed_filters.evfilt_write = !!( + fd2_node->events & EPOLLOUT); + + if (needed_filters.evfilt_write == 0) { + needed_filters.evfilt_write = + fd2_node->eof_state ? 1 : EV_CLEAR; + } + } else { + __builtin_unreachable(); + } + + goto out; + } + + if (fd2_node->node_type == NODE_TYPE_KQUEUE) { + needed_filters.evfilt_read = !!(fd2_node->events & EPOLLIN); + needed_filters.evfilt_write = 0; + + assert(fd2_node->eof_state == 0); + + if (needed_filters.evfilt_read == 0) { + needed_filters.evfilt_read = EV_CLEAR; + } + + goto out; + } + + if (fd2_node->node_type == NODE_TYPE_SOCKET) { + needed_filters.evfilt_read = !!(fd2_node->events & EPOLLIN); + + if (needed_filters.evfilt_read == 0 && + (fd2_node->events & EPOLLRDHUP)) { + needed_filters.evfilt_read = (fd2_node->eof_state & + EOF_STATE_READ_EOF) + ? 1 + : EV_CLEAR; + } + +#ifdef EVFILT_EXCEPT + needed_filters.evfilt_except = !!(fd2_node->events & EPOLLPRI); +#else + if (needed_filters.evfilt_read == 0 && + (fd2_node->events & EPOLLPRI)) { + needed_filters.evfilt_read = fd2_node->pollpri_active + ? 1 + : EV_CLEAR; + } +#endif + + needed_filters.evfilt_write = !!(fd2_node->events & EPOLLOUT); + + /* Let's use EVFILT_READ to drive the POLLHUP. */ + if (fd2_node->eof_state == + (EOF_STATE_READ_EOF | EOF_STATE_WRITE_EOF)) { + if (needed_filters.evfilt_read != 1 && + needed_filters.evfilt_write != 1) { + needed_filters.evfilt_read = 1; + } + + if (needed_filters.evfilt_read) { + needed_filters.evfilt_write = 0; + } else { + needed_filters.evfilt_read = 0; + } + } + + /* We need something to detect POLLHUP. */ + if (fd2_node->eof_state == 0 && + needed_filters.evfilt_read == 0 && + needed_filters.evfilt_write == 0) { + needed_filters.evfilt_read = EV_CLEAR; + } + + if (fd2_node->eof_state == EOF_STATE_READ_EOF) { + if (needed_filters.evfilt_write == 0) { + needed_filters.evfilt_write = EV_CLEAR; + } + } + + if (fd2_node->eof_state == EOF_STATE_WRITE_EOF) { + if (needed_filters.evfilt_read == 0) { + needed_filters.evfilt_read = EV_CLEAR; + } + } + + goto out; + } + + needed_filters.evfilt_read = !!(fd2_node->events & EPOLLIN); + needed_filters.evfilt_write = !!(fd2_node->events & EPOLLOUT); + + if (fd2_node->events == 0) { + needed_filters.evfilt_read = fd2_node->eof_state ? 1 + : EV_CLEAR; + } + +out: + if (fd2_node->is_edge_triggered) { + if (needed_filters.evfilt_read) { + needed_filters.evfilt_read = EV_CLEAR; + } + if (needed_filters.evfilt_write) { + needed_filters.evfilt_write = EV_CLEAR; + } + if (needed_filters.evfilt_except) { + needed_filters.evfilt_except = EV_CLEAR; + } + } + + assert(needed_filters.evfilt_read || needed_filters.evfilt_write); + assert(needed_filters.evfilt_read == 0 || + needed_filters.evfilt_read == 1 || + needed_filters.evfilt_read == EV_CLEAR); + assert(needed_filters.evfilt_write == 0 || + needed_filters.evfilt_write == 1 || + needed_filters.evfilt_write == EV_CLEAR); + assert(needed_filters.evfilt_except == 0 || + needed_filters.evfilt_except == 1 || + needed_filters.evfilt_except == EV_CLEAR); + + return needed_filters; +} + +static void +registered_fds_node_update_flags_from_epoll_event(RegisteredFDsNode *fd2_node, + struct epoll_event *ev) +{ + fd2_node->events = ev->events & + (EPOLLIN | EPOLLPRI | EPOLLRDHUP | EPOLLOUT); + fd2_node->data = ev->data; + fd2_node->is_edge_triggered = ev->events & EPOLLET; + fd2_node->is_oneshot = ev->events & EPOLLONESHOT; + + if (fd2_node->is_oneshot) { + fd2_node->is_edge_triggered = true; + } +} + +static errno_t +registered_fds_node_add_self_trigger(RegisteredFDsNode *fd2_node, + EpollFDCtx *epollfd) +{ + struct kevent kevs[1]; + +#ifdef EVFILT_USER + EV_SET(&kevs[0], (uintptr_t)fd2_node, EVFILT_USER, /**/ + EV_ADD | EV_CLEAR, 0, 0, fd2_node); +#else + if (fd2_node->self_pipe[0] < 0 && fd2_node->self_pipe[1] < 0) { + if (pipe2(fd2_node->self_pipe, O_NONBLOCK | O_CLOEXEC) < 0) { + errno_t ec = errno; + fd2_node->self_pipe[0] = fd2_node->self_pipe[1] = -1; + return ec; + } + + assert(fd2_node->self_pipe[0] >= 0); + assert(fd2_node->self_pipe[1] >= 0); + } + + EV_SET(&kevs[0], fd2_node->self_pipe[0], EVFILT_READ, /**/ + EV_ADD | EV_CLEAR, 0, 0, fd2_node); +#endif + + if (kevent(epollfd->kq, kevs, 1, NULL, 0, NULL) < 0) { + return errno; + } + + return 0; +} + +static void +registered_fds_node_trigger_self(RegisteredFDsNode *fd2_node, + EpollFDCtx *epollfd) +{ +#ifdef EVFILT_USER + struct kevent kevs[1]; + EV_SET(&kevs[0], (uintptr_t)fd2_node, EVFILT_USER, /**/ + 0, NOTE_TRIGGER, 0, fd2_node); + (void)kevent(epollfd->kq, kevs, 1, NULL, 0, NULL); +#else + (void)epollfd; + assert(fd2_node->self_pipe[1] >= 0); + + char c = 0; + (void)write(fd2_node->self_pipe[1], &c, 1); +#endif +} + +static void +registered_fds_node_feed_event(RegisteredFDsNode *fd2_node, + EpollFDCtx *epollfd, struct kevent const *kev) +{ + int revents = 0; + + if (fd2_node->node_type == NODE_TYPE_POLL) { + assert(fd2_node->revents == 0); + +#ifdef EVFILT_USER + assert(kev->filter == EVFILT_USER); +#else + char c[32]; + while (read(fd2_node->self_pipe[0], c, sizeof(c)) >= 0) { + } +#endif + + struct pollfd pfd = { + .fd = fd2_node->fd, + .events = (short)fd2_node->events, + }; + + revents = poll(&pfd, 1, 0) < 0 ? EPOLLERR : pfd.revents; + + fd2_node->revents = revents & POLLNVAL ? 0 : (uint32_t)revents; + assert(!(fd2_node->revents & + ~(uint32_t)(POLLIN | POLLOUT | POLLERR | POLLHUP))); + return; + } + + if (fd2_node->node_type == NODE_TYPE_FIFO && +#ifdef EVFILT_USER + kev->filter == EVFILT_USER +#else + (fd2_node->self_pipe[0] >= 0 && + kev->ident == (uintptr_t)fd2_node->self_pipe[0]) +#endif + ) { + assert(fd2_node->revents == 0); + + assert(!fd2_node->has_evfilt_read); + assert(!fd2_node->has_evfilt_write); + assert(!fd2_node->has_evfilt_except); + + NeededFilters needed_filters = get_needed_filters(fd2_node); + assert(needed_filters.evfilt_write); + + struct kevent nkev[1]; + EV_SET(&nkev[0], fd2_node->fd, EVFILT_WRITE, + EV_ADD | (needed_filters.evfilt_write & EV_CLEAR) | + EV_RECEIPT, + 0, 0, fd2_node); + + if (kevent(epollfd->kq, nkev, 1, nkev, 1, NULL) != 1 || + nkev[0].data != 0) { + revents = EPOLLERR | EPOLLOUT; + + if (!fd2_node->is_edge_triggered) { + registered_fds_node_trigger_self(fd2_node, + epollfd); + } + + goto out; + } else { + fd2_node->has_evfilt_write = true; + return; + } + } + +#ifdef EVFILT_EXCEPT + assert(kev->filter == EVFILT_READ || kev->filter == EVFILT_WRITE || + kev->filter == EVFILT_EXCEPT); +#else + assert(kev->filter == EVFILT_READ || kev->filter == EVFILT_WRITE); +#endif + assert((int)kev->ident == fd2_node->fd); + + if (kev->filter == EVFILT_READ) { + revents |= EPOLLIN; +#ifndef EVFILT_EXCEPT + if (fd2_node->events & EPOLLPRI) { + struct pollfd pfd = { + .fd = fd2_node->fd, + .events = POLLPRI, + }; + + if ((poll(&pfd, 1, 0) == 1) && + (pfd.revents & POLLPRI)) { + revents |= EPOLLPRI; + fd2_node->pollpri_active = true; + } else { + fd2_node->pollpri_active = false; + } + } +#endif + } else if (kev->filter == EVFILT_WRITE) { + revents |= EPOLLOUT; + } +#ifdef EVFILT_EXCEPT + else if (kev->filter == EVFILT_EXCEPT) { + assert((kev->fflags & NOTE_OOB) != 0); + + revents |= EPOLLPRI; + goto out; + } +#endif + + if (fd2_node->node_type == NODE_TYPE_SOCKET) { + if (kev->filter == EVFILT_READ) { + if (kev->flags & EV_EOF) { + fd2_node->eof_state |= EOF_STATE_READ_EOF; + } else { + fd2_node->eof_state &= ~EOF_STATE_READ_EOF; + } + } else if (kev->filter == EVFILT_WRITE) { + if (kev->flags & EV_EOF) { + fd2_node->eof_state |= EOF_STATE_WRITE_EOF; + } else { + fd2_node->eof_state &= ~EOF_STATE_WRITE_EOF; + } + } + } else { + if (kev->filter == EVFILT_READ) { + if (kev->flags & EV_EOF) { + fd2_node->eof_state = EOF_STATE_READ_EOF | + EOF_STATE_WRITE_EOF; + } else { + fd2_node->eof_state = 0; + } + } else if (kev->filter == EVFILT_WRITE) { + if (kev->flags & EV_EOF) { + fd2_node->eof_state = EOF_STATE_READ_EOF | + EOF_STATE_WRITE_EOF; + } else { + fd2_node->eof_state = 0; + } + } + } + + if (kev->flags & EV_ERROR) { + revents |= EPOLLERR; + } + + if (kev->flags & EV_EOF) { + if (kev->fflags) { + revents |= EPOLLERR; + } + } + + if (fd2_node->eof_state) { + int epoll_event; + + if (fd2_node->node_type == NODE_TYPE_FIFO) { + if (kev->filter == EVFILT_READ) { + epoll_event = EPOLLHUP; + if (kev->data == 0) { + revents &= ~EPOLLIN; + } + } else if (kev->filter == EVFILT_WRITE) { + if (fd2_node->has_evfilt_read) { + assert( + fd2_node->node_data.fifo.readable); + assert( + fd2_node->node_data.fifo.writable); + + /* + * Any non-zero revents must have come + * from the EVFILT_READ filter. It + * could either be "POLLIN", + * "POLLIN | POLLHUP" or "POLLHUP", so + * we know if there is data to read. + * But we also know that the FIFO is + * done, so set POLLHUP because it + * would be set anyway. + * + * If revents is zero, not setting it + * will simply ignore this EVFILT_WRITE + * and wait for the next EVFILT_READ + * (which will be EOF). + */ + + if (fd2_node->revents != 0) { + fd2_node->revents |= POLLHUP; + } + return; + } + + epoll_event = EPOLLERR; + if (kev->data < PIPE_BUF) { + revents &= ~EPOLLOUT; + } + } else { + __builtin_unreachable(); + } + } else if (fd2_node->node_type == NODE_TYPE_SOCKET) { + epoll_event = 0; + + if (fd2_node->eof_state & EOF_STATE_READ_EOF) { + epoll_event |= EPOLLIN | EPOLLRDHUP; + } + + if (fd2_node->eof_state & EOF_STATE_WRITE_EOF) { + epoll_event |= EPOLLOUT; + } + + if (fd2_node->eof_state == + (EOF_STATE_READ_EOF | EOF_STATE_WRITE_EOF)) { + epoll_event |= EPOLLHUP; + } + } else { + epoll_event = EPOLLHUP; + } + + revents |= epoll_event; + } + +out: + fd2_node->revents |= (uint32_t)revents; + fd2_node->revents &= (fd2_node->events | EPOLLHUP | EPOLLERR); + + if (fd2_node->revents && (uintptr_t)fd2_node->fd == kev->ident) { + if (kev->filter == EVFILT_READ) { + fd2_node->got_evfilt_read = true; + } else if (kev->filter == EVFILT_WRITE) { + fd2_node->got_evfilt_write = true; + } +#ifdef EVFILT_EXCEPT + else if (kev->filter == EVFILT_EXCEPT) { + fd2_node->got_evfilt_except = true; + } +#endif + } +} + +static void +registered_fds_node_register_for_completion(int *kq, + RegisteredFDsNode *fd2_node) +{ + struct kevent kev[3]; + int n = 0; + + if (fd2_node->has_evfilt_read && !fd2_node->got_evfilt_read) { + EV_SET(&kev[n++], fd2_node->fd, EVFILT_READ, + EV_ADD | EV_ONESHOT | EV_RECEIPT, 0, 0, fd2_node); + } + if (fd2_node->has_evfilt_write && !fd2_node->got_evfilt_write) { + EV_SET(&kev[n++], fd2_node->fd, EVFILT_WRITE, + EV_ADD | EV_ONESHOT | EV_RECEIPT, 0, 0, fd2_node); + } + if (fd2_node->has_evfilt_except && !fd2_node->got_evfilt_except) { +#ifdef EVFILT_EXCEPT + EV_SET(&kev[n++], fd2_node->fd, EVFILT_EXCEPT, + EV_ADD | EV_ONESHOT | EV_RECEIPT, NOTE_OOB, 0, fd2_node); +#else + assert(0); +#endif + } + + if (n == 0) { + return; + } + + if (*kq < 0) { + *kq = kqueue(); + } + + if (*kq >= 0) { + (void)kevent(*kq, kev, n, kev, n, NULL); + } +} + +static void +registered_fds_node_complete(int kq) +{ + if (kq < 0) { + return; + } + + struct kevent kevs[32]; + int n; + + while ((n = kevent(kq, /**/ + NULL, 0, kevs, 32, &(struct timespec){0, 0})) > 0) { + for (int i = 0; i < n; ++i) { + RegisteredFDsNode *fd2_node = + (RegisteredFDsNode *)kevs[i].udata; + + registered_fds_node_feed_event(fd2_node, NULL, + &kevs[i]); + } + } + + (void)close(kq); +} + +static int +fd_cmp(RegisteredFDsNode *e1, RegisteredFDsNode *e2) +{ + return (e1->fd < e2->fd) ? -1 : (e1->fd > e2->fd); +} + +RB_PROTOTYPE_STATIC(registered_fds_set_, registered_fds_node_, entry, fd_cmp); +RB_GENERATE_STATIC(registered_fds_set_, registered_fds_node_, entry, fd_cmp); + +errno_t +epollfd_ctx_init(EpollFDCtx *epollfd, int kq) +{ + errno_t ec; + + *epollfd = (EpollFDCtx){ + .kq = kq, + .registered_fds = RB_INITIALIZER(®istered_fds), + .self_pipe = {-1, -1}, + }; + + TAILQ_INIT(&epollfd->poll_fds); + + if ((ec = pthread_mutex_init(&epollfd->mutex, NULL)) != 0) { + return ec; + } + + if ((ec = pthread_mutex_init(&epollfd->nr_polling_threads_mutex, + NULL)) != 0) { + pthread_mutex_destroy(&epollfd->mutex); + return ec; + } + + if ((ec = pthread_cond_init(&epollfd->nr_polling_threads_cond, + NULL)) != 0) { + pthread_mutex_destroy(&epollfd->nr_polling_threads_mutex); + pthread_mutex_destroy(&epollfd->mutex); + return ec; + } + + return 0; +} + +errno_t +epollfd_ctx_terminate(EpollFDCtx *epollfd) +{ + errno_t ec = 0; + errno_t ec_local; + + ec_local = pthread_cond_destroy(&epollfd->nr_polling_threads_cond); + ec = ec ? ec : ec_local; + ec_local = pthread_mutex_destroy(&epollfd->nr_polling_threads_mutex); + ec = ec ? ec : ec_local; + ec_local = pthread_mutex_destroy(&epollfd->mutex); + ec = ec ? ec : ec_local; + + RegisteredFDsNode *np; + RegisteredFDsNode *np_temp; + RB_FOREACH_SAFE(np, registered_fds_set_, &epollfd->registered_fds, + np_temp) + { + RB_REMOVE(registered_fds_set_, &epollfd->registered_fds, np); + registered_fds_node_destroy(np); + } + + free(epollfd->kevs); + free(epollfd->pfds); + if (epollfd->self_pipe[0] >= 0 && epollfd->self_pipe[1] >= 0) { + (void)close(epollfd->self_pipe[0]); + (void)close(epollfd->self_pipe[1]); + } + + return ec; +} + +static errno_t +epollfd_ctx_make_kevs_space(EpollFDCtx *epollfd, size_t cnt) +{ + assert(cnt > 0); + + if (cnt <= epollfd->kevs_length) { + return 0; + } + + size_t size; + if (__builtin_mul_overflow(cnt, sizeof(struct kevent), &size)) { + return ENOMEM; + } + + struct kevent *new_kevs = realloc(epollfd->kevs, size); + if (!new_kevs) { + return errno; + } + + epollfd->kevs = new_kevs; + epollfd->kevs_length = cnt; + + return 0; +} + +static errno_t +epollfd_ctx_make_pfds_space(EpollFDCtx *epollfd) +{ + size_t cnt = 1 + epollfd->poll_fds_size; + + if (cnt <= epollfd->pfds_length) { + return 0; + } + + size_t size; + if (__builtin_mul_overflow(cnt, sizeof(struct pollfd), &size)) { + return ENOMEM; + } + + struct pollfd *new_pfds = realloc(epollfd->pfds, size); + if (!new_pfds) { + return errno; + } + + epollfd->pfds = new_pfds; + epollfd->pfds_length = cnt; + + return 0; +} + +static errno_t +epollfd_ctx__add_self_trigger(EpollFDCtx *epollfd) +{ + struct kevent kevs[1]; + +#ifdef EVFILT_USER + EV_SET(&kevs[0], 0, EVFILT_USER, EV_ADD | EV_CLEAR, 0, 0, 0); +#else + if (epollfd->self_pipe[0] < 0 && epollfd->self_pipe[1] < 0) { + if (pipe2(epollfd->self_pipe, O_NONBLOCK | O_CLOEXEC) < 0) { + errno_t ec = errno; + epollfd->self_pipe[0] = epollfd->self_pipe[1] = -1; + return ec; + } + + assert(epollfd->self_pipe[0] >= 0); + assert(epollfd->self_pipe[1] >= 0); + } + + EV_SET(&kevs[0], epollfd->self_pipe[0], EVFILT_READ, /**/ + EV_ADD | EV_CLEAR, 0, 0, 0); +#endif + + if (kevent(epollfd->kq, kevs, 1, NULL, 0, NULL) < 0) { + return errno; + } + + return 0; +} + +static void +epollfd_ctx__trigger_self(EpollFDCtx *epollfd) +{ +#ifdef EVFILT_USER + struct kevent kevs[1]; + EV_SET(&kevs[0], 0, EVFILT_USER, 0, NOTE_TRIGGER, 0, 0); + (void)kevent(epollfd->kq, kevs, 1, NULL, 0, NULL); +#else + assert(epollfd->self_pipe[0] >= 0); + assert(epollfd->self_pipe[1] >= 0); + + char c = 0; + (void)write(epollfd->self_pipe[1], &c, 1); +#endif +} + +static void +epollfd_ctx__trigger_repoll(EpollFDCtx *epollfd) +{ + (void)pthread_mutex_lock(&epollfd->nr_polling_threads_mutex); + unsigned long nr_polling_threads = epollfd->nr_polling_threads; + (void)pthread_mutex_unlock(&epollfd->nr_polling_threads_mutex); + + if (nr_polling_threads == 0) { + return; + } + + epollfd_ctx__trigger_self(epollfd); + + (void)pthread_mutex_lock(&epollfd->nr_polling_threads_mutex); + while (epollfd->nr_polling_threads != 0) { + pthread_cond_wait(&epollfd->nr_polling_threads_cond, + &epollfd->nr_polling_threads_mutex); + } + (void)pthread_mutex_unlock(&epollfd->nr_polling_threads_mutex); + +#ifndef EVFILT_USER + char c[32]; + while (read(epollfd->self_pipe[0], c, sizeof(c)) >= 0) { + } +#endif +} + +static void +epollfd_ctx__remove_node_from_kq(EpollFDCtx *epollfd, + RegisteredFDsNode *fd2_node) +{ + if (fd2_node->is_on_pollfd_list) { + TAILQ_REMOVE(&epollfd->poll_fds, fd2_node, pollfd_list_entry); + fd2_node->is_on_pollfd_list = false; + assert(epollfd->poll_fds_size != 0); + --epollfd->poll_fds_size; + + epollfd_ctx__trigger_repoll(epollfd); + } + + if (fd2_node->self_pipe[0] >= 0) { + struct kevent kevs[1]; + EV_SET(&kevs[0], fd2_node->self_pipe[0], EVFILT_READ, /**/ + EV_DELETE, 0, 0, 0); + (void)kevent(epollfd->kq, kevs, 1, NULL, 0, NULL); + + char c[32]; + while (read(fd2_node->self_pipe[0], c, sizeof(c)) >= 0) { + } + } + + if (fd2_node->node_type == NODE_TYPE_POLL) { +#ifdef EVFILT_USER + struct kevent kevs[1]; + EV_SET(&kevs[0], (uintptr_t)fd2_node, EVFILT_USER, /**/ + EV_DELETE, 0, 0, 0); + (void)kevent(epollfd->kq, kevs, 1, NULL, 0, NULL); +#endif + } else { + struct kevent kevs[3]; + int fd2 = fd2_node->fd; + + EV_SET(&kevs[0], fd2, EVFILT_READ, /**/ + EV_DELETE | EV_RECEIPT, 0, 0, 0); + EV_SET(&kevs[1], fd2, EVFILT_WRITE, /**/ + EV_DELETE | EV_RECEIPT, 0, 0, 0); +#ifdef EVFILT_USER + EV_SET(&kevs[2], (uintptr_t)fd2_node, EVFILT_USER, /**/ + EV_DELETE | EV_RECEIPT, 0, 0, 0); +#endif + (void)kevent(epollfd->kq, kevs, 3, kevs, 3, NULL); + + fd2_node->has_evfilt_read = false; + fd2_node->has_evfilt_write = false; + fd2_node->has_evfilt_except = false; + } +} + +static errno_t +epollfd_ctx__register_events(EpollFDCtx *epollfd, RegisteredFDsNode *fd2_node) +{ + errno_t ec = 0; + + /* Only sockets support EPOLLRDHUP and EPOLLPRI. */ + if (fd2_node->node_type != NODE_TYPE_SOCKET) { + fd2_node->events &= ~(uint32_t)EPOLLRDHUP; + fd2_node->events &= ~(uint32_t)EPOLLPRI; + } + + int const fd2 = fd2_node->fd; + struct kevent kev[4] = { + {.data = 0}, + {.data = 0}, + {.data = 0}, + {.data = 0}, + }; + + assert(fd2 >= 0); + + int evfilt_read_index = -1; + int evfilt_write_index = -1; + + if (fd2_node->node_type != NODE_TYPE_POLL) { + if (fd2_node->is_registered) { + epollfd_ctx__remove_node_from_kq(epollfd, fd2_node); + } + + int n = 0; + + assert(!fd2_node->has_evfilt_read); + assert(!fd2_node->has_evfilt_write); + assert(!fd2_node->has_evfilt_except); + + NeededFilters needed_filters = get_needed_filters(fd2_node); + + if (needed_filters.evfilt_read) { + fd2_node->has_evfilt_read = true; + evfilt_read_index = n; + EV_SET(&kev[n++], fd2, EVFILT_READ, + EV_ADD | (needed_filters.evfilt_read & EV_CLEAR), + 0, 0, fd2_node); + } + if (needed_filters.evfilt_write) { + fd2_node->has_evfilt_write = true; + evfilt_write_index = n; + EV_SET(&kev[n++], fd2, EVFILT_WRITE, + EV_ADD | (needed_filters.evfilt_write & EV_CLEAR), + 0, 0, fd2_node); + } + + assert(n != 0); + + if (needed_filters.evfilt_except) { +#ifdef EVFILT_EXCEPT + fd2_node->has_evfilt_except = true; + EV_SET(&kev[n++], fd2, EVFILT_EXCEPT, + EV_ADD | (needed_filters.evfilt_except & EV_CLEAR), + NOTE_OOB, 0, fd2_node); +#else + assert(0); +#endif + } + + for (int i = 0; i < n; ++i) { + kev[i].flags |= EV_RECEIPT; + } + + int ret = kevent(epollfd->kq, kev, n, kev, n, NULL); + if (ret < 0) { + ec = errno; + goto out; + } + + assert(ret == n); + + for (int i = 0; i < n; ++i) { + assert((kev[i].flags & EV_ERROR) != 0); + } + } + + /* Check for fds that only support poll. */ + if (((fd2_node->node_type == NODE_TYPE_OTHER && + kev[0].data == ENODEV) || + fd2_node->node_type == NODE_TYPE_POLL)) { + + assert((fd2_node->events & /**/ + ~(uint32_t)(EPOLLIN | EPOLLOUT)) == 0); + assert(fd2_node->is_registered || + fd2_node->node_type == NODE_TYPE_OTHER); + + fd2_node->has_evfilt_read = false; + fd2_node->has_evfilt_write = false; + fd2_node->has_evfilt_except = false; + + fd2_node->node_type = NODE_TYPE_POLL; + + if ((ec = registered_fds_node_add_self_trigger(fd2_node, + epollfd)) != 0) { + goto out; + } + + if (!fd2_node->is_on_pollfd_list) { + if ((ec = /**/ + epollfd_ctx__add_self_trigger(epollfd)) != 0) { + goto out; + } + + TAILQ_INSERT_TAIL(&epollfd->poll_fds, fd2_node, + pollfd_list_entry); + fd2_node->is_on_pollfd_list = true; + ++epollfd->poll_fds_size; + } + + /* This is outside the above if because poll ".events" might + * have changed which needs a retriggering. */ + epollfd_ctx__trigger_repoll(epollfd); + + goto out; + } + + for (int i = 0; i < 4; ++i) { + if (kev[i].data != 0) { + if ((kev[i].data == EPIPE +#ifdef __NetBSD__ + || kev[i].data == EBADF +#endif + ) && + i == evfilt_write_index && + fd2_node->node_type == NODE_TYPE_FIFO) { + + fd2_node->eof_state = EOF_STATE_READ_EOF | + EOF_STATE_WRITE_EOF; + fd2_node->has_evfilt_write = false; + + if (evfilt_read_index < 0) { + if ((ec = registered_fds_node_add_self_trigger( + fd2_node, epollfd)) != 0) { + goto out; + } + + registered_fds_node_trigger_self( + fd2_node, epollfd); + } + } else { + ec = (int)kev[i].data; + goto out; + } + } + } + + ec = 0; + +out: + return ec; +} + +static void +epollfd_ctx_remove_node(EpollFDCtx *epollfd, RegisteredFDsNode *fd2_node) +{ + epollfd_ctx__remove_node_from_kq(epollfd, fd2_node); + + RB_REMOVE(registered_fds_set_, &epollfd->registered_fds, fd2_node); + assert(epollfd->registered_fds_size > 0); + --epollfd->registered_fds_size; + + registered_fds_node_destroy(fd2_node); +} + +#if defined(__FreeBSD__) +static void +modify_fifo_rights_from_capabilities(RegisteredFDsNode *fd2_node) +{ + assert(fd2_node->node_data.fifo.readable); + assert(fd2_node->node_data.fifo.writable); + + cap_rights_t rights; + memset(&rights, 0, sizeof(rights)); + + if (cap_rights_get(fd2_node->fd, &rights) == 0) { + cap_rights_t test_rights; + + cap_rights_init(&test_rights, CAP_READ); + bool has_read_rights = cap_rights_contains(&rights, + &test_rights); + + cap_rights_init(&test_rights, CAP_WRITE); + bool has_write_rights = cap_rights_contains(&rights, + &test_rights); + + if (has_read_rights != has_write_rights) { + fd2_node->node_data.fifo.readable = has_read_rights; + fd2_node->node_data.fifo.writable = has_write_rights; + } + } +} +#endif + +static errno_t +epollfd_ctx_add_node(EpollFDCtx *epollfd, int fd2, struct epoll_event *ev, + struct stat const *statbuf) +{ + RegisteredFDsNode *fd2_node = registered_fds_node_create(fd2); + if (!fd2_node) { + return ENOMEM; + } + + if (S_ISFIFO(statbuf->st_mode)) { + int tmp; + + if (ioctl(fd2_node->fd, FIONREAD, &tmp) < 0 && + errno == ENOTTY) { +#ifdef __FreeBSD__ + /* + * On FreeBSD we need to distinguish between kqueues + * and native eventfds. + */ + if (ioctl(fd2_node->fd, FIONBIO, &tmp) < 0 && + errno == ENOTTY) { + fd2_node->node_type = NODE_TYPE_KQUEUE; + } else { + fd2_node->node_type = NODE_TYPE_OTHER; + } +#else + fd2_node->node_type = NODE_TYPE_KQUEUE; +#endif + } else { + fd2_node->node_type = NODE_TYPE_FIFO; + + int fl = fcntl(fd2, F_GETFL, 0); + if (fl < 0) { + errno_t ec = errno; + registered_fds_node_destroy(fd2_node); + return ec; + } + + fl &= O_ACCMODE; + + if (fl == O_RDWR) { + fd2_node->node_data.fifo.readable = true; + fd2_node->node_data.fifo.writable = true; +#if defined(__FreeBSD__) + modify_fifo_rights_from_capabilities(fd2_node); +#endif + } else if (fl == O_WRONLY) { + fd2_node->node_data.fifo.writable = true; + } else if (fl == O_RDONLY) { + fd2_node->node_data.fifo.readable = true; + } else { + registered_fds_node_destroy(fd2_node); + return EINVAL; + } + } + } else if (S_ISSOCK(statbuf->st_mode)) { + fd2_node->node_type = NODE_TYPE_SOCKET; + } else { + /* May also be NODE_TYPE_POLL, + will be checked when registering. */ + fd2_node->node_type = NODE_TYPE_OTHER; + } + + registered_fds_node_update_flags_from_epoll_event(fd2_node, ev); + + void *colliding_node = RB_INSERT(registered_fds_set_, + &epollfd->registered_fds, fd2_node); + (void)colliding_node; + assert(colliding_node == NULL); + ++epollfd->registered_fds_size; + + errno_t ec = epollfd_ctx__register_events(epollfd, fd2_node); + if (ec != 0) { + epollfd_ctx_remove_node(epollfd, fd2_node); + return ec; + } + + fd2_node->is_registered = true; + + return 0; +} + +static errno_t +epollfd_ctx_modify_node(EpollFDCtx *epollfd, RegisteredFDsNode *fd2_node, + struct epoll_event *ev) +{ + registered_fds_node_update_flags_from_epoll_event(fd2_node, ev); + + assert(fd2_node->is_registered); + + errno_t ec = epollfd_ctx__register_events(epollfd, fd2_node); + if (ec != 0) { + epollfd_ctx_remove_node(epollfd, fd2_node); + return ec; + } + + return 0; +} + +static errno_t +epollfd_ctx_ctl_impl(EpollFDCtx *epollfd, int op, int fd2, + struct epoll_event *ev) +{ + assert(op == EPOLL_CTL_DEL || ev != NULL); + + if (epollfd->kq == fd2) { + return EINVAL; + } + + if (op != EPOLL_CTL_DEL && + ((ev->events & + ~(uint32_t)(EPOLLIN | EPOLLOUT | EPOLLRDHUP | /**/ + EPOLLPRI | /* unsupported by FreeBSD's kqueue! */ + EPOLLHUP | EPOLLERR | /**/ + EPOLLET | EPOLLONESHOT)))) { + return EINVAL; + } + + RegisteredFDsNode *fd2_node; + { + RegisteredFDsNode find; + find.fd = fd2; + + fd2_node = RB_FIND(registered_fds_set_, /**/ + &epollfd->registered_fds, &find); + } + + struct stat statbuf; + if (fstat(fd2, &statbuf) < 0) { + errno_t ec = errno; + + /* If the fstat fails for any reason we must clear + * internal state to avoid EEXIST errors in future + * calls to epoll_ctl. */ + if (fd2_node) { + epollfd_ctx_remove_node(epollfd, fd2_node); + } + + return ec; + } + + errno_t ec; + + if (op == EPOLL_CTL_ADD) { + ec = fd2_node + ? EEXIST + : epollfd_ctx_add_node(epollfd, fd2, ev, &statbuf); + } else if (op == EPOLL_CTL_DEL) { + ec = !fd2_node + ? ENOENT + : (epollfd_ctx_remove_node(epollfd, fd2_node), 0); + } else if (op == EPOLL_CTL_MOD) { + ec = !fd2_node + ? ENOENT + : epollfd_ctx_modify_node(epollfd, fd2_node, ev); + } else { + ec = EINVAL; + } + + return ec; +} + +void +epollfd_ctx_fill_pollfds(EpollFDCtx *epollfd, struct pollfd *pfds) +{ + pfds[0] = (struct pollfd){.fd = epollfd->kq, .events = POLLIN}; + + RegisteredFDsNode *poll_node; + size_t i = 1; + TAILQ_FOREACH(poll_node, &epollfd->poll_fds, pollfd_list_entry) + { + pfds[i++] = (struct pollfd){ + .fd = poll_node->fd, + .events = poll_node->node_type == NODE_TYPE_POLL + ? (short)poll_node->events + : POLLPRI, + }; + } +} + +errno_t +epollfd_ctx_ctl(EpollFDCtx *epollfd, int op, int fd2, struct epoll_event *ev) +{ + errno_t ec; + + (void)pthread_mutex_lock(&epollfd->mutex); + ec = epollfd_ctx_ctl_impl(epollfd, op, fd2, ev); + (void)pthread_mutex_unlock(&epollfd->mutex); + + return ec; +} + +static errno_t +epollfd_ctx_wait_impl(EpollFDCtx *epollfd, struct epoll_event *ev, int cnt, + int *actual_cnt) +{ + errno_t ec; + + assert(cnt >= 1); + + ec = epollfd_ctx_make_pfds_space(epollfd); + if (ec != 0) { + return ec; + } + + epollfd_ctx_fill_pollfds(epollfd, epollfd->pfds); + + int n = poll(epollfd->pfds, (nfds_t)(1 + epollfd->poll_fds_size), 0); + if (n < 0) { + return errno; + } + if (n == 0) { + *actual_cnt = 0; + return 0; + } + + { + RegisteredFDsNode *poll_node, *tmp_poll_node; + size_t i = 1; + TAILQ_FOREACH_SAFE(poll_node, &epollfd->poll_fds, + pollfd_list_entry, tmp_poll_node) + { + struct pollfd *pfd = &epollfd->pfds[i++]; + + if (pfd->revents & POLLNVAL) { + epollfd_ctx_remove_node(epollfd, poll_node); + } else if (pfd->revents) { + registered_fds_node_trigger_self(poll_node, + epollfd); + } + } + } + +again:; + + /* + * Each registered fd can produce a maximum of 3 kevents. If + * the provided space in 'ev' is large enough to hold results + * for all registered fds, provide enough space for the kevent + * call as well. Add some wiggle room for the 'poll only fd' + * notification mechanism. + */ + if ((size_t)cnt >= epollfd->registered_fds_size) { + if (__builtin_add_overflow(cnt, 1, &cnt)) { + return ENOMEM; + } + if (__builtin_mul_overflow(cnt, 3, &cnt)) { + return ENOMEM; + } + } + + ec = epollfd_ctx_make_kevs_space(epollfd, (size_t)cnt); + if (ec != 0) { + return ec; + } + + struct kevent *kevs = epollfd->kevs; + assert(kevs != NULL); + + n = kevent(epollfd->kq, NULL, 0, kevs, cnt, &(struct timespec){0, 0}); + if (n < 0) { + return errno; + } + + int j = 0; + + for (int i = 0; i < n; ++i) { + RegisteredFDsNode *fd2_node = + (RegisteredFDsNode *)kevs[i].udata; + + if (!fd2_node) { +#ifdef EVFILT_USER + assert(kevs[i].filter == EVFILT_USER); +#else + assert(kevs[i].filter == EVFILT_READ); +#endif + assert(kevs[i].udata == 0); + continue; + } + + uint32_t old_revents = fd2_node->revents; + NeededFilters old_needed_filters = get_needed_filters( + fd2_node); + + registered_fds_node_feed_event(fd2_node, epollfd, &kevs[i]); + + if (fd2_node->node_type != NODE_TYPE_POLL && + !(fd2_node->is_edge_triggered && + fd2_node->eof_state == + (EOF_STATE_READ_EOF | EOF_STATE_WRITE_EOF) && + fd2_node->node_type != NODE_TYPE_FIFO)) { + + NeededFilters needed_filters = get_needed_filters( + fd2_node); + + if (old_needed_filters.evfilt_read != + needed_filters.evfilt_read || + old_needed_filters.evfilt_write != + needed_filters.evfilt_write) { + + if (epollfd_ctx__register_events(epollfd, + fd2_node) != 0) { + epollfd_ctx__remove_node_from_kq( + epollfd, fd2_node); + } + } + } + + if (fd2_node->revents && !old_revents) { + ev[j++].data.ptr = fd2_node; + } + } + + { + int completion_kq = -1; + + for (int i = 0; i < j; ++i) { + RegisteredFDsNode *fd2_node = + (RegisteredFDsNode *)ev[i].data.ptr; + + if (n == cnt || fd2_node->is_edge_triggered) { + registered_fds_node_register_for_completion( + &completion_kq, fd2_node); + } + } + + registered_fds_node_complete(completion_kq); + } + + for (int i = 0; i < j; ++i) { + RegisteredFDsNode *fd2_node = + (RegisteredFDsNode *)ev[i].data.ptr; + + ev[i].events = fd2_node->revents; + ev[i].data = fd2_node->data; + + fd2_node->revents = 0; + fd2_node->got_evfilt_read = false; + fd2_node->got_evfilt_write = false; + fd2_node->got_evfilt_except = false; + + if (fd2_node->is_oneshot) { + epollfd_ctx__remove_node_from_kq(epollfd, fd2_node); + } + } + + if (n && j == 0) { + goto again; + } + + *actual_cnt = j; + return 0; +} + +errno_t +epollfd_ctx_wait(EpollFDCtx *epollfd, struct epoll_event *ev, int cnt, + int *actual_cnt) +{ + errno_t ec; + + (void)pthread_mutex_lock(&epollfd->mutex); + ec = epollfd_ctx_wait_impl(epollfd, ev, cnt, actual_cnt); + (void)pthread_mutex_unlock(&epollfd->mutex); + + return ec; +} diff --git a/tpws/epoll-shim/src/epollfd_ctx.h b/tpws/epoll-shim/src/epollfd_ctx.h new file mode 100644 index 0000000..1af7195 --- /dev/null +++ b/tpws/epoll-shim/src/epollfd_ctx.h @@ -0,0 +1,108 @@ +#ifndef EPOLLFD_CTX_H_ +#define EPOLLFD_CTX_H_ + +#include "fix.h" + +#define SHIM_SYS_SHIM_HELPERS +#include + +#include +#include + +#include +#include +#include + +#include +#include + +struct registered_fds_node_; +typedef struct registered_fds_node_ RegisteredFDsNode; + +typedef enum { + EOF_STATE_READ_EOF = 0x01, + EOF_STATE_WRITE_EOF = 0x02, +} EOFState; + +typedef enum { + NODE_TYPE_FIFO = 1, + NODE_TYPE_SOCKET = 2, + NODE_TYPE_KQUEUE = 3, + NODE_TYPE_OTHER = 4, + NODE_TYPE_POLL = 5, +} NodeType; + +struct registered_fds_node_ { + RB_ENTRY(registered_fds_node_) entry; + TAILQ_ENTRY(registered_fds_node_) pollfd_list_entry; + + int fd; + epoll_data_t data; + + bool is_registered; + + bool has_evfilt_read; + bool has_evfilt_write; + bool has_evfilt_except; + + bool got_evfilt_read; + bool got_evfilt_write; + bool got_evfilt_except; + + NodeType node_type; + union { + struct { + bool readable; + bool writable; + } fifo; + } node_data; + int eof_state; + bool pollpri_active; + + uint16_t events; + uint32_t revents; + + bool is_edge_triggered; + bool is_oneshot; + + bool is_on_pollfd_list; + int self_pipe[2]; +}; + +typedef TAILQ_HEAD(pollfds_list_, registered_fds_node_) PollFDList; +typedef RB_HEAD(registered_fds_set_, registered_fds_node_) RegisteredFDsSet; + +typedef struct { + int kq; // non owning + pthread_mutex_t mutex; + + PollFDList poll_fds; + size_t poll_fds_size; + + RegisteredFDsSet registered_fds; + size_t registered_fds_size; + + struct kevent *kevs; + size_t kevs_length; + + struct pollfd *pfds; + size_t pfds_length; + + pthread_mutex_t nr_polling_threads_mutex; + pthread_cond_t nr_polling_threads_cond; + unsigned long nr_polling_threads; + + int self_pipe[2]; +} EpollFDCtx; + +errno_t epollfd_ctx_init(EpollFDCtx *epollfd, int kq); +errno_t epollfd_ctx_terminate(EpollFDCtx *epollfd); + +void epollfd_ctx_fill_pollfds(EpollFDCtx *epollfd, struct pollfd *pfds); + +errno_t epollfd_ctx_ctl(EpollFDCtx *epollfd, int op, int fd2, + struct epoll_event *ev); +errno_t epollfd_ctx_wait(EpollFDCtx *epollfd, struct epoll_event *ev, int cnt, + int *actual_cnt); + +#endif diff --git a/tpws/epoll-shim/src/eventfd_ctx.h b/tpws/epoll-shim/src/eventfd_ctx.h new file mode 100644 index 0000000..3e5bb55 --- /dev/null +++ b/tpws/epoll-shim/src/eventfd_ctx.h @@ -0,0 +1,31 @@ +#ifndef EVENTFD_CTX_H_ +#define EVENTFD_CTX_H_ + +#include "fix.h" + +#include +#include +#include + +#include + +#define EVENTFD_CTX_FLAG_SEMAPHORE (1 << 0) + +typedef struct { + int kq_; // non owning + int flags_; + pthread_mutex_t mutex_; + + bool is_signalled_; + int self_pipe_[2]; // only used if EVFILT_USER is not available + uint_least64_t counter_; +} EventFDCtx; + +errno_t eventfd_ctx_init(EventFDCtx *eventfd, int kq, unsigned int counter, + int flags); +errno_t eventfd_ctx_terminate(EventFDCtx *eventfd); + +errno_t eventfd_ctx_write(EventFDCtx *eventfd, uint64_t value); +errno_t eventfd_ctx_read(EventFDCtx *eventfd, uint64_t *value); + +#endif diff --git a/tpws/epoll-shim/src/fix.c b/tpws/epoll-shim/src/fix.c new file mode 100644 index 0000000..6fbd3f5 --- /dev/null +++ b/tpws/epoll-shim/src/fix.c @@ -0,0 +1,19 @@ +#include "fix.h" + +#ifdef __APPLE__ + +#include + +int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *tmo_p, const sigset_t *sigmask) +{ + // macos does not implement ppoll + // this is a hacky ppoll shim. only for tpws which does not require sigmask + if (sigmask) + { + errno = EINVAL; + return -1; + } + return poll(fds,nfds,tmo_p ? tmo_p->tv_sec*1000 + tmo_p->tv_nsec/1000000 : -1); +} + +#endif diff --git a/tpws/epoll-shim/src/fix.h b/tpws/epoll-shim/src/fix.h new file mode 100644 index 0000000..ebefc14 --- /dev/null +++ b/tpws/epoll-shim/src/fix.h @@ -0,0 +1,20 @@ +#pragma once + +#ifndef _ERRNO_T_DEFINED +#define _ERRNO_T_DEFINED +typedef int errno_t; +#endif + +#ifdef __APPLE__ + +#include +#include +#include + +struct itimerspec { + struct timespec it_interval; + struct timespec it_value; +}; +int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *tmo_p, const sigset_t *sigmask); + +#endif diff --git a/tpws/epoll-shim/src/signalfd_ctx.h b/tpws/epoll-shim/src/signalfd_ctx.h new file mode 100644 index 0000000..8623f63 --- /dev/null +++ b/tpws/epoll-shim/src/signalfd_ctx.h @@ -0,0 +1,19 @@ +#ifndef SIGNALFD_CTX_H_ +#define SIGNALFD_CTX_H_ + +#include "fix.h" + +#include +#include +#include + +typedef struct { + int kq; // non owning +} SignalFDCtx; + +errno_t signalfd_ctx_init(SignalFDCtx *signalfd, int kq, const sigset_t *sigs); +errno_t signalfd_ctx_terminate(SignalFDCtx *signalfd); + +errno_t signalfd_ctx_read(SignalFDCtx *signalfd, uint32_t *ident); + +#endif diff --git a/tpws/epoll-shim/src/timerfd_ctx.h b/tpws/epoll-shim/src/timerfd_ctx.h new file mode 100644 index 0000000..8b41507 --- /dev/null +++ b/tpws/epoll-shim/src/timerfd_ctx.h @@ -0,0 +1,38 @@ +#ifndef TIMERFD_CTX_H_ +#define TIMERFD_CTX_H_ + +#include "fix.h" + +#include +#include +#include +#include + +#include +#include + +typedef struct { + int kq; // non owning + int flags; + pthread_mutex_t mutex; + + int clockid; + /* + * Next expiration time, absolute (clock given by clockid). + * If it_interval is != 0, it is a periodic timer. + * If it_value is == 0, the timer is disarmed. + */ + struct itimerspec current_itimerspec; + uint64_t nr_expirations; +} TimerFDCtx; + +errno_t timerfd_ctx_init(TimerFDCtx *timerfd, int kq, int clockid); +errno_t timerfd_ctx_terminate(TimerFDCtx *timerfd); + +errno_t timerfd_ctx_settime(TimerFDCtx *timerfd, int flags, + struct itimerspec const *new, struct itimerspec *old); +errno_t timerfd_ctx_gettime(TimerFDCtx *timerfd, struct itimerspec *cur); + +errno_t timerfd_ctx_read(TimerFDCtx *timerfd, uint64_t *value); + +#endif diff --git a/tpws/gzip.c b/tpws/gzip.c new file mode 100644 index 0000000..cb46670 --- /dev/null +++ b/tpws/gzip.c @@ -0,0 +1,82 @@ +#include "gzip.h" +#include +#include +#include + +#define ZCHUNK 16384 +#define BUFMIN 128 +#define BUFCHUNK (1024*128) + +int z_readfile(FILE *F, char **buf, size_t *size) +{ + z_stream zs; + int r; + unsigned char in[ZCHUNK]; + size_t bufsize; + void *newbuf; + + memset(&zs, 0, sizeof(zs)); + + *buf = NULL; + bufsize = *size = 0; + + r = inflateInit2(&zs, 47); + if (r != Z_OK) return r; + + do + { + zs.avail_in = fread(in, 1, sizeof(in), F); + if (ferror(F)) + { + r = Z_ERRNO; + goto zerr; + } + if (!zs.avail_in) break; + zs.next_in = in; + do + { + if ((bufsize - *size) < BUFMIN) + { + bufsize += BUFCHUNK; + newbuf = *buf ? realloc(*buf, bufsize) : malloc(bufsize); + if (!newbuf) + { + r = Z_MEM_ERROR; + goto zerr; + } + *buf = newbuf; + } + zs.avail_out = bufsize - *size; + zs.next_out = (unsigned char*)(*buf + *size); + r = inflate(&zs, Z_NO_FLUSH); + if (r != Z_OK && r != Z_STREAM_END) goto zerr; + *size = bufsize - zs.avail_out; + } while (r == Z_OK && zs.avail_in); + } while (r == Z_OK); + + if (*size < bufsize) + { + // free extra space + if ((newbuf = realloc(*buf, *size))) *buf = newbuf; + } + + inflateEnd(&zs); + return Z_OK; + +zerr: + inflateEnd(&zs); + if (*buf) + { + free(*buf); + *buf = NULL; + } + return r; +} + +bool is_gzip(FILE* F) +{ + unsigned char magic[2]; + bool b = !fseek(F, 0, SEEK_SET) && fread(magic, 1, 2, F) == 2 && magic[0] == 0x1F && magic[1] == 0x8B; + fseek(F, 0, SEEK_SET); + return b; +} diff --git a/tpws/gzip.h b/tpws/gzip.h new file mode 100644 index 0000000..15e30d2 --- /dev/null +++ b/tpws/gzip.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include +#include + +int z_readfile(FILE *F,char **buf,size_t *size); +bool is_gzip(FILE* F); diff --git a/tpws/helpers.c b/tpws/helpers.c new file mode 100644 index 0000000..29427da --- /dev/null +++ b/tpws/helpers.c @@ -0,0 +1,146 @@ +#define _GNU_SOURCE + +#include "helpers.h" +#include +#include +#include +#include +#include +#include +#include + +char *strncasestr(const char *s,const char *find, size_t slen) +{ + char c, sc; + size_t len; + + if ((c = *find++) != '\0') + { + len = strlen(find); + do + { + do + { + if (slen-- < 1 || (sc = *s++) == '\0') return NULL; + } while (toupper(c) != toupper(sc)); + if (len > slen) return NULL; + } while (strncasecmp(s, find, len) != 0); + s--; + } + return (char *)s; +} + +void print_sockaddr(const struct sockaddr *sa) +{ + char str[64]; + switch (sa->sa_family) + { + case AF_INET: + if (inet_ntop(sa->sa_family, &((struct sockaddr_in*)sa)->sin_addr, str, sizeof(str))) + printf("%s:%d", str, ntohs(((struct sockaddr_in*)sa)->sin_port)); + break; + case AF_INET6: + if (inet_ntop(sa->sa_family, &((struct sockaddr_in6*)sa)->sin6_addr, str, sizeof(str))) + printf("%s:%d", str, ntohs(((struct sockaddr_in6*)sa)->sin6_port)); + break; + default: + printf("UNKNOWN_FAMILY_%d", sa->sa_family); + } +} + + +// -1 = error, 0 = not local, 1 = local +bool check_local_ip(const struct sockaddr *saddr) +{ + struct ifaddrs *addrs,*a; + + if (getifaddrs(&addrs)<0) return false; + a = addrs; + + bool bres=false; + while (a) + { + if (a->ifa_addr && sacmp(a->ifa_addr,saddr)) + { + bres=true; + break; + } + a = a->ifa_next; + } + + freeifaddrs(addrs); + return bres; +} +void print_addrinfo(const struct addrinfo *ai) +{ + char str[64]; + while (ai) + { + switch (ai->ai_family) + { + case AF_INET: + if (inet_ntop(ai->ai_family, &((struct sockaddr_in*)ai->ai_addr)->sin_addr, str, sizeof(str))) + printf("%s\n", str); + break; + case AF_INET6: + if (inet_ntop(ai->ai_family, &((struct sockaddr_in6*)ai->ai_addr)->sin6_addr, str, sizeof(str))) + printf( "%s\n", str); + break; + } + ai = ai->ai_next; + } +} + + + +bool saismapped(const struct sockaddr_in6 *sa) +{ + // ::ffff:1.2.3.4 + return !memcmp(sa->sin6_addr.s6_addr,"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff",12); +} +bool samappedcmp(const struct sockaddr_in *sa1,const struct sockaddr_in6 *sa2) +{ + return saismapped(sa2) && !memcmp(sa2->sin6_addr.s6_addr+12,&sa1->sin_addr.s_addr,4); +} +bool sacmp(const struct sockaddr *sa1,const struct sockaddr *sa2) +{ + return sa1->sa_family==AF_INET && sa2->sa_family==AF_INET && !memcmp(&((struct sockaddr_in*)sa1)->sin_addr,&((struct sockaddr_in*)sa2)->sin_addr,sizeof(struct in_addr)) || + sa1->sa_family==AF_INET6 && sa2->sa_family==AF_INET6 && !memcmp(&((struct sockaddr_in6*)sa1)->sin6_addr,&((struct sockaddr_in6*)sa2)->sin6_addr,sizeof(struct in6_addr)) || + sa1->sa_family==AF_INET && sa2->sa_family==AF_INET6 && samappedcmp((struct sockaddr_in*)sa1,(struct sockaddr_in6*)sa2) || + sa1->sa_family==AF_INET6 && sa2->sa_family==AF_INET && samappedcmp((struct sockaddr_in*)sa2,(struct sockaddr_in6*)sa1); +} +uint16_t saport(const struct sockaddr *sa) +{ + return htons(sa->sa_family==AF_INET ? ((struct sockaddr_in*)sa)->sin_port : + sa->sa_family==AF_INET6 ? ((struct sockaddr_in6*)sa)->sin6_port : 0); +} +bool saconvmapped(struct sockaddr_storage *a) +{ + if ((a->ss_family == AF_INET6) && saismapped((struct sockaddr_in6*)a)) + { + uint32_t ip4 = *(uint32_t*)(((struct sockaddr_in6*)a)->sin6_addr.s6_addr+12); + uint16_t port = ((struct sockaddr_in6*)a)->sin6_port; + a->ss_family = AF_INET; + ((struct sockaddr_in*)a)->sin_addr.s_addr = ip4; + ((struct sockaddr_in*)a)->sin_port = port; + return true; + } + return false; +} + + + +int set_keepalive(int fd) +{ + int yes=1; + return setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(int))!=-1; +} +int get_so_error(int fd) +{ + // getsockopt(SO_ERROR) clears error + int errn; + socklen_t optlen = sizeof(errn); + if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &errn, &optlen) == -1) + errn=errno; + return errn; +} diff --git a/tpws/helpers.h b/tpws/helpers.h new file mode 100644 index 0000000..25079e0 --- /dev/null +++ b/tpws/helpers.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include +#include + +char *strncasestr(const char *s,const char *find, size_t slen); + +void print_sockaddr(const struct sockaddr *sa); +void print_addrinfo(const struct addrinfo *ai); +bool check_local_ip(const struct sockaddr *saddr); + +bool saismapped(const struct sockaddr_in6 *sa); +bool samappedcmp(const struct sockaddr_in *sa1,const struct sockaddr_in6 *sa2); +bool sacmp(const struct sockaddr *sa1,const struct sockaddr *sa2); +uint16_t saport(const struct sockaddr *sa); +// true = was converted +bool saconvmapped(struct sockaddr_storage *a); + +int set_keepalive(int fd); +int get_so_error(int fd); diff --git a/tpws/hostlist.c b/tpws/hostlist.c new file mode 100644 index 0000000..87116e2 --- /dev/null +++ b/tpws/hostlist.c @@ -0,0 +1,112 @@ +#include +#include "hostlist.h" +#include "gzip.h" +#include "params.h" + +static bool addpool(strpool **hostlist, char **s, char *end) +{ + char *p; + + // advance until eol lowering all chars + for (p = *s; p +#include "strpool.h" + +bool LoadHostList(strpool **hostlist, char *filename); +bool SearchHostList(strpool *hostlist, const char *host, bool debug); diff --git a/tpws/macos/net/pfvar.h b/tpws/macos/net/pfvar.h new file mode 100644 index 0000000..e6f6b6e --- /dev/null +++ b/tpws/macos/net/pfvar.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +// taken from an older apple SDK +// some fields are different from BSDs + +#define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook) + +enum { PF_INOUT, PF_IN, PF_OUT, PF_FWD }; + +struct pf_addr { + union { + struct in_addr v4; + struct in6_addr v6; + u_int8_t addr8[16]; + u_int16_t addr16[8]; + u_int32_t addr32[4]; + } pfa; /* 128-bit address */ +#define v4 pfa.v4 +#define v6 pfa.v6 +#define addr8 pfa.addr8 +#define addr16 pfa.addr16 +#define addr32 pfa.addr32 +}; + +union pf_state_xport { + u_int16_t port; + u_int16_t call_id; + u_int32_t spi; +}; + +struct pfioc_natlook { + struct pf_addr saddr; + struct pf_addr daddr; + struct pf_addr rsaddr; + struct pf_addr rdaddr; + union pf_state_xport sxport; + union pf_state_xport dxport; + union pf_state_xport rsxport; + union pf_state_xport rdxport; + sa_family_t af; + u_int8_t proto; + u_int8_t proto_variant; + u_int8_t direction; +}; diff --git a/tpws/macos/sys/tree.h b/tpws/macos/sys/tree.h new file mode 100644 index 0000000..697fddf --- /dev/null +++ b/tpws/macos/sys/tree.h @@ -0,0 +1,803 @@ +/* $NetBSD: tree.h,v 1.8 2004/03/28 19:38:30 provos Exp $ */ +/* $OpenBSD: tree.h,v 1.7 2002/10/17 21:51:54 art Exp $ */ +/* $FreeBSD: releng/12.2/sys/sys/tree.h 326256 2017-11-27 15:01:59Z pfg $ */ + +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright 2002 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _SYS_TREE_H_ +#define _SYS_TREE_H_ + +#include + +/* + * This file defines data structures for different types of trees: + * splay trees and red-black trees. + * + * A splay tree is a self-organizing data structure. Every operation + * on the tree causes a splay to happen. The splay moves the requested + * node to the root of the tree and partly rebalances it. + * + * This has the benefit that request locality causes faster lookups as + * the requested nodes move to the top of the tree. On the other hand, + * every lookup causes memory writes. + * + * The Balance Theorem bounds the total access time for m operations + * and n inserts on an initially empty tree as O((m + n)lg n). The + * amortized cost for a sequence of m accesses to a splay tree is O(lg n); + * + * A red-black tree is a binary search tree with the node color as an + * extra attribute. It fulfills a set of conditions: + * - every search path from the root to a leaf consists of the + * same number of black nodes, + * - each red node (except for the root) has a black parent, + * - each leaf node is black. + * + * Every operation on a red-black tree is bounded as O(lg n). + * The maximum height of a red-black tree is 2lg (n+1). + */ + +#define SPLAY_HEAD(name, type) \ +struct name { \ + struct type *sph_root; /* root of the tree */ \ +} + +#define SPLAY_INITIALIZER(root) \ + { NULL } + +#define SPLAY_INIT(root) do { \ + (root)->sph_root = NULL; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ENTRY(type) \ +struct { \ + struct type *spe_left; /* left element */ \ + struct type *spe_right; /* right element */ \ +} + +#define SPLAY_LEFT(elm, field) (elm)->field.spe_left +#define SPLAY_RIGHT(elm, field) (elm)->field.spe_right +#define SPLAY_ROOT(head) (head)->sph_root +#define SPLAY_EMPTY(head) (SPLAY_ROOT(head) == NULL) + +/* SPLAY_ROTATE_{LEFT,RIGHT} expect that tmp hold SPLAY_{RIGHT,LEFT} */ +#define SPLAY_ROTATE_RIGHT(head, tmp, field) do { \ + SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(tmp, field); \ + SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ + (head)->sph_root = tmp; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ROTATE_LEFT(head, tmp, field) do { \ + SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(tmp, field); \ + SPLAY_LEFT(tmp, field) = (head)->sph_root; \ + (head)->sph_root = tmp; \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_LINKLEFT(head, tmp, field) do { \ + SPLAY_LEFT(tmp, field) = (head)->sph_root; \ + tmp = (head)->sph_root; \ + (head)->sph_root = SPLAY_LEFT((head)->sph_root, field); \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_LINKRIGHT(head, tmp, field) do { \ + SPLAY_RIGHT(tmp, field) = (head)->sph_root; \ + tmp = (head)->sph_root; \ + (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field); \ +} while (/*CONSTCOND*/ 0) + +#define SPLAY_ASSEMBLE(head, node, left, right, field) do { \ + SPLAY_RIGHT(left, field) = SPLAY_LEFT((head)->sph_root, field); \ + SPLAY_LEFT(right, field) = SPLAY_RIGHT((head)->sph_root, field);\ + SPLAY_LEFT((head)->sph_root, field) = SPLAY_RIGHT(node, field); \ + SPLAY_RIGHT((head)->sph_root, field) = SPLAY_LEFT(node, field); \ +} while (/*CONSTCOND*/ 0) + +/* Generates prototypes and inline functions */ + +#define SPLAY_PROTOTYPE(name, type, field, cmp) \ +void name##_SPLAY(struct name *, struct type *); \ +void name##_SPLAY_MINMAX(struct name *, int); \ +struct type *name##_SPLAY_INSERT(struct name *, struct type *); \ +struct type *name##_SPLAY_REMOVE(struct name *, struct type *); \ + \ +/* Finds the node with the same key as elm */ \ +static __inline struct type * \ +name##_SPLAY_FIND(struct name *head, struct type *elm) \ +{ \ + if (SPLAY_EMPTY(head)) \ + return(NULL); \ + name##_SPLAY(head, elm); \ + if ((cmp)(elm, (head)->sph_root) == 0) \ + return (head->sph_root); \ + return (NULL); \ +} \ + \ +static __inline struct type * \ +name##_SPLAY_NEXT(struct name *head, struct type *elm) \ +{ \ + name##_SPLAY(head, elm); \ + if (SPLAY_RIGHT(elm, field) != NULL) { \ + elm = SPLAY_RIGHT(elm, field); \ + while (SPLAY_LEFT(elm, field) != NULL) { \ + elm = SPLAY_LEFT(elm, field); \ + } \ + } else \ + elm = NULL; \ + return (elm); \ +} \ + \ +static __inline struct type * \ +name##_SPLAY_MIN_MAX(struct name *head, int val) \ +{ \ + name##_SPLAY_MINMAX(head, val); \ + return (SPLAY_ROOT(head)); \ +} + +/* Main splay operation. + * Moves node close to the key of elm to top + */ +#define SPLAY_GENERATE(name, type, field, cmp) \ +struct type * \ +name##_SPLAY_INSERT(struct name *head, struct type *elm) \ +{ \ + if (SPLAY_EMPTY(head)) { \ + SPLAY_LEFT(elm, field) = SPLAY_RIGHT(elm, field) = NULL; \ + } else { \ + int __comp; \ + name##_SPLAY(head, elm); \ + __comp = (cmp)(elm, (head)->sph_root); \ + if(__comp < 0) { \ + SPLAY_LEFT(elm, field) = SPLAY_LEFT((head)->sph_root, field);\ + SPLAY_RIGHT(elm, field) = (head)->sph_root; \ + SPLAY_LEFT((head)->sph_root, field) = NULL; \ + } else if (__comp > 0) { \ + SPLAY_RIGHT(elm, field) = SPLAY_RIGHT((head)->sph_root, field);\ + SPLAY_LEFT(elm, field) = (head)->sph_root; \ + SPLAY_RIGHT((head)->sph_root, field) = NULL; \ + } else \ + return ((head)->sph_root); \ + } \ + (head)->sph_root = (elm); \ + return (NULL); \ +} \ + \ +struct type * \ +name##_SPLAY_REMOVE(struct name *head, struct type *elm) \ +{ \ + struct type *__tmp; \ + if (SPLAY_EMPTY(head)) \ + return (NULL); \ + name##_SPLAY(head, elm); \ + if ((cmp)(elm, (head)->sph_root) == 0) { \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL) { \ + (head)->sph_root = SPLAY_RIGHT((head)->sph_root, field);\ + } else { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + (head)->sph_root = SPLAY_LEFT((head)->sph_root, field);\ + name##_SPLAY(head, elm); \ + SPLAY_RIGHT((head)->sph_root, field) = __tmp; \ + } \ + return (elm); \ + } \ + return (NULL); \ +} \ + \ +void \ +name##_SPLAY(struct name *head, struct type *elm) \ +{ \ + struct type __node, *__left, *__right, *__tmp; \ + int __comp; \ +\ + SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ + __left = __right = &__node; \ +\ + while ((__comp = (cmp)(elm, (head)->sph_root)) != 0) { \ + if (__comp < 0) { \ + __tmp = SPLAY_LEFT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if ((cmp)(elm, __tmp) < 0){ \ + SPLAY_ROTATE_RIGHT(head, __tmp, field); \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKLEFT(head, __right, field); \ + } else if (__comp > 0) { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if ((cmp)(elm, __tmp) > 0){ \ + SPLAY_ROTATE_LEFT(head, __tmp, field); \ + if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKRIGHT(head, __left, field); \ + } \ + } \ + SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ +} \ + \ +/* Splay with either the minimum or the maximum element \ + * Used to find minimum or maximum element in tree. \ + */ \ +void name##_SPLAY_MINMAX(struct name *head, int __comp) \ +{ \ + struct type __node, *__left, *__right, *__tmp; \ +\ + SPLAY_LEFT(&__node, field) = SPLAY_RIGHT(&__node, field) = NULL;\ + __left = __right = &__node; \ +\ + while (1) { \ + if (__comp < 0) { \ + __tmp = SPLAY_LEFT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if (__comp < 0){ \ + SPLAY_ROTATE_RIGHT(head, __tmp, field); \ + if (SPLAY_LEFT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKLEFT(head, __right, field); \ + } else if (__comp > 0) { \ + __tmp = SPLAY_RIGHT((head)->sph_root, field); \ + if (__tmp == NULL) \ + break; \ + if (__comp > 0) { \ + SPLAY_ROTATE_LEFT(head, __tmp, field); \ + if (SPLAY_RIGHT((head)->sph_root, field) == NULL)\ + break; \ + } \ + SPLAY_LINKRIGHT(head, __left, field); \ + } \ + } \ + SPLAY_ASSEMBLE(head, &__node, __left, __right, field); \ +} + +#define SPLAY_NEGINF -1 +#define SPLAY_INF 1 + +#define SPLAY_INSERT(name, x, y) name##_SPLAY_INSERT(x, y) +#define SPLAY_REMOVE(name, x, y) name##_SPLAY_REMOVE(x, y) +#define SPLAY_FIND(name, x, y) name##_SPLAY_FIND(x, y) +#define SPLAY_NEXT(name, x, y) name##_SPLAY_NEXT(x, y) +#define SPLAY_MIN(name, x) (SPLAY_EMPTY(x) ? NULL \ + : name##_SPLAY_MIN_MAX(x, SPLAY_NEGINF)) +#define SPLAY_MAX(name, x) (SPLAY_EMPTY(x) ? NULL \ + : name##_SPLAY_MIN_MAX(x, SPLAY_INF)) + +#define SPLAY_FOREACH(x, name, head) \ + for ((x) = SPLAY_MIN(name, head); \ + (x) != NULL; \ + (x) = SPLAY_NEXT(name, head, x)) + +/* Macros that define a red-black tree */ +#define RB_HEAD(name, type) \ +struct name { \ + struct type *rbh_root; /* root of the tree */ \ +} + +#define RB_INITIALIZER(root) \ + { NULL } + +#define RB_INIT(root) do { \ + (root)->rbh_root = NULL; \ +} while (/*CONSTCOND*/ 0) + +#define RB_BLACK 0 +#define RB_RED 1 +#define RB_ENTRY(type) \ +struct { \ + struct type *rbe_left; /* left element */ \ + struct type *rbe_right; /* right element */ \ + struct type *rbe_parent; /* parent element */ \ + int rbe_color; /* node color */ \ +} + +#define RB_LEFT(elm, field) (elm)->field.rbe_left +#define RB_RIGHT(elm, field) (elm)->field.rbe_right +#define RB_PARENT(elm, field) (elm)->field.rbe_parent +#define RB_COLOR(elm, field) (elm)->field.rbe_color +#define RB_ROOT(head) (head)->rbh_root +#define RB_EMPTY(head) (RB_ROOT(head) == NULL) + +#define RB_SET(elm, parent, field) do { \ + RB_PARENT(elm, field) = parent; \ + RB_LEFT(elm, field) = RB_RIGHT(elm, field) = NULL; \ + RB_COLOR(elm, field) = RB_RED; \ +} while (/*CONSTCOND*/ 0) + +#define RB_SET_BLACKRED(black, red, field) do { \ + RB_COLOR(black, field) = RB_BLACK; \ + RB_COLOR(red, field) = RB_RED; \ +} while (/*CONSTCOND*/ 0) + +#ifndef RB_AUGMENT +#define RB_AUGMENT(x) do {} while (0) +#endif + +#define RB_ROTATE_LEFT(head, elm, tmp, field) do { \ + (tmp) = RB_RIGHT(elm, field); \ + if ((RB_RIGHT(elm, field) = RB_LEFT(tmp, field)) != NULL) { \ + RB_PARENT(RB_LEFT(tmp, field), field) = (elm); \ + } \ + RB_AUGMENT(elm); \ + if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field)) != NULL) { \ + if ((elm) == RB_LEFT(RB_PARENT(elm, field), field)) \ + RB_LEFT(RB_PARENT(elm, field), field) = (tmp); \ + else \ + RB_RIGHT(RB_PARENT(elm, field), field) = (tmp); \ + } else \ + (head)->rbh_root = (tmp); \ + RB_LEFT(tmp, field) = (elm); \ + RB_PARENT(elm, field) = (tmp); \ + RB_AUGMENT(tmp); \ + if ((RB_PARENT(tmp, field))) \ + RB_AUGMENT(RB_PARENT(tmp, field)); \ +} while (/*CONSTCOND*/ 0) + +#define RB_ROTATE_RIGHT(head, elm, tmp, field) do { \ + (tmp) = RB_LEFT(elm, field); \ + if ((RB_LEFT(elm, field) = RB_RIGHT(tmp, field)) != NULL) { \ + RB_PARENT(RB_RIGHT(tmp, field), field) = (elm); \ + } \ + RB_AUGMENT(elm); \ + if ((RB_PARENT(tmp, field) = RB_PARENT(elm, field)) != NULL) { \ + if ((elm) == RB_LEFT(RB_PARENT(elm, field), field)) \ + RB_LEFT(RB_PARENT(elm, field), field) = (tmp); \ + else \ + RB_RIGHT(RB_PARENT(elm, field), field) = (tmp); \ + } else \ + (head)->rbh_root = (tmp); \ + RB_RIGHT(tmp, field) = (elm); \ + RB_PARENT(elm, field) = (tmp); \ + RB_AUGMENT(tmp); \ + if ((RB_PARENT(tmp, field))) \ + RB_AUGMENT(RB_PARENT(tmp, field)); \ +} while (/*CONSTCOND*/ 0) + +/* Generates prototypes and inline functions */ +#define RB_PROTOTYPE(name, type, field, cmp) \ + RB_PROTOTYPE_INTERNAL(name, type, field, cmp,) +#define RB_PROTOTYPE_STATIC(name, type, field, cmp) \ + RB_PROTOTYPE_INTERNAL(name, type, field, cmp, __unused static) +#define RB_PROTOTYPE_INTERNAL(name, type, field, cmp, attr) \ + RB_PROTOTYPE_INSERT_COLOR(name, type, attr); \ + RB_PROTOTYPE_REMOVE_COLOR(name, type, attr); \ + RB_PROTOTYPE_INSERT(name, type, attr); \ + RB_PROTOTYPE_REMOVE(name, type, attr); \ + RB_PROTOTYPE_FIND(name, type, attr); \ + RB_PROTOTYPE_NFIND(name, type, attr); \ + RB_PROTOTYPE_NEXT(name, type, attr); \ + RB_PROTOTYPE_PREV(name, type, attr); \ + RB_PROTOTYPE_MINMAX(name, type, attr); +#define RB_PROTOTYPE_INSERT_COLOR(name, type, attr) \ + attr void name##_RB_INSERT_COLOR(struct name *, struct type *) +#define RB_PROTOTYPE_REMOVE_COLOR(name, type, attr) \ + attr void name##_RB_REMOVE_COLOR(struct name *, struct type *, struct type *) +#define RB_PROTOTYPE_REMOVE(name, type, attr) \ + attr struct type *name##_RB_REMOVE(struct name *, struct type *) +#define RB_PROTOTYPE_INSERT(name, type, attr) \ + attr struct type *name##_RB_INSERT(struct name *, struct type *) +#define RB_PROTOTYPE_FIND(name, type, attr) \ + attr struct type *name##_RB_FIND(struct name *, struct type *) +#define RB_PROTOTYPE_NFIND(name, type, attr) \ + attr struct type *name##_RB_NFIND(struct name *, struct type *) +#define RB_PROTOTYPE_NEXT(name, type, attr) \ + attr struct type *name##_RB_NEXT(struct type *) +#define RB_PROTOTYPE_PREV(name, type, attr) \ + attr struct type *name##_RB_PREV(struct type *) +#define RB_PROTOTYPE_MINMAX(name, type, attr) \ + attr struct type *name##_RB_MINMAX(struct name *, int) + +/* Main rb operation. + * Moves node close to the key of elm to top + */ +#define RB_GENERATE(name, type, field, cmp) \ + RB_GENERATE_INTERNAL(name, type, field, cmp,) +#define RB_GENERATE_STATIC(name, type, field, cmp) \ + RB_GENERATE_INTERNAL(name, type, field, cmp, __unused static) +#define RB_GENERATE_INTERNAL(name, type, field, cmp, attr) \ + RB_GENERATE_INSERT_COLOR(name, type, field, attr) \ + RB_GENERATE_REMOVE_COLOR(name, type, field, attr) \ + RB_GENERATE_INSERT(name, type, field, cmp, attr) \ + RB_GENERATE_REMOVE(name, type, field, attr) \ + RB_GENERATE_FIND(name, type, field, cmp, attr) \ + RB_GENERATE_NFIND(name, type, field, cmp, attr) \ + RB_GENERATE_NEXT(name, type, field, attr) \ + RB_GENERATE_PREV(name, type, field, attr) \ + RB_GENERATE_MINMAX(name, type, field, attr) + +#define RB_GENERATE_INSERT_COLOR(name, type, field, attr) \ +attr void \ +name##_RB_INSERT_COLOR(struct name *head, struct type *elm) \ +{ \ + struct type *parent, *gparent, *tmp; \ + while ((parent = RB_PARENT(elm, field)) != NULL && \ + RB_COLOR(parent, field) == RB_RED) { \ + gparent = RB_PARENT(parent, field); \ + if (parent == RB_LEFT(gparent, field)) { \ + tmp = RB_RIGHT(gparent, field); \ + if (tmp && RB_COLOR(tmp, field) == RB_RED) { \ + RB_COLOR(tmp, field) = RB_BLACK; \ + RB_SET_BLACKRED(parent, gparent, field);\ + elm = gparent; \ + continue; \ + } \ + if (RB_RIGHT(parent, field) == elm) { \ + RB_ROTATE_LEFT(head, parent, tmp, field);\ + tmp = parent; \ + parent = elm; \ + elm = tmp; \ + } \ + RB_SET_BLACKRED(parent, gparent, field); \ + RB_ROTATE_RIGHT(head, gparent, tmp, field); \ + } else { \ + tmp = RB_LEFT(gparent, field); \ + if (tmp && RB_COLOR(tmp, field) == RB_RED) { \ + RB_COLOR(tmp, field) = RB_BLACK; \ + RB_SET_BLACKRED(parent, gparent, field);\ + elm = gparent; \ + continue; \ + } \ + if (RB_LEFT(parent, field) == elm) { \ + RB_ROTATE_RIGHT(head, parent, tmp, field);\ + tmp = parent; \ + parent = elm; \ + elm = tmp; \ + } \ + RB_SET_BLACKRED(parent, gparent, field); \ + RB_ROTATE_LEFT(head, gparent, tmp, field); \ + } \ + } \ + RB_COLOR(head->rbh_root, field) = RB_BLACK; \ +} + +#define RB_GENERATE_REMOVE_COLOR(name, type, field, attr) \ +attr void \ +name##_RB_REMOVE_COLOR(struct name *head, struct type *parent, struct type *elm) \ +{ \ + struct type *tmp; \ + while ((elm == NULL || RB_COLOR(elm, field) == RB_BLACK) && \ + elm != RB_ROOT(head)) { \ + if (RB_LEFT(parent, field) == elm) { \ + tmp = RB_RIGHT(parent, field); \ + if (RB_COLOR(tmp, field) == RB_RED) { \ + RB_SET_BLACKRED(tmp, parent, field); \ + RB_ROTATE_LEFT(head, parent, tmp, field);\ + tmp = RB_RIGHT(parent, field); \ + } \ + if ((RB_LEFT(tmp, field) == NULL || \ + RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\ + (RB_RIGHT(tmp, field) == NULL || \ + RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\ + RB_COLOR(tmp, field) = RB_RED; \ + elm = parent; \ + parent = RB_PARENT(elm, field); \ + } else { \ + if (RB_RIGHT(tmp, field) == NULL || \ + RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK) {\ + struct type *oleft; \ + if ((oleft = RB_LEFT(tmp, field)) \ + != NULL) \ + RB_COLOR(oleft, field) = RB_BLACK;\ + RB_COLOR(tmp, field) = RB_RED; \ + RB_ROTATE_RIGHT(head, tmp, oleft, field);\ + tmp = RB_RIGHT(parent, field); \ + } \ + RB_COLOR(tmp, field) = RB_COLOR(parent, field);\ + RB_COLOR(parent, field) = RB_BLACK; \ + if (RB_RIGHT(tmp, field)) \ + RB_COLOR(RB_RIGHT(tmp, field), field) = RB_BLACK;\ + RB_ROTATE_LEFT(head, parent, tmp, field);\ + elm = RB_ROOT(head); \ + break; \ + } \ + } else { \ + tmp = RB_LEFT(parent, field); \ + if (RB_COLOR(tmp, field) == RB_RED) { \ + RB_SET_BLACKRED(tmp, parent, field); \ + RB_ROTATE_RIGHT(head, parent, tmp, field);\ + tmp = RB_LEFT(parent, field); \ + } \ + if ((RB_LEFT(tmp, field) == NULL || \ + RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) &&\ + (RB_RIGHT(tmp, field) == NULL || \ + RB_COLOR(RB_RIGHT(tmp, field), field) == RB_BLACK)) {\ + RB_COLOR(tmp, field) = RB_RED; \ + elm = parent; \ + parent = RB_PARENT(elm, field); \ + } else { \ + if (RB_LEFT(tmp, field) == NULL || \ + RB_COLOR(RB_LEFT(tmp, field), field) == RB_BLACK) {\ + struct type *oright; \ + if ((oright = RB_RIGHT(tmp, field)) \ + != NULL) \ + RB_COLOR(oright, field) = RB_BLACK;\ + RB_COLOR(tmp, field) = RB_RED; \ + RB_ROTATE_LEFT(head, tmp, oright, field);\ + tmp = RB_LEFT(parent, field); \ + } \ + RB_COLOR(tmp, field) = RB_COLOR(parent, field);\ + RB_COLOR(parent, field) = RB_BLACK; \ + if (RB_LEFT(tmp, field)) \ + RB_COLOR(RB_LEFT(tmp, field), field) = RB_BLACK;\ + RB_ROTATE_RIGHT(head, parent, tmp, field);\ + elm = RB_ROOT(head); \ + break; \ + } \ + } \ + } \ + if (elm) \ + RB_COLOR(elm, field) = RB_BLACK; \ +} + +#define RB_GENERATE_REMOVE(name, type, field, attr) \ +attr struct type * \ +name##_RB_REMOVE(struct name *head, struct type *elm) \ +{ \ + struct type *child, *parent, *old = elm; \ + int color; \ + if (RB_LEFT(elm, field) == NULL) \ + child = RB_RIGHT(elm, field); \ + else if (RB_RIGHT(elm, field) == NULL) \ + child = RB_LEFT(elm, field); \ + else { \ + struct type *left; \ + elm = RB_RIGHT(elm, field); \ + while ((left = RB_LEFT(elm, field)) != NULL) \ + elm = left; \ + child = RB_RIGHT(elm, field); \ + parent = RB_PARENT(elm, field); \ + color = RB_COLOR(elm, field); \ + if (child) \ + RB_PARENT(child, field) = parent; \ + if (parent) { \ + if (RB_LEFT(parent, field) == elm) \ + RB_LEFT(parent, field) = child; \ + else \ + RB_RIGHT(parent, field) = child; \ + RB_AUGMENT(parent); \ + } else \ + RB_ROOT(head) = child; \ + if (RB_PARENT(elm, field) == old) \ + parent = elm; \ + (elm)->field = (old)->field; \ + if (RB_PARENT(old, field)) { \ + if (RB_LEFT(RB_PARENT(old, field), field) == old)\ + RB_LEFT(RB_PARENT(old, field), field) = elm;\ + else \ + RB_RIGHT(RB_PARENT(old, field), field) = elm;\ + RB_AUGMENT(RB_PARENT(old, field)); \ + } else \ + RB_ROOT(head) = elm; \ + RB_PARENT(RB_LEFT(old, field), field) = elm; \ + if (RB_RIGHT(old, field)) \ + RB_PARENT(RB_RIGHT(old, field), field) = elm; \ + if (parent) { \ + left = parent; \ + do { \ + RB_AUGMENT(left); \ + } while ((left = RB_PARENT(left, field)) != NULL); \ + } \ + goto color; \ + } \ + parent = RB_PARENT(elm, field); \ + color = RB_COLOR(elm, field); \ + if (child) \ + RB_PARENT(child, field) = parent; \ + if (parent) { \ + if (RB_LEFT(parent, field) == elm) \ + RB_LEFT(parent, field) = child; \ + else \ + RB_RIGHT(parent, field) = child; \ + RB_AUGMENT(parent); \ + } else \ + RB_ROOT(head) = child; \ +color: \ + if (color == RB_BLACK) \ + name##_RB_REMOVE_COLOR(head, parent, child); \ + return (old); \ +} \ + +#define RB_GENERATE_INSERT(name, type, field, cmp, attr) \ +/* Inserts a node into the RB tree */ \ +attr struct type * \ +name##_RB_INSERT(struct name *head, struct type *elm) \ +{ \ + struct type *tmp; \ + struct type *parent = NULL; \ + int comp = 0; \ + tmp = RB_ROOT(head); \ + while (tmp) { \ + parent = tmp; \ + comp = (cmp)(elm, parent); \ + if (comp < 0) \ + tmp = RB_LEFT(tmp, field); \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + RB_SET(elm, parent, field); \ + if (parent != NULL) { \ + if (comp < 0) \ + RB_LEFT(parent, field) = elm; \ + else \ + RB_RIGHT(parent, field) = elm; \ + RB_AUGMENT(parent); \ + } else \ + RB_ROOT(head) = elm; \ + name##_RB_INSERT_COLOR(head, elm); \ + return (NULL); \ +} + +#define RB_GENERATE_FIND(name, type, field, cmp, attr) \ +/* Finds the node with the same key as elm */ \ +attr struct type * \ +name##_RB_FIND(struct name *head, struct type *elm) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + int comp; \ + while (tmp) { \ + comp = cmp(elm, tmp); \ + if (comp < 0) \ + tmp = RB_LEFT(tmp, field); \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + return (NULL); \ +} + +#define RB_GENERATE_NFIND(name, type, field, cmp, attr) \ +/* Finds the first node greater than or equal to the search key */ \ +attr struct type * \ +name##_RB_NFIND(struct name *head, struct type *elm) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + struct type *res = NULL; \ + int comp; \ + while (tmp) { \ + comp = cmp(elm, tmp); \ + if (comp < 0) { \ + res = tmp; \ + tmp = RB_LEFT(tmp, field); \ + } \ + else if (comp > 0) \ + tmp = RB_RIGHT(tmp, field); \ + else \ + return (tmp); \ + } \ + return (res); \ +} + +#define RB_GENERATE_NEXT(name, type, field, attr) \ +/* ARGSUSED */ \ +attr struct type * \ +name##_RB_NEXT(struct type *elm) \ +{ \ + if (RB_RIGHT(elm, field)) { \ + elm = RB_RIGHT(elm, field); \ + while (RB_LEFT(elm, field)) \ + elm = RB_LEFT(elm, field); \ + } else { \ + if (RB_PARENT(elm, field) && \ + (elm == RB_LEFT(RB_PARENT(elm, field), field))) \ + elm = RB_PARENT(elm, field); \ + else { \ + while (RB_PARENT(elm, field) && \ + (elm == RB_RIGHT(RB_PARENT(elm, field), field)))\ + elm = RB_PARENT(elm, field); \ + elm = RB_PARENT(elm, field); \ + } \ + } \ + return (elm); \ +} + +#define RB_GENERATE_PREV(name, type, field, attr) \ +/* ARGSUSED */ \ +attr struct type * \ +name##_RB_PREV(struct type *elm) \ +{ \ + if (RB_LEFT(elm, field)) { \ + elm = RB_LEFT(elm, field); \ + while (RB_RIGHT(elm, field)) \ + elm = RB_RIGHT(elm, field); \ + } else { \ + if (RB_PARENT(elm, field) && \ + (elm == RB_RIGHT(RB_PARENT(elm, field), field))) \ + elm = RB_PARENT(elm, field); \ + else { \ + while (RB_PARENT(elm, field) && \ + (elm == RB_LEFT(RB_PARENT(elm, field), field)))\ + elm = RB_PARENT(elm, field); \ + elm = RB_PARENT(elm, field); \ + } \ + } \ + return (elm); \ +} + +#define RB_GENERATE_MINMAX(name, type, field, attr) \ +attr struct type * \ +name##_RB_MINMAX(struct name *head, int val) \ +{ \ + struct type *tmp = RB_ROOT(head); \ + struct type *parent = NULL; \ + while (tmp) { \ + parent = tmp; \ + if (val < 0) \ + tmp = RB_LEFT(tmp, field); \ + else \ + tmp = RB_RIGHT(tmp, field); \ + } \ + return (parent); \ +} + +#define RB_NEGINF -1 +#define RB_INF 1 + +#define RB_INSERT(name, x, y) name##_RB_INSERT(x, y) +#define RB_REMOVE(name, x, y) name##_RB_REMOVE(x, y) +#define RB_FIND(name, x, y) name##_RB_FIND(x, y) +#define RB_NFIND(name, x, y) name##_RB_NFIND(x, y) +#define RB_NEXT(name, x, y) name##_RB_NEXT(y) +#define RB_PREV(name, x, y) name##_RB_PREV(y) +#define RB_MIN(name, x) name##_RB_MINMAX(x, RB_NEGINF) +#define RB_MAX(name, x) name##_RB_MINMAX(x, RB_INF) + +#define RB_FOREACH(x, name, head) \ + for ((x) = RB_MIN(name, head); \ + (x) != NULL; \ + (x) = name##_RB_NEXT(x)) + +#define RB_FOREACH_FROM(x, name, y) \ + for ((x) = (y); \ + ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_SAFE(x, name, head, y) \ + for ((x) = RB_MIN(name, head); \ + ((x) != NULL) && ((y) = name##_RB_NEXT(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_REVERSE(x, name, head) \ + for ((x) = RB_MAX(name, head); \ + (x) != NULL; \ + (x) = name##_RB_PREV(x)) + +#define RB_FOREACH_REVERSE_FROM(x, name, y) \ + for ((x) = (y); \ + ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ + (x) = (y)) + +#define RB_FOREACH_REVERSE_SAFE(x, name, head, y) \ + for ((x) = RB_MAX(name, head); \ + ((x) != NULL) && ((y) = name##_RB_PREV(x), (x) != NULL); \ + (x) = (y)) + +#endif /* _SYS_TREE_H_ */ diff --git a/tpws/params.h b/tpws/params.h new file mode 100644 index 0000000..9d10d04 --- /dev/null +++ b/tpws/params.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include "strpool.h" + +enum splithttpreq { split_none = 0, split_method, split_host }; + +#define MAX_BINDS 32 +struct bind_s +{ + char bindaddr[64],bindiface[IF_NAMESIZE]; + bool bind_if6; + bool bindll,bindll_force; + int bind_wait_ifup,bind_wait_ip,bind_wait_ip_ll; +}; + +struct params_s +{ + struct bind_s binds[MAX_BINDS]; + int binds_last; + bool bind_wait_only; + uint16_t port; + + uint8_t proxy_type; + bool no_resolve; + bool skip_nodelay; + bool droproot; + uid_t uid; + gid_t gid; + bool daemon; + int maxconn,maxfiles,max_orphan_time; + int local_rcvbuf,local_sndbuf,remote_rcvbuf,remote_sndbuf; + + bool tamper; // any tamper option is set + bool hostcase, hostdot, hosttab, hostnospace, methodspace, methodeol, unixeol, domcase; + int hostpad; + char hostspell[4]; + enum splithttpreq split_http_req; + bool split_any_protocol; + int split_pos; + char hostfile[256]; + char pidfile[256]; + strpool *hostlist; + + int debug; +}; + +extern struct params_s params; + +#define _DBGPRINT(format, level, ...) { if (params.debug>=level) printf(format "\n", ##__VA_ARGS__); } +#define VPRINT(format, ...) _DBGPRINT(format,1,##__VA_ARGS__) +#define DBGPRINT(format, ...) _DBGPRINT(format,2,##__VA_ARGS__) diff --git a/tpws/protocol.c b/tpws/protocol.c new file mode 100644 index 0000000..b5b1404 --- /dev/null +++ b/tpws/protocol.c @@ -0,0 +1,132 @@ +#define _GNU_SOURCE + +#include "protocol.h" +#include "helpers.h" +#include +#include +#include +#include + +const char *http_methods[] = { "GET /","POST /","HEAD /","OPTIONS /","PUT /","DELETE /","CONNECT /","TRACE /",NULL }; +bool IsHttp(const uint8_t *data, size_t len) +{ + const char **method; + size_t method_len; + for (method = http_methods; *method; method++) + { + method_len = strlen(*method); + if (method_len <= len && !memcmp(data, *method, method_len)) + return true; + } + return false; +} +bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_host) +{ + const uint8_t *p, *s, *e=data+len; + + p = (uint8_t*)strncasestr((char*)data, "\nHost:", len); + if (!p) return false; + p+=6; + while(pp) + { + size_t slen = s-p; + if (host && len_host) + { + if (slen>=len_host) slen=len_host-1; + for(size_t i=0;i=6 && data[0]==0x16 && data[1]==0x03 && data[2]==0x01 && data[5]==0x01 && (ntohs(*(uint16_t*)(data+3))+5)<=len; +} +bool TLSFindExt(const uint8_t *data, size_t len, uint16_t type, const uint8_t **ext, size_t *len_ext) +{ + // +0 + // u8 ContentType: Handshake + // u16 Version: TLS1.0 + // u16 Length + // +5 + // u8 HandshakeType: ClientHello + // u24 Length + // u16 Version + // c[32] random + // u8 SessionIDLength + // + // u16 CipherSuitesLength + // + // u8 CompressionMethodsLength + // + // u16 ExtensionsLength + + size_t l,ll; + + l = 1+2+2+1+3+2+32; + // SessionIDLength + if (len<(l+1)) return false; + ll = data[6]<<16 | data[7]<<8 | data[8]; // HandshakeProtocol length + if (len<(ll+9)) return false; + l += data[l]+1; + // CipherSuitesLength + if (len<(l+2)) return false; + l += ntohs(*(uint16_t*)(data+l))+2; + // CompressionMethodsLength + if (len<(l+1)) return false; + l += data[l]+1; + // ExtensionsLength + if (len<(l+2)) return false; + + data+=l; len-=l; + l=ntohs(*(uint16_t*)data); + data+=2; len-=2; + if (l=4) + { + uint16_t etype=*(uint16_t*)data; + size_t elen=ntohs(*(uint16_t*)(data+2)); + data+=4; l-=4; + if (l=len_host) slen=len_host-1; + for(size_t i=0;i +#include +#include + +bool IsHttp(const uint8_t *data, size_t len); +bool HttpExtractHost(const uint8_t *data, size_t len, char *host, size_t len_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/redirect.c b/tpws/redirect.c new file mode 100644 index 0000000..e562d7f --- /dev/null +++ b/tpws/redirect.c @@ -0,0 +1,211 @@ +#include "redirect.h" +#include +#include +#include +#include +#include +#include +#include + +#include "params.h" +#include "helpers.h" + +//#if !defined(USE_PF) && defined(__OpenBSD__) +#if !defined(USE_PF) && (defined(__OpenBSD__) || defined(__APPLE__)) + #define USE_PF 1 +#endif + +#ifdef __linux__ + #include + #ifndef IP6T_SO_ORIGINAL_DST + #define IP6T_SO_ORIGINAL_DST 80 + #endif +#endif +#ifdef USE_PF + #include + #include +#endif + + + +#if defined(USE_PF) +static int redirector_fd=-1; + +void redir_close() +{ + if (redirector_fd!=-1) + { + close(redirector_fd); + redirector_fd = -1; + DBGPRINT("closed redirector"); + } +} +static bool redir_open_private(const char *fname, int flags) +{ + redir_close(); + redirector_fd = open(fname, flags); + if (redirector_fd < 0) + { + perror("redir_openv_private: "); + return false; + } + DBGPRINT("opened redirector %s",fname); + return true; +} +bool redir_init() +{ + return redir_open_private("/dev/pf", O_RDONLY); +} + +static bool destination_from_pf(const struct sockaddr *accept_sa, struct sockaddr_storage *orig_dst) +{ + struct pfioc_natlook nl; + + if (redirector_fd==-1) return false; + + if (accept_sa->sa_family!=orig_dst->ss_family) + { + DBGPRINT("accept_sa and orig_dst sa_family mismatch : %d %d", accept_sa->sa_family, orig_dst->ss_family); + return false; + } + + memset(&nl, 0, sizeof(nl)); + nl.proto = IPPROTO_TCP; + nl.direction = PF_OUT; + nl.af = orig_dst->ss_family; + switch(orig_dst->ss_family) + { + case AF_INET: + { + struct sockaddr_in *sin = (struct sockaddr_in *)orig_dst; + nl.saddr.v4.s_addr = ((struct sockaddr_in*)accept_sa)->sin_addr.s_addr; + nl.daddr.v4.s_addr = sin->sin_addr.s_addr; +#ifdef __APPLE__ + nl.sxport.port = ((struct sockaddr_in*)accept_sa)->sin_port; + nl.dxport.port = sin->sin_port; +#else + nl.sport = ((struct sockaddr_in*)accept_sa)->sin_port; + nl.dport = sin->sin_port; +#endif + } + break; + case AF_INET6: + { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)orig_dst; + nl.saddr.v6 = ((struct sockaddr_in6*)accept_sa)->sin6_addr; + nl.daddr.v6 = sin6->sin6_addr; +#ifdef __APPLE__ + nl.sxport.port = ((struct sockaddr_in6*)accept_sa)->sin6_port; + nl.dxport.port = sin6->sin6_port; +#else + nl.sport = ((struct sockaddr_in6*)accept_sa)->sin6_port; + nl.dport = sin6->sin6_port; +#endif + } + break; + default: + DBGPRINT("destination_from_pf : unexpected address family %d",orig_dst->ss_family); + return false; + } + + if (ioctl(redirector_fd, DIOCNATLOOK, &nl) < 0) + { + DBGPRINT("ioctl(DIOCNATLOOK) failed: %s",strerror(errno)); + return false; + } + DBGPRINT("destination_from_pf : got orig dest addr from pf"); + + switch(nl.af) + { + case AF_INET: + orig_dst->ss_family = nl.af; +#ifdef __APPLE__ + ((struct sockaddr_in*)orig_dst)->sin_port = nl.rdxport.port; +#else + ((struct sockaddr_in*)orig_dst)->sin_port = nl.rdport; +#endif + ((struct sockaddr_in*)orig_dst)->sin_addr = nl.rdaddr.v4; + break; + case AF_INET6: + orig_dst->ss_family = nl.af; +#ifdef __APPLE__ + ((struct sockaddr_in6*)orig_dst)->sin6_port = nl.rdxport.port; +#else + ((struct sockaddr_in6*)orig_dst)->sin6_port = nl.rdport; +#endif + ((struct sockaddr_in6*)orig_dst)->sin6_addr = nl.rdaddr.v6; + break; + default: + DBGPRINT("destination_from_pf : DIOCNATLOOK returned unexpected address family %d",nl.af); + return false; + } + + return true; +} + + +#else + +bool redir_init() {return true;} +void redir_close() {}; + +#endif + + + +//Store the original destination address in orig_dst +bool get_dest_addr(int sockfd, const struct sockaddr *accept_sa, struct sockaddr_storage *orig_dst) +{ + char orig_dst_str[INET6_ADDRSTRLEN]; + socklen_t addrlen = sizeof(*orig_dst); + int r; + + memset(orig_dst, 0, addrlen); + + //For UDP transparent proxying: + //Set IP_RECVORIGDSTADDR socket option for getting the original + //destination of a datagram + +#ifdef __linux__ + // DNAT + r=getsockopt(sockfd, SOL_IP, SO_ORIGINAL_DST, (struct sockaddr*) orig_dst, &addrlen); + if (r<0) + r = getsockopt(sockfd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, (struct sockaddr*) orig_dst, &addrlen); + if (r<0) + { + DBGPRINT("both SO_ORIGINAL_DST and IP6T_SO_ORIGINAL_DST failed !"); +#endif + // TPROXY : socket is bound to original destination + r=getsockname(sockfd, (struct sockaddr*) orig_dst, &addrlen); + if (r<0) + { + perror("getsockname: "); + return false; + } + if (orig_dst->ss_family==AF_INET6) + ((struct sockaddr_in6*)orig_dst)->sin6_scope_id=0; // or MacOS will not connect() +#ifdef USE_PF + if (!destination_from_pf(accept_sa, orig_dst)) + DBGPRINT("pf filter destination_from_pf failed"); +#endif +#ifdef __linux__ + } +#endif + if (saconvmapped(orig_dst)) + DBGPRINT("Original destination : converted ipv6 mapped address to ipv4"); + + if (params.debug) + { + if (orig_dst->ss_family == AF_INET) + { + inet_ntop(AF_INET, &(((struct sockaddr_in*) orig_dst)->sin_addr), orig_dst_str, INET_ADDRSTRLEN); + VPRINT("Original destination for socket fd=%d : %s:%d", sockfd,orig_dst_str, htons(((struct sockaddr_in*) orig_dst)->sin_port)) + } + else if (orig_dst->ss_family == AF_INET6) + { + inet_ntop(AF_INET6,&(((struct sockaddr_in6*) orig_dst)->sin6_addr), orig_dst_str, INET6_ADDRSTRLEN); + VPRINT("Original destination for socket fd=%d : [%s]:%d", sockfd,orig_dst_str, htons(((struct sockaddr_in6*) orig_dst)->sin6_port)) + } + } + return true; +} diff --git a/tpws/redirect.h b/tpws/redirect.h new file mode 100644 index 0000000..8a6831e --- /dev/null +++ b/tpws/redirect.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +bool get_dest_addr(int sockfd, const struct sockaddr *accept_sa, struct sockaddr_storage *orig_dst); +bool redir_init(); +void redir_close(); diff --git a/tpws/sec.c b/tpws/sec.c new file mode 100644 index 0000000..a3308f6 --- /dev/null +++ b/tpws/sec.c @@ -0,0 +1,172 @@ +#define _GNU_SOURCE + +#include +#include +#include "sec.h" +#include +#include +#include + +#ifdef __linux__ + +#include + +bool checkpcap(uint64_t caps) +{ + if (!caps) return true; // no special caps reqd + + struct __user_cap_header_struct ch = {_LINUX_CAPABILITY_VERSION_3, getpid()}; + struct __user_cap_data_struct cd[2]; + uint32_t c0 = (uint32_t)caps; + uint32_t c1 = (uint32_t)(caps>>32); + + return !capget(&ch,cd) && (cd[0].effective & c0)==c0 && (cd[1].effective & c1)==c1; +} +bool setpcap(uint64_t caps) +{ + struct __user_cap_header_struct ch = {_LINUX_CAPABILITY_VERSION_3, getpid()}; + struct __user_cap_data_struct cd[2]; + + cd[0].effective = cd[0].permitted = (uint32_t)caps; + cd[0].inheritable = 0; + cd[1].effective = cd[1].permitted = (uint32_t)(caps>>32); + cd[1].inheritable = 0; + + return !capset(&ch,cd); +} +int getmaxcap() +{ + int maxcap = CAP_LAST_CAP; + FILE *F = fopen("/proc/sys/kernel/cap_last_cap", "r"); + if (F) + { + fscanf(F, "%d", &maxcap); + fclose(F); + } + return maxcap; + +} +bool dropcaps() +{ + uint64_t caps = 0; + int maxcap = getmaxcap(); + + if (setpcap(caps|(1< +#include + +#ifdef __linux__ + +#include + +bool checkpcap(uint64_t caps); +bool setpcap(uint64_t caps); +int getmaxcap(); +bool dropcaps(); +#endif + +bool can_drop_root(); +bool droproot(uid_t uid, gid_t gid); +void print_id(); +void daemonize(); +bool writepid(const char *filename); diff --git a/tpws/socks.h b/tpws/socks.h new file mode 100644 index 0000000..9026a64 --- /dev/null +++ b/tpws/socks.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#pragma pack(push,1) + +#define S4_CMD_CONNECT 1 +#define S4_CMD_BIND 2 +typedef struct +{ + uint8_t ver,cmd; + uint16_t port; + uint32_t ip; +} s4_req; +#define S4_REQ_HEADER_VALID(r,l) (l>=sizeof(s4_req) && r->ver==4) +#define S4_REQ_CONNECT_VALID(r,l) (S4_REQ_HEADER_VALID(r,l) && r->cmd==S4_CMD_CONNECT) + +#define S4_REP_OK 90 +#define S4_REP_FAILED 91 +typedef struct +{ + uint8_t zero,rep; + uint16_t port; + uint32_t ip; +} s4_rep; + + + +#define S5_AUTH_NONE 0 +#define S5_AUTH_GSSAPI 1 +#define S5_AUTH_USERPASS 2 +#define S5_AUTH_UNACCEPTABLE 0xFF +typedef struct +{ + uint8_t ver,nmethods,methods[255]; +} s5_handshake; +#define S5_REQ_HANDHSHAKE_VALID(r,l) (l>=3 && r->ver==5 && r->nmethods && l>=(2+r->nmethods)) +typedef struct +{ + uint8_t ver,method; +} s5_handshake_ack; + +#define S5_CMD_CONNECT 1 +#define S5_CMD_BIND 2 +#define S5_CMD_UDP_ASSOC 3 +#define S5_ATYP_IP4 1 +#define S5_ATYP_DOM 3 +#define S5_ATYP_IP6 4 +typedef struct +{ + uint8_t ver,cmd,rsv,atyp; + union { + struct { + struct in_addr addr; + uint16_t port; + } d4; + struct { + struct in6_addr addr; + uint16_t port; + } d6; + struct { + uint8_t len; + char domport[255+2]; // max hostname + binary port + } dd; + }; +} s5_req; +#define S5_REQ_HEADER_VALID(r,l) (l>=4 && r->ver==5) +#define S5_IP46_VALID(r,l) (r->atyp==S5_ATYP_IP4 && l>=(4+sizeof(r->d4)) || r->atyp==S5_ATYP_IP6 && l>=(4+sizeof(r->d6))) +#define S5_REQ_CONNECT_VALID(r,l) (S5_REQ_HEADER_VALID(r,l) && r->cmd==S5_CMD_CONNECT && (S5_IP46_VALID(r,l) || r->atyp==S5_ATYP_DOM && l>=5 && l>=(5+r->dd.len))) +#define S5_PORT_FROM_DD(r,l) (l>=(4+r->dd.len+2) ? ntohs(*(uint16_t*)(r->dd.domport+r->dd.len)) : 0) + +#define S5_REP_OK 0 +#define S5_REP_GENERAL_FAILURE 1 +#define S5_REP_NOT_ALLOWED_BY_RULESET 2 +#define S5_REP_NETWORK_UNREACHABLE 3 +#define S5_REP_HOST_UNREACHABLE 4 +#define S5_REP_CONN_REFUSED 5 +#define S5_REP_TTL_EXPIRED 6 +#define S5_REP_COMMAND_NOT_SUPPORTED 7 +#define S5_REP_ADDR_TYPE_NOT_SUPPORTED 8 +typedef struct +{ + uint8_t ver,rep,rsv,atyp; + union { + struct { + struct in_addr addr; + uint16_t port; + } d4; + }; +} s5_rep; + +#pragma pack(pop) diff --git a/tpws/strpool.c b/tpws/strpool.c new file mode 100644 index 0000000..41e62a9 --- /dev/null +++ b/tpws/strpool.c @@ -0,0 +1,76 @@ +#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; +} diff --git a/tpws/strpool.h b/tpws/strpool.h new file mode 100644 index 0000000..5932ba3 --- /dev/null +++ b/tpws/strpool.h @@ -0,0 +1,19 @@ +#pragma once + +#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); diff --git a/tpws/tamper.c b/tpws/tamper.c new file mode 100644 index 0000000..f9f2748 --- /dev/null +++ b/tpws/tamper.c @@ -0,0 +1,235 @@ +#define _GNU_SOURCE + +#include "tamper.h" +#include "params.h" +#include "hostlist.h" +#include "protocol.h" +#include +#include + +// pHost points to "Host: ..." +bool find_host(char **pHost,char *buf,size_t bs) +{ + if (!*pHost) + { + *pHost = memmem(buf, bs, "\nHost:", 6); + if (*pHost) + { + (*pHost)++; + VPRINT("Found Host: at pos %zu",*pHost - buf) + } + } + return !!*pHost; +} + +static const char *http_methods[] = { "GET /","POST /","HEAD /","OPTIONS /","PUT /","DELETE /","CONNECT /","TRACE /",NULL }; +void modify_tcp_segment(char *segment,size_t segment_buffer_size,size_t *size,size_t *split_pos) +{ + char *p, *pp, *pHost = NULL; + size_t method_len = 0, pos; + const char **method; + bool bIsHttp = false, bBypass = false; + char bRemovedHostSpace = 0; + char Host[128]; + + *split_pos=0; + + for (method = http_methods; *method; method++) + { + method_len = strlen(*method); + if (method_len <= *size && !memcmp(segment, *method, method_len)) + { + bIsHttp = true; + method_len -= 2; // "GET /" => "GET" + break; + } + } + if (bIsHttp) + { + VPRINT("Data block looks like http request start : %s", *method) + // cpu saving : we search host only if and when required. we do not research host every time we need its position + if (params.hostlist && find_host(&pHost,segment,*size)) + { + p = pHost + 5; + while (p < (segment + *size) && (*p == ' ' || *p == '\t')) p++; + pp = p; + while (pp < (segment + *size) && (pp - p) < (sizeof(Host) - 1) && *pp != '\r' && *pp != '\n') pp++; + memcpy(Host, p, pp - p); + Host[pp - p] = '\0'; + VPRINT("Requested Host is : %s", Host) + for(p = Host; *p; p++) *p=tolower(*p); + bBypass = !SearchHostList(params.hostlist,Host,!!params.debug); + } + if (!bBypass) + { + if (params.unixeol) + { + p = pp = segment; + while ((p = memmem(p, segment + *size - p, "\r\n", 2))) + { + *p = '\n'; p++; + memmove(p, p + 1, segment + *size - p - 1); + (*size)--; + if (pp == (p - 1)) + { + // probably end of http headers + VPRINT("Found double EOL at pos %zu. Stop replacing.", pp - segment) + break; + } + pp = p; + } + pHost = NULL; // invalidate + } + if (params.methodeol && (*size+1+!params.unixeol)<=segment_buffer_size) + { + VPRINT("Adding EOL before method") + if (params.unixeol) + { + memmove(segment + 1, segment, *size); + (*size)++;; + segment[0] = '\n'; + } + else + { + memmove(segment + 2, segment, *size); + *size += 2; + segment[0] = '\r'; + segment[1] = '\n'; + } + pHost = NULL; // invalidate + } + if (params.methodspace && *size '%c%c%c%c:' at pos %zu", params.hostspell[0], params.hostspell[1], params.hostspell[2], params.hostspell[3], pHost - segment) + memcpy(pHost, params.hostspell, 4); + } + if (params.hostpad && find_host(&pHost,segment,*size)) + { + // add : XXXXX: segment_buffer_size) + VPRINT("could not add host padding : buffer too small") + else + { + if ((hostpad+*size)>segment_buffer_size) + { + hostpad=segment_buffer_size-*size; + VPRINT("host padding reduced to %zu bytes : buffer too small", hostpad) + } + else + VPRINT("host padding with %zu bytes", hostpad) + + p = pHost; + pos = p - segment; + memmove(p + hostpad, p, *size - pos); + (*size) += hostpad; + while(hostpad) + { + #define MAX_HDR_SIZE 2048 + size_t padsize = hostpad > hsize ? hostpad-hsize : 0; + if (padsize>MAX_HDR_SIZE) padsize=MAX_HDR_SIZE; + // if next header would be too small then add extra padding to the current one + if ((hostpad-padsize-hsize) +#include + +bool find_host(char **pHost,char *buf,size_t bs); +void modify_tcp_segment(char *segment,size_t segment_buffer_size,size_t *size,size_t *split_pos); diff --git a/tpws/tpws b/tpws/tpws new file mode 120000 index 0000000..83fc382 --- /dev/null +++ b/tpws/tpws @@ -0,0 +1 @@ +../binaries/x86_64/tpws \ No newline at end of file diff --git a/tpws/tpws.c b/tpws/tpws.c new file mode 100644 index 0000000..be11d47 --- /dev/null +++ b/tpws/tpws.c @@ -0,0 +1,878 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tpws.h" + +#ifdef BSD + #include +#endif + +#include "tpws_conn.h" +#include "hostlist.h" +#include "params.h" +#include "sec.h" +#include "redirect.h" + +struct params_s params; + +bool bHup = false; +static void onhup(int sig) +{ + printf("HUP received !\n"); + if (params.hostlist) + printf("Will reload hostlist on next request\n"); + bHup = true; +} +// should be called in normal execution +void dohup() +{ + if (bHup) + { + if (params.hostlist) + { + if (!LoadHostList(¶ms.hostlist, params.hostfile)) + { + // what will we do without hostlist ?? sure, gonna die + exit(1); + } + } + bHup = false; + } +} + + + +static int8_t block_sigpipe() +{ + sigset_t sigset; + memset(&sigset, 0, sizeof(sigset)); + + //Get the old sigset, add SIGPIPE and update sigset + if (sigprocmask(SIG_BLOCK, NULL, &sigset) == -1) { + perror("sigprocmask (get)"); + return -1; + } + + if (sigaddset(&sigset, SIGPIPE) == -1) { + perror("sigaddset"); + return -1; + } + + if (sigprocmask(SIG_BLOCK, &sigset, NULL) == -1) { + perror("sigprocmask (set)"); + return -1; + } + + return 0; +} + + +static bool is_interface_online(const char *ifname) +{ + struct ifreq ifr; + int sock; + + if ((sock=socket(PF_INET, SOCK_DGRAM, IPPROTO_IP))==-1) + return false; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ); + ifr.ifr_name[IFNAMSIZ-1] = 0; + ioctl(sock, SIOCGIFFLAGS, &ifr); + close(sock); + return !!(ifr.ifr_flags & IFF_UP); +} + + +static void exithelp() +{ + 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=prefer|force\t; prefer or force ipv6 link local\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; accept only link locals first N seconds then any\n" + " --bind-wait-only\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" + " --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" + " --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" +#else + " --maxfiles=\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" + " --debug=0|1|2\t\t\t; 0(default)=silent 1=verbose 2=debug\n" + "\nTAMPERING:\n" + " --hostlist=\t\t; only act on host in the list (one host per line, subdomains auto apply)\n" + " --split-http-req=method|host\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" + " --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" + ); + exit(1); +} +static void cleanup_params() +{ + if (params.hostlist) + { + StrPoolDestroy(¶ms.hostlist); + params.hostlist = NULL; + } +} +static void exithelp_clean() +{ + cleanup_params(); + exithelp(); +} +static void exit_clean(int code) +{ + cleanup_params(); + exit(code); +} +static void nextbind_clean() +{ + params.binds_last++; + if (params.binds_last>=MAX_BINDS) + { + fprintf(stderr,"maximum of %d binds are supported\n",MAX_BINDS); + exit_clean(1); + } +} +static void checkbind_clean() +{ + if (params.binds_last<0) + { + fprintf(stderr,"start new bind with --bind-addr,--bind-iface*\n"); + exit_clean(1); + } +} + + +void parse_params(int argc, char *argv[]) +{ + int option_index = 0; + int v, i; + + memset(¶ms, 0, sizeof(params)); + memcpy(params.hostspell, "host", 4); // default hostspell + params.maxconn = DEFAULT_MAX_CONN; + params.max_orphan_time = DEFAULT_MAX_ORPHAN_TIME; + params.binds_last = -1; + if (can_drop_root()) + { + params.uid = params.gid = 0x7FFFFFFF; // default uid:gid + params.droproot = true; + } + + const struct option long_options[] = { + { "help",no_argument,0,0 },// optidx=0 + { "h",no_argument,0,0 },// optidx=1 + { "bind-addr",required_argument,0,0 },// optidx=2 + { "bind-iface4",required_argument,0,0 },// optidx=3 + { "bind-iface6",required_argument,0,0 },// optidx=4 + { "bind-linklocal",required_argument,0,0 },// optidx=5 + { "bind-wait-ifup",required_argument,0,0 },// optidx=6 + { "bind-wait-ip",required_argument,0,0 },// optidx=7 + { "bind-wait-ip-linklocal",required_argument,0,0 },// optidx=8 + { "bind-wait-only",no_argument,0,0 },// optidx=9 + { "port",required_argument,0,0 },// optidx=10 + { "daemon",no_argument,0,0 },// optidx=11 + { "user",required_argument,0,0 },// optidx=12 + { "uid",required_argument,0,0 },// optidx=13 + { "maxconn",required_argument,0,0 },// optidx=14 + { "maxfiles",required_argument,0,0 },// optidx=15 + { "max-orphan-time",required_argument,0,0 },// optidx=16 + { "hostcase",no_argument,0,0 },// optidx=17 + { "hostspell",required_argument,0,0 },// optidx=18 + { "hostdot",no_argument,0,0 },// optidx=19 + { "hostnospace",no_argument,0,0 },// optidx=20 + { "hostpad",required_argument,0,0 },// optidx=21 + { "domcase",no_argument,0,0 },// optidx=22 + { "split-http-req",required_argument,0,0 },// optidx=23 + { "split-pos",required_argument,0,0 },// optidx=24 + { "split-any-protocol",optional_argument,0,0},// optidx=25 + { "methodspace",no_argument,0,0 },// optidx=26 + { "methodeol",no_argument,0,0 },// optidx=27 + { "hosttab",no_argument,0,0 },// optidx=28 + { "unixeol",no_argument,0,0 },// optidx=29 + { "hostlist",required_argument,0,0 },// optidx=30 + { "pidfile",required_argument,0,0 },// optidx=31 + { "debug",optional_argument,0,0 },// optidx=32 + { "local-rcvbuf",required_argument,0,0 },// optidx=33 + { "local-sndbuf",required_argument,0,0 },// optidx=34 + { "remote-rcvbuf",required_argument,0,0 },// optidx=35 + { "remote-sndbuf",required_argument,0,0 },// optidx=36 + { "socks",no_argument,0,0 },// optidx=37 + { "no-resolve",no_argument,0,0 },// optidx=38 + { "skip-nodelay",no_argument,0,0 },// optidx=39 + { NULL,0,NULL,0 } + }; + while ((v = getopt_long_only(argc, argv, "", long_options, &option_index)) != -1) + { + if (v) exithelp_clean(); + switch (option_index) + { + case 0: + case 1: + exithelp_clean(); + break; + case 2: /* bind-addr */ + nextbind_clean(); + { + char *p = strchr(optarg,'%'); + if (p) + { + *p=0; + strncpy(params.binds[params.binds_last].bindiface, p+1, sizeof(params.binds[params.binds_last].bindiface)); + } + strncpy(params.binds[params.binds_last].bindaddr, optarg, sizeof(params.binds[params.binds_last].bindaddr)); + } + params.binds[params.binds_last].bindaddr[sizeof(params.binds[params.binds_last].bindaddr) - 1] = 0; + break; + case 3: /* bind-iface4 */ + nextbind_clean(); + params.binds[params.binds_last].bind_if6=false; + strncpy(params.binds[params.binds_last].bindiface, optarg, sizeof(params.binds[params.binds_last].bindiface)); + params.binds[params.binds_last].bindiface[sizeof(params.binds[params.binds_last].bindiface) - 1] = 0; + break; + case 4: /* bind-iface6 */ + nextbind_clean(); + params.binds[params.binds_last].bind_if6=true; + strncpy(params.binds[params.binds_last].bindiface, optarg, sizeof(params.binds[params.binds_last].bindiface)); + params.binds[params.binds_last].bindiface[sizeof(params.binds[params.binds_last].bindiface) - 1] = 0; + break; + case 5: /* bind-linklocal */ + checkbind_clean(); + params.binds[params.binds_last].bindll = true; + if (!strcmp(optarg, "force")) + params.binds[params.binds_last].bindll_force=true; + else if (strcmp(optarg, "prefer")) + { + fprintf(stderr, "invalid parameter in bind-linklocal : %s\n",optarg); + exit_clean(1); + } + break; + case 6: /* bind-wait-ifup */ + checkbind_clean(); + params.binds[params.binds_last].bind_wait_ifup = atoi(optarg); + break; + case 7: /* bind-wait-ip */ + checkbind_clean(); + params.binds[params.binds_last].bind_wait_ip = atoi(optarg); + break; + case 8: /* bind-wait-ip-linklocal */ + checkbind_clean(); + params.binds[params.binds_last].bind_wait_ip_ll = atoi(optarg); + break; + case 9: /* bind-wait-only */ + params.bind_wait_only = true; + break; + case 10: /* port */ + i = atoi(optarg); + if (i <= 0 || i > 65535) + { + fprintf(stderr, "bad port number\n"); + exit_clean(1); + } + params.port = (uint16_t)i; + break; + case 11: /* daemon */ + params.daemon = true; + break; + case 12: /* user */ + { + struct passwd *pwd = getpwnam(optarg); + if (!pwd) + { + fprintf(stderr, "non-existent username supplied\n"); + exit_clean(1); + } + params.uid = pwd->pw_uid; + params.gid = pwd->pw_gid; + params.droproot = true; + break; + } + case 13: /* uid */ + params.gid=0x7FFFFFFF; // default git. drop gid=0 + params.droproot = true; + if (!sscanf(optarg,"%u:%u",¶ms.uid,¶ms.gid)) + { + fprintf(stderr, "--uid should be : uid[:gid]\n"); + exit_clean(1); + } + break; + case 14: /* maxconn */ + params.maxconn = atoi(optarg); + if (params.maxconn <= 0 || params.maxconn > 10000) + { + fprintf(stderr, "bad maxconn\n"); + exit_clean(1); + } + break; + case 15: /* maxfiles */ + params.maxfiles = atoi(optarg); + if (params.maxfiles < 0) + { + fprintf(stderr, "bad maxfiles\n"); + exit_clean(1); + } + break; + case 16: /* max-orphan-time */ + params.max_orphan_time = atoi(optarg); + if (params.max_orphan_time < 0) + { + fprintf(stderr, "bad max_orphan_time\n"); + exit_clean(1); + } + break; + case 17: /* hostcase */ + params.hostcase = true; + params.tamper = true; + break; + case 18: /* hostspell */ + if (strlen(optarg) != 4) + { + fprintf(stderr, "hostspell must be exactly 4 chars long\n"); + exit_clean(1); + } + params.hostcase = true; + memcpy(params.hostspell, optarg, 4); + params.tamper = true; + break; + case 19: /* hostdot */ + params.hostdot = true; + params.tamper = true; + break; + case 20: /* hostnospace */ + params.hostnospace = true; + params.tamper = true; + break; + case 21: /* hostpad */ + params.hostpad = atoi(optarg); + params.tamper = true; + break; + case 22: /* domcase */ + params.domcase = true; + params.tamper = true; + break; + case 23: /* split-http-req */ + if (!strcmp(optarg, "method")) + params.split_http_req = split_method; + else if (!strcmp(optarg, "host")) + params.split_http_req = split_host; + else + { + fprintf(stderr, "Invalid argument for split-http-req\n"); + exit_clean(1); + } + params.tamper = true; + break; + case 24: /* split-pos */ + i = atoi(optarg); + if (i) + params.split_pos = i; + else + { + fprintf(stderr, "Invalid argument for split-pos\n"); + exit_clean(1); + } + params.tamper = true; + break; + case 25: /* split-any-protocol */ + params.split_any_protocol = true; + break; + case 26: /* methodspace */ + params.methodspace = true; + params.tamper = true; + break; + case 27: /* methodeol */ + params.methodeol = true; + params.tamper = true; + break; + case 28: /* hosttab */ + params.hosttab = true; + params.tamper = true; + break; + case 29: /* unixeol */ + params.unixeol = true; + params.tamper = true; + break; + case 30: /* hostlist */ + if (!LoadHostList(¶ms.hostlist, optarg)) + exit_clean(1); + strncpy(params.hostfile,optarg,sizeof(params.hostfile)); + params.hostfile[sizeof(params.hostfile)-1]='\0'; + params.tamper = true; + break; + case 31: /* pidfile */ + strncpy(params.pidfile,optarg,sizeof(params.pidfile)); + params.pidfile[sizeof(params.pidfile)-1]='\0'; + break; + case 32: + params.debug = optarg ? atoi(optarg) : 1; + break; + case 33: /* local-rcvbuf */ + params.local_rcvbuf = atoi(optarg)/2; + break; + case 34: /* local-sndbuf */ + params.local_sndbuf = atoi(optarg)/2; + break; + case 35: /* remote-rcvbuf */ + params.remote_rcvbuf = atoi(optarg)/2; + break; + case 36: /* remote-sndbuf */ + params.remote_sndbuf = atoi(optarg)/2; + break; + case 37: /* socks */ + params.proxy_type = CONN_TYPE_SOCKS; + break; + case 38: /* no-resolve */ + params.no_resolve = true; + break; + case 39: /* skip-nodelay */ + params.skip_nodelay = true; + break; + } + } + if (!params.bind_wait_only && !params.port) + { + fprintf(stderr, "Need port number\n"); + exit_clean(1); + } + if (params.binds_last<=0) + { + params.binds_last=0; // default bind to all + } + if (params.skip_nodelay && (params.split_http_req || params.split_pos)) + { + fprintf(stderr, "Cannot split with --skip-nodelay\n"); + exit_clean(1); + } +} + + +static bool is_linklocal(const struct sockaddr_in6* a) +{ + return a->sin6_addr.s6_addr[0]==0xFE && (a->sin6_addr.s6_addr[1] & 0xC0)==0x80; +} +static bool find_listen_addr(struct sockaddr_storage *salisten, const char *bindiface, bool bind_if6, bool bindll, int *if_index) +{ + struct ifaddrs *addrs,*a; + bool found=false; + + if (getifaddrs(&addrs)<0) + return false; + + int maxpass = (bind_if6 && !bindll) ? 2 : 1; + for(int pass=0;passifa_addr) + { + if (a->ifa_addr->sa_family==AF_INET && + *bindiface && !bind_if6 && !strcmp(a->ifa_name, bindiface)) + { + salisten->ss_family = AF_INET; + memcpy(&((struct sockaddr_in*)salisten)->sin_addr, &((struct sockaddr_in*)a->ifa_addr)->sin_addr, sizeof(struct in_addr)); + found=true; + goto ex; + } + // ipv6 links locals are fe80::/10 + else if (a->ifa_addr->sa_family==AF_INET6 + && + (!*bindiface && bindll || + *bindiface && bind_if6 && !strcmp(a->ifa_name, bindiface)) + && + (bindll && is_linklocal((struct sockaddr_in6*)a->ifa_addr) || + !bindll && (pass || !is_linklocal((struct sockaddr_in6*)a->ifa_addr))) + ) + { + salisten->ss_family = AF_INET6; + memcpy(&((struct sockaddr_in6*)salisten)->sin6_addr, &((struct sockaddr_in6*)a->ifa_addr)->sin6_addr, sizeof(struct in6_addr)); + if (if_index) *if_index = if_nametoindex(a->ifa_name); + found=true; + goto ex; + } + } + a = a->ifa_next; + } + } +ex: + freeifaddrs(addrs); + return found; +} + +static bool read_system_maxfiles(rlim_t *maxfile) +{ +#ifdef __linux__ + FILE *F; + int n; + uintmax_t um; + if (!(F=fopen("/proc/sys/fs/file-max","r"))) + return false; + n=fscanf(F,"%ju",&um); + fclose(F); + if (!n) return false; + *maxfile = (rlim_t)um; + return true; +#elif defined(BSD) + int maxfiles,mib[2]={CTL_KERN, KERN_MAXFILES}; + size_t len = sizeof(maxfiles); + if (sysctl(mib,2,&maxfiles,&len,NULL,0)==-1) + return false; + *maxfile = (rlim_t)maxfiles; + return true; +#else + return false; +#endif +} +static bool write_system_maxfiles(rlim_t maxfile) +{ +#ifdef __linux__ + FILE *F; + int n; + if (!(F=fopen("/proc/sys/fs/file-max","w"))) + return false; + n=fprintf(F,"%ju",(uintmax_t)maxfile); + fclose(F); + return !!n; +#elif defined(BSD) + int maxfiles=(int)maxfile,mib[2]={CTL_KERN, KERN_MAXFILES}; + if (sysctl(mib,2,NULL,0,&maxfiles,sizeof(maxfiles))==-1) + return false; + return true; +#else + return false; +#endif +} + +static bool set_ulimit() +{ + rlim_t fdmax,fdmin_system,cur_lim=0; + int n; + + if (!params.maxfiles) + { + // 4 fds per tamper connection (2 pipe + 2 socket), 6 fds for tcp proxy connection (4 pipe + 2 socket) + // additional 1/2 for unpaired remote legs sending buffers + // 16 for listen_fd, epoll, hostlist, ... +#ifdef SPLICE_PRESENT + fdmax = (params.tamper ? 4 : 6) * params.maxconn; +#else + fdmax = 2 * params.maxconn; +#endif + fdmax += fdmax/2 + 16; + } + else + fdmax = params.maxfiles; + fdmin_system = fdmax + 4096; + DBGPRINT("set_ulimit : fdmax=%ju fdmin_system=%ju",(uintmax_t)fdmax,(uintmax_t)fdmin_system) + + if (!read_system_maxfiles(&cur_lim)) + return false; + DBGPRINT("set_ulimit : current system file-max=%ju",(uintmax_t)cur_lim) + if (cur_lim 0) + { + int sec=0; + if (!is_interface_online(params.binds[i].bindiface)) + { + printf("waiting for ifup of %s for up to %d second(s)...\n",params.binds[i].bindiface,params.binds[i].bind_wait_ifup); + do + { + sleep(1); + sec++; + } + while (!is_interface_online(params.binds[i].bindiface) && sec=params.binds[i].bind_wait_ifup) + { + printf("wait timed out\n"); + goto exiterr; + } + } + } + if (!(if_index = if_nametoindex(params.binds[i].bindiface)) && params.binds[i].bind_wait_ip<=0) + { + printf("bad iface %s\n",params.binds[i].bindiface); + goto exiterr; + } + } + if (*params.binds[i].bindaddr) + { + if (inet_pton(AF_INET, params.binds[i].bindaddr, &((struct sockaddr_in*)(&list[i].salisten))->sin_addr)) + { + list[i].salisten.ss_family = AF_INET; + } + else if (inet_pton(AF_INET6, params.binds[i].bindaddr, &((struct sockaddr_in6*)(&list[i].salisten))->sin6_addr)) + { + list[i].salisten.ss_family = AF_INET6; + list[i].ipv6_only = 1; + } + else + { + printf("bad bind addr : %s\n", params.binds[i].bindaddr); + goto exiterr; + } + } + else + { + if (*params.binds[i].bindiface || params.binds[i].bindll) + { + bool found; + int sec=0; + + if (params.binds[i].bind_wait_ip > 0) + { + printf("waiting for ip on %s for up to %d second(s)...\n", *params.binds[i].bindiface ? params.binds[i].bindiface : "", params.binds[i].bind_wait_ip); + if (params.binds[i].bindll && !params.binds[i].bindll_force && params.binds[i].bind_wait_ip_ll>0) + printf("during the first %d second(s) accepting only link locals...\n", params.binds[i].bind_wait_ip_ll); + } + + for(;;) + { + found = find_listen_addr(&list[i].salisten,params.binds[i].bindiface,params.binds[i].bind_if6,params.binds[i].bindll,&if_index); + if (found) break; + + if (params.binds[i].bindll && !params.binds[i].bindll_force && sec>=params.binds[i].bind_wait_ip_ll) + if ((found = find_listen_addr(&list[i].salisten,params.binds[i].bindiface,params.binds[i].bind_if6,false,&if_index))) + { + printf("link local address wait timeout. using global address\n"); + break; + } + + if (sec>=params.binds[i].bind_wait_ip) + break; + + sleep(1); + sec++; + } + + if (!found) + { + printf("suitable ip address not found\n"); + goto exiterr; + } + list[i].ipv6_only=1; + } + else + { + list[i].salisten.ss_family = AF_INET6; + // leave sin6_addr zero + } + } + if (list[i].salisten.ss_family == AF_INET6) + { + list[i].salisten_len = sizeof(struct sockaddr_in6); + ((struct sockaddr_in6*)(&list[i].salisten))->sin6_port = htons(params.port); + if (is_linklocal((struct sockaddr_in6*)(&list[i].salisten))) + ((struct sockaddr_in6*)(&list[i].salisten))->sin6_scope_id = if_index; + } + else + { + list[i].salisten_len = sizeof(struct sockaddr_in); + ((struct sockaddr_in*)(&list[i].salisten))->sin_port = htons(params.port); + } + } + + if (params.bind_wait_only) + { + printf("bind wait condition satisfied. exiting.\n"); + exit_v = 0; + goto exiterr; + } + + if (params.proxy_type==CONN_TYPE_TRANSPARENT && !redir_init()) + { + fprintf(stderr,"could not initialize redirector !!!\n"); + goto exiterr; + } + + for(i=0;i<=params.binds_last;i++) + { + VPRINT("Binding %d",i); + + if ((listen_fd[i] = socket(list[i].salisten.ss_family, SOCK_STREAM, 0)) == -1) { + perror("socket: "); + goto exiterr; + } +#ifndef __OpenBSD__ +// in OpenBSD always IPV6_ONLY for wildcard sockets + if ((list[i].salisten.ss_family == AF_INET6) && setsockopt(listen_fd[i], IPPROTO_IPV6, IPV6_V6ONLY, &list[i].ipv6_only, sizeof(int)) == -1) + { + perror("setsockopt (IPV6_ONLY): "); + goto exiterr; + } +#endif + + if (setsockopt(listen_fd[i], SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) + { + perror("setsockopt (SO_REUSEADDR): "); + goto exiterr; + } + + //Mark that this socket can be used for transparent proxying + //This allows the socket to accept connections for non-local IPs + if (params.proxy_type==CONN_TYPE_TRANSPARENT) + { + #ifdef __linux__ + if (setsockopt(listen_fd[i], SOL_IP, IP_TRANSPARENT, &yes, sizeof(yes)) == -1) + { + perror("setsockopt (IP_TRANSPARENT): "); + goto exiterr; + } + #elif defined(BSD) && defined(SO_BINDANY) + if (setsockopt(listen_fd[i], SOL_SOCKET, SO_BINDANY, &yes, sizeof(yes)) == -1) + { + perror("setsockopt (SO_BINDANY): "); + goto exiterr; + } + #endif + } + + if (!set_socket_buffers(listen_fd[i], params.local_rcvbuf, params.local_sndbuf)) + goto exiterr; + if (!params.local_rcvbuf) + { + // HACK : dont know why but if dont set RCVBUF explicitly RCVBUF of accept()-ed socket can be very large. may be linux bug ? + int v; + socklen_t sz=sizeof(int); + if (!getsockopt(listen_fd[i],SOL_SOCKET,SO_RCVBUF,&v,&sz)) + { + v/=2; + setsockopt(listen_fd[i],SOL_SOCKET,SO_RCVBUF,&v,sizeof(int)); + } + } + if (bind(listen_fd[i], (struct sockaddr *)&list[i].salisten, list[i].salisten_len) == -1) { + perror("bind: "); + goto exiterr; + } + if (listen(listen_fd[i], BACKLOG) == -1) { + perror("listen: "); + goto exiterr; + } + } + + set_ulimit(); + + if (params.droproot && !droproot(params.uid,params.gid)) + goto exiterr; + print_id(); + + //splice() causes the process to receive the SIGPIPE-signal if one part (for + //example a socket) is closed during splice(). I would rather have splice() + //fail and return -1, so blocking SIGPIPE. + if (block_sigpipe() == -1) { + fprintf(stderr, "Could not block SIGPIPE signal\n"); + goto exiterr; + } + + printf(params.proxy_type==CONN_TYPE_SOCKS ? "socks mode\n" : "transparent proxy mode\n"); + if (!params.tamper) printf("TCP proxy mode (no tampering)\n"); + + signal(SIGHUP, onhup); + + retval = event_loop(listen_fd,params.binds_last+1); + exit_v = retval < 0 ? EXIT_FAILURE : EXIT_SUCCESS; + printf("Exiting\n"); + +exiterr: + redir_close(); + for(i=0;i<=params.binds_last;i++) if (listen_fd[i]!=-1) close(listen_fd[i]); + cleanup_params(); + return exit_v; +} diff --git a/tpws/tpws.h b/tpws/tpws.h new file mode 100644 index 0000000..0753ec5 --- /dev/null +++ b/tpws/tpws.h @@ -0,0 +1,9 @@ +#pragma once + +#ifdef __linux__ + #define SPLICE_PRESENT +#endif + +#include + +void dohup(); diff --git a/tpws/tpws_conn.c b/tpws/tpws_conn.c new file mode 100644 index 0000000..ab2b605 --- /dev/null +++ b/tpws/tpws_conn.c @@ -0,0 +1,1289 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tpws.h" +#include "tpws_conn.h" +#include "redirect.h" +#include "tamper.h" +#include "params.h" +#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; +/* +static void count_legs(struct tailhead *conn_list) +{ + tproxy_conn_t *conn = NULL; + + legs_local = legs_remote = 0; + TAILQ_FOREACH(conn, conn_list, conn_ptrs) + conn->remote ? legs_remote++ : legs_local++; + +} +*/ +static void print_legs() +{ + VPRINT("Legs : local:%d remote:%d", legs_local, legs_remote) +} + + +static bool socks5_send_rep(int fd,uint8_t rep) +{ + s5_rep s5rep; + memset(&s5rep,0,sizeof(s5rep)); + s5rep.ver = 5; + s5rep.rep = rep; + s5rep.atyp = S5_ATYP_IP4; + return send(fd,&s5rep,sizeof(s5rep),MSG_DONTWAIT)==sizeof(s5rep); +} +static bool socks5_send_rep_errno(int fd,int errn) +{ + uint8_t rep; + switch(errn) + { + case 0: + rep=S5_REP_OK; break; + case ECONNREFUSED: + rep=S5_REP_CONN_REFUSED; break; + case ENETUNREACH: + rep=S5_REP_NETWORK_UNREACHABLE; break; + case ETIMEDOUT: + case EHOSTUNREACH: + rep=S5_REP_HOST_UNREACHABLE; break; + default: + rep=S5_REP_GENERAL_FAILURE; + } + return socks5_send_rep(fd,rep); +} +static bool socks4_send_rep(int fd, uint8_t rep) +{ + s4_rep s4rep; + memset(&s4rep, 0, sizeof(s4rep)); + s4rep.rep = rep; + return send(fd, &s4rep, sizeof(s4rep), MSG_DONTWAIT) == sizeof(s4rep); +} +static bool socks4_send_rep_errno(int fd, int errn) +{ + return socks4_send_rep(fd, errn ? S4_REP_FAILED : S4_REP_OK); +} +static bool socks_send_rep(uint8_t ver, int fd, uint8_t rep5) +{ + return ver==5 ? socks5_send_rep(fd, rep5) : socks4_send_rep(fd, rep5 ? S4_REP_FAILED : S4_REP_OK); +} +static bool socks_send_rep_errno(uint8_t ver, int fd, int errn) +{ + return ver==5 ? socks5_send_rep_errno(fd,errn) : socks4_send_rep_errno(fd, errn); +} +static bool proxy_remote_conn_ack(tproxy_conn_t *conn, int sock_err) +{ + // if proxy mode acknowledge connection request + // conn = remote. conn->partner = local + if (!conn->remote || !conn->partner) return false; + bool bres = true; + switch(conn->partner->conn_type) + { + case CONN_TYPE_SOCKS: + if (conn->partner->socks_state==S_WAIT_CONNECTION) + { + conn->partner->socks_state=S_TCP; + bres = socks_send_rep_errno(conn->partner->socks_ver,conn->partner->fd,sock_err); + DBGPRINT("socks connection acknowledgement. bres=%d remote_errn=%d remote_fd=%d local_fd=%d",bres,sock_err,conn->fd,conn->partner->fd) + } + break; + } + return bres; +} + + + +static bool send_buffer_create(send_buffer_t *sb, char *data, size_t len) +{ + if (sb->data) + { + fprintf(stderr,"FATAL : send_buffer_create but buffer is not empty\n"); + exit(1); + } + sb->data = malloc(len); + if (!sb->data) + { + DBGPRINT("send_buffer_create failed. errno=%d",errno) + return false; + } + if (data) memcpy(sb->data,data,len); + sb->len = len; + sb->pos = 0; + return true; +} +static void send_buffer_free(send_buffer_t *sb) +{ + if (sb->data) + { + free(sb->data); + sb->data = NULL; + } +} +static void send_buffers_free(send_buffer_t *sb_array, int count) +{ + for (int i=0;iwr_buf,sizeof(conn->wr_buf)/sizeof(conn->wr_buf[0])); +} +static bool send_buffer_present(send_buffer_t *sb) +{ + return !!sb->data; +} +static bool send_buffers_present(send_buffer_t *sb_array, int count) +{ + for(int i=0;idata + sb->pos, sb->len - sb->pos, 0); + DBGPRINT("send_buffer_send len=%zu pos=%zu wr=%zd err=%d",sb->len,sb->pos,wr,errno) + if (wr>0) + { + sb->pos += wr; + if (sb->pos >= sb->len) + { + send_buffer_free(sb); + } + } + else if (wr<0 && errno==EAGAIN) wr=0; + + return wr; +} +static ssize_t send_buffers_send(send_buffer_t *sb_array, int count, int fd, size_t *real_wr) +{ + ssize_t wr=0,twr=0; + + for (int i=0;iconn_type==CONN_TYPE_SOCKS && conn->socks_state!=S_TCP); +} + +static bool conn_partner_alive(tproxy_conn_t *conn) +{ + return conn->partner && conn->partner->state!=CONN_CLOSED; +} +static bool conn_buffers_present(tproxy_conn_t *conn) +{ + return send_buffers_present(conn->wr_buf,sizeof(conn->wr_buf)/sizeof(conn->wr_buf[0])); +} +static ssize_t conn_buffers_send(tproxy_conn_t *conn) +{ + size_t wr,real_twr; + wr = send_buffers_send(conn->wr_buf,sizeof(conn->wr_buf)/sizeof(conn->wr_buf[0]), conn->fd, &real_twr); + conn->twr += real_twr; + return wr; +} +static bool conn_has_unsent(tproxy_conn_t *conn) +{ + return conn->wr_unsent || conn_buffers_present(conn); +} +static int conn_bytes_unread(tproxy_conn_t *conn) +{ + int numbytes=-1; + ioctl(conn->fd, FIONREAD, &numbytes); + return numbytes; +} +static bool conn_has_unsent_pair(tproxy_conn_t *conn) +{ + return conn_has_unsent(conn) || (conn_partner_alive(conn) && conn_has_unsent(conn->partner)); +} + + +static ssize_t send_or_buffer(send_buffer_t *sb, int fd, char *buf, size_t len) +{ + ssize_t wr=0; + if (len) + { + wr = send(fd, buf, len, 0); + if (wr<0 && errno==EAGAIN) wr=0; + if (wr>=0 && wr=2) + { + int v; + socklen_t sz; + sz=sizeof(int); + if (!getsockopt(fd,SOL_SOCKET,SO_RCVBUF,&v,&sz)) + DBGPRINT("fd=%d SO_RCVBUF=%d",fd,v) + sz=sizeof(int); + if (!getsockopt(fd,SOL_SOCKET,SO_SNDBUF,&v,&sz)) + DBGPRINT("fd=%d SO_SNDBUF=%d",fd,v) + } +} + +bool set_socket_buffers(int fd, int rcvbuf, int sndbuf) +{ + DBGPRINT("set_socket_buffers fd=%d rcvbuf=%d sndbuf=%d",fd,rcvbuf,sndbuf) + if (rcvbuf && setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(int)) <0) + { + perror("setsockopt (SO_RCVBUF): "); + close(fd); + return false; + } + if (sndbuf && setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(int)) <0) + { + perror("setsockopt (SO_SNDBUF): "); + close(fd); + return false; + } + dbgprint_socket_buffers(fd); + return true; +} + +//Createas a socket and initiates the connection to the host specified by +//remote_addr. +//Returns -1 if something fails, >0 on success (socket fd). +static int connect_remote(const struct sockaddr *remote_addr) +{ + int remote_fd = 0, yes = 1, no = 0; + + + if((remote_fd = socket(remote_addr->sa_family, SOCK_STREAM, 0)) < 0) + { + perror("socket (connect_remote): "); + return -1; + } + // Use NONBLOCK to avoid slow connects affecting the performance of other connections + // separate fcntl call to comply with macos + if (fcntl(remote_fd, F_SETFL, O_NONBLOCK)<0) + { + perror("socket set O_NONBLOCK (connect_remote): "); + close(remote_fd); + return -1; + } + if(setsockopt(remote_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) + { + perror("setsockopt (SO_REUSEADDR, connect_remote): "); + close(remote_fd); + return -1; + } + if (!set_socket_buffers(remote_fd, params.remote_rcvbuf, params.remote_sndbuf)) + return -1; + if(!set_keepalive(remote_fd)) + { + perror("set_keepalive: "); + close(remote_fd); + return -1; + } + if (setsockopt(remote_fd, IPPROTO_TCP, TCP_NODELAY, params.skip_nodelay ? &no : &yes, sizeof(int)) <0) + { + perror("setsockopt (SO_NODELAY, connect_remote): "); + close(remote_fd); + return -1; + } + if(connect(remote_fd, remote_addr, remote_addr->sa_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)) < 0) + { + if(errno != EINPROGRESS) + { + perror("connect (connect_remote): "); + close(remote_fd); + return -1; + } + } + DBGPRINT("Connecting remote fd=%d",remote_fd) + + return remote_fd; +} + + +//Free resources occupied by this connection +static void free_conn(tproxy_conn_t *conn) +{ + if (!conn) return; + if (conn->fd) close(conn->fd); + if (conn->splice_pipe[0]) + { + close(conn->splice_pipe[0]); + close(conn->splice_pipe[1]); + } + conn_free_buffers(conn); + if (conn->partner) conn->partner->partner=NULL; + free(conn); +} +static tproxy_conn_t *new_conn(int fd, bool remote) +{ + tproxy_conn_t *conn; + + //Create connection object and fill in information + if((conn = (tproxy_conn_t*) malloc(sizeof(tproxy_conn_t))) == NULL) + { + fprintf(stderr, "Could not allocate memory for connection\n"); + return NULL; + } + + memset(conn, 0, sizeof(tproxy_conn_t)); + conn->state = CONN_UNAVAILABLE; + conn->fd = fd; + conn->remote = remote; + +#ifdef SPLICE_PRESENT + // if dont tamper - both legs are spliced, create 2 pipes + // otherwise create pipe only in local leg + if((!params.tamper || !remote) && pipe2(conn->splice_pipe, O_NONBLOCK) != 0) + { + fprintf(stderr, "Could not create the splice pipe\n"); + free_conn(conn); + return NULL; + } +#endif + + return conn; +} + +static bool epoll_set(tproxy_conn_t *conn, uint32_t events) +{ + struct epoll_event ev; + + memset(&ev, 0, sizeof(ev)); + ev.events = events; + ev.data.ptr = (void*) conn; + DBGPRINT("epoll_set fd=%d events=%08X",conn->fd,events); + if(epoll_ctl(conn->efd, EPOLL_CTL_MOD, conn->fd, &ev)==-1 && + epoll_ctl(conn->efd, EPOLL_CTL_ADD, conn->fd, &ev)==-1) + { + perror("epoll_ctl (add/mod)"); + return false; + } + return true; +} +static bool epoll_del(tproxy_conn_t *conn) +{ + struct epoll_event ev; + + memset(&ev, 0, sizeof(ev)); + + DBGPRINT("epoll_del fd=%d",conn->fd); + if(epoll_ctl(conn->efd, EPOLL_CTL_DEL, conn->fd, &ev)==-1) + { + perror("epoll_ctl (del)"); + return false; + } + return true; +} + +static bool epoll_update_flow(tproxy_conn_t *conn) +{ + if (conn->bFlowInPrev==conn->bFlowIn && conn->bFlowOutPrev==conn->bFlowOut && conn->bPrevRdhup==(conn->state==CONN_RDHUP)) + return true; // unchanged, no need to syscall + uint32_t evtmask = (conn->state==CONN_RDHUP ? 0 : EPOLLRDHUP)|(conn->bFlowIn?EPOLLIN:0)|(conn->bFlowOut?EPOLLOUT:0); + if (!epoll_set(conn, evtmask)) + return false; + DBGPRINT("SET FLOW fd=%d to in=%d out=%d state_rdhup=%d",conn->fd,conn->bFlowIn,conn->bFlowOut,conn->state==CONN_RDHUP) + conn->bFlowInPrev = conn->bFlowIn; + conn->bFlowOutPrev = conn->bFlowOut; + conn->bPrevRdhup = (conn->state==CONN_RDHUP); + return true; +} +static bool epoll_set_flow(tproxy_conn_t *conn, bool bFlowIn, bool bFlowOut) +{ + conn->bFlowIn = bFlowIn; + conn->bFlowOut = bFlowOut; + return epoll_update_flow(conn); +} + +//Acquires information, initiates a connect and initialises a new connection +//object. Return NULL if anything fails, pointer to object otherwise +static tproxy_conn_t* add_tcp_connection(int efd, struct tailhead *conn_list,int local_fd, const struct sockaddr *accept_sa, uint16_t listen_port, conn_type_t proxy_type) +{ + struct sockaddr_storage orig_dst; + tproxy_conn_t *conn; + int remote_fd=0; + + if (proxy_type==CONN_TYPE_TRANSPARENT) + { + if(!get_dest_addr(local_fd, accept_sa, &orig_dst)) + { + fprintf(stderr, "Could not get destination address\n"); + close(local_fd); + return NULL; + } + if (check_local_ip((struct sockaddr*)&orig_dst) && saport((struct sockaddr*)&orig_dst)==listen_port) + { + VPRINT("Dropping connection to local address to the same port to avoid loop") + close(local_fd); + return NULL; + } + } + + // socket buffers inherited from listen_fd + dbgprint_socket_buffers(local_fd); + + if(!set_keepalive(local_fd)) + { + perror("set_keepalive: "); + close(local_fd); + return 0; + } + + if (proxy_type==CONN_TYPE_TRANSPARENT) + { + if ((remote_fd = connect_remote((struct sockaddr *)&orig_dst)) < 0) + { + fprintf(stderr, "Failed to connect\n"); + close(local_fd); + return NULL; + } + } + + if(!(conn = new_conn(local_fd, false))) + { + if (remote_fd) close(remote_fd); + close(local_fd); + return NULL; + } + conn->conn_type = proxy_type; // only local connection has proxy_type. remote is always in tcp mode + conn->state = CONN_AVAILABLE; // accepted connection is immediately available + conn->efd = efd; + + if (proxy_type==CONN_TYPE_TRANSPARENT) + { + if(!(conn->partner = new_conn(remote_fd, true))) + { + free_conn(conn); + close(remote_fd); + return NULL; + } + conn->partner->partner = conn; + conn->partner->efd = efd; + + //remote_fd is connecting. Non-blocking connects are signaled as done by + //socket being marked as ready for writing + if (!epoll_set(conn->partner, EPOLLOUT)) + { + free_conn(conn->partner); + free_conn(conn); + return NULL; + } + } + + //Transparent proxy mode : + // Local socket can be closed while waiting for connection attempt. I need + // to detect this when waiting for connect() to complete. However, I dont + // want to get EPOLLIN-events, as I dont want to receive any data before + // remote connection is established + //Proxy mode : I need to service proxy protocol + // remote connection not started until proxy handshake is complete + + if (!epoll_set(conn, proxy_type==CONN_TYPE_TRANSPARENT ? EPOLLRDHUP : (EPOLLIN|EPOLLRDHUP))) + { + free_conn(conn->partner); + free_conn(conn); + return NULL; + } + + TAILQ_INSERT_HEAD(conn_list, conn, conn_ptrs); + legs_local++; + if (conn->partner) + { + TAILQ_INSERT_HEAD(conn_list, conn->partner, conn_ptrs); + legs_remote++; + } + return conn; +} + +//Checks if a connection attempt was successful or not +//Returns true if successfull, false if not +static bool check_connection_attempt(tproxy_conn_t *conn, int efd) +{ + int errn = 0; + socklen_t optlen = sizeof(errn); + + if (conn->state!=CONN_UNAVAILABLE || !conn->remote) + { + // locals are connected since accept + // remote need to be checked only once + return true; + } + + if (!conn_partner_alive(conn)) + { + // local leg died ? + VPRINT("check_connection_attempt : fd=%d (remote) : local leg died. failing this connection attempt.", conn->fd) + return false; + } + + // check the connection was sucessfull. it means its not in in SO_ERROR state + if(getsockopt(conn->fd, SOL_SOCKET, SO_ERROR, &errn, &optlen) == -1) + { + perror("getsockopt (SO_ERROR)"); + return false; + } + if (!errn) + { + VPRINT("Socket fd=%d (remote) connected", conn->fd) + if (!epoll_set_flow(conn, true, false) || !epoll_set_flow(conn->partner, true, false)) + return false; + conn->state = CONN_AVAILABLE; + } + return proxy_remote_conn_ack(conn,get_so_error(conn->fd)) && !errn; +} + + + + +static bool epoll_set_flow_pair(tproxy_conn_t *conn) +{ + bool bHasUnsent = conn_has_unsent(conn); + bool bHasUnsentPartner = conn_partner_alive(conn) ? conn_has_unsent(conn->partner) : false; + + DBGPRINT("epoll_set_flow_pair fd=%d remote=%d partner_fd=%d bHasUnsent=%d bHasUnsentPartner=%d state_rdhup=%d", + conn->fd , conn->remote, conn_partner_alive(conn) ? conn->partner->fd : 0, bHasUnsent, bHasUnsentPartner, conn->state==CONN_RDHUP) + if (!epoll_set_flow(conn, !bHasUnsentPartner && (conn->state!=CONN_RDHUP), bHasUnsent || conn->state==CONN_RDHUP)) + return false; + if (conn_partner_alive(conn)) + { + if (!epoll_set_flow(conn->partner, !bHasUnsent && (conn->partner->state!=CONN_RDHUP), bHasUnsentPartner || conn->partner->state==CONN_RDHUP)) + return false; + } + return true; +} + +static bool handle_unsent(tproxy_conn_t *conn) +{ + ssize_t wr=0,twr=0; + + DBGPRINT("+handle_unsent, fd=%d has_unsent=%d has_unsent_partner=%d",conn->fd,conn_has_unsent(conn),conn_partner_alive(conn) ? conn_has_unsent(conn->partner) : false) + +#ifdef SPLICE_PRESENT + if (conn->wr_unsent) + { + wr = splice(conn->splice_pipe[0], NULL, conn->fd, NULL, conn->wr_unsent, SPLICE_F_MOVE | SPLICE_F_NONBLOCK); + DBGPRINT("splice unsent=%zd wr=%zd err=%d",conn->wr_unsent,wr,errno) + if (wr<0) + { + if (errno==EAGAIN) wr=0; + else return false; + } + twr += wr; + conn->twr += wr; + conn->wr_unsent -= wr; + } +#endif + if (!conn->wr_unsent && conn_buffers_present(conn)) + { + wr=conn_buffers_send(conn); + DBGPRINT("conn_buffers_send wr=%zd",wr) + if (wr<0) return false; + twr += wr; + } + return epoll_set_flow_pair(conn); +} + + +bool proxy_mode_connect_remote(const struct sockaddr *sa, tproxy_conn_t *conn, struct tailhead *conn_list) +{ + int remote_fd; + + if (params.debug>=1) + { + printf("socks target for fd=%d is : ", conn->fd); + print_sockaddr(sa); + printf("\n"); + } + if (check_local_ip((struct sockaddr *)sa)) + { + VPRINT("Dropping connection to local address for security reasons") + socks_send_rep(conn->socks_ver, conn->fd, S5_REP_NOT_ALLOWED_BY_RULESET); + return false; + } + + if ((remote_fd = connect_remote(sa)) < 0) + { + fprintf(stderr, "socks failed to connect (1) errno=%d\n", errno); + socks_send_rep_errno(conn->socks_ver, conn->fd, errno); + return false; + } + if (!(conn->partner = new_conn(remote_fd, true))) + { + close(remote_fd); + fprintf(stderr, "socks out-of-memory (1)\n"); + socks_send_rep(conn->socks_ver, conn->fd, S5_REP_GENERAL_FAILURE); + return false; + } + conn->partner->partner = conn; + conn->partner->efd = conn->efd; + if (!epoll_set(conn->partner, EPOLLOUT)) + { + fprintf(stderr, "socks epoll_set error %d\n", errno); + free_conn(conn->partner); + conn->partner = NULL; + socks_send_rep(conn->socks_ver, conn->fd, S5_REP_GENERAL_FAILURE); + return false; + } + TAILQ_INSERT_HEAD(conn_list, conn->partner, conn_ptrs); + legs_remote++; + print_legs(); + DBGPRINT("socks connecting") + conn->socks_state = S_WAIT_CONNECTION; + return true; +} + +static bool handle_proxy_mode(tproxy_conn_t *conn, struct tailhead *conn_list) +{ + // To simplify things I dont care about buffering. If message splits, I just hang up + // in proxy mode messages are short. they can be split only intentionally. all normal programs send them in one packet + + ssize_t rd,wr; + char buf[sizeof(s5_req)]; // s5_req - the largest possible req + struct sockaddr_storage ss; + + // receive proxy control message + rd=recv(conn->fd, buf, sizeof(buf), MSG_DONTWAIT); + DBGPRINT("handle_proxy_mode rd=%zd",rd) + if (rd<1) return false; // hangup + switch(conn->conn_type) + { + case CONN_TYPE_SOCKS: + switch(conn->socks_state) + { + case S_WAIT_HANDSHAKE: + DBGPRINT("S_WAIT_HANDSHAKE") + if (buf[0] != 5 && buf[0] != 4) return false; // unknown socks version + conn->socks_ver = buf[0]; + DBGPRINT("socks version %u", conn->socks_ver) + if (conn->socks_ver==5) + { + s5_handshake *m = (s5_handshake*)buf; + s5_handshake_ack ack; + uint8_t k; + + ack.ver=5; + if (!S5_REQ_HANDHSHAKE_VALID(m,rd)) + { + DBGPRINT("socks5 proxy handshake invalid") + return false; + } + for (k=0;knmethods;k++) if (m->methods[k]==S5_AUTH_NONE) break; + if (k>=m->nmethods) + { + DBGPRINT("socks5 client wants authentication but we dont support") + ack.method=S5_AUTH_UNACCEPTABLE; + wr=send(conn->fd,&ack,sizeof(ack),MSG_DONTWAIT); + return false; + } + DBGPRINT("socks5 recv valid handshake") + ack.method=S5_AUTH_NONE; + wr=send(conn->fd,&ack,sizeof(ack),MSG_DONTWAIT); + if (wr!=sizeof(ack)) + { + DBGPRINT("socks5 handshake ack send error. wr=%zd errno=%d",wr,errno) + return false; + } + DBGPRINT("socks5 send handshake ack OK") + conn->socks_state=S_WAIT_REQUEST; + return true; + } + else + { + // socks4 does not have separate handshake phase. it starts with connect request + // ipv6 and domain resolving are not supported + s4_req *m = (s4_req*)buf; + if (!S4_REQ_HEADER_VALID(m, rd)) + { + DBGPRINT("socks4 request invalid") + return false; + } + if (m->cmd!=S4_CMD_CONNECT) + { + // BIND is not supported + DBGPRINT("socks4 unsupported command %02X", m->cmd) + socks4_send_rep(conn->fd, S4_REP_FAILED); + return false; + } + if (!S4_REQ_CONNECT_VALID(m, rd)) + { + DBGPRINT("socks4 connect request invalid") + socks4_send_rep(conn->fd, S4_REP_FAILED); + return false; + } + if (!m->port) + { + DBGPRINT("socks4 zero port") + socks4_send_rep(conn->fd, S4_REP_FAILED); + return false; + } + ss.ss_family = AF_INET; + ((struct sockaddr_in*)&ss)->sin_port = m->port; + ((struct sockaddr_in*)&ss)->sin_addr.s_addr = m->ip; + return proxy_mode_connect_remote((struct sockaddr *)&ss, conn, conn_list); + } + break; + case S_WAIT_REQUEST: + DBGPRINT("S_WAIT_REQUEST") + { + s5_req *m = (s5_req*)buf; + + if (!S5_REQ_HEADER_VALID(m,rd)) + { + DBGPRINT("socks5 request invalid") + return false; + } + if (m->cmd!=S5_CMD_CONNECT) + { + // BIND and UDP are not supported + DBGPRINT("socks5 unsupported command %02X", m->cmd) + socks5_send_rep(conn->fd,S5_REP_COMMAND_NOT_SUPPORTED); + return false; + } + if (!S5_REQ_CONNECT_VALID(m,rd)) + { + DBGPRINT("socks5 connect request invalid") + return false; + } + DBGPRINT("socks5 recv valid connect request") + switch(m->atyp) + { + case S5_ATYP_IP4: + ss.ss_family = AF_INET; + ((struct sockaddr_in*)&ss)->sin_port = m->d4.port; + ((struct sockaddr_in*)&ss)->sin_addr = m->d4.addr; + break; + case S5_ATYP_IP6: + ss.ss_family = AF_INET6; + ((struct sockaddr_in6*)&ss)->sin6_port = m->d6.port; + ((struct sockaddr_in6*)&ss)->sin6_addr = m->d6.addr; + ((struct sockaddr_in6*)&ss)->sin6_flowinfo = 0; + ((struct sockaddr_in6*)&ss)->sin6_scope_id = 0; + break; + case S5_ATYP_DOM: + // NOTE : resolving is blocking. do you want it really ? + { + struct addrinfo *ai,hints; + char sdom[256]; + int r; + uint16_t port; + char sport[6]; + + if (params.no_resolve) + { + DBGPRINT("socks5 hostname resolving disabled") + socks5_send_rep(conn->fd,S5_REP_NOT_ALLOWED_BY_RULESET); + return false; + } + port=S5_PORT_FROM_DD(m,rd); + if (!port) + { + DBGPRINT("socks5 no port is given") + socks5_send_rep(conn->fd,S5_REP_HOST_UNREACHABLE); + return false; + } + snprintf(sport,sizeof(sport),"%u",port); + memcpy(sdom,m->dd.domport,m->dd.len); + sdom[m->dd.len] = '\0'; + DBGPRINT("socks5 resolving hostname '%s' port '%s'",sdom,sport) + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_socktype = SOCK_STREAM; + r=getaddrinfo(sdom,sport,&hints,&ai); + if (r) + { + DBGPRINT("socks5 getaddrinfo error %d",r) + socks5_send_rep(conn->fd,S5_REP_HOST_UNREACHABLE); + return false; + } + if (params.debug>=2) + { + printf("socks5 hostname resolved to :\n"); + print_addrinfo(ai); + } + memcpy(&ss,ai->ai_addr,ai->ai_addrlen); + freeaddrinfo(ai); + } + break; + default: + return false; // should not be here. S5_REQ_CONNECT_VALID checks for valid atyp + + } + return proxy_mode_connect_remote((struct sockaddr *)&ss,conn,conn_list); + } + break; + case S_WAIT_CONNECTION: + DBGPRINT("socks received message while in S_WAIT_CONNECTION. hanging up") + break; + default: + DBGPRINT("socks received message while in an unexpected connection state") + break; + } + break; + } + return false; +} + +#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) +{ + int numbytes; + ssize_t rd = 0, wr = 0; + size_t bs; + + + DBGPRINT("+handle_epoll") + + if (!conn_in_tcp_mode(conn)) + { + if (!(evt & EPOLLIN)) + return true; // nothing to read + return handle_proxy_mode(conn,conn_list); + } + + if (!handle_unsent(conn)) + return false; // error + if (!conn_partner_alive(conn) && !conn_has_unsent(conn)) + return false; // when no partner, we only waste read and send unsent + + if (!(evt & EPOLLIN)) + return true; // nothing to read + + if (!conn_partner_alive(conn)) + { + // throw it to a black hole + char waste[65070]; + ssize_t trd=0; + + while((rd=recv(conn->fd, waste, sizeof(waste), MSG_DONTWAIT))>0 && trdtrd+=rd; + } + DBGPRINT("wasted recv=%zd all_rd=%zd err=%d",rd,trd,errno) + return true; + } + + // do not receive new until old is sent + if (conn_has_unsent(conn->partner)) + return true; + + bool oom=false; + + numbytes=conn_bytes_unread(conn); + DBGPRINT("numbytes=%d",numbytes) + if (numbytes>0) + { +#ifdef SPLICE_PRESENT + if (!params.tamper || conn->remote) + { + // incoming data from remote leg we splice without touching + // pipe is in the local leg, so its in conn->partner->splice_pipe + // if we dont tamper - splice both legs + + rd = splice(conn->fd, NULL, conn->partner->splice_pipe[1], NULL, SPLICE_LEN, SPLICE_F_MOVE | SPLICE_F_NONBLOCK); + DBGPRINT("splice fd=%d remote=%d len=%d rd=%zd err=%d",conn->fd,conn->remote,SPLICE_LEN,rd,errno) + if (rd<0 && errno==EAGAIN) rd=0; + if (rd>0) + { + conn->trd += rd; + conn->partner->wr_unsent += rd; + wr = splice(conn->partner->splice_pipe[0], NULL, conn->partner->fd, NULL, conn->partner->wr_unsent, SPLICE_F_MOVE | SPLICE_F_NONBLOCK); + DBGPRINT("splice fd=%d remote=%d wr=%zd err=%d",conn->partner->fd,conn->partner->remote,wr,errno) + if (wr<0 && errno==EAGAIN) wr=0; + if (wr>0) + { + conn->partner->wr_unsent -= wr; + conn->partner->twr += wr; + } + } + } + else +#endif + { + // incoming data from local leg + char buf[RD_BLOCK_SIZE + 4]; + + rd = recv(conn->fd, buf, RD_BLOCK_SIZE, MSG_DONTWAIT); + DBGPRINT("recv fd=%d rd=%zd err=%d",conn->fd, rd,errno) + if (rd<0 && errno==EAGAIN) rd=0; + if (rd>0) + { + conn->trd+=rd; + + size_t split_pos=0; + + bs = rd; +#ifndef SPLICE_PRESENT + if (!conn->remote && params.tamper) +#endif + modify_tcp_segment(buf,sizeof(buf),&bs,&split_pos); + + if (split_pos) + { + VPRINT("Splitting at pos %zu", split_pos) + wr = send_or_buffer(conn->partner->wr_buf, conn->partner->fd, buf, split_pos); + DBGPRINT("send_or_buffer(1) fd=%d wr=%zd err=%d",conn->partner->fd,wr,errno) + if (wr >= 0) + { + conn->partner->twr += wr; + wr = send_or_buffer(conn->partner->wr_buf + 1, conn->partner->fd, buf + split_pos, bs - split_pos); + DBGPRINT("send_or_buffer(2) fd=%d wr=%zd err=%d",conn->partner->fd,wr,errno) + if (wr>0) conn->partner->twr += wr; + } + } + else + { + wr = send_or_buffer(conn->partner->wr_buf, conn->partner->fd, buf, bs); + DBGPRINT("send_or_buffer(3) fd=%d wr=%zd err=%d",conn->partner->fd,wr,errno) + if (wr>0) conn->partner->twr += wr; + } + if (wr<0 && errno==ENOMEM) oom=true; + } + } + + if (!epoll_set_flow_pair(conn)) + return false; + } + + DBGPRINT("-handle_epoll rd=%zd wr=%zd",rd,wr) + if (oom) DBGPRINT("handle_epoll: OUT_OF_MEMORY") + + // do not fail if partner fails. + // if partner fails there will be another epoll event with EPOLLHUP or EPOLLERR + return rd>=0 && !oom; +} + +static bool remove_closed_connections(int efd, struct tailhead *close_list) +{ + tproxy_conn_t *conn = NULL; + bool bRemoved = false; + + while ((conn = TAILQ_FIRST(close_list))) + { + TAILQ_REMOVE(close_list, conn, conn_ptrs); + + shutdown(conn->fd,SHUT_RDWR); + epoll_del(conn); + VPRINT("Socket fd=%d (partner_fd=%d, remote=%d) closed, connection removed. total_read=%zu total_write=%zu event_count=%d", + conn->fd, conn->partner ? conn->partner->fd : 0, conn->remote, conn->trd, conn->twr, conn->event_count) + if (conn->remote) legs_remote--; else legs_local--; + free_conn(conn); + bRemoved = true; + } + return bRemoved; +} + +// move to close list connection and its partner +static void close_tcp_conn(struct tailhead *conn_list, struct tailhead *close_list, tproxy_conn_t *conn) +{ + if (conn->state != CONN_CLOSED) + { + conn->state = CONN_CLOSED; + TAILQ_REMOVE(conn_list, conn, conn_ptrs); + TAILQ_INSERT_TAIL(close_list, conn, conn_ptrs); + } +} + + +static bool read_all_and_buffer(tproxy_conn_t *conn, int buffer_number) +{ + if (conn_partner_alive(conn)) + { + int numbytes=conn_bytes_unread(conn); + DBGPRINT("read_all_and_buffer(%d) numbytes=%d",buffer_number,numbytes) + if (numbytes>0) + { + if (send_buffer_create(conn->partner->wr_buf+buffer_number, NULL, numbytes)) + { + ssize_t rd = recv(conn->fd, conn->partner->wr_buf[buffer_number].data, numbytes, MSG_DONTWAIT); + if (rd>0) + { + conn->trd+=rd; + conn->partner->wr_buf[buffer_number].len = rd; + + conn->partner->bFlowOut = true; + if (epoll_update_flow(conn->partner)) + return true; + } + send_buffer_free(conn->partner->wr_buf+buffer_number); + } + } + } + return false; +} + + +static bool conn_timed_out(tproxy_conn_t *conn) +{ + if (conn->orphan_since && conn->state==CONN_UNAVAILABLE) + { + time_t timediff = time(NULL) - conn->orphan_since; + return timediff>=params.max_orphan_time; + } + else + return false; +} +static void conn_close_timed_out(struct tailhead *conn_list, struct tailhead *close_list) +{ + tproxy_conn_t *c,*cnext = NULL; + + DBGPRINT("conn_close_timed_out") + + c = TAILQ_FIRST(conn_list); + while(c) + { + cnext = TAILQ_NEXT(c,conn_ptrs); + if (conn_timed_out(c)) + { + DBGPRINT("closing timed out connection: fd=%d remote=%d",c->fd,c->remote) + close_tcp_conn(conn_list,close_list,c); + } + c = cnext; + } +} + +static void conn_close_both(struct tailhead *conn_list, struct tailhead *close_list, tproxy_conn_t *conn) +{ + if (conn_partner_alive(conn)) close_tcp_conn(conn_list,close_list,conn->partner); + close_tcp_conn(conn_list,close_list,conn); +} +static void conn_close_with_partner_check(struct tailhead *conn_list, struct tailhead *close_list, tproxy_conn_t *conn) +{ + close_tcp_conn(conn_list,close_list,conn); + if (conn_partner_alive(conn)) + { + if (!conn_has_unsent(conn->partner)) + close_tcp_conn(conn_list,close_list,conn->partner); + else if (conn->partner->remote && conn->partner->state==CONN_UNAVAILABLE && params.max_orphan_time) + // time out only remote legs that are not connected yet + conn->partner->orphan_since = time(NULL); + } +} + +int event_loop(int *listen_fd, size_t listen_fd_ct) +{ + int retval = 0, num_events = 0; + int tmp_fd = 0; //Used to temporarily hold the accepted file descriptor + tproxy_conn_t *conn = NULL; + int efd=0, i; + struct epoll_event ev, events[MAX_EPOLL_EVENTS]; + struct tailhead conn_list, close_list; + time_t tm,last_timeout_check=0; + tproxy_conn_t *listen_conn = NULL; + size_t sct; + struct sockaddr_storage accept_sa; + socklen_t accept_salen; + + if (!listen_fd_ct) return -1; + + legs_local = legs_remote = 0; + //Initialize queue (remember that TAILQ_HEAD just defines the struct) + TAILQ_INIT(&conn_list); + TAILQ_INIT(&close_list); + + if ((efd = epoll_create(1)) == -1) { + perror("epoll_create"); + return -1; + } + + if (!(listen_conn=calloc(listen_fd_ct,sizeof(*listen_conn)))) + { + perror("calloc listen_conn"); + return -1; + } + + //Start monitoring listen sockets + memset(&ev, 0, sizeof(ev)); + ev.events = EPOLLIN; + for(sct=0;sctevent_count++; + + if (conn->listener) + { + DBGPRINT("\nEVENT mask %08X fd=%d accept",events[i].events,conn->fd) + + accept_salen = sizeof(accept_sa); + //Accept new connection + tmp_fd = accept(conn->fd, (struct sockaddr*)&accept_sa, &accept_salen); + if (tmp_fd < 0) + { + perror("Failed to accept connection : "); + } + else if (legs_local >= params.maxconn) // each connection has 2 legs - local and remote + { + close(tmp_fd); + VPRINT("Too many local legs : %d", legs_local) + } + // separate fcntl call to comply with macos + else if (fcntl(tmp_fd, F_SETFL, O_NONBLOCK) < 0) + { + perror("socket set O_NONBLOCK (accept): "); + close(tmp_fd); + } + else if (!(conn=add_tcp_connection(efd, &conn_list, tmp_fd, (struct sockaddr*)&accept_sa, params.port, params.proxy_type))) + { + // add_tcp_connection closes fd in case of failure + VPRINT("Failed to add connection"); + } + else + { + print_legs(); + VPRINT("Socket fd=%d (local) connected", conn->fd) + } + } + else + { + DBGPRINT("\nEVENT mask %08X fd=%d remote=%d fd_partner=%d",events[i].events,conn->fd,conn->remote,conn_partner_alive(conn) ? conn->partner->fd : 0) + + if (conn->state != CONN_CLOSED) + { + if (events[i].events & (EPOLLHUP|EPOLLERR)) + { + int errn = get_so_error(conn->fd); + const char *se; + switch (events[i].events & (EPOLLHUP|EPOLLERR)) + { + case EPOLLERR: se="EPOLLERR"; break; + case EPOLLHUP: se="EPOLLHUP"; break; + case EPOLLHUP|EPOLLERR: se="EPOLLERR EPOLLHUP"; break; + default: se=NULL; + } + 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); + conn_close_with_partner_check(&conn_list,&close_list,conn); + continue; + } + if (events[i].events & EPOLLOUT) + { + if (!check_connection_attempt(conn, efd)) + { + VPRINT("Connection attempt failed for fd=%d", conn->fd) + conn_close_both(&conn_list,&close_list,conn); + continue; + } + } + if (events[i].events & EPOLLRDHUP) + { + DBGPRINT("EPOLLRDHUP") + read_all_and_buffer(conn,2); + + if (conn_has_unsent(conn)) + { + DBGPRINT("conn fd=%d has unsent, not closing", conn->fd) + conn->state = CONN_RDHUP; // only writes + epoll_set_flow(conn,false,true); + } + else + { + DBGPRINT("conn fd=%d has no unsent, closing", conn->fd) + conn_close_with_partner_check(&conn_list,&close_list,conn); + } + continue; + } + + if (events[i].events & (EPOLLIN|EPOLLOUT)) + { + // will not receive this until successful check_connection_attempt() + if (!handle_epoll(conn, &conn_list, events[i].events)) + { + DBGPRINT("handle_epoll false") + conn_close_with_partner_check(&conn_list,&close_list,conn); + continue; + } + } + } + + } + } + tm = time(NULL); + if (last_timeout_check!=tm) + { + // limit whole list lookups to once per second + last_timeout_check=tm; + conn_close_timed_out(&conn_list,&close_list); + } + if (remove_closed_connections(efd, &close_list)) + { + // at least one leg was removed. recount legs + print_legs(); + } + + fflush(stderr); fflush(stdout); // for console messages + } + +ex: + if (efd) close(efd); + if (listen_conn) free(listen_conn); + return retval; +} diff --git a/tpws/tpws_conn.h b/tpws/tpws_conn.h new file mode 100644 index 0000000..193695c --- /dev/null +++ b/tpws/tpws_conn.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include + +#define BACKLOG 10 +#define MAX_EPOLL_EVENTS 64 +#define IP_TRANSPARENT 19 //So that application compiles on OpenWRT +#define SPLICE_LEN 65536 +#define DEFAULT_MAX_CONN 512 +#define DEFAULT_MAX_ORPHAN_TIME 5 + +int event_loop(int *listen_fd, size_t listen_fd_ct); + +//Three different states of a connection +enum{ + CONN_UNAVAILABLE=0, // connecting + CONN_AVAILABLE, // operational + CONN_RDHUP, // received RDHUP, only sending unsent buffers. more RDHUPs are blocked + CONN_CLOSED // will be deleted soon +}; +typedef uint8_t conn_state_t; + +// data in a send_buffer can be sent in several stages +// pos indicates size of already sent data +// when pos==len its time to free buffer +struct send_buffer +{ + char *data; + size_t len,pos; +}; +typedef struct send_buffer send_buffer_t; + +enum{ + CONN_TYPE_TRANSPARENT=0, + CONN_TYPE_SOCKS +}; +typedef uint8_t conn_type_t; + +struct tproxy_conn +{ + bool listener; // true - listening socket. false = connecion socket + bool remote; // false - accepted, true - connected + int efd; // epoll fd + int fd; + int splice_pipe[2]; + conn_state_t state; + conn_type_t conn_type; + + struct tproxy_conn *partner; // other leg + time_t orphan_since; + + // socks5 state machine + enum { + S_WAIT_HANDSHAKE=0, + S_WAIT_REQUEST, + S_WAIT_CONNECTION, + S_TCP + } socks_state; + uint8_t socks_ver; + + // these value are used in flow control. we do not use ET (edge triggered) polling + // if we dont disable notifications they will come endlessly until condition becomes false and will eat all cpu time + bool bFlowIn,bFlowOut, bFlowInPrev,bFlowOutPrev, bPrevRdhup; + + // total read,write + size_t trd,twr; + // number of epoll_wait events + unsigned int event_count; + + // connection is either spliced or send/recv + // spliced connection have pipe buffering but also can have send_buffer's + // pipe buffer comes first, then send_buffer's from 0 to countof(wr_buf)-1 + // send/recv connection do not have pipe and wr_unsent is meaningless, always 0 + ssize_t wr_unsent; // unsent bytes in the pipe + // buffer 0 : send before split_pos + // buffer 1 : send after split_pos + // buffer 2 : after RDHUP read all and buffer to the partner + // buffer 3 : after HUP read all and buffer to the partner + // (2 and 3 should not be filled simultaneously, but who knows what can happen. if we have to refill non-empty buffer its FATAL) + // all buffers are sent strictly from 0 to countof(wr_buf)-1 + // buffer cannot be sent if there is unsent data in a lower buffer + struct send_buffer wr_buf[4]; + + //Create the struct which contains ptrs to next/prev element + TAILQ_ENTRY(tproxy_conn) conn_ptrs; +}; +typedef struct tproxy_conn tproxy_conn_t; + +//Define the struct tailhead (code in sys/queue.h is quite intuitive) +//Use tail queue for efficient delete +TAILQ_HEAD(tailhead, tproxy_conn); + + +bool set_socket_buffers(int fd, int rcvbuf, int sndbuf); diff --git a/tpws/uthash.h b/tpws/uthash.h new file mode 100644 index 0000000..f34c1f9 --- /dev/null +++ b/tpws/uthash.h @@ -0,0 +1,1217 @@ +/* +Copyright (c) 2003-2018, Troy D. Hanson http://troydhanson.github.com/uthash/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef UTHASH_H +#define UTHASH_H + +#define UTHASH_VERSION 2.0.2 + +#include /* memcmp, memset, strlen */ +#include /* ptrdiff_t */ +#include /* exit */ + +/* These macros use decltype or the earlier __typeof GNU extension. + As decltype is only available in newer compilers (VS2010 or gcc 4.3+ + when compiling c++ source) this code uses whatever method is needed + or, for VS2008 where neither is available, uses casting workarounds. */ +#if !defined(DECLTYPE) && !defined(NO_DECLTYPE) +#if defined(_MSC_VER) /* MS compiler */ +#if _MSC_VER >= 1600 && defined(__cplusplus) /* VS2010 or newer in C++ mode */ +#define DECLTYPE(x) (decltype(x)) +#else /* VS2008 or older (or VS2010 in C mode) */ +#define NO_DECLTYPE +#endif +#elif defined(__BORLANDC__) || defined(__ICCARM__) || defined(__LCC__) || defined(__WATCOMC__) +#define NO_DECLTYPE +#else /* GNU, Sun and other compilers */ +#define DECLTYPE(x) (__typeof(x)) +#endif +#endif + +#ifdef NO_DECLTYPE +#define DECLTYPE(x) +#define DECLTYPE_ASSIGN(dst,src) \ +do { \ + char **_da_dst = (char**)(&(dst)); \ + *_da_dst = (char*)(src); \ +} while (0) +#else +#define DECLTYPE_ASSIGN(dst,src) \ +do { \ + (dst) = DECLTYPE(dst)(src); \ +} while (0) +#endif + +/* a number of the hash function use uint32_t which isn't defined on Pre VS2010 */ +#if defined(_WIN32) +#if defined(_MSC_VER) && _MSC_VER >= 1600 +#include +#elif defined(__WATCOMC__) || defined(__MINGW32__) || defined(__CYGWIN__) +#include +#else +typedef unsigned int uint32_t; +typedef unsigned char uint8_t; +#endif +#elif defined(__GNUC__) && !defined(__VXWORKS__) +#include +#else +typedef unsigned int uint32_t; +typedef unsigned char uint8_t; +#endif + +#ifndef uthash_malloc +#define uthash_malloc(sz) malloc(sz) /* malloc fcn */ +#endif +#ifndef uthash_free +#define uthash_free(ptr,sz) free(ptr) /* free fcn */ +#endif +#ifndef uthash_bzero +#define uthash_bzero(a,n) memset(a,'\0',n) +#endif +#ifndef uthash_memcmp +#define uthash_memcmp(a,b,n) memcmp(a,b,n) +#endif +#ifndef uthash_strlen +#define uthash_strlen(s) strlen(s) +#endif + +#ifndef uthash_noexpand_fyi +#define uthash_noexpand_fyi(tbl) /* can be defined to log noexpand */ +#endif +#ifndef uthash_expand_fyi +#define uthash_expand_fyi(tbl) /* can be defined to log expands */ +#endif + +#ifndef HASH_NONFATAL_OOM +#define HASH_NONFATAL_OOM 0 +#endif + +#if HASH_NONFATAL_OOM +/* malloc failures can be recovered from */ + +#ifndef uthash_nonfatal_oom +#define uthash_nonfatal_oom(obj) do {} while (0) /* non-fatal OOM error */ +#endif + +#define HASH_RECORD_OOM(oomed) do { (oomed) = 1; } while (0) +#define IF_HASH_NONFATAL_OOM(x) x + +#else +/* malloc failures result in lost memory, hash tables are unusable */ + +#ifndef uthash_fatal +#define uthash_fatal(msg) exit(-1) /* fatal OOM error */ +#endif + +#define HASH_RECORD_OOM(oomed) uthash_fatal("out of memory") +#define IF_HASH_NONFATAL_OOM(x) + +#endif + +/* initial number of buckets */ +#define HASH_INITIAL_NUM_BUCKETS 32U /* initial number of buckets */ +#define HASH_INITIAL_NUM_BUCKETS_LOG2 5U /* lg2 of initial number of buckets */ +#define HASH_BKT_CAPACITY_THRESH 10U /* expand when bucket count reaches */ + +/* calculate the element whose hash handle address is hhp */ +#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho))) +/* calculate the hash handle from element address elp */ +#define HH_FROM_ELMT(tbl,elp) ((UT_hash_handle *)(((char*)(elp)) + ((tbl)->hho))) + +#define HASH_ROLLBACK_BKT(hh, head, itemptrhh) \ +do { \ + struct UT_hash_handle *_hd_hh_item = (itemptrhh); \ + unsigned _hd_bkt; \ + HASH_TO_BKT(_hd_hh_item->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \ + (head)->hh.tbl->buckets[_hd_bkt].count++; \ + _hd_hh_item->hh_next = NULL; \ + _hd_hh_item->hh_prev = NULL; \ +} while (0) + +#define HASH_VALUE(keyptr,keylen,hashv) \ +do { \ + HASH_FCN(keyptr, keylen, hashv); \ +} while (0) + +#define HASH_FIND_BYHASHVALUE(hh,head,keyptr,keylen,hashval,out) \ +do { \ + (out) = NULL; \ + if (head) { \ + unsigned _hf_bkt; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _hf_bkt); \ + if (HASH_BLOOM_TEST((head)->hh.tbl, hashval) != 0) { \ + HASH_FIND_IN_BKT((head)->hh.tbl, hh, (head)->hh.tbl->buckets[ _hf_bkt ], keyptr, keylen, hashval, out); \ + } \ + } \ +} while (0) + +#define HASH_FIND(hh,head,keyptr,keylen,out) \ +do { \ + unsigned _hf_hashv; \ + HASH_VALUE(keyptr, keylen, _hf_hashv); \ + HASH_FIND_BYHASHVALUE(hh, head, keyptr, keylen, _hf_hashv, out); \ +} while (0) + +#ifdef HASH_BLOOM +#define HASH_BLOOM_BITLEN (1UL << HASH_BLOOM) +#define HASH_BLOOM_BYTELEN (HASH_BLOOM_BITLEN/8UL) + (((HASH_BLOOM_BITLEN%8UL)!=0UL) ? 1UL : 0UL) +#define HASH_BLOOM_MAKE(tbl,oomed) \ +do { \ + (tbl)->bloom_nbits = HASH_BLOOM; \ + (tbl)->bloom_bv = (uint8_t*)uthash_malloc(HASH_BLOOM_BYTELEN); \ + if (!(tbl)->bloom_bv) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \ + (tbl)->bloom_sig = HASH_BLOOM_SIGNATURE; \ + } \ +} while (0) + +#define HASH_BLOOM_FREE(tbl) \ +do { \ + uthash_free((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \ +} while (0) + +#define HASH_BLOOM_BITSET(bv,idx) (bv[(idx)/8U] |= (1U << ((idx)%8U))) +#define HASH_BLOOM_BITTEST(bv,idx) (bv[(idx)/8U] & (1U << ((idx)%8U))) + +#define HASH_BLOOM_ADD(tbl,hashv) \ + HASH_BLOOM_BITSET((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U))) + +#define HASH_BLOOM_TEST(tbl,hashv) \ + HASH_BLOOM_BITTEST((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U))) + +#else +#define HASH_BLOOM_MAKE(tbl,oomed) +#define HASH_BLOOM_FREE(tbl) +#define HASH_BLOOM_ADD(tbl,hashv) +#define HASH_BLOOM_TEST(tbl,hashv) (1) +#define HASH_BLOOM_BYTELEN 0U +#endif + +#define HASH_MAKE_TABLE(hh,head,oomed) \ +do { \ + (head)->hh.tbl = (UT_hash_table*)uthash_malloc(sizeof(UT_hash_table)); \ + if (!(head)->hh.tbl) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head)->hh.tbl->tail = &((head)->hh); \ + (head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS; \ + (head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2; \ + (head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head); \ + (head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc( \ + HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \ + (head)->hh.tbl->signature = HASH_SIGNATURE; \ + if (!(head)->hh.tbl->buckets) { \ + HASH_RECORD_OOM(oomed); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + } else { \ + uthash_bzero((head)->hh.tbl->buckets, \ + HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \ + HASH_BLOOM_MAKE((head)->hh.tbl, oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (oomed) { \ + uthash_free((head)->hh.tbl->buckets, \ + HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + } \ + ) \ + } \ + } \ +} while (0) + +#define HASH_REPLACE_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,replaced,cmpfcn) \ +do { \ + (replaced) = NULL; \ + HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \ + if (replaced) { \ + HASH_DELETE(hh, head, replaced); \ + } \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn); \ +} while (0) + +#define HASH_REPLACE_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add,replaced) \ +do { \ + (replaced) = NULL; \ + HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \ + if (replaced) { \ + HASH_DELETE(hh, head, replaced); \ + } \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add); \ +} while (0) + +#define HASH_REPLACE(hh,head,fieldname,keylen_in,add,replaced) \ +do { \ + unsigned _hr_hashv; \ + HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \ + HASH_REPLACE_BYHASHVALUE(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced); \ +} while (0) + +#define HASH_REPLACE_INORDER(hh,head,fieldname,keylen_in,add,replaced,cmpfcn) \ +do { \ + unsigned _hr_hashv; \ + HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \ + HASH_REPLACE_BYHASHVALUE_INORDER(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced, cmpfcn); \ +} while (0) + +#define HASH_APPEND_LIST(hh, head, add) \ +do { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail); \ + (head)->hh.tbl->tail->next = (add); \ + (head)->hh.tbl->tail = &((add)->hh); \ +} while (0) + +#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \ +do { \ + do { \ + if (cmpfcn(DECLTYPE(head)(_hs_iter), add) > 0) { \ + break; \ + } \ + } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \ +} while (0) + +#ifdef NO_DECLTYPE +#undef HASH_AKBI_INNER_LOOP +#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \ +do { \ + char *_hs_saved_head = (char*)(head); \ + do { \ + DECLTYPE_ASSIGN(head, _hs_iter); \ + if (cmpfcn(head, add) > 0) { \ + DECLTYPE_ASSIGN(head, _hs_saved_head); \ + break; \ + } \ + DECLTYPE_ASSIGN(head, _hs_saved_head); \ + } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \ +} while (0) +#endif + +#if HASH_NONFATAL_OOM + +#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \ +do { \ + if (!(oomed)) { \ + unsigned _ha_bkt; \ + (head)->hh.tbl->num_items++; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \ + HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \ + if (oomed) { \ + HASH_ROLLBACK_BKT(hh, head, &(add)->hh); \ + HASH_DELETE_HH(hh, head, &(add)->hh); \ + (add)->hh.tbl = NULL; \ + uthash_nonfatal_oom(add); \ + } else { \ + HASH_BLOOM_ADD((head)->hh.tbl, hashval); \ + HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \ + } \ + } else { \ + (add)->hh.tbl = NULL; \ + uthash_nonfatal_oom(add); \ + } \ +} while (0) + +#else + +#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \ +do { \ + unsigned _ha_bkt; \ + (head)->hh.tbl->num_items++; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \ + HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \ + HASH_BLOOM_ADD((head)->hh.tbl, hashval); \ + HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \ +} while (0) + +#endif + + +#define HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh,head,keyptr,keylen_in,hashval,add,cmpfcn) \ +do { \ + IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \ + (add)->hh.hashv = (hashval); \ + (add)->hh.key = (char*) (keyptr); \ + (add)->hh.keylen = (unsigned) (keylen_in); \ + if (!(head)) { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = NULL; \ + HASH_MAKE_TABLE(hh, add, _ha_oomed); \ + IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \ + (head) = (add); \ + IF_HASH_NONFATAL_OOM( } ) \ + } else { \ + void *_hs_iter = (head); \ + (add)->hh.tbl = (head)->hh.tbl; \ + HASH_AKBI_INNER_LOOP(hh, head, add, cmpfcn); \ + if (_hs_iter) { \ + (add)->hh.next = _hs_iter; \ + if (((add)->hh.prev = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev)) { \ + HH_FROM_ELMT((head)->hh.tbl, (add)->hh.prev)->next = (add); \ + } else { \ + (head) = (add); \ + } \ + HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev = (add); \ + } else { \ + HASH_APPEND_LIST(hh, head, add); \ + } \ + } \ + HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \ + HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE_INORDER"); \ +} while (0) + +#define HASH_ADD_KEYPTR_INORDER(hh,head,keyptr,keylen_in,add,cmpfcn) \ +do { \ + unsigned _hs_hashv; \ + HASH_VALUE(keyptr, keylen_in, _hs_hashv); \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, keyptr, keylen_in, _hs_hashv, add, cmpfcn); \ +} while (0) + +#define HASH_ADD_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,cmpfcn) \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn) + +#define HASH_ADD_INORDER(hh,head,fieldname,keylen_in,add,cmpfcn) \ + HASH_ADD_KEYPTR_INORDER(hh, head, &((add)->fieldname), keylen_in, add, cmpfcn) + +#define HASH_ADD_KEYPTR_BYHASHVALUE(hh,head,keyptr,keylen_in,hashval,add) \ +do { \ + IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \ + (add)->hh.hashv = (hashval); \ + (add)->hh.key = (char*) (keyptr); \ + (add)->hh.keylen = (unsigned) (keylen_in); \ + if (!(head)) { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = NULL; \ + HASH_MAKE_TABLE(hh, add, _ha_oomed); \ + IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \ + (head) = (add); \ + IF_HASH_NONFATAL_OOM( } ) \ + } else { \ + (add)->hh.tbl = (head)->hh.tbl; \ + HASH_APPEND_LIST(hh, head, add); \ + } \ + HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \ + HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE"); \ +} while (0) + +#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add) \ +do { \ + unsigned _ha_hashv; \ + HASH_VALUE(keyptr, keylen_in, _ha_hashv); \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, keyptr, keylen_in, _ha_hashv, add); \ +} while (0) + +#define HASH_ADD_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add) \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add) + +#define HASH_ADD(hh,head,fieldname,keylen_in,add) \ + HASH_ADD_KEYPTR(hh, head, &((add)->fieldname), keylen_in, add) + +#define HASH_TO_BKT(hashv,num_bkts,bkt) \ +do { \ + bkt = ((hashv) & ((num_bkts) - 1U)); \ +} while (0) + +/* delete "delptr" from the hash table. + * "the usual" patch-up process for the app-order doubly-linked-list. + * The use of _hd_hh_del below deserves special explanation. + * These used to be expressed using (delptr) but that led to a bug + * if someone used the same symbol for the head and deletee, like + * HASH_DELETE(hh,users,users); + * We want that to work, but by changing the head (users) below + * we were forfeiting our ability to further refer to the deletee (users) + * in the patch-up process. Solution: use scratch space to + * copy the deletee pointer, then the latter references are via that + * scratch pointer rather than through the repointed (users) symbol. + */ +#define HASH_DELETE(hh,head,delptr) \ + HASH_DELETE_HH(hh, head, &(delptr)->hh) + +#define HASH_DELETE_HH(hh,head,delptrhh) \ +do { \ + struct UT_hash_handle *_hd_hh_del = (delptrhh); \ + if ((_hd_hh_del->prev == NULL) && (_hd_hh_del->next == NULL)) { \ + HASH_BLOOM_FREE((head)->hh.tbl); \ + uthash_free((head)->hh.tbl->buckets, \ + (head)->hh.tbl->num_buckets * sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head) = NULL; \ + } else { \ + unsigned _hd_bkt; \ + if (_hd_hh_del == (head)->hh.tbl->tail) { \ + (head)->hh.tbl->tail = HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev); \ + } \ + if (_hd_hh_del->prev != NULL) { \ + HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev)->next = _hd_hh_del->next; \ + } else { \ + DECLTYPE_ASSIGN(head, _hd_hh_del->next); \ + } \ + if (_hd_hh_del->next != NULL) { \ + HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->next)->prev = _hd_hh_del->prev; \ + } \ + HASH_TO_BKT(_hd_hh_del->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \ + HASH_DEL_IN_BKT((head)->hh.tbl->buckets[_hd_bkt], _hd_hh_del); \ + (head)->hh.tbl->num_items--; \ + } \ + HASH_FSCK(hh, head, "HASH_DELETE_HH"); \ +} while (0) + +/* convenience forms of HASH_FIND/HASH_ADD/HASH_DEL */ +#define HASH_FIND_STR(head,findstr,out) \ +do { \ + unsigned _uthash_hfstr_keylen = (unsigned)uthash_strlen(findstr); \ + HASH_FIND(hh, head, findstr, _uthash_hfstr_keylen, out); \ +} while (0) +#define HASH_ADD_STR(head,strfield,add) \ +do { \ + unsigned _uthash_hastr_keylen = (unsigned)uthash_strlen((add)->strfield); \ + HASH_ADD(hh, head, strfield[0], _uthash_hastr_keylen, add); \ +} while (0) +#define HASH_REPLACE_STR(head,strfield,add,replaced) \ +do { \ + unsigned _uthash_hrstr_keylen = (unsigned)uthash_strlen((add)->strfield); \ + HASH_REPLACE(hh, head, strfield[0], _uthash_hrstr_keylen, add, replaced); \ +} while (0) +#define HASH_FIND_INT(head,findint,out) \ + HASH_FIND(hh,head,findint,sizeof(int),out) +#define HASH_ADD_INT(head,intfield,add) \ + HASH_ADD(hh,head,intfield,sizeof(int),add) +#define HASH_REPLACE_INT(head,intfield,add,replaced) \ + HASH_REPLACE(hh,head,intfield,sizeof(int),add,replaced) +#define HASH_FIND_PTR(head,findptr,out) \ + HASH_FIND(hh,head,findptr,sizeof(void *),out) +#define HASH_ADD_PTR(head,ptrfield,add) \ + HASH_ADD(hh,head,ptrfield,sizeof(void *),add) +#define HASH_REPLACE_PTR(head,ptrfield,add,replaced) \ + HASH_REPLACE(hh,head,ptrfield,sizeof(void *),add,replaced) +#define HASH_DEL(head,delptr) \ + HASH_DELETE(hh,head,delptr) + +/* HASH_FSCK checks hash integrity on every add/delete when HASH_DEBUG is defined. + * This is for uthash developer only; it compiles away if HASH_DEBUG isn't defined. + */ +#ifdef HASH_DEBUG +#define HASH_OOPS(...) do { fprintf(stderr,__VA_ARGS__); exit(-1); } while (0) +#define HASH_FSCK(hh,head,where) \ +do { \ + struct UT_hash_handle *_thh; \ + if (head) { \ + unsigned _bkt_i; \ + unsigned _count = 0; \ + char *_prev; \ + for (_bkt_i = 0; _bkt_i < (head)->hh.tbl->num_buckets; ++_bkt_i) { \ + unsigned _bkt_count = 0; \ + _thh = (head)->hh.tbl->buckets[_bkt_i].hh_head; \ + _prev = NULL; \ + while (_thh) { \ + if (_prev != (char*)(_thh->hh_prev)) { \ + HASH_OOPS("%s: invalid hh_prev %p, actual %p\n", \ + (where), (void*)_thh->hh_prev, (void*)_prev); \ + } \ + _bkt_count++; \ + _prev = (char*)(_thh); \ + _thh = _thh->hh_next; \ + } \ + _count += _bkt_count; \ + if ((head)->hh.tbl->buckets[_bkt_i].count != _bkt_count) { \ + HASH_OOPS("%s: invalid bucket count %u, actual %u\n", \ + (where), (head)->hh.tbl->buckets[_bkt_i].count, _bkt_count); \ + } \ + } \ + if (_count != (head)->hh.tbl->num_items) { \ + HASH_OOPS("%s: invalid hh item count %u, actual %u\n", \ + (where), (head)->hh.tbl->num_items, _count); \ + } \ + _count = 0; \ + _prev = NULL; \ + _thh = &(head)->hh; \ + while (_thh) { \ + _count++; \ + if (_prev != (char*)_thh->prev) { \ + HASH_OOPS("%s: invalid prev %p, actual %p\n", \ + (where), (void*)_thh->prev, (void*)_prev); \ + } \ + _prev = (char*)ELMT_FROM_HH((head)->hh.tbl, _thh); \ + _thh = (_thh->next ? HH_FROM_ELMT((head)->hh.tbl, _thh->next) : NULL); \ + } \ + if (_count != (head)->hh.tbl->num_items) { \ + HASH_OOPS("%s: invalid app item count %u, actual %u\n", \ + (where), (head)->hh.tbl->num_items, _count); \ + } \ + } \ +} while (0) +#else +#define HASH_FSCK(hh,head,where) +#endif + +/* When compiled with -DHASH_EMIT_KEYS, length-prefixed keys are emitted to + * the descriptor to which this macro is defined for tuning the hash function. + * The app can #include to get the prototype for write(2). */ +#ifdef HASH_EMIT_KEYS +#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) \ +do { \ + unsigned _klen = fieldlen; \ + write(HASH_EMIT_KEYS, &_klen, sizeof(_klen)); \ + write(HASH_EMIT_KEYS, keyptr, (unsigned long)fieldlen); \ +} while (0) +#else +#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) +#endif + +/* default to Jenkin's hash unless overridden e.g. DHASH_FUNCTION=HASH_SAX */ +#ifdef HASH_FUNCTION +#define HASH_FCN HASH_FUNCTION +#else +#define HASH_FCN HASH_JEN +#endif + +/* The Bernstein hash function, used in Perl prior to v5.6. Note (x<<5+x)=x*33. */ +#define HASH_BER(key,keylen,hashv) \ +do { \ + unsigned _hb_keylen = (unsigned)keylen; \ + const unsigned char *_hb_key = (const unsigned char*)(key); \ + (hashv) = 0; \ + while (_hb_keylen-- != 0U) { \ + (hashv) = (((hashv) << 5) + (hashv)) + *_hb_key++; \ + } \ +} while (0) + + +/* SAX/FNV/OAT/JEN hash functions are macro variants of those listed at + * http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx */ +#define HASH_SAX(key,keylen,hashv) \ +do { \ + unsigned _sx_i; \ + const unsigned char *_hs_key = (const unsigned char*)(key); \ + hashv = 0; \ + for (_sx_i=0; _sx_i < keylen; _sx_i++) { \ + hashv ^= (hashv << 5) + (hashv >> 2) + _hs_key[_sx_i]; \ + } \ +} while (0) +/* FNV-1a variation */ +#define HASH_FNV(key,keylen,hashv) \ +do { \ + unsigned _fn_i; \ + const unsigned char *_hf_key = (const unsigned char*)(key); \ + (hashv) = 2166136261U; \ + for (_fn_i=0; _fn_i < keylen; _fn_i++) { \ + hashv = hashv ^ _hf_key[_fn_i]; \ + hashv = hashv * 16777619U; \ + } \ +} while (0) + +#define HASH_OAT(key,keylen,hashv) \ +do { \ + unsigned _ho_i; \ + const unsigned char *_ho_key=(const unsigned char*)(key); \ + hashv = 0; \ + for(_ho_i=0; _ho_i < keylen; _ho_i++) { \ + hashv += _ho_key[_ho_i]; \ + hashv += (hashv << 10); \ + hashv ^= (hashv >> 6); \ + } \ + hashv += (hashv << 3); \ + hashv ^= (hashv >> 11); \ + hashv += (hashv << 15); \ +} while (0) + +#define HASH_JEN_MIX(a,b,c) \ +do { \ + a -= b; a -= c; a ^= ( c >> 13 ); \ + b -= c; b -= a; b ^= ( a << 8 ); \ + c -= a; c -= b; c ^= ( b >> 13 ); \ + a -= b; a -= c; a ^= ( c >> 12 ); \ + b -= c; b -= a; b ^= ( a << 16 ); \ + c -= a; c -= b; c ^= ( b >> 5 ); \ + a -= b; a -= c; a ^= ( c >> 3 ); \ + b -= c; b -= a; b ^= ( a << 10 ); \ + c -= a; c -= b; c ^= ( b >> 15 ); \ +} while (0) + +#define HASH_JEN(key,keylen,hashv) \ +do { \ + unsigned _hj_i,_hj_j,_hj_k; \ + unsigned const char *_hj_key=(unsigned const char*)(key); \ + hashv = 0xfeedbeefu; \ + _hj_i = _hj_j = 0x9e3779b9u; \ + _hj_k = (unsigned)(keylen); \ + while (_hj_k >= 12U) { \ + _hj_i += (_hj_key[0] + ( (unsigned)_hj_key[1] << 8 ) \ + + ( (unsigned)_hj_key[2] << 16 ) \ + + ( (unsigned)_hj_key[3] << 24 ) ); \ + _hj_j += (_hj_key[4] + ( (unsigned)_hj_key[5] << 8 ) \ + + ( (unsigned)_hj_key[6] << 16 ) \ + + ( (unsigned)_hj_key[7] << 24 ) ); \ + hashv += (_hj_key[8] + ( (unsigned)_hj_key[9] << 8 ) \ + + ( (unsigned)_hj_key[10] << 16 ) \ + + ( (unsigned)_hj_key[11] << 24 ) ); \ + \ + HASH_JEN_MIX(_hj_i, _hj_j, hashv); \ + \ + _hj_key += 12; \ + _hj_k -= 12U; \ + } \ + hashv += (unsigned)(keylen); \ + switch ( _hj_k ) { \ + case 11: hashv += ( (unsigned)_hj_key[10] << 24 ); /* FALLTHROUGH */ \ + case 10: hashv += ( (unsigned)_hj_key[9] << 16 ); /* FALLTHROUGH */ \ + case 9: hashv += ( (unsigned)_hj_key[8] << 8 ); /* FALLTHROUGH */ \ + case 8: _hj_j += ( (unsigned)_hj_key[7] << 24 ); /* FALLTHROUGH */ \ + case 7: _hj_j += ( (unsigned)_hj_key[6] << 16 ); /* FALLTHROUGH */ \ + case 6: _hj_j += ( (unsigned)_hj_key[5] << 8 ); /* FALLTHROUGH */ \ + case 5: _hj_j += _hj_key[4]; /* FALLTHROUGH */ \ + case 4: _hj_i += ( (unsigned)_hj_key[3] << 24 ); /* FALLTHROUGH */ \ + case 3: _hj_i += ( (unsigned)_hj_key[2] << 16 ); /* FALLTHROUGH */ \ + case 2: _hj_i += ( (unsigned)_hj_key[1] << 8 ); /* FALLTHROUGH */ \ + case 1: _hj_i += _hj_key[0]; \ + } \ + HASH_JEN_MIX(_hj_i, _hj_j, hashv); \ +} while (0) + +/* The Paul Hsieh hash function */ +#undef get16bits +#if (defined(__GNUC__) && defined(__i386__)) || defined(__WATCOMC__) \ + || defined(_MSC_VER) || defined (__BORLANDC__) || defined (__TURBOC__) +#define get16bits(d) (*((const uint16_t *) (d))) +#endif + +#if !defined (get16bits) +#define get16bits(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8) \ + +(uint32_t)(((const uint8_t *)(d))[0]) ) +#endif +#define HASH_SFH(key,keylen,hashv) \ +do { \ + unsigned const char *_sfh_key=(unsigned const char*)(key); \ + uint32_t _sfh_tmp, _sfh_len = (uint32_t)keylen; \ + \ + unsigned _sfh_rem = _sfh_len & 3U; \ + _sfh_len >>= 2; \ + hashv = 0xcafebabeu; \ + \ + /* Main loop */ \ + for (;_sfh_len > 0U; _sfh_len--) { \ + hashv += get16bits (_sfh_key); \ + _sfh_tmp = ((uint32_t)(get16bits (_sfh_key+2)) << 11) ^ hashv; \ + hashv = (hashv << 16) ^ _sfh_tmp; \ + _sfh_key += 2U*sizeof (uint16_t); \ + hashv += hashv >> 11; \ + } \ + \ + /* Handle end cases */ \ + switch (_sfh_rem) { \ + case 3: hashv += get16bits (_sfh_key); \ + hashv ^= hashv << 16; \ + hashv ^= (uint32_t)(_sfh_key[sizeof (uint16_t)]) << 18; \ + hashv += hashv >> 11; \ + break; \ + case 2: hashv += get16bits (_sfh_key); \ + hashv ^= hashv << 11; \ + hashv += hashv >> 17; \ + break; \ + case 1: hashv += *_sfh_key; \ + hashv ^= hashv << 10; \ + hashv += hashv >> 1; \ + } \ + \ + /* Force "avalanching" of final 127 bits */ \ + hashv ^= hashv << 3; \ + hashv += hashv >> 5; \ + hashv ^= hashv << 4; \ + hashv += hashv >> 17; \ + hashv ^= hashv << 25; \ + hashv += hashv >> 6; \ +} while (0) + +#ifdef HASH_USING_NO_STRICT_ALIASING +/* The MurmurHash exploits some CPU's (x86,x86_64) tolerance for unaligned reads. + * For other types of CPU's (e.g. Sparc) an unaligned read causes a bus error. + * MurmurHash uses the faster approach only on CPU's where we know it's safe. + * + * Note the preprocessor built-in defines can be emitted using: + * + * gcc -m64 -dM -E - < /dev/null (on gcc) + * cc -## a.c (where a.c is a simple test file) (Sun Studio) + */ +#if (defined(__i386__) || defined(__x86_64__) || defined(_M_IX86)) +#define MUR_GETBLOCK(p,i) p[i] +#else /* non intel */ +#define MUR_PLUS0_ALIGNED(p) (((unsigned long)p & 3UL) == 0UL) +#define MUR_PLUS1_ALIGNED(p) (((unsigned long)p & 3UL) == 1UL) +#define MUR_PLUS2_ALIGNED(p) (((unsigned long)p & 3UL) == 2UL) +#define MUR_PLUS3_ALIGNED(p) (((unsigned long)p & 3UL) == 3UL) +#define WP(p) ((uint32_t*)((unsigned long)(p) & ~3UL)) +#if (defined(__BIG_ENDIAN__) || defined(SPARC) || defined(__ppc__) || defined(__ppc64__)) +#define MUR_THREE_ONE(p) ((((*WP(p))&0x00ffffff) << 8) | (((*(WP(p)+1))&0xff000000) >> 24)) +#define MUR_TWO_TWO(p) ((((*WP(p))&0x0000ffff) <<16) | (((*(WP(p)+1))&0xffff0000) >> 16)) +#define MUR_ONE_THREE(p) ((((*WP(p))&0x000000ff) <<24) | (((*(WP(p)+1))&0xffffff00) >> 8)) +#else /* assume little endian non-intel */ +#define MUR_THREE_ONE(p) ((((*WP(p))&0xffffff00) >> 8) | (((*(WP(p)+1))&0x000000ff) << 24)) +#define MUR_TWO_TWO(p) ((((*WP(p))&0xffff0000) >>16) | (((*(WP(p)+1))&0x0000ffff) << 16)) +#define MUR_ONE_THREE(p) ((((*WP(p))&0xff000000) >>24) | (((*(WP(p)+1))&0x00ffffff) << 8)) +#endif +#define MUR_GETBLOCK(p,i) (MUR_PLUS0_ALIGNED(p) ? ((p)[i]) : \ + (MUR_PLUS1_ALIGNED(p) ? MUR_THREE_ONE(p) : \ + (MUR_PLUS2_ALIGNED(p) ? MUR_TWO_TWO(p) : \ + MUR_ONE_THREE(p)))) +#endif +#define MUR_ROTL32(x,r) (((x) << (r)) | ((x) >> (32 - (r)))) +#define MUR_FMIX(_h) \ +do { \ + _h ^= _h >> 16; \ + _h *= 0x85ebca6bu; \ + _h ^= _h >> 13; \ + _h *= 0xc2b2ae35u; \ + _h ^= _h >> 16; \ +} while (0) + +#define HASH_MUR(key,keylen,hashv) \ +do { \ + const uint8_t *_mur_data = (const uint8_t*)(key); \ + const int _mur_nblocks = (int)(keylen) / 4; \ + uint32_t _mur_h1 = 0xf88D5353u; \ + uint32_t _mur_c1 = 0xcc9e2d51u; \ + uint32_t _mur_c2 = 0x1b873593u; \ + uint32_t _mur_k1 = 0; \ + const uint8_t *_mur_tail; \ + const uint32_t *_mur_blocks = (const uint32_t*)(_mur_data+(_mur_nblocks*4)); \ + int _mur_i; \ + for (_mur_i = -_mur_nblocks; _mur_i != 0; _mur_i++) { \ + _mur_k1 = MUR_GETBLOCK(_mur_blocks,_mur_i); \ + _mur_k1 *= _mur_c1; \ + _mur_k1 = MUR_ROTL32(_mur_k1,15); \ + _mur_k1 *= _mur_c2; \ + \ + _mur_h1 ^= _mur_k1; \ + _mur_h1 = MUR_ROTL32(_mur_h1,13); \ + _mur_h1 = (_mur_h1*5U) + 0xe6546b64u; \ + } \ + _mur_tail = (const uint8_t*)(_mur_data + (_mur_nblocks*4)); \ + _mur_k1=0; \ + switch ((keylen) & 3U) { \ + case 0: break; \ + case 3: _mur_k1 ^= (uint32_t)_mur_tail[2] << 16; /* FALLTHROUGH */ \ + case 2: _mur_k1 ^= (uint32_t)_mur_tail[1] << 8; /* FALLTHROUGH */ \ + case 1: _mur_k1 ^= (uint32_t)_mur_tail[0]; \ + _mur_k1 *= _mur_c1; \ + _mur_k1 = MUR_ROTL32(_mur_k1,15); \ + _mur_k1 *= _mur_c2; \ + _mur_h1 ^= _mur_k1; \ + } \ + _mur_h1 ^= (uint32_t)(keylen); \ + MUR_FMIX(_mur_h1); \ + hashv = _mur_h1; \ +} while (0) +#endif /* HASH_USING_NO_STRICT_ALIASING */ + +/* iterate over items in a known bucket to find desired item */ +#define HASH_FIND_IN_BKT(tbl,hh,head,keyptr,keylen_in,hashval,out) \ +do { \ + if ((head).hh_head != NULL) { \ + DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (head).hh_head)); \ + } else { \ + (out) = NULL; \ + } \ + while ((out) != NULL) { \ + if ((out)->hh.hashv == (hashval) && (out)->hh.keylen == (keylen_in)) { \ + if (uthash_memcmp((out)->hh.key, keyptr, keylen_in) == 0) { \ + break; \ + } \ + } \ + if ((out)->hh.hh_next != NULL) { \ + DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (out)->hh.hh_next)); \ + } else { \ + (out) = NULL; \ + } \ + } \ +} while (0) + +/* add an item to a bucket */ +#define HASH_ADD_TO_BKT(head,hh,addhh,oomed) \ +do { \ + UT_hash_bucket *_ha_head = &(head); \ + _ha_head->count++; \ + (addhh)->hh_next = _ha_head->hh_head; \ + (addhh)->hh_prev = NULL; \ + if (_ha_head->hh_head != NULL) { \ + _ha_head->hh_head->hh_prev = (addhh); \ + } \ + _ha_head->hh_head = (addhh); \ + if ((_ha_head->count >= ((_ha_head->expand_mult + 1U) * HASH_BKT_CAPACITY_THRESH)) \ + && !(addhh)->tbl->noexpand) { \ + HASH_EXPAND_BUCKETS(addhh,(addhh)->tbl, oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (oomed) { \ + HASH_DEL_IN_BKT(head,addhh); \ + } \ + ) \ + } \ +} while (0) + +/* remove an item from a given bucket */ +#define HASH_DEL_IN_BKT(head,delhh) \ +do { \ + UT_hash_bucket *_hd_head = &(head); \ + _hd_head->count--; \ + if (_hd_head->hh_head == (delhh)) { \ + _hd_head->hh_head = (delhh)->hh_next; \ + } \ + if ((delhh)->hh_prev) { \ + (delhh)->hh_prev->hh_next = (delhh)->hh_next; \ + } \ + if ((delhh)->hh_next) { \ + (delhh)->hh_next->hh_prev = (delhh)->hh_prev; \ + } \ +} while (0) + +/* Bucket expansion has the effect of doubling the number of buckets + * and redistributing the items into the new buckets. Ideally the + * items will distribute more or less evenly into the new buckets + * (the extent to which this is true is a measure of the quality of + * the hash function as it applies to the key domain). + * + * With the items distributed into more buckets, the chain length + * (item count) in each bucket is reduced. Thus by expanding buckets + * the hash keeps a bound on the chain length. This bounded chain + * length is the essence of how a hash provides constant time lookup. + * + * The calculation of tbl->ideal_chain_maxlen below deserves some + * explanation. First, keep in mind that we're calculating the ideal + * maximum chain length based on the *new* (doubled) bucket count. + * In fractions this is just n/b (n=number of items,b=new num buckets). + * Since the ideal chain length is an integer, we want to calculate + * ceil(n/b). We don't depend on floating point arithmetic in this + * hash, so to calculate ceil(n/b) with integers we could write + * + * ceil(n/b) = (n/b) + ((n%b)?1:0) + * + * and in fact a previous version of this hash did just that. + * But now we have improved things a bit by recognizing that b is + * always a power of two. We keep its base 2 log handy (call it lb), + * so now we can write this with a bit shift and logical AND: + * + * ceil(n/b) = (n>>lb) + ( (n & (b-1)) ? 1:0) + * + */ +#define HASH_EXPAND_BUCKETS(hh,tbl,oomed) \ +do { \ + unsigned _he_bkt; \ + unsigned _he_bkt_i; \ + struct UT_hash_handle *_he_thh, *_he_hh_nxt; \ + UT_hash_bucket *_he_new_buckets, *_he_newbkt; \ + _he_new_buckets = (UT_hash_bucket*)uthash_malloc( \ + 2UL * (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + if (!_he_new_buckets) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero(_he_new_buckets, \ + 2UL * (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + (tbl)->ideal_chain_maxlen = \ + ((tbl)->num_items >> ((tbl)->log2_num_buckets+1U)) + \ + ((((tbl)->num_items & (((tbl)->num_buckets*2U)-1U)) != 0U) ? 1U : 0U); \ + (tbl)->nonideal_items = 0; \ + for (_he_bkt_i = 0; _he_bkt_i < (tbl)->num_buckets; _he_bkt_i++) { \ + _he_thh = (tbl)->buckets[ _he_bkt_i ].hh_head; \ + while (_he_thh != NULL) { \ + _he_hh_nxt = _he_thh->hh_next; \ + HASH_TO_BKT(_he_thh->hashv, (tbl)->num_buckets * 2U, _he_bkt); \ + _he_newbkt = &(_he_new_buckets[_he_bkt]); \ + if (++(_he_newbkt->count) > (tbl)->ideal_chain_maxlen) { \ + (tbl)->nonideal_items++; \ + _he_newbkt->expand_mult = _he_newbkt->count / (tbl)->ideal_chain_maxlen; \ + } \ + _he_thh->hh_prev = NULL; \ + _he_thh->hh_next = _he_newbkt->hh_head; \ + if (_he_newbkt->hh_head != NULL) { \ + _he_newbkt->hh_head->hh_prev = _he_thh; \ + } \ + _he_newbkt->hh_head = _he_thh; \ + _he_thh = _he_hh_nxt; \ + } \ + } \ + uthash_free((tbl)->buckets, (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + (tbl)->num_buckets *= 2U; \ + (tbl)->log2_num_buckets++; \ + (tbl)->buckets = _he_new_buckets; \ + (tbl)->ineff_expands = ((tbl)->nonideal_items > ((tbl)->num_items >> 1)) ? \ + ((tbl)->ineff_expands+1U) : 0U; \ + if ((tbl)->ineff_expands > 1U) { \ + (tbl)->noexpand = 1; \ + uthash_noexpand_fyi(tbl); \ + } \ + uthash_expand_fyi(tbl); \ + } \ +} while (0) + + +/* This is an adaptation of Simon Tatham's O(n log(n)) mergesort */ +/* Note that HASH_SORT assumes the hash handle name to be hh. + * HASH_SRT was added to allow the hash handle name to be passed in. */ +#define HASH_SORT(head,cmpfcn) HASH_SRT(hh,head,cmpfcn) +#define HASH_SRT(hh,head,cmpfcn) \ +do { \ + unsigned _hs_i; \ + unsigned _hs_looping,_hs_nmerges,_hs_insize,_hs_psize,_hs_qsize; \ + struct UT_hash_handle *_hs_p, *_hs_q, *_hs_e, *_hs_list, *_hs_tail; \ + if (head != NULL) { \ + _hs_insize = 1; \ + _hs_looping = 1; \ + _hs_list = &((head)->hh); \ + while (_hs_looping != 0U) { \ + _hs_p = _hs_list; \ + _hs_list = NULL; \ + _hs_tail = NULL; \ + _hs_nmerges = 0; \ + while (_hs_p != NULL) { \ + _hs_nmerges++; \ + _hs_q = _hs_p; \ + _hs_psize = 0; \ + for (_hs_i = 0; _hs_i < _hs_insize; ++_hs_i) { \ + _hs_psize++; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + if (_hs_q == NULL) { \ + break; \ + } \ + } \ + _hs_qsize = _hs_insize; \ + while ((_hs_psize != 0U) || ((_hs_qsize != 0U) && (_hs_q != NULL))) { \ + if (_hs_psize == 0U) { \ + _hs_e = _hs_q; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + _hs_qsize--; \ + } else if ((_hs_qsize == 0U) || (_hs_q == NULL)) { \ + _hs_e = _hs_p; \ + if (_hs_p != NULL) { \ + _hs_p = ((_hs_p->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \ + } \ + _hs_psize--; \ + } else if ((cmpfcn( \ + DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_p)), \ + DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_q)) \ + )) <= 0) { \ + _hs_e = _hs_p; \ + if (_hs_p != NULL) { \ + _hs_p = ((_hs_p->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \ + } \ + _hs_psize--; \ + } else { \ + _hs_e = _hs_q; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + _hs_qsize--; \ + } \ + if ( _hs_tail != NULL ) { \ + _hs_tail->next = ((_hs_e != NULL) ? \ + ELMT_FROM_HH((head)->hh.tbl, _hs_e) : NULL); \ + } else { \ + _hs_list = _hs_e; \ + } \ + if (_hs_e != NULL) { \ + _hs_e->prev = ((_hs_tail != NULL) ? \ + ELMT_FROM_HH((head)->hh.tbl, _hs_tail) : NULL); \ + } \ + _hs_tail = _hs_e; \ + } \ + _hs_p = _hs_q; \ + } \ + if (_hs_tail != NULL) { \ + _hs_tail->next = NULL; \ + } \ + if (_hs_nmerges <= 1U) { \ + _hs_looping = 0; \ + (head)->hh.tbl->tail = _hs_tail; \ + DECLTYPE_ASSIGN(head, ELMT_FROM_HH((head)->hh.tbl, _hs_list)); \ + } \ + _hs_insize *= 2U; \ + } \ + HASH_FSCK(hh, head, "HASH_SRT"); \ + } \ +} while (0) + +/* This function selects items from one hash into another hash. + * The end result is that the selected items have dual presence + * in both hashes. There is no copy of the items made; rather + * they are added into the new hash through a secondary hash + * hash handle that must be present in the structure. */ +#define HASH_SELECT(hh_dst, dst, hh_src, src, cond) \ +do { \ + unsigned _src_bkt, _dst_bkt; \ + void *_last_elt = NULL, *_elt; \ + UT_hash_handle *_src_hh, *_dst_hh, *_last_elt_hh=NULL; \ + ptrdiff_t _dst_hho = ((char*)(&(dst)->hh_dst) - (char*)(dst)); \ + if ((src) != NULL) { \ + for (_src_bkt=0; _src_bkt < (src)->hh_src.tbl->num_buckets; _src_bkt++) { \ + for (_src_hh = (src)->hh_src.tbl->buckets[_src_bkt].hh_head; \ + _src_hh != NULL; \ + _src_hh = _src_hh->hh_next) { \ + _elt = ELMT_FROM_HH((src)->hh_src.tbl, _src_hh); \ + if (cond(_elt)) { \ + IF_HASH_NONFATAL_OOM( int _hs_oomed = 0; ) \ + _dst_hh = (UT_hash_handle*)(((char*)_elt) + _dst_hho); \ + _dst_hh->key = _src_hh->key; \ + _dst_hh->keylen = _src_hh->keylen; \ + _dst_hh->hashv = _src_hh->hashv; \ + _dst_hh->prev = _last_elt; \ + _dst_hh->next = NULL; \ + if (_last_elt_hh != NULL) { \ + _last_elt_hh->next = _elt; \ + } \ + if ((dst) == NULL) { \ + DECLTYPE_ASSIGN(dst, _elt); \ + HASH_MAKE_TABLE(hh_dst, dst, _hs_oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (_hs_oomed) { \ + uthash_nonfatal_oom(_elt); \ + (dst) = NULL; \ + continue; \ + } \ + ) \ + } else { \ + _dst_hh->tbl = (dst)->hh_dst.tbl; \ + } \ + HASH_TO_BKT(_dst_hh->hashv, _dst_hh->tbl->num_buckets, _dst_bkt); \ + HASH_ADD_TO_BKT(_dst_hh->tbl->buckets[_dst_bkt], hh_dst, _dst_hh, _hs_oomed); \ + (dst)->hh_dst.tbl->num_items++; \ + IF_HASH_NONFATAL_OOM( \ + if (_hs_oomed) { \ + HASH_ROLLBACK_BKT(hh_dst, dst, _dst_hh); \ + HASH_DELETE_HH(hh_dst, dst, _dst_hh); \ + _dst_hh->tbl = NULL; \ + uthash_nonfatal_oom(_elt); \ + continue; \ + } \ + ) \ + HASH_BLOOM_ADD(_dst_hh->tbl, _dst_hh->hashv); \ + _last_elt = _elt; \ + _last_elt_hh = _dst_hh; \ + } \ + } \ + } \ + } \ + HASH_FSCK(hh_dst, dst, "HASH_SELECT"); \ +} while (0) + +#define HASH_CLEAR(hh,head) \ +do { \ + if ((head) != NULL) { \ + HASH_BLOOM_FREE((head)->hh.tbl); \ + uthash_free((head)->hh.tbl->buckets, \ + (head)->hh.tbl->num_buckets*sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head) = NULL; \ + } \ +} while (0) + +#define HASH_OVERHEAD(hh,head) \ + (((head) != NULL) ? ( \ + (size_t)(((head)->hh.tbl->num_items * sizeof(UT_hash_handle)) + \ + ((head)->hh.tbl->num_buckets * sizeof(UT_hash_bucket)) + \ + sizeof(UT_hash_table) + \ + (HASH_BLOOM_BYTELEN))) : 0U) + +#ifdef NO_DECLTYPE +#define HASH_ITER(hh,head,el,tmp) \ +for(((el)=(head)), ((*(char**)(&(tmp)))=(char*)((head!=NULL)?(head)->hh.next:NULL)); \ + (el) != NULL; ((el)=(tmp)), ((*(char**)(&(tmp)))=(char*)((tmp!=NULL)?(tmp)->hh.next:NULL))) +#else +#define HASH_ITER(hh,head,el,tmp) \ +for(((el)=(head)), ((tmp)=DECLTYPE(el)((head!=NULL)?(head)->hh.next:NULL)); \ + (el) != NULL; ((el)=(tmp)), ((tmp)=DECLTYPE(el)((tmp!=NULL)?(tmp)->hh.next:NULL))) +#endif + +/* obtain a count of items in the hash */ +#define HASH_COUNT(head) HASH_CNT(hh,head) +#define HASH_CNT(hh,head) ((head != NULL)?((head)->hh.tbl->num_items):0U) + +typedef struct UT_hash_bucket { + struct UT_hash_handle *hh_head; + unsigned count; + + /* expand_mult is normally set to 0. In this situation, the max chain length + * threshold is enforced at its default value, HASH_BKT_CAPACITY_THRESH. (If + * the bucket's chain exceeds this length, bucket expansion is triggered). + * However, setting expand_mult to a non-zero value delays bucket expansion + * (that would be triggered by additions to this particular bucket) + * until its chain length reaches a *multiple* of HASH_BKT_CAPACITY_THRESH. + * (The multiplier is simply expand_mult+1). The whole idea of this + * multiplier is to reduce bucket expansions, since they are expensive, in + * situations where we know that a particular bucket tends to be overused. + * It is better to let its chain length grow to a longer yet-still-bounded + * value, than to do an O(n) bucket expansion too often. + */ + unsigned expand_mult; + +} UT_hash_bucket; + +/* random signature used only to find hash tables in external analysis */ +#define HASH_SIGNATURE 0xa0111fe1u +#define HASH_BLOOM_SIGNATURE 0xb12220f2u + +typedef struct UT_hash_table { + UT_hash_bucket *buckets; + unsigned num_buckets, log2_num_buckets; + unsigned num_items; + struct UT_hash_handle *tail; /* tail hh in app order, for fast append */ + ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */ + + /* in an ideal situation (all buckets used equally), no bucket would have + * more than ceil(#items/#buckets) items. that's the ideal chain length. */ + unsigned ideal_chain_maxlen; + + /* nonideal_items is the number of items in the hash whose chain position + * exceeds the ideal chain maxlen. these items pay the penalty for an uneven + * hash distribution; reaching them in a chain traversal takes >ideal steps */ + unsigned nonideal_items; + + /* ineffective expands occur when a bucket doubling was performed, but + * afterward, more than half the items in the hash had nonideal chain + * positions. If this happens on two consecutive expansions we inhibit any + * further expansion, as it's not helping; this happens when the hash + * function isn't a good fit for the key domain. When expansion is inhibited + * the hash will still work, albeit no longer in constant time. */ + unsigned ineff_expands, noexpand; + + uint32_t signature; /* used only to find hash tables in external analysis */ +#ifdef HASH_BLOOM + uint32_t bloom_sig; /* used only to test bloom exists in external analysis */ + uint8_t *bloom_bv; + uint8_t bloom_nbits; +#endif + +} UT_hash_table; + +typedef struct UT_hash_handle { + struct UT_hash_table *tbl; + void *prev; /* prev element in app order */ + void *next; /* next element in app order */ + struct UT_hash_handle *hh_prev; /* previous hh in bucket order */ + struct UT_hash_handle *hh_next; /* next hh in bucket order */ + void *key; /* ptr to enclosing struct's key */ + unsigned keylen; /* enclosing struct's key len */ + unsigned hashv; /* result of hash-fcn(key) */ +} UT_hash_handle; + +#endif /* UTHASH_H */ diff --git a/uninstall_easy.sh b/uninstall_easy.sh new file mode 100755 index 0000000..c06f84f --- /dev/null +++ b/uninstall_easy.sh @@ -0,0 +1,259 @@ +#!/bin/sh + +# automated script for easy uninstalling zapret + +EXEDIR="$(dirname "$0")" +EXEDIR="$(cd "$EXEDIR"; pwd)" +IPSET_DIR="$EXEDIR/ipset" + +GET_LIST_PREFIX=/ipset/get_ +SYSTEMD_SYSTEM_DIR=/lib/systemd/system +[ -d "$SYSTEMD_SYSTEM_DIR" ] || SYSTEMD_SYSTEM_DIR=/usr/lib/systemd/system + +exists() +{ + which $1 >/dev/null 2>/dev/null +} +whichq() +{ + which $1 2>/dev/null +} + +exitp() +{ + echo + echo press enter to continue + read A + exit $1 +} + +require_root() +{ + [ $(id -u) -ne "0" ] && { + echo root is required + exists sudo && exec sudo "$0" + exists su && exec su -c "$0" + echo su or sudo not found + exitp 2 + } +} + + +check_system() +{ + echo \* checking system + + SYSTEM="" + SYSTEMCTL=$(whichq systemctl) + + local UNAME=$(uname) + if [ "$UNAME" = "Linux" ]; then + if [ -x "$SYSTEMCTL" ] ; then + SYSTEM=systemd + elif [ -f "/etc/openwrt_release" ] && exists opkg && exists uci ; then + SYSTEM=openwrt + else + echo system is not either systemd based or openwrt. check readme.txt for manual setup info. + exitp 5 + fi + elif [ "$UNAME" = "Darwin" ]; then + SYSTEM=macos + else + echo easy installer only supports Linux and MacOS. check readme.txt for supported systems and manual setup info. + exitp 5 + fi + echo system is based on $SYSTEM +} + + +crontab_del() +{ + exists crontab || return + + echo \* removing crontab entry + + CRONTMP=/tmp/cron.tmp + crontab -l >$CRONTMP 2>/dev/null + if grep -q "$GET_LIST_PREFIX" $CRONTMP; then + echo removing following entries from crontab : + grep "$GET_LIST_PREFIX" $CRONTMP + grep -v "$GET_LIST_PREFIX" $CRONTMP >$CRONTMP.2 + crontab $CRONTMP.2 + rm -f $CRONTMP.2 + fi + rm -f $CRONTMP +} + + +service_stop_systemd() +{ + echo \* stopping zapret service + + "$SYSTEMCTL" daemon-reload + "$SYSTEMCTL" disable zapret + "$SYSTEMCTL" stop zapret +} + +service_remove_systemd() +{ + echo \* removing zapret service + + rm -f "$SYSTEMD_SYSTEM_DIR/zapret.service" + "$SYSTEMCTL" daemon-reload +} + +timer_remove_systemd() +{ + echo \* removing zapret-list-update timer + + "$SYSTEMCTL" daemon-reload + "$SYSTEMCTL" disable zapret-list-update.timer + "$SYSTEMCTL" stop zapret-list-update.timer + rm -f "$SYSTEMD_SYSTEM_DIR/zapret-list-update.service" "$SYSTEMD_SYSTEM_DIR/zapret-list-update.timer" + "$SYSTEMCTL" daemon-reload +} + + + +remove_systemd() +{ + INIT_SCRIPT=/etc/init.d/zapret + + service_stop_systemd + service_remove_systemd + timer_remove_systemd + crontab_del +} + + + + + +openwrt_fw_section_find() +{ + # $1 - fw include postfix + # echoes section number + + i=0 + while true + do + path=$(uci -q get firewall.@include[$i].path) + [ -n "$path" ] || break + [ "$path" = "$OPENWRT_FW_INCLUDE$1" ] && { + echo $i + return 0 + } + i=$(($i+1)) + done + return 1 +} +openwrt_fw_section_del() +{ + # $1 - fw include postfix + + local id=$(openwrt_fw_section_find $1) + [ -n "$id" ] && { + uci delete firewall.@include[$id] && uci commit firewall + rm -f "$OPENWRT_FW_INCLUDE$1" + } +} + +remove_openwrt_firewall() +{ + echo \* removing firewall script + + openwrt_fw_section_del + # from old zapret versions. now we use single include + openwrt_fw_section_del 6 + + # free some RAM + "$IPSET_DIR/create_ipset.sh" clear +} + +restart_openwrt_firewall() +{ + echo \* restarting firewall + + fw3 -q restart || { + echo could not restart firewall + exitp 30 + } +} + +remove_openwrt_iface_hook() +{ + echo \* removing ifup hook + + rm -f /etc/hotplug.d/iface/??-zapret +} + + +service_remove_sysv() +{ + echo \* removing zapret service + + [ -x "$INIT_SCRIPT" ] && { + "$INIT_SCRIPT" disable + "$INIT_SCRIPT" stop + } + rm -f "$INIT_SCRIPT" +} + +remove_openwrt() +{ + INIT_SCRIPT=/etc/init.d/zapret + OPENWRT_FW_INCLUDE=/etc/firewall.zapret + + remove_openwrt_firewall + restart_openwrt_firewall + service_remove_sysv + remove_openwrt_iface_hook + crontab_del +} + + +service_remove_macos() +{ + echo \* removing zapret service + + rm -f /Library/LaunchDaemons/zapret.plist + zapret_stop_daemons +} + +remove_macos_firewall() +{ + echo \* removing zapret PF hooks + + pf_anchors_clear + pf_anchors_del + pf_anchor_root_del + pf_anchor_root_reload +} + +remove_macos() +{ + remove_macos_firewall + service_remove_macos + crontab_del +} + + +check_system +require_root + +[ "$SYSTEM" = "macos" ] && . "$EXEDIR/init.d/macos/functions" + +case $SYSTEM in + systemd) + remove_systemd + ;; + openwrt) + remove_openwrt + ;; + macos) + remove_macos + ;; +esac + + +exitp 0