Reimplement the `xor_protected_area` algorithm using explicit SIMD
instructions for the data packet path. This is by far the most common
type of WireGuard packet, and it also maps very cleanly onto a 8x16 SIMD
vector type since the first 16 bytes of a data packet is obfuscated in
LWO.
This path seems to auto-vectorize well on x86_64 (I saw no performance
increase on my AMD Ryzen 7 PRO 6850U), but less so on more niche CPU
archs. On Raspberry PI I saw a ~20% increase in throuhput and on Mac Neo
I saw a ~25% increase in throuhput.
The source address we report when implementing `UdpRecv` should not
actually matter. Let `talpid-wireguard` take care of it and just set it
to the wireguard endpoint instead.
It no longer pays the large cost of having a local UDP socket in front
of it. The `ObfuscatedTransport` does do some work on each `recv` to
handle different multiplexer phases. I didn't attempt to optimize this
because the cost was negligible compared to the socket syscalls.
The socket interface is still available by wrapping the multiplexer in a
`LocalSocketRunner`, as with other obfuscation types.
The multiplexer now records the `sender_index` of every handshake
initiation it fans out, and only selects a transport that answers one
of them with a handshake response whose `mac1` is valid for the
client's public key.
Sending one packet through the multiplexer to a relay on loopback, with a
single LWO transport (medians):
before after
plain UDP (control) 6.28 µs 6.29 µs
single obfuscator 21.37 µs 21.54 µs
(via socket)
multiplexer 32.56 µs 21.44 µs
The 11 µs is one loopback round trip, which puts the multiplexer level
with reaching the obfuscator directly plus one loopback socket.
The multiplexer reported a hardcoded 60. It now reports the largest
overhead among the transports it may select, which is what has to fit
while it is still deciding which one to use.
Drive obfuscation transports using `ObfuscatedTransport` instead of
loopback sockets.
The multiplexer itself is still behind a loopback socket.
A transport that fails to receive is now dropped instead of failing the
multiplexer, so the remaining ones can still connect.
`pad` followed by `obfuscate`, against `obfuscate` alone:
obfuscate pad + obfuscate
data (1232 B) 8.80 ns 13.30 ns
handshake init (148 B) 70.98 ns 190.29 ns
LWO v2 is ~2x faster, likely since it XORs only the 16-byte protected
area rather than the full 32-byte header:
v1 v2
obfuscate 20.97 ns (54.7 GiB/s) 9.87 ns (116.3 GiB/s)
deobfuscate 19.10 ns (60.1 GiB/s) 9.02 ns (127.2 GiB/s)
Each obfuscation method implements a send()/recv() interface --
`ObfuscatedTransport` -- rather than binding a client socket on localhost
and serving it itself.
The local socket becomes a layer on top of that interface.
While connecting with multiplexer obfuscation, every candidate
obfuscation endpoint has to be allowed through the firewall, since it is
not known which one will be used. Previously that wide set was kept for
the entire lifetime of the tunnel. The multiplexer now announces the
remote endpoint it commits to, and the connected state only allows that
one.
The multiplexer also stops the transports it did not select.
Note that the host routes to each candidate endpoint, added on macOS and
Windows, are left in place.
Exclude every socket that talks to a remote obfuscation server from the
tunnel, using the bypass provided by the caller. The default bypass is a
no-op, so this is a no-op until a caller provides an implementation.
When GotaTun is used with LWO obfuscation, packets previously traveled
through a localhost UDP proxy:
GotaTun -> loopback UDP -> LWO proxy -> remote socket -> relay
Kernel WireGuard continues to use the existing proxy path unchanged, as
do other obfuscation methods