File: docs/manual.en.md
[!NOTE]
This manual is mostly AI translated from russian
Contents
- Contents
- Introduction
- Project structure
- Traffic processing scheme
- Traffic interception from the OS Kernel
- nfqws2
- nfqws2 C interface
- Base constants
- Standard blobs
- Environment variables
- C functions
- Logging
- IP conversion
- Bitwise operations
- Unsigned number operations
- Integer division
- Random Data Generation
- Parsing
- Cryptography
- Compression
- System functions
- Packet handling options
- Dissection and reconstruction
- conntrack
- Obtaining IP addresses
- Receiving and sending Packets
- Working with payloads
- Instance execution management
- Timer control
- zapret-lib.lua base function library
- Base desync functions
- Utility functions
- String operations
- Raw string handling
- TCP sequence number handling
- Position handling
- Dissection
- Working with L3 and L4 protocol elements
- Working with hostnames
- File name and path operations
- Reading and writing Files
- Data compression
- autottl
- Operations with dissects
- IP addresses and interfaces
- Sending
- Standard direction and payload filters
- Working with multi-packet payloads
- Timer helpers
- Orchestration
- zapret-antidpi.lua DPI attack program library
- zapret-auto.lua automation and orchestration library
- Auxiliary programs
- blockcheck2
- Startup scripts
- Other firmwares
- Windows
Introduction
zapret2 is a packet manipulator primarily designed to perform various autonomous real-time attacks on Deep Packet Inspection (DPI) systems. Its main objective is to bypass resource blocks or protocol restrictions. However, zapret2's capabilities are not limited to this; its architecture allows for other types of packet manipulation, such as bidirectional (client-server) protocol obfuscation to hide traffic from DPI, among other applications.
Project structure
The core component of zapret2 is the nfqws2 program (dvtws2 on BSD, winws2 on Windows). Written in C, it serves as the primary packet manipulator. It includes functions for packet interception, basic filtering, recognition of major protocols and payloads, support for host and IP lists, automated hostlists with block detection, a system of multiple profiles (strategies), raw packet transmission, and other utility functions. However, it does not contain the logic for traffic modification itself; this is handled by Lua code called from nfqws2.
Consequently, the Lua code is the next most critical part of the project. The base package includes the zapret-lib.lua helper library, the zapret-antidpi.lua DPI attack library, and the zapret-auto.lua orchestration library for dynamic decision-making. Additionally, it features zapret-tests.lua for testing C functions, zapret-obfs.lua for protocol obfuscation, and zapret-pcap.lua for capturing traffic into .cap files.
The project requires LuaJIT-2.1+ or PUC Lua 5.3+. Older versions are not tested and not supported.
Traffic redirection from the kernel is handled by iptables and nftables in Linux, ipfw in FreeBSD, and pf in OpenBSD. On Windows, this functionality is built directly into the winws2 process via the WinDivert driver. The kernel interception scheme, nfqws2, and the Lua code constitute the project's minimal working core. Everything else is supplementary, secondary, or optional.
Secondary components include Linux startup scripts (init.d, common, ipset, install_easy.sh, uninstall_easy.sh) and blockcheck2, a tool for automated strategy testing. The purpose of the startup scripts is to coordinate the initialization of tables and nfqws2 instances while accounting for the specifics of various distributions (OpenWrt, systemd, OpenRC). They also provide support for synchronized updates of various lists and loading IP lists into kernel-space (ipset). Users can implement these tasks using their own tools if preferred or if the provided startup scripts do not meet their needs. These scripts store all settings in a config file located in the project root; this config is specific to the scripts, and nfqws2 is unaware of it.
Two C-based programs are provided for list processing: mdig, a multi-threaded hostlist resolver capable of handling lists of any size, and ip2net, a tool for aggregating individual IP addresses into subnets to reduce their footprint. These programs are used by the startup scripts and in blockcheck2.
Startup scripts and the installer support installation on any classic Linux distribution with systemd or OpenRC, as well as OpenWrt firmware.
If a system does not meet these requirements, manual integration is possible.
macOS is not supported because it lacks a suitable packet interception and management tool. The standard BSD tool ipdivert was removed from the kernel by the manufacturer.
Traffic processing scheme
Networks operate with IP packets, making them the fundamental unit of processing. The OS kernel's network subsystem handles the receiving and sending of packets.nfqws2 does not run in kernel mode; it is a user-mode process. Therefore, the first stage of processing involves passing packets from the OS kernel to the nfqws2 process. All four interception methods provide some degree of packet filtering. Linux offers the most extensive capabilities.
The more unnecessary traffic is filtered out at this stage, the lower the CPU load, as passing packets between the kernel and user space involves significant overhead.
When a packet arrives at nfqws2, the first step is to parse it according to the OSI model layers - extracting IP, IPv6, TCP, and UDP headers, as well as the data field. This process is called dissection.
The result of this process is a dissect - a representation of the packet as structures where individual fields can be addressed.
Next, the conntrack subsystem built into nfqws2 comes into play. This is a flow-tracking system built on top of individual packets.
It searches for an existing flow record based on the packet's L3/L4 data. If no record exists, one is created. Old records with no recent activity are deleted.conntrack tracks the logical direction of packets within a flow (inbound/outbound), counts the number of packets and bytes passed in both directions, and monitors TCP sequence numbers. It is also used, when necessary, to reassemble messages transmitted across multiple packets.
The payload type - the content of an individual packet or a group of packets - is identified via signatures. Based on the payload type, the protocol type for the entire flow is determined and maintained until the flow terminates. Different types of payloads can pass through a single flow.
For example, the XMPP protocol usually carries several types of XMPP-specific messages as well as TLS-related messages.
The flow protocol remains "xmpp," but subsequent packets are assigned various payload types - both known and unknown. Unknown payloads are identified as "unknown."
If a specific payload and flow protocol type require the reconstruction of a message from multiple packets, nfqws2 begins buffering them in association with the conntrack record and prevents their immediate transmission. Once all packets of the message are received, the composite payload is reconstructed and, if necessary, decrypted.
Further decisions are then made based on the fully assembled payload - reasm or the result of assembly and decryption - decrypt.
Once the necessary information about the payload is obtained, the profile classification system takes over.
Profiles contain a set of filters and action commands.
Profiles are filtered by L3 (IP protocol version, IP protocol number, ipsets - IP address lists), L4 (TCP, UDP ports, ICMP type/code), and L6/L7 (flow protocol type, hostlists).
Profiles are scanned strictly in order from first to last. Upon the first filter match, that profile is selected, and the scanning stops.
If none of the conditions are met, a default profile with no actions is selected.
All subsequent actions are performed within the scope of the selected profile. The chosen profile is cached in the conntrack entry, so a fresh lookup is not required for every packet.
A re-lookup occurs if the source data changes - specifically, upon the detection of an L7 protocol or a hostname. In these instances, a new search is performed, and the profile is switched if necessary. A stream may undergo up to two such switches throughout its lifetime, as there are only two mutable parameters.
Once a profile is selected, what constitutes its action-oriented logic? Actions are handled by Lua functions, and a profile can contain any number of them. Each call to a Lua function within a profile is referred to as an instance. A single function may be called multiple times with different parameters. Thus, the term "instance" describes a specific execution of a function, uniquely identified by the profile number and its sequential position within that profile. Instances are invoked using the --lua-desync parameters. Each instance receives an arbitrary set of arguments defined within --lua-desync. The execution order is critical to the strategy's logic and follows the exact sequence in which the --lua-desync parameters are specified.
in-profile filters are also available. There are three types: the --payload filter (a list of payloads accepted by the instance) and two range filters, --in-range and --out-range, which define the specific byte range within the stream that the instance should process. Once defined, in-profile filters apply to all subsequent instances until they are redefined. The primary purpose of these filters is to minimize relatively slow Lua calls by offloading as much decision-making as possible to the C-side code.
When a packet reaches a Lua instance, the function receives two parameters: ctx and desync. ctx provides a context for interacting with specific C-side functions. The desync parameter is a table containing various attributes of the packet being processed. Most notably, it includes the dissect (the dis subtable) and information from the conntrack entry (the track subtable). Numerous other parameters can be inspected by executing var_debug(desync) or by using the pre-built pktdebug instance.
During the replay of delayed packets, the Lua instance receives details such as the part number, the total number of parts in the original message, the current part's position, and reasm or decrypt data if available.
Lua code can utilize the global variable space to store data that isn't specific to a single packet. It also has access to the desync.track.lua_state table, where it can store any information tied to the conntrack entry; this table remains consistent across every packet in the stream. Conversely, the desync table can be used to generate and store temporary data relevant only to the current packet's processing chain. Subsequent Lua instances receive the same desync table, allowing them to inherit data from previous instances.
A Lua instance can clone the current dissect, modify it, generate its own dissects, and send them via C-side calls. The output of each instance is a verdict: VERDICT_PASS (do nothing with the current dissect), VERDICT_MODIFY (send the modified dissect content at the end of the processing chain), or VERDICT_DROP (drop the current dissect). Verdicts from all instances are aggregated: MODIFY overrides PASS, and DROP overrides both PASS and MODIFY.
A Lua instance can opt out of receiving further packets for a flow in the in/out direction - this is known as instance cutoff.
It can also disconnect the in/out direction of the current flow from all Lua processing - lua cutoff.
An instance can request the cancellation of the entire subsequent chain of Lua instance calls for the current dissect. The instance making this decision takes on the role of coordinating further actions.
Such an instance is called an orchestrator. It receives an execution plan from the C code, which includes all profile filters and call parameters for all remaining instances. It then decides when and under what conditions to invoke them (or not) and whether to modify their parameters. This enables dynamic scenarios without modifying the core strategy code.
Examples include detecting resource blocking and switching strategies if the previous one failed.
If all instances in the current profile have entered a cutoff state for the current flow, or if the current flow position is beyond the upper bound of the range filters, no further Lua calls will be made for this flow. The C code marks such flows with a special "lua cutoff" flag, which is checked as efficiently as possible without invoking Lua code, thereby saving CPU resources.
After the entire chain of profile instances has been executed, the C code receives a final verdict on what to do with the current dissect: send it as is, send a modified version, or drop it.
Finally, nfqws2 returns to waiting for the next packet, and the cycle repeats.
Traffic interception from the OS Kernel
Traffic interception in the Linux kernel
This is achieved using iptables or nftables via the NFQUEUE mechanism.nftables is preferred because it allows working with traffic after NAT, whereas iptables does not. This is critical when processing forwarded traffic. With iptables, post-NAT interception is impossible; therefore, certain techniques that break NAT cannot be implemented on forwarded traffic using iptables.nftables has one significant drawback: excessive memory requirements when loading large sets. For example, loading 100K IP addresses requires 256–320 MB, which often exceeds the capacity of many routers. ipset (used with iptables) can handle this even with 64 MB of RAM.
If you have to choose between iptables and nftables, you should definitely choose nftables. Support for nftables in the startup scripts is more robust, and the technology itself is much more "neighbor-friendly" toward rules from other programs because it uses separate tables. In iptables, everything is mixed together, and one program's rules might break another's. iptables should be considered a legacy option for compatibility when no other choice exists. In a modern Linux distribution, you should definitely avoid iptables. However, if you are using an older Linux version (kernel older than 5.15 or nft older than 1.0.1) and cannot upgrade, iptables is better, as older kernels and nft versions will have issues.
The following test examples are intended for custom startup systems or manual execution.
The zapret startup scripts generate the necessary rules automatically; you do not need to write iptables/nftables rules yourself.
Traffic interception using nftables
A test table for a POSTNAT scheme.
It ensures the interception of the first incoming and outgoing packets of a flow after NAT (if present).
Due to NAT, client IP addresses are lost and replaced by the WAN interface IP.
The number of initial packets intercepted is adjusted according to your strategy. Excessive interception creates additional CPU load.
Intercepting RST and FIN packets is recommended for optimal conntrack performance.
Filtering by mark is necessary to prevent loops. Without this, the system may hang or malfunction.
notrack is required to prevent NAT from interfering with techniques that are incompatible with it.
Packets generated by nfqws2 should not undergo NAT validity checks, as this could cause them to be dropped by standard table rules.
NAT IP substitution is unnecessary because packets reaching nfqws2 have already passed through NAT and possess the correct source address and port for the WAN.
IFACE_WAN=wan
MAX_PKT_IN=15
MAX_PKT_OUT=15
FWMARK=0x40000000
PORTS_TCP=80,443
PORTS_UDP=443
QNUM=200
nft create table inet ztest
nft add chain inet ztest postnat "{type filter hook postrouting priority srcnat+1;}"
nft add rule inet ztest postnat oifname $IFACE_WAN meta mark and $FWMARK == 0 udp dport "{$PORTS_UDP}" ct original packets 1-$MAX_PKT_OUT queue num $QNUM bypass
nft add rule inet ztest postnat oifname $IFACE_WAN meta mark and $FWMARK == 0 tcp dport "{$PORTS_TCP}" ct original packets 1-$MAX_PKT_OUT queue num $QNUM bypass
nft add rule inet ztest postnat oifname $IFACE_WAN meta mark and $FWMARK == 0 tcp dport "{$PORTS_TCP}" tcp flags fin,rst queue num $QNUM bypass
nft add chain inet ztest pre "{type filter hook prerouting priority filter;}"
nft add rule inet ztest pre iifname $IFACE_WAN udp sport "{$PORTS_UDP}" ct reply packets 1-$MAX_PKT_IN queue num $QNUM bypass
nft add rule inet ztest pre iifname $IFACE_WAN tcp sport "{$PORTS_TCP}" ct reply packets 1-$MAX_PKT_IN queue num $QNUM bypass
nft add rule inet ztest pre iifname $IFACE_WAN tcp sport "{$PORTS_TCP}" "tcp flags & (syn | ack) == (syn | ack)" queue num $QNUM bypass
nft add rule inet ztest pre iifname $IFACE_WAN tcp sport "{$PORTS_TCP}" tcp flags fin,rst queue num $QNUM bypass
nft add chain inet ztest predefrag "{type filter hook output priority -401;}"
nft add rule inet ztest predefrag "mark & $FWMARK != 0x00000000 notrack"Deleting the test table:
nft delete table inet ztestTraffic interception using iptables
[!CAUTION]
Starting with Linux kernel 6.17, there is a kernel configuration parameterCONFIG_NETFILTER_XTABLES_LEGACY, which may be "not set" by default in some distributions. Disabling this setting turns offiptables-legacy. This is part of the ongoing iptables deprecation process. However,iptables-nftwill continue to function as it utilizes thenftablesbackend.
Test rules for the PRENAT scheme.
These ensure the interception of the initial incoming and outgoing packets of a stream before NAT is applied, if NAT is present.
Internal network source addresses and ports are preserved. Attacks on passing traffic that break NAT are impossible, though they remain possible from the system itself.
/* Detailed source-code truncated for AI context efficiency. */Removing zapret test rules:
for tables in iptables ip6tables; do
$tables -t mangle -D POSTROUTING -j ztest_post
$tables -t mangle -D PREROUTING -j ztest_pre
$tables -t mangle -F ztest_post
$tables -t mangle -X ztest_post
$tables -t mangle -F ztest_pre
$tables -t mangle -X ztest_pre
doneFlushing all rules from the mangle table, including any other existing rules:
iptables -F -t mangle
ip6tables -F -t mangleTraffic interception in the FreeBSD kernel
The primary challenge when intercepting traffic on non-Linux systems is the inability to intercept only the initial packets of a stream. You can only intercept the entire stream in a specific direction.
FreeBSD is particularly limited in this regard, as it lacks raw payload filtering capabilities (filtering based on packet content).
Consequently, the first set of rules intercepts all outgoing traffic on specific ports, but only SYN+ACK, FIN, and RST packets for incoming TCP traffic. This allows the use of autottl mode and ensures conntrack functions correctly without excessive CPU load.
However, this configuration will break any functionality that requires other types of incoming traffic.
RULE=100
IFACE_WAN=vmx0
PORTS_TCP=80,443
PORTS_UDP=443
PORT_DIVERT=989
ipfw delete $RULE
ipfw add $RULE divert $PORT_DIVERT tcp from any to any $PORTS_TCP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT udp from any to any $PORTS_UDP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any tcpflags syn,ack in not diverted recv $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any tcpflags fin in not diverted recv $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any tcpflags rst in not diverted recv $IFACE_WANAlternative: Bidirectional stream interception. This puts a significant load on the CPU.
Every gigabyte downloaded will pass through dvtws2. Usually, only 1 or 2 packets are actually needed; everything else is a waste of CPU cycles, but ipfw offers no other alternatives.
RULE=100
IFACE_WAN=vmx0
PORTS_TCP=80,443
PORTS_UDP=443
PORT_DIVERT=989
ipfw delete $RULE
ipfw add $RULE divert $PORT_DIVERT tcp from any to any $PORTS_TCP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT udp from any to any $PORTS_UDP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any in not diverted recv $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT udp from any $PORTS_UDP to any in not diverted recv $IFACE_WANWhile traffic interception via pf divert-to is theoretically possible, the loop prevention mechanism is broken in practice, making pf unusable for this purpose.
On pfSense and OPNsense, additional steps are required to enable both pf and ipfw simultaneously. This frequently leads to issues, conflicts, and glitches.
Traffic interception in the OpenBSD kernel
OpenBSD relies solely on pf. Its divert loop prevention mechanism works correctly.
The first approach involves intercepting all outbound traffic by port, while intercepting only SYN+ACK, FIN, and RST for inbound TCP. This allows for the use of autottl mode and ensures conntrack functions correctly without putting heavy load on the CPU. However, this method will break anything that requires other types of incoming traffic.
pf requires configuration files. You write a pf file (typically /etc/pf.conf) and then apply it using pfctl -f /etc/pf.conf. Use pfctl -e to enable pf and pfctl -d to disable it.
You can also use anchors with separate rule files; refer to the pf documentation for details.
The no state tricks are necessary to prevent the automatic interception of incoming packets within an established flow.
IFACE_WAN = "em0"
PORTS_TCP = "80,443"
PORTS_UDP = "443"
PORT_DIVERT = "989"
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } flags SA/SA divert-packet port $PORT_DIVERT no state
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } flags R/R divert-packet port $PORT_DIVERT no state
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } flags F/F divert-packet port $PORT_DIVERT no state
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } no state
pass out quick on $IFACE_WAN proto tcp to port { $PORTS_TCP } divert-packet port $PORT_DIVERT no state
pass out quick on $IFACE_WAN proto udp to port { $PORTS_UDP } divert-packet port $PORT_DIVERT no stateThe second approach involves intercepting the flow in both directions. This is particularly CPU-intensive.
Every gigabyte downloaded will pass through dvtws2. Usually, only 1 or 2 packets are actually needed, while the rest simply wastes CPU cycles; however, pf does not provide any other way to handle this.
Inbound interception for the same ports is handled automatically via state tracking.
IFACE_WAN = "em0"
PORTS_TCP = "80,443"
PORTS_UDP = "443"
PORT_DIVERT = "989"
pass out quick on $IFACE_WAN proto tcp to port { $PORTS_TCP } divert-packet port $PORT_DIVERT
pass out quick on $IFACE_WAN proto udp to port { $PORTS_UDP } divert-packet port $PORT_DIVERT[!CAUTION]
FreeBSD uses a different version ofpfwith slightly different syntax. Furthermore,pfin FreeBSD is effectively broken for this use case because loop prevention does not work. While macOS also usespf,ipdiverthas been removed from its kernel, so these rules will not function.
Traffic interception in the Windows kernel
Windows lacks native tools for traffic interception. Instead, a third-party solution is used: the WinDivert driver.
Control is integrated directly into the winws2 process.
WinDivert accepts text-based filters similar to those used by Wireshark and tcpdump.
These filters support filtering by IP (without ipset), ports, and raw payloads. However, they do not support bitwise logical operations, bit shifts, flow tracking, or limiting interception to the first few packets of a connection.
The WinDivert driver is no longer under active development. However, signed versions of the driver are available that are compatible with all modern Windows releases, though only for the x86_64 architecture. For ARM64, there is an unsigned driver that requires enabling Test Signing mode.
When using winws2 on Windows 11 ARM64, you must use the x86_64 version because winws2 is built for Cygwin, which does not support ARM. In this configuration, the .sys driver must be replaced with the unsigned ARM64 version. Running on Windows 10 ARM64 is theoretically possible, but only with the 32-bit winws2 x86 version, as Windows 10 does not support x64 emulation.
WinDivert is a frequent target for antivirus software. While it is a "hacker tool," it is not a virus. It is best understood as a Windows equivalent to iptables. Conflicts with third-party kernel-mode software - primarily antiviruses and firewalls - can occur, occasionally resulting in Blue Screens of Death (BSOD). Fixing these issues is practically impossible, largely due to driver signing requirements; obtaining a signature is prohibitively difficult and expensive for individuals without corporate backing.
WinDivert cannot reliably intercept forwarded traffic when using Windows' built-in network sharing or NAT. Consequently, features for handling forwarded traffic are not implemented. The only available workaround is to set up a proxy server.
winws2 can accept full raw filters-you can write the filter yourself and specify it using the --wf-raw=<filter> or --wf-raw=@<filter_file> parameters. Since this is often cumbersome, a built-in filter constructor is provided.
--wf-tcp-out, --wf-tcp-in, --wf-udp-out, and --wf-udp-in take a list of ports (80,443) or port ranges (80,443,500-1000) and enable full port interception for the specified direction.
--wf-icmp-out, --wf-icmp-in take a list of icmp types or icmp types+codes.
--wf-ipp-in, --wf-ipp-out take a list of raw ip protocol numbers (extracting end protocol from ipv6 extension headers is not supported).
--wf-raw-part accepts partial WinDivert filters. The syntax is identical to --wf-raw. Multiple --wf-raw-part instances can be used. These partial filters are integrated by the constructor into the final filter using OR logic: [specified ports] OR [filter1] OR [filter2].
--wf-raw-filter - partial WinDivert filter combined with resulting full WinDivert filter by AND. Only one is allowed.
Typical use - filtering by small list of IP addresses. For example, if your task is icmp tunnel to the server, intercepting all icmp type is a waste of CPU.
Much better to add --wf-raw-filter="ip.SrcAddr==1.2.3.4" to --wf-icmp-in=0.
--wf-save=<filter_file> writes the filter created by the constructor to a file for subsequent analysis and modification.
The filter constructor automatically intercepts incoming TCP packets with SYN+ACK, FIN, and RST flags. There is no need to write specific rules for these.
--wf-filter-lan (enabled by default) filters out packets destined for non-global IP addresses, such as 192.168.0.0/16.
--wf-tcp-empty (disabled by default) enables the interception of empty TCP packets that do not carry SYN, FIN, or RST flags. If this parameter is disabled, empty ACK packets are not intercepted, which significantly reduces CPU overhead. However, certain strategies may require these packets. Only you can determine if they are necessary for your use case.
If any TCP port interception is active, HTTP redirect interception is automatically enabled to allow autohostlist to function. HTTP redirect interception works via payload signatures, meaning it inspects bytes at specific positions within the packet content.
For UDP protocols-especially those where the port is not fixed-it is preferable to use custom --wf-raw-part filters to save on CPU cycles. Filters can also be used for TCP, but you must account for the requirements of conntrack. It requires at least the SYN packet, and ideally FIN and RST as well. If you need to filter based on messages that span more than one TCP segment, this cannot be achieved using WinDivert filters alone; full port interception for that direction is required.
nfqws2
General principles for setting parameters
All nfqws2 parameters are passed via the command line or loaded from a file using the same format.nfqws2 utilizes the standard getopt_long_only parser.
Options follow the --name[=value] format. Some options do not require parameters, others do, and some take them optionally.
The getopt parser allows specifying values using either an = sign or a space. However, extraneous values passed via spaces may be ignored, meaning seemingly erroneous parameters might not trigger an error. It is best practice to always use the = sign for values.
Reading parameters from a file is implemented via the single @config_file option.
If this is used, all other command-line parameters will be ignored.
Options will be read from the file as if its contents were entered directly into the command line.
This feature is not supported in the Android and OpenBSD versions.
Full list of options
General parameters for all versions - nfqws2, dvtws2, winws2.
/* Detailed source-code truncated for AI context efficiency. */Specific parameters for nfqws2:
--qnum=<nfqueue_number> ; NFQUEUE number in Linux
--user= ; change uid/gid to those associated with the specified username
--uid=uid[:gid1,gid2,...] ; change uid/gid to the specified numeric values
--bind-fix4 ; fix for the issue where generated packets on Linux exit from the wrong interface when using PBR (IPv4)
--bind-fix6 ; same as above for IPv6
--fwmark= ; mark bit to prevent looping. default = 0x40000000
--filter-ssid=ssid1[,ssid2,ssid3,...] ; profile filter: WiFi network name (SSID)
--filter-ssid-neg=[0|1] ; profile filter: negate SSID filterSpecific parameters for dvtws2:
--port= ; divert port number
--user= ; change uid/gid to those associated with the specified username
--uid=uid[:gid1,gid2,...] ; change uid/gid to the specified numeric valuesSpecific parameters for winws2:
/* Detailed source-code truncated for AI context efficiency. */Protocol detection
nfqws2 signatures the payload types of individual packets or packet groups.
All packets without data have payload type "empty", undetermined payloads - type "unknown".
The flow protocol is assigned after receiving the first known payload and remains with the flow for the rest of its existence.
In this case, subsequent payloads can have both a known type and an unknown one.
In payload and flow protocol filters special values are available - "all" and "known". "all" means any, "known" - not "empty" and not "unknown".
Table of recognizable payload types and flow protocols
| flow protocol | L4 | payloads |
|---|---|---|
| http | tcp | http_reqhttp_reply |
| tls | tcp | tls_client_hellotls_server_hello |
| xmpp | tcp | xmpp_streamxmpp_starttlsxmpp_proceedxmpp_features |
| mtproto | tcp | mtproto_initial |
| bt | tcp | bt_handshake |
| quic | udp | quic_initial |
| wireguard | udp | wireguard_initiationwireguard_responsewireguard_cookiewireguard_keepalive |
| dht | udp | dht |
| utp_bt | udp | utp_bt_handshake |
| discord | udp | discord_ip_discovery |
| stun | udp | stun |
| dns | udp | dns_query dns_response |
| dtls | udp | dtls_client_hellodtls_server_hello |
| icmp | ipv4ipv6icmp |
Special payload types are "ipv4", "ipv6", "icmp". "ipv4" and "ipv6" are generated for icmp with attached IP packet.
Other icmp have "icmp" payload type.
Using multiple profiles
Profiles exist to select a specific traffic manipulation strategy based on the specified filter conditions.
The general scheme for using profiles is as follows:
nfqws2 <global_parameters>
<filter 1> <strategy 1> --new
<filter 2> <strategy 2> --new
...............
<filter N> <strategy N>When a packet arrives and there is no existing conntrack entry for it, a profile is selected.
Profile filters are checked sequentially - from start to finish, left to right - and in no other way.
Only one profile ever wins: the first one to match the filter conditions. All others are ignored.
If no filter matches, an empty profile (ID 0) is selected, which performs no actions on the traffic.
All conditions, except for --filter-l7 and hostlists, are definitive and known from the moment flow processing begins (at the start of the connection).
Initially, the flow protocol and the hostname (extracted from the flow's messages) are usually unknown.
Once these values are identified, the profile search is performed again. If a different profile is selected, a "jump" occurs.
Up to two such jumps can occur, as there are only two variables affecting selection that are unknown at the start.
For TLS, HTTP, and QUIC protocols, there is typically only one jump because the protocol and hostname are determined within a single packet or group of packets. For XMPP, there are two jumps: first, XMPP itself is identified; then, the transition to TLS is detected, and only then is the hostname extracted.
When writing strategies, they should be designed with this jump logic in mind.
If a strategy needs to start from the very first packet and continue working after a profile change, you must duplicate the calls across all profiles the flow might pass through.
Four filter groups - tcp, udp, icmp, ipp are OR combined. If there're no filters in these groups - everything is allowed.
If any filters are defined - all undefined groups are blocked.
ipp filter does not work with tcp, udp and icmp. They are checked by their specific filters. For example, --filter-ipp=6 does not work. What is meant could be achieved with --filter-tcp=*.
icmp filter matches both icmp and icmpv6 - they are processed the same they.
However, icmp types differ between icmp and icmpv6.
Profile templates
When there are many complex and repetitive strategies or groups of the same parameters, it may be convenient to use templates.
A template is also a profile but it doesn't go to the working profile list but to a separate list of templates.
The profile becomes a template by setting the --template = <name> parameter.
It can then be imported (--import = <name>) into another profile or template.
Simple parameters - number, string, bool - are imported only if they were specified in the imported template. When you import a template into a template in the destination template, they are also considered specified.
List parameters are added to the end of the corresponding list. Such parameters are anything that can take a list of values. For example, hostlists or --filter-tcp.
The template number and name are not copied.
The --import directives can be anywhere. Previous simple specified parameters are overwritten by new imported or specified in the current profile parameters.
A special prohibition is the replacement of autohostlist. The template may have an autohostlist, but importing somewhere that already has an autohostlist will fail.
nfqws2 <global_parameters>
--template=tpl1 --new
--template=tpl2 --new
--template --import tpl2 --name tpl3 --new
--import tpl1 --name prof1 <additional_parameters_1> --new
--import tpl3 --name prof2 <additional_parameters_2> --new
--name prof3In this example, there are 3 active profiles and 3 templates, one of which imports settings from another.
- Profile prof1 receives a combination of `` and
<additional_parameters_1>. - Profile prof2 receives a combination of
,, and<additional_parameters_2>. - Profile prof3 receives ``. It does not import any templates.
Any parameters applicable to profiles, including filters, are allowed within templates. Global parameters are not a part of templates or profiles.
Filtering by ipsets
- For TCP and UDP the server address is matched in client mode and the client address in server mode.
- Related ICMP use the cached profile from the original packet.
- Unrelated ICMP and и raw IP are matched by either source or destination ip. To match an ipset either IP must match.
Filtering by lists
If hostlist filters are used - meaning there is at least one domain in any hostlist or an autohostlist is specified - the profile will never be selected if the hostname is missing.
The case where there is no autohostlist and all list files are empty is treated as if no hostlist filter exists.
If there is no autohostlist but there are entries in standard hostlists, the profile is selected only if the current host matches any of the include lists and does not match any of the exclude lists.
If an autohostlist is present, the profile is always selected as long as a hostname is available, regardless of whether it appears in any of that profile's lists. The specific actions then depend on whether the host is found in the lists.
- If a host is in the exclusion lists, no action is taken, and no attempt is made to determine if the resource is reachable.
- If a host is not in the exclusion lists but is in the inclusion lists, the strategy is applied without attempting to verify resource reachability.
- If a host is in neither the exclusion nor the inclusion lists, no strategy is applied, and access failure detection begins. If a failure occurs, the failure counter increments. If a success occurs or the time interval between failures exceeds
--hostlist-auto-fail-time, the counter resets.
When the counter reaches--hostlist-auto-fail-threshold, the host is added to the auto-list.
On the next request, the host will be treated as if it were in the inclusion list. - Hostlist and ipset files are reloaded automatically when modified - restarting
nfqws2is not required. - The SIGHUP signal marks all lists for a forced reload during the processing of the next packet.
- Each entry for a domain, IP address, or subnet must be on a new line.
- Hostlists and ipsets support comments. Empty lines and lines starting with
#are ignored. - In hostlists, subdomains are handled automatically. Wildcards (
*) are not supported. If a domain begins with the^symbol, automatic subdomain matching is disabled for that specific entry. - ipsets can include both IPv4 and IPv6 addresses and subnets.
- In the static versions of
--ipset-ipand--hostlist-domains, domains are separated by commas. The#and^symbols are also supported in static hostlists. - Gzip compression is supported for all list files.
Autohostlist failure detector
The detector only triggers when a hostname is present. A failure is defined as:
- tcp: At least
--hostlist-auto-retrans-thresholdretransmissions occur within the outgoing relative sequence--hostlist-auto-retrans-maxseq. If--hostlist-auto-retrans-reset=1, an RST is sent to the retransmitter to stop its attempts (which could otherwise last a very long time). - tcp: An RST is received within an incoming relative sequence of 1 to
--hostlist-auto-incoming-maxseq. - tcp: An
http_replypayload is received, and the HTTP response is a 302 or 307 redirect to an absolute URL with a second-level domain that does not match the host's second-level domain. - udp: At least
--hostlist-auto-udp-outpackets are sent, but no more than--hostlist-auto-udp-inpackets are received. This situation indicates that the client is sending requests, but the server is either not responding or responding less than the protocol requires.
A success is defined as:
- tcp: The outgoing relative sequence exceeds
--hostlist-auto-retrans-maxseq. The client managed to send enough data that DPI interference is unlikely. - tcp: The incoming relative sequence exceeds
--hostlist-auto-incoming-maxseq. The server sent enough data that it does not resemble a DPI-generated response. - udp: The number of received packets exceeds
--hostlist-auto-udp-in. The server has responded sufficiently.
In the event of a failure, if no more than --hostlist-auto-fail-time seconds have passed since the previous failure, the failure counter increments.
If more time has passed, the counter resets and starts over.
In the event of a success, the counter is reset. The resource is considered functional, and the previous failure is treated as temporary and unrelated to blocking.
When the counter reaches --hostlist-auto-fail-threshold, the host is added to the list.
Most success and failure criteria require analyzing both incoming and outgoing traffic; therefore, sufficient traffic interception is necessary for these criteria to trigger.
Network presence filter
If, for example, you need to apply one strategy for Wi-Fi and another for Ethernet, this is done via a filter based on the interface name.
But what if you connect to different Wi-Fi networks or plug your Ethernet cable into different locations?
For Ethernet, a solution exists only on Windows; there is none for other systems. For Wi-Fi, solutions are available for Linux and Windows, but not for BSD.
The Wi-Fi filter accepts a comma-separated list of SSIDs; however, its implementation differs between Linux and Windows.
In Linux, the --filter-ssid profile filter is used. When specified, nfqws2 attempts to resolve the SSID on the interface where the packet is being sent or received. If successful, it checks the SSID against the list; if not, the filter condition is not met, and the profile is not selected. This approach allows the software to function correctly even if you are connected to multiple Wi-Fi networks via different adapters.
The approach in Windows is different. It monitors the presence of the specified Wi-Fi networks across all Wi-Fi adapters. If any of them match an SSID in the list, WinDivert interception is enabled; otherwise, it is disabled. To handle different Wi-Fi networks with different strategies, you must run multiple instances of winws2. One instance will activate while others deactivate. The SSID list is specified using the --ssid-filter parameter.
Another way to address this-and not just for Wi-Fi-is by using the Network List Manager (NLM) filter.--nlm-list[=all] returns a list of GUIDs for connected networks (or all networks if the "all" value is specified). You then enter the comma-separated list of GUIDs into --nlm-filter.
An NLM network is the result of the system detecting a connection to a specific network. You might connect to a router via Wi-Fi or Ethernet, but it will be recognized as the same network. To distinguish between networks, the system typically looks at the gateway's MAC address. NLM technology is interesting and useful, but unfortunately, adequate management tools were only available in Windows 7. In newer systems, you have to dig into PowerShell or the Registry to manually assign connections to the correct GUIDs if the system categorizes them incorrectly. Alternatively, you can simply use the list of GUIDs automatically assigned by the system.
SSID/NLM filters can be negated in both systems.
Server mode
Certain types of manipulations can be performed not only from the client side but also from the server side. nfqws2 was designed to fully support both inbound and outbound traffic, on both the client and server sides.
The concept of "direction" in a network is largely relative. For the endpoints of a packet, everything is clear: something is sent, something is received. But for a router, it is not clear at all. There is only an incoming interface and an outgoing interface. Essentially, the two directions are equivalent if we only consider the L3 level - the level of individual IP packets.
Therefore, direction in nfqws2 is handled by tracking flows. A flow is either a TCP connection or a sequence of UDP packets. Since UDP does not have the concept of a connection, the general term "flow" is used.
A flow is characterized by a 4-tuple: ip1:port1-ip2:port2. This set of values determines which flow a packet belongs to.
In nfqws2, flows are tracked by conntrack. The party that sends the first SYN (TCP) or the first UDP packet is considered the client, and the opposite end is the server. If a flow entry in conntrack was created by a SYN,ACK packet (TCP), that end is considered the server and the opposite end the client. In this way, conntrack determines the roles in establishing the connection and maintains a separate set of counters for each role-how many packets have passed, how many data packets, how many bytes transferred, etc.
In client mode, the "outgoing" direction is considered the direction from the client; in server mode (--server), it is the direction from the server. The "inbound" direction is the opposite.
When --server is specified, the directions are inverted. --in-range, --out-range, as well as the desync.outgoing flag in Lua functions, are swapped to correspond to the data actually being sent or received from the server's perspective. The client sends requests (http_req) and receives replies (http_reply). The server sends replies (http_reply) and receives requests (http_req).
In both client and server modes, ipset lookups are performed using the destination IP address for the outgoing direction and the source IP address for the inbound direction.
In client mode, port filters check the destination port for the outgoing direction and the source port for the inbound direction.
In server mode, port filters check the destination port for inbound traffic and the source port for outbound traffic.
This is because, in the context of filtering, only the server port is truly meaningful. Client ports are typically selected randomly from a specific range and are unsuitable for effective filtering.
Consequently, the server filters client IPs via ipsets, while the client filters server IPs. However, both the server and the client filter based on the server port.
Linux conntrack uses a similar method for determining direction. For a client, outbound packets are marked as original and inbound as reply. For a server, it is the opposite. This must be taken into account when writing interception rules that rely on conntrack direction.
Direction can also be determined by interface names. In a standard LAN-WAN configuration, packets received from the WAN are considered inbound, while those sent via the WAN are considered outbound. Using the WAN interface is crucial because NAT is typically involved; for nfqws2, it is essential to intercept outbound packets after NAT and inbound packets before NAT.
If the router operates without NAT (typical for IPv6), the stage at which packets are intercepted does not matter. All IP addresses are equivalent. You can connect to the internet, and the internet can connect to you; you are an integral part of it with direct IP addressing. In practice, however, inbound connections are unlikely, as you will likely protect yourself with a firewall. Therefore, direction can still be clearly defined by the interface. Nevertheless, if you do host an internal server, you can run a separate nfqws2 instance in server mode for it, while using standard mode for client traffic.
In BSD, ipfw or pf rules are typically written this way - "xmit wan", "recv wan" - supplemented by filters for destination ports on xmit and source ports on recv (or vice versa for server mode). This reliably identifies the traffic intended for interception based on its direction.
IP cache
The ipcache is an in-memory data structure that allows the process to store information using an IP address and interface name as a key. This data can later be retrieved and used to fill in missing information. Currently, it is used in the following scenarios:
IP, interface => incoming ttl. The TTL/HL of the first inbound packet is cached. This allows Lua functions (
autottl) to use it immediately starting from the very first packet, even before a response has been received.IP => hostname. The hostname is cached (independent of the interface) to facilitate profile lookups via hostlists and to provide the name to Lua functions when the hostname is not yet known. This mode is disabled by default and can be enabled using the
ipcache-hostnameparameter. This technique is experimental. Its main drawback is the lack of a one-to-one correspondence between domains and IP addresses; multiple domains can point to the same IP. In the event of a collision, the hostname is overwritten by the most recent entry. Furthermore, a domain may cycle through different IPs on a CDN-one address now, another an hour later. This issue is addressed via the cache entry lifetime:--ipcache-lifetime(defaulting to 2 hours). Nevertheless, you may find that in your specific case, the benefits of this technique outweigh the drawbacks. Be prepared for behavior that may seem confusing at first; such issues can be investigated using the--debuglog.
Hostnames for caching are retrieved from L7 protocol analysis and DNS responses (excluding DoH).
DNS extraction works only via the UDP protocol and requires redirecting packets from port 53. Only DNS responses with source port 53 are processed.
On Windows, this is configured using --wf-udp-in=53. Since DNS queries will likely be directed to the local network, the --wf-filter-lan=0 parameter is required.
If a DNS proxy is running on Windows and DNS is configured to use localhost, --wf-filter-loopback=0 will also be necessary.
On a router, if encrypted DNS is used, you must capture requests from the client before they are encrypted on the LAN interface.
If encryption is not used, they can be captured on the WAN interface as well.
For the LAN interface, these will be outgoing; for the WAN interface, they will be incoming, following the conntrack "reply" direction.
When using startup scripts in Linux, there is a custom script called 80-dns-intercept designed to intercept DNS responses. Simply copy it into the "custom.d" directory.
Signals
- SIGHUP: Forcefully reloads all host list files and ipsets.
- SIGUSR1: Outputs the current contents of the conntrack pool.
- SIGUSR2: Outputs autohostlist counters and the contents of the ipcache pool.
Debugging
The --debug parameter enables the output of debugging messages.
--debug=0: Disables output.--debug,--debug=1: Output to the console.--debug=@<filename>: Output to a file. There is no limit on file size, but the file may be deleted at any time, and recording will resume from a clean slate.--debug=syslog: Output to syslog. Reading methods depend on the syslog daemon.rsyslogwrites files to/var/log, whilebusybox logdis read vialogread.--debug=android: Output to the Android log. Read vialogcat. This is only available in versions built with the Android NDK.
The ability to use the --debug log is essential for troubleshooting settings and is absolutely necessary for writing your own Lua code.
All error messages (DLOG_ERR) and critical status messages (DLOG_CONDUP) are mirrored to the console regardless of the chosen log target.
Error messages are sent to stderr.
The --dry-run parameter allows you to test the validity of command-line options and the accessibility of used files under dropped privileges.--dry-run does not initialize the Lua engine and therefore cannot detect Lua syntax errors.
Virtual machines
Most desktop hypervisors break bypass techniques when the network is connected via the hypervisor's built-in NAT. This is the default setting. A bridged connection is required. This issue is confirmed to exist in both VMware and VirtualBox.
Sandbox
For security purposes, nfqws2 drops its privileges after initialization.
All Lua code is executed only after privileges have been dropped; it never receives the original permissions.
BSD:
- Changes UID/GID to those specified in the
--userand--uidparameters. Defaults to0x7FFFFFFF.
Linux:
- Changes UID/GID to those specified in the
--userand--uidparameters. Defaults to0x7FFFFFFF. - Drops capabilities to
cap_net_rawandcap_net_admin(required for NFQUEUE). The bounding set is cleared to zero. - Sets the
NO_NEW_PRIVSflag to prevent SUID bits and file capabilities from functioning. If the kernel is older than 3.5 andNO_NEW_PRIVSis not supported, a warning is displayed, but execution continues. - Enables a seccomp filter that prohibits
execand several file operations - reading directory contents, creating/deleting directories, creating special files (links, devices),chmod,chown, sending signals (kill), andptrace.
In the event of a violation, the process is terminated. If the kernel does not support seccomp, a warning is displayed, but execution continues.
Windows:
- While the
WinDivertdriver requires administrator privileges, thewinws2process sets its own integrity level to Low Mandatory Level after initialization. This prevents write access to almost all files and objects protected by a security descriptor. The process can no longer manage services or perform privileged actions. However, the Administrators group remains in the process token, so nothing prevents reading most files if they are accessible to Administrators. Lua lacks built-in tools for reading directory contents, making it difficult for an attacker to discover files of interest. - All
Se*privileges are irrevocably removed from the token, except forSeChangeNotifyPrivilege. - A Job object is used to prohibit the creation of child processes and restrict desktop interaction - clipboard access, changing desktop settings, changing display settings, etc.
There is a simple way to pass a writable directory to the Lua code using the --writable[=<dirname>] parameter. nfqws2 creates the directory and assigns permissions so that the Lua code can write files there, then passes the directory name in the WRITABLE environment variable. If dirname is not specified, a directory is created within %USERPROFILE%/AppData/LocalLow on Windows.
On the Lua side, dangerous functions are removed: os.execute, io.popen, package.loadlib, and the debug module. On GitHub, nfqws2 executables are built with a version of LuaJIT that excludes FFI.
Calling Lua code
Lua code is invoked in 3 stages:
- Once at program startup via
--lua-init=code|@file. If the parameter value starts with@, a file is executed; otherwise, the value is treated as Lua code. Gzip file compression is supported. The program first checks for "file", then "file.gz". - During profile processing via
--lua-desync=function_name:arg1[=val1]:arg2[=val2]:argN[=valN].
This starts with the function name, followed by arguments and their values separated by colons. All values are strings. If a value is not specified, it defaults to an empty string.
Two types of automatic substitutions are implemented on the C side:%varsubstitutes the value of the variabledesync.varorvarif the former is missing.#varsubstitutes the length of the variabledesync.varorvarif the former is missing.
Colons and the%,#symbols at the beginning can be escaped with a backslash\. - During timer processing.
Both --lua-init and --lua-desync can be used multiple times. Execution follows the exact order in which they are specified.
Passing blobs
A blob is a binary data block of any size that can be loaded into a Lua variable by the C code at program startup.
--blob=:[+ofs]@<filename>|0xHEX
item_name– the name of the Lua variable.[+ofs]@<filename>– loading from a file starting at offsetofs.0xHEX– loading from a HEX string.
Direct file operations from Lua code are not recommended unless absolutely necessary. Lua code runs with restricted privileges; intended operations might fail or behave inconsistently across different operating systems and environments. Blob loading occurs before entering the sandbox, providing a higher chance of success.
In-profile filters
These come in three types: --payload, --in-range, and --out-range. Filter values remain active from the moment they are specified until the next override.
--payload=type1[,type2][,type3]...accepts a comma-separated list of known payloads, "all", or "known". The default is--payload=all.--(in-range|out-range)=[(n|a|d|s|p)](-|<)[(n|a|d|s|p)]sets conntrack counter ranges for inbound and outbound directions. The default is--in-range=x,--out-range=a.
Ranges are specified in the following formats: mX-mY, mX<mY, -mY, <mY, mX-, where m is the counter mode, X is the lower bound, and Y is the upper bound. Modes x and a are specified as a single letter without a range or counter value. The - sign indicates an inclusive upper bound, while < indicates an exclusive one.
The following counter modes are available:
- 'a' - always
- 'x' - never
- 'n' - packet number
- 'd' - data packet number
- 'b' - byte position of transmitted data
- 's' - tcp: relative sequence of the start of the current packet (works within a 2 GB range)
- 'p' - tcp: relative sequence of the upper bound of the current packet (s + payload length; works within a 2 GB range)
[!CAUTION]
In winws2, the--wf-tcp-empty=0parameter is enabled by default. This blocks the interception of empty ACK packets, which can reduce CPU usage by approximately 50% during intensive downloads. Empty ACKs are unnecessary for most strategies. However, this also breaks the "n" counter-it will not reflect the actual number of packets in the connection. If you require an accurate count, specify the--wf-tcp-empty=1parameter. On other systems, counter accuracy depends directly on the capture filters. A counter cannot and will not track what is not intercepted.
nfqws2 monitors the upper bounds of counters for all Lua instances.
If the upper bound for a direction is exceeded in all instances, or if the instances voluntarily enter a "cutoff" state for that direction, a "lua cutoff" occurs disabling Lua processing for the current thread. This is designed to conserve CPU resources, as checking a single boolean flag requires virtually no processing power.
Typical instance invocation scheme within a profile
The following example demonstrates a setup where we attempt to use specific "fakes" for tls_client_hello and http_req payloads. If these fail, the system switches to multidisorder for TLS and multisplit for HTTP. If those also fail, it cycles through the strategies.
--filter-tcp=80,443 --filter-l7=http,tls
--out-range=-s34228
--in-range=-s5556 --lua-desync=circular
--in-range=x
--payload=tls_client_hello
--lua-desync=fake:blob=fake_default_tls:badsum:strategy=1
--lua-desync=multidisorder:strategy=2
--payload=http_req
--lua-desync=fake:blob=fake_default_http:badsum:strategy=1
--lua-desync=multisplit:strategy=2The specific mechanics of these functions are less important here; the focus is on understanding how in-profile filters operate and how parameters are passed to Lua instances.
- The profile filter for TCP ports and protocol types prevents Lua from being called for irrelevant traffic. The profile will not be engaged at all if the filter conditions are not met.
--out-rangeis specified to cut off the outbound flow from Lua after a relative sequence of (32768 + 1460) to save CPU. This value is chosen due to the specifics of thecircularfunction: thes32768value is used as the default trigger threshold in the success detector, and1460is the maximum possible data length in a TCP packet. This may be unnecessary on Linux if theconnbytesfilter is used.circularrequires the initial incoming packets of a stream to function, but these are disabled by default. Therefore, we enable them up to the relative sequence position5556. By default, the success detector reacts ats4096. We add one additional maximum-sized packet of 1460 bytes for safety.- The remaining instances do not require incoming traffic, so we disable it again. The effect of
--in-range=xextends to the end of the profile. - The
--payloaddirective applies to the two instances following it. - The line
--lua-desync=fake:blob=fake_default_tls:badsum:strategy=1calls thefakefunction with three arguments:blob,badsum, andstrategy. The value for thebadsumargument is an empty string.
Lua desync function prototype
A standard Lua function uses the following prototype:
function desync_f(ctx, desync)
ctx: Context for calling certain C functions.desync: A table containing all passed values, including arguments, the dissection of the current packet, etc.
The function returns a verdict for the current packet: VERDICT_PASS, VERDICT_MODIFY, or VERDICT_DROP. If nothing is returned, the result defaults to VERDICT_PASS.
VERDICT_PASS: Passes the packet as-is, ignoring any changes to the dissection.VERDICT_MODIFY: Reconstructs and sends the current dissection.VERDICT_DROP: Drops the current packet.VERDICT_PRESERVE_NEXT- special bit to be added to the main verdict. During reconstruction of modified packet use next protocol fields in ipv6 header and ipv6 extension headers. If not set they are autogenerated by dissect content.
The results of all lua-desync instances are aggregated: VERDICT_MODIFY overrides VERDICT_PASS, and VERDICT_DROP overrides them both. VERDICT_PRESERVE_NEXT is applied if any instance set it.
Structure of the desync table
The best way to study the desync structure is by examining its actual content while running the pktdebug test desync function from zapret-lib.lua.
http-req packet from an IPv6 request to http://one.one.one.one
desync:
.target
.port
number 80
.ip6
string 26 06 47 00 47 00 00 00 00 00 00 00 00 00 10 01
.func
string pktdebug
.func_n
number 1
.profile_n
number 1
.l7payload
string http_req
.dis
.tcp
.th_dport
number 80
.th_x2
number 0
.th_off
number 8
.th_sum
number 18781
.th_win
number 64
.options
.1
.kind
number 1
.2
.kind
number 1
.3
.data
string 30 40 18 9A 6F A5 3E 89
.kind
number 8
.th_seq
number 19930989
.th_ack
number 1489231977
.th_flags
number 24
.th_urp
number 0
.th_sport
number 48118
.ip6
.ip6_flow
number 1871905881
.ip6_hlim
number 64
.ip6_dst
string 26 06 47 00 47 00 00 00 00 00 00 00 00 00 10 01
.exthdr
.ip6_plen
number 110
.ip6_src
string 1A E5 18 81 E1 CD E8 24 BA 16 39 FF FE 8A DE 12
.ip6_nxt
number 6
.payload
string 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20 63 75 72 6C 2F 38 2E 38 2E 30 0D 0A 41 63 63 65 70 74 3A 20 2A 2F 2A 0D 0A 0D 0A
.l4proto
number 6
.transport_len
number 110
.l3_len
number 40
.l4_len
number 32
.reasm_offset
number 0
.reasm_data
string 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20 63 75 72 6C 2F 38 2E 38 2E 30 0D 0A 41 63 63 65 70 74 3A 20 2A 2F 2A 0D 0A 0D 0A
.ifout
string eth0
.fwmark
number 0
.func_instance
string pktdebug_1_1
.replay
boolean false
.track
.pos
.dt
number 0.013066192
.server
.tcp
.pos
number 1
.rseq
number 1
.scale
number 13
.mss
number 1360
.winsize_calc
number 65535
.uppos
number 0
.seq0
number 1489231976
.seq
number 1489231977
.uppos_prev
number 0
.winsize
number 65535
.pcounter
number 1
.pdcounter
number 0
.pbcounter
number 0
.client
.tcp
.pos
number 79
.rseq
number 1
.scale
number 10
.mss
number 1380
.winsize_calc
number 65536
.uppos
number 79
.seq0
number 19930988
.seq
number 19930989
.uppos_prev
number 0
.winsize
number 64
.pcounter
number 3
.pdcounter
number 1
.pbcounter
number 78
.reverse
.tcp
.pos
number 1
.rseq
number 1
.scale
number 13
.mss
number 1360
.winsize_calc
number 65535
.uppos
number 0
.seq0
number 1489231976
.seq
number 1489231977
.uppos_prev
number 0
.winsize
number 65535
.pcounter
number 1
.pdcounter
number 0
.pbcounter
number 0
.direct
.tcp
.pos
number 79
.rseq
number 1
.scale
number 10
.mss
number 1380
.winsize_calc
number 65536
.uppos
number 79
.seq0
number 19930988
.seq
number 19930989
.uppos_prev
number 0
.winsize
number 64
.pcounter
number 3
.pdcounter
number 1
.pbcounter
number 78
.lua_state
.hostname
string one.one.one.one
.hostname_is_ip
boolean false
.lua_in_cutoff
boolean true
.lua_out_cutoff
boolean false
.t_start
number 1700000000
.incoming_ttl
number 51
.l7proto
string http
.arg
.testarg1
string val1
.testarg2
string val2
.tcp_mss
number 1360
.l7proto
string http
.outgoing
boolean true
desync
| Field | Type | Description | Notes |
|---|---|---|---|
| func | string | name of the desync function | |
| func_n | number | instance number within the profile | |
| func_instance | string | instance name | derived from the function name, instance number, and profile number |
| profile_n | number | profile number | |
| profile_name | string | profile name | optional |
| cookie | string | value of the nfqws2 --cookie parameter for the profile | optional |
| outgoing | bool | true if the direction is outgoing | |
| ifin | string | incoming interface name | optional |
| ifout | string | outgoing interface name | optional |
| fwmark | number | fwmark of the current packet | Linux only |
| target | table | table containing the IP address and port used for checking ipsets and port filters | |
| replay | bool | delayed packet replay | |
| replay_piece | number | index of the part being replayed | 1-based indexing |
| replay_piece_count | number | total number of parts being replayed | |
| replay_piece_last | bool | indicates the last part being replayed | |
| l7payload | string | payload type of the current packet or packet group | defaults to "unknown" if undetermined |
| l7proto | string | flow protocol | defaults to "unknown" if undetermined |
| reasm_data | string | result of reassembling a multi-packet message, or the payload itself if no reassembly occurred | currently applied only to TCP |
| reasm_offset | number | offset of the current replayed packet within the reassembled data | currently applied only to TCP |
| decrypt_data | string | result of reassembling and decrypting the payload or payloads of multiple packets | used for QUIC |
| tcp_mss | number | MSS of the remote end of the TCP connection | always present, only for TCP |
| track | table | data bound to the conntrack entry | only if conntrack is present; may be absent |
| arg | table | all instance arguments and their values | % and # substitutions have already been resolved |
| dis | table | dissection of the current packet |
Dissect structure
The dissection includes the fields ip, ip6, tcp, and udp.ip is present in the case of IPv4, while ip6 is present for IPv6.
The IP protocol version can be determined by the presence of the ip or ip6 field.
The L4 protocol can be determined by the presence of tcp or udp.
The header tables mirror the field names of C structures from netinet/{ip,ip6,tcp,udp}.h.
IP addresses and IPv4 options are passed as raw strings.
To convert a raw IP address to text format, the ntop C function can be used. It automatically determines the IP version based on the buffer size.
IPv6 extension headers and TCP options are represented as tables.
All multi-byte numeric values are automatically converted from network byte order to host byte order.
dissect
| Field | Type | Description |
|---|---|---|
| ip | table | IPv4 header |
| ip6 | table | IPv6 header |
| frag_off | number | IP fragment offset. present only in IP fragments. |
| tcp | table | TCP header |
| udp | table | UDP header |
| icmp | table | ICMP header |
| l4proto | number | IPPROTO_TCP or IPPROTO_UDP |
| transport_len | number | packet length excluding L3 headers |
| l3_len | number | length of L3 headers, including IP options and IPv6 extension headers |
| l4_len | number | length of the L4 header, including TCP options |
| payload | string | L4 payload or everything after L3 headers in case of raw IP |
ip
| Field | Description |
|---|---|
| ip_v | IP version - 4 |
| ip_hl | IP header length in 4-byte blocks. 5 if no IP options are present. |
| ip_tos | Type of Service. Contains the DSCP (Differentiated Services Code Point). |
| ip_len | Total IP packet length, including all headers and payload. |
| ip_id | Packet identification for reassembling fragments. |
| ip_off | Fragment offset, including MF (More Fragments) and DF (Don't Fragment) flags. |
| ip_ttl | Time to Live - maximum number of hops. |
| ip_p | IP protocol number. Typically IPPROTO_TCP or IPPROTO_UDP. |
| ip_sum | IP header checksum. |
| ip_src | Source IP address. |
| ip_dst | Destination IP address. |
| options | Binary IP options block (rarely used, typically dropped by most network devices). |
ip6
| Field | Description |
|---|---|
| ip6_flow | First 4 bytes of the IPv6 header: version (6), traffic class, and flow label. |
| ip6_plen | Payload length: the length of the packet excluding the base 40-byte IPv6 header (IP6_BASE_LEN). |
| ip6_nxt | Next header/protocol. If no exthdr: IPPROTO_TCP (6) or IPPROTO_UDP (17). |
| ip6_hlim | Hop limit. Serves the same purpose as TTL in IPv4. |
| ip6_src | Source IPv6 address. |
| ip6_dst | Destination IPv6 address. |
| exthdr | Array of extension header tables (1-indexed). |
ip6 exthdr
| Field | Description |
|---|---|
| type | header type: IPPROTO_HOPOPTS, IPPROTO_ROUTING, IPPROTO_DSTOPTS, IPPROTO_MH, IPPROTO_HIP, IPPROTO_SHIM6, IPPROTO_FRAGMENT, IPPROTO_AH |
| next | type of the next header. Similar to type. For the final header, this may be IPPROTO_TCP or IPPROTO_UDP. |
| data | data excluding the first two bytes (type and length) |
udp
| Field | Description |
|---|---|
| uh_sport | source port |
| uh_dport | destination port |
| uh_ulen | UDP length - UDP_BASE_LEN (8) header + payload length |
| uh_sum | UDP checksum |
tcp
| Field | Description |
|---|---|
| th_sport | source port |
| th_dport | destination port |
| th_x2 | reserved field; used for extended TCP flags |
| th_off | TCP header size in 4-byte blocks |
| th_flags | TCP flags: TH_FIN, TH_SYN, TH_RST, TH_PUSH, TH_ACK, TH_URG, TH_ECE, TH_CWR |
| th_seq | sequence number |
| th_ack | acknowledgement number |
| th_win | TCP window size |
| th_sum | TCP checksum |
| th_urp | urgent pointer |
| options | array of tables for TCP options (1-indexed) |
tcp options
| Field | Description |
|---|---|
| kind | option type: TCP_KIND_END, TCP_KIND_NOOP, TCP_KIND_MSS, TCP_KIND_SCALE, TCP_KIND_SACK_PERM, TCP_KIND_SACK, TCP_KIND_TS, TCP_KIND_MD5, TCP_KIND_AO, TCP_KIND_FASTOPEN |
| data | the option data block excluding kind and length; absent for TCP_KIND_END and TCP_KIND_NOOP |
icmp
icmp header is the first 8 bytes of ICMP. This part is mandatory for all icmp types - both ipv4 and ipv6.
Following data including optional headers or attached IP packet goes to payload.
| Field | Description |
|---|---|
| icmp_type | icmp type |
| icmp_code | icmp code |
| icmp_cksum | ICMP checksum |
| icmp_data | 32-bit field at 4-byte offset |
Handling multi-packet payloads
The assembly of a multi-packet payload is referred to as reasm (short for reassemble).
It is performed automatically by the C code if a payload requiring assembly is encountered, conntrack is available, and assembly has not been disabled via --reasm-disable.
Currently, two such payloads are supported: tls_client_hello and quic_initial. Both may contain Kyber post-quantum cryptography, which is too large to fit into a single packet.
For tls_client_hello, standard payload assembly of sequential TCP segments is performed, merging them into a single reasm_data block.
For quic_initial, individual packets are accumulated in an internal buffer, after which they are decrypted, merged, and defragmented. This handles payload parts scattered across packets and different offsets (a technique used by Chrome to prevent others from oversimplifying their algorithms, ensuring they follow standards and can correctly reassemble payloads from parts).
Until assembly is finalized, packets are accumulated in the internal buffer without calling Lua. Once finalized, the individual parts are replayed (replay). The Lua instances receive a dissection of each delayed packet, but with the fields desync.replay=true, desync.replay_piece, desync.replay_count, and desync.replay_piece_last set.
In the case of standard TCP reassembly, the desync.reasm_data field is populated, containing the full block of assembled data. desync.dis.payload continues to return the payloads of individual replayed packets. For TCP, if no replay occurs, desync.reasm_data contains a copy of desync.dis.payload.
During QUIC reassembly, desync.reasm_data is absent. Instead, the desync.decrypt_data field is provided, containing the result of decryption and defragmentation of all payloads within the assembly. For QUIC, reasm_data contains the tls_client_hello without the record layer.
The track table structure
The track table is present in desync only if a conntrack entry was found for the current packet.
It may not be found if the nfqws2 process did not receive the SYN or SYN-ACK packet.
For instance, the connection might have been established before nfqws2 was started, you might not have intercepted SYN and SYN-ACK from the kernel, or you might have explicitly disabled conntrack via --ctrack-disable.
Your code must always check for the existence of track before accessing it; otherwise, the script will crash.
The same applies to optional track fields. Test your code with --ctrack-disable and across different protocols-TCP and UDP.
track
| Field | Type | Description | Note |
|---|---|---|---|
| incoming_ttl | number | TTL/HL of the first incoming packet in the stream | May be absent if not determined |
| l7proto | string | flow protocol | Always present; defaults to "unknown" |
| hostname | string | Hostname, determined through L6/L7 protocol analysis | Appears only after identification |
| hostname_is_ip | bool | Whether the hostname is an IP address | Only present if a hostname exists |
| lua_state | table | A table for storing state associated with the stream | Always present; passed with every packet |
| lua_in_cutoff | bool | Lua cutoff for the incoming direction | Read-only |
| lua_out_cutoff | bool | Lua cutoff for the outgoing direction | Read-only |
| t_start | number | Unix time of the first packet in the stream | Includes a high-precision fractional part |
| pos | table | Counters for various directions | Contains the tables: client, server, direct, and reverse |
The track.pos table contains sub-tables with sets of counters for two directions: client and server.client refers to packets from the client, while server refers to packets from the server.direct and reverse are simply references to client and server. Which one direct and reverse point to depends on the current direction (desync.outgoing) and the server mode (b_server).direct always points to the current direction, while reverse points to the opposite one.
track.pos contains an additional field: dt. This is the packet arrival time in seconds relative to t_start.
It includes a high-precision fractional part.
The list of counter table fields is provided below. The tcp sub-table is present only for the TCP protocol.
| Field | Description | Note |
|---|---|---|
| pcounter | packet counter | |
| pdcounter | data packet counter | packets where the L4 payload size is not 0 |
| pbcounter | transmitted bytes counter | only L4 payload sizes are counted; headers are ignored |
| ip6_flow | last ip6.ip6_flow field | absent if unknown or if the IP protocol is not IPv6 |
| tcp.seq0 | initial connection sequence | |
| tcp.seq | current packet sequence | |
| tcp.rseq | relative sequence of the current packet | calculated as seq - seq0 |
| tcp.rseq_over_2G | rseq has crossed the 2 GB boundary | s and p positions can no longer be relied upon |
| tcp.pos | relative sequence of the current packet's upper boundary | calculated as rseq + payload_size |
| tcp.uppos | maximum pos in the connection | |
| tcp.uppos_prev | uppos in the previous data packet | useful for detecting retransmissions |
| tcp.winsize | last th_win field | without scale correction |
| tcp.scale | last value of the TCP scale option | |
| tcp.winsize_calc | winsize corrected for scale | effective TCP window size |
| tcp.mss | last value of the TCP MSS option |
mss, winsize, and scale are transmitted from one side of the connection to the other so that each side knows the acceptable parameters of its peer.
When using these fields, it is critical not to confuse the sides.
If you need to know what packet size can be sent, you must look at the opposite side - what it is capable of receiving.
The mss is duplicated in the desync.tcp_mss field regardless of whether conntrack is present. The value there is already calculated for use in determining the size of the packet to be sent.
If conntrack is unavailable or if mss was not negotiated by the parties, a default value is set - DEFAULT_MSS (1220).
When working with sequences, you must account for their 32-bit unsigned nature.
If you add 100 to 4,294,967,280 (0xFFFFFFF0), the result will not be 4,294,967,380 (0x100000054), but 84 (0x54).
If you add these numbers in Lua, you will get 4,294,967,380 because Lua represents numbers with more than 32 bits of precision and uses signed values.
For sequence operations and comparisons, it is recommended to use the C functions u32add and bitand.
For example, the expression 0==bitand(u32add(seq1, -seq2), 0x80000000) is equivalent to seq1 >= seq2.
However, the latter simple comparison will not work correctly, whereas the former will-provided that seq1 has not drifted from seq2 by more than 2 GB.
It is impossible to track anything beyond that using sequences. Always keep in mind that when transferring large volumes of data, sequences cannot serve as a counter.
The p*counter fields are 64-bit counters, so they do not suffer from this issue.
ICMP processing
Some icmp types may contain an attached source packet to which icmp was generated. They are called "related."
Such payloads are recognized, they are used to search the original conntrack record.
If it is found, the cached profile (the one to which the attached packet belongs) is selected.
The direction is chosen as the reverse of the found record.
The payload type is set to "ipv4" or "ipv6," the session protocol type is set from the source packet profile.
Then icmp goes through the profile in the usual way. Desync function must be aware of this possibility.
If icmp does not contain an attached packet, it is invalid or conntrack record is not found, icmp passes on its own
without track.
conntrack works only with tcp and udp, it does not keep records of pings or other icmp types.
No counters change when icmp traverses the conntrack entry.
raw IP processing
If the ip protocol is not recognized as tcp, udp, icmp, icmpv6, it is considered raw ip.
Dissect has ip/ip6 field and payload. Payload contains all data after L3 headers.
desync.track is always missing.
Timers
Lua code can be called independently of received network data.
The event source is time.
A timer is an nfqws2 object, identified by a unique name, that allows you to call a specified Lua function at a specified frequency or once after a certain period of time.
The timer is set by the timer_set function, and deleted by the timer_del function.
nfqws2 is a single-threaded program, like the Lua engine. Timers are called in the same thread that processes network data.
In Linux and Windows, packets are received in blocks, not one at a time. A block is processed until all remaining packets are exhausted.
Timers are called between block processing. If processing takes a significant amount of time, the timer call may not be perfectly timed.
Timers can be useful for handling unreplied packets.
For example, you need to send something somewhere and read the response. But the other party might not respond or the response might not arrive.
To prevent your system from hanging in an undefined state and leaving garbage in memory, a timer can help.
Using timers, desync functions, and send functions, you can build a full-fledged state machine - even your own implementation of TCP or another guaranteed delivery system.
The system should be designed asynchronously, using a state machine. Direct sleep delays is not the option, as they break the queue-based traffic processing scheme.
While you're waiting, everything else will hang.
In the --intercept=0 mode, if there are timers, nfqws2 doesn't exit immediately, but calls the timers until there are none left.
After this, the process terminates.
Timer function prototype
function timer(name, data)The timer function can be passed any Lua variable of any type.
A table can be used to store state associated with the timer.
Simple type variables can be used to pass read-only values one-way.
data is set when the timer is started via timer_set.
When the timer is deleted, the variable is automatically deallocated if there are no other references to it.
If a timer function fails with an error, the timer is forcibly deleted.
Therefore, when developing timer functions, it is especially important to avoid error conditions.
A one-shot timer is automatically deleted after the timer function is called. Manual deletion is not required, although it is permitted.
nfqws2 C interface
Before executing --lua-init, the C code sets up base constants, blobs, and C functions.
Base constants
| Field | Type | Description | Note |
|---|---|---|---|
| qnum | number | NFQUEUE queue number | Linux only |
| divert_port | number | divert port number | BSD only |
| desync_fwmark | number | fwmark for Linux, sockarg for BSD, 0 for Windows | |
| NFQWS2_VER | string | nfqws2 version | string returned by --version |
| NFQWS2_COMPAT_VER | number | sequence number of incompatible interface changes with nfqws2 | incremented by 1 for each change |
| b_debug | bool | --debug enabled | debug message output |
| b_daemon | bool | --daemon enabled | daemonize process, detach from tty |
| b_server | bool | --server enabled | Server mode |
| b_ipcache_hostname | bool | --ipcache-hostname enabled | Caching of hostnames corresponding to IP addresses |
| b_ctrack_disable | bool | --ctrack-disable enabled | Conntrack disabled |
| VERDICT_PASSVERDICT_MODIFYVERDICT_DROPVERDICT_PRESERVE_NEXT | number | Desync function verdict codeVERDICT_PRESERVE_NEXT is the bit addition to the main verdict | |
| DEFAULT_MSS | number | Default MSS value | 1220 |
| IP_BASE_LEN | number | Base IPv4 header size | 20 |
| IP6_BASE_LEN | number | Base IPv6 header size | 40 |
| TCP_BASE_LEN | number | Base TCP header size | 20 |
| UDP_BASE_LEN | number | UDP header size | 8 |
| TCP_KIND_ENDTCP_KIND_NOOPTCP_KIND_MSSTCP_KIND_SCALETCP_KIND_SACK_PERMTCP_KIND_SACKTCP_KIND_TSTCP_KIND_MD5TCP_KIND_AOTCP_KIND_FASTOPEN | number | TCP option type codes (kinds) | |
| TH_FINTH_SYNTH_RSTTH_PUSHTH_ACKTH_URGTH_ECETH_CWR | number | TCP flags | Can be combined using + |
| IP_MF | number | IP "more fragments" flag | 0x8000, part of the ip_off field |
| IP_DF | number | IP "don't fragment" flag | 0x4000, part of the ip_off field |
| IP_RF | number | IP "reserved" flag | 0x2000, part of the ip_off field |
| IP_OFFMASK | number | Bitmask for the ip_off field corresponding to the fragment offset | 0x1FFF |
| IP_FLAGMASK | number | Bitmask for the ip_off field corresponding to IP flags | 0xE000 |
| IPTOS_ECN_MASK | number | Bitmask for the ip_tos field corresponding to ECN | 0x03 |
| IPTOS_ECN_NOT_ECT | number | Not ECN-Capable Transport | 0x00 |
| IPTOS_ECN_ECT1 | number | ECN Capable Transport(1) | 0x01 |
| IPTOS_ECN_ECT0 | number | ECN Capable Transport(0) | 0x02 |
| IPTOS_ECN_CE | number | Congestion Experienced | 0x03 |
| IPTOS_DSCP_MASK | number | bitmask of the ip_tos field corresponding to DSCP | 0xFC |
| IP6F_MORE_FRAG | number | "More fragments" bit of the ip6f_offlg field from the IPv6 fragment header | 0x0001 |
| IPV6_FLOWLABEL_MASK | number | flow label in ip6_flow | 0x000FFFFF |
| IPV6_FLOWINFO_MASK | number | flow label and traffic class in ip6_flow | 0x0FFFFFFF |
| IPPROTO_IPIPPROTO_IPV6IPPROTO_ICMPIPPROTO_ICMPV6IPPROTO_TCPIPPROTO_UDPIPPROTO_SCTPIPPROTO_HOPOPTSIPPROTO_ROUTINGIPPROTO_FRAGMENTIPPROTO_AHIPPROTO_ESPIPPROTO_DSTOPTSIPPROTO_MHIPPROTO_HIPIPPROTO_SHIM6IPPROTO_NONE | number | IP protocol numbers | used in IPv4 and IPv6 |
| ICMP_ECHOREPLYICMP_DEST_UNREACHICMP_REDIRECTICMP_ECHOICMP_TIME_EXCEEDEDICMP_TIMESTAMPICMP_TIMESTAMPREPLYICMP_INFO_REQUESTICMP_INFO_REPLY | number | icmp types | ipv4 only |
| ICMP_UNREACH_NETICMP_UNREACH_HOSTICMP_UNREACH_PROTOCOLICMP_UNREACH_PORTICMP_UNREACH_NEEDFRAGICMP_UNREACH_SRCFAILICMP_UNREACH_NET_UNKNOWNICMP_UNREACH_HOST_UNKNOWNICMP_UNREACH_NET_PROHIBICMP_UNREACH_HOST_PROHIBICMP_UNREACH_TOSNETICMP_UNREACH_TOSHOSTICMP_UNREACH_FILTER_PROHIBICMP_UNREACH_HOST_PRECEDENCEICMP_UNREACH_PRECEDENCE_CUTOFF | number | icmp codes for destination unreachable | ipv4 only |
| ICMP_REDIRECT_NETICMP_REDIRECT_HOSTICMP_REDIRECT_TOSNETICMP_REDIRECT_TOSHOST | number | icmp codes for redirect | ipv4 only |
| ICMP_TIMXCEED_INTRANSICMP_TIMXCEED_REASS | number | icmp codes for time exceeded | ipv4 only |
| ICMP6_ECHO_REQUESTICMP6_ECHO_REPLYICMP6_DST_UNREACHICMP6_PACKET_TOO_BIGICMP6_TIME_EXCEEDEDICMP6_PARAM_PROBMLD_LISTENER_QUERYMLD_LISTENER_REPORTMLD_LISTENER_REDUCTIONND_ROUTER_SOLICITND_ROUTER_ADVERTND_NEIGHBOR_SOLICITND_NEIGHBOR_ADVERTND_REDIRECT | number | icmpv6 types | ipv6 only |
| ICMP6_DST_UNREACH_NOROUTEICMP6_DST_UNREACH_ADMINICMP6_DST_UNREACH_BEYONDSCOPEICMP6_DST_UNREACH_ADDRICMP6_DST_UNREACH_NOPORT | number | icmpv6 code for destination unreachable | ipv6 only |
| ICMP6_TIME_EXCEED_TRANSITICMP6_TIME_EXCEED_REASSEMBLY | number | icmpv6 codes for time exceeded | ipv6 only |
| ICMP6_PARAMPROB_HEADERICMP6_PARAMPROB_NEXTHEADERICMP6_PARAMPROB_OPTION | number | icmpv6 codes for parameter problem | ipv6 only |
Standard blobs
- fake_default_tls - a fake Firefox TLS ClientHello without Kyber, SNI=www.microsoft.com
- fake_default_http - an HTTP request to <www.iana.org>
- fake_default_quic - 0x40 + 619*0x00
Environment variables
| env | Purpose |
|---|---|
| WRITABLE | A directory writable by Lua. Corresponds to the --writable option. |
| APPDATALOW | (Windows only) The AppData location for the low mandatory level. This is also writable, but using --writable is preferred for cross-platform compatibility. |
C functions
Logging
function DLOG(string)
function DLOG_ERR(string)
function DLOG_CONDUP(string)These functions output a string to the --debug log, appending an EOL (End of Line).
- DLOG - standard output.
- DLOG_CONDUP - standard output + console output if logging to a file or syslog is enabled.
- DLOG_ERR - similar to DLOG_CONDUP, but all console output is directed to stderr.
IP conversion
function ntop(raw_ip)
function pton(string_ip)- ntop converts a raw byte string containing an IPv4 or IPv6 address into a human-readable string representation. The IP version is determined by the size of
raw_ip(4 or 16 bytes). If the size does not match, it returnsnil. - pton converts a string representation of an IPv4 or IPv6 address into a
raw_ipbyte string. If the string is not a valid IPv4 or IPv6 address, it returnsnil.
Bitwise operations
Lua 5.1, which LuaJIT is based on, lacks built-in bitwise operations. LuaJIT includes a built-in bitop module.
Lua 5.3 has built-in bitwise operators but lacks the bitop module. While it can be loaded, this is only possible if static compilation is not used and the module is installed. The nfqws2 version on GitHub is built statically.
When working with network packet fields, bitwise operations are essential.
Bitwise operations and shifts are typically implemented as a single machine instruction, making them native operations for the processor. Replacing them with constructs based on floating-point math (exponentiation, division, rounding, etc.) is inefficient, especially considering that many routers and other embedded devices lack an FPU.
To avoid this inconsistency, nfqws2 provides its own set of bitwise and shift functions that are independent of the Lua engine type or version. All bitwise operations work with unsigned numbers ranging from 8 to 48 bits.
If negative numbers are passed, they are interpreted using two's complement. For example, in 32-bit mode, -2 becomes 0xFFFFFFFE; in 8-bit mode, it becomes 0xFE.
Higher bit depths are not supported due to incompatibilities between Lua 5.3+ and older versions.
Only Lua 5.3 implements a 64-bit integer type. Older versions use the double floating-point format with a 52-bit mantissa, which allows for safe handling of up to 48-bit integers.
Standard shift and bitwise logical operations:
function bitlshift(u48, bits)
function bitrshift(u48, bits)
function bitand(u48_1, u48_2, ...., u48_N)
function bitor(u48_1, u48_2, ...., u48_N)
function bitxor(u48_1, u48_2, ...., u48_N)
function bitnot(u48)
function bitnot8(u8)
function bitnot16(u16)
function bitnot24(u24)
function bitnot32(u32)
function bitnot48(u48)bitand, bitor, and bitxor support an arbitrary number of arguments.bitnot has versions for different bit depths. The generic bitnot is an alias for bitnot48.
Functions for getting and setting individual bits:
function bitget(u48, bit_from, bit_to)
function bitset(u48, bit_from, bit_to, set)- bitget extracts a number from a range of bits in
u48frombit_fromtobit_to. Bit numbering starts at 0. - bitset writes the value
setinto the bit range ofu48frombit_fromtobit_to. Bit numbering starts at 0. Higher bits ofsetthat exceed the range(bit_to - bit_from)are ignored.
Unsigned number operations
Bit depth is always critical when performing unsigned number operations, as it determines the result.
Consequently, all functions include the bit depth in their name. If arguments exceeding the specified bit depth are passed, an error is triggered.
uX
function u8(raw_string[, offset])
function u16(raw_string[, offset])
function u24(raw_string[, offset])
function u32(raw_string[, offset])
function u48(raw_string[, offset])These functions are used to extract numeric fields in big-endian format from a raw string.
The offset parameter is the byte position from the start of the raw string, starting at 1.
Similar built-in functionality (string.unpack) is only available in Lua 5.3 and later.
buX
function bu8(u8)
function bu16(u16)
function bu24(u24)
function bu32(u32)
function bu48(u48)These functions convert a number into a big-endian raw string.
To assemble a structure from numeric fields, you can use the standard string concatenation operator ...
swapX
function swap16(u16)
function swap24(u24)
function swap32(u32)
function swap48(u48)These functions invert the byte order of a u16, u24, u32, or u48 value. If your structure uses little-endian byte order, you can combine uX/buX with swapX.
uXadd
function u8add(u8_1, u8_2, ...., u8_N)
function u16add(u16_1, u16_2, ...., u16_N)
function u24add(u24_1, u24_2, ...., u24_N)
function u32add(u32_1, u32_2, ...., u32_N)
function u48add(u48_1, u48_2, ...., u48_N)Functions for adding an arbitrary number of unsigned integers of a specified bit-depth.
Operands must fit within the specified bit-depth; otherwise, an error is raised.
Any carry-over beyond the bit-depth is ignored (modular arithmetic).
Integer division
divint
Native integer division is only available in Lua 5.3+, which includes an explicit integer data type.
To avoid issues with rounding, integer division is implemented via a C function:
function divint(dividend, divisor)This function has no bit-depth restrictions. Internally, it uses the int64_t type.
In Lua 5.3+, there is no loss of precision. In older versions, numbers may be distorted if the operands or the result exceed the size of a double's mantissa.
Random Data Generation
brandom
These functions generate a raw string of a specified size consisting of random bytes.
The random data generated is not cryptographically secure.
function brandom(size)
function brandom_az(size)
function brandom_az09(size)brandomreturns bytes from 0 to 255.brandom_azreturns characters from 'a' to 'z'.brandom_az09returns characters from 'a' to 'z' and digits from '0' to '9'.
Parsing
parse_hex
function parse_hex(hex_string)Returns a raw string containing the bytes represented by a hex string. For example: "1234ABCD" => "\x12\x34\xAB\xCD".
If the hex_string is invalid, it returns nil.
Cryptography
Lua typically has a standard binding module for OpenSSL that provides a wide range of cryptographic functions.
However, relying on external modules is often not an option, as Lua is frequently linked statically without support for loading external modules.
To avoid unnecessary dependencies and extra files-especially since OpenSSL's multi-megabyte size is a concern for embedded systems - this implementation is self-contained.
nfqws2 does not use any external crypto libraries, but it includes a minimal set of cryptographic operations required for certain protocols (such as QUIC).
These functions are exposed to Lua and can be used for any purpose.
bcryptorandom
function bcryptorandom(size)Generates a raw string containing a cryptographically secure block of random data of the specified size. The source is /dev/random.
Random data source - getrandom() (Linux), getentropy() (BSD), /dev/random./dev/urandom is used as a fallback if the previous sources failed or block.
lack of entropy pool is typical Android problem.
bxor,bor,band
function bxor(data1, data2)
function band(data1, data2)
function bor(data1, data2)Return per-byte xor,and,or between 2 equal sized raw string. Error is generated if string sizes are not equal.
hash
function hash(hash_type, data)Returns a raw string representing the hash of a data block (the data raw string). hash_type can be "sha256" or "sha224".
aes
function aes(encrypt, key, data)Performs simple encryption or decryption of a single AES block.
encrypt:trueto encrypt,falseto decrypt.key: A raw string. The size must be 16, 24, or 32 bytes, corresponding to AES-128, AES-192, or AES-256.data: A raw string. The size must be exactly 16 bytes.- Returns a 16-byte raw string containing the result of the operation.
- An error is raised if the
keyordatasizes are incorrect.
aes_gcm
function aes_gcm(encrypt, key, iv, data[, associated_data])AES encryption in GCM mode.
encrypt-trueto encrypt,falseto decrypt.key- raw string. The key size must be 16, 24, or 32 bytes, corresponding to AES-128, AES-192, and AES-256.iv- 12-byte raw string. IT IS MANDATORY TO GENERATE A RANDOM IV FOR EVERY ENCRYPTED DATA BLOCK AND TRANSMIT IT ALONGSIDE THE DATA. If an IV IS REUSED WITH THE SAME KEY, THE ENCRYPTION IS EASILY COMPROMISED. Usebcryptorandomto generate the IV.data- raw string of arbitrary size. The cipher uses a keystream, so the source data is not tied to the AES block size.associated_data- unencrypted data transmitted with the encrypted message that is included in the ATAG calculation. Can benil.- Returns 2 values: a raw string (the encrypted data block) and a raw string (the ATAG or authentication tag). The ATAG can be transmitted with the encrypted message, IV, and associated data to verify their integrity.
- Throws an error if the key or IV sizes are incorrect.
aes_ctr
function aes_ctr(key, iv, data)AES encryption in CTR mode.
key- raw string. The key size must be 16, 24, or 32 bytes, corresponding to AES-128, AES-192, and AES-256.iv- 16-byte raw string. IT IS MANDATORY TO GENERATE A RANDOM IV FOR EVERY ENCRYPTED DATA BLOCK AND TRANSMIT IT ALONGSIDE THE DATA. If an IV IS REUSED WITH THE SAME KEY, THE ENCRYPTION IS EASILY COMPROMISED. Usebcryptorandomto generate the IV.data- raw string of arbitrary size. The cipher uses a keystream, so the source data is not tied to the AES block size.- Encryption works via XOR with a keystream, making it symmetric. Encryption and decryption are the same operation.
- Returns a raw string: the encrypted data block.
- Throws an error if the key or IV sizes are incorrect.
hkdf
function hkdf(hash_type, salt, ikm, info, okm_len)HKDF - HMAC-based Key Derivation Function. A key generator based on arbitrary input keying material (IKM).
The function includes both extraction and expansion phases.
hash_typecan be "sha256" or "sha224".salt- raw string of arbitrary size; can benil. This is non-secret information that allows for different results from the same IKM. Ifnil, a block of zero bytes equal to the hash function's output size is used.ikm- raw string (input keying material). OKM (output keying material) is generated based on this data, the salt, and the info.info- raw string of arbitrary size; can benil. Similar to salt, but salt is mixed in during the extraction phase, while info is used during expansion. Ifnil, a zero-length info string is used.okm_len- the required length of the OKM (output keying material).- Returns a raw string: the OKM.
Compression
gunzip
function gunzip_init([windowBits])
function gunzip_end(zstream)
function gunzip_inflate(zstream, compressed_data[, expected_uncompressed_chunk_size])gunzip_initcreates and returns a gzip stream context for subsequent function calls. See the zlib documentation forwindowBitsvalues (default is 47).gunzip_endreleases the gzip context. While it can be handled by the garbage collector, it is better to call it explicitly.- instead of calling
gunzip_endin Lua 5.5+ it's possible to use to-be-close variable for the context gunzip_inflatedecompresses the next part of the gzipped data. Data can be fed into the function in chunks. Decompressed parts are concatenated to obtain the full data. Returns 2 arguments: the decompressed data and a boolean flag indicating the end of the gzip stream. Returnsnilin case of corrupted data or memory allocation failure.expected_uncompressed_chunk_size- an optional parameter to optimize memory allocation for decompressed data. If the buffer is insufficient,reallocis called, which copies memory blocks and impacts performance. The size should be chosen based on the expected compression ratio with a small margin. The default is four times the size ofcompressed_data.
gzip
function gzip_init([windowBits[, level[, memlevel]]])
function gzip_end(zstream)
function gzip_deflate(zstream, uncompressed_data[, expected_compressed_chunk_size])gzip_initcreates and returns a gzip stream context for subsequent function calls. ForwindowBitsvalues, refer to the zlib documentation (default is 31).levelspecifies the compression level from 1 to 9 (default is 9), andmemleveldefines the allowed memory usage level from 1 to 8 (default is 8).gzip_endreleases the gzip context. While it can be released by the garbage collector, it is better to call this function explicitly.- instead of calling
gzip_endin Lua 5.5+ it's possible to use to-be-close variable for the context gzip_deflatecompresses the next chunk of data. Data can be fed in parts. The compressed chunks are concatenated to form the complete data set. To finalize the stream after all data has been fed, the function must be called withuncompressed_data=niloruncompressed_data="". It returns two arguments: the compressed data and a boolean flag indicating the end of the gzip stream. If a gzip error occurs or memory is insufficient, it returnsnil.expected_compressed_chunk_sizeis an optional parameter used to optimize memory allocation for compressed data. If the buffer is insufficient,reallocis called, which copies memory blocks and impacts performance. The size should be chosen based on the expected compression ratio with a small margin. The default is half the size ofuncompressed_data.
System functions
uname
function uname()Returns the same value as the uname shell command-the OS kernel name (e.g., "Linux", "FreeBSD", "OpenBSD").
On Windows, it returns a string starting with "CYGWIN" followed by the version.
clock_gettime
function clock_gettime()
function clock_getfloattime()clock_gettime retrieves the precise time. Returns two values: Unix time in seconds and the nanosecond component. The built-in os.time() function does not provide nanoseconds.
clock_getfloattime returns unixtime in the floating point format. Nanoseconds go to the fractional part.
getpid
function getpid()
function gettid()getpid()returns the current process identifier (PID).gettid()returns the current thread identifier (TID).
stat
function stat(filename)If successful returns the following table :
| Field | Type | Description |
|---|---|---|
| type | string | file type : file, dir, socket, blockdev, chardev, fifo, unknown |
| size | number | file size |
| mtime | number | modification unixtime in floating point format |
| inode | number | inode. In Windows doesn't fit into number datatype of luajit but fits into integer datatype of Lua5.3+ |
| dev | number | device id |
In case of error returns 3 values : nil, error string, error number (errno).
time
function localtime(unixtime)
function gmtime(unixtime)
function timelocal(tm)
function timegm(tm)localtime and gmtime return dissected unixtime similar to C "struct tm". timelocal and timegm are reverse functions.
| Field | Type | Description |
|---|---|---|
| sec* | number | second |
| min* | number | minute |
| hour* | number | hour |
| mon* | number | month starting from 0 |
| mday* | number | day of month starting from 1 |
| year* | number | full year, not from 1900 |
| wday | number | day of week. 0 = sunday |
| yday | number | day of year starting from 0 |
| isdst* | number | not zero if summer time in effect |
| zone | string | time zone |
| str | string | formatted string: "dd.mm.yyyy hh:mi:ss" |
Fields marked with * are required for reverse translation.
Packet handling options
The following functions use standard sets of options: rawsend and reconstruct.
These are tables with specific fields. If nil is passed, it is assumed that no fields are set.
standard reconstruct
Dissect reconstruction options-reconstruct_opts. Reconstruction is the process of assembling a raw packet from a dissect.
| Field | Type | Description |
|---|---|---|
| keepsum | bool | Use checksum from the dissect as is. Do not calculate or corrupt. |
| badsum | bool | Corrupt the L4 checksum. Calculates the checksum and XORs it with a random value from 1 to 0xFFFF |
| ip6_preserve_next | bool | Use the "next" values from ip6.exthdr |
| ip6_last_proto | number | If ip6_preserve_next=false, specifies the IP protocol for the last exthdr |
When assembling IPv6 by default, the chain of IP protocols in the exthdr is built automatically.
Each exthdr has a type field, making it clear what to write into the previous exthdr or the main ip6 header. The next protocol of the last exthdr is set to IPPROTO_TCP or IPPROTO_UDP depending on whether tcp or udp tables are present in the dissect.
In most cases, this is convenient because when inserting an exthdr, you do not need to reconstruct the entire chain of next protocols or fill in the next fields manually. The dissect reconstructor handles this for you.
Use ip6_preserve_next if you have a special task that requires manual crafting of the next protocol fields.
In this case, the next fields in exthdr and ip6.ip6_nxt are left as is.
If you are reconstructing an IPv6 header separately and are not using the ip6_preserve_next option, it is impossible to automatically determine what should be written into the last exthdr. In this case, ip6_last_proto is used, or IPPROTO_NONE if ip6_last_proto is not specified.
badsum has been moved to reconstruction because TCP and UDP checksums are calculated based on the entire IP packet.
The checksum includes elements from the IP/IPv6 header, the entire TCP header, and the payload itself.
Therefore, it is impossible to guarantee a corrupted checksum by looking at individual parts alone.
No matter what value you input, there is a small probability (1/65536) that it will happen to be valid.
The packet is formed based on the L3 header, then the L4 header (TCP,UDP,ICMP, if present) is appended, followed by the payload.
During reconstruction IP protocol fields are ignored. Therefore, it is possible to construct TCP,UDP,ICMP packets with a modified IP protocol.
standard rawsend
Raw packet sending options - rawsend_opts
| Field | Type | Description |
|---|---|---|
| repeats | number | The number of times to repeat sending the exact same packet. |
| fwmark | number | The fwmark of the outgoing packet. Linux only. Defaults to 0. The desync_mark bit is forced. |
| ifout | string | The outgoing interface. May or may not be used depending on the situation. |
ifout should always be passed exactly as it was received in the dissect.
A correct ifout is mandatory for Windows. It is not used on BSD.
On Linux, it is only used if the --bind-fix4 or --bind-fix6 options are enabled, depending on the IP version.
It is recommended to pass fwmark exactly as it was received in the dissect.
In special cases where your routing table rules rely on it, you can mix in your own bits.
repeats sends a binary-identical packet the specified number of times without inspecting its contents.
No changes are made, including to the ip_id.
If you need the ip_id to change, you can set it to 0; Windows will then automatically fill in incrementing values.
Other systems will not do this. If you need to manage the ip_id manually across all systems, repeats is not the right choice.
Dissection and reconstruction
Dissection is the process of obtaining a structured representation of a raw IP packet.
Reconstruction is the reverse process-generating a raw IP packet from a dissect.
dissect
function dissect(raw_ip)Returns a table representing the dissection of the raw_ip packet.
This is the same operation that occurs automatically before desync functions are called based on the profile.
Those functions receive an already prepared dissect.
reconstruct_dissect
function reconstruct_dissect(dissect[, reconstruct_opts])Returns raw_ip. All checksums are calculated automatically. L4 checksums are intentionally corrupted if badsum is specified in reconstruct_opts.
Reconstruction of fragmented IP packets involves special magic.
- if the "frag_off" field is present, tcp/udp/icmp headers are ignored, payload contains raw ip payload. Incoming fragmented packets come in this form. nfqws2 does not defragment at the IP layer. But this is very persistently done by Linux systems - in order for a fragment to come to nfqws2, you need to try hard by inserting "notrack" into prerouting or output. Dissects in this form can be reconstructed as is. But preparing them in Lua is extremely inconvenient, since you will have to go through the black magic of working with a binary representation.
- If the "frag_off" field is absent, fragment reconstruction is performed on the entire packet's dissect involving both Lua and C code. Lua code must prepare a dissect of the full packet intended for fragmentation, but fill certain fields as they should appear in the fragment:
- ipv4:
ip.ip_lenmust be calculated as it should appear in the fragment.
The C code usesip.ip_lento determine the size of the fragmented portion.
Theip.ip_offfield must contain the fragment offset and theIP_MFflag if it is not the last fragment.ip.ip_idmust not be 0. - ipv6: You must insert a fragment header into
ip6.exthdrand fill in theident,offset, and theIP6F_MORE_FRAGbit if it is not the last fragment.ip6.ip6_lenmust be calculated as it should appear in the fragment. The C code determines the fragment size based on this length.
The Lua code chooses the position of the fragment header. Everything following the fragment header is considered the fragmentable part.
If the C code detects that fragmentation is required, it validates the calculated lengths and offsets. If they are correct, it shifts the contents of the raw packet within the reconstruction buffer after reconstruction to produce a fragment with the required data.
reconstruct_hdr
function reconstruct_tcphdr(tcp)
function reconstruct_udphdr(udp)
function reconstruct_icmphdr(icmp)
function reconstruct_iphdr(ip)
function reconstruct_ip6hdr(ip6[, reconstruct_opts])Reconstructs the corresponding raw headers from the dissect tables. Returns the raw version of the header.
- IPv6 reconstruction utilizes
reconstruct_opts, specificallyip6_preserve_nextandip6_last_proto. - The IP header checksum is calculated automatically since it does not depend on any other components.
- TCP, UDP and ICMP checksums are not calculated automatically because they depend on other components.
csum_fix
function csum_ip4_fix(raw_ipv4_header)
function csum_tcp_fix(raw_ip_header, raw_tcp_header, payload)
function csum_udp_fix(raw_ip_header, raw_udp_header, payload)
function csum_icmp_fix(raw_ip_header, raw_icmp_header, payload)Functions for fixing checksums. Since strings in Lua are immutable, these functions return a copy of the respective header with the corrected checksum.
csum_ipv4_fixis straightforward: it takes an IP header as input and returns an IP header with the corrected checksum.csum_tcp_fix,csum_udp_fixcsum_icmp_fixtake a raw IP header (IPv4 or IPv6), a TCP/UDP/ICMP header, and the payload. The IP version is detected automatically. The checksum is calculated based on the L3 and L4 headers and the payload.
Direct reconstruction of individual headers is rarely necessary. Typically, all tasks are handled by functions working with dissects.
conntrack
function conntrack_feed(dissect/raw_packet[, reconstruct_opts])"Feed" dissect or raw packet string to conntrack the way as if it was received from the network.
reconstruct_opts is meaningful only for dissects.
2 values are returned : track and bool value "outgoing".
outgoing is true if a new conntrack record was created and it was a SYN packet in case of tcp or any packet in case of udp.
If conntrack record already exists outgoing is true if the record was found by direct tuple - src_ip, src_port, dst_ip, dst_port.
If the record was found by reverse tuple - dst_ip, dst_port, src_ip, src_port - outgoing is false.
The function can come in handy if you are obfuscating and transmitting data in a distorted form. For example, tcp is converted to icmp or the SYN flag is corrupted.
The receiving end will issue the first corrupted packet without track, because it is either not tcp and not udp, or there was no valid tcp handshake.
After deobfuscation, you can correct the situation by executing conntrack_feed and assigning desync.track its result.
If conntrack is disabled or the packet is not valid tcp or udp, nil is returned.
Obtaining IP addresses
function get_source_ip(target)Get source IP (raw string) that would be used for connection the the target IP.
Returns nil if destination is unreachable.
function get_ifaddrs()Get all interfaces and IPs on them (analog of "ip addr", "ifconfig", "ipconfig").
Returns table indexed by interface names. In Windows interface names are in the "number.number" form - IfxIdx.SubIfIdx.
This is WinDivert compatible.
Interface table contents :
| Field | Type | Description |
|---|---|---|
| index | number | interface index |
| mtu | number | MTU. for loopback can be 64K or even 0xFFFFFFFF |
| flags | number | os-specific bit flags |
| ssid | string | wifi SSID if known. SSIDs are obtained only if --filter-ssid is used in any profile |
| guidiftypeindex6speed_xmitspeed_recvmetric4metric6conntype | number | (Windows only) additional fields from GetAdaptersAddresses() |
| addr | table | integer indexed array of addresses |
Address contents :
| Field | Type | Description |
|---|---|---|
| addr | string | ipv4 or ipv6 address - raw string |
| netmask | string | subnet mask - raw string |
| broadcast | string | (ipv4 only) broadcast address - raw string |
| dst | string | ifa_dstaddr from getifaddrs() - raw string |
Only "addr" field is always present. Others can be absent.
Receiving and sending Packets
rawsend
function rawsend(raw_data[, rawsend_opts])
function rawsend_dissect(dissect[, rawsend_opts[, reconstruct_opts]])rawsendworks with a raw string containing a fully assembled IPv4 or IPv6 packet.rawsend_dissectassembles a packet from a dissect and sends it.dissectrefers to the table structure described in the corresponding section.
raw_packet
function raw_packet(ctx)Upon invocation, Lua functions receive a ready-made dissect of the current packet. The raw representation is rarely required, so it is not provided to the desync engine by default to conserve resources.
It can be retrieved on demand using the raw_packet function.
Working with payloads
Markers
- Absolute positive marker - a numeric offset within the payload.
- Absolute negative marker - a numeric offset within the payload relative to the byte following the end. -1 points to the last byte.
- Relative marker - a positive or negative offset relative to a logical position within the payload.
Relative positions:
- method - the start of the HTTP method ('GET', 'POST', 'HEAD', ...). The method is usually at position 0, but may shift due to
methodeol, in which case the position may become 1 or 2. - host - the start of the hostname.
- endhost - the byte following the last byte of the hostname.
- sld - the start of the second-level domain (SLD) in the hostname.
- endsld - the byte following the last byte of the second-level domain.
- midsld - the middle of the second-level domain.
- sniext - the start of the SNI extension data field in TLS. Any extension consists of 2-byte type and length fields, followed by the data field.
- extlen - the length field of TLS extensions.
Relative markers work with logical elements of specific known payloads and will not function with arbitrary data.
Example marker list: 100,midsld,sniext+1,endhost-2,-10.
resolve_pos
The purpose of the following functions is to convert markers into absolute positions.
function resolve_pos(blob,l7payload_type,marker[,zero_based_pos])
function resolve_multi_pos(blob,l7payload_type,marker_list[,zero_based_pos])
function resolve_range(blob,l7payload_type,marker_list[,strict,zero_based_pos])resolve_posworks with a single marker. If the marker does not resolve, it returnsnil.resolve_multi_posworks with a comma-separated list of markers. It returns an array of unique absolute positions. If some markers do not resolve, they will be omitted from the result.resolve_rangeresolves a list of exactly 2 markers representing a range within the payload. Ifstrict = trueand any marker fails to resolve, it returnsnil. Otherwise, if the first marker fails to resolve, it is replaced with 0. If the second marker fails to resolve, it is replaced with the payload length. If both fail to resolve, it returnsnil.- If
zero_based_pos=trueis set, all positions start from 0; otherwise, they start from 1, as is standard in Lua. - An
erroris raised for invalid values ofl7payload_type,marker,marker_list, or if the number of markers forresolve_rangeis not equal to 2.
tls_mod
function tls_mod(blob, modlist[, payload])- blob - the blob being acted upon
- payload - the content to which the blob is modified. For different mods payload can be arbitrary or a valid TLS is required.
- modlist - comma separated list of modifications
Mods :
- rnd - fill random and session id fields with random data
- dupsid - copy session id from payload. executaed after "rnd". requires valid TLS payload or not applied.
- rndsni - replace SNI with random domain. If length(original sni)>=7 chars, a random subdomain is generated from a random well known 3-letter TLD. Otherwise a random string matching regexp
[a-z][a-z0-9]* - sni=domain.com - replace SNI with specified domain
- padencap - Adjust the blob so that the payload becomes part of the padding extension. The payload can be arbitrary.
Instance execution management
instance_cutoff
function instance_cutoff(ctx[, outgoing])Voluntary self-cutoff of an instance for the specified direction.
The instance will no longer be called within the current flow.
outgoing = true- outbound directionoutgoing = false- inbound directionoutgoing = nil- both directions
lua_cutoff
function lua_cutoff(ctx[, outgoing])Similar to instance_cutoff, but the entire profile is cut off from the flow.
When the profile changes after obtaining a hostname or detecting the flow protocol, the lua_cutoff is reset, as the new profile contains a completely different set of instances that have not requested to be cut off.
A lua_cutoff state can also occur naturally if all instances have exceeded their upper range limit or have self-cutoff from the direction.
execution_plan
function execution_plan(ctx)Returns an array of information about all subsequent, pending instances in the current profile, their in-profile filters, and arguments.
Plan Element
| Field | Type | Description |
|---|---|---|
| func | string | desync function name |
| func_n | number | instance number within the profile |
| func_instance | string | instance name (derived from the function name, instance number, and profile number) |
| range | table | effective range of counters --in-range or --out-range depending on the current direction |
| payload | table | effective payload filter : payload name indexed table. |
| payload_filter | string | effective payload filter : a comma-separated list of payload names. |
| arg | table | instance arguments |
range
| Field | Type | Description |
|---|---|---|
| from | table | lower bound position |
| to | table | upper bound position |
| upper_cutoff | bool | true = upper bound is exclusive, false = inclusive |
pos - from, to
| Field | Type | Description |
|---|---|---|
| mode | string | counter mode - a, x, n, d, b, s, p |
| pos | number | counter value |
execution_plan_cancel
function execution_plan_cancel(ctx)A one-time cancellation of all subsequent instances within a profile.
The instance performing the cancellation takes over the coordination of further actions and is called the orchestrator.
Timer control
Timer creation and deletion functions can be called from any Lua code.
This could be lua-init, lua-desync, or a timer function. A timer function can also act on itself, such as changing the period or terminating its own calls.
Timers are identified by a name. Multiple timers with different names can call the same timer function.
It will be passed the timer name and arbitrary data as parameters (#timer-function-prototype)
timer_set
Create or replace the timer.
function timer_set(name, func, period, oneshot, data)- name - a unique timer name. If a timer with this name already exists, it is deleted and replaced with a new one. When replaced, the countdown restarts.
- func - the timer function. string or function. if it's a string function is resolved during timer_set call and remembered as a function, not as a string.
- period - the timer call frequency in milliseconds
- oneshot - a bool indicating whether the timer is single-shot (true) or periodic (false)
- data - an arbitrary variable passed to the timer-function
timer_del
Delete the timer.
function timer_del(name)timer_info
Get information about the timer identified by name.
function timer_info(name)Returns table in case of success, nil otherwise.
| Field | Type | Description |
|---|---|---|
| name | string | unique timer name |
| func | function | timer function |
| oneshot | bool | true = single shot timer, false = periodic timer |
| period | number | timer period in msec |
| fires | number | number of timer calls made |
timer_enum
Get an array of unique names of all existing timers.
function timer_enum()zapret-lib.lua base function library
Nearly every function includes detailed comments regarding its purpose and parameters.
Reviewing the Lua code and comments will provide a better understanding of what a specific function does and how to call it.
Base desync functions
These can be used directly in --lua-desync.
luaexec
function luaexec(ctx, desync)Executes arbitrary Lua code specified in the "code" argument.
The code can address the desync table-it is temporarily assigned to a global variable named desync, which is cleared once the code finishes executing.
Example: --lua-desync=luaexec:code="desync.rnd=brandom(math.random(5,10))"
pass
function pass(ctx, desync)Does nothing except output a "pass" message to the debug log.
pktdebug
function pktdebug(ctx, desync)Outputs the desync structure to the debug log.
argdebug
function argdebug(ctx, desync)Outputs the arguments table to the debug log.
posdebug
function posdebug(ctx, desync)Outputs information about the current conntrack positions for both outbound and inbound directions to the debug log.
detect_payload_str
function detect_payload_str(ctx, desync)- arg:
pattern- the substring to search for withindesync.dis.payload - arg:
payload- the value to assign todesync.l7payloadif the substring is found - arg:
undetected- (optional) the value to assign todesync.l7payloadif the substring is not found
Example of a basic protocol detector. It searches for a pattern in the payload; if found, it sets desync.l7payload = desync.arg.payload. Otherwise, if desync.arg.undetected is provided, it sets desync.l7payload = desync.arg.undetected.
Protocol detectors of this type have no effect on the underlying C code. The C implementation knows nothing about your custom protocol or payload type. Your value cannot be specified in the --payload parameter. However, it can be used with the payload filters of many desync functions.
desync_orchestrator_example
function desync_orchestrator_example(ctx, desync)A test orchestrator. It performs no special logic other than executing the original execution plan as-is.
Utility functions
var_debug
function var_debug(v)Outputs information about the parameter v (type and value) to the debug log.
If the parameter is a table, the function recursively traverses nested values and tables, presenting the information as a tree structure.
deepcopy
function deepcopy(orig)Creates a copy of the orig variable.
Its primary purpose is to recursively create a copy of a table including all its sub-tables.
In Lua, tables are passed by reference. No matter which variable you use to modify a table, only one instance of that table exists; changes will be visible through all variables referencing it.
To create a true copy, you must initialize a new table and assign all fields from the source table to it, copying sub-tables recursively in the same manner. This is what the deepcopy function handles.
Simple types in Lua are assigned by value. All strings are stored in a single pool to prevent content duplication. String variables reference this pool. Strings are immutable; you can only assign a different string to a variable. If a new string has a unique value not present in the pool, a new pool entry is created; otherwise, a reference to the existing entry is assigned.
logical_xor
function logical_xor(a,b)Returns the result of a logical XOR between a and b. result = a and not b or not a and b
array_search
function array_search(a, v)
function array_field_search(a, f, v)Performs a linear search in table a for value v. array_field_search assumes that the elements of table a are themselves tables, and performs the search on field f.
String operations
in_list
function in_list(s, v)find_next_line
Checks whether string v is included in a comma-separated list of strings s. For example, abc is included in the list xyz,abc,12345.
function find_next_line(s, pos)Works with multiline text s. Lines are separated by EOL characters - \n or \r\n.
Returns two values: the starting position of the current line and the starting position of the next line (or the end of the text s if no more lines remain).
Raw string handling
hex
function string2hex(s)
function has_nonprintable(s)
function make_readable(s)
function str_or_hex(s)
function hexdump(s, max)
function hexdump_dlog(s)string2hexconverts a raw string into a character-based hex representation. Bytes are separated by spaces."\xAB\xCD\x01\x02"=>"AB CD 01 02"has_nonprintablereturnstrueif stringscontains characters other than 0x20–0x7F,\n,\r, or\t.make_readablereplaces all characters except those in the 0x20–0x7F range with dots.str_or_hexreturns the string itself ifhas_nonprintable(s)isfalse; otherwise, it returnsstring2hex(s).hexdumpconverts the initial bytes of raw strings(up tomaxbytes) into a hex string plus the result ofmake_readable. This is a classic hex dump.hexdump_dlogperforms ahexdumpand outputs the result to the debug log.
pattern
function pattern(pat, offset, len)pattern represents a segment of an infinitely repeating raw string pat, starting at position offset (1-based indexing) with a length of len.
blob
function blob(desync, name[, def])
function blob_or_def(desync, name[, def])blobis the standard function for retrieving a blob. Ifnamestarts with0x, the remainder is interpreted as a HEX string. Otherwise, the variablenameis read first fromdesync. If not found there, it is taken from global variables. If it still cannot be found, the valuedefis used. Ifnameisnilor an empty string, an error is raised.blob_or_defreturnsdefifnameisnil; otherwise, it behaves identically toblob.
function barray(a, packer)
function btable(a, packer)barrayuses only numeric indices starting from 1. The order is preserved.btableuses all indices but does not guarantee order.
Packs elements of array a in ascending order of index from 1 to the last.packer is a function that takes an element of a and returns a raw string.
For numeric arrays, number packing functions can be used as the packer.
Returns a raw string.
TCP sequence number handling
function seq_ge(seq1, seq2)
function seq_gt(seq1, seq2)
function seq_lt(seq1, seq2)
function seq_le(seq1, seq2)
function seq_within(seq, seq_low, seq_hi)
function is_retransmission(desync)seq_{ge|gt|lt|le}performs a comparison of sequence numbers within a 2 GB range. If the difference is larger, the result will be incorrect.gestands for>=,gtfor>,lefor<=, andltfor<.seq_withinchecks ifseq_low <= seq <= seq_hi.is_retransmissionchecks if the current TCP dissection is a retransmission.
Position handling
function pos_counter_overflow(desync, mode[, reverse])
function pos_get_pos(track_pos, mode)
function pos_get(desync, mode[, reverse])
function pos_check_from(desync, range)
function pos_check_to(desync, range)
function pos_check_range(desync, range)
function pos_range_str(range)
function pos_str(desync, pos)The mode parameter contains a single-letter string representing the counter mode: 'a', 'x', 'n', 'd', 'b', 's', or 'p'.
By default, these functions operate on the current direction. If the reverse parameter is provided and set to true, the opposite direction is used.
- pos_counter_overflow - true if mode is 's' or 'p' and the relative TCP sequence has exceeded 2 GB. The counters can no longer be used.
- pos_get_pos - retrieve the value of the 'mode' counter from the
track_postable.track_poscan bedesync.track.pos.{direct,reverse,client,server}. - pos_get - retrieve the 'mode' counter value for the current or opposite direction.
- pos_check_from - check if the current position satisfies the lower bound of the range.
- pos_check_to - check if the current position satisfies the upper bound of the range.
- pos_range - check if the current position satisfies the range (both lower and upper bounds).
- pos_str - convert a position table
posinto the standard string format<mode>, for example,s100. - pos_range_str - convert a range table into the standard string format
<mode_from>(-|<)<mode_to>, for example,d1-p5000.
Dissection
Dissection is the process of parsing a message to represent it in a structured format.
dissect_url
function dissect_url(url)Returns a table containing the parsed parts of a URL in the format proto://creds@domain:port/uri.
If any part is missing, the corresponding field will not be present in the table.
Example of parsing https://user:[email protected]:12345/my_uri/script.php?a=1&b=3
.proto
string https
.creds
string user:pass
.domain
string domain.com
.port
string 12345
.uri
string /my_uri/script.php?a=1&b=3
dissect_nld
function dissect_nld(domain, level)Retrieves a domain of a specific level from the domain string. For example, level=2 for 'www.microsoft.com' returns 'microsoft.com'.
If the specified level does not exist, it returns nil.
dissect_http
function http_dissect_req(http)
function http_dissect_reply(http)
function http_reconstruct_req(hdis[, unixeol])Parses an HTTP request or response. The http parameter is a multi-line string.
The parsed result is a table with nested sub-tables.
Headers provide the start and end positions for both the header name and the value itself.
To find a header by name use array_field_search with field name "header_low" which contains header name in lower case.
The HTTP request reconstructor takes a parsed table and recreates the raw string. The unixeol parameter replaces the standard HTTP line ending (0D0A) with 0A. This is non-standard and will break almost all servers except for Nginx.
Example of parsing an HTTP request http://testhost.com/testuri
.uri
string /test_uri
.headers
.1
.header
string Content-Length
.header_low
string content-length
.value
string 330
.pos_start
number 43
.pos_end
number 61
.pos_header_end
number 56
.pos_value_start
number 59
.2
.header
string Host
.header_low
string host
.value
string testhost.com
.pos_start
number 24
.pos_end
number 41
.pos_header_end
number 27
.pos_value_start
number 30
.method
string GET
Example of parsing an HTTP response
.code
number 200
.headers
.1
.pos_header_end
number 28
.pos_value_start
number 31
.header
string Content-Type
.header_low
string content-type
.value
string text/html
.pos_start
number 17
.pos_end
number 39
.2
.pos_header_end
number 54
.pos_value_start
number 57
.header
string Content-Length
.header_low
string content-length
.value
string 650
.pos_start
number 41
.pos_end
number 59
dissect_tls
function tls_dissect(tls, offset, partialOK)
function tls_reconstruct(tdis)Parsing and reconstruction of TLS. Capabilities of these functions:
- Any TLS handshake without a TLS record (including Client/Server Hello). For example, data extracted from
desync.decrypt_datain QUIC. - Any TLS records: Handshake, Certificate, Change Cipher Spec, and others.
- Handshakes split across multiple TLS records (e.g., the result of
tpws --tlsrec). - (Dissect only) Incomplete data blocks if
partialOK=true. The system recovers as much as possible, though a full reassembly will not be feasible. - All handshakes are moved to a separate table. A dissection is performed for Client/Server Hello; others are kept as raw data fields.
- TLS extensions from Client/Server Hello: server name, ALPN, supported versions, compress certificate, signature algorithms, delegated credentials, supported groups, EC point formats, PSK key exchange modes, key share, and QUIC transport parameters. Other extensions are not parsed and are kept as raw data fields.
- If a record layer is present, reconstruction is performed according to the lengths of individual records. If the final segment does not fit, the TLS record is expanded to accommodate the remaining data.
- If no changes are made, the dissect + reconstruct process yields binary-identical blobs.
The functions do not work with DTLS.
tls_dissect returns a table - a parsed raw TLS string starting from the specified offset (1-indexed). reconstruct_dissect returns the raw string of the assembled tdis parsing. In case of an error, nil is returned.
The simplest way to obtain a dissection sample is: --payload=tls_client_hello --lua-desync=luaexec:code="var_debug(tls_dissect(desync.reasm_data))".
Then, initiate a TLS request.
Example of a TLS dissection from a request to https://google.com
.rec
.1
.ver
number 769
.type
number 22
.name
string handshake
.len
number 512
.encrypted
boolean false
.htype
number 1
.data
string 01 00 01 FC 03 03 87 36 D1 0E 19 78 8F 8B 41 5E 05 74 92 EF E7 9D 3E 83 F3 9D F4 C4 C6 6C 3E DC 5A 8C EF FD BC B4 20 1C AF 31 7A EB D2 FD 8B 1F C6 E8 DB CF 02 28 93 C4 AE 13 E1 17 ED 62 D8 3D 2F DE 03 67 A1 1A 44 00 3C 13 02 13 03 13 01 C0 2C C0 30 00 9F CC A9 CC A8 CC AA C0 2B C0 2F 00 9E C0 24 C0 28 00 6B C0 23 C0 27 00 67 C0 0A C0 14 00 39 C0 09 C0 13 00 33 00 9D 00 9C 00 3D 00 3C 00 35 00 2F 01 00 01 77 FF 01 00 01 00 00 00 00 0F 00 0D 00 00 0A 67 6F 6F 67 6C 65 2E 63 6F 6D 00 0B 00 04 03 00 01 02 00 0A 00 16 00 14 00 1D 00 17 00 1E 00 19 00 18 01 00 01 01 01 02 01 03 01 04 00 10 00 0E 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31 00 16 00 00 00 17 00 00 00 31 00 00 00 0D 00 30 00 2E 04 03 05 03 06 03 08 07 08 08 08 1A 08 1B 08 1C 08 09 08 0A 08 0B 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02 00 2B 00 05 04 03 04 03 03 00 2D 00 02 01 01 00 33 00 26 00 24 00 1D 00 20 E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D 00 15 00 AE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.handshake
.1
.dis
.ver
number 771
.type
number 1
.name
string client_hello
.cipher_suites
.1
number 4866
.2
number 4867
.3
number 4865
.4
number 49196
.5
number 49200
.6
number 159
.7
number 52393
.8
number 52392
.9
number 52394
.10
number 49195
.11
number 49199
.12
number 158
.13
number 49188
.14
number 49192
.15
number 107
.16
number 49187
.17
number 49191
.18
number 103
.19
number 49162
.20
number 49172
.21
number 57
.22
number 49161
.23
number 49171
.24
number 51
.25
number 157
.26
number 156
.27
number 61
.28
number 60
.29
number 53
.30
number 47
.compression_methods
.1
number 0
.ext
.1
.type
number 65281
.name
string renegotiation_info
.data
string 00
.2
.dis
.list
.1
.name
string google.com
.type
number 0
.type
number 0
.name
string server_name
.data
string 00 0D 00 00 0A 67 6F 6F 67 6C 65 2E 63 6F 6D
.3
.dis
.list
.1
number 0
.2
number 1
.3
number 2
.type
number 11
.name
string ec_point_formats
.data
string 03 00 01 02
.4
.dis
.list
.1
number 29
.2
number 23
.3
number 30
.4
number 25
.5
number 24
.6
number 256
.7
number 257
.8
number 258
.9
number 259
.10
number 260
.type
number 10
.name
string supported_groups
.data
string 00 14 00 1D 00 17 00 1E 00 19 00 18 01 00 01 01 01 02 01 03 01 04
.5
.dis
.list
.1
string h2
.2
string http/1.1
.type
number 16
.name
string application_layer_protocol_negotiation
.data
string 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31
.6
.type
number 22
.name
string encrypt_then_mac
.data
string
.7
.type
number 23
.name
string extended_master_secret
.data
string
.8
.type
number 49
.name
string post_handshake_auth
.data
string
.9
.dis
.list
.1
number 1027
.2
number 1283
.3
number 1539
.4
number 2055
.5
number 2056
.6
number 2074
.7
number 2075
.8
number 2076
.9
number 2057
.10
number 2058
.11
number 2059
.12
number 2052
.13
number 2053
.14
number 2054
.15
number 1025
.16
number 1281
.17
number 1537
.18
number 771
.19
number 769
.20
number 770
.21
number 1026
.22
number 1282
.23
number 1538
.type
number 13
.name
string signature_algorithms
.data
string 00 2E 04 03 05 03 06 03 08 07 08 08 08 1A 08 1B 08 1C 08 09 08 0A 08 0B 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02
.10
.dis
.list
.1
number 772
.2
number 771
.type
number 43
.name
string supported_versions
.data
string 04 03 04 03 03
.11
.dis
.list
.1
number 1
.type
number 45
.name
string psk_key_exchange_modes
.data
string 01 01
.12
.dis
.list
.1
.group
number 29
.kex
string E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D
.type
number 51
.name
string key_share
.data
string 00 24 00 1D 00 20 E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D
.13
.type
number 21
.name
string padding
.data
string 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.session_id
string 1C AF 31 7A EB D2 FD 8B 1F C6 E8 DB CF 02 28 93 C4 AE 13 E1 17 ED 62 D8 3D 2F DE 03 67 A1 1A 44
.random
string 87 36 D1 0E 19 78 8F 8B 41 5E 05 74 92 EF E7 9D 3E 83 F3 9D F4 C4 C6 6C 3E DC 5A 8C EF FD BC B4
.type
number 1
.name
string client_hello
.data
string 01 00 01 FC 03 03 87 36 D1 0E 19 78 8F 8B 41 5E 05 74 92 EF E7 9D 3E 83 F3 9D F4 C4 C6 6C 3E DC 5A 8C EF FD BC B4 20 1C AF 31 7A EB D2 FD 8B 1F C6 E8 DB CF 02 28 93 C4 AE 13 E1 17 ED 62 D8 3D 2F DE 03 67 A1 1A 44 00 3C 13 02 13 03 13 01 C0 2C C0 30 00 9F CC A9 CC A8 CC AA C0 2B C0 2F 00 9E C0 24 C0 28 00 6B C0 23 C0 27 00 67 C0 0A C0 14 00 39 C0 09 C0 13 00 33 00 9D 00 9C 00 3D 00 3C 00 35 00 2F 01 00 01 77 FF 01 00 01 00 00 00 00 0F 00 0D 00 00 0A 67 6F 6F 67 6C 65 2E 63 6F 6D 00 0B 00 04 03 00 01 02 00 0A 00 16 00 14 00 1D 00 17 00 1E 00 19 00 18 01 00 01 01 01 02 01 03 01 04 00 10 00 0E 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31 00 16 00 00 00 17 00 00 00 31 00 00 00 0D 00 30 00 2E 04 03 05 03 06 03 08 07 08 08 08 1A 08 1B 08 1C 08 09 08 0A 08 0B 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02 00 2B 00 05 04 03 04 03 03 00 2D 00 02 01 01 00 33 00 26 00 24 00 1D 00 20 E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D 00 15 00 AE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Successfully parsed elements are presented as dis subtables, while others remain as raw data fields.
Some elements include a name field for visual analysis purposes only; the type fields are the primary identifiers.
To locate values within lists, use the array search functions.
Numerous TLS-related constants are defined in zapret-lib.lua. Before hardcoding values, check if a suitable constant already exists.
The handshake table is indexed by handshake type. The most common types are TLS_HANDSHAKE_TYPE_CLIENT and TLS_HANDSHAKE_TYPE_SERVER. Since these have values 1 and 2 respectively, it might appear that handshake elements follow a sequential order starting from 1, but this is not the case.
Extensions and other lists are indexed numerically starting from 1 rather than by type. This is because their sequence matters and multiple elements of the same type may exist.
When adding custom data, you must reproduce the minimum required structure. You can choose to populate only the raw data field; if no dis subtable is present, this field will be used during reconstruction. If a dis subtable exists, it must be correctly populated according to the specific data element.
The following code example searches for the SNI extension within the tdis dissect. If missing, it inserts it at the beginning and then adds the domain "example.com".```
local idx_sni = array_field_search(tdis.handshake[TLS_HANDSHAKE_TYPE_CLIENT].dis.ext, "type", TLS_EXT_SERVER_NAME)
if not idx_sni then
table.insert(tdis.handshake[TLS_HANDSHAKE_TYPE_CLIENT].dis.ext, 1, { type = TLS_EXT_SERVER_NAME, dis = { list = {} } } )
idx_sni = 1
end
table.insert(tdis.handshake[TLS_HANDSHAKE_TYPE_CLIENT].dis.ext[idx_sni].dis.list, { name = "example.com", type = 0 } )Working with L3 and L4 protocol elements
find_tcp_options
function find_tcp_option(options, kind)Returns the first element of dis.tcp.options with the specified kind. Returns nil if not found.
ip6hdr
function find_ip6_exthdr(exthdr, proto)Returns the first element of dis.ip6.exthdr where type = proto.
function insert_ip6_exthdr(ip6, idx, header_type, data)
function del_ip6_exthdr(ip6, idx)
function fix_ip6_next(ip6, last_proto)ip protocol
function ip_proto_l3(dis)
function ip_proto_l4(dis)
function ip_proto(dis)Functions discover ip protocol of the end payload.
- ip_proto_l3 - ipv4 - ip.ip_p , ipv6 - ip6.ip6_nxt or next from the last extension header. nil, if next field is not set.
- ip_proto_l4 - IPPROTO_TCP, IPPROTO_UDP, IPPROTO_ICMP, IPPROTO_ICMPV6 depending on presence of tcp,udp,icmp,ip6. nil if tcp,udp,icmp are absent.
- ip_proto - ip_proto_l4. If it returned nil, then ip_proto_l3.
function fix_ip_proto(dis, proto)Set end protocol as proto. If proto is not passed or nil - use result of "ip_proto(dis)".
packet_len
These functions work with the IPv6 header dissect ip6 and its extension headers (ip6.exthdr).
When inserting or deleting extension headers, the correct chain of subsequent protocols is maintained, starting from the base IPv6 header.
insert_ip6_exthdrinserts an extension header with the protocolheader_typeanddatainto theip6dissect at the specified indexidx. Ifidx=nil, it is appended to the end. Thedatasize must be 6+N4 forIPPROTO_AHand 6+N8 for others; otherwise, errors will occur during reconstruction.del_ip6_exthdrremoves the extension header at the specified indexidx.fix_ip6_nextrestores the correct chain of subsequent protocols usingip6.ip6_nxtand thetypefields withinip6.exthdr.
function l3_base_len(dis)
function l3_extra_len(dis[, ip6_exthdr_last_idx])
function l3_len(dis)
function l4_base_len(dis)
function l4_extra_len(dis)
function l4_len(dis)
function l3l4_extra_len(dis)
function l3l4_len(dis)
function packet_len(dis)Calculates the sizes of various dissect elements after reconstruction.
l3_base_len– Base length of the IP/IPv6 header without options or extension headers.l3_extra_len– Length of IP options or the total length of all extension headers. Ifip6_exthdr_last_idxis specified, it counts extension headers up to that index.l3_len– Total length of the IP/IPv6 header including options and extension headers.l4_base_len– Base length of the TCP or UDP header.l4_extra_len– Length of TCP options for TCP; 0 for UDP.l4_len– Total length of the TCP header with options, or the UDP header length.l3l4_extra_len– Sum ofl3_extra_lenandl4_extra_len.l3l4_len– Total length of IP/IPv6 and TCP headers, including all options and extension headers.packet_len– Total length of the reconstructed packet, including the L4 payload.
Working with hostnames
genhost
function genhost(len[, template])Generates a random hostname of length len.
- If a
templateis provided, it generates a random subdomain to fit the specifiedlen. If the lengthlenis insufficient, it returns a left-truncated version of the template. - If
template=nil, it generates a random subdomain for one of the known 3-letter TLDs. Iflen < 7, it generates a random domain of lengthlenwithout dots.
Examples:
-- template "google.com", len=16 : h82aj.google.com
-- template "google.com", len=11 : .google.com
-- template "google.com", len=10 : google.com
-- template "google.com", len=7 : gle.com
-- no template, len=6 : b8c54a
-- no template, len=7 : u9a.edu
-- no template, len=10 : jgha7c.comhost_ip
function host_ip(desync)
function host_or_ip(desync)host_ipreturns a string representation ofdesync.target.ipordesync.target.ip6.host_or_ipreturnsdesync.track.hostnameif bothtrackandtrack.hostnameexist; otherwise, it returnshost_ip(desync).
File name and path operations
function is_absolute_path(path)
function append_path(path, file)
function writeble_file_name(filename)is_absolute_pathreturns true if the path starts from the root. It accounts for CYGWIN path specifics.append_pathappends a file or directory namefiletopath, using '/' as a separator.writeble_file_namereturnsfilenameif it contains an absolute path or if theWRITABLEenvironment variable is not set. Otherwise, it retrieves the path from theWRITABLEenvironment variable and appends thefilenameusingappend_path.
Reading and writing Files
function readfile(filename)Reads the entire file. Throws an error if opening or reading the file fails.
function z_readfile(filename[, expected_ratio])Automatically detects whether the file is gzipped. If so, it decompresses it; otherwise, it reads it as is. Throws an error if opening or reading the file fails.expected_ratio is the expected ratio of decompressed data to compressed data (defaults to 4).
function writefile(filename, data)Writes data to a file. Throws an error if opening the file fails.
Data compression
function is_gzip_file(filename)Returns true if the file is a gzip file, otherwise false. Throws an error if the file cannot be opened.
function gunzip_file(filename[, expected_ratio[, read_block_size]])Decompresses a file and returns it as a raw string. Throws an error if opening or reading the file fails. Returns nil in case of memory exhaustion. read_block_size determines the chunk size for reading (defaults to 16K).expected_ratio is the expected ratio of decompressed data to compressed data (defaults to 4).
function gzip_file(filename, data[, expected_ratio[, level[, memlevel[, compress_block_size]]]])Compresses a raw string into a gzip file. Throws an error if opening or reading the file fails. Returns nil if the gzip data is corrupted or memory is exhausted.level is the compression level from 1 to 9 (defaults to 9), memlevel is the allowed memory usage level from 1 to 8 (defaults to 8). compress_block_size is the chunk size for compression (defaults to 16K).expected_ratio is the expected ratio of decompressed data to compressed data (defaults to 2).
autottl
function parse_autottl(s)
function autottl(incoming_ttl, attl)/* Detailed source-code truncated for AI context efficiency. */
function apply_ip_id(desync[, dis[, ipid_options[, def]]])Applies the ip_id policy from ipid_options to the dissect dis.
If dis is nil, desync.dis is used.
If ipid_options is nil, desync.arg is used.def contains the default assignment mode. If nil, "seq" is applied.
apply_fooling
function apply_fooling(desync[, dis[, fooling_options]])Applies a set of L3/L4 header modifications (fooling), as described in fooling_options, to the dissect dis.
If dis is nil, desync.dis is used.
If fooling_options is nil, desync.arg is used.
ipfrag2
function ipfrag2(dis, ipfrag_options)The standard fragmenter function. It returns an array of two fragment dissects derived from the original dissect dis.
It is invoked via rawsend_dissect_ipfrag if the ipfrag field is missing in ipfrag_options.
You are unlikely to need to call this function manually.
If you need to split IP packets differently, you can create your own fragmenter by analogy and specify it in ipfrag_options.
In the case of IPv6, the fragment header is inserted after all hopbyhop, routing, and the first destopt headers. This constitutes the "unfragmentable part."
The fragment header follows, and everything after it is considered the "fragmentable part."
The unfragmentable part is transmitted in every fragment with modified fragment header fields; the rest is sliced across the data block following the fragment header according to the fragment offset.
According to the standard, for IPv6 fragmentation, the "next protocol" is only read from the first fragment (where offset=0).
In subsequent fragments, it does not have to match and is ignored. Manipulating the "next protocol" field of subsequent fragments is a well-known penetration attack technique described in various security articles, allowing one to bypass certain firewalls.ipfrag2 implements this capability for two fragments via the ipfrag_next parameter.
Some firewalls can only be bypassed using a larger number of fragments-this would require a custom fragmenter function.
wssize_rewrite
function wsize_rewrite(dis, arg)Rewrite dis.tcp.th_win and the scale factor in TCP options within the dissect, if present. Increasing the scaling factor is blocked.
- arg: wsize - window size
- arg: scale - scale factor
- returns true if any changes were made
dis_reverse
function dis_reverse(dis)Swap the source and destination IP addresses and ports, as well as the seq and ack numbers.
IP addresses and interfaces
function update_ifaddrs()A wrapper around the C function get_ifaddrs. It may happen that you need to find the addresses on each packet.
Calling get_ifaddrs every time is CPU intensive. Addresses and interfaces rarely change.
update_ifaddrs() maintains the cache, which is updated no more than once per second.
The result is stored in the global variable "ifaddrs".
function ip2ifname(ip)Get the name of the interface on which the IP address is present using the ifaddrs cache. nil if not found.
Sending
The following functions can accept multiple blocks of the options described above, each represented by the options parameter field.
All functions utilize options.reconstruct and options.rawsend. These correspond to the parameter format of the C function rawsend_dissect.
rawsend_dissect_ipfrag
function rawsend_dissect_ipfrag(dis[, options])Sends dissect dis with IP fragmentation as specified in options.ipfrag. If omitted, it is sent without fragmentation.
Uses a custom fragmenter function if options.ipfrag.ipfrag is provided.
Sends fragments in reverse order if options.ipfrag.ipfrag_disorder is specified.
rawsend_dissect_segmented
function rawsend_dissect_segmented(desync[, dis[, mss[, options]]])Sends dissect dis with automatic TCP segmentation based on MSS, applying options.fooling and options.ipid.
The ipid is applied to each fragment. Segmentation is not possible for UDP and is not performed.
- if dis is nil, desync.dis is used.
- if mss is nil, desync.tcp_mss is used.
- if options is nil, options are created from desync.arg
rawsend_payload_segmented
function rawsend_payload_segmented(desync[, payload[, seq[, options]]])Constructs a temporary dissect based on desync.dis, with optional payload replacement and optional seq offset, applying options, and sends it via rawsend_dissect_segmented.
The MSS is taken from desync.tcp_mss.
If options are missing, they are created based on desync.arg.
Standard options are formed as follows:
ipfrag,ipid, andfoolingtake their values fromdesync.arg.rawsend:repeatsis taken fromdesync.arg.repeats;ifoutandfwmarkare taken fromdesync.argif present, otherwise fromdesync(the context passed to the desync function).reconstruct: onlydesync.arg.badsumis used; other options are ignored.
Standard direction and payload filters
function direction_check(desync[, def])
function direction_cutoff_opposite(ctx, desync[, def])The direction filter is a string ("in", "out", or "any") passed via desync.arg.dir. If the argument is missing, the def value is used.
direction_checkverifies if the current direction matches the filter.direction_cutoff_oppositeperforms an instance cutoff on the current direction if it does not match the filter.
function payload_match_filter(l7payload[, l7payload_filter[, def]])
function payload_check(desync[, def])These functions operate on a string representing a comma-separated list of payloads. All empty packets have the payload empty, and unknown ones are unknown. Special values include all and known. all means any payload; known means anything that is not unknown or empty. A ~ prefix at the beginning denotes logical inversion (non-match).
payload_match_filterchecks ifl7payloadmatches thel7payload_filterlist, ordefifl7payload_filterisnil. If both arenil, the list defaults to "known".payload_checkcallspayload_match_filter(desync.l7payload, desync.arg.payload, def).
Working with multi-packet payloads
Typically, operations are performed on the entire reasm rather than its individual parts. This is the purpose of reassembly: to avoid dealing with separate packets and instead process the entire message at once.
The standard scenario involves processing after receiving the first part of a replay and either ignoring or dropping the remaining parts. The choice between ignoring or dropping may depend on the success of actions involving reasm. For example, whether or not a segmented reasm was successfully sent. If successful, all other parts must be dropped; otherwise, they will be sent as duplicates in the original segmentation. If an error occurs and the segmented packets could not be sent, dropping the rest would prevent the full message from reaching the recipient, leading to retransmissions. In such cases, it is better to leave them as is - this way nothing breaks.
function replay_first(desync)
function replay_drop_set(desync, v)
function replay_drop(desync)replay_firstreturns true if the current dissect is not a replay or is its first part.replay_drop_setmarks a boolean flagvindesync.track.lua_stateindicating whether subsequent parts of a replay should be dropped.replay_dropreturns true if the current part of the replay needs to be dropped. If the part is the last one, it automatically clears the flag.
These functions work correctly with both replays and regular dissects. For regular dissects, replay_first is always true, replay_drop_set does not change the flag, and replay_drop is always false.
Timer helpers
function dis_timer_name(dis)Construct timer name based on ip src and dst addresses, l4 protocol name, port numbers or icmp codes.
May not be unique.
function desync_timer_name(desync)Construct timer name that includes instance name, dis_timer_name result, conntrack packet number.
If track is absent random characters are added to the end.
This name can be considered unique per desync+instance and can be used as an oneshot timer name.
Orchestration
This group includes functions that support the orchestration and shimming processes.
Shims are duplicates of C code functions for situations where we lack the ctx context required to communicate with the C code.
Once orchestration begins, all subsequent instances are called by the orchestrator or nested orchestrators.
The last available ctx is the one from the primary orchestrator. If this ctx is passed to other instances, they will act on behalf of the orchestrator rather than themselves; therefore, ctx=nil should be passed to them.
After an execution plan is canceled, the C code stops servicing subsequent instances and does not provide a ctx for them. Consequently, if execution needs to continue in the standard style, duplicate mechanisms for instance cutoff and range/payload filters are required.
To ensure --lua-desync functions work transparently under orchestration, standard shims must be used instead of direct C function calls that require a ctx. For nested orchestrators to function correctly, you must follow the standard scheme of storing the execution plan in desync.plan and use the helper functions described below.
It may be difficult to understand how orchestration works from a dry description alone. It is recommended to study the code of actual orchestrators and use this description to clarify the meaning of specific actions.
instance_cutoff_shim
function instance_cutoff_shim(ctx, desync[, dir])Performs a standard instance cutoff in the direction dir if ctx is present; otherwise, it performs the cutoff via a duplicate mechanism whose state is stored in desync.track.lua_state. dir = true for the outgoing direction, dir = false for incoming, and dir = nil for both directions.
cutoff_shim_check
function cutoff_shim_check(desync)Checks the instance cutoff state for desync.func_instance in the desync.outgoing direction.
apply_arg_prefix
function apply_arg_prefix(desync)Performs substitution of argument values from desync.arg that start with %, #, \.
Multiple calls are safe but do not update desync.arg if blobs were changed.
apply_execution_plan
function apply_execution_plan(desync, instance)Copies the instance identification and its arguments from an execution plan instance into the desync object, thereby recreating the desync state as if the instance were called directly by C code.
With one exception : apply_arg_prefix is not applied because args can refer a blob created by previous conditionally executed instances.
The execution plan is provided by the C function execution_plan() as an array of instance elements.
verdict_aggregate
function verdict_aggregate(v1, v2)Aggregates verdicts v1 and v2. VERDICT_MODIFY overrides VERDICT_PASS, while VERDICT_DROP overrides both.
plan_instance_execute
function plan_instance_execute(desync, verdict, instance)
function plan_instance_execute_preapplied(desync, verdict, instance)Executes an execution plan instance, taking into account the instance cutoff and standard payload and range filters.
Calls apply_arg_prefix right before calling the instance.
Returns the aggregation of the current verdict and the instance verdict.
The "preapplied" version does not apply execution plan, allowing the calling code to do so.
Sometimes, to decide whether to call an instance, you need a desync table configured for the called instance.
"preapplied" version allows to avoid double copying.
plan_instance_pop
function plan_instance_pop(desync)Retrieves, removes, and returns the first element of the execution plan from desync.plan. Returns nil if there are no elements.
plan_clear
function plan_clear(desync, max)Clears up to the "max" instances if "max" is defined or the whole execution plan in desync.plan.
orchestrate
function orchestrate(ctx, desync)If the orchestrator is the primary one (i.e., ctx is present), it retrieves the execution plan, places it into desync.plan, and then executes execution_plan_cancel().
If ctx=nil, it does nothing, assuming the plan is already in desync.plan.
replay_execution_plan
function replay_execution_plan(desync, max)/* Detailed source-code truncated for AI context efficiency. */
function drop(ctx, desync)- arg: standard direction
- arg: standard payload
- By default,
payload=allanddirection=any, meaning it drops everything.
Issues a VERDICT_DROP if the filter conditions are met.
send
function send(ctx, desync)- arg: standard direction
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: delay - packet send delay in msec
- Default
ip_idmode isnone.
Sends the current dissect with optional modifications applied.
If delay is specified packet data and send options are remembered. After specified time packet is sent out.
The function returns VERDICT_PASS - the sending of the current dissect is not canceled. If you want to cancel, use the drop instance.
pktmod
function pktmod(ctx, desync)- arg: standard direction
- arg: standard fooling
- arg: standard ipid
Applies modifications to the current dissect without sending it or issuing a verdict.
HTTP fooling
http_hostcase
function http_hostcase(ctx, desync)- arg: standard direction
- arg: spell - the exact spelling of the header. Defaults to "host".
Changes the case of the Host: HTTP header.
http_domcase
function http_domcase(ctx, desync)- arg: standard direction
Changes the case of the domain within the Host: header. The case alternates for every character: rUtRaCkEr.oRg.
http_methodeol
function http_methodeol(ctx, desync)- arg: standard direction
Inserts \r\n before the method, stripping the last two characters from the User-Agent: header content. This only works with Nginx; it breaks other servers.
If used with other http tampering functions should be the last !
http_unixeol
function http_unixeol(ctx, desync)- arg: standard direction
Replaces the 0D0A line endings with 0A. The difference in length is compensated for by adding spaces to the end of the User-Agent header. This only works with Nginx; it breaks other servers.
Window size replacement
wsize
function wsize(ctx, desync)- arg: wsize - TCP window size.
- arg: scale - scaling factor. Replaced in the TCP option if present. Only reduction is allowed; increasing the factor is blocked.
Changes tcp.th_win and/or the scaling factor in the TCP SYN,ACK packet, then executes an instance cutoff. If the modification is successful, it sets VERDICT_MODIFY.
The goal of this technique is to spoof the window size from the client or server side so that the peer responds by sending the next packet in parts, as the full packet would not fit into the spoofed window size. This may lead to performance slowdowns. This is an obsolete technique; it is recommended to use it only from the server side as a last resort for clients that do not employ any other bypass methods. From the client side, TCP segmentation techniques are preferred as they avoid speed penalties and offer more flexibility.
wssize
function wssize(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function syndata(ctx, desync)- arg: standard fooling
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: blob - a blob containing the fake payload. It must fit into a single packet; segmentation is not possible.
- arg: tls_mod - apply the specified tls_mod to the blob payload.
The function adds a payload to the TCP SYN packet, applies modifications to it, and sends it instead of the original, issuing a VERDICT_DROP.
If a non-SYN packet passes through, instance cutoff is executed.
Thus, the manipulation is performed on all SYN retransmissions, after which the function stops.
It is a phase-zero strategy that works with hostlists only in --ipcache-hostname mode.
tls_client_hello_clone
function tls_client_hello_clone(ctx, desync)- arg: standard direction
- arg: blob - the name of the blob that receives the result
- arg: fallback - the name of the blob to be copied to the result if the payload is of the wrong type or invalid
- arg: sni_del_ext - delete the SNI extension; other parameters are ignored
- arg: sni_del - delete all hosts
- arg: sni_snt - replace the "server name type" field for all existing hosts
- arg: sni_snt_new - the "server name type" field for newly added hosts
- arg: sni_first - add a host to the beginning of the list
- arg: sni_last - add a host to the end of the list
Prepares a blob with the specified name in the desync table, filled with the result of modifying the current reasm.
Works only with TCP and the tls_client_hello payload. If an SNI modification is specified and the SNI extension is missing, it is added to the beginning of the extensions list.
Order of operations:
- sni_del_ext. All other SNI operations become irrelevant and will not be executed.
- sni_del
- sni_snt
- sni_first
- sni_last
This function does not affect traffic by itself; it only prepares data for other functions.
fake
function fake(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: blob - a blob containing the fake payload. It can be of any length-segmentation is performed automatically.
- arg: optional - abort the operation if the blob is missing.
- arg: tls_mod - apply the specified tls_mod to the blob payload.
- default payload filter - "known"
This is a direct fake-a separate packet or group of packets. The function does not issue a verdict and does not block the transmission of the original packet.
rst
function rst(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: rstack - send RST,ACK instead of RST.
- default payload filter - "known"
Sends an empty TCP packet with RST or RST+ACK flags. The function does not issue a verdict and does not block the transmission of the original packet.
TCP segmentation
multisplit
function multisplit(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function multidisorder(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function multidisorder_legacy(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - a comma-separated list of markers - split points. Default is "2".
- arg: seqovl - marker - an offset relative to the current sequence to create an additional part of the segment extending to the left.
- arg: optional - skip the operation if a blob is specified but missing. If seqovl_pattern is specified but missing, use the 0x00 pattern.
- arg: seqovl_pattern - the blob used to fill the seqovl. Default is 0x00.
A multidisorder implementation fully compatible with nfqws1.
The new multidisorder works with the entire reasm (reassembly), whereas the legacy nfqws1 version works with individual replay segments. Consequently, the sequence of parts will differ when handling multi-packet requests.
In the new version, the original segmentation is not preserved. If any split segment exceeds the MSS, it is further divided by the MSS and sent in ascending sequence order. In the legacy version, the original segmentation was preserved. Split points were normalized based on the offset of each individual reasm part. Segments were sent in reverse order only within each part, while the subsequent part followed with its original sequence increment. Similarly, seqovl was normalized and applied only within the original segment where the normalized position was located.
fakedsplit
function fakedsplit(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function fakeddisorder(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function hostfakesplit(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function tcpseg(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - a list of two markers defining the boundaries of the TCP segment
- arg: seqovl - a number representing the offset relative to the current sequence to create an additional segment part that extends to the left beyond the TCP window boundary
- arg: seqovl_pattern - a blob used to fill the seqovl. Defaults to 0x00
- arg: blob - replace the current payload with the specified blob
- arg: optional - skip the operation if a blob is specified but missing. If
seqovl_patternis specified but missing, use the 0x00 pattern. - default payload filter - "known"
Sends a part of the current dissect, reasm, or an arbitrary blob, limited by two pos markers with optional application of the seqovl technique in the same way as in multisplit. Additional segmentation is performed automatically if the MSS is exceeded.
In the case of reasm, it only works when receiving its first part (as it operates on the reasm as a whole, not its individual parts).
No verdict is issued.
Using tcpseg, you can perform seqovl without segmentation by using markers "0,-1". To replace the current dissect, it can be combined with drop.
oob
function oob(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function udplen(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: min - do not touch packets with an L4 payload length smaller than this
- arg: max - do not touch packets with an L4 payload length larger than this
- arg: increment - how much to increase (+) or decrease (-) the L4 payload length
- arg: pattern - the blob used to fill the end of the packet when increasing the length
- arg: pattern_offset - initial offset within the pattern
- default payload filter - "known"
The function increases or decreases the length of the UDP L4 payload. When decreasing, part of the information is truncated and lost; when increasing, the extra space is filled with the pattern. UDP segmentation is impossible - if the MTU or PMTU is exceeded, the packet will be fragmented by OS on IP level. An error in case of exceeding the MTU will only be reported on Linux; other systems will silently fail to send the packet (WinDivert and ipdivert have no means of error detection).
dht_dn
function dht_dn(ctx, desync)- arg: standard direction
- arg: dn - the number N following 'd' in a DHT message
DHT uses the bencode format for transmitting messages. 'd' represents the directory data type. DHT messages typically start with 'd1' or 'd2' and end with 'e' (end). Some DPIs have these exact signatures hardcoded-only 'd1' or 'd1'+'d2'. However, one can use 'd3', 'd4', etc., if the content is edited correctly without violating the bencode format. This is what this function does. It only works on payloads with the "dht" type.
Other Functions
synack
function synack(ctx, desync)- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
Sends a SYN,ACK packet before the SYN to confuse the DPI regarding the TCP connection direction. This attack is referred to in literature as "TCB turnaround." It breaks NAT-usage through NAT is impossible. Usage on transit traffic requires nftables and POSTNAT mode. After a non-SYN packet passes, it performs an instance cutoff. It does not issue a verdict.
synack_split
function synack_split(ctx, desync)/* Detailed source-code truncated for AI context efficiency. */
function automate_conn_record(desync)Returns a table serving as automation state storage bound to the stream. Automation functions can utilize any fields within it.
standard_hostkey
function standard_hostkey(desync)- arg: reqhost - require a hostname; do not operate via IP.
- arg: nld - the domain level number to which the hostname is truncated. If not specified, no truncation occurs.
A standard host-key generator. This function calculates a key string associated with the hostname or the IP address if the hostname is unavailable. If it fails to generate a key, it returns nil.
automate_host_record
function automate_host_record(desync)- arg: key - a key within the global
autostatetable. If not specified, the current instance name is used as the key. - arg: hostkey - the name of the host-key generator function. If not specified, standard_hostkey is used.
Returns a table serving as automation state storage bound to a specific host or IP address. It utilizes two keys: key and hostkey. The resulting location is autostate.key.hostkey. If the hostkey cannot be retrieved, it returns nil.
Handling successes and failures
A simple, though not entirely precise, explanation of "success" and "failure" is "the site opens" versus "the site does not open." In automation logic, these states are abstract. Any process can result in a success or a failure. Detecting these states is necessary for tracking failures via a counter. The counter increments on failure and resets on success. When a target threshold is reached, a flag is returned, allowing external logic to perform an action-such as switching strategies.
automate_failure_counter
function automate_failure_counter(hrec, crec, fails, maxtime)- hrec - host storage
- crec - stream storage
- fails - the target number of failures.
- maxtime - the time in seconds after which, since the last failure, the next failure restarts the count from scratch.
Returns true if the counter reaches the fails value. The counter is reset upon returning true.
automate_failure_counter_reset
function automate_failure_counter_reset(hrec)- hrec - host storage
Resets the failure counter value.
Success and failure detection
Success and failure detectors are swappable functions that take desync and crec as parameters and return true if a success or failure is detected.
automate_failure_check
function automate_failure_check(desync, hrec, crec)- hrec - host storage
- crec - stream storage
- arg: success_detector - the name of the success detector function. Defaults to
standard_success_detectorif not specified. - arg: failure_detector - the name of the failure detector function. Defaults to
standard_failure_detectorif not specified. - arg: fails - the target failure counter value. Default is 3.
- arg: maxtime - the maximum time in seconds between failures before the counter resets. Default is 60 seconds.
This function maintains the failure counter by invoking the success and failure detectors. It returns true if the counter reaches the target value. The counter resets automatically in this case.
standard_success_detector
Standard success and failure detectors require inbound and outbound traffic to be redirected in the volume necessary to trigger their criteria.
For relative sequence detection, packets up to the specified rseq plus the maximum payload length of one packet (1460 bytes for TCP) are required.
function standard_success_detector(desync, crec)- crec - stream storage
- arg: maxseq - the outgoing relative sequence threshold for marking a success. Default is 32768. Purpose: Sufficient data has been sent without the stream stalling due to blocking.
- arg: inseq - the incoming relative sequence threshold for marking a success. Default is 4096. Purpose: The peer has sent enough data to ensure it is not a DPI-generated response.
- arg: udp_out, udp_in - more than
udp_inUDP packets received, provided thatudp_out> 0. Purpose: The peer has sent enough data to ensure it is not a DPI-generated response.
Standard success detector.
standard_failure_detector
function standard_failure_detector(desync, crec)- crec - stream storage
- arg: maxseq - count retransmissions within outgoing relative sequences from 1 to
maxseq. Default is 32768. - arg: retrans - consider it a failure if there are at least
retransretransmissions. Default is 3. - arg: reset - send an RST to the retransmitter to terminate long wait times.
- arg: inseq - treat RSTs and HTTP redirects within incoming relative sequences from 1 to
inseqas failures. Default is 4096. - arg: no_rst - do not treat RST as a failure.
- arg: no_http_redirect - do not treat HTTP redirects as failures.
- arg: udp_out, udp_in - treat as a failure if >=
udp_outpackets are sent but <=udp_inpackets are received. Purpose: We are sending a lot, but receiving little or no response.
Standard failure detector.
DPI-based HTTP redirects are defined the same way as in autohostlists.
Orchestrators
circular
function circular(ctx, desync)- arg: standard host storage
- arg: standard checker
- arg: (standard detector only) standard success detector
- arg: (standard detector only) standard failure detector
This orchestrator tracks failures and cycles through strategies once the failure counter reaches the fails target. All subsequent instances are labeled with a "strategy" argument containing the strategy number, starting from 1. Instances without a "strategy" argument are not called by circular. Strategy numbers must be continuous from 1 to the last; gaps are not allowed and will trigger an error. If any instance of strategy N includes a "final" argument, that strategy becomes the last one-further cycling is blocked.
Usage example:
--lua-desync=circular:fails=4:retrans=2:maxseq=16384
--lua-desync=argdebug:v=1.1:strategy=1
--lua-desync=argdebug:v=1.2:strategy=1
--lua-desync=argdebug:v=2.1:strategy=2
--lua-desync=argdebug:v=3.1:strategy=3:final
--lua-desync=argdebug:v=3.2:strategy=3
--lua-desync=argdebug:v=3.3:strategy=3In this example, debug output is used instead of real strategies to trace circular operations in the debug log. There are 3 strategies: the 1st strategy includes 2 instances, the 2nd includes 1, and the 3rd includes 3. The 3rd strategy is final; returning to the 1st is blocked. To test this, try accessing blocked sites via curl to observe the failure detector's operation.
repeater
function repeater(ctx, desync)- arg: instances - how many subsequent instances to repeat.
- arg: repeats - the number of repetitions.
- arg: stop - do not execute the instances following "instances" as a standalone pass.
- arg: clear - clear the execution plan after repetitions.
- arg: iff - the name of the condition function for continuing the repetition cycle. If not specified, the condition is always true.
- arg: neg - invert the
iffvalue. Default is false.
As the name suggests, the repeater orchestrator repeats the subsequent instances a specified number of repeats. The repetition follows the pattern 1-2-3-1-2-3-1-2-3-4-5-6. In this example, 4-5-6 are the instances following the first three, assuming instances=3. If stop or clear is specified, 4-5-6 are not called. The clear flag additionally clears the execution plan, which is useful for interacting with higher-level orchestrators.
The iff function allows you to define an additional dynamic condition for continuing the repetition loop. If xor(iff, neg) = false, the loop is terminated.
repeater supports arbitrary levels of nesting.
In the example below, the resulting call sequence is: 1 1 1 2 2 2 1 1 1 2 2 2 3.
The stop parameter in nested instances prevents the execution of instances that do not belong to their own repetition cycle.
The first repeater is not restricted by stop, so it proceeds to execute 3.
--lua-desync=repeater:repeats=2:instances=4
--lua-desync=repeater:repeats=3:stop --lua-desync=argdebug:v=1
--lua-desync=repeater:repeats=3:stop --lua-desync=argdebug:v=2
--lua-desync=argdebug:v=3condition
function condition(ctx, desync)- arg:
iff- name of the iff function - arg:
neg- invert theiffvalue; defaults tofalse - arg:
instances- how many following instances to execute conditionally. all if not defined.
condition calls iff. If iff xor neg = true, all instances in the plan are executed; otherwise, the plan is cleared.
per_instance_condition
function per_instance_condition(ctx, desync)- arg:
instances- how many following instances to execute conditionally. all if not defined.
All following instanced are called only if they have "cond" argument with the "iff" function name and it returns true. The "cond_neg" argument inverts "cond" result.
Names are not iff/neg to avoid conflict with other orchestrators.
stopif
function stopif(ctx, desync)- arg:
iff- name of the iff function - arg:
neg- invert theiffvalue; defaults tofalse
stopif calls iff. If iff xor neg = true, the plan is cleared; otherwise, no action is taken.
stopif is useful as a nested orchestrator. For example, it can be used with circular to block strategy execution under certain conditions. condition is unsuitable for this purpose because it has no awareness of higher-level orchestrators or the "strategy" parameter, and would simply execute instances indiscriminately until the end. stopif clears the plan, thereby halting further execution of the parent orchestrator.
iff functions
These are used across several orchestrators and take desync as a parameter.
They can contain any logic programmable in Lua. The base set includes several iff functions for demonstration and testing purposes.
cond_true
function cond_true(desync)Always returns true.
cond_false
function cond_false(desync)Always returns false.
cond_random
function cond_random(desync)- arg:
percent- the probability of returningtrue. Defaults to 50.
Returns true randomly based on the percent probability; otherwise returns false.
cond_payload_str
function cond_payload_str(desync)- arg:
pattern- the string to search for in the payload.
Returns true if the substring pattern is present in desync.dis.payload.
This is a basic signature detector. If the C code does not recognize the protocol you need, you can write your own signature detector and run subsequent instances under a condition orchestrator using your detector as the iff function.
cond_tcp_has_ts
function cond_tcp_has_ts(desync)Returns true if the dissect is tcp and has tcp timestamp option.
cond_lua
function cond_lua(desync)Executes a Lua code from the "cond_code" argument. The code returns condition value. Direct addressing of the desync table is possible within the code.
desync.arg is passed without called "apply_arg_prefix" : %, #, \ remain as is without substitution because can refer blobs created by previous conditionally executed instances.
Auxiliary programs
ip2net
The ip2net utility is designed to convert IPv4 or IPv6 address lists into a list of subnets to reduce the overall list size. It reads from stdin and outputs to stdout.
-4 ; list is ipv4 (default)
-6 ; list is ipv6
--prefix-length=min[-max] ; range of prefix lengths to consider. e.g., 22-30 (ipv4), 56-64 (ipv6)
--v4-threshold=mul/div ; ipv4: include subnets where at least mul/div addresses are filled. e.g., 3/4
--v6-threshold=N ; ipv6: minimum number of IPs required to create a subnetThe list may contain entries in the format ip/prefix and ip1-ip2. These entries are passed to stdout without modification.
They are accepted by the ipset command. For hash:net lists, ipset can generate optimal ip/prefix coverage from ip1-ip2 ranges.
FreeBSD's ipfw supports ip/prefix but does not support ip1-ip2.ip2net filters input data, discarding invalid IP addresses.
A subnet is selected if it contains a specified minimum number of addresses.
For IPv4, the minimum is set as a percentage of the subnet size (mul/div, e.g., 3/4); for IPv6, the minimum is specified directly.
The subnet size is determined by the following algorithm:
First, the tool searches within the specified prefix length range for subnets that contain the maximum number of addresses.
If multiple such subnets are found, the smallest one (the one with the longest prefix) is chosen.
For example, given the parameters v6_threshold=2 and prefix_length=32-64, and the following IPv6 addresses:
1234:5678:aaaa::5
1234:5678:aaaa::6
1234:5678:aaac::5
Result:
1234:5678:aaa8::/45These addresses are also part of a /32 subnet. However, there is no point in using "blanket coverage" when the exact same addresses fit perfectly into a /45 and their count remains the same.
If v6_threshold is changed to 4, the result will be:
1234:5678:aaaa::5
1234:5678:aaaa::6
1234:5678:aaac::5In this case, the IPs will not be merged into a subnet because there are too few of them.
If prefix_length=56-64 is used, the result will be:
1234:5678:aaaa::/64
1234:5678:aaac::5The CPU time required for calculations depends heavily on the width of the prefix length range, the size of the target subnets, and the length of the list.
If ip2net takes too long, avoid using excessively large subnets and narrow the prefix length range.
Note that mul/div arithmetic uses integers. If the 32-bit integer limit is exceeded, the results are unpredictable.
Avoid using values like 5000000/10000000; 1/2 is much more efficient.
mdig
This program is designed for multithreaded resolving of large lists via the system DNS.
It reads a list of domains from stdin and outputs the resolution results to stdout. Errors are sent to stderr.
--family=<4|6|46> ; select IP address family: ipv4, ipv6, ipv4+ipv6
--threads= ; number of threads. Default is 1.
--eagain=<eagain_retries> ; number of retries after EAI_AGAIN. Default is 10.
--eagain-delay=<ms> ; wait time in ms between EAI_AGAIN retries. Default is 500.
--verbose ; debug log to console
--stats=N ; output statistics every N domains
--log-resolved=<file> ; save successfully resolved domains to a file
--log-failed=<file> ; save failed domain resolutions to a file
--dns-make-query=<domain> ; output a binary DNS query for the domain to stdout. If --family=6, the query will be AAAA, otherwise A.
--dns-parse-query ; parse a binary DNS response and output all IPv4 and IPv6 addresses from it to stdoutThe --dns-make-query and --dns-parse-query parameters allow you to resolve a single domain through an arbitrary channel.
For example, you can perform a DoH (DNS over HTTPS) request using only mdig and curl as follows:
mdig --family=6 --dns-make-query=rutracker.org | curl --data-binary @- -H "Content-Type: application/dns-message" https://cloudflare-dns.com/dns-query | mdig --dns-parse-query/* Detailed source-code truncated for AI context efficiency. */
config dnsmasq
list server '77.88.8.88#1253'/etc/config/network
config interface 'wan'
option peerdns '0'/* Detailed source-code truncated for AI context efficiency. */
> testing iana.org on its original
!!!!! AVAILABLE !!!!!
> testing rutracker.org on 192.0.43.8 (iana.org)
curl: (28) Operation timed out after 1002 milliseconds with 0 bytes received
> testing iana.org on 172.67.182.196 (rutracker.org)
HTTP/1.1 409 Conflict
> testing iana.org on 104.21.32.39 (rutracker.org)
HTTP/1.1 409 Conflict
> testing iana.org on its original ip
!!!!! AVAILABLE !!!!!
> testing rutracker.org on 192.0.43.8 (iana.org)
curl: (28) Connection timed out after 1001 milliseconds
> testing iana.org on 172.67.182.196 (rutracker.org)
curl: (35) OpenSSL/3.2.1: error:0A000410:SSL routines::ssl/tls alert handshake failure
> testing iana.org on 104.21.32.39 (rutracker.org)
curl: (35) OpenSSL/3.2.1: error:0A000410:SSL routines::ssl/tls alert handshake failure
> testing iana.org on its original ip
!!!!! AVAILABLE !!!!!
> testing rutracker.org on 192.0.43.8 (iana.org)
HTTP/1.1 307 Temporary Redirect
Location: https://www.gblnet.net/blocked.php
> testing iana.org on 172.67.182.196 (rutracker.org)
HTTP/1.1 409 Conflict
> testing iana.org on 104.21.32.39 (rutracker.org)
HTTP/1.1 409 Conflict
> testing iana.org on its original ip
!!!!! AVAILABLE !!!!!
> testing rutracker.org on 192.0.43.8 (iana.org)
curl: (35) Recv failure: Connection reset by peer
> testing iana.org on 172.67.182.196 (rutracker.org)
curl: (35) OpenSSL/3.2.1: error:0A000410:SSL routines::ssl/tls alert handshake failure
> testing iana.org on 104.21.32.39 (rutracker.org)
curl: (35) OpenSSL/3.2.1: error:0A000410:SSL routines::ssl/tls alert handshake failureExample of full IP block or TCP port block in the absence of domain blocking
* port block tests ipv4 startmail.com:80
ncat -z -w 1 145.131.90.136 80
145.131.90.136 does not connect. netcat code 1
ncat -z -w 1 145.131.90.152 80
145.131.90.152 does not connect. netcat code 1
* curl_test_http ipv4 startmail.com
- checking without DPI bypass
curl: (28) Connection timed out after 2002 milliseconds
UNAVAILABLE code=28
- IP block tests (requires manual interpretation)
> testing iana.org on its original ip
!!!!! AVAILABLE !!!!!
> testing startmail.com on 192.0.43.8 (iana.org)
HTTP/1.1 302 Found
Location: https://www.iana.org/
> testing iana.org on 145.131.90.136 (startmail.com)
curl: (28) Connection timed out after 2002 milliseconds
> testing iana.org on 145.131.90.152 (startmail.com)
curl: (28) Connection timed out after 2002 millisecondsStandard tests
standard test
This is the primary test, consisting of a series of subtests located in blockcheck2.d/standard. If you do not need certain tests, you can copy the standard directory to my and keep only the necessary scripts there. The def.in file is mandatory.
It accepts several additional variables:
MIN_TTL - minimum TTL
MAX_TTL - maximum TTL. 0 disables TTL tests
MIN_AUTOTTL_DELTA - minimum negative autottl delta
MAX_AUTOTTL_DELTA - maximum negative autottl delta. 0 disables AUTOTTL tests
FAKE_REPEATS - number of fake repeats
FOOLINGS46_TCP - space-separated list of TCP foolings for IPv4+IPv6
FOOLINGS6_TCP - space-separated list of specific TCP foolings for IPv6
FAKE_HTTP - path to the HTTP fake file
FAKE_HTTPS - path to the HTTPS fake file
FAKED_PATTERN_HTTP - path to the fakedsplit/fakeddisorder pattern file for HTTP
FAKED_PATTERN_HTTPS - path to the fakedsplit/fakeddisorder pattern file for HTTPS
FAKE_QUIC - path to the QUIC fake file
SEQOVL_PATTERN_HTTP - path to the seqovl pattern file for HTTP
SEQOVL_PATTERN_HTTPS - path to the seqovl pattern file for HTTPS
MULTIDISORDER=multidisorder_legacy - replace multidisorder with the version from nfqws1
NOTEST_BASIC_HTTP=1 - disable 10-http-basic.sh tests
NOTEST_MISC_HTTP=1 - disable http 15-misc.sh tests
NOTEST_MISC_HTTPS=1 - disable https 15-misc.sh tests
NOTEST_MULTI_HTTP=1 - disable http 20-multi.sh tests
NOTEST_MULTI_HTTPS=1 - disable https 20-multi.sh tests
NOTEST_SEQOVL_HTTP=1 - disable http 23-seqovl.sh tests
NOTEST_SEQOVL_HTTPS=1 - disable https 23-seqovl.sh tests
NOTEST_SYNDATA_HTTP=1 - disable http 24-syndata.sh tests
NOTEST_SYNDATA_HTTPS=1 - disable https 24-syndata.sh tests
NOTEST_FAKE_HTTP=1 - disable http 25-fake.sh tests
NOTEST_FAKE_HTTPS=1 - disable https 25-fake.sh tests
NOTEST_FAKED_HTTP=1 - disable http 25-faked.sh tests
NOTEST_FAKED_HTTPS=1 - disable https 25-faked.sh tests
NOTEST_HOSTFAKE_HTTP=1 - disable http 35-hostfake.sh tests
NOTEST_HOSTFAKE_HTTPS=1 - disable https 35-hostfake.sh tests
NOTEST_FAKE_MULTI_HTTP=1 - disable http 50-fake-multi.sh tests
NOTEST_FAKE_MULTI_HTTPS=1 - disable https 50-fake-multi.sh tests
NOTEST_FAKE_FAKED_HTTP=1 - disable http 55-fake-faked.sh tests
NOTEST_FAKE_FAKED_HTTPS=1 - disable https 55-fake-faked.sh tests
NOTEST_FAKE_HOSTFAKE_HTTP=1 - disable http 60-fake-hostfake.sh tests
NOTEST_FAKE_HOSTFAKE_HTTPS=1 - disable https 60-fake-hostfake.sh tests
NOTEST_QUIC=1 - disable 90-quic.sh testscustom test
A simple tester that uses strategy lists from files. Strategies must be on separate lines; line breaks within a single strategy are not allowed. Separate lists are used for different protocols: list_http.txt, list_https_tls12.txt, list_https_tls13.txt, and list_quic.sh. These files support comments starting with #.
When defining parameters, keep in mind that they will be interpreted as shell arguments. Special characters must be escaped according to shell rules.
For example, leaving < unquoted across the entire parameter or incorrectly quoting the --luaexec=code=print("abc") parameter will result in an error. If your Lua code uses strings, it is best to enclose them in single quotes and wrap the entire parameter in double quotes.
Note that blockcheck2 will output strategy parameters without escaping.
The recommended way to use this is to copy it into its own subdirectory within blockcheck2.d and populate the .txt files with your tests. Then, select your custom test name from the dialog.
Summary
At the end of the test, all successful strategies are displayed for each domain and each IP protocol version. If more than one domain was tested, the intersection of successful strategies (those that worked for all) is also provided. However, this intersection can only be fully relied upon when using SCANLEVEL=force. Otherwise, strategies that might have worked for subsequent domains might not have been tested for the first one.
Shell variables
CURL - path to the curl executable
CURL_MAX_TIME - curl timeout in seconds
CURL_MAX_TIME_QUIC - curl timeout for QUIC. If not set, CURL_MAX_TIME is used
CURL_MAX_TIME_DOH - curl timeout for DoH servers
CURL_CMD=1 - display curl commands
CURL_OPT - additional curl options. `-k` to ignore certificates, `-v` for verbose protocol output
CURL_HTTPS_GET=1 - use the GET method instead of HEAD for HTTPS
DOMAINS - space-separated list of domains or domains with URIs to test
TEST - name of the test
IPVS=4|6|46 - IP protocol versions to test
ENABLE_HTTP=0|1 - enable plain HTTP test
ENABLE_HTTPS_TLS12=0|1 - enable HTTPS TLS 1.2 test
ENABLE_HTTPS_TLS13=0|1 - enable HTTPS TLS 1.3 test
ENABLE_HTTP3=0|1 - enable QUIC test
REPEATS - number of test attempts
PARALLEL=0|1 - enable parallel attempts. May overwhelm the site due to aggressive requests and lead to incorrect results
SCANLEVEL=quick|standard|force - scanning level
BATCH=1 - batch mode (no prompts or waiting for console input)
HTTP_PORT, HTTPS_PORT, QUIC_PORT - port numbers for the respective protocols
SKIP_DNSCHECK=1 - skip DNS check
SKIP_IPBLOCK=1 - skip port or IP blocking tests
PKTWS_EXTRA_POST - additional nfqws/dvtws/winws parameters added after the main strategy
PKTWS_EXTRA_POST_1 .. PKTWS_EXTRA_POST_9 - separate additional parameters that contain spaces
PKTWS_EXTRA_PRE - additional nfqws/dvtws/winws parameters added before the main strategy
PKTWS_EXTRA_PRE_1 .. PKTWS_EXTRA_PRE_9 - separate additional parameters that contain spaces
DNSCHECK_DNS - space-separated list of third-party DNS servers for DNS spoofing tests
DNSCHECK_DOM - space-separated list of domains for DNS spoofing tests
SECURE_DNS=0|1 - force enable or disable DoH
DOH_SERVERS - space-separated list of DoH URLs for automatic selection of a working server
DOH_SERVER - specific DoH URL (disables auto-selection)
UNBLOCKED_DOM - an unblocked domain used for IP block tests
SIMULATE=1 - enable simulation mode for debugging script logic. Disables real curl requests and replaces them with random results.
SIM_SUCCESS_RATE= - simulation success probability as a percentage/* Detailed source-code truncated for AI context efficiency. */
#!/bin/sh
. /opt/zapret2/init.d/openwrt/functions
#. /opt/zapret2/init.d/sysv/functions
create_ipset no-update/* Detailed source-code truncated for AI context efficiency. */
alloc_dnum()
# $1 - name of the variable to which the daemon number is assigned
alloc_qnum()
# $1 - name of the variable to which the queue number is assignedFunctions for obtaining dynamic numbers from the pool are necessary to prevent conflicts between different startup scripts. A queue number is a unique value; two instances cannot bind to the same queue, as the second one will fail with an error. A daemon number is required for PID tracking. If numbers overlap, start/stop/restart operations will not function correctly.
These functions should be called from the main body of the script, not from within a function. Custom scripts are always executed in alphabetical order-this is the standard scheme for ".d" directories in Unix. Given the same set of scripts, the same qnum and dnum values will always be returned for each script during both startup and shutdown. This allows them to be used as unique identifiers without colliding with other scripts. If the composition of custom scripts is changed after startup, this rule is broken, which will lead to issues. Therefore, it is best not to modify the set of custom scripts while zapret2 is running.
Working with daemons
do_nfqws()
# $1 - 1 to start, 0 to stop
# $2 - daemon number
# $3 - nfqws2 parametersStarts or stops an nfqws2 instance. Base parameters are appended automatically. These include user selection, fwmark, and the connection of standard Lua scripts: zapret-lib.lua, zapret-antidpi.lua, and zapret-auto.lua.
You must specify the queue number yourself using --qnum.
filter_apply_hostlist_target()
# $1 - name of the variable containing nfqws2 optionsReplaces the <HOSTLIST> and <HOSTLIST_NOAUTO> markers in $1 depending on the MODE_FILTER and the presence of list files in ipset.
standard_mode_daemons()
# $1 - 1 to start, 0 to stopStarts or stops the standard instance of nfqws2.
Any config variables can be overridden locally.
Working with iptables
fw_nfqws_post()
fw_nfqws_pre()
# $1 - 1 to enable, 0 to disable
# $2 - iptables filter for IPv4
# $3 - iptables filter for IPv6
# $4 - queue numberEnables or disables redirection rules to the queue for nfqws2. Filters are written separately for IPv4 and IPv6 because they may contain protocol-specific elements that would cause errors on the other IP version.
"post" refers to the chain for outgoing traffic, while "pre" refers to incoming traffic.
$FW_EXTRA_PRE is prepended to the rules, and $FW_EXTRA_POST is appended.
What should NOT be included in the filter:
- Checking standard exclude ipsets:
nozapret,nozapret6. - Checking by DESYNC_FWMARK.
Whether rules are applied for each IP version depends on the config settings.
zapret_do_firewall_standard_tpws_rules_ipt()
# $1 - 1 to enable, 0 to disableApplies or removes iptables rules for the standard nfqws2 instance.
You can locally override config variables and apply FW_EXTRA_POST and FW_EXTRA_PRE.
filter_apply_ipset_target()
# $1 - name of the variable containing iptables filter rules
# $2 - name of the variable containing ip6tables filter rulesAdds checks for the standard zapret/zapret6 sets in the outgoing direction (dst) to variables $1 and $2.
reverse_nfqws_rule_stream()
# stdin - iptables or ip6tables filter rules
reverse_nfqws_rule()
# $@ - iptables or ip6tables filter rulesSwaps iptables filter elements from the forward to the reverse direction (changes dst to src). The result is sent to stdout.
ipt()
ipta()
ipt_del()
ipt6()
ipt6a()
ipt6_del()
# $@ - iptables rule
ipt_add_del()
ipta_add_del()
ipt6_add_del()
ipt6a_add_del()
# $1 - 1 to add, 0 to delete
# $2+ - all other arguments define the iptables ruleA set of functions for managing iptables rules. All functions that add rules first check for the rule's existence; if it is already present, it will not be added. This is necessary to prevent rule duplication. All rules are prefixed with $FW_EXTRA_PRE and suffixed with $FW_EXTRA_POST. The suffix "6" indicates IPv6 support; its absence indicates IPv4.
All rules begin with the chain name, omitting iptables commands such as -A, -D, -I, etc.
ipt- iptables -I - prepend to the beginning.ipta- iptables -A - append to the end.ipt_del- iptables -D - delete.ipt_add_del- add via -I or delete.$1: 1 for addition, 0 for deletion. The rule itself starts from$2.ipta_add_del- same as above, but uses -A instead of -I.
ipt_first_packets()
# $1 - number of packets or the string "keepalive"Outputs to stdout: -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes $RANGE. RANGE is defined as "1:$1". If $1 is "keepalive", nothing is output (no connbytes filter).
ipt_port_ipset()
# $1 - ipset name
# $2 - comma separated port or port range list. ranges are port1-port2Creates "bitmap:port" ipset filled with the supplied port list. If the set already exists it's elements are replaced.
Working with nftables
nft_fw_nfqws_post()
nft_fw_nfqws_pre()
# $1 - nftables filter for ipv4
# $2 - nftables filter for ipv6
# $3 - queue numberEnables or disables redirection rules to the queue for nfqws2. Filters are written separately for IPv4 and IPv6 because they may contain version-specific elements that would trigger an error on the other IP version.
"post" refers to the chain for outgoing traffic, "pre" for incoming traffic. The chain type is selected based on the POSTNAT variable. If you need a specific chain, you can override POSTNAT for a single call or as a local variable within the function.
Rules are prefixed with $FW_EXTRA_PRE and suffixed with $FW_EXTRA_POST.
What should NOT be included in the filter:
- Checks for standard exclude ipsets -
nozapret,nozapret6. - Checks based on DESYNC_FWMARK.
Whether rules are applied for each IP version depends on the config settings.
zapret_do_firewall_standard_tpws_rules_nft()Applies nftables rules for the standard nfqws2 instance.
You can locally override config variables and apply FW_EXTRA_POST and FW_EXTRA_PRE.
nft_filter_apply_ipset_target()
# $1 - name of the variable containing nftables filter rules for ipv4
# $2 - name of the variable containing nftables filter rules for ipv6Adds checks for standard zapret/zapret6 sets in the outbound direction (dst) to variables $1 and $2.
nft_reverse_nfqws_rule()
# $@ - nftables filter ruleReverses nftables filter elements from the forward to the reverse direction - replaces dst with src. The result is output to stdout.
nft_add_chain()
# $1 - chain name
# $2 - parameters inside { }
nft_delete_chain()
# $1 - chain name
nft_create_set()
# $1 - set name
# $2 - parameters inside { }
nft_del_set()
# $1 - set name
nft_flush_set()
# $1 - set name
nft_set_exists()
# $1 - set name
nft_add_set_element()
# $1 - set or map name
# $2 - element
nft_add_set_elements()
# $1 - set or map name
# $2,$3,... - elements
nft_flush_chain()
# $1 - chain name
nft_add_rule()
# $1 - chain name
# $2+ - nftables rule
nft_insert_rule()
# $1 - chain name
# $2+ - nftables ruleThese functions operate on the zapret2 table, allowing you to avoid direct nft calls with hardcoded table names. The names are self-explanatory.
nft_first_packets()
# $1 - number of packets or "keepalive"/* Detailed source-code truncated for AI context efficiency. */
ln -s /opt/zapret2/init.d/openwrt/zapret2 /etc/init.d
/etc/init.d/zapret2 enable- Updating firewall rules on interface up/down events.
ln -s /opt/zapret2/init.d/openwrt/90-zapret2 /etc/hotplug.d/iface- (fw3 only) Firewall integration.
ln -s /opt/zapret2/init.d/openwrt/firewall.zapret2 /etc
uci add firewall include
uci set firewall.@include[-1].path="/etc/firewall.zapret"
uci set firewall.@include[-1].reload=1
uci commit- List updates - a cron job calling
/opt/zapret2/ipset/get_config.shat a random time during the night every 2 days to update lists. This assumes the router runs 24/7.
OpenWRT cheat sheet
- Start service:
/etc/init.d/zapret2 start - Stop service:
/etc/init.d/zapret2 stop - Restart service:
/etc/init.d/zapret2 restart - Service status:
/etc/init.d/zapret2 status - Disable autostart:
/etc/init.d/zapret2 disable - Enable autostart:
/etc/init.d/zapret2 enable
systemd integration principles
- Autostart
cp /opt/zapret2/init.d/systemd/zapret2.service /lib/systemd/system
systemctl daemon-reload
systemctl enable zapret2- List update timer - runs at a random time of day every 2 days. It is designed for any system, including desktops that may only be powered on during the day.
cp /opt/zapret2/init.d/systemd/zapret2-list-update.* /lib/systemd/system
systemctl daemon-reload
systemctl enable zapret2-list-update.timer
systemctl start zapret2-list-update.timersystemd cheat sheet
- Start service:
systemctl start zapret2 - Stop service:
systemctl stop zapret2 - Restart service:
systemctl restart zapret2 - Service status:
systemctl status zapret2 - Disable autostart:
systemctl disable zapret2 - Enable autostart:
systemctl enable zapret2
OpenRC integration principles
- Autostart
ln -s /opt/zapret2/init.d/openrc/zapret2 /etc/init.d
rc-update add zapret2- List updates - a cron job to call
/opt/zapret2/ipset/get_config.shat a random time during the day every 2 days. This is designed for any system, including desktops that may only be powered on during the day.
OpenRC cheat sheet
- Start service:
rc-service zapret2 start - Stop service:
rc-service zapret2 stop - Restart service:
rc-service zapret2 restart - Service status:
rc-service zapret2 status - Disable autostart:
rc-update del zapret2 - Enable autostart:
rc-update add zapret2
Alternative installation on systemd
On classic Linux distributions with systemd, you can use the provided template unit init.d/systemd/[email protected] to run nfqws2 instances.
cp /opt/zapret2/init.d/systemd/nfqws2\@.service /lib/systemd/systemsystemctl daemon-reloadmkdir /etc/zapret2- Create a text file in
/etc/zapret2containing the nfqws2 command-line parameters. The file should be namedINSTANCE.conf, whereINSTANCEis a name of your choice. - Enable autostart:
systemctl enable nfqws2@INSTANCE - Start:
systemctl start nfqws2@INSTANCE - Stop:
systemctl stop nfqws2@INSTANCE - Restart:
systemctl restart nfqws2@INSTANCE
This method does not apply iptables/nftables rules - you will have to handle that separately, as well as write the rules themselves. The rules must be placed somewhere so they are applied after the system starts. For example, you can create a separate systemd unit that runs a shell script or nft -f /path/to/file.nft.
Other firmwares
For static binaries, the host environment doesn't matter: PC, Android, set-top box, router, or any other device. Any firmware or Linux distribution will work; static binaries run on everything. They only require a kernel with the necessary build options or modules. However, in addition to binaries, the project uses scripts that rely on certain standard utilities.
Main reasons why you cannot simply install this system on any device out of the box:
- Lack of shell access to the device
- Lack of root privileges
- Absence of a r/w partition for writing and persistent file storage
- No way to add items to autostart
- Absence of cron
- Non-disableable flow offload or other proprietary netfilter implementations
- Missing kernel modules or specific kernel build options
- Missing iptables modules (
/usr/lib/iptables/lib*.so) - Missing standard utilities (like
ipset,curl) or their "stripped-down" (lightweight) versions - A limited or non-standard
shshell
If your firmware provides everything required, you can adapt zapret for your device to one degree or another.
Rebuilding the kernel or its modules will likely be quite difficult. To do this, you would at least need the source code for your firmware. User-mode components can be added relatively painlessly if there is a place to store them. For devices that have a r/w area, the Entware project exists. Some firmwares even offer a simplified installation of Entware via a web interface. Entware provides a repository of user-mode components that are installed in /opt. These can compensate for the lack of software in the base firmware, with the exception of the kernel.
You can attempt to use the SysV init script.
If the system reports missing basic programs, you should install them via Entware.
Before running the script, the path to these additional programs must be added to your PATH.
Note that Entware is patched to replace standard file paths like /etc/passwd with /opt/etc/passwd.
Zapret's static binaries are built without accounting for this, so the --user option may not work-the system looks for the user in /etc/passwd (which is on a read-only partition), while adduser adds users to /opt/etc/passwd.
Therefore, you may need to uncomment WS_USER in the config and specify a user that already exists in /etc/passwd.
For Keenetic devices, there are additional critical nuances; without addressing them, zapret will either crash every few minutes or fail to work with UDP.
There are ready-made third-party solutions available for Keenetic integration.
A detailed description of settings for other firmwares is beyond the scope of this project.
OpenWrt is one of the few relatively full-featured Linux systems for embedded devices.
It was chosen for this project based on the following characteristics:
- Full root access to the device via shell. This is usually absent in factory firmwares but present in many alternative ones.
- Read/write (r/w) root partition. This is a nearly unique feature of OpenWrt. Factory and most alternative firmwares are built on a squashfs root (r/o), where configurations are stored in a specially formatted area of internal memory called NVRAM. Systems without an r/w root are severely limited; they cannot install software from repositories without complex workarounds and are primarily designed for users who are only slightly more advanced than average, offering fixed functionality managed through a web interface. Alternative firmwares can usually mount an r/w partition to some area of the file system, while factory firmwares typically only mount USB drives and even then, they may only support FAT and NTFS rather than Unix file systems.
- The ability to move the root file system to external media (extroot) or create an overlay on it.
- The presence of the
opkgpackage manager and a software repository. - Flow offloading is predictably, standardly, and selectively manageable, as well as disableable.
- All kernel modules are available in the repository and can be installed via
opkg, eliminating the need to recompile the kernel. - All
iptablesmodules are available in the repository and can be installed viaopkg. - The repository contains a vast number of standard programs and additional software.
- The availability of an SDK, allowing you to compile any missing components.
Windows
zapret was initially written for unix. It uses unix emulation layer cygwin on Windows to help migration.
If winws2 is run standalone cygwin1.dll is required in it's directory. If winws2 is run inside cygwin environment - cygwin1.dll must NOT be present in it's directory or it won't run.
cygwin emulate shared PID namespace and deliver signals only within one cygwin1.dll instance !
To send signals sending program (kill, killall) must be run with the same cygwin1.dll as winws2.
It's possible to install cygwin and use winws2 inside cygwin installation.
But it may be more comfortable to use zapret-win-bundle which includes minimal cygwin.
cygwin prompt is pre-configured with aliases for blockcheck, blockcheck2, winws, winws2, winws2 with standard Lua scripts.
32-bit Windows versions are not supported by zapret-win-bundle.
Windows 7
Requirements for Windows driver signing have changed in 2021.
Official free updates of Windows 7 ended in 2020.
After 2020 for the years paid updates were available (ESU).
One of the updates from ESU enables signatures used in WinDivert 2.2.2-A.
There are several options :
Take
WinDivert64.sysandWinDivert.dllversion2.2.0-Cor2.2.0-Dfrom here and replace existing files.Use "BypassESU" patcher. Google it by name.
Use UpdatePack7R2. If you are in Russia or Belarus temporary change region in Control Panel.
Windows Server
To use --ssid-filter install wireless networking feature.
Run power shell as administrator and execute command Install-WindowsFeature -Name Wireless-Networking.
Then reboot the system.
Windows ARM64
The main problem is lack of a signed WinDivert driver. Therefore, it is required to enable the test signature mode: 'bcdedit/set {current} testsigning on'.
There's unsigned WinDivert64.sys driver in zapret-win-bundle.
There is also cmd file for rolling the driver to the arm64 Win11.
Another problem is lack of cygwin for non-x86 platforms. However, win11 has x64 emulation, so you can use the x64 build, but with replaced WinDivert64.sys driver.
There is no need to replace WinDivert.dll - only WinDivert64.sys needs to be replaced. It's verified and confirmed working.
There is no x64 emulation on win10 arm64, but there is x86 32-bit emulation.
Therefore, theoretically you can use the win32 variant and put the WinDivert64.sys driver for arm64 to the same dir.
This hasn't been verified.
File: docs/manual.md
Содержание
- Содержание
- Введение
- Структура проекта
- Схема обработки трафика
- Перехват трафика из ядра ОС
- nfqws2
- С интерфейс nfqws2
- Базовые константы
- Стандартные блобы
- Переменные окружения
- C функции
- Логгинг
- Конвертация IP
- Битовые операции
- Операции с беззнаковыми числами
- Целочисленное деление
- Генерация случайных данных
- Парсинг
- Криптография
- Компрессия
- Системные функции
- Опции по работе с пакетами
- Диссекция и реконструкция
- conntrack
- Получение ip адресов
- Прием и отсылка пакетов
- Работа с пейлоадами
- Управление выполнением инстансов
- Управление таймерами
- Библиотека базовых функций zapret-lib.lua
- Базовые desync функции
- Служебные функции
- Работа со строками
- Обслуживание raw string
- Обслуживание tcp sequence numbers
- Обслуживание позиций
- Диссекция
- Работа с элементами L3 и L4 протоколов
- Работа с именами хостов
- Операции с именами файлов и путями
- Чтение и запись файлов
- Компрессия данных
- autottl
- Операции с диссектами
- IP адреса и интерфейсы
- Отсылка
- Стандартные фильтры direction и payload
- Работа с многопакетными пейлоадам
- Помощь с таймерами
- Оркестрация
- Библиотека программ атаки на DPI zapret-antidpi.lua
- Библиотека программ автоматизации и оркестрации zapret-auto.lua
- Вспомогательные программы
- blockcheck2
- Скрипты запуска
- Другие прошивки
- Особенности Windows
Введение
zapret2 является пакетным манипулятором, основная задача которого - совершение различных автономных атак на DPI в реальном времени
с целью преодоления ограничений (блокировок) ресурсов или сетевых протоколов.
Однако, этим возможности zapret2 не ограничиваются. Архитектура позволяет выполнять и другие виды пакетных манипуляций.
Например, двусторонняя (клиент+сервер) обфускация протоколов с целью их сокрытия от DPI. Возможны и иные применения.
Структура проекта
Главный компонент zapret2 - программа nfqws2 (dvtws2 на BSD, winws2 на Windows), написанная на C, которая и является пакетным манипулятором.
Содержит функции по перехвату пакетов, базовой фильтрации, распознаванию основных протоколов и пейлоадов, поддержке хост и IP листов, автоматических хостлистов
с распознаванием блокировок, систему множественных профилей (стратегий), возможности по отсылке raw пакетов и другие сервисные функции.
Однако, там нет никаких возможностей собственно для воздействия на трафик. Это вынесено в код на языке Lua, вызываемый из nfqws2.
Поэтому следующая по важности часть проекта - Lua код. В базовый комплект входит библиотека функций-хелперов zapret-lib.lua, библиотека программ автономных атак на DPI zapret-antidpi.lua, библиотека функций принятия динамических решений (оркестрации) zapret-auto.lua.
Дополнительно присутствует набор тестов C функций zapret-tests.lua, средства обфускации трафика zapret-obfs.lua и средство записи дампа пакетов в cap файлы zapret-pcap.lua.
Проект может работать с LuaJIT-2.1+ или с PUC Lua 5.3+. Более старые версии могут иметь несовместимости, не тестируются и не поддерживаются.
Функции перенаправления трафика из ядра в Linux возложены на iptables и nftables, в FreeBSD - на ipfw, в OpenBSD - на pf, в Windows - встроены в сам процесс winws2 посредством драйвера WinDivert.
Схема перехвата трафика из ядра, nfqws2 и Lua код составляют минимально рабочее ядро проекта. Все остальное является дополнительным, второстепенным и опциональным.
Из второстепенных компонент - скрипты запуска под Linux - init.d, common, ipset, install_easy.sh, uninstall_easy.sh и средство автоматизации тестирования стратегий blockcheck2.
Цель скриптов запуска - согласовать процесс поднятия таблиц и запуск инстансов nfqws2, учесть особенности интеграции в различные дистрибутивы (openwrt, systemd, openrc).
Дополнительная функция - обеспечить поддержку и согласованное обновление различных листов и загрузку IP листов в пространство ядра - ipset.
Все это можно сделать при желании и собственными средствами, если так удобнее или функционал скриптов запуска не подходит.
Скрипты запуска выносят все настройки в файл config, лежащий в корне проекта. Этот конфиг относится только к ним, nfqws2 ничего о нем не знает.
Для обработки листов предусмотрены 2 программы, написанные на C. mdig - многопоточный ресолвер хостлистов неограниченного объема.
ip2net - программа для группировки отдельных IP адресов в подсети с целью сокращения их объема.
Эти программы используются в скриптах запуска и в blockcheck2.
Скрипты запуска и инсталлятор поддерживают установку на любые классические дистрибутивы Linux с systemd или openrc, из прошивок - на openwrt.
Если система не удовлетворяет указанным требованиям - возможна самостоятельная "доприкрутка" к системе.
macOS не поддерживается по причине отсутствия подходящего средства перехвата и управления пакетами. Стандартное для BSD средство ipdivert было убрано из ядра производителем.
Политика apple - все закрывать, ужесточать, вырезать низкоуровневые интерфейсы, вводить требования подписи. Из-за этого реализация собственного подходящего средства перехвата пакетов крайне затруднена.
Схема обработки трафика
Сети работают с IP пакетами. Поэтому единицей обработки являются именно они. Приемом и отправкой пакетов занимается сетевая подсистема в ядре ОС.
nfqws2 работает не в ядре (kernel mode), а является процессом пространства пользователя (user mode). Поэтому первая часть процесса обработки состоит
в передаче пакетов из ядра ОС в процесс nfqws2. Все 4 средства перехвата обладают некоторыми возможностями фильтрации пакетов. Максимальные возможности - в Linux.
Чем больше на этом этапе будет отсечено ненужного трафика, тем меньше будет нагрузка на процессор, поскольку передача пакетов из ядра в user mode и обратно сопряжена со значительными накладными расходами.
Пакет пришел в nfqws2. Первое, что он делает, это разбирает его по уровням OSI модели - выделяет ip , ipv6, tcp, udp заголовки и поле данных. Это называется диссекцией.
Результатом диссекции является диссект - представление пакета в виде структур, которые можно адресовать по полям.
Далее задействуется встроенная в nfqws2 подсистема conntrack - система отслеживания потоков поверх отдельно взятых пакетов.
Ищется уже имеющаяся запись о потоке на основании данных L3/L4 пакета. Если ее нет - создается. Старые записи, по которым давно нет активности, удаляются.
conntrack отслеживает логическое направление пакетов в потоке (входящее/исходящее), ведет подсчет количества прошедших пакетов и байт в обе стороны,
следит за sequence numbers для tcp. Он же используется в случае необходимости для сборки сообщений, передаваемых несколькими пакетами.
Сигнатурно определяется тип пейлоада - содержимого отдельно взятого пакета или группы пакетов. На основании типа пейлоада определяется тип протокола
всего потока, который сохраняется за потоком до конца его существования. В рамках одного потока могут проходить разные типы пейлоадов.
Например, протокол потока xmpp обычно несет несколько видов специфических для xmpp сообщений и сообщения, связанные с tls.
Тип протокола потока xmpp остается, но последующие пакеты получают различные типы пейлоада - как известные, так и неизвестные. Неизвестные пейлоады определяются как тип "unknown".
Если для конкретного пейлоада и типа протокола потока выясняется необходимость реконструкции сообщения из нескольких пакетов, nfqws2 начинает их накапливать в связи с записью в conntrack
и запрещает их немедленную отправку. После приема всех пакетов сообщения происходит реконструкция и при необходимости дешифровка составного пейлоада.
Дальнейшие решения принимаются уже на базе полностью собранного пейлоада - reasm или результата сборки и дешифровки - decrypt.
Когда необходимая информация о пейлоаде получена, наступает очередь системы классификации по профилям.
Профили содержат систему фильтров и команды действия внутри профиля.
Профили фильтруются по L3 - версия IP протокола, номер IP протокола, ipset-ы - списки IP адресов, L4 - порты tcp или udp, type/code для icmp, L6/L7 - тип протокола потока, списки доменов (хостлисты).
Профили сканируются строго в порядке от первого к последнему. При первом совпадении условий фильтра профиля выбирается этот профиль, а сканирование прекращается.
Если ни одно из условий не выполнено, выбирается профиль по умолчанию, в котором нет никаких действий.
Все дальнейшие действия выполняются уже в рамках выбранного профиля. Выбранный профиль кэшируется в записи conntrack, поэтому для каждого пакета поиск заново не выполняется.
Повторный поиск выполняется в случае изменения исходных данных - при обнаружении L7 протокола и при обнаружении имени хоста. В последних случаях производится повторный поиск
и при необходимости переключение профиля. Таких переключений может быть за время существования потока до двух, поскольку есть лишь 2 изменяемых параметра.
Профиль выбран. Из чего состоит его содержание, отвечающее за действия ?
За действия отвечают Lua функции. В профиле их может быть произвольное количество.
Каждый вызов Lua функции из профиля называется инстансом. Функция может быть одна, вызовов несколько - с разными параметрами.
Поэтому и применяется понятие инстанса - экземпляра вызываемой функции, который идентифицируется номером профиля и порядковым номером вызова внутри профиля.
Инстансы вызываются через параметры --lua-desync. Каждый инстанс получает набор произвольных параметров, задаваемых в --lua-desync.
Порядок вызовов имеет принципиальное значение для логики стратегии и выполняется строго в порядке задания параметров --lua-desync.
Присутствуют и внутрипрофильные фильтры. Их 3 типа - фильтр --payload - список принимаемых инстансом пейлоадов, и 2 диапазонных фильтра --in-range и --out-range,
позволяющих задать диапазон позиций внутри потока, который интересен для инстанса. Внутрипрофильные фильтры после их определения действуют на все последующие инстансы
до их переопределения. Главный смысл наличия внутрипрофильных фильтров - сократить число относительно медленных вызовов Lua , принимая максимум решений на стороне C кода.
Пакет пришел в Lua инстанс. Функция имеет 2 параметра - ctx и desync. ctx - это контекст для связи с некоторыми функциями на стороне C кода.
desync - таблица, содержащая множество параметров обрабатываемого пакета. Прежде всего это диссект - подтаблица dis.
Информация из записи conntrack - подтаблица track. Еще целый ряд параметров, который можно увидеть, выполнив var_debug(desync) или просто вызвав готовый инстанс pktdebug.
Если идет перепроигрывание задержанных пакетов (replay), Lua инстанс получает информацию о номере части, количестве частей исходного сообщения, позиции текущей части, reasm или decrypt при наличии.
Lua код может использовать глобальное пространство переменных для хранения данных, не относящихся к конкретному обрабатываемому пакету. Ему доступна таблица desync.track.lua_state,
в которой он может хранить любую информацию, связанную с записью conntrack. При каждом новом пакете потока в Lua выдается одна и та же таблица.
Таблицу desync можно использовать для генерации и хранения временных данных, актуальных в цепочке обработки текущего пакета.
Следующие Lua инстансы получают ту же таблицу desync и тем самым могут принимать данные от предыдущих инстансов.
Lua инстанс может создавать копии текущего диссекта, вносить в них изменения, генерировать собственные диссекты, отправлять их через вызовы C кода.
Итогом работы каждого инстанса является вердикт - VERDICT_PASS - не делать ничего с текущим диссектом, VERDICT_MODIFY - в конце всей цепочки отослать модифицированное содержимое диссекта,
VERDICT_DROP - дропнуть текущий диссект. Вердикты всех инстансов агрегируются - MODIFY замещает PASS, а DROP замещает и PASS, и MODIFY.
Lua инстанс может сам себя отключить от получения дальнейших пакетов потока по направлению in/out - это называется instance cutoff.
Может отключить направление in/out текущего потока от всей Lua обработки - lua cutoff.
Может запросить отмену всей дальнейшей цепочки вызовов Lua инстансов по текущему диссекту. Инстанс, принимающий такое решение, берет на себя функцию координации дальнейших действий.
Такой инстанс называется оркестратором. Он получает от C кода план дальнейшего выполнения со всеми фильтрами профиля и параметрами вызова всех оставшихся инстансов
и сам принимает решения когда и при каких условиях их вызывать или не вызывать, менять их параметры. Так реализуются динамические сценарии без модификации основных составляющих кода стратегии.
Например, определение блокировки ресурса и смена стратегии, если предыдущая не сработала.
Если все инстансы текущего профиля вошли в состояние cutoff по текущему потоку, либо текущая позиция потока находится за верхней границей range фильтров, значит по этому потоку больше не будет Lua
вызовов. C код помечает такие потоки специальным признаком "lua cutoff", который проверяется максимально быстро без вызовов кода Lua. Тем самым экономятся ресурсы процессора.
После выполнения всей цепочки инстансов профиля C код получает итоговый вердикт - что делать с текущим диссектом. Отправить как есть, отправить модифицированный вариант или дропнуть.
В конце nfqws2 переходит к ожиданию следующего пакета, и цикл повторяется вновь.
Перехват трафика из ядра ОС
Перехват трафика в ядре Linux
Осуществляется при помощи iptables или nftables с использованием механизма очередей NFQUEUE.
nftables - предпочтительны, потому что позволяют работать с трафиком после NAT, а iptables - нет.
Это важно при обработке проходящего (forwarded) трафика. На iptables перехват после NAT невозможен, поэтому некоторые воздействия, ломающие NAT,
на iptables на проходящем трафике нереализуемы.
У nftables есть один важный недостаток - чрезмерное требование к памяти при загрузке больших set-ов. Например, чтобы загрузить 100K IP адресов, требуется от 256-320 Mb, что для роутеров часто оказывается за пределом возможностей.
ipset от iptables такое может провернуть даже на 64 Mb RAM.
Если стоит выбор между iptables или nftables, то однозначно выбирайте nftables. Поддержка nftables в скриптах запуска сделана более качественно, а сама технология nftables гораздо более уживчива к "соседям" - правилам разных программ, поскольку использует отдельные таблицы. В iptables все намешано в кучу, и одни программы , задающие iptables, могут попортить правила других программ. iptables можно рассматривать как legacy вариант для совместимости , когда иных вариантов нет. В современном дистрибутиве Linux точно не стоит выбирать iptables. Но если Linux более старый, ядро старее 5.15 или nft старее 1.0.1, а возможности обновить их нет, тогда лучше iptables. Со старыми ядрами и старым nft будут проблемы.
Приведенные далее тестовые примеры предназначены для своей системы запуска или запуска вручную.
Скрипты запуска zapret сами генерируют необходимые правила, никаких iptables/nftables самому писать не нужно.
Перехват трафика с помощью nftables
Тестовая таблица для POSTNAT схемы.
Обеспечивает перехват первых входящих и исходящих пакетов по потоку после NAT, если таковой присутствует.
Из-за NAT IP адреса клиентов теряются, замещаясь IP wan интерфейса.
Количество первых пакетов регулируется согласно вашей стратегии. Лишний перехват - дополнительная нагрузка на CPU.
Перехват RST и FIN желателен для максимально корректной работы conntrack.
Фильтр по mark необходим для предотвращения кольца. Без этого возможны зависания и неправильная работа.
notrack нужен, чтобы NAT не ломал техники, которые не совместимы с NAT.
Генерируемые nfqws2 пакеты не должны проходить проверки на валидность с точки зрения NAT и дропаться стандартными правилами таблиц.
Подстановка IP адресов NAT не требуется, поскольку попадающий на nfqws2 пакет уже прошел NAT и имеет корректные адрес и порт источника для wan.
IFACE_WAN=wan
MAX_PKT_IN=15
MAX_PKT_OUT=15
FWMARK=0x40000000
PORTS_TCP=80,443
PORTS_UDP=443
QNUM=200
nft create table inet ztest
nft add chain inet ztest postnat "{type filter hook postrouting priority srcnat+1;}"
nft add rule inet ztest postnat oifname $IFACE_WAN meta mark and $FWMARK == 0 udp dport "{$PORTS_UDP}" ct original packets 1-$MAX_PKT_OUT queue num $QNUM bypass
nft add rule inet ztest postnat oifname $IFACE_WAN meta mark and $FWMARK == 0 tcp dport "{$PORTS_TCP}" ct original packets 1-$MAX_PKT_OUT queue num $QNUM bypass
nft add rule inet ztest postnat oifname $IFACE_WAN meta mark and $FWMARK == 0 tcp dport "{$PORTS_TCP}" tcp flags fin,rst queue num $QNUM bypass
nft add chain inet ztest pre "{type filter hook prerouting priority filter;}"
nft add rule inet ztest pre iifname $IFACE_WAN udp sport "{$PORTS_UDP}" ct reply packets 1-$MAX_PKT_IN queue num $QNUM bypass
nft add rule inet ztest pre iifname $IFACE_WAN tcp sport "{$PORTS_TCP}" ct reply packets 1-$MAX_PKT_IN queue num $QNUM bypass
nft add rule inet ztest pre iifname $IFACE_WAN tcp sport "{$PORTS_TCP}" "tcp flags & (syn | ack) == (syn | ack)" queue num $QNUM bypass
nft add rule inet ztest pre iifname $IFACE_WAN tcp sport "{$PORTS_TCP}" tcp flags fin,rst queue num $QNUM bypass
nft add chain inet ztest predefrag "{type filter hook output priority -401;}"
nft add rule inet ztest predefrag "mark & $FWMARK != 0x00000000 notrack"Удаление тестовой таблицы :
nft delete table inet ztestПерехват трафика с помощью iptables
[!CAUTION]
Начиная с ядер Linux 6.17 присутствует параметр конфигурации ядра CONFIG_NETFILTER_XTABLES_LEGACY, который по умолчанию в дистрибутиве может быть "not set". Отсутствие этой настройки выключает iptables-legacy. Это часть процесса депрекации iptables. Тем не менее iptables-nft будут работать, поскольку используют backend nftables.
Тестовые правила для PRENAT схемы.
Обеспечивают перехват первых входящих и исходящих пакетов по потоку до NAT, если таковой присутствует.
Адреса и порты источника внутренней сети сохраняются. Атаки на проходящий трафик, ломающие NAT, невозможны, но возможны с самой системы.
/* Detailed source-code truncated for AI context efficiency. */Удаление тестовых правил zapret :
for tables in iptables ip6tables; do
$tables -t mangle -D POSTROUTING -j ztest_post
$tables -t mangle -D PREROUTING -j ztest_pre
$tables -t mangle -F ztest_post
$tables -t mangle -X ztest_post
$tables -t mangle -F ztest_pre
$tables -t mangle -X ztest_pre
doneУдаление всех правил из таблицы mangle, включая и иные правила :
iptables -F -t mangle
ip6tables -F -t mangleПерехват трафика в ядре FreeBSD
Основная боль при перехвате трафика на системах, отличных от Linux, - невозможность перехватить первые пакеты потока.
Можно перехватить только весь поток целиком по направлению.
В BSD с этим дела хуже всего - нет даже возможностей фильтрации raw payload, то есть по содержимому пакета.
Поэтому первый набор правил - это перехват всех исходящих по портам и перехват только SYN+ACK,FIN,RST для TCP, чтобы
без нагрузки на процессор можно было задействовать режим autottl и максимально корректно работал conntrack.
Однако, в таком варианте не будет работать ничего, что требует иного входящего трафика.
RULE=100
IFACE_WAN=vmx0
PORTS_TCP=80,443
PORTS_UDP=443
PORT_DIVERT=989
ipfw delete $RULE
ipfw add $RULE divert $PORT_DIVERT tcp from any to any $PORTS_TCP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT udp from any to any $PORTS_UDP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any tcpflags syn,ack in not diverted recv $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any tcpflags fin in not diverted recv $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any tcpflags rst in not diverted recv $IFACE_WANВариант с перехватом потока в обе стороны. Особо сильно загружает процессор.
Все скачиваемые гигабайты пойдут через dvtws2. Из них обычно нужно всего 1-2 пакета, все остальное - впустую расходует CPU,
но средства ipfw не предоставляют иных возможностей.
RULE=100
IFACE_WAN=vmx0
PORTS_TCP=80,443
PORTS_UDP=443
PORT_DIVERT=989
ipfw delete $RULE
ipfw add $RULE divert $PORT_DIVERT tcp from any to any $PORTS_TCP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT udp from any to any $PORTS_UDP out not diverted xmit $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT tcp from any $PORTS_TCP to any in not diverted recv $IFACE_WAN
ipfw add $RULE divert $PORT_DIVERT udp from any $PORTS_UDP to any in not diverted recv $IFACE_WANТеоретически возможен перехват через pf divert-to, но на практике механизм предотвращения зацикливания сломан,
поэтому использовать pf нереально.
На pfsense и opnsense требуются дополнительные меры для задействования pf и ipfw одновременно.
Часто с этим бывают проблемы, конфликты и глюки.
Перехват трафика в ядре OpenBSD
В OpenBSD есть только pf. С механизмом предотвращения зацикливания divert все в порядке.
Первый вариант - перехват всех исходящих по портам и перехват только SYN+ACK,FIN,RST для TCP, чтобы
без нагрузки на процессор можно было задействовать режим autottl и максимально корректно работал conntrack.
Однако, в таком варианте не будет работать ничего, что требует иного входящего трафика.
pf требует файлов с правилами. Вы пишите pf файл (обычно используется /etc/pf.conf), потом его применяете черезpfctl -f /etc/pf.conf. pfctl -e включает pf, pfctl -d - выключает.
Возможно использование якорей (anchors) с отдельными файлами правил, читайте документацию по pf.
Трюки с no state нужны, чтобы предотвратить автоматический перехват и входящих пакетов по потоку.
IFACE_WAN = "em0"
PORTS_TCP = "80,443"
PORTS_UDP = "443"
PORT_DIVERT = "989"
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } flags SA/SA divert-packet port $PORT_DIVERT no state
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } flags R/R divert-packet port $PORT_DIVERT no state
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } flags F/F divert-packet port $PORT_DIVERT no state
pass in quick on $IFACE_WAN proto tcp from port { $PORTS_TCP } no state
pass out quick on $IFACE_WAN proto tcp to port { $PORTS_TCP } divert-packet port $PORT_DIVERT no state
pass out quick on $IFACE_WAN proto udp to port { $PORTS_UDP } divert-packet port $PORT_DIVERT no stateВариант с перехватом потока в обе стороны. Особо сильно загружает процессор.
Все скачиваемые гигабайты пойдут через dvtws2. Из них обычно нужно всего 1-2 пакета, все остальное - впустую расходует CPU,
но средства pf не предоставляют иных возможностей.
Перехват входящих по тем же портам обеспечивается автоматически за счет state.
IFACE_WAN = "em0"
PORTS_TCP = "80,443"
PORTS_UDP = "443"
PORT_DIVERT = "989"
pass out quick on $IFACE_WAN proto tcp to port { $PORTS_TCP } divert-packet port $PORT_DIVERT
pass out quick on $IFACE_WAN proto udp to port { $PORTS_UDP } divert-packet port $PORT_DIVERT[!CAUTION]
В FreeBSD другая версия pf и немного другой синтаксис. Однако, фактически pf в FreeBSD сломан, поскольку не работает предотвращение зацикливания. В macOS хотя и используется pf, ipdivert из ядра убран, правила работать не будут.
Перехват трафика в ядре Windows
В Windows нет встроенных средств для перехвата трафика. Используется стороннее решение - драйвер WinDivert.
Управление интегрируется в сам процесс winws2.
WinDivert принимает текстовые фильтры, похожие на фильтры wireshark и tcpdump.
В них есть возможности фильтрации по ip (без ipset), портам и raw пейлоадам. Нет побитовых логических операций и сдвигов.
Нет отслеживания потоков и ограничения по первым пакетам.
Драйвер WinDivert больше не разрабатывается, однако имеются подписанные варианты драйвера, совместимые со всеми современными Windows,
но только для архитектуры x86_64. На arm64 есть неподписанный драйвер, требующий тестового режима подписи драйверов.
При использовании winws2 на Windows 11 arm64 приходится пользоваться x86_64 версией, поскольку winws2 написан под cygwin, а его нет для arm.
Драйвер .sys при этом заменяется на неподписанную arm64 версию. Запуск на Windows 10 arm64 теоретически возможен, но только с 32-битной
версией winws2 x86, поскольку эмуляция x64 в Windows 10 не предусмотрена.
WinDivert является частой целью для нападок антивирусов. Это хакерский инструмент, но вирусом он не является.
Правильнее воспринимать его как замену iptables для Windows.
Иногда случаются конфликты со сторонним ПО, использующим драйвера режима ядра - прежде всего антивирусы и фаерволы, вплоть до синих экранов.
Практически исправить это нереально хотя бы из-за подписи драйверов, которую получить простому смертному без стоящих за ним корпораций очень непросто и накладно.
WinDivert не может обеспечить корректного перехвата проходящего трафика при раздаче сети средствами Windows и как следствие использовании NAT,
поэтому возможности работы по проходящему трафику не реализованы. Единственный доступный вариант - установить proxy server.
winws2 может принимать полные raw фильтры - вы пишите фильтр сами и указываете его в параметре --wf-raw=<filter> или --wf-raw=@<filter_file>.
Но это обычно не очень удобно, поэтому существует встроенный конструктор фильтров.
--wf-tcp-out, --wf-tcp-in, --wf-udp-out, --wf-udp-in берут список портов (80,443) или диапазонов портов (80,443,500-1000)
и включают полный перехват портов по указанному направлению.
--wf-icmp-out, --wf-icmp-in берут список icmp_type или icmp_type+icmp_code.
--wf-ipp-in, --wf-ipp-out берут список raw IP протоколов (для ipv6 не поддерживается выявление истинного протокола при наличии extension headers).
--wf-raw-part принимает частичные WinDivert фильтры. Синтаксис аналогичен --wf-raw. --wf-raw-part может быть несколько.
Частичные фильтры встраиваются конструктором в итоговый фильтр по принципу OR. Или указанные порты, или указанные icmp, или указанные ipp, или ваш фильтр1 или ваш фильтр2.
--wf-raw-filter - частичный WinDivert фильтр, объединяемый по AND с итоговым полным фильтром. Может быть только один. Типичное применение - фильтрация по небольшому списку IP адресов.
Например, задача - сделать icmp тоннель до сервера. Перехват всего icmp определенного типа слишком расточителен по процессору и бессмысленен.
Намного лучше к --wf-icmp-in=0 будет добавить --wf-raw-filter="ip.SrcAddr==1.2.3.4".
--wf-save=<filter_file> записывает созданный конструктором фильтр в файл для последующего анализа и модификации.
Конструктор фильтров автоматически перехватывает входящие tcp с флагами SYN+ACK,FIN,RST. Писать специально правила не нужно.
--wf-filter-lan (по умолчанию включен) отфильтровывает пакеты на не глобальные IP адреса, такие как 192.168.0.0/16.
--wf-tcp-empty (по умолчанию выключен) включает перехват пустых tcp пакетов без флагов SYN,FIN,RST.
Если параметр не включен, перехват пустых ACK пакетов не производится, что позволяет существенно сэкономить на процессоре.
Но для некоторых стратегий эти пакеты могут понадобиться. Только вы можете знать нужны они вам или нет.
Если есть перехват любого tcp порта, автоматически включается перехват всех http редиректов, чтобы работал autohostlist.
Перехват http redirect работает по payload сигнатуре, то есть проверяются байты в определенных позициях содержимого пакета.
Предпочтительно по udp протоколам, особенно по тем, где порт не определен, использовать свои --wf-raw-part фильтры,
чтобы сэкономить на процессоре. Для tcp тоже возможны фильтры, но надо учитывать потребности conntrack. Он нуждается
как минимум в SYN пакете, а желательно еще и FIN, RST. Если нужна фильтрация по сообщениям , занимающим более одного tcp сегмента,
такое отфильтровать средствами WinDivert невозможно - требуется полный перехват порта по направлению.
nfqws2
Общие принципы задания параметров
Все параметры nfqws2 передаются в командной строке, либо загружаются из файла в том же самом формате.
nfqws2 использует стандартный парсер getopt_long_only.
Опции имеют формат --name[=value]. Некоторые опции не требуют параметров, другие требуют, а третьи могут их брать опционально.
Парсер getopt позволяет задавать значение через знак = или через пробел. Лишние значения через пробел могут игнорироваться,
поэтому казалось бы ошибочные параметры могут не вызвать ошибку. Лучше всегда писать значения через знак =.
Чтение параметров из файла реализовано через задание единственной опции @config_file.
Все остальные параметры командной строки будут проигнорированы.
Опции будут прочитаны из файла, как будто бы вы ввели его содержимое в командной строке.
Возможность не поддерживается в Android и OpenBSD версиях.
Полный список опций
Общие параметры для всех версий - nfqws2, dvtws2, winws2.
/* Detailed source-code truncated for AI context efficiency. */Специфические параметры nfqws2 :
--qnum=<nfqueue_number> ; номер очереди NFQUEUE в Linux
--user= ; сменить uid/gid на те, что связаны с указанным именем пользователя
--uid=uid[:gid1,gid2,...] ; сменить uid/gid на указанные числовые значения
--bind-fix4 ; лечение проблемы ухода генерированных пакетов на Linux с неверного интерфейса при использовании PBR (ipv4)
--bind-fix6 ; аналогично для ipv6
--fwmark= ; бит в mark для предотвращения зацикливания. default = 0x40000000
--filter-ssid=ssid1[,ssid2,ssid3,...] ; фильтр профиля : имя wifi сети
--filter-ssid-neg=[0|1] ; фильтр профиля : инверсия фильтра wifi сетиСпецифические параметры dvtws2 :
--port= ; номер divert порта
--user= ; сменить uid/gid на те, что связаны с указанным именем пользователя
--uid=uid[:gid1,gid2,...] ; сменить uid/gid на указанные числовые значенияСпецифические параметры winws2 :
/* Detailed source-code truncated for AI context efficiency. */Распознавание протоколов
nfqws2 сигнатурно распознает типы пейлоадов отдельно взятых пакетов или групп пакетов.
Все пустые пакеты имеют пейлоад empty, неизвестные - unknown.
Протокол потока присваивается после получения первого известного пейлоада и остается с потоком до конца его существования.
При этом последующие пейлоады могут иметь как известный тип, так и неизвестный.
В фильтрах по пейлоаду и протоколу потока доступны специальные значения - all и known. All означает любой, known - не empty и не unknown.
Таблица распознаваемых типов пейлоада и протоколов потока
| Протокол потока | L4 | Пейлоады |
|---|---|---|
| http | tcp | http_reqhttp_reply |
| tls | tcp | tls_client_hellotls_server_hello |
| xmpp | tcp | xmpp_streamxmpp_starttlsxmpp_proceedxmpp_features |
| mtproto | tcp | mtproto_initial |
| bt | tcp | bt_handshake |
| quic | udp | quic_initial |
| wireguard | udp | wireguard_initiationwireguard_responsewireguard_cookiewireguard_keepalive |
| dht | udp | dht |
| utp_bt | udp | utp_bt_handshake |
| discord | udp | discord_ip_discovery |
| stun | udp | stun |
| dns | udp | dns_querydns_response |
| dtls | udp | dtls_client_hellodtls_server_hello |
| любой | icmp | ipv4ipv6icmp |
Особые типы пейлоада - ipv4,ipv6,icmp. ipv4 и ipv6 генерируются, когда icmp содержит прикрепленный пакет.
В остальных случаях icmp имеет тип пейлоада "icmp".
Использование множественных профилей
Профили существуют, чтобы в зависимости от указанных условий фильтра выбрать ту или иную стратегию воздействия на трафик.
Общая схема использования профилей следующая :
nfqws2 <глобальные_параметры>
<фильтр 1> <стратегия 1> --new
<фильтр 2> <стратегия 2> --new
...............
<фильтр N> <стратегия N>Когда на вход поступает пакет, и для него еще нет записи в conntrack, происходит выбор профиля.
Фильтры профиля проверяются от первого до последнего - с начала в конец - слева направо, и никак иначе.
Всегда выигрывает только один профиль - по первому совпадению условий фильтра, а все остальные не задействуются.
Если не сработал ни один фильтр, выбирается пустой профиль с номером 0, не предполагающий никаких действий с трафиком.
Все условия, кроме --filter-l7 и хостлистов, являются однозначными и известными с момента начала обработки потока (с начала соединения).
В начале, как правило, еще неизвестен протокол потока и имя хоста, выделяемое из сообщений потока.
Когда эти величины становятся известны, происходит поиск профиля заново. Если выбирается другой профиль - происходит перескок.
Таких перескоков может быть до двух, поскольку есть только 2 величины, влияющие на выбор, неизвестные с самого начала.
Для протоколов потока tls, http, quic обычно бывает только 1 перескок, поскольку определение протокола и имени хоста
происходит по одному пакету или группе пакетов. Для XMPP это 2 перескока - сначала определяется xmpp как таковой,
затем ловится переход на TLS, и только в нем выделяется имя хоста.
При написании стратегий следует их продумывать с учетом логики перескоков.
Если нужно, чтобы стратегия начала работу с самого первого пакета и продолжила работать дальше после изменения профиля,
нужно дублировать вызовы во всех профилях, по которым может пройти поток.
4 группы фильтров - tcp, udp, icmp и ipp объединяются по принципу OR. Если нет ни одного фильтра из этих групп, то
считается разрешенным все. Если задается хотя бы один, остальные группы блокируются, если не определены.
Фильтр ipp не работает с протоколами tcp, udp и icmp. Проверка пойдет по группам фильтра, соответствующим протоколу. Например, если задано только --filter-ipp=6,
это не значит, что будут пропущены все tcp. Наоборот, это блокирует все, включая и сам tcp, потому что не задан --filter-tcp. Правильно будет указать --filter-tcp=*.
icmp автоматически подразумевает и icmpv6 - они обрабатываются единым образом. Однако, icmp types для icmp и icmpv6 отличаются. Это нужно учитывать.
Шаблоны профилей
Когда имеется много сложных и повторяющихся стратегий или групп одинаковых параметров, может быть удобно использовать шаблоны.
Шаблон - это такой же профиль, только он не идет в работу, а попадает в отдельный список шаблонов.
Шаблоном профиль становится через задание параметра --template=<name>.
Далее он может быть импортирован (--import=<name>) в другой профиль или другой шаблон.
Простые параметры - число, строка, bool - импортируются только, если они были заданы в импортируемом шаблоне. При импорте шаблона в другой шаблон в шаблоне назначения они также считаются заданными.
Списочные параметры добавляются в конец списка. Такими параметрами является все, что может принимать список значений. Например, хостлисты или --filter-tcp.
Номер и имя шаблона не копируются.
Директив --import может быть сколько угодно в любом месте. Предыдущие простые заданные параметры затираются новыми импортированными или заданными в текущем профиле.
Особый запрет - замещение autohostlist. Шаблон может иметь autohostlist, но при импорте куда-то, где уже есть autohostlist, будет ошибка.
nfqws2 <глобальные_параметры>
--template=tpl1 <базовые параметры 1> --new
--template=tpl2 <базовые параметры 2> --new
--template --import tpl2 --name tpl3 <базовые параметры 3> --new
--import tpl1 --name prof1 <дополнительные параметры 1> --new
--import tpl3 --name prof2 <дополнительные параметры 2> --new
--name prof3 <параметры 3>В примере имеется 3 рабочих профиля и 3 шаблона, 1 из которых импортирует настройки другого.
- Профиль prof1 получает объединение
<базовые параметры 1>и<дополнительные параметры 1>. - Профиль prof2 получает объединение
<базовые параметры 2>,<базовые параметры 3>и<дополнительные параметры 2> - Профиль prof3 получает
<параметры 3>. Он не импортирует шаблоны.
В шаблонах допустимы любые параметры, относящиеся к профилям, включая и фильтры. Глобальные параметры не являются частью профилей и шаблонов.
Фильтрация по ipset
- В случае tcp и udp сравнивается адрес сервера в клиентском режиме и адрес клиента в серверном.
- related icmp проходят по профилю исходного пакета, поиск профиля не выполняется.
- Для unrelated icmp и raw ip проверяются адреса источника и приемника, совпадение фиксируется при совпадении любого из адресов.
Фильтрация по листам
Если имеются фильтры по хостлистам, и есть хотя бы один домен в любом хостлисте или указан автохостлист,
то профиль никогда не будет выбран при отсутствующем имени хоста.
Случай, когда нет автохостлиста, а все файлы листов пустые, приравнивается к отсутствию фильтра по хостлисту.
Если нет автохостлиста, но есть записи в обычных хостлистах, то профиль выбирается только если текущий
хост проходит по любому из включающих хостлистов, но не проходит ни по одному из исключающих.
Если есть автохостлист, то при наличии имени хоста профиль выбирается всегда вне зависимости от его вхождения
в какие-либо листы этого профиля. Действия зависят от вхождения в листы.
- Если хост входит в исключающие листы, не происходит никаких действий и не происходит попытки выяснить работает ли ресурс.
- Если хост не входит в исключающие листы, но входит во включающие - происходит применение стратегии без
попыток выяснить работает ли ресурс с ней. - Если хост не входит ни в исключающие листы, ни во включающие - стратегия не применяется, происходит обнаружение
неудачи доступа к ресурсу. Если случилась неудача, увеличивается счетчик неудач. Если случается удача или превышается
интервал времени между неудачами--hostlist-auto-fail-time- счетчик сбрасывается.
Когда счетчик достигает--hostlist-auto-fail-threshold, происходит занесение хоста в автолист.
При следующем запросе будет считаться, что хост входит во включающий лист. - Файлы хостлистов и ipset-ов перечитываются автоматически при изменении - перезапуск nfqws2 не нужен.
- Сигнал SIGHUP помечает все листы для принудительной перечитки при обработки следующего пакета
- Каждая запись о домене, IP адресе или подсети идет на новой строке.
- Хостлисты и ipset-ы поддерживают комментарии. Пустые строки и строки, начинающиеся с
#, игнорируются. - В хостлистах поддомены учитываются автоматически.
*не поддерживается. Если в начале идет символ^, автоматический учет поддоменов отменяется для этого домена. - ipset-ы могут включать адреса и подсети как ipv4, так и ipv6.
- В статическом варианте
--ipset-ipи--hostlist-domainsдомены идут через запятую. В статическом хостлисте#и^так же поддерживаются. - Для листов поддерживается сжатие gzip.
Детектор неудач автохостлистов
Детектор срабатывает только при наличии имени хоста. Неудачей считается :
- tcp : происходит не менее
--hostlist-auto-retrans-thresholdретрансмиссий в пределах исходящего relative sequence--hostlist-auto-retrans-maxseq. если--hostlist-auto-retrans-reset=1, ретрансмиттеру шлется RST, чтобы он прекратил попытки достучаться (это может быть очень долго). - tcp : приходит RST в пределах входящего relative sequence от 1 до
--hostlist-auto-incoming-maxseq - tcp : принят пейлоад
http_replyи http ответ является переадресацией 302 или 307 на абсолютный URL с доменом 2 уровня, не совпадающим с доменом 2 уровня хоста. - udp : ушло не менее
--hostlist-auto-udp-outпакетов, пришло не более--hostlist-auto-udp-inпакетов. Эта ситуация означает, что клиент шлет запросы, а сервер на них не отвечает или отвечает меньше, чем должен по протоколу.
Удачей считается :
- tcp : превышение исходящего relative sequence
--hostlist-auto-retrans-maxseq. Клиент смог отослать достаточно много, что вряд ли бы случилось в случае реакции DPI. - tcp : превышение входящего relative sequence
--hostlist-auto-incoming-maxseq. Сервер прислал достаточно много, чтобы это не было похоже на ответ DPI. - udp : превышение количества пришедших пакетов
--hostlist-auto-udp-in. Сервер ответил достаточно много.
При неудаче, если с прошлой неудачи прошло не более --hostlist-auto-fail-time секунд, счетчик неудач увеличивается.
Если прошло больше времени - счетчик сбрасывается, счет идет заново.
При удаче счетчик сбрасывается. Считается, что ресурс работает, а сбой был временным и не связанным с блокировкой.
При достижении счетчиком --hostlist-auto-fail-threshold происходит занесение хоста в лист.
Большинство критериев удачи или неудачи требует анализа входящего и исходящего трафика, поэтому необходим их перехват в достаточном объеме
для возможности срабатывания критериев.
Фильтр по наличию сетей
Если, допустим, вам нужно применить одну стратегию для wifi, другую для провода, это делается через фильтр, основанный на имени интерфейса.
Но что делать, если вы подключаетесь к разным wifi сетям или подключаете провод в разных локациях ?
В случае провода решение есть только на Windows, на других системах - нет. Для wifi есть решение на Linux и Windows, на BSD - нет.
Фильтр по wifi берет список SSID через запятую, однако он реализован по-разному в Linux и Windows.
В Linux используется фильтр профиля --filter-ssid. Если он задан, nfqws2 пытается получить SSID на интерфейсе, куда этот пакет уходит
или откуда он приходит. Если ему это удается - происходит проверка по списку сетей, если нет - условие фильтра не соблюдается, профиль не выбирается.
Такая концепция позволяет работать даже если вы подключаетесь к нескольким wifi сетям на разных адаптерах.
В Windows концепция иная. Мониторится наличие указанных wifi сетей на всех wifi адаптерах, и если на любом из них SSID есть,
перехват WinDivert включается, а иначе выключается. Чтобы обслужить wifi сети с разными стратегиями нужно запускать несколько инстансов winws2.
Один будет включаться, остальные - отключаться. Список SSID задается параметром --ssid-filter.
Другой способ решить вопрос, и не только с wifi, - использование фильтра NLM - Network List Manager.--nlm-list[=all] вернет список GUID подключенных или всех сетей, если указано значение параметра "all".
Далее вписываете список GUID через запятую в --nlm-filter.
Сеть NLM - это результат детекта системой подключения к конкретной сети.
Вы можете подключиться к роутеру по wifi или проводу, но сеть будет одна. Для различения сетей система обычно смотрит на MAC шлюза.
Технология NLM интересная и полезная, но , увы , адекватное управление ей было только в Windows 7.
В остальных системах нужно копаться в powershell или лезть в реестр, чтобы раскидать подключения по нужным GUID,
если вдруг они раскидались системой неправильно. Но можно и не бороться, а просто внести список GUID, назначенных системой автоматически.
В обеих системах есть параметры, позволяющие инвертировать проверку.
Серверный режим
Некоторые виды воздействий можно осуществлять не только со стороны клиента, но и сервера.
nfqws2 создавался с учетом полноценной работы как по входящему, так и по исходящему трафику,
как на клиентской стороне, так и на стороне сервера.
Понятие направления в сети во многом условно. Для адресатов пакетов все ясно -
что-то отсылается, что-то принимается. Но для роутера это неясно совсем.
Есть только входящий интерфейс и исходящий. По сути 2 направления равнозначны,
если рассматривать только уровень L3 - уровень отдельных IP пакетов.
Поэтому направление в nfqws2 учитывается за счет слежения за потоками.
Поток - это либо tcp соединение, либо последовательность udp пакетов.
udp не предполагает понятия соединения, поэтому оставлено общее название - поток.
Поток характеризуется 4 величинами : ip1:port1-ip2:port2. Их совокупность (tuple)
определяет принадлежность пакета к конкретному потоку.
За потоками в nfqws2 следит conntrack.
Тот, кто первым послал SYN (tcp) или первый UDP пакет, считается клиентом,
а противоположный конец - сервером.
Если создание записи о потоке в conntrack произошло по SYN,ACK пакету (tcp),
то этот конец считается сервером, а противоположный - клиентом.
Таким образом conntrack определяет роли в установлении соединения и хранит
по каждой роли отдельный набор счетчиков - сколько пакетов прошло,
сколько пакетов с данными, сколько байт передано и тд.
В клиентском режиме исходящим направлением считается направление от клиента,
в серверном (--server) - от сервера. Входящим считается противоположное направление.
При указании --server направления инвертируются.--in-range, --out-range, а так же признак desync.outgoing в Lua функциях
меняются местами, чтобы соответствовать фактически отсылаемым или принимаемым данным
со стороны сервера. Клиент шлет запросы (http_req) и принимает ответы (http_reply).
Сервер шлет ответы (http_reply) и принимает запросы (http_req).
Для обоих режимов - клиентского и серверного - поиск в ipset осуществляется по IP адресу
назначения для исходящего направления и IP адресу источника для входящего направления.
В клиентском режиме фильтры портов проверяют порт назначения для исходящего направления и порт источника для входящего направления.
В серверном режиме фильтры портов проверяют порт назначения для входящего направления и порт источника для исходящего направления.
Это сделано потому, что в фильтрации реальный смысл имеет только порт сервера.
Порт клиента выбирается как правило случайно из некоторого диапазона и не подходит для осмысленной фильтрации.
Таким образом, сервер фильтрует по ipset ip клиентов, а клиент - ip серверов.
Но и сервер, и клиент фильтруют порт сервера.
Linux conntrack имеет похожий способ определения направлений.
Для клиента исходящими будут пакеты original, входящими - reply. Для сервера - наоборот.
Это нужно учитывать при написании правил перехвата, полагающихся на направление conntrack.
Можно определять направление и по именам интерфейсов. При стандартной конфигурации lan-wan
входящими считаются пакеты, полученные с wan , а исходящими - отправленные через wan.
Именно wan, поскольку обычно используется NAT, а для nfqws2 важно перехватывать исходящие пакеты после NAT
и принимать входящие до NAT.
Если роутер работает без NAT (типично для ipv6), не имеет значения на каком этапе перехватываются пакеты.
Все IP адреса равнозначны. Вы можете подключаться к интернету, интернет - к вам. Вы - его составная часть
с прямой IP адресацией. Но на практике все же к вам подключаться скорее всего не будут, а вы защититесь
от таких подключений фаерволом. Поэтому направление все равно по интерфейсу определяется достаточно однозначно.
Но если у вас все же есть внутренний сервер, вы можете для него запустить отдельный инстанс nfqws2 в серверном режиме,
а для клиентского трафика - в обычном режиме.
В BSD правила ipfw или pf обычно так и пишутся - "xmit wan", "recv wan" + добавляются фильтры по номерам
портов назначения для xmit и портов источника для recv (или наоборот для серверного режима).
Это достаточно надежно идентифицирует необходимый к перехвату трафик по направлению.
Кэш IP
ipcache представляет собой структуру в памяти процесса, позволяющую по ключу IP адреса и имени интерфейса запоминать некоторую информацию,
которую впоследствии можно извлечь и использовать как недостающие данные. На текущий момент это применяются в следующих ситуациях :
IP,interface => incoming ttl . Кэшируется TTL/HL первого входящего пакета для последующего применения в Lua функциях (autottl) прямо с первого пакета, когда еще ответа не было.
IP => hostname . Кэшируется имя хоста, вне привязки к интерфейсу, для последующего поиска профиля с фильтрами по хостлистам и передачи в Lua функции, когда имя хоста еще неизвестно.
Режим отключен по умолчанию и включается через параметр--ipcache-hostname.
Данная техника является экспериментальной. Ее проблема в том, что как такового нет однозначного соответствия между доменом и IP. Множество доменов могут ссылаться на тот же IP адрес.
При коллизии происходит замещение имени хоста на последний вариант.
Домен может скакать по разным IP на CDN. Сейчас один адрес, через час - другой. Эта проблема решается через время жизни записей кэша :--ipcache-lifetime. По умолчанию 2 часа.
Однако, может случиться и так, что в вашем случае применение техники несет больше пользы, чем проблем. Будьте готовы к непонятному на первый взгляд поведению, которое может быть исследовано только через--debugлог.
Имена хостов для кэширования берутся из анализа L7 протоколов и из DNS ответов (не DoH).
Извлечение DNS работает только по udp протоколу и требует перенаправления пакетов с порта 53. Нужны только ответы DNS, имеющие source port 53.
На Windows это будет --wf-udp-in=53. Скорее всего будет обращение к DNS внутри локальной сети, поэтому нужен параметр --wf-filter-lan=0.
Если на Windows крутится какой-то dns proxy, а DNS настроен на localhost, потребуется еще и --wf-filter-loopback=0.
На роутере, если используется шифрованный DNS, надо ловить запросы от клиента еще до шифрования - на LAN интерфейсе.
Если шифрование не используется, можно ловить и на WAN.
Для LAN интерфейса они будут исходящими, для интерфейса WAN - входящими, по направлению conntrack - reply.
При использовании скриптов запуска в Linux для перехвата DNS ответов существует custom script 80-dns-intercept. Его достаточно скопировать в "custom.d".
Сигналы
- SIGHUP принудительно перечитывает все файлы хостлистов и ipset
- SIGUSR1 выводит содержимое пула conntrack
- SIGUSR2 выводит счетчики autohostlist и содержимое пула ipcache
Отладка
Параметр --debug включает вывод отладочных сообщений.
--debug=0выключает вывод--debug,--debug=1вывод на консоль--debug=@<filename>вывод в файл. размер файла ничем не ограничивается, но файл может быть удален в любой момент, и запись продолжится с чистого листа--debug=syslogвывод в syslog. чтение зависит от syslog daemon. rsyslog пишет файлы в /var/log. busybox logd читается через logread.--debug=androidвывод в android log. чтение через logcat. доступно только в версиях, собранных в Android NDK
Умение обращаться с --debug логом совершенно необходимо для отладки настроек и тем более для написания собственного Lua кода.
Все сообщения об ошибках (DLOG_ERR) и особо важные сообщения (DLOG_CONDUP) дублируются на консоли вне зависимости от log target.
Сообщения об ошибках выводятся в stderr.
Параметр --dry-run позволяет протестировать корректность опций командной строки и доступность используемых файлов под сброшенными привилегиями.--dry-run не инициализирует движок Lua, и поэтому не может обнаружить синтаксические ошибки Lua.
Виртуальные машины
Большинство десктопных гипервизоров ломают техники обхода при подключении сети через встроенный в гипервизор NAT. Это настройка по умолчанию. Необходимо подключение через мост (bridge). Проблема точно есть в vmware и virtualbox.
Песочница
В целях безопасности nfqws2 после инициализации сбрасывает свои привилегии.
Весь Lua код выполняется только после сброса привилегий. Он никогда не получает исходные права.
BSD :
- Меняется UID/GID на указанные в параметрах
--user,--uid. По умолчанию на 0x7FFFFFFF.
Linux :
- Меняется UID/GID на указанные в параметрах
--user,--uid. По умолчанию на 0x7FFFFFFF. - Сбрасываются capabilities до cap_net_raw, cap_net_admin (требуется для NFQUEUE). Сбрасывается bounding set до нуля.
- Выставляется признак NO_NEW_PRIVS, чтобы не работали suid биты и caps на файлах. Если ядро старее 3.5, NO_NEW_PRIVS не поддерживается. В этом случае выводится предупреждение, выполнение не прекращается.
- Включается seccomp фильтр, запрещающий exec и ряд файловых операций - чтение содержимого каталогов, создание/удаление каталогов, создание специальных файлов (линки, девайсы), chmod, chown, посылание сигналов (kill), ptrace.
В случае нарушения процесс аварийно завершается. Если ядро не поддерживает seccomp, выводится предупреждение, но выполнение не прекращается.
Windows :
- Хотя драйвер WinDivert требует привилегий администратора, после его инициализации процесс winws2 ставит себе low mandatory level.
Это предотвращает доступ на запись практически ко всем файлам и объектам, защищенным security descriptor.
Процесс больше не может управлять службами и осуществлять привилегированные действия. Однако, группа Administrators остается в токене процесса,
поэтому ничто не предотвращает чтение большинства файлов, если на них есть доступ для Administrators. Lua не имеет встроенных средств
чтения содержимого каталогов, поэтому обнаружение интересующих файлов для злоумышленника затруднено. - Безвозвратно убираются все Se* привилегии из токена, кроме SeChangeNotifyPrivilege.
- С помощью Job запрещается создание дочерних процессов и ограничивается взаимодействие с десктопом - clipboard, change desktop, change display settings и тд
Есть простой способ передать Lua коду каталог, доступный на запись - параметр --writable[=<dirname>].
nfqws2 создает каталог, назначает на него такие права, чтобы Lua код смог писать туда файлы, передает имя директории в переменной env WRITABLE.
Если dirname не задан, на Windows создается каталог внутри %USERPROFILE%/AppData/LocalLow
Со стороны Lua убираются опасные функции - os.execute, io.popen, package.loadlib и модуль debug.
На github исполняемые файлы nfqws2 собираются с вариантом luajit без FFI.
Вызов Lua кода
Lua код вызывается в 3 местах.
- Однократно при запуске программы через
--lua-init=code|@file. Если значение параметра начинается с@, выполняется файл, иначе значение параметра является Lua кодом. Поддерживается сжатие файлов gzip. Сначала проверяется "file", потом "file.gz". - При обработке профиля через
--lua-desync=function_name:arg1[=val1]:arg2[=val2]:argN[=valN].
Сначала идет имя функции, затем через двоеточия аргументы и их значения.
Все значения являются строками. Если значение не задано, оно равно пустой строке.
Реализовано 2 типа автоматических подстановок на стороне C кода.%varподставляет значение переменнойdesync.varилиvar, если первая отсутствует.#varподставляет длину переменнойdesync.varилиvar, если первая отсутствует.
Двоеточия и знаки%,#в начале могут быть эскейпнуты через\. - При обработке таймеров
И --lua-init, и --lua-desync может быть несколько. Выполнение производится строго в порядке указания.
Передача блобов
Блоб - это двоичный блок данных любого размера, который может быть загружен в переменную Lua средствами C кода при старте программы.
--blob=:[+ofs]@<filename>|0xHEX
item_name- имя переменной Lua.[+ofs]@<filename>- загрузка из файла со смещения ofs.0xHEX- загрузка из HEX строки
Прямые операции с файлами из кода Lua не рекомендованы без необходимости.
Lua код выполняется с ограниченными правами, задуманное может не получиться или не работать на разных ОС в разных условиях.
Загрузка blob происходит до вхождения в песочницу, поэтому шансов на успех больше.
Внутрипрофильные фильтры
Они бывают трех видов - --payload, --in-range, --out-range.
Значения фильтров действуют с момента их указания до следующего переопределения.
--payload=type1[,type2][,type3]...принимает список известных пейлоадов через запятую, "all" или "known". Список известных пейлоадов. По умолчанию--payload=all.--(in-range|out-range)=[(n|a|d|s|p)](-|<)[(n|a|d|s|p)]задает диапазоны счетчиков conntrack по входящему и исходящему направлениям. По умолчанию--in-range=x,--out-range=a.
Диапазоны задаются в формах : mX-mY, mX<mY, -mY, <mY, mX-, где m - режим счетчика, X - нижнее значение, Y - верхнее значение.
Режимы x и a задаются без диапазона и значения счетчика - единственной буквой. Знак - означает включающую верхнюю границу, < - исключающую.
Доступны следующие режимы счетчика :
- 'a' - всегда
- 'x' - никогда
- 'n' - номер пакета
- 'd' - номер пакета с данными
- 'b' - байт-позиция переданных данных
- 's' - tcp: relative sequence начала текущего пакета. работает в пределах 2 GB
- 'p' - tcp: relative sequence верхней границы текущего пакета (s + длина пейлоада). работает в пределах 2 GB
[!CAUTION]
В winws2 по умолчанию включен параметр--wf-tcp-empty=0. Он блокирует перехват пустых пакетов с ACK, что позволяет примерно в 2 раза сэкономить на процессоре при интенсивных скачиваниях. Пустые ACK в большинстве стратегий не нужны. Но это же и ломает счетчик "n" - он не будет показывать реальное количество пакетов по соединению. Если вам нужно точное значение, укажите параметр--wf-tcp-empty=1. В других системах точность счетчиков напрямую зависит от фильтров перехвата. Счетчик не может и не будет считать то, что не перехватывается.
nfqws2 следит за превышением верхней границы счетчиков для всех Lua инстансов.
Если во всех инстансах превышена верхняя граница по направлению или инстансы вошли по направлению в состояние cutoff добровольно,
происходит lua cutoff - выключение процессинга Lua в текущем потоке. Это нужно для экономии ресурсов процессора,
поскольку проверка единственного bool признака практически не требует никаких ресурсов.
Типичная схема вызова инстансов внутри профиля
В рассматриваемом примере решается задача для пейлоадов tls_client_hello и http_req попробовать фейки,
отдельные для каждого типа пейлоада, а если это не работает, перейти на multidisorder для tls и multisplit для http.
Если и это не работает - крутить стратегии по кругу.
--filter-tcp=80,443 --filter-l7=http,tls
--out-range=-s34228
--in-range=-s5556 --lua-desync=circular
--in-range=x
--payload=tls_client_hello
--lua-desync=fake:blob=fake_default_tls:badsum:strategy=1
--lua-desync=multidisorder:strategy=2
--payload=http_req
--lua-desync=fake:blob=fake_default_http:badsum:strategy=1
--lua-desync=multisplit:strategy=2Не суть важно как работают конкретные функции, сейчас важно понять как работают внутрипрофильные фильтры и как передаются параметры Lua инстансам.
- Профильный фильтр по tcp портам и типу протокола потока позволяет избежать вызовов Lua на другом трафике.
Профиль вообще не будет задействован, если условия фильтра не выполнятся. --out-rangeуказан, чтобы отсечь поток от Lua по исходящему направлению после relative sequence (32768+1460) - для экономии процессора.
Такое значение выбрано из-за специфики функции circular - значение s32768 используется в детекторе успеха как порог срабатывания по умолчанию,
1460 - максимально возможная длина данных в tcp пакете. На Linux может быть не нужно, если используется фильтр connbytes.- circular для своей работы требует начальные входящие пакеты потока, а по умолчанию они отключены.
Поэтому включаем вплоть до позиции relative sequence 5556. По умолчанию детектор успеха реагирует на s4096. Добавлен еще 1 пакет макс 1460 байт. - Остальные инстансы не нуждаются во входящем трафике. Снова отключаем. Действие
--in-range=xраспространяется до конца профиля. - Действие
--payloadраспространяется на два следующих за ним инстанса. - Строчка
--lua-desync=fake:blob=fake_default_tls:badsum:strategy=1вызывает функциюfakeс 3 аргументами :blob,badsum,strategy.
Значением аргументаbadsumявляется пустая строка.
Прототип Lua desync функции
Стандартная Lua функция имеет прототип
function desync_f(ctx,desync)
- ctx - контекст для вызова некоторых C функций
- desync - таблица, содержащая все передаваемые значения, включая аргументы, диссект текущего пакета и т.д.
Функция возвращает вердикт по текущему пакету - VERDICT_PASS, VERDICT_MODIFY, VERDICT_DROP.
Может не возвращать ничего, тогда результат приравниваться к VERDICT_PASS.
- VERDICT_PASS передает пакет как есть без учета изменений диссекта
- VERDICT_MODIFY выполняет реконструкцию и отправку текущего диссекта
- VERDICT_DROP дропает текущий пакет
- VERDICT_PRESERVE_NEXT - отдельный бит, который нужно прибавить к основному вердикту - использовать поля "next protocol" в ipv6 header и ipv6 extension headers. в ином случае они генерируются автоматом по содержимому диссекта
Результат всех lua-desync инстансов агрегируется : VERDICT_MODIFY замещает VERDICT_PASS, VERDICT_DROP замещает их обоих.
VERDICT_PRESERVE_NEXT применяется, если хотя бы один инстанс его вернул.
Структура таблицы desync
Лучше всего изучать структуру desync по ее реальному содержимому, выполняя тестовую desync функцию pktdebug из zapret-lib.lua.
Пакет http-req от запроса по ipv6 к http://one.one.one.one
desync:
.target
.port
number 80
.ip6
string 26 06 47 00 47 00 00 00 00 00 00 00 00 00 10 01
.func
string pktdebug
.func_n
number 1
.profile_n
number 1
.l7payload
string http_req
.dis
.tcp
.th_dport
number 80
.th_x2
number 0
.th_off
number 8
.th_sum
number 18781
.th_win
number 64
.options
.1
.kind
number 1
.2
.kind
number 1
.3
.data
string 30 40 18 9A 6F A5 3E 89
.kind
number 8
.th_seq
number 19930989
.th_ack
number 1489231977
.th_flags
number 24
.th_urp
number 0
.th_sport
number 48118
.ip6
.ip6_flow
number 1871905881
.ip6_hlim
number 64
.ip6_dst
string 26 06 47 00 47 00 00 00 00 00 00 00 00 00 10 01
.exthdr
.ip6_plen
number 110
.ip6_src
string 1A E5 18 81 E1 CD E8 24 BA 16 39 FF FE 8A DE 12
.ip6_nxt
number 6
.payload
string 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20 63 75 72 6C 2F 38 2E 38 2E 30 0D 0A 41 63 63 65 70 74 3A 20 2A 2F 2A 0D 0A 0D 0A
.l4proto
number 6
.transport_len
number 110
.l3_len
number 40
.l4_len
number 32
.reasm_offset
number 0
.reasm_data
string 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 3A 20 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 2E 6F 6E 65 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A 20 63 75 72 6C 2F 38 2E 38 2E 30 0D 0A 41 63 63 65 70 74 3A 20 2A 2F 2A 0D 0A 0D 0A
.ifout
string eth0
.fwmark
number 0
.func_instance
string pktdebug_1_1
.replay
boolean false
.track
.pos
.dt
number 0.013066192
.server
.tcp
.pos
number 1
.rseq
number 1
.scale
number 13
.mss
number 1360
.winsize_calc
number 65535
.uppos
number 0
.seq0
number 1489231976
.seq
number 1489231977
.uppos_prev
number 0
.winsize
number 65535
.pcounter
number 1
.pdcounter
number 0
.pbcounter
number 0
.client
.tcp
.pos
number 79
.rseq
number 1
.scale
number 10
.mss
number 1380
.winsize_calc
number 65536
.uppos
number 79
.seq0
number 19930988
.seq
number 19930989
.uppos_prev
number 0
.winsize
number 64
.pcounter
number 3
.pdcounter
number 1
.pbcounter
number 78
.reverse
.tcp
.pos
number 1
.rseq
number 1
.scale
number 13
.mss
number 1360
.winsize_calc
number 65535
.uppos
number 0
.seq0
number 1489231976
.seq
number 1489231977
.uppos_prev
number 0
.winsize
number 65535
.pcounter
number 1
.pdcounter
number 0
.pbcounter
number 0
.direct
.tcp
.pos
number 79
.rseq
number 1
.scale
number 10
.mss
number 1380
.winsize_calc
number 65536
.uppos
number 79
.seq0
number 19930988
.seq
number 19930989
.uppos_prev
number 0
.winsize
number 64
.pcounter
number 3
.pdcounter
number 1
.pbcounter
number 78
.lua_state
.hostname
string one.one.one.one
.hostname_is_ip
boolean false
.lua_in_cutoff
boolean true
.lua_out_cutoff
boolean false
.t_start
number 1700000000
.incoming_ttl
number 51
.l7proto
string http
.arg
.testarg1
string val1
.testarg2
string val2
.tcp_mss
number 1360
.l7proto
string http
.outgoing
boolean true
desync
| Поле | Тип | Содержание | Примечание |
|---|---|---|---|
| func | string | имя desync функции | |
| func_n | number | номер инстанса внутри профиля | |
| func_instance | string | название инстанса | производная имени функции, номера инстанса и номера профиля |
| profile_n | number | номер профиля | |
| profile_name | string | название профиля | может отсутствовать |
| cookie | string | значение параметра nfqws2 --cookie для профиля | может отсутствовать |
| outgoing | bool | true , если направление исходящее | |
| ifin | string | имя входящего интерфейса | может отсутствовать |
| ifout | string | имя исходящего интерфейса | может отсутствовать |
| fwmark | number | fwmark текущего пакета | только в Linux |
| target | table | таблица, включающая ip адрес и порт, на базе которых проверяются ipset-ы и фильтры по портам | |
| replay | bool | проигрывание задержанного пакета (replay) | |
| replay_piece | number | номер проигрываемой части | нумерация с 1 |
| replay_piece_count | number | количество проигрываемых частей | |
| replay_piece_last | bool | последняя проигрываемая часть | |
| l7payload | string | тип пейлоада текущего пакета или группы пакетов | если неизвестно - unknown |
| l7proto | string | тип протокола потока | если неизвестно - unknown |
| reasm_data | string | результат сборки многопакетного сообщения, либо сам пейлоад, если сборки не было | пока применяется только для tcp |
| reasm_offset | number | смещение текущего переигрываемого пакета в сборке | пока применяется только для tcp |
| decrypt_data | string | результат сборки и дешифровки пейлоада или пейлоадов нескольких пакетов | применяется для quic |
| tcp_mss | number | MSS противоположного конца tcp соединения | присутствует всегда, только для tcp |
| track | table | данные, привязанные к записи conntrack | только если есть conntrack, может не быть |
| arg | table | все аргументы инстанса и их значения | подстановки % и # уже замещены |
| dis | table | диссект текущего пакета |
Структура диссекта
Диссект включает в себя поля ip, ip6, tcp, udp.
ip присутствует в случае ipv4, ip6 - в случае ipv6.
По наличию ip или ip6 можно определить версию ip протокола.
По наличию tcp или udp можно определять L4 протокол.
Сами таблицы хедеров копируют названия полей C структур из netinet/{ip,ip6,tcp,udp}.h.
ip адреса и ipv4 options передаются как raw string.
Для преобразования raw ip в текстовую форму можно использовать C функцию ntop. Она определяет версию ip автоматически по размеру.
ipv6 extension headers и tcp options представляются в форме таблиц.
Все числовые многобайтовые значения автоматически переведены из network byte order в machine byte order.
dissect
| Поле | Тип | Описание |
|---|---|---|
| ip | table | заголовок ipv4 |
| ip6 | table | заголовок ipv6 |
| frag_off | number | смещение IP фрагмента. присутствует только в IP фрагментах. |
| tcp | table | заголовок tcp |
| udp | table | заголовок udp |
| icmp | table | заголовок icmp |
| l4proto | number | IPPROTO_TCP или IPPROTO_UDP |
| transport_len | number | длина пакета без L3 заголовков |
| l3_len | number | длина L3 заголовков, включая ip options и ipv6 extension headers |
| l4_len | number | длина L4 заголовка, включая tcp options |
| payload | string | L4 пейлоад или содержимое пакета после L3 заголовков для raw IP |
ip
| Поле | Описание |
|---|---|
| ip_v | версия ip - 4 |
| ip_hl | длина ip заголовка в блоках по 4 байта. 5 без ip options. |
| ip_tos | type of service. содержит DSCP |
| ip_len | полная длина ip пакета вместе со всеми заголовками и пейлоадом |
| ip_id | идентификация пакета для сборки из фрагментов |
| ip_off | offset фрагмента, флаги MF (more fragments) и DF (dont fragment) |
| ip_ttl | time to live - максимальное количество хопов |
| ip_p | номер ip протокола. как правило IPPROTO_TCP или IPPROTO_UDP |
| ip_sum | чексумма ip хедера |
| ip_src | ip источника |
| ip_dst | ip назначения |
| options | бинарный блок ip options (практически не используется, режется всеми) |
ip6
| Поле | Описание |
|---|---|
| ip6_flow | первые 4 байта ipv6 header : version (6), traffic class, flow label |
| ip6_plen | длина пакета за вычетом базового хедера ipv6 - IP6_BASE_LEN (40) байт |
| ip6_nxt | следующий протокол. если нет exthdr - IPPROTO_TCP (6) или IPPROTO_UDP (17) |
| ip6_hlim | hop limit. имеет тот же смысл, что и TTL в ipv4 |
| ip6_src | ipv6 адрес источника |
| ip6_dst | ipv6 адрес приемника |
| exthdr | массив таблиц расширенных хедеров (индекс от 1) |
ip6 exthdr
| Поле | Описание |
|---|---|
| type | тип хедера : IPPROTO_HOPOPTS, IPPROTO_ROUTING, IPPROTO_DSTOPTS, IPPROTO_MH, IPPROTO_HIP, IPPROTO_SHIM6, IPPROTO_FRAGMENT, IPPROTO_AH |
| next | тип следующего хедера. аналогично type. для последнего хедера может быть IPPROTO_TCP или IPPROTO_UDP |
| data | данные без первых двух байтов - типа и длины |
udp
| Поле | Описание |
|---|---|
| uh_sport | порт источника |
| uh_dport | порт приемника |
| uh_ulen | длина udp - header UDP_BASE_LEN (8) + длина пейлоада |
| uh_sum | чексумма udp |
tcp
| Поле | Описание |
|---|---|
| th_sport | порт источника |
| th_dport | порт приемника |
| th_x2 | зарезервированное поле. используется для расширенных tcp flags |
| th_off | размер tcp хедера в блоках по 4 байта |
| th_flags | tcp флаги : TH_FIN,TH_SYN,TH_RST,TH_PUSH,TH_ACK,TH_URG,TH_ECE,TH_CWR |
| th_seq | sequence number |
| th_ack | acknowledgement number |
| th_win | размер tcp окна |
| th_sum | чексумма tcp |
| th_urp | urgent pointer |
| options | массив таблиц tcp опций (индекс от 1) |
tcp options
| Поле | Описание |
|---|---|
| kind | тип опции : TCP_KIND_END, TCP_KIND_NOOP, TCP_KIND_MSS, TCP_KIND_SCALE, TCP_KIND_SACK_PERM, TCP_KIND_SACK, TCP_KIND_TS, TCP_KIND_MD5, TCP_KIND_AO, TCP_KIND_FASTOPEN |
| data | блок данных опции без kind и length. отсутствует для TCP_KIND_END и TCP_KIND_NOOP |
icmp
Заголовком icmp считается его постоянная часть - первые 8 байт. Они универсальны как для ipv4, так и для ipv6 версии.
Остальное содержимое icmp зависит от типа icmp и располагается в payload, включая возможные специальные поля заголовка или обрезанный исходный пакет, на который был сгенерирован icmp.
| Поле | Описание |
|---|---|
| icmp_type | тип icmp |
| icmp_code | код icmp |
| icmp_cksum | чексумма icmp |
| icmp_data | 32-битное поле данных со смещения 4 |
Особенности приема многопакетных пейлоадов
Сборка многопакетного пейлоада называется реасм (reasm - reassemble).
Она производится автоматически C кодом, если встречен пейлоад, требующий сборки, доступен conntrack и не указан запрет на сборку через --reasm-disable.
На текущий момент таких пейлоадов 2 - tls_client_hello и quic_initial. Оба могут содержать постквантовую криптографию kyber, которая не помещается в один пакет.
Для tls_client_hello производится обычная сборка пейлоадов последовательных tcp сегментов и объединение их в единый блок reasm_data.
Для quic_initial отдельные пакеты накапливаются во внутреннем буфере, после чего происходит их дешифровка, объединение и дефрагментация разбросанных по пакетам и разным смещениям частей пейлоада (так делает chrome, чтобы заставить всех не упрощать свои алгоритмы, следовать стандартам и уметь корректно собирать пейлоад по частям).
Пока сборка не финализирована, выполняется накопление пакетов во внутреннем буфере без вызовов Lua. Как только она финализирована, происходит перепроигрывание отдельных частей (replay). В Lua инстансы приходит диссект каждого задержанного пакета, но при этом устанавливаются поля desync.replay=true, desync.replay_piece, desync.replay_count и desync.replay_piece_last.
В случае стандартной tcp сборки выставляется поле desync.reasm_data, содержащее полный блок собранных данных. В desync.dis.payload по-прежнему возвращаются пейлоады отдельных перепроигрываемых пакетов. Для tcp, если нет replay, desync.reasm_data содержит копию desync.dis.payload.
При сборке quic desync.reasm_data отсутствует. Вместо него передается поле desync.decrypt_data, содержащее результат дешифровки и дефрагментации всех пейлоадов, входящих в сборку. Для quic reasm_data содержит tls_client_hello без record layer.
Структура track
Таблица track присутствует в desync только, если для текущего пакета была найдена запись в conntrack.
Она может быть не найдена, если процесс nfqws2 не получил SYN или SYN,ACK пакет.
Например, соединение было установлено ранее, чем был запущен nfqws2, или вы не перехватили SYN и SYN,ACK из ядра,
или если вы принудительно выключили conntrack через --ctrack-disable.
Весь ваш код должен проверять наличие track прежде, чем обращаться к нему, иначе код будет падать с ошибками.
То же самое верно для опциональных полей track. Проверяйте ваш код с --ctrack-disable и на разных протоколах - tcp, udp.
track
| Поле | Тип | Описание | Примечание |
|---|---|---|---|
| incoming_ttl | number | ttl/hl первого входящего пакета по потоку | может не быть, если не определено |
| l7proto | string | протокол потока | есть всегда. если неизвестно - unknown |
| hostname | string | имя хоста. определяется на основе анализа L6/L7 протоколов | появляется только после определения |
| hostname_is_ip | bool | является ли hostname ip адресом | только если есть hostname |
| lua_state | table | таблица для хранения состояния, привязанного к потоку | есть всегда, передается с каждым пакетом потока |
| lua_in_cutoff | bool | отсечение Lua от входящего направления | только для чтения |
| lua_out_cutoff | bool | отсечение Lua от исходящего направления | только для чтения |
| t_start | number | unix time первого пакета потока | включает дробную часть с высокой точностью |
| pos | table | счетчики по различным направлениям | содержит таблицы client, server, direct, reverse |
Таблица track.pos содержит подтаблицы с набором счетчиков по двум направлениям - client и server.
client означает пакеты от клиента, server - пакеты от сервера.
direct and reverse являются просто ссылкам на client и server. Куда указывает direct и reverse зависит
от текущего направления - desync.outgoing и серверного режима - b_server.
direct всегда указывает на текущее направление, reverse - на противоположное.
track.pos содержит еще одно поле - dt. Время получения пакета в секундах с момента t_start.
Включает дробную часть с высокой точностью.
Список полей таблицы счетчиков приведен ниже. Подтаблица tcp присутствует только для tcp протокола.
| Поле | Описание | Примечание |
|---|---|---|
| pcounter | счетчик пакетов | |
| pdcounter | счетчик пакетов с данными | пакеты, у которых размер L4 пейлоада не равен 0 |
| pbcounter | счетчик переданных байт | считаются только размеры пейлоада L4, заголовки - нет |
| ip6_flow | последнее поле ip6.ip6_flow | отсутствует, если неизвестно или ip протокол - не ipv6 |
| tcp.seq0 | начальный sequence соединения | |
| tcp.seq | sequence текущего пакета | |
| tcp.rseq | relative sequence текущего пакета | вычисляется как seq-seq0 |
| tcp.rseq_over_2G | был переход rseq за границу 2 GB | s и p позиции больше нельзя учитывать |
| tcp.pos | relative sequence верхней границы текущего пакета | вычисляется как rseq+payload_size |
| tcp.uppos | максимальный pos в соединении | |
| tcp.uppos_prev | uppos в предыдущем пакете с данными | полезно для определения ретрансмиссий |
| tcp.winsize | последнее поле th_win | без коррекции по scale |
| tcp.scale | последнее значение tcp опции scale | |
| tcp.winsize_calc | коррекция winsize с учетом scale | эффективное значение tcp window size |
| tcp.mss | последнее значение tcp опции mss |
mss, winsize, scale передаются от одной стороны соединения другой стороне, чтобы она знала о допустимых параметрах партнера.
При использовани этих полей важно не перепутать сторону.
Если вам нужно узнать какие по размеру пакеты можно отсылать , вам нужно смотреть противоположную сторону - что она может принять.
mss дублируется в поле desync.tcp_mss независимо от наличия conntrack. Значение там уже рассчитано на то, что оно будет использовано
для вычисления размера пакета для отсылки.
Если вдруг conntrack нет или mss не был согласован сторонами, выставляется значение по умолчанию - DEFAULT_MSS (1220).
При работе с sequence нужно учитывать их 32-битную беззнаковую природу.
Если к 4294967280 (0xFFFFFFF0) прибавить 100, будет не 4294967380 (0x100000054), а 84 (0x54).
Если вы сложите в Lua эти числа - вы получите 4294967380, потому что там используется представление чисел с разрядностью более 32 и со знаком.
Для операций с sequence и их сравнения рекомендуется использовать C функции u32add и bitand.
Например, выражение 0==bitand(u32add(seq1, -seq2), 0x80000000) соответствует seq1>=seq2.
Но последний простой вариант не будет работать корректно, а первый - будет, если seq1 ушел от seq2 не более, чем на 2 GB.
Большее и не отследить через sequence. Нужно всегда учитывать, что при передаче больших объемов данных они не могут служить счетчиком.p*counter являются 64-битными счетчиками, поэтому у них этой проблемы нет.
Особенности обработки icmp
Некоторые типы icmp могут содержать прикрепленный исходный пакет, на который был сгенерирован icmp. Они называются "related".
Такие пейлоады распознаются, по ним ищется исходная запись conntrack.
Если она находится, выбирается кэшированный профиль (тот, к которому относится прикрепленный пакет).
Направление выбирается как обратное относительно найденной записи.
Тип пейлоада устанавливается как "ipv4" или "ipv6", тип протокола сеанса - из профиля исходного пакета.
Далее icmp проходит обычным образом по профилю. Нужно учитывать, что в функцию может прийти диссект с icmp, без tcp или udp,
но с desync.track
Если icmp не содержит прикрепленного пакета, он инвалидный или не найдена запись conntrack, icmp проходит самостоятельно
без track.
conntrack работает только с tcp и udp, он не ведет учет пингов или других типов icmp.
При проходе icmp по записи conntrack никакие счетчики не меняются.
Особенности обработки raw ip
Если ip протокол не распознан как tcp,udp,icmp,icmpv6, он считается raw ip.
В диссекте выдаются поля ip, ip6, payload. payload включает содержимое пакета после L3 заголовков.
desync.track всегда отсутствует.
Таймеры
Lua код может вызываться и вне привязки к принимаемым данным из сети.
Источником события является время.
Таймер - это объект nfqws2, идентифицируемый по уникальному имени и позволяющий вызывать
указанную Lua функцию с определенной периодичностью, либо однократно через какое-то время.
Установкой таймера занимается функция timer_set, удаляет таймер функция timer_del.
nfqws2 - программа однопоточная, как и движок Lua. Таймеры вызываются в контексте того же потока, что и обработка сетевых данных.
В Linux и Windows пакеты принимаются не по одному, а блоками. Блок обрабатывается до полного исчерпания оставшихся пакетов.
Таймеры вызываются между обработкой блоков. Если обработка заняла достаточно много времени, то вызов таймера может быть
не идеальным по времени.
Таймеры могут быть полезны для обработки ситуаций отсутствия реакции из сети на отсылаемые пакеты.
Например, вам надо что-то куда-то послать и прочитать ответ. Но оппонент может и не ответить или ответ может не дойти.
Чтобы ваша схема не подвисла в неопределенном состоянии и не оставила мусор в памяти, может помочь таймер.
С помощью таймеров, desync функций и функций отсылки можно построить полноценную машину состояний - вплоть
до собственной реализации tcp или иной системы гарантированной доставки.
Проектировать схему нужно асинхронно в стиле конечного автомата. Прямые задержки sleep в коде не предусмотрены, поскольку ломают
схему обработки трафика через очередь. Пока вы ждете - остальное повиснет.
Другое возможное применение - различного рода задержки трафика. Запомнить, дропнуть, отослать через какое-то время.
Без таймеров отсылать все придется сразу без задержки.
В режиме --intercept=0 при наличии таймеров nfqws2 не выходит сразу, а исправно вызывает таймеры, пока не останется ни одного.
После этого процесс завершается.
Прототип функции таймера
function timer(name, data)Таймер-функции может быть передана произвольная Lua переменная любого типа.
Таблицу можно использовать для хранения состояния, привязанного к таймеру.
Переменные простых типов могут односторонне передать какие-то read-only значения.
data задается при запуске таймера через timer_set.
При удалении таймера переменная автоматически освобождается, если на нее нет других ссылок.
Если таймер-функция вываливается с error, таймер удаляется принудительно.
Долбежки по error не будет - таймер просто останавливается и перестает работать.
Поэтому при разработке таймер функций особо важно не допускать варианты с error.
Однократный таймер автоматически удаляется после вызова таймер-функции. Самостоятельно удалять не требуется, хотя это и допустимо.
С интерфейс nfqws2
Перед выполнением --lua-init C код выставляет базовые константы, блобы и C функции.
Базовые константы
| Поле | Тип | Описание | Примечание |
|---|---|---|---|
| qnum | number | номер очереди NFQUEUE | только в Linux |
| divert_port | number | номер порта divert | только в BSD |
| desync_fwmark | number | fwmark для Linux, sockarg для BSD, 0 в Windows | |
| NFQWS2_VER | string | версия nfqws2 | строка, выводимая по --version |
| NFQWS2_COMPAT_VER | number | порядковый номер несовместимых изменений интерфейса с nfqws2 | увеличивается на 1 после каждого изменения |
| b_debug | bool | включен --debug | вывод отладочных сообщений |
| b_daemon | bool | включен --daemon | демонизация процесса, отвязка от tty |
| b_server | bool | включен --server | серверный режим |
| b_ipcache_hostname | bool | включен --ipcache-hostname | кэширование имен хостов, соответствующих IP адресам |
| b_ctrack_disable | bool | включен --ctrack-disable | отключен conntrack |
| VERDICT_PASSVERDICT_MODIFYVERDICT_DROPVERDICT_PRESERVE_NEXT | number | код вердикта desync функцииVERDICT_PRESERVE_NEXT складывается как бит | |
| DEFAULT_MSS | number | значение MSS по умолчанию | 1220 |
| IP_BASE_LEN | number | базовый размер ipv4 хедера | 20 |
| IP6_BASE_LEN | number | базовый размер ipv6 хедера | 40 |
| TCP_BASE_LEN | number | базовый размер tcp хедера | 20 |
| UDP_BASE_LEN | number | размер udp хедера | 8 |
| TCP_KIND_ENDTCP_KIND_NOOPTCP_KIND_MSSTCP_KIND_SCALETCP_KIND_SACK_PERMTCP_KIND_SACKTCP_KIND_TSTCP_KIND_MD5TCP_KIND_AOTCP_KIND_FASTOPEN | number | коды типов tcp опций (kinds) | |
| TH_FINTH_SYNTH_RSTTH_PUSHTH_ACKTH_URGTH_ECETH_CWR | number | tcp флаги | можно складывать через + |
| IP_MF | number | флаг IP "more fragments" | 0x8000, часть поля ip_off |
| IP_DF | number | флаг IP "dont fragment" | 0x4000, часть поля ip_off |
| IP_RF | number | флаг IP "reserved" | 0x2000, часть поля ip_off |
| IP_OFFMASK | number | битовая маска поля ip_off, соответствующая fragment offset | 0x1FFF |
| IP_FLAGMASK | number | битовая маска поля ip_off, соответствующая IP флагам | 0xE000 |
| IPTOS_ECN_MASK | number | битовая маска поля ip_tos, соответствующая ECN | 0x03 |
| IPTOS_ECN_NOT_ECT | number | Not ECN-Capable Transport | 0x00 |
| IPTOS_ECN_ECT1 | number | ECN Capable Transport(1) | 0x01 |
| IPTOS_ECN_ECT0 | number | ECN Capable Transport(0) | 0x02 |
| IPTOS_ECN_CE | number | Congestion Experienced | 0x03 |
| IPTOS_DSCP_MASK | number | битовая маска поля ip_tos, соответствующая DSCP | 0xFC |
| IP6F_MORE_FRAG | number | бит "More fragment" поля ip6f_offlg из ipv6 fragment header | 0x0001 |
| IPV6_FLOWLABEL_MASK | number | flow label в ip6_flow | 0x000FFFFF |
| IPV6_FLOWINFO_MASK | number | flow label, traffic class в ip6_flow | 0x0FFFFFFF |
| IPPROTO_IPIPPROTO_IPV6IPPROTO_IPIPIPPROTO_ICMPIPPROTO_ICMPV6IPPROTO_TCPIPPROTO_UDPIPPROTO_SCTPIPPROTO_HOPOPTSIPPROTO_ROUTINGIPPROTO_FRAGMENTIPPROTO_AHIPPROTO_ESPIPPROTO_DSTOPTSIPPROTO_MHIPPROTO_HIPIPPROTO_SHIM6IPPROTO_NONE | number | номера IP протоколов | используются в ipv4 и ipv6 |
| ICMP_ECHOREPLYICMP_DEST_UNREACHICMP_REDIRECTICMP_ECHOICMP_TIME_EXCEEDEDICMP_TIMESTAMPICMP_TIMESTAMPREPLYICMP_INFO_REQUESTICMP_INFO_REPLY | number | типы icmp | только ipv4 |
| ICMP_UNREACH_NETICMP_UNREACH_HOSTICMP_UNREACH_PROTOCOLICMP_UNREACH_PORTICMP_UNREACH_NEEDFRAGICMP_UNREACH_SRCFAILICMP_UNREACH_NET_UNKNOWNICMP_UNREACH_HOST_UNKNOWNICMP_UNREACH_NET_PROHIBICMP_UNREACH_HOST_PROHIBICMP_UNREACH_TOSNETICMP_UNREACH_TOSHOSTICMP_UNREACH_FILTER_PROHIBICMP_UNREACH_HOST_PRECEDENCEICMP_UNREACH_PRECEDENCE_CUTOFF | number | коды icmp для destination unreachable | только ipv4 |
| ICMP_REDIRECT_NETICMP_REDIRECT_HOSTICMP_REDIRECT_TOSNETICMP_REDIRECT_TOSHOST | number | коды icmp для icmp redirect | только ipv4 |
| ICMP_TIMXCEED_INTRANSICMP_TIMXCEED_REASS | number | коды icmp для time exceeded | только ipv4 |
| ICMP6_ECHO_REQUESTICMP6_ECHO_REPLYICMP6_DST_UNREACHICMP6_PACKET_TOO_BIGICMP6_TIME_EXCEEDEDICMP6_PARAM_PROBMLD_LISTENER_QUERYMLD_LISTENER_REPORTMLD_LISTENER_REDUCTIONND_ROUTER_SOLICITND_ROUTER_ADVERTND_NEIGHBOR_SOLICITND_NEIGHBOR_ADVERTND_REDIRECT | number | типы icmpv6 | только ipv6 |
| ICMP6_DST_UNREACH_NOROUTEICMP6_DST_UNREACH_ADMINICMP6_DST_UNREACH_BEYONDSCOPEICMP6_DST_UNREACH_ADDRICMP6_DST_UNREACH_NOPORT | number | коды icmpv6 для destination unreachable | только ipv6 |
| ICMP6_TIME_EXCEED_TRANSITICMP6_TIME_EXCEED_REASSEMBLY | number | коды icmpv6 для time exceeded | только ipv6 |
| ICMP6_PARAMPROB_HEADERICMP6_PARAMPROB_NEXTHEADERICMP6_PARAMPROB_OPTION | number | коды icmpv6 для parameter problem | только ipv6 |
Стандартные блобы
- fake_default_tls - фейковый tls_client_hello от firefox без kyber, SNI=www.microsoft.com
- fake_default_http - http запрос к "www.iana.org"
- fake_default_quic - 0x40 + 619*0x00
Переменные окружения
| env | Назначение |
|---|---|
| WRITABLE | Директория, доступная на запись Lua. Результат опции --writable |
| APPDATALOW | (только Windows) Расположение AppData для low mandatory level. Сюда тоже можно записывать, но предпочтительно использовать --writable для кросс-платформенности. |
C функции
Логгинг
function DLOG(string)
function DLOG_ERR(string)
function DLOG_CONDUP(string)Функции выводят строку с добавлением EOL в --debug лог.
- DLOG - обычный вывод
- DLOG_CONDUP - обычный вывод + вывод на консоль, если включено логирование в файл или syslog
- DLOG_ERR - аналогично DLOG_CONDUP, но все выводимое на консоль идет в stderr
Конвертация IP
function ntop(raw_ip)
function pton(string_ip)- ntop конвертирует raw строку с байтами ipv4 или ipv6 адреса в читаемое строковое представление. версия IP определяется по размеру raw_ip - 4 или 16 байт. При несоответствии размера возвращается nil.
- pton конвертирует строковое представление ipv4 или ipv6 адреса в raw_ip. Если строковое представление не является корректным ipv4 или ipv6 адресом, возвращается nil.
Битовые операции
Lua 5.1, на котором основан luajit, не имеет встроенных битовых операций. Luajit имеет встроенный модуль bitop.
Lua 5.3 имеет встроенные битовые операции, но не имеет встроенного модуля bitop. Он может быть подгружен,
но только если не использована статическая компиляция и если модуль установлен. nfqws2 на github собирается статически.
При работе с полями сетевых пакетов без битовых операций никуда.
Битовые операции и сдвиги обычно реализуются одной машинной командой. Для процессора это родные операции.
Заменять их конструкциями, основанных на математике с плавающей точкой неразумно (возведение в степень, деление, округление и тд),
особенно в условиях частого отсутствия FPU на роутерах и других embedded устройствах.
Чтобы не зависеть от всей этой неразберихи, в nfqws2 выставляется свой собственный набор битовых функций и функций сдвига,
который не зависит от типа движка Lua и его версии. Все типы битовых операций работают с беззнаковыми числами от 8 до 48 бит.
При передаче отрицательных чисел они интерпретируются в дополнительном коде. Например, в 32 битах -2 становится 0xFFFFFFFE, в 8 битах - 0xFE.
Большая разрядность не поддерживается, поскольку возникают несовместимости между Lua 5.3+ и более старыми версиями.
Только в Lua 5.3 реализован тип integer 64 bit. В более старых используется формат плавающей точки double с мантиссой 52 бит, что позволяет передать до 48 бит int.
Стандартные операции сдвига и побитовые логические операции :
function bitlshift(u48, bits)
function bitrshift(u48, bits)
function bitand(u48_1, u48_2, ...., u48_N)
function bitor(u48_1, u48_2, ...., u48_N)
function bitxor(u48_1, u48_2, ...., u48_N)
function bitnot(u48)
function bitnot8(u8)
function bitnot16(u16)
function bitnot24(u24)
function bitnot32(u32)
function bitnot48(u48)bitand, bitor и bitxor работают с произвольным количеством чисел.
bitnot имеет версии для различных разрядностей. Вариант bitnot является синонимом bitnot48.
Операции получения и установки отдельных битов :
function bitget(u48, bit_from, bit_to)
function bitset(u48, bit_from, bit_to, set)- bitget получает число из диапазона битов u48 с номерами от bit_from до bit_to. нумерация битов с 0.
- bitset записывает число set в диапазон битов u48 с номерами от bit_from до bit_to. нумерация битов с 0. старшие биты set, выходящие за пределы (bit_to-bit_from), игнорируются.
Операции с беззнаковыми числами
При операциях с числами без знака всегда важна разрядность. От нее зависит результат.
Поэтому все функции имеют в своем названии разрядность. При передаче аргументов, выходящих за пределы разрядности, вызывается error.
uX
function u8(raw_string[, offset])
function u16(raw_string[, offset])
function u24(raw_string[, offset])
function u32(raw_string[, offset])
function u48(raw_string[, offset])Эти функции используются для извлечения числовых полей в формате big endian из raw строки.
offset - номер байта от начала raw строки, начиная с 1.
Аналогичные встроенные средства (string.unpack) есть только в Lua 5.3.
buX
function bu8(u8)
function bu16(u16)
function bu24(u24)
function bu32(u32)
function bu48(u48)Преобразуют число в raw строку в формате big endian.
Чтобы собрать структуру из числовых полей, можно использовать обычную операцию конкатенации строк ..
swapX
function swap16(u16)
function swap24(u24)
function swap32(u32)
function swap48(u48)Инвертируют порядок следования байт в u16, u24, u32 или u48. Если в вашей структуре порядок байт little endian,
можно использовать uX/buX + swapX.
uXadd
function u8add(u8_1, u8_2, ...., u8_N)
function u16add(u16_1, u16_2, ...., u16_N)
function u24add(u24_1, u24_2, ...., u24_N)
function u32add(u32_1, u32_2, ...., u32_N)
function u48add(u48_1, u48_2, ...., u48_N)Функции для сложения произвольного количества беззнаковых чисел указанной разрядности.
Операнды должны вписываться в указанную разрядность, иначе вызывается error.
Перенос старшего разряда игнорируется.
Целочисленное деление
divint
Встроенное целочисленное деление есть только в Lua 5.3+, где есть тип данных integer.
Чтобы не возиться с округлением, целочисленное деление реализовано C функцией :
function divint(dividend, divisor)В этой функции нет ограничения на разрядность. Внутри используется тип int64_t.
В Lua 5.3+ потерь разрядности нет, в более старых будут искажения чисел, если разрядность операндов или результата превышает размер мантиссы типа double.
Генерация случайных данных
brandom
Функции генерируют raw строку указанного размера , состоящую из случайных байт.
Случайные данные не являются криптографически стойкими.
function brandom(size)
function brandom_az(size)
function brandom_az09(size)- brandom возвращает байты от 0 до 255
- brandom_az - символы от 'a' до 'z'
- brandom_az09 - символы от 'a' до 'z' и числа от '0' до '9'
Парсинг
parse_hex
function parse_hex(hex_string)Возвращает raw строку с байтами из hex строки. "1234ABCD" => "\0x11\x34\0xAB\0xCD".
В случае некорректности hex_string возвращается nil.
Криптография
В Lua есть стандартный модуль биндинга к openssl, откуда можно взять широкий набор криптографических функций.
Но завязываться на внешние модули нельзя - Lua обычно линкуется статически без возможности загрузки внешних модулей.
Не должно быть лишних зависимостей и дополнительных файлов. openssl имеет размер несколько Mb, что критично для embedded систем.
nfqws2 не использует никакие криптобиблиотеки, но имеет минимальный набор криптографических операций для работы с некоторыми протоколами (QUIC).
Эти функции выставляются в Lua и могут использоваться для любых целей.
bcryptorandom
function bcryptorandom(size)Генерирует raw строку - криптографически стойкий блок случайных данных указанного размера.
Источник - getrandom() (Linux), getentropy() (BSD), /dev/random./dev/urandom используется как fallback, если остальные варианты не работают или блокируют.
Отсутствие данных в /dev/random - типичная проблема для Android.
bxor,bor,band
function bxor(data1, data2)
function band(data1, data2)
function bor(data1, data2)Возвращает результат побайтового xor,and или or между raw строками data1 и data2. data1 и data2 должны быть равны по длине,
иначе вызывается error.
hash
function hash(hash_type, data)Возвращает raw строку - hash от блока данных - raw строки data. hash_type может быть "sha256" или "sha224".
aes
function aes(encrypt, key, data)Простое шифрование или дешифрование одного блока aes.
- encrypt - true - encrypt , false - decrypt
- key - raw строка. размер должен быть 16,24,32 байт, что соответствует aes128,aes192,aes256
- data - raw строка. размер должен быть 16 байт
- возвращается raw строка 16 байт с результатом операции
- в случае неверных размеров key или data вызывается error
aes_gcm
function aes_gcm(encrypt, key, iv, data[, associated_data])Шифрование в режиме aes-gcm.
- encrypt - true - encrypt , false - decrypt
- key - raw строка. размер key должен быть 16,24,32 байт, что соответствует aes128,aes192,aes256
- iv - raw строка 12 байт. ОБЯЗАТЕЛЬНО ГЕНЕРИРУЕТСЯ СЛУЧАЙНО ДЛЯ КАЖДОГО ШИФРУЕМОГО БЛОКА ДАННЫХ И ПЕРЕДАЕТСЯ ВМЕСТЕ С НИМ. При ПОВТОРНОМ ИСПОЛЬЗОВАНИИ iv С ТЕМ ЖЕ КЛЮЧОМ ШИФРОВАНИЕ ЛЕГКО ВЗЛАМЫВАЕТСЯ. Для генерации iv следует использовать bcryptorandom.
- data - raw строка произвольного размера. Шифр использует гамму, поэтому исходные данные не привязаны к размеру блока AES.
- associated_data - нешифруемые данные, передаваемые вместе с шифрованным сообщением и участвующие в подсчете atag. Может быть nil.
- возвращается 2 значения : raw строка - блок шифрованных данных и raw строка atag (authentication tag). atag может быть передан вместе с шифрованным сообщением, iv и associated_data для проверки их целостности.
- в случае неверных размеров key или iv вызывается error
aes_ctr
function aes_ctr(key, iv, data)Шифрование в режиме aes-ctr.
- key - raw строка. размер key должен быть 16,24,32 байт, что соответствует aes128,aes192,aes256
- iv - raw строка 16 байт. ОБЯЗАТЕЛЬНО ГЕНЕРИРУЕТСЯ СЛУЧАЙНО ДЛЯ КАЖДОГО ШИФРУЕМОГО БЛОКА ДАННЫХ И ПЕРЕДАЕТСЯ ВМЕСТЕ С НИМ. При ПОВТОРНОМ ИСПОЛЬЗОВАНИИ iv С ТЕМ ЖЕ КЛЮЧОМ ШИФРОВАНИЕ ЛЕГКО ВЗЛАМЫВАЕТСЯ. Для генерации iv следует использовать bcryptorandom.
- data - raw строка произвольного размера. Шифр использует гамму, поэтому исходные данные не привязаны к размеру блока AES.
- шифрование работает по принципу xor с гаммой, поэтому симметрично. шифрование и дешифрование - одна операция.
- возвращается raw строка - блок шифрованных данных
- в случае неверных размеров key или iv вызывается error
hkdf
function hkdf(hash_type, salt, ikm, info, okm_len)HKDF - HMAC-based Key Derivation Function. Генератор ключей на базе произвольных входных данных - input keying material (ikm).
Функция включает extraction и expansion.
- hash_type может быть "sha256" или "sha224"
- salt - raw строка произвольного размера, может быть nil. "соль". несекретная информация. позволяет сделать результат разным на тех же ikm для разных значений salt. если nil, используется блок нулевых байт размером, равным размеру результата hash функции.
- ikm - raw строка - input keying material. на базе этих данных, salt и info генерируется okm - output keying material
- info - raw строка произвольного размера, может быть nil. аналогично salt, но salt подмешивается на extraction phase, а info - на expansion. если nil, то используется info нулевого размера.
- okm_len - требуемая длина okm - output keying material
- возвращается raw строка - okm
Компрессия
gunzip
function gunzip_init([windowBits])
function gunzip_end(zstream)
function gunzip_inflate(zstream, compressed_data[, expected_uncompressed_chunk_size])- gunzip_init создает и возвращает контекст gzip потока для последующих вызовов других функций. Значение windowBits см. в документации по zlib (по умолчанию 47).
- gunzip_end освобождает контекст gzip. может быть освобожден сборщиком мусора, но лучше вызывать явно.
- gunzip_inflate разжимает очередную часть зипованных данных. Данные можно скармливать частями. Разжатые части конкатенируются для получения полных данных. Возвращается 2 аргумента : разжатые данные и bool признак конца gzip. В случае испорченных данных или при нехватке памяти возвращается nil.
- expected_uncompressed_chunk_size - необязательный параметр для оптимизации выделения памяти под разжимаемые данные. Если буфера не хватает, вызываются realloc, копирующие блоки памяти и влияющие на производительность. Размер следует выбирать согласно ожидаемой степени сжатия с небольшим запасом. По умолчанию - четырехкратный размер compressed_data.
gzip
function gzip_init([windowBits[, level[, memlevel]]])
function gzip_end(zstream)
function gzip_deflate(zstream, uncompressed_data[, expected_compressed_chunk_size])- gzip_init создает и возвращает контекст gzip потока для последующих вызовов других функций. Значение windowBits см. в документации по zlib (по умолчанию 31). level - уровень сжатия от 1 до 9 (по умолчанию 9), memlevel - допустимый уровень использования памяти от 1 до 8 (по умолчанию 8).
- gzip_end освобождает контекст gzip. может быть освобожден сборщиком мусора, но лучше вызывать явно.
- вместо явного вызова gzip_end в Lua 5.5+ для контекста можно использовать to-be-closed variable
- gzip_deflate сжимает очередную часть данных. Данные можно скармливать частями. Сжатые части конкатенируются для получения полных данных. Для финализации потока по окончанию скармливания данных функция должна быть вызвана с uncompressed_data=nil или uncompressed_data="". Возвращается 2 аргумента : сжатые данные и bool признак конца gzip. При ошибках gzip или нехватке памяти возвращается nil.
- expected_compressed_chunk_size - необязательный параметр для оптимизации выделения памяти под сжимаемые данные. Если буфера не хватает, вызываются realloc, копирующие блоки памяти и влияющие на производительность. Размер следует выбирать согласно ожидаемой степени сжатия с небольшим запасом. По умолчанию - половина размера uncompressed_data.
Системные функции
uname
function uname()Возвращает то же самое, что и команда uname в shell - название ядра ОС. "Linux", "FreeBSD", "OpenBSD".
В Windows возвращается строка, начинающаяся с "CYGWIN", далее следует версия.
clock_gettime
function clock_gettime()
function clock_getfloattime()Узнать точное время. clock_gettime возвращает 2 значения - unixtime в секундах и наносекунды. Встроенная функция os.time() не выдает наносекунды.
clock_getfloattime возвращает unixtime в формате с плавающей точкой. Наносекунды идут в дробную часть.
getpid
function getpid()
function gettid()- getpid() возвращает идентификатор текущего процесса - PID
- gettid() возвращает идентификатор текущего потока - TID
stat
function stat(filename)В случае успеха возвращает таблицу :
| Поле | Тип | Описание |
|---|---|---|
| type | string | тип файла : file, dir, socket, blockdev, chardev, fifo, unknown |
| size | number | размер файла |
| mtime | number | unixtime время модификации в формате с плавающей точкой |
| inode | number | inode. на Windows не влезает в тип number luajit, но влезает в integer Lua 5.3+ |
| dev | number | device id |
В случае неудачи возвращает 3 значения : nil, строка ошибки, код ошибки errno.
time
function localtime(unixtime)
function gmtime(unixtime)
function timelocal(tm)
function timegm(tm)Функции localtime и gmtime возвращают таблицу, аналогичную struct tm в C. timelocal и timegm - обратные функции.
| Поле | Тип | Описание |
|---|---|---|
| sec* | number | секунды |
| min* | number | минуты |
| hour* | number | часы |
| mon* | number | месяц, начиная с 0 |
| mday* | number | день месяца |
| year* | number | год - полный, не с 1900 года |
| wday | number | день недели. 0 - воскресенье, 6 - суббота |
| yday | number | номер дня в году, начиная с 0 |
| isdst* | number | не 0, если включен переход на летнее/зимнее время |
| zone | string | часовой пояс (timezone) |
| str | string | отформатированная строка dd.mm.yyyy hh:mi:ss |
Поля, помеченные *, нужны для обратного перевода. Остальные не учитываются.
Опции по работе с пакетами
В следующих функциях будут использоваться стандартные наборы опций - rawsend и reconstruct.
Они представляют собой таблицы со специфическими полями. Если nil, считается, что ни одно поле не задано.
standard reconstruct
Опции реконструкции диссектов - reconstruct_opts. Реконструкция - это сборка raw пакета из диссекта.
| Поле | Тип | Описание |
|---|---|---|
| keepsum | bool | взять чексумму L4 из диссекта, не считать и не портить |
| badsum | bool | испортить L4 checksum. посчитать чексумму и сделать xor со случайным значением от 1 до 0xFFFF |
| ip6_preserve_next | bool | использовать значения "next" из ip6.exthdr |
| ip6_last_proto | number | если ip6_preserve_next=false, IP протокол последнего exthdr |
При сборке ipv6 по умолчанию цепочка ip протоколов в exthdr собирается автоматически.
У каждого exthdr есть поле type, поэтому понятно что вписывать в предыдущий exthdr,
либо в основной ip6 хедер. next протокол последнего exthdr устанавливается как IPPROTO_TCP,
IPPROTO_UDP, IPPROTO_ICMP, IPPROTO_ICMPV6 в зависимости от наличия в диссекте таблиц tcp, udp, icmp, ip6.
В большинстве случаев это удобно, поскольку вам при вставлении exthdr не надо реконструировать
всю цепочку next протоколов и не надо заполнять поля next. За вас это сделает реконструктор диссекта.
Режим ip6_preserve_next используется, если у вас специальная цель, требующая ручной крафтинг полей next protocol.
В этом случае поля next в exthdr и ip6.ip6_nxt оставляются как есть.
Если не задан ip6_preserve_next и задан ip6_last_proto, ip протокол полезной нагрузки замещается на ip6_last_proto.
badsum вынесен в реконструкцию, поскольку чексуммы tcp и udp считаются на базе всего IP пакета.
В сумме участвуют элементы хедера ip/ip6, весь хедер tcp и сам пейлоад.
Поэтому по отдельным частям гарантированно испортить чексумму невозможно.
Что бы вы туда не вписали, существует маленькая вероятность (1/65536), что она окажется верной.
Пакет формируется на базе L3 заголовка, далее к нему пристыковывается L4 заголовок - tcp,udp,icmp, если присутствует, в конце идет payload.
В процессе реконструкции это всегда так - поля ip protocol не учитываются. Поэтому можно конструировать tcp,udp,icmp пакеты с измененным ip protocol.
standard rawsend
Опции отсылки raw пакетов - rawsend_opts
| Поле | Тип | Описание |
|---|---|---|
| repeats | number | количество повторов отсылки одного и того же пакета |
| fwmark | number | fwmark исходящего пакета. только для linux. по умолчанию 0. бит desync_mark устанавливается принудительно |
| ifout | string | исходящий интерфейс. может использоваться или не использоваться в разных ситуациях |
ifout всегда следует передавать таким, каким он пришел в диссекте.
Для Windows правильный ifout обязателен. На BSD он не используется.
На Linux используется только если включены опции --bind-fix4 или --bind-fix6 в зависимости от версии ip.
fwmark желательно передавать таким, каким он пришел в диссекте.
При особых случаях, когда на этом построены ваши правила таблиц, можно домешивать туда свои биты.
repeats шлют бинарно идентичный пакет указанное количество раз, не разбираясь в его содержимом.
Никаких изменений , в том числе ip_id, не производится.
Если вам нужно изменение ip_id, можно его сделать 0, тогда Windows сама допишет увеличивающиеся значения.
Другие системы - нет. Если вам надо на любых системах управлять ip_id, то repeats - не вариант.
Диссекция и реконструкция
Диссекция - процесс получения структурированного представления raw ip пакета.
Реконструкция - обратный процесс - получение raw ip из диссекта.
dissect
function dissect(raw_ip)Возвращает таблицу - диссект пакета raw_ip.
Это та же самая операция, что происходит автоматически до вызова desync функций по профилю.
Они получают уже готовый диссект.
reconstruct_dissect
function reconstruct_dissect(dissect[, reconstruct_opts])Возвращает raw_ip. Все чексуммы считаются автоматически. L4 чексуммы портятся, если задан badsum в reconstruct_opts.
Реконструкция диссектов с IP фрагментацией происходит особым образом. Есть 2 варианта.
- Если присутствует поле "frag_off", tcp/udp/icmp хедеры игнорируются, payload содержит raw ip payload. В таком виде приходят диссекты фрагментированных IP пакетов. nfqws2 не выполняет дефрагментацию на IP уровне. Но это очень настойчиво делают Linux системы - чтобы пришел фрагмент в nfqws2 надо сильно постараться, вставив в prerouting или output "notrack". Диссекты в таком виде могут быть реконструированы как есть. Но готовить их в Lua крайне неудобно, поскольку придется проходить через черную магию работы с бинарным представлением.
- Если поле "frag_off" отсутствует, реконструкция фрагментов выполняется из диссекта целого пакета с участием как Lua, так и C кода. Lua код должен подготовить диссект полного пакета, подлежащего фрагментации, но заполнить некоторые поля такими, какими они должны быть во фрагменте.
- ipv4 : ip.ip_len должен быть рассчитан таким, каким он должен быть во фрагменте.
По ip.ip_len С код определяет размер фрагментированной части.
Поле ip.ip_off должно содержать offset фрагмента и признак IP_MF, если это не последний фрагмент. ip.ip_id должен быть не 0. - ipv6 : нужно вставить в ip6.exthdr fragment header, в котором заполнить ident, offset и бит IP6F_MORE_FRAG, если это не последний фрагмент.
ip6.ip6_len должен быть рассчитан таким, каким он должен быть во фрагменте. По этой длине C код определяет размер фрагмента.
Позицию fragment header выбирает Lua код. Все, что после fragment header, считается fragmentable part.
Если C код видит признаки необходимости фрагментации, он проверяет корректность рассчитанных длин и смещений,
и если они корректны, после реконструкции сдвигает содержимое raw пакета в буфере реконструкции, чтобы получился фрагмент с нужными данными.
reconstruct_hdr
function reconstruct_tcphdr(tcp)
function reconstruct_udphdr(udp)
function reconstruct_icmphdr(icmp)
function reconstruct_iphdr(ip)
function reconstruct_ip6hdr(ip6 [,reconstruct_opts])Реконструкция соответствующих raw заголовков из таблиц диссекта. Возвращает raw вариант заголовка.
- реконструкция ip6 задействует reconstruct_opts. из них берется ip6_preserve_next и ip6_last_proto.
- чексумма ip header считается автоматически, поскольку ни от чего больше не зависит
- чексуммы tcp, udp и icmp не считаются, поскольку зависят от других компонент
csum_fix
function csum_ip4_fix(raw_ipv4_header)
function csum_tcp_fix(raw_ip_header, raw_tcp_header, payload)
function csum_udp_fix(raw_ip_header, raw_udp_header, payload)
function csum_icmp_fix(raw_ip_header, raw_icmp_header, payload)Функции для выправления чексумм. Поскольку строки в Lua immutable, они возвращают копию соответствующего заголовка
с исправленной чексуммой.
- с csum_ipv4_fix все просто. на входе ip заголовок, на выходе ip заголовок с исправленной суммой
- csum_tcp_fix, csum_udp_fix, csum_icmp_fix берут raw ip заголовок (ipv4 или ipv6), tcp/udp/icmp заголовок и пейлоад. версия ip определяется автоматически. чексумма считается на базе L3, L4 заголовков и пейлоада.
Прямая реконструкция отдельных заголовков нужна редко. Обычно все задачи выполняют функции по работе с диссектами.
conntrack
function conntrack_feed(dissect/raw_packet[, reconstruct_opts])"Скормить" conntrack пакет таким образом, как если бы он пришел из сети и был проанализирован.
Пакет может быть таблицей-диссектом или raw string. reconstruct_opts имеет смысл только для диссектов.
Возвращается 2 значения - track и bool признак "outgoing".
outgoing принимает значение true, если создается новая запись conntrack и она удовлетворяет признакам клиента - SYN в случае tcp, любой пакет в случае udp.
Если запись уже существует, outgoing = true, если запись была найдена по прямой паре - src_ip, src_port, dst_ip, dst_port.
Если запись была найдена по обратной паре - dst_ip, dst_port, src_ip, src_port, outgoing = false.
Функция может пригодиться, если выполняется обфускация и передача данных в искаженном виде. Например, tcp преобразуется в icmp или портится флаг SYN.
Принимающий конец выдаст первый искаженный пакет без track, потому что это либо не tcp и не udp, либо не было валидного tcp handshake.
После деобфускации вы можете исправить ситуацию, выполнив conntrack_feed и присвоив desync.track его результат.
Если conntrack выключен или пакет не является валидным tcp или udp, возвращается nil.
Получение ip адресов
function get_source_ip(target)Получить из таблицы маршрутизации ip адрес источника, который будет использован для подключению к ip адресу target.
target представляет собой raw string ipv4 или ipv6.
Функция возвращает raw string адреса источника или nil, если destination unreachable.
function get_ifaddrs()Получить список IP адресов на всех интерфейсах (аналог ip addr, ifconfig, ipconfig).
Возвращается таблица, индексы которой являются именами интерфейсов.
Для Windows имена интерфейсов представлены строками "число.число" - IfIdx.SubIfIdx - как раз то, что выдает и берет WinDivert.
Содержимое интерфейса :
| Поле | Тип | Описание |
|---|---|---|
| index | number | индекс интерфейса |
| mtu | number | MTU. для loopback может быть 64K или даже 0xFFFFFFFF |
| flags | number | битовые флаги. зависят от ОС |
| ssid | string | SSID wifi сети, если известен. имена сетей получаются только при использовании --filter-ssid |
| guidiftypeindex6speed_xmitspeed_recvmetric4metric6conntype | number | (только Windows) дополнительные поля из GetAdaptersAddresses() |
| addr | table | таблица адресов с числовыми индексами от 1 |
Содержимое адреса :
| Поле | Тип | Описание |
|---|---|---|
| addr | string | ipv4 или ipv6 адрес |
| netmask | string | маска подсети |
| broadcast | string | (только ipv4) broadcast адрес |
| dst | string | содержимое ifa_dstaddr из getifaddrs() |
Единственное гарантированное поле - addr. Остальных может не быть.
Прием и отсылка пакетов
rawsend
function rawsend(raw_data[, rawsend_opts])
function rawsend_dissect(dissect[, rawsend_opts[, reconstruct_opts]])- rawsend работает с raw строкой, содержащий полностью собранный ipv4 или ipv6 пакет
- rawsend_dissect собирает пакет из диссекта и отправляет
- dissect представляет собой таблицу, описанную в соответствующем разделе
raw_packet
function raw_packet(ctx)Lua функции получают готовый диссект текущего пакета при вызове. raw представление требуется редко, поэтому
в целях экономии ресурсов оно не выдается в desync.
Его можно получить по запросу через функцию raw_packet.
Работа с пейлоадами
маркеры
- Абсолютный положительный маркер - числовое смещение внутри пейлоада.
- Абсолютный отрицательный маркер - числовое смещение внутри пейлоада от следующего за концом байта. -1 указывает на последний байт.
- Относительный маркер - положительное или отрицательное смещение относительно логической позиции внутри пейлоада.
Относительные позиции :
- method - начало метода HTTP ('GET', 'POST', 'HEAD', ...). Метод обычно всегда находится на позиции 0, но может сместиться из-за methodeol. Тогда позиция может стать 1 или 2.
- host - начало имени хоста
- endhost - байт, следующий за последним байтом имени хоста
- sld - начало домена 2 уровня в имени хоста
- endsld - байт, следующий за последним байтом домена 2 уровня в имени хоста
- midsld - середина домена 2 уровня в имени хоста
- sniext - начало поля данных SNI extension в TLS. Любой extension состоит из 2-байтовых полей type и length, за ними идет поле данных.
- extlen - поле длины TLS extensions
Относительные маркеры работают с логическими элементами отдельных известных пейлоадов, поэтому они не будут работать с чем попало.
Пример списка маркеров : 100,midsld,sniext+1,endhost-2,-10.
resolve_pos
Задача следующих функций - преобразовать маркеры в абсолютные позиции.
function resolve_pos(blob,l7payload_type,marker[,zero_based_pos])
function resolve_multi_pos(blob,l7payload_type,marker_list[,zero_based_pos])
function resolve_range(blob,l7payload_type,marker_list[,strict,zero_based_pos])- resolve_pos работает с единственным маркером. если маркер не ресолвится, возвращается nil.
- resolve_multi_pos работает со списком маркеров через запятую. возвращает массив уникальных абсолютных позиций. если некоторые маркеры не ресолвятся - их не будет в результате.
- resolve_range ресолвит список из ровно 2 маркеров, представляющих собой диапазон внутри пейлоада. Если strict = true, и любой маркер не ресолвится, возвращается nil. Иначе если первый маркер не ресолвится - он заменяется на 0. Если не ресолвится второй маркер - он заменяется на длину пейлоада. Если не ресолвятся оба - возвращается nil.
- если задано zero_based_pos=true, все позиции начинаются с 0, иначе с 1, как это принято в Lua.
- при невалидных значениях l7payload_type, marker, marker_list, если количество маркеров не равно 2 для resolve_range - вызывается error
tls_mod
function tls_mod(blob, modlist[, payload])- blob - блоб, с которым производится действие
- payload - содержимое, под которое модифицируется blob. для разных модов может быть произвольным или требуется валидный tls.
- modlist - список модификаций через запятую
Моды :
- rnd - заполнить поля random и session id случайными данными
- dupsid - скопировать session id с payload. выполняется после rnd, требует валидного tls в payload, иначе не применяется
- rndsni - замена sni на случайный. если длина оригинального sni >=7 символов, выбирается случайный поддомен из известных 3-буквенных TLD. иначе без точки случайные символы
[a-z][a-z0-9]* - sni=domain.com - замена sni на конкретный домен
- padencap - подкорректировать blob таким образом, чтобы payload стал частью padding extension. payload может быть произвольным.
Управление выполнением инстансов
instance_cutoff
function instance_cutoff(ctx[, outgoing])Добровольное само-отсечение инстанса по выбранному направлению.
Инстанс перестает вызываться в рамках текущего потока.
- outgoing = true - исходящее направление
- outgoing = false - входящее направление
- outgoing = nil - оба направления
lua_cutoff
function lua_cutoff(ctx[, outgoing])Аналогично instance_cutoff, но от потока отсекается весь профиль.
При смене профиля после получения hostname или обнаружения протокола потока lua cutoff сбрасывается,
поскольку в новом профиле совершенно другой набор инстансов, которые не просили их отсекать.
Состояние lua cutoff может случиться и естественным образом, если все инстансы превысили верхнюю границу
range или самоотсекли себя от направления.
execution_plan
function execution_plan(ctx)Получить массив информации о всех последующих , еще не выполненных, инстансах текущего профиля,
их внутрипрофильных фильтрах и аргументах.
Элемент plan
| Поле | Тип | Описание |
|---|---|---|
| func | string | имя desync функции |
| func_n | number | номер инстанса внутри профиля |
| func_instance | string | название инстанса. производная имени функции, номера инстанса и номера профиля |
| range | table | эффективный диапазон счетчиков --in-range или --out-range в зависимости от текущего направления |
| payload | table | эффективный фильтр payload . таблица с индексами - названиями типа пейлоада |
| payload_filter | string | эффективный фильтр payload . список названий пейлоадов через запятую (иное представление payload) |
| arg | table | аргументы инстанса |
range
| Поле | Тип | Описание |
|---|---|---|
| from | table | позиция нижней границы |
| to | table | позиция верхней границы |
| upper_cutoff | bool | true = верхняя граница невключительна, false = включительна |
pos - from,to
| Поле | Тип | Описание |
|---|---|---|
| mode | string | режим счетчика - a, x, n, d, b, s, p |
| pos | number | значение счетчика |
execution_plan_cancel
function execution_plan_cancel(ctx)Однократная отмена выполнения всех следующих инстансов внутри профиля.
Инстанс, выполняющий отмену, берет на себя координацию дальнейших действий и называется оркестратором.
Управление таймерами
Функции создания и удаления таймера могут быть вызваны из любого Lua кода.
Это может быть lua-init, lua-desync или таймер функция. Таймер функция может в том числе действовать в отношении себя самой -
заменить период или прекратить собственные вызовы.
Таймеры идентифицируются именем. Несколько таймеров с разными именами могут вызывать одну и ту же таймер-функцию.
Ей будет передано имя таймера и произвольные данные в качестве параметров.
timer_set
Создать или заменить таймер.
function timer_set(name, func, period, oneshot, data)- name - уникальное имя таймера. если таймер с таким именем уже есть, он удаляется и замещается новым. При замещении отсчет времени начинается заново.
- func - таймер функция. string или function. в случае string функция ищется по имени в момент вызова timer_set и запоминается по ссылке, а не как строка.
- period - периодичность вызова таймера в миллисекундах
- oneshot - bool признак является ли таймер однократным (true) или периодическим (false)
- data - произвольная переменная, передаваемая таймер-функции
timer_del
Удалить таймер. Текущая функция не прерывается, но последующих вызовов не будет.
function timer_del(name)timer_info
Получить информацию о таймере.
function timer_info(name)В случае успеха возвращается таблица, в случае неудачи - nil.
| Поле | Тип | Описание |
|---|---|---|
| name | string | уникальное имя таймера |
| func | function | таймер функция |
| oneshot | bool | true = однократный таймер, false = периодический |
| period | number | период таймера в мсек |
| fires | number | количество выполненных вызовов таймера |
timer_enum
Получить массив уникальных имен всех существующих таймеров.
function timer_enum()Библиотека базовых функций zapret-lib.lua
Почти каждая функция имеет подробные комментарии о своем предназначении и параметрах.
Чтение Lua кода и комментариев позволит лучше понять для чего нужна конкретная функция и как ее вызывать.
Базовые desync функции
Их можно в готовом виде использовать в --lua-desync.
luaexec
function luaexec(ctx, desync)Выполнить произвольный Lua код, заданный в аргументе "code".
Код может адресовать таблицу desync - она временно присваивается глобальной переменной desync,
а по завершению кода глобальная переменная убирается.
Пример : --lua-desync=luaexec:code="desync.rnd=brandom(math.random(5,10))"
pass
function pass(ctx, desync)Ничего не делать, только вывести в debug log сообщение "pass".
pktdebug
function pktdebug(ctx, desync)Вывести структуру desync в debug log.
argdebug
function argdebug(ctx, desync)Вывести таблицу аргументов в debug log.
posdebug
function posdebug(ctx, desync)Вывести в debug log информацию о текущих conntrack позициях по прямому и обратному направлению.
detect_payload_str
function detect_payload_str(ctx, desync)- arg: pattern - подстрока для поиска в desync.dis.payload
- arg: payload - присвоить это значение desync.l7payload в случае наличия подстроки
- arg: undetected - если присутствует, присвоить это значение desync.l7payload в случае отсутствия подстроки
Пример простейшего протокольного детектора. Ищет подстроку pattern в пейлоаде, в случае нахождения присваивает desync.l7payload = desync.arg.payload , иначе если есть desync.arg.undetected, присваивает desync.l7payload = desync.arg.undetected.
Подобного рода протокольные детекторы не имеют никакой силы для C кода. Он ничего не знает о вашем протоколе и вашем типе пейлоада. Ваше значение нельзя указать в параметре --payload. Но можно использовать payload фильтры многих desync функций.
desync_orchestrator_example
function desync_orchestrator_example(ctx, desync)Тестовый оркестратор. Ничего специального не делает, только выполняет execution plan в исходном виде.
Служебные функции
var_debug
function var_debug(v)Выводит в debug log информацию о параметре v - тип и значение.
Если это таблица, происходит рекурсивный проход по вложенным значениям и таблицам, информация представляется в виде дерева.
deepcopy
function deepcopy(orig)Копирует переменную orig.
Основная цель - создать копию таблицы со всеми подтаблицами рекурсивно.
Таблицы в Lua передаются по ссылке. Через какую бы переменную вы не изменяли таблицу, таблица существует только одна.
Изменения будут видны через любые переменные, на нее ссылающиеся.
Чтобы сделать реальную копию, нужно создать новую таблицу и присвоить ей все поля исходной таблицы,
кроме подтаблиц. Подтаблицы надо точно так же копировать рекурсивно. Этим и занимается функция deepcopy.
Простые типы в Lua присваиваются по значению. Все строки хранятся в едином пуле, где исключено дублирование по содержимому.
Строковые переменные ссылаются на пул. Строки менять невозможно, они - immutable, можно только присваивать другие строки.
Если новая строка будет иной по значению, и ее нет в пуле, будет создан новый элемент пула. Иначе будет присвоена ссылка
на существующий элемент.
logical_xor
function logical_xor(a,b)Возвращает результат логического xor a и b. result = a and not b or not a and b
array_search
function array_search(a, v)
function array_field_search(a, f, v)Линейный поиск в таблице a значения v. array_field_search предполагает, что элемент таблицы a - таблица, поиск идет по полю f.
Работа со строками
in_list
function in_list(s, v)find_next_line
Включена ли строка v в список строк через запятую s. Например, abc включено в список xyz,abc,12345.
function find_next_line(s, pos)Работает с многострочным текстом s. Строки разделяются EOL - '\n' или '\r\n'.
Возвращается 2 значения - позиция начала строки и начала следующей строки, либо конца текста s, если строк больше нет.
Обслуживание raw string
hex
function string2hex(s)
function has_nonprintable(s)
function make_readable(s)
function str_or_hex(s)
function hexdump(s, max)
function hexdump_dlog(s)- string2hex преобразует raw строку в символьное hex представление. байты разделены пробелами. "\xAB\xCD\x01\0x2" => "AB CD 01 02"
- has_nonprintable возвращает true, если в строке s есть символы, кроме 0x20-0x7F, '\n', '\r', '\t'
- make_readable заменяет все символы, кроме 0x20-0x7F, точками
- str_or_hex возвращает саму строку, если has_nonpritable(s) = false, иначе string2hex(s)
- hexdump преобразует начальные байт raw строки s (до max байт) в hex строку + результат make_readable. Классический hex dump.
- hexdump_dlog выполняет hexdump и выводит результат в debug log
pattern
function pattern(pat, offset, len)pattern - это часть условно бесконечно повторяющейся raw строки pat, начинающаяся с позиции offset (нумерация с 1) и длиной len
blob
function blob(desync, name[, def])
function blob_or_def(desync, name[, def])- blob - стандартная функция получения блоба. Если name начинается с
0x, то дальнейшее интерпретируется как HEX строка.
Иначе читается переменная name сначала в desync. Если там нет, берется глобальная переменная.
Если и ее нет, берется значение def. Если name = nil или пустая строка, вызывается error. - blob_or_def - возвращает def, если name = nil, иначе аналогично blob
function barray(a, packer)
function btable(a, packer)- barray использует только числовые индексы, начиная с 1. порядок соблюдается
- btable использует все индексы, но не гарантирует порядок
Упаковка элементов массива a в порядке возрастания индекса от 1 до последнего.packer - функция, берущая элемент a и возвращающая raw string.
Для числовых массивов в качестве packer можно использовать функции паковки чисел.
Возвращается raw string.
Обслуживание tcp sequence numbers
function seq_ge(seq1, seq2)
function seq_gt(seq1, seq2)
function seq_lt(seq1, seq2)
function seq_le(seq1, seq2)
function seq_within(seq, seq_low, seq_hi)
function is_retransmission(desync)- seq_{ge|gt|lt|le} выполняет сравнение sequence numbers в пределах 2 GB. если разница больше - результат неверный. ge означает
>=, gt>, le<=, lt<. - seq_within -
seq_low <= seq <= seq_hi - is_retransmission - является ли текущий диссект tcp ретрансмиссией
Обслуживание позиций
function pos_counter_overflow(desync, mode[, reverse])
function pos_get_pos(track_pos, mode)
function pos_get(desync, mode[, reverse])
function pos_check_from(desync, range)
function pos_check_to(desync, range)
function pos_check_range(desync, range)
function pos_range_str(range)
function pos_str(desync, pos)Параметр mode содержит строку с одной буквой режима счетчика - 'a','x','n','d','b','s','p'.
По умолчанию функции работают с текущим направлением. Если есть параметр reverse и он задан как true, берется противоположное направление.
- pos_counter_overflow - true, если mode = 's' или 'p' и произошел выход relative tcp sequence за пределы 2 GB. Счетчики больше не могут использоваться.
- pos_get_pos - получить значение счетчика mode из таблицы счетчиков
track_pos.track_posможет бытьdesync.track.pos.{direct,reverse,client,server} - pos_get - получить значение счетчика mode по текущему или противоположному направлению
- pos_check_from - проверить удовлетворяет ли текущая позиция нижней границе range
- pos_check_to - проверить удовлетворяет ли текущая позиция верхней границе range
- pos_range - проверить удовлетворяет ли текущая позиция range (нижней и верхней границе)
- pos_str - преобразование таблицы позиции pos в стандартную строковую форму
<mode>, напримерs100. - pos_range_str - преобразование таблицы диапазона range в стандартную строковую форму
<mode_from>(-|<)<mode_to>, напримерd1-p5000.
Диссекция
Диссекция - это разбор некоторого сообщения для представления в структурированной форме.
dissect_url
function dissect_url(url)Возвращает таблицу, где разобраны части URL вида proto://creds@domain:port/uri.
Если какая-либо из частей отсутствует, соответствующего поля нет в таблице.
Пример разборки https://user:[email protected]:12345/my_uri/script.php?a=1&b=3
.proto
string https
.creds
string user:pass
.domain
string domain.com
.port
string 12345
.uri
string /my_uri/script.php?a=1&b=3
dissect_nld
function dissect_nld(domain, level)Получение домена уровня level из domain. level=2 'www.microsoft.com' => 'microsoft.com'.
Если уровня level нет, возвращается nil.
dissect_http
function http_dissect_req(http)
function http_dissect_reply(http)
function http_reconstruct_req(hdis[, unixeol])Разборка HTTP запроса или ответа http. http представляет собой многострочный текст.
Разборка представляет собой таблицу с вложенными подтаблицами.
В заголовках выдаются позиции начала и конца названия заголовка и самого значения.
Все позиции - внутри строки http.
Для нахождения хедеров по названию используйте array_field_search по полю "header_low", которое содержит название хедера в нижнем регистре.
Реконструктор http запроса берет таблицу-разбор и воссоздает raw string. Параметр unixeol заменяет стандартный для http перевод строки 0D0A на 0A. Это нестандарт и ломает все сервера, кроме nginx.
Пример разборки http запроса http://testhost.com/testuri
.uri
string /test_uri
.headers
.1
.header
string Content-Length
.header_low
string content-length
.value
string 330
.pos_start
number 43
.pos_end
number 61
.pos_header_end
number 56
.pos_value_start
number 59
.2
.header
string Host
.header_low
string host
.value
string testhost.com
.pos_start
number 24
.pos_end
number 41
.pos_header_end
number 27
.pos_value_start
number 30
.method
string GET
Пример разборки http ответа
.code
number 200
.headers
.1
.pos_header_end
number 28
.pos_value_start
number 31
.header
string Content-Type
.header_low
string content-type
.value
string text/html
.pos_start
number 17
.pos_end
number 39
.2
.pos_header_end
number 54
.pos_value_start
number 57
.header
string Content-Length
.header_low
string content-length
.value
string 650
.pos_start
number 41
.pos_end
number 59
dissect_tls
function tls_dissect(tls, offset, partialOK)
function tls_reconstruct(tdis)Разборка и сборка TLS. Что понимают и могут функции :
- Любой TLS handshake без TLS record (включая client/server hello). Например, взятый из
desync.decrypt_dataот QUIC. - Любые TLS records. Handshake, certificate, change cipher spec и другие.
- Резаные handshake на несколько TLS records (например, результат
tpws --tlsrec) - (только dissect) Неполные блоки данных, если parialOK=true. Восстанавливается максимум возможного, но полноценно собрать уже не выйдет.
- Все handshake выносятся в отдельную таблицу. Для client/server hello выполняется диссект, остальные оставляются как raw data field.
- TLS extensions из client/server hello : server name, alpn, supported versions, compress certificate, signature algorithms, delegated credentials, supported groups, ec point formats, psk key exchange modes, key share, quic transport parameters. Остальные extensions не парсятся и оставляются как raw data field.
- Если есть record layer, реконструкция выполняется согласно длинам отдельных records. Если последняя часть не влезает, tls record расширяется под оставшиеся данные.
- При отсутствии изменений dissect+reconstruct дают бинарно идентичные блобы.
Функции не работают с DTLS.
tls_dissect возвращает таблицу - разбор raw строки tls со смещения offset (начиная с 1), reconstruct_dissect возвращает raw строку собранного разбора tdis. В случае ошибки возвращается nil.
Простейший способ получить образец диссекта : --payload=tls_client_hello --lua-desync=luaexec:code="var_debug(tls_dissect(desync.reasm_data))".
И вызвать TLS запрос.
Пример диссекта TLS от запроса к https://google.com
.rec
.1
.ver
number 769
.type
number 22
.name
string handshake
.len
number 512
.encrypted
boolean false
.htype
number 1
.data
string 01 00 01 FC 03 03 87 36 D1 0E 19 78 8F 8B 41 5E 05 74 92 EF E7 9D 3E 83 F3 9D F4 C4 C6 6C 3E DC 5A 8C EF FD BC B4 20 1C AF 31 7A EB D2 FD 8B 1F C6 E8 DB CF 02 28 93 C4 AE 13 E1 17 ED 62 D8 3D 2F DE 03 67 A1 1A 44 00 3C 13 02 13 03 13 01 C0 2C C0 30 00 9F CC A9 CC A8 CC AA C0 2B C0 2F 00 9E C0 24 C0 28 00 6B C0 23 C0 27 00 67 C0 0A C0 14 00 39 C0 09 C0 13 00 33 00 9D 00 9C 00 3D 00 3C 00 35 00 2F 01 00 01 77 FF 01 00 01 00 00 00 00 0F 00 0D 00 00 0A 67 6F 6F 67 6C 65 2E 63 6F 6D 00 0B 00 04 03 00 01 02 00 0A 00 16 00 14 00 1D 00 17 00 1E 00 19 00 18 01 00 01 01 01 02 01 03 01 04 00 10 00 0E 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31 00 16 00 00 00 17 00 00 00 31 00 00 00 0D 00 30 00 2E 04 03 05 03 06 03 08 07 08 08 08 1A 08 1B 08 1C 08 09 08 0A 08 0B 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02 00 2B 00 05 04 03 04 03 03 00 2D 00 02 01 01 00 33 00 26 00 24 00 1D 00 20 E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D 00 15 00 AE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.handshake
.1
.dis
.ver
number 771
.type
number 1
.name
string client_hello
.cipher_suites
.1
number 4866
.2
number 4867
.3
number 4865
.4
number 49196
.5
number 49200
.6
number 159
.7
number 52393
.8
number 52392
.9
number 52394
.10
number 49195
.11
number 49199
.12
number 158
.13
number 49188
.14
number 49192
.15
number 107
.16
number 49187
.17
number 49191
.18
number 103
.19
number 49162
.20
number 49172
.21
number 57
.22
number 49161
.23
number 49171
.24
number 51
.25
number 157
.26
number 156
.27
number 61
.28
number 60
.29
number 53
.30
number 47
.compression_methods
.1
number 0
.ext
.1
.type
number 65281
.name
string renegotiation_info
.data
string 00
.2
.dis
.list
.1
.name
string google.com
.type
number 0
.type
number 0
.name
string server_name
.data
string 00 0D 00 00 0A 67 6F 6F 67 6C 65 2E 63 6F 6D
.3
.dis
.list
.1
number 0
.2
number 1
.3
number 2
.type
number 11
.name
string ec_point_formats
.data
string 03 00 01 02
.4
.dis
.list
.1
number 29
.2
number 23
.3
number 30
.4
number 25
.5
number 24
.6
number 256
.7
number 257
.8
number 258
.9
number 259
.10
number 260
.type
number 10
.name
string supported_groups
.data
string 00 14 00 1D 00 17 00 1E 00 19 00 18 01 00 01 01 01 02 01 03 01 04
.5
.dis
.list
.1
string h2
.2
string http/1.1
.type
number 16
.name
string application_layer_protocol_negotiation
.data
string 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31
.6
.type
number 22
.name
string encrypt_then_mac
.data
string
.7
.type
number 23
.name
string extended_master_secret
.data
string
.8
.type
number 49
.name
string post_handshake_auth
.data
string
.9
.dis
.list
.1
number 1027
.2
number 1283
.3
number 1539
.4
number 2055
.5
number 2056
.6
number 2074
.7
number 2075
.8
number 2076
.9
number 2057
.10
number 2058
.11
number 2059
.12
number 2052
.13
number 2053
.14
number 2054
.15
number 1025
.16
number 1281
.17
number 1537
.18
number 771
.19
number 769
.20
number 770
.21
number 1026
.22
number 1282
.23
number 1538
.type
number 13
.name
string signature_algorithms
.data
string 00 2E 04 03 05 03 06 03 08 07 08 08 08 1A 08 1B 08 1C 08 09 08 0A 08 0B 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02
.10
.dis
.list
.1
number 772
.2
number 771
.type
number 43
.name
string supported_versions
.data
string 04 03 04 03 03
.11
.dis
.list
.1
number 1
.type
number 45
.name
string psk_key_exchange_modes
.data
string 01 01
.12
.dis
.list
.1
.group
number 29
.kex
string E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D
.type
number 51
.name
string key_share
.data
string 00 24 00 1D 00 20 E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D
.13
.type
number 21
.name
string padding
.data
string 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.session_id
string 1C AF 31 7A EB D2 FD 8B 1F C6 E8 DB CF 02 28 93 C4 AE 13 E1 17 ED 62 D8 3D 2F DE 03 67 A1 1A 44
.random
string 87 36 D1 0E 19 78 8F 8B 41 5E 05 74 92 EF E7 9D 3E 83 F3 9D F4 C4 C6 6C 3E DC 5A 8C EF FD BC B4
.type
number 1
.name
string client_hello
.data
string 01 00 01 FC 03 03 87 36 D1 0E 19 78 8F 8B 41 5E 05 74 92 EF E7 9D 3E 83 F3 9D F4 C4 C6 6C 3E DC 5A 8C EF FD BC B4 20 1C AF 31 7A EB D2 FD 8B 1F C6 E8 DB CF 02 28 93 C4 AE 13 E1 17 ED 62 D8 3D 2F DE 03 67 A1 1A 44 00 3C 13 02 13 03 13 01 C0 2C C0 30 00 9F CC A9 CC A8 CC AA C0 2B C0 2F 00 9E C0 24 C0 28 00 6B C0 23 C0 27 00 67 C0 0A C0 14 00 39 C0 09 C0 13 00 33 00 9D 00 9C 00 3D 00 3C 00 35 00 2F 01 00 01 77 FF 01 00 01 00 00 00 00 0F 00 0D 00 00 0A 67 6F 6F 67 6C 65 2E 63 6F 6D 00 0B 00 04 03 00 01 02 00 0A 00 16 00 14 00 1D 00 17 00 1E 00 19 00 18 01 00 01 01 01 02 01 03 01 04 00 10 00 0E 00 0C 02 68 32 08 68 74 74 70 2F 31 2E 31 00 16 00 00 00 17 00 00 00 31 00 00 00 0D 00 30 00 2E 04 03 05 03 06 03 08 07 08 08 08 1A 08 1B 08 1C 08 09 08 0A 08 0B 08 04 08 05 08 06 04 01 05 01 06 01 03 03 03 01 03 02 04 02 05 02 06 02 00 2B 00 05 04 03 04 03 03 00 2D 00 02 01 01 00 33 00 26 00 24 00 1D 00 20 E0 40 E1 0A BF AD 5B 08 48 16 E5 A6 A9 90 E4 28 A1 67 40 1F AF A4 7B 9B 0A F9 32 2A 01 95 8B 5D 00 15 00 AE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Элементы, которые удалось разобрать, представлены в виде подтаблиц dis. Остальные остаются как raw data field.
Для некоторых элементов заполняется поле "name". Оно служит для удобства визуального анализа и больше ни для чего не используется.
Первичными являются поля type.
Для нахождения значений в списках используйте функции поиска в массивах.
Множество констант, связанных с TLS, определено в zapret-lib.lua. Прежде чем писать фиксированные значения, посмотрите нет ли нужной константы.
Таблица handshake индексируется по типу handshake. Самыми типичными являются TLS_HANDSHAKE_TYPE_CLIENT и TLS_HANDSHAKE_TYPE_SERVER.
Они имеют значения 1 и 2 соответственно, поэтому может показаться, что элементы handshake идут от 1 и по возрастающей. Это не так.
extensions и другие списки индексируются по номеру с 1, а не по типу, потому что важен порядок их следования, и может быть несколько элементов одного типа.
Если вы что-то добавляете свое, вам нужно воспроизвести минимальный вариант исходной структуры.
Можно заполнить только raw data field. Если нет подтаблицы dis - при реконструкции будет взято оно.
Если есть dis, то он должен быть заполнен корректно согласно рассматриваемому элементу данных.
Следующий пример кода ищет SNI extension в диссекте tdis, а в случае отсутствия добавляет в начало. Затем добавляется домен "example.com".
local idx_sni = array_field_search(tdis.handshake[TLS_HANDSHAKE_TYPE_CLIENT].dis.ext, "type", TLS_EXT_SERVER_NAME)
if not idx_sni then
table.insert(tdis.handshake[TLS_HANDSHAKE_TYPE_CLIENT].dis.ext, 1, { type = TLS_EXT_SERVER_NAME, dis = { list = {} } } )
idx_sni = 1
end
table.insert(tdis.handshake[TLS_HANDSHAKE_TYPE_CLIENT].dis.ext[idx_sni].dis.list, { name = "example.com", type = 0 } )Работа с элементами L3 и L4 протоколов
find_tcp_options
function find_tcp_option(options, kind)Вернуть первый элемент dis.tcp.options с опцией kind. nil, если не найдено.
ip6hdr
function find_ip6_exthdr(exthdr, proto)Вернуть первый элемент dis.ip6.exthdr с type = proto.
function insert_ip6_exthdr(ip6, idx, header_type, data)
function del_ip6_exthdr(ip6, idx)
function fix_ip6_next(ip6, last_proto)ip protocol
function ip_proto_l3(dis)
function ip_proto_l4(dis)
function ip_proto(dis)Функции "додумывают" ip protocol полезной нагрузки диссекта dis.
- ip_proto_l3 - ipv4 - ip.ip_p , ipv6 - ip6.ip6_nxt или next из последнего extension header. nil, если next пуст.
- ip_proto_l4 - IPPROTO_TCP, IPPROTO_UDP, IPPROTO_ICMP, IPPROTO_ICMPV6 в зависимости от наличия в диссекте tcp,udp,icmp,ip6. nil, если tcp,udp,icmp отсутствуют.
- ip_proto - ip_proto_l4. если он вернул nil, то ip_proto_l3.
function fix_ip_proto(dis, proto)Установить протокол полезной нагрузки диссекта dis как proto. Если proto не задан - использовать результат ip_proto(dis).
packet_len
Эти функции работают с диссектом ipv6 заголовка ip6 и его extension headers - ip6.exthdr.
При вставлении или удалении extension headers сохраняется корректная цепочка следующих протоколов, начиная с базового ipv6 заголовка.
- insert_ip6_exthdr вставляет extension header с протоколом header_type и данными data в диссект ip6 по индексу ip6.exthdr idx. Если idx=nil, вставляется в конец. Размер data должен быть 6+N4 для IPPROTO_AH и 6+N8 для остальных, иначе будут ошибки при реконструкции.
- del_ip6_exthdr удаляет extension header по индексу ip6.exthdr idx.
- fix_ip6_next восстанавливает корректную цепочку следующих протоколов за счет ip6.ip6_nxt и полей type в ip6.exthdr.
function l3_base_len(dis)
function l3_extra_len(dis[, ip6_exthdr_last_idx])
function l3_len(dis)
function l4_base_len(dis)
function l4_extra_len(dis)
function l4_len(dis)
function l3l4_extra_len(dis)
function l3l4_len(dis)
function packet_len(dis)Подсчет размеров различных элементов диссекта после реконструкции.
- l3_base_len - базовая длина ip/ip6 заголовка без опций и extension headers.
- l3_extra_len - длина ip options или суммарная длина всех extension headers. Если задан ip6_exthdr_last_idx, считаются extension headers до указанного индекса.
- l3_len - полная длина ip/ip6 с опциями и extension headers
- l4_base_len - базовая длина tcp или udp заголовка
- l4_extra_len - длина tcp options для tcp, 0 для udp
- l4_len - полная длина tcp заголовка с опциями или длина udp заголовка
- l3l4_extra_len - сумма l3_extra_len и l4_extra_len
- l3l4_len - полная длина ip/ip6 и tcp заголовков со всеми options и extension headers
- packet_len - полная длина реконструированного пакета, включая L4 пейлоад
Работа с именами хостов
genhost
function genhost(len[, template])Генерирует случайный хост длиной len.
- Если есть template, генерирует случайный поддомен, чтобы вписаться в длину len. Если длины len не хватает, возвращает обрез template слева.
- Если template=nil, генерирует случайный поддомен одного из известных 3-буквенных TLD. Если len<7, генерирует случайный домен без точек длиной len.
Примеры :
-- template "google.com", len=16 : h82aj.google.com
-- template "google.com", len=11 : .google.com
-- template "google.com", len=10 : google.com
-- template "google.com", len=7 : gle.com
-- no template, len=6 : b8c54a
-- no template, len=7 : u9a.edu
-- no template, len=10 : jgha7c.comhost_ip
function host_ip(desync)
function host_or_ip(desync)- host_ip возвращает символьное представление desync.target.ip или desync.target.ip6
- host_or_ip возвращает desync.track.hostname, если есть track и есть track.hostname, иначе host_ip(desync)
Операции с именами файлов и путями
function is_absolute_path(path)
function append_path(path,file)
function writable_file_name(filename)- is_absolute_path возвращает true, если путь path начинается с корня. Учитываются особенности путей CYGWIN.
- append_path дописывает имя файла или каталога file к пути path, разделяя их знаком '/'
- writable_file_name возвращает filename, если filename содержит абсолютный путь или env
WRITABLEотсутствует.
Иначе берется путь из envWRITABLEи к нему дописывается имя файла filename через append_path.
Чтение и запись файлов
function readfile(filename)Читает весь файл. Вызывается error в случае ошибки при открытии или чтении файла.
function z_readfile(filename[, expected_ratio])Автоматически определяет является ли файл gzip. Если да - разжимает, если нет - читает без изменений. Вызывается error в случае ошибки при открытии или чтении файла.
expected_ratio - ожидаемое соотношение длины разжатых данных к длине сжатых данных (по умолчанию 4).
function writefile(filename, data)Записывает data в файл. Вызывается error в случае ошибки при открытии файла.
Компрессия данных
function is_gzip_file(filename)true, если файл является gzip, иначе false. При ошибке открытия файла вызывается error.
function gunzip_file(filename[, expected_ratio[, read_block_size]])Разжимает файл и возвращает raw string. В случае ошибки открытия или чтения файла вызывается error. При нехватке памяти возвращается nil. read_block_size - частями какого размера читается файл (по умолчанию 16K).
expected_ratio - ожидаемое соотношение длины разжатых данных к длине сжатых данных (по умолчанию 4).
function gzip_file(filename, data[, expected_ratio[, level[, memlevel[, compress_block_size]]]])Сжимает raw строку в gzip файл. В случае ошибки открытия или чтения файла вызывается error. При испорченных gzip данных или нехватке памяти возвращается nil.
level - уровень сжатия от 1 до 9 (по умолчанию 9), memlevel - допустимый уровень использования памяти от 1 до 8 (по умолчанию 8). compress_block_size - частями какого размера жмется файл (по умолчанию 16K).
expected_ratio - ожидаемое соотношение длины разжатых данных к длине сжатых данных (по умолчанию 2).
autottl
function parse_autottl(s)
function autottl(incoming_ttl, attl)Механизм autottl служит, чтобы автоматически на базе TTL входящего пакета определять такой TTL,
который либо немного не достает до противоположного конца, либо немного превышает длину пути до него.
delta - это положительная или отрицательная разница с догадкой о длине пути. min-max - допустимый диапазон.
Если итоговый результат выходит за пределы диапазона, назначаются его крайние значения.
Если delta<0 и результат оказывается равен пути или длиннее, либо если delta>=0 и результат оказывается короче пути, алгоритм уходит в ошибку и выдает nil.
Вычисление производится на базе предположения о симметричности входящего и исходящего пути и TTL по умолчанию, используемых в основных ОС - 64, 128, 255.
Эвристика работает не всегда из-за потенциальной неверности данных предположений, но иногда ее можно настроить до приемлемого уровня погрешности.
- parse_autottl переводит строку вида
<delta>,<min>-<max>в таблицу с идентичными полями. Вызывается error, если формат s неверен. - autottl производит эвристическую догадку о длине в хопах на базе TTL входящих пакетов и вычисляет TTL , учитывая дельту и допустимый диапазон.
Готовый incoming_ttl можно взять из desync. attl имеют формат таблицы, получаемой через parse_autottl.
Операции с диссектами
В следующих функция и функциях отсылки применяются стандартные блоки опций, представленных в виде полей отдельно передаваемой таблицы.
Таблицы опций имеет формат desync.arg. Может передаваться прямо desync.arg без изменений.
standard ipid
ipid_options
| Поле | Описание |
|---|---|
| ip_id | режим назначения ip_id : seq, rnd, zero, noneseq - последовательноеrnd - случайноеzero - нольnone - не менять |
| ip_id_conn | запоминать последнее сгенерированное значение seq и начинать с него в следующем пакете. не работает без desync.track |
Из seq далеко не всегда получится ожидаемая и приемлемая последовательность.
ip_id применяется только в ряде функций, оно не применяется ко всем проходящим пакетам автоматически.
Поскольку ОС не следит за измененными ip_id, в пакетах, которые не трогали, оно может начать идти заново.
Windows заменяет нулевые ip_id на собственную последовательность, остальные ОС - нет.
На любых ОС можно "держать" какое-то время сквозной линейный порядок ip_id через смесь оригинальных и генерированных пакетов. Для этого нужно применять политику ip_id=seq:ip_id_conn ко всем поддерживающим ipid desync функциям, а для остальных пейлоадов использовать связку инстансов send с той же ip_id политикой и drop в ограниченном интервале --out-range. Слишком долго это делать не стоит, поскольку увеличит нагрузку на CPU.
standard fooling
fooling_options
| Поле | Описание |
|---|---|
| ip_ttl | изменить TTL в ipv4 заголовке на указанный |
| ip6_ttl | изменить TTL (HL) в ipv6 заголовке на указанный |
| ip_autottl | изменить TTL в ipv4 заголовке на автоматически определяемый по шаблону autottl delta,min-max. При невозможности определения TTL берется ip_ttl, если есть. Иначе TTL не меняется. |
| ip6_autottl | изменить HL в ipv6 заголовке на автоматически определяемый по шаблону autottl delta,min-max. При невозможности определения HL берется ip6_ttl, если есть. Иначе HL не меняется. |
| ip6_hopbyhop | вставить extension header "hop-by-hop options". по умолчанию данные - 6 нулей, но можно задать hex строку с данными. длина должна быть 6+N*8 |
| ip6_hopbyhop2 | вставить второй extension header "hop-by-hop options" |
| ip6_destopt | вставить extension header "destination options". по умолчанию данные - 6 нулей, но можно задать hex строку с данными. длина должна быть 6+N*8 |
| ip6_destopt2 | вставить второй extension header "destination options" |
| ip6_routing | вставить extension header "routing options". по умолчанию данные - 6 нулей, но можно задать hex строку с данными. длина должна быть 6+N*8 |
| ip6_ah | вставить extension header "authentication header". по умолчанию данные - 2 нуля и 4 случайных байта, но можно задать hex строку с данными. длина должна быть 6+N*4 |
| tcp_seq | положительное или отрицательное смещение sequence |
| tcp_ack | положительное или отрицательное смещение ack sequence |
| tcp_ts | положительное или отрицательное смещение timestamp. работает только если уже есть timestamp option |
| tcp_md5 | добавить MD5 header, если его еще нет. по умолчанию - случайные байты, но можно задать hex string длиной 16 байт |
| tcp_flags_set | установить флаги TCP. флаги представлены списком через запятую : FIN,SYN,RST,PUSH,ACK,FIN,URG,ECE,CWR |
| tcp_flags_unset | снять флаги TCP. аналогично tcp_flags_set |
| tcp_ts_up | поднять tcp timestamp опцию в самое начало, если она есть |
| tcp_nop_del | удалить все TCP опции NOP для освобождения места в заголовке TCP |
| fool | имя кастомной функции фулинга. она берет диссект и таблицу fooling_options |
ipv6 extension headers добавляются в следующем порядке:
- hopbyhop
- hopbyhop2
- destopt
- routing
- destopt2
- ah
tcp_ts_up - очень странное явление, обнаруженное в процессе тестирования nfqws2.
Оказывается, если есть tcp опция timestamp, linux стабильно отбрасывает пакеты с валидным seq и инвалидным ack только если опция идет первой.
nfqws1 не соблюдал порядок tcp опций, timestamp получался первым всегда.
Поэтому оказалось, что старая версия работает стабильно , а новая нет.
tcp_ts_up дублирует старое поведение - двигает timestamp в самый верх.
standard ipfrag
Опции IP фрагментации ipfrag_options содержат только два стандартных параметра. Остальное берут заменяемые функции фрагментаторы,
для которых существуют свои специфические опции.
ipfrag_options
| Поле | Описание |
|---|---|
| ipfrag | имя функции фрагментатора. если нет, использовать ipfrag2. фрагментатор возвращает массив диссектов - фрагментов |
| ipfrag_disorder | отправить фрагменты в обратном порядке |
| ipfrag_pos_udp | (фрагментатор ipfrag2) позиция фрагментации udp. должна быть кратна 8, по умолчанию 8 |
| ipfrag_pos_tcp | (фрагментатор ipfrag2) позиция фрагментации tcp. должна быть кратна 8, по умолчанию 32 |
| ipfrag_next | (фрагментатор ipfrag2) тип следующего протокола в "fragment" extension header второго фрагмента |
apply_ip_id
function apply_ip_id(desync[, dis[, ipid_options[, def]]])Применить политику ip_id из ipid_options к диссекту dis.
Если dis = nil, берется desync.dis.
Если ipid_options = nil, берется desync.arg.
def содержит режим назначения по умолчанию. Если nil, применяется "seq".
apply_fooling
function apply_fooling(desync[, dis[, fooling_options]])Применяет набор модификаций L3/L4 заголовков (фулинг), описанный в fooling_options, к диссекту dis.
Если dis = nil, берется desync.dis.
Если fooling_options = nil, берется desync.arg
ipfrag2
function ipfrag2(dis, ipfrag_options)Стандартная функция фрагментатор. Возвращает массив из двух диссектов-фрагментов исходного диссекта dis.
Задействуется через rawsend_dissect_ipfrag, если отсутствует поле ipfrag в ipfrag_options.
Отдельно вызывать эту функцию вряд ли понадобится.
Если вам нужно резать IP пакет как-то иначе, вы можете по аналогии создать свой фрагментатор и указать его в ipfrag_options.
В случае ipv6 fragment header вставляется после всех hopbyhop, routing и первого destopt. Это unfragmentable part.
Дальше идет fragment header, а все остальное после него является fragmentable part.
Unfragmentable part передается в каждом фрагменте с измененными полями fragment header, остальное режется поверх
всего блока данных за fragment header согласно смещению фрагмента.
По стандарту в случае ipv6 фрагментации next протокол берется только из первого фрагмента с offset=0.
В остальных фрагментах он не обязан совпадать и игнорируется. Манипуляция полем "next protocol" последующих фрагментов -
известная и описанная в статьях penetration атака, позволяющая пробить некоторые фаерволы.
ipfrag2 реализует эту возможность на двух фрагментах через указание параметра ipfrag_next.
Некоторые фаерволы пробиваются только большим количество фрагментов - для этого потребуется своя функция фрагментатор.
wssize_rewrite
function wsize_rewrite(dis, arg)Переписать в диссекте dis tcp.th_win и scale factor в tcp опции, если она присутствует. Увеличение scaling factor блокируется.
- arg: wsize - window size
- arg: scale - scale factor
- возвращает true, если какое-либо изменение было произведено
dis_reverse
function dis_reverse(dis)Поменять местами ip адреса и порты src/dst и seq/ack.
IP адреса и интерфейсы
function update_ifaddrs()Обертка вокруг C функции get_ifaddrs. Может так случиться, что надо узнавать адреса на каждом пакете.
Каждый раз дергать get_ifaddrs тяжело для системы. Адреса и интерфейсы меняются редко. Хотелось бы из закэшировать.
update_ifaddrs() берет на себя функцию ведения кэша, который обновляется не чаще 1 раза в секунду.
Результат кладется в глобальную переменную ifaddrs.
function ip2ifname(ip)Получить имя интерфейса, на котором находится ip адрес, используя кэш ifaddrs. nil, если не найден.
Отсылка
Следующие функции могут брать несколько блоков описанных выше опций, каждый из которых представлен полем параметра options.
Во всех функциях используется options.reconstruct и options.rawsend. Они соответствуют формату параметров C функции rawsend_dissect.
rawsend_dissect_ipfrag
function rawsend_dissect_ipfrag(dis[, options])Отправить диссект dis с IP фрагментацией, заданной в options.ipfrag. Если отсутствует - отправить без фрагментации.
Использует кастомную функцию фрагментатор, если указано options.ipfrag.ipfrag.
Отсылает фрагменты в обратном порядке, если указано options.ipfrag.ipfrag_disorder.
rawsend_dissect_segmented
function rawsend_dissect_segmented(desync[, dis[, mss[, options]]])Отправить диссект dis с автоматической tcp сегментацией на базе mss с применением options.fooling и options.ipid.
ipid применяется к каждому фрагменту. Для udp сегментация невозможна и не выполняется.
- Если dis отсутствует, берется desync.dis.
- Если mss отсутствует, берется desync.tcp_mss.
- Если options отсутствуют, они создаются на базе desync.arg.
rawsend_payload_segmented
function rawsend_payload_segmented(desync[, payload[, seq[, options]]])Сконструировать временный диссект на базе desync.dis с опциональным замещением пейлоада на payload и опциональным смещением seq, применяя options, и отослать через rawsend_dissect_segmented.
mss берется из desync.tcp_mss.
Если options отсутствуют, они создаются на базе desync.arg.
Стандартные options формируются следующим образом :
- ipfrag, ipid, fooling принимают значение desync.arg
- rawsend : repeats берется из desync.arg.repeats, ifout и fwmark берутся из desync.arg, если они там есть, либо из desync (то, что пришло в desync функцию)
- reconstruct : берется только desync.arg.badsum, остальные опции не используются
Стандартные фильтры direction и payload
function direction_check(desync[, def])
function direction_cutoff_opposite(ctx, desync[, def])Фильтр по направлению представляет собой строку "in", "out" или "any" и передается в desync.arg.dir. Если аргумент отсутствует, берется значение def.
- direction_check проверяет соответствие текущего направления фильтру.
- direction_cutoff_opposite выполняет instance cutoff на текущее направление, если оно не соответствует фильтру.
function payload_match_filter(l7payload[, l7payload_filter[, def]])
function payload_check(desync[, def])Функции работают со строкой - списком пейлоадов через запятую. Особые значения - all и known. all означает любой пейлоад, known - не unknown и не empty. Префикс ~ в начале означает логическую инверсию - несоответствие списку.
См. распознавание протоколов.
- payload_match_filter проверяет соответствие l7payload списку l7payload_filter или def, если l7payload_filter=nil. Если оба nil - список берется как "known".
- payload_check вызывает
payload_match_filter(desync.l7payload, desync.arg.payload, def)
Работа с многопакетными пейлоадам
Обычно идет работа с целым reasm, вместо отдельных его частей. В этом и есть смысл сборки, чтобы не копаться в отдельных пакетах, а разбираться со всем сообщением сразу.
Стандартный сценарий предполагает работу после приема первой части replay и игнорирование, либо drop остальных частей. Выбор между игнорированием или drop может зависеть от успешности действий с reasm. Например, удалось или не удалось отправить reasm сегментированно. Если удалось - нужно дропнуть все остальные части, потому что иначе они пойдут дублями в оригинальной сегментации. Если возникла какая-то ошибка, сегментированные пакеты отправить не удалось, и при этом дропнуть все остальное, до адресата не дойдет полное сообщение, начнутся ретрансмиссии, поэтому лучше оставить как есть - так хоть ничего не поломается.
function replay_first(desync)
function replay_drop_set(desync, v)
function replay_drop(desync)- replay_first возвращает true, если текущий диссект не является replay или является его первой частью
- replay_drop_set помечает в desync.track.lua_state boolean признак необходимости "v" дропнуть последующие части replay
- replay_drop возвращает true, если необходимо дропнуть текущую часть replay. Если часть последняя - автоматически снимает признак.
Функции работают корректно как с реплеем, так и обычными диссектами. Для обычных диссектов replay_first всегда true, replay_drop_set не меняет признак, replay_drop всегда false.
Помощь с таймерами
function dis_timer_name(dis)Конструирует имя таймера, включающее в себя ip адреса src,dst, имя протокола l4, номера портов или icmp коды.
Имя таймера может не быть уникальным.
function desync_timer_name(desync)Конструирует имя таймера, включающее в себя имя инстанса, результат dis_timer_name, номер пакета в conntrack по прямому направлению.
Если track отсутствует, добавляются случайные символы.
Имя таймера можно считать уникальным на desync+instance, его можно использовать как oneshot timer name.
Оркестрация
В эту группу функций входят функции поддержки процесса оркестрации и прокладки.
Прокладки - это дублеры функций C кода для ситуации, когда у нас нет контекста ctx для связи с C кодом.
После начала оркестрации все дальнейшие инстансы вызываются оркестратором или вложенными оркестраторами.
Последний ctx, который доступен, это ctx первого оркестратора. Если его передавать другим инстансам, они будут действовать от имени оркестратора, а не от своего, поэтому им следует передавать ctx=nil.
После отмены execution plan C код не обслуживает последующие инстансы и не выдает на них ctx. Поэтому если нужно продолжить выполнение в стандартном стиле, необходимы дублирующие механизмы instance cutoff и фильтров range и payload.
Чтобы функции --lua-desync прозрачно работали под оркестрацией, необходимо использовать стандартные прокладки вместо прямых вызовов C функций,
берущих ctx. Чтобы корректно работали вложенные оркестраторы, нужно придерживаться стандартной схемы хранения execution plan в desync.plan
и пользоваться описанными ниже функциями-хелперами.
По сухому описанию может быть сложно понять как работает оркестрация. Рекомендуется изучить код реальных оркестраторов,
а описанием пользоваться для уточнения смысла отдельных действий.
instance_cutoff_shim
function instance_cutoff_shim(ctx, desync[, dir])Выполняет обычный instance cutoff по направлению dir, если ctx присутствует, иначе cutoff через дублирующий механизм,
состояние которого хранится в desync.track.lua_state. dir = true - исходящее направление, dir = false - входящее, dir = nil - оба направления.
cutoff_shim_check
function cutoff_shim_check(desync)Проверяет состояние instance cutoff для desync.func_instance по направлению desync.outgoing.
apply_arg_prefix
function apply_arg_prefix(desync)Выполняет подстановку значений аргументов из desync.arg, начинающихся с % и #, \.
Функция ставит специальную метку в desync.arg , чтобы избежать двойного разименования. Повторные вызовы безопасны, но не обновляют desync.arg при изменении блобов.
apply_execution_plan
function apply_execution_plan(desync, instance)Копирует в desync идентификацию инстанса и его аргументы из элемента execution plan instance,
тем самым воссоздает состояние desync, как если бы instance был вызван напрямую C кодом.
За одним исключением : apply_arg_prefix не применяется, поскольку может содержать несуществующие блоб, существование которого зависит от условного выполнения предыдущих инстансов.
execution plan выдается C функцией execution_plan() как массив, элементами которого являются instance.
verdict_aggregate
function verdict_aggregate(v1, v2)Агрегация вердиктов v1 и v2. VERDICT_MODIFY замещает VERDICT_PASS, VERDICT_DROP замещает их обоих.
plan_instance_execute
function plan_instance_execute(desync, verdict, instance)
function plan_instance_execute_preapplied(desync, verdict, instance)Выполняет элемент execution plan instance с учетом instance cutoff и стандартных фильтров payload и range.
При совпадении условий непосредственно перед вызовом выполняет apply_arg_prefix.
Возвращает агрегацию verdict и вердикта instance.
Вариант "preapplied" не выполняет apply_execution_plan, позволяя это сделать вызывающему коду.
Иногда для принятия решения вызывать ли instance требуется таблица desync, настроенная на вызываемый инстанс.
Чтобы не делать apply дважды (там копирование desync.arg) и существует этот вариант.
plan_instance_pop
function plan_instance_pop(desync)Берет, удаляет и возвращает первый элемент execution plan из desync.plan. Если элементов нет - возвращает nil.
plan_clear
function plan_clear(desync, max)Очищает первые max инстансов, если max задан, либо весь execution plan в desync.plan.
orchestrate
function orchestrate(ctx, desync)Если оркестратор - первый, т.е. присутствует ctx, забирает execution plan и помещает его в desync.plan, а потом выполняет execution_plan_cancel().
Если ctx=nil - не делает ничего. Считается, что план уже находится в desync.plan.
replay_execution_plan
function replay_execution_plan(desync, max)Выполняет max инстансов, если max задан, либо весь execution plan из desync.plan с учетом instance cutoff и стандартных фильтров payload и range.
Библиотека программ атаки на DPI zapret-antidpi.lua
Стандартные наборы параметров
Многие функции берут стандартные наборы аргументов, классифицируемые по их назначению.
Дополнительные фильтры по направлению и пейлоаду внутри функций anti-dpi сделаны в основном на случай не слишком грамотного написания опций командной строки как дополнительный предохранитель и защита от дурака, чтобы сделать флуд как это было в winws1 с --dpi-desync-any-protocol непреднамеренно было непросто.
Другая цель - дать возможность фильтрации по протоколам, о которых C код не знает и которые обнаруживаются Lua детекторами, такими как detect_payload_str.
В nfqws2 по умолчанию стоит запрет на передачу входящих (--in-range=x), неограниченная передача исходящих (--out-range=a ) и пропуск любых пейлоадов (--payload=all), что соответствуют поведению nfqws1 с опцией --dpi-desync-any-protocol. В nfqws1 все атаки были зашиты в C код, поэтому было известно какие техники работают с какими пейлоадами. Каким-то нужны были любые пакеты, в том числе пустые, а другим - только tls hello или http request.
nfqws2 ничего не знает о том, что нужно --lua-desync функциям. Поэтому фильтрация направления и типа пейлоадов ложится целиком на вас. По умолчанию стоит запрет только на входящие, потому что они используются редко, а пользователь может не написать ограничение, и все это пойдет на Lua функции и будет напрасно грузить процессор гигабайтами скачиваемого трафика.
standard direction
Фильтр по направлению. В большинстве функций, использующих фильтр по направлению, значение по умолчанию - "out", но есть и те, где по умолчанию "any". Фильтр по направлению можно реализовать и средствами C кода --in-range и --out-range.
standard direction
| Поле | Описание |
|---|---|
| dir | in - входящее направление out - исходящее направлениеany - любое направление |
standard payload
Фильтр по пейлоаду берет список типов пейлоада.
standard payload
| Поле | Описание |
|---|---|
| payload | список допустимых пейлоадов через запятую. ~ в начале означает инверсию |
Базовые функции
drop
function drop(ctx, desync)- arg: standard direction
- arg: standard payload
- По умолчанию payload=all, direction=any, то есть drop всего.
Выносит VERDICT_DROP при выполнении условий фильтра.
send
function send(ctx, desync)- arg: standard direction
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: delay - задержка отправки в мсек
- режим ip_id по умолчанию - none
Отсылает текущий диссект c опциональным применением модификаций.
При наличии параметра delay данные пакета и опции отправки запоминаются. Спустя указанное время происходит отправка.
Функция отсылки с delay реализована тривиальным образом через таймер с именем, уникальным для диссекта.
Множественные send с delay не вызывают дополнительную отправку, вместо этого замещается предыдущая отправка.
Если вам нужно задерживать несколько вариаций (например, результат tcp сегментации) и нужна очередь для отправки в строгом порядке,
то вам нужно писать свою функцию - send не подойдет.
Функция выносит VERDICT_PASS - отсылка текущего диссекта не отменяется. Если нужно отменить - используйте инстанс drop.
pktmod
function pktmod(ctx, desync)- arg: standard direction
- arg: standard fooling
- arg: standard ipid
Применить модификации к текущему диссекту без отправки и вынесения вердикта.
Дурение http
http_hostcase
function http_hostcase(ctx, desync)- arg: standard direction
- arg: spell - точное написание заголовка. по умолчанию "host"
Заменяет регистр http заголовка Host:
http_domcase
function http_domcase(ctx, desync)- arg: standard direction
Меняет регистр написания домена внутри заголовка Host:. Верхний и нижний регистр чередуется каждый символ : rUtRaCkEr.oRg.
http_methodeol
function http_methodeol(ctx, desync)- arg: standard direction
Вставляет '\r\n' перед методом, отрезая 2 последних символа из содержимого заголовка User-Agent:. Работает только на nginx, остальные сервера ломает.
Если используется совместно с другими функциями http тамперинга, должен идти последним.
http_unixeol
function http_unixeol(ctx, desync)- arg: standard direction
Заменяет перевод строки 0D0A на 0A. Разницу в длине добавляет пробелами в конец хедера "User-Agent". Работает только на nginx, остальные сервера ломает.
Замена window size
wsize
function wsize(ctx, desync)- arg: wsize - размер tcp окна
- arg: scale - scaling фактор. заменяется в tcp option, если есть. только уменьшение, увеличение блокируется
Меняет tcp.th_win и/или scaling factor в tcp опции в tcp пакете SYN,ACK, после чего выполняет instance cutoff. Если изменение выполнено - выставляет VERDICT_MODIFY.
Цель техники - подменить window size со стороны клиента или сервера, чтобы в ответ на это клиент отослал следующий пакет частями, поскольку целиком он не влезает в window size. Может приводить к замедлению. Устаревшая техника, рекомендуется применять только с сервера в крайнем случае для работы по клиентам, которые не делают ничего для обхода блокировки. С клиента лучше применять техники tcp сегментации, поскольку они лишены побочных эффектов в виде замедления скорости и имеют больше возможностей.
wssize
function wssize(ctx, desync)- arg: standard direction
- arg: wsize - размер tcp окна
- arg: scale - scaling фактор. заменяется в tcp option, если есть. только уменьшение, увеличение блокируется
- arg: forced_cutoff - список типов пейлоадов через запятую, при получении которых выполняется instance cutoff. Если нужно применять wssize бесконечно, можно указать forced_cutoff=no, то есть несуществующий тип пейлоада, который не придет никогда.
Меняет tcp.th_win и/или scaling factor в tcp опции во всех tcp пакетах потока по направлению до достижения условия "cutoff".
Если изменение выполнено - выставляет VERDICT_MODIFY.
"cutoff" наступает при получении любого пакета с данными, если аргумент forced_cutoff не задан, или при получении одного из указанных в аргументе forced_cutoff пейлоадов.
В этом случае выполняется instance cutoff.
Цель техники - заставить сервер фрагментировать свои ответы, когда DPI их проверяет (TLS 1.2).
Нужно держать сервер "в тонусе", чтобы он думал, что клиент не может принимать tcp сегменты большого размера и сам резал свои ответы,
но лишь до прохода критической фазы проверки.
После этого нужно отпустить воздействие, иначе оно приведет к катастрофической потере скорости вплоть до модемной.
Режет скорость в любом случае. Является техникой нулевой фазы - с хостлистами может применяться только с включенным --ipcache-hostname.
При использовании хостлистов может быть необходимо дублировать в отдельном профиле, который задействуется до узнавания hostname.
В этом случае будет применяться всегда без проверки на хостлист и всегда резать скорость.
Типичные параметры: wsize=1:scale=6. К применению рекомендуется только при отсутствии альтернатив.
Фейки
Фейки - это отдельные пакеты с ложной информацией, которую DPI должен принять, а сервер - нет.
Фейки бывают прямые и скрытые.
Прямые фейки - это отдельные пакеты, скрытые фейки - часть оригинальных модифицированных пакетов или групп пакетов.
Прямым фейкам всегда необходимо искажение какой-либо информации в заголовках пакета, чтобы пейлоад не попал в серверное приложение,
иначе это поломает соединение. Без фулинга фейк должен повторять часть оригинальной информации, чтобы сервер не получил ложную информацию.
Скрытые фейки не воспринимаются сервером в силу характеристик пакетов, частью которых они являются.
syndata
function syndata(ctx, desync)- arg: standard fooling
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: blob - blob, содержащий фейковый payload. Должен помещаться в один пакет, сегментация невозможна.
- arg: tls_mod - применить указанный tls_mod к пейлоаду blob
Функция добавляет в tcp SYN пакет пейлоад, применяет к нему модификации и отправляет вместо оригинала, вынося VERDICT_DROP.
Если проходит пакет без SYN, выполняется instance cutoff.
Таким образом воздействие выполняется на все ретрансмиссии SYN, после чего функция прекращает работу.
Является стратегией нулевой фазы, которая работает с хостлистами только в режиме --ipcache-hostname.
tls_client_hello_clone
function tls_client_hello_clone(ctx, desync)- arg: standard direction
- arg: blob - имя блоба, получающего результат
- arg: fallback - имя блоба, который копируется в результат, если пейлоад неверного типа или невалидный
- arg: sni_del_ext - удалить sni extension. остальные параметры не используются
- arg: sni_del - удалить все хосты
- arg: sni_snt - заменить на всех уже имеющихся хостах поле "server name type"
- arg: sni_snt_new - поле "server name type" для добавляемых хостов
- arg: sni_first - добавить хост в начало списка
- arg: sni_last - добавить хост в конец списка списка
Подготавливает blob с указанным именем в таблице desync, заполненный результатом модификации текущего reasm.
Работает только с tcp и пейлоадом tls_client_hello. Если задана модификация SNI и SNI extension отсутствует - он добавляется в начало списка extensions.
Очередность выполнения операций :
- sni_del_ext . Все остальные операции со SNI теряют смысл и не выполнятся.
- sni_del
- sni_snt
- sni_first
- sni_last
Сама по себе эта функция никак не влияет на трафик, а лишь подготавливает данные для других функций.
fake
function fake(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: blob - blob, содержащий фейковый payload. Может быть любой длины - сегментация выполняется автоматически.
- arg: optional - отказ от операции, если blob отсутствует
- arg: tls_mod - применить указанный tls_mod к пейлоаду blob
- payload фильтр по умолчанию - "known"
Это прямой фейк - отдельный пакет или группа пакетов. Функция не выносит вердикт и не блокирует отправку оригинала.
rst
function rst(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: rstack - отсылка RST,ACK вместо RST
- payload фильтр по умолчанию - "known"
Отослать пустой TCP пакет с флагами RST или RST+ACK. Функция не выносит вердикт и не блокирует отправку оригинала.
TCP сегментация
multisplit
function multisplit(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - список маркеров через запятую - точек разреза. По умолчанию "2".
- arg: seqovl - число - смещение относительно текущего sequence для создания дополнительной части сегмента, выходящей влево за границу tcp window
- arg: seqovl_pattern - blob, используемый для заполнения seqovl. По умолчанию 0x00
- arg: blob - заменить текущий пейлоад на указанный blob
- arg: optional - отказ от операции, если blob задан, но отсутствует. если seqovl_pattern задан, но отсутствует, использовать паттерн 0x00.
- arg: nodrop - отказ от вынесения VERDICT_DROP
- payload фильтр по умолчанию - "known"
Multisplit реализует последовательную сегментацию текущего диссекта или реасма с разрезом в определяемых списком маркеров позициях. Опционально поддерживается замена блока данных на произвольный blob и техника seqovl.
Выносится VERDICT_DROP после успешной отправки всех сегментов, если не указано "nodrop".
Если происходит перепроигрывание (replay) задержанных пакетов и присутствует reasm, то вместо desync.dis.payload берется desync.reasm_data. Нарезание происходит только при перепроигрывании первой части reasm, по остальным частям выносится VERDICT_DROP, если отсылка была успешна и не указано "nodrop". Поскольку весь reasm уже отправлен нарезанным, нет смысла повторно отправлять его оригинальные части.
Может использоваться для отправки произвольных данных, в том числе фейков с заменой текущего пейлоада на произвольный blob.
О размерах частей и вписывании в MTU думать не нужно - применяется дополнительная автоматическая сегментация по MSS.
seqovl может быть только числом, маркеры не поддерживаются. Применяется к первому нарезаемому сегменту. К пейлоаду первого сегмента приписывается слева seqovl_pattern по размеру seqovl, а tcp.th_seq уменьшается на seqovl. Таким образом слева образуется блок данных, выходящий влево за tcp window, и поэтому он игнорируется сервером, а часть, входящая в tcp window - принимается.
seqovl - это фактически средство замешивания фейковых и реальных данных, средство создания скрытых фейков в реальных tcp сегментах. Если DPI не ведет учет sequence numbers, он может проглотить весь передаваемый сегмент и купиться на ложную информацию в начале, которая сервером принята не будет.
Особое преимущество seqovl - в отсутствии необходимости фулинга. Сервер принимает только часть сегмента за счет игры с sequence numbers, а не за счет модификации каких-то элементов ip и tcp заголовков, что привело бы к полному непринятию.
multidisorder
function multidisorder(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - список маркеров через запятую - точек разреза. По умолчанию "2".
- arg: seqovl - маркер - смещение относительно текущего sequence для создания дополнительной части сегмента, выходящей влево
- arg: seqovl_pattern - blob, используемый для заполнения seqovl. По умолчанию 0x00
- arg: blob - заменить текущий пейлоад на указанный blob
- arg: optional - отказ от операции, если blob задан, но отсутствует. если seqovl_pattern задан, но отсутствует, использовать паттерн 0x00.
- arg: nodrop - отказ от вынесения VERDICT_DROP
- payload фильтр по умолчанию - "known"
Аналогично multisplit, но отправка сегментов производится в обратном порядке - с последнего по первый.
Техника seqovl в данном случае работает иначе. Она применяется ко второму в оригинальной очередности (предпоследнему отсылаемому) сегменту. seqovl может быть маркером. Например, можно сделать разрез по midsld, а seqovl сделать "midsld-1". seqovl обязательно должен быть меньше первого сегмента в оригинальной очередности (последнего отсылаемого), иначе эта ситуация распознается и seqovl отменяется.
Смысл seqovl в варианте disorder - в переписывании буфера сокета на принимающем конце. tcp сокет выдает данные в приложение последовательно, в порядке их оригинальной передачи. Если сначала приходит сегмент "спереди", не образующий с уже принятыми данными непрерывной последовательности, происходит задержка информации в буфере без выдачи ее приложению. Если далее приходит перекрывающийся по sequence сегмент, то информация из него переписывает уже имеющуюся в буфере. Так ведут себя все системы, кроме Windows, поэтому на Windows серверах эта техника не работает. Windows сохраняет старую информацию.
К предпоследнему отсылаемому сегменту (2-му по оригинальной очередности) приписывается слева seqovl_pattern размером seqovl (результат ресолвинга маркера), а tcp.th_seq уменьшается на seqovl.
Последний отсылаемый сегмент (1-й в оригинальной очередности) отправляется неизменным, переписывая в буфере сокета ложные данные из seqovl_pattern реальными. Восстанавливается непрерывная последовательность потока, данные передаются в сокет приложения.
multidisorder_legacy
function multidisorder_legacy(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - список маркеров через запятую - точек разреза. По умолчанию "2".
- arg: seqovl - маркер - смещение относительно текущего sequence для создания дополнительной части сегмента, выходящей влево
- arg: optional - отказ от операции, если blob задан, но отсутствует. если seqovl_pattern задан, но отсутствует, использовать паттерн 0x00.
- arg: seqovl_pattern - blob, используемый для заполнения seqovl. По умолчанию 0x00
Реализация multidisorder, полностью совместимая с nfqws1.
Новый multidisorder работает с reasm целиком, а старый из nfqws1 - по отдельным частям replay. Поэтому порядок следования частей в случае многопакетных запросов будет разным.
В новом варианте не сохраняется оригинальная сегментация. Если какая-то часть разреза получилась больше MSS, она дополнительно режется по MSS и отправляется в порядке увеличения sequence. В старом варианте оригинальная сегментация сохранялась. Использовалась нормализация точек разреза по смещению каждой отдельной части reasm. Сегменты отправлялись в обратном порядке только внутри каждой части, но следующая часть шла с оригинальным инкрементом sequence. seqovl так же нормализовался и применялся только внутри того оригинального сегмента, в какой попала нормализованная позиция.
fakedsplit
function fakedsplit(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - один маркер - точка разреза. По умолчанию "2".
- arg: seqovl - число - смещение относительно текущего sequence для создания дополнительной части сегмента, выходящей влево за границу tcp window
- arg: seqovl_pattern - blob, используемый для заполнения seqovl. По умолчанию 0x00
- arg: blob - заменить текущий пейлоад на указанный blob
- arg: optional - отказ от операции, если blob задан, но отсутствует. если seqovl_pattern задан, но отсутствует, использовать паттерн 0x00.
- arg: nodrop - отказ от вынесения VERDICT_DROP
- arg: nofake1, nofake2, nofake3, nofake4 - отказ от отсылки отдельных фейков
- arg: pattern - blob, которым заполняются фейковые части. По умолчанию 0x00.
- payload фильтр по умолчанию - "known"
Функция работает аналогично multisplit с одной позицией разреза, но с замешиванием фейков между реальными сегментами. Фейки совпадают в размерах с отсылаемыми частями и формируются на базе pattern со смещением, которое соответствует смещению tcp sequence отсылаемой части относительно первой.
Для фейков необходим фулинг, чтобы они не были приняты сервером.
Последовательность отсылки :
- Фейк 1-й части. (fake1)
- Реальная 1-я часть.
- Фейк 1-й части. (fake2)
- Фейк 2-й части. (fake3)
- Реальная 1-я часть.
- Фейк 2-й части. (fake4)
Цель данной техники - запутать DPI что есть оригинал, а что есть fake. Части одного размера, в одной мусор, в другой - реальные данные. Что выбрать ? Не знаем... Все выглядит как ретрансмиссии, имеет те же sequence и размеры.
- К оригиналам применяется только fooling_opts.tcp_ts_up. reconstruct_opts не применяются.
- К фейкам применяются fooling_opts и reconstruct_opts в полном объеме.
- ipid_opts и rawsend_opts применяются и к фейкам, и к оригиналам. ipfrag_opts не задействуются ни для фейков, ни для оригиналов.
В случае успеха отсылки выносится VERDICT_DROP, если не указано "nodrop".
blob позволяет заменить текущий пейлоад на произвольный blob, и тем самым отослать любой совместимый пейлоад с тем же разбиением.
fakeddisorder
function fakeddisorder(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - один маркер - точка разреза. По умолчанию "2".
- arg: seqovl - маркер - смещение относительно текущего sequence для создания дополнительной части сегмента, выходящей влево
- arg: seqovl_pattern - blob, используемый для заполнения seqovl. По умолчанию 0x00
- arg: blob - заменить текущий пейлоад на указанный blob
- arg: optional - отказ от операции, если blob задан, но отсутствует. если seqovl_pattern задан, но отсутствует, использовать паттерн 0x00.
- arg: nodrop - отказ от вынесения VERDICT_DROP
- arg: nofake1, nofake2, nofake3, nofake4 - отказ от отсылки отдельных фейков
- arg: pattern - blob, которым заполняются фейковые части. По умолчанию 0x00.
- payload фильтр по умолчанию - "known"
Функция работает аналогично multidisorder с одной позицией разреза, но с замешиванием фейков между реальными сегментами. Фейки совпадают в размерах с отсылаемыми частями и формируются на базе pattern со смещением, которое соответствует смещению tcp sequence отсылаемой части относительно первой.
Для фейков необходим фулинг, чтобы они не были приняты сервером.
Последовательность отсылки :
- Фейк 2-й части. (fake1)
- Реальная 2-я часть.
- Фейк 2-й части. (fake2)
- Фейк 1-й части. (fake3)
- Реальная 1-я часть.
- Фейк 1-й части. (fake4)
Кроме запутывания DPI в реальных и фейковых сегментах, добавляется еще и запутывание в их последовательности.
- К оригиналам применяется только fooling_opts.tcp_ts_up. reconstruct_opts не применяются.
- К фейкам применяются fooling_opts и reconstruct_opts в полном объеме.
- ipid_opts и rawsend_opts применяются и к фейкам, и к оригиналом. ipfrag_opts не задействуются ни для фейков, ни для оригиналов.
В случае успеха отсылки выносится VERDICT_DROP, если не указано "nodrop".
blob позволяет заменить текущий пейлоад на произвольный blob, и тем самым отослать любой совместимый пейлоад с тем же разбиением.
hostfakesplit
function hostfakesplit(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard reconstruct
- arg: standard rawsend
- arg: host - шаблон для генерации фейкового хоста - random.template
- arg: midhost - маркер для дополнительного разреза сегмента с реальным хостом
- arg: disorder_after - маркер для дополнительного разреза заключительной реальной части и отправки сегментов в обратном порядке
- arg: nofake, nofake2 - отказ от отсылки отдельных фейков
- arg: blob - заменить текущий пейлоад на указанный blob
- arg: optional - отказ от операции, если blob задан, но отсутствует
- arg: nodrop - отказ от вынесения VERDICT_DROP
- payload фильтр по умолчанию - "known"
Это специальный "резатель" с замешиванием фейков для пейлоадов, в которых присутствует имя хоста - http_req и tls_client_hello.
Двумя основными точкам разреза являются начало имени хоста - маркер "host" и конец имени хоста - маркер "endhost". Дополнительными и опциональными точками разреза являются маркер midhost (должен быть в пределах host..endost) и маркер disorder_after (должен быть больше endhost). При разрезе по disorder_after части отправляются в обратном порядке.
Для фейков необходим фулинг, чтобы они не были приняты сервером.
Последовательность отсылки :
- Реальная часть до host
- Фейк host..endhost-1 (fake1)
- Реальная часть host..endhost, либо 2 части : host..midhost-1, midhost..endhost-1
- Фейк host..endhost-1 (fake2)
- Реальная часть после host, либо 2 части : disorder_after..-1, endhost..disorder_after-1
- К оригиналам применяется только fooling_opts.tcp_ts_up. reconstruct_opts не применяются.
- К фейкам применяются fooling_opts и reconstruct_opts в полном объеме.
- ipid_opts и rawsend_opts применяются и к фейкам, и к оригиналом. ipfrag_opts не задействуются ни для фейков, ни для оригиналов.
В случае успеха отсылки выносится VERDICT_DROP, если не указано "nodrop".
blob позволяет заменить текущий пейлоад на произвольный blob, и тем самым отослать любой совместимый пейлоад с тем же разбиением.
tcpseg
function tcpseg(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: pos - список из двух маркеров, определяющий границы tcp сегмента
- arg: seqovl - число - смещение относительно текущего sequence для создания дополнительной части сегмента, выходящей влево за границу tcp window
- arg: seqovl_pattern - blob, используемый для заполнения seqovl. По умолчанию 0x00
- arg: blob - заменить текущий пейлоад на указанный blob
- arg: optional - отказ от операции, если blob задан, но отсутствует. если seqovl_pattern задан, но отсутствует, использовать паттерн 0x00.
- payload фильтр по умолчанию - "known"
Отсылает часть текущего диссекта, реасма, или произвольного blob, ограниченную двумя маркерами pos с опциональным применением техники seqovl таким же способом, как и в multisplit. Дополнительная сегментация при превышении MSS производится автоматически.
В случае reasm работает только при приеме его первой части (т.к. работает целиком по reasm, а не отдельным его частям).
Вердикт не выносится.
С помощью tcpseg можно сделать seqovl без сегментации, используя маркеры "0,-1". Для замещения текущего диссекта можно комбинировать с drop.
oob
function oob(ctx, desync)- arg: standard fooling
- arg: standard ipid
- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: char - 1 символ oob
- arg: byte - числовое значение байта OOB 0..255
- arg: urp - позиционный маркер для urgent pointer, "b" или "e". по умолчанию - "b"
Функция перехватывает TCP handshake, сдвигая sequence влево на 1 байт, затем вставляет 1 байт OOB в первый отсылаемый пейлоад и отсылает его.
После отработки функция уходит в cutoff по обоим направлениям.
ОС получателя выбрасывает OOB байт из потока, DPI может не выбрасывать и получать оригинальное сообщение со вставленным в него посторонним байтом, что может портить смысл всего сообщения.
- OOB - устаревший, но все еще поддерживаемый в ОС механизм. Имеется 2 стандарта. Один предполагает, что th_urp указывает на байт OOB,
другой - на следующий за ним байт. Поэтому значение th_urp=0 невалидно по одному из стандартов, но все еще может работать.
Чтобы задействовать его укажите "urp=b". Как показывает практика, "b" работает только на Linux серверах, остальные ломает. - Значение маркера urp означает позицию самого OOB байта. th_urp , кроме случая "b", увеличивается на 1 - согласно интерпретации RFC, поддерживаемой в основных ОС.
- "urp=e" вставляет OOB байт после самого последнего байта пейлоада - для обхода DPI как правило бесполезно, поскольку DPI получает все оригинальное сообщение.
- Для протоколов, в которых в самом начале сервер ждет запроса от клиента, требуется разрешение входящих в пределах
--in-range=-s1. В Windows--wf-tcp-inне нужен. Достаточно автоматически перехватываемых SYN. К таким протоколам относятся http и tls. - Для протоколов, где сервер что-то шлет до первого сообщения от клиента, требуется разрешить все входящие пакеты до отсылки первого исходящего пакета с данными. В Windows
--wf-tcp-inобязателен. - Не может быть отфильтровано по пейлоаду, поскольку после начала модификации tcp handshake соскок уже невозможен, иначе поедут sequence.
- oob - длящаяся десинхронизация. Если возможно переключение профилей до момента окончания работы oob, oob должен быть дублирован в другой профиль, иначе TCP будет сломан из-за десинхронизации sequence.
- Функция помечает специальный флаг, что была вызвана с самого начала соединения. Если управление получено после перескока профиля, происходит немедленный cutoff.
- Фильтрация по хостлистам возможна только при
--ipcache-hostname. Если в кэше хоста еще нет, и профиль получает управление не с начала соединения, срабатывает предыдущий пункт. - Не может работать с функциями, предполагающими отправку текущего пейлоада - multisplit, multidisorder, fakedsplit, fakeddisorder и тд. Они будут работать, но будут слать дубль без OOB. Ожидаемый результат "разбить на сегменты и всунуть OOB" как в tpws получить таким образом невозможно.
- Если пейлоад многопакетный - отсылается весь reasm. OOB вставляется в тот сегмент, куда попал urp.
В этом сегменте th_urp нормализуется по смещению сегмента, выставляется флаг TH_URG. Остальные части шлются как есть. Функция дропает весь replay, затем уходит в cutoff по обоим направлениям.
Дурение udp
Для udp намного меньше вариантов, чем для tcp, в силу простоты данного протокола. С ним особо нечего делать.
От stateful DPI могут помочь фейки. От stateless они не помогут. Может помочь фрагментация на уровне IP.
Для ipv6 могут сработать extension headers.
Из остального - только искажение самого пейлоада. Не все программы стерпят искажение информации, многие просто отбросят искаженные пакеты. Но есть и такие, в которых понятно что можно "подкрутить".
udplen
function udplen(ctx, desync)- arg: standard direction
- arg: standard payload
- arg: min - не трогать пакеты с длиной L4 пейлоада меньше
- arg: max - не трогать пакеты с длиной L4 пейлоада больше
- arg: increment - на сколько увеличить (+) или уменьшить (-) длину L4 пейлоада
- arg: pattern - blob, которым заполняется конец пакета при увеличении длины
- arg: pattern_offset - начальное смещение внутри pattern
- payload фильтр по умолчанию - "known"
Функция увеличивает или уменьшает длину L4 пейлоада udp. При уменьшении часть информации обрезается и теряется, при увеличении - заполняется pattern. Сегментация udp невозможна - при превышении MTU или PMTU ОС выполнит фрагментацию на уровне IP. Ошибка в случае превышения MTU будет только на Linux, остальные системы молча не отправят пакет (WinDivert и ipdivert не имеют средств обнаружения ошибки).
dht_dn
function dht_dn(ctx, desync)- arg: standard direction
- arg: dn - число N, следующее после 'd' в сообщении dht
dht использует формат bencode для передачи сообщений. 'd' - это тип данных directory. Сообщения dht как правило начинаются с 'd1' или 'd2' и заканчиваются 'e' (end). В некоторых DPI прописаны именно такие сигнатуры - только 'd1' или 'd1'+'d2'. Но можно поставить и 'd3', 'd4', ..., если корректно отредактировать содержимое, не нарушив формат bencode. Этим и занимается данная функция. Работает только по пейлоаду с типом "dht".
Другие функции
synack
function synack(ctx, desync)- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
Отсылает перед SYN пакет SYN,ACK, чтобы запутать DPI относительно направления tcp соединения. Атака называется в литературе "TCB turnaround". Ломает NAT - через NAT применение невозможно. Применение на проходящем трафике требует nftables и режим POSTNAT. После прохода пакета без SYN выполняет instance cutoff. Вердикт не выносит.
synack_split
function synack_split(ctx, desync)- arg: standard ipfrag
- arg: standard reconstruct
- arg: standard rawsend
- arg: mode - "syn", "synack" или "acksyn"
Методика предназначена для серверов. В литературе имеет название "TCP split handshake". Заменяет исходящий от сервера пакет SYN,ACK на SYN, 2 пакета SYN + ACK или 2 пакета ACK + SYN. В случае успеха отсылки выносит VERDICT_DROP. После прохода пакета без SYN,ACK выполняет instance cutoff.
Многие DPI ожидают стандартный ответ на SYN в виде SYN,ACK. В реальности получается, что ответ на SYN является тоже SYN, а потом клиент шлет серверу SYN,ACK. Тем самым DPI перестает понимать какая сторона является клиентом, а какая сервером, и алгоритм проверки дает сбой. Атака может сработать даже если клиент не делает ничего для обхода блокировки. Может использоваться совместно с клиентскими техниками для точечного пробития tcp.
Библиотека программ автоматизации и оркестрации zapret-auto.lua
Стандартный порядок применения инстансов линеен - слева направо с учетом внутрипрофильных фильтров и instance cutoff. nfqws2 никаких других вариантов не предоставляет.
Конечно, вы можете написать свою Lua функцию, которая сделает что нужно и когда нужно. Но вам придется при этом изобрести какое-то количество велосипедов, дублировать код или того хуже - патчить стандартные функции antidpi, добавляя туда ваши хотелки, а потом это самостоятельно поддерживать.
Суть механизмов оркестрации в отделении управляющей логики от логики непосредственных действий, чтобы не надо было ничего патчить, а если и писать свои функции, то писать в них только сам алгоритм управления, не мешая его с алгоритмами действий.
Оркестрация неразрывно связана с понятием плана выполнения (execution plan). Он включает в себя список инстансов, которые нужно вызвать последовательно с их параметрами и фильтрами. Базовый линейный оркестратор заложен в C код. Но эту роль может взять на себя и Lua функция, в которой можно запрограммировать любую логику.
Например, можно сделать автоматические стратегии - если одна не работает, использовать другую. C код имеет подобную логику только в механизме автоматических хостлистов. Но она не реализует динамическую смену стратегий.
Хранилище состояний
Логика автоматизации как правило простирается между пакетами и опирается на conntrack. Для хранения состояния потока используются элементы desync.track.lua_state.
Другая часть информации простирается между потоками и привязана к хосту. Для хранения этой информации используются глобальные таблицы, индексируемые по ключам.
automate_conn_record
function automate_conn_record(desync)Возвращает таблицу - хранилище состояния автоматизации, привязанное к потоку. Функции автоматизации могут использовать любые поля.
standard_hostkey
function standard_hostkey(desync)- arg: reqhost - требовать наличие hostname, не работать по IP
- arg: nld - номер уровня домена, до которого отсекается hostname. если не задано - не отсекается
Стандартный генератор host-ключей. Функция вычисляет строку-ключ, связанный с именем хоста или IP адресом, если имя хоста отсутствует. Если не может - возвращает nil.
automate_host_record
function automate_host_record(desync)- arg: key - ключ внутри глобальной таблицы autostate. если не задано, в качестве ключа используется имя текущего инстанса
- arg: hostkey - имя функции генератора host-ключей. если не задано, используется standard_hostkey
Возвращат таблицу - хранилище состояния автоматизации, привязанное к хосту или IP адресу. Используется 2 ключа - key и hostkey. Итоговое расположение - autostate.key.hostkey. Если hostkey получить не удается - возвращается nil.
Обслуживание удач и неудач
Простое, но далеко неточное, объяснение понятиям "удача" и "неудача" - "сайт открывается" и "сайт не открывается". В логике автоматизации эти состояния абстрактны. Любой процесс может претерпеть удачу или неудачу. Детект состояния удачи или неудачи нужен для подсчета неудач через счетчик. При неудаче счетчик увеличивается, при успехе - сбрасывается, при достижении целевого значения - возвращается признак, по которому внешняя логика может выполнить любое действие. Например, сменить стратегию.
automate_failure_counter
function automate_failure_counter(hrec, crec, fails, maxtime)- hrec - хост хранилище
- crec - потоковое хранилище
- fails - целевое количество неудач
- maxtime - время в секундах, после которого с момента предыдущей неудачи следующая неудача начинает счет заново
Возвращает true, если счетчик достиг значения fails. При этом счетчик сбрасывается.
automate_failure_counter_reset
function automate_failure_counter_reset(hrec)- hrec - хост хранилище
Сбрасывает значение счетчика.
Детекция удач и неудач
Детекторы удач и неудач - это заменяемые функции, берущие в качестве параметров desync и crec и возвращающие true , если зафиксирована удача или неудача.
automate_failure_check
function automate_failure_check(desync, hrec, crec)- hrec - хост хранилище
- crec - потоковое хранилище
- arg: success_detector - имя функции детектора удач. если не задано, используется standard_success_detector.
- arg: failure_detector - имя функции детектора неудач. если не задано, используется standard_failure_detector.
- arg: fails - целевое значение счетчика неудач. По умолчанию - 3.
- arg: maxtime - максимальное время в секундах между неудачами, после которого счетчик сбрасывается. По умолчанию - 60 сек.
Функция выполняет ведение счетчика неудач, вызывая детекторы удачи и неудачи. Возвращает true, если счетчик достиг целевого значения. При этом счетчик сбрасывается автоматически.
standard_success_detector
Стандартные детекторы удач и неудач требуют перенаправления входящего и исходящего трафика в объеме, необходимом для срабатывания их критериев.
По relative sequence - нужны пакеты до указанного rseq + длина максимального пейлоада одного пакета (1460 байт для tcp).
function standard_success_detector(desync, crec)- crec - потоковое хранилище
- arg: maxseq - исходящий relative sequence, при достижении которого фиксируется удача. по умолчанию 32768. Смысл : отправлено достаточно много без застревания потока из-за блокировки.
- arg: inseq - входящий relative sequence, при достижении которого фиксируется удача. по умолчанию 4096. Смысл : оппонент прислал достаточно много, чтобы это не было реакцией DPI.
- arg: udp_out, udp_in - принято более udp_in udp пакетов при условии, что udp_out>0. Смысл : оппонент прислал достаточно много, чтобы это не было реакцией DPI.
Стандартный детектор удач.
standard_failure_detector
function standard_failure_detector(desync, crec)- crec - потоковое хранилище
- arg: maxseq - считать ретрансмиссии в пределах исходящих relative sequence от 1 до maxseq. По умолчанию - 32768.
- arg: retrans - считать неудачей не менее retrans ретрансмиссий. По умолчанию - 3.
- arg: reset - посылать ретрансмиттеру RST, чтобы прекратить долгое ожидание
- arg: inseq - считать неудачей RST и http redirect в пределах входящих relative sequence от 1 до inseq. По умолчанию - 4096.
- arg: no_rst - не определять RST как неудачу
- arg: no_http_redirect - не определять http redirect как неудачу
- arg: udp_out, udp_in - считать неудачей ситуацию, когда по потоку отослано >=udp_out пакетов, а принято <=udp_in пакетов. Смысл : мы много шлем, а нам не отвечают или отвечают мало.
Стандартный детектор неудач.
http редиректом от DPI считается то же самое , что и в автохостлистах.
Оркестраторы
circular
function circular(ctx, desync)- arg: standard host storage
- arg: standard checker
- arg: (только для стандартного детектора) standard success detector
- arg: (только для стандартного детектора) standard failure detector
Оркестратор позволяет считать неудачи и менять стратегии по кругу по достижению счетчиком неудач целевого значения fails. Все последующие инстансы маркируются аргументом "strategy", который содержит номер стратегии, начиная от 1. Инстансы, не имеющие аргумента "strategy", circular не вызывает. Номера стратегии должны идти непрерывно от 1 до последней, промежутки не допускаются, иначе вызывается error. Если в любом инстансе стратегии N имеется аргумент "final", эта стратегия является последней - дальнейшее хождение по кругу блокируется.
Пример запуска :
--lua-desync=circular:fails=4:retrans=2:maxseq=16384
--lua-desync=argdebug:v=1.1:strategy=1
--lua-desync=argdebug:v=1.2:strategy=1
--lua-desync=argdebug:v=2.1:strategy=2
--lua-desync=argdebug:v=3.1:strategy=3:final
--lua-desync=argdebug:v=3.2:strategy=3
--lua-desync=argdebug:v=3.3:strategy=3Вместо реальных стратегий в примере используется вывод отладочной информации, чтобы проследить операции circular в debug log. Имеется 3 стратегии. 1-я стратегия включает 2 инстанса, 2-я - 1, 3-я - 3. 3-я стратегия является финальной, переход к 1 блокируется. Тренироваться нужно, дергая заблокированные сайты через curl, чтобы увидеть работу детектора неудач.
repeater
function repeater(ctx, desync)- arg: instances - сколько ближайших инстансов повторять
- arg: repeats - количество повторов
- arg: stop - не проигрывать однократно последующие инстансы после "instances"
- arg: clear - очистить execution plan после повторений
- arg: iff - имя функции условия для продолжения цикла повторов. если не задано - условие всегда true
- arg: neg - инвертировать значение iff. по умолчанию - false
Смысл repeater заключен в самом названии - он повторяет последующие инстансы в количестве instances repeats раз. Повторение идет по принципу 1-2-3-1-2-3-1-2-3-4-5-6. 4-5-6 в данном случае - последующие за 1-2-3 инстансы, если instances=3. Если задано stop или clear, 4-5-6 не вызываются. clear дополнительно очищает execution plan - бывает нужно для взаимодействия с вышестоящими оркестраторами.
Функция iff позволяет задать дополнительное динамическое условие продолжения цикла повторов. Если xor(iff, neg) = false, выполнение цикла прерывается.
repeater может сколько угодно раз быть вложенным.
В примере последовательность вызова получается : 1 1 1 2 2 2 1 1 1 2 2 2 3.
Параметр stop у вложенных инстансов не позволяет выполнить инстансы, не относящиеся к собственному циклу повтора.
Первый repeater не ограничен stop, поэтому он выполняет 3.
--lua-desync=repeater:repeats=2:instances=4
--lua-desync=repeater:repeats=3:stop --lua-desync=argdebug:v=1
--lua-desync=repeater:repeats=3:stop --lua-desync=argdebug:v=2
--lua-desync=argdebug:v=3condition
function condition(ctx, desync)- arg: iff - имя функции условия
- arg: neg - инвертировать значение iff. по умолчанию - false
- arg: instances - сколько последующих инстансов выполнять условно. все, если не задано.
condition вызывает iff. если iff xor neg = true, выполняются все инстансы plan, иначе план очищается.
per_instance_condition
function per_instance_condition(ctx, desync)- arg: instances - сколько последующих инстансов выполнять условно. все, если не задано.
Все последующие инстансы вызываются только, если у них есть аргумент "cond", содержащий iff функцию, и она возвращает true. Аргумент "cond_neg" инвертирует ее значение.
Имена аргументов не iff/neg, чтобы исключить конфликт с другими оркестраторами.
stopif
function stopif(ctx, desync)- arg: iff - имя функции условия
- arg: neg - инвертировать значение iff. по умолчанию - false
stopif вызывает iff. если iff xor neg = true, план очищается, иначе не делается ничего.
stopif может быть полезен как вложенный оркестратор. Например, его можно использовать с circular, чтобы блокировать выполнение стратегий при определенных условиях. condition для этих целей не подходит, потому что ничего не знает о вышестоящих оркестраторах, не знает о параметре "strategy", и будет выполнять инстансы без разбору до конца. stopif очистит план, тем самым прекратив дальнейшее выполнение вышестоящего оркестратора.
iff функции
Они используются в нескольких оркестраторах. Берут desync в качестве параметра.
Могут содержать любую логику, которую можно запрограммировать на Lua. В базовом комплекте есть несколько iff функций для демонстрации возможностей и тестирования.
cond_true
function cond_true(desync)Всегда true
cond_false
function cond_false(desync)Всегда false
cond_random
function cond_random(desync)- arg: percent - процент выпадения true. по умолчанию 50
Случайное значение true с вероятностью percent, иначе false.
cond_payload_str
function cond_payload_str(desync)- arg: pattern - строка, которая ищется в пейлоаде
Возвращает true, если в desync.dis.payload присутствует подстрока pattern.
Это простейший сигнатурный детектор. Если C код не распознает нужный вам протокол, вы можете написать свой сигнатурный детектор и запускать последующие инстансы под оркестратором condition с вашим детектором в качестве iff.
cond_tcp_has_ts
function cond_tcp_has_ts(desync)Возвращает true, если диссект является tcp и присутствует timestamp tcp option.
cond_lua
function cond_lua(desync)Выполняет Lua код из аргумента "cond_code". Код возвращает значение условия через return. Возможна прямая адресация таблицы desync.
desync.arg передаются с НЕ разименованными %, #, \, поскольку разименование может ссылаться на блобы, создаваемые предыдущими условно вызываемыми инстансами.
Вспомогательные программы
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 - гораздо лучше.
mdig
Программа предназначена для многопоточного ресолвинга больших листов через системный DNS.
Она берет из stdin список доменов и выводит в stdout результат ресолвинга. Ошибки выводятся в stderr.
--family=<4|6|46> ; выбор семейства IP адресов : ipv4, ipv6, ipv4+ipv6
--threads= ; количество потоков. по умолчанию 1.
--eagain=<eagain_retries> ; количество попыток повтора после EAI_AGAIN. по умолчанию 10
--eagain-delay=<ms> ; время ожидания в мсек между повторами по EAI_AGAIN. по умолчанию 500.
--verbose ; дебаг-лог на консоль
--stats=N ; выводить статистику каждые N доменов
--log-resolved=<file> ; сохранять успешно отресолвленные домены в файл
--log-failed=<file> ; сохранять неудачно отресолвленные домены в файл
--dns-make-query=<domain> ; вывести в stdout бинарный DNS запрос по домену. если --family=6, запрос будет AAAA, иначе A.
--dns-parse-query ; распарсить бинарный DNS ответ и выдать все ivp4 и ipv6 адреса из него в stdoutПараметры --dns-make-query и --dns-parse-query позволяют провести ресолвинг одного домена через произвольный канал.
Например, следующим образом можно выполнить DoH запрос, используя лишь mdig и curl :
mdig --family=6 --dns-make-query=rutracker.org | curl --data-binary @- -H "Content-Type: application/dns-message" https://cloudflare-dns.com/dns-query | mdig --dns-parse-queryblockcheck2
blockcheck2 - это средство автоматизации проверки стратегий. Представляет собой posix shell script. Имеет модульную структуру тестов.
Тест - это набор подключаемых шелл-скриптов для тестирования блока стратегий. Тестируемые стратегии могут формироваться алгоритмом скрипта в зависимости от различных условий или успешности предыдущих проверок, чтобы сократить общее время проверок.
Наборы тестов располагаются в субдиректориях blockcheck2.d. Название субдиректории есть название теста.
blockcheck2 по умолчанию работает в интерактивном режиме, выводя сообщения и спрашивая у пользователя параметры. Но параметров слишком много, поэтому спрашиваются далеко не все из них, а только основные. Оставшиеся передаются через shell переменные.
Типичная схема запуска с переменными :
BATCH=1 DOMAINS=bbc.com CURL_CMD=1 SKIP_DNSCHECK=1 /opt/zapret2/blockcheck2.sh
Если нужно записать лог, это делается стандартными для shell средствами :
/opt/zapret2/blockcheck2.sh | tee /tmp/blockcheck2.log
В win bundle можно пользоваться cygwin prompt (cygwin/cygwin-admin.cmd). Там уже созданы alias-ы для запуска blockcheck из первой версии zapret, blockcheck2, winws, winws2 с подключенными стандартными Lua скриптами. Это удобно, потому что не нужно думать о путях и каждый раз писать или вставлять откуда-то огромный текст, тем более при наличии национальных символов и пробелов в путях, путаясь в экранировании символов и кодировках.
Возможно последовательное тестирование нескольких доменов. Для этого нужно задать их через пробел.
Поддерживаются URI вида rutracker.org/forum/index.php. Префикса с протоколом вида https:// быть не должно. По умолчанию используется URI - корень ('/'). http тестируется через метод GET, https - через метод HEAD, поскольку под tls все равно ничего не видно. Однако, бывают ситуации, когда идет блок не сразу - по длинному ответу сервера. В этом случае можно пользоваться CURL_HTTPS_GET=1 и указать URI, по которому сервер возвращает длинный ответ.
blockcheck2 не является панацеей, не является средством генерации магических букв, которые надо куда-то вписать, чтобы сайты начали открываться. Это настраиваемое средство исследования DPI и автоматизации рутинной работы. Понимание выдаваемых результатов и способов их применения ложится на пользователя.
Не стоит так же ждать от blockcheck каких-то сверхсложных алгоритмов выбора стратегий.