Testing "exotic" p2p VPN
How did the moose begin
My standard "everyday" solution when it comes to connecting computers into a single network is Wireguard.
Wireguard is good, supports p2p, and generally has no downsides.
The downsides come from having part of my home infrastructure located in territory controlled by a country that has blocked Wireguard by signatures.
This is, of course, utterly disgusting, and what's even more disgusting is that these blocks have long since stopped following any kind of legislation.
The result is an incomprehensible black box that can do anything, behave however it wants, and nobody knows how this shaitan-machine even works anymore.
So it's time for penetration.
Why not obfuscation?
Actually, there are several projects that allow obfuscating Wireguard traffic and punching through firewalls.
udp2raw, wstunnel and others handle this excellently.
And Amnezia VPN has made their own fork of Wireguard, specifically for breaking through government censorship.
But the main problem with obfuscation is the reduction of effective packet MTU. Because we wrap one packet in another packet, and this overhead takes up space.
And that's not good.
What I want from a VPN
p2p mesh network
Wireguard is good, of course, but routing all traffic through one server has consequences.
The consequences usually include launching a Mars rover to switch the VPN to another server in case of IP blocking or just because the server started feeling unwell.
And routing traffic halfway around the planet just to get access to a machine that's within arm's reach — that's just wrong.Open source and selfhosted
In matters like this, relying on a third-party provider is either dangerous or useless.
Tailscale, for example, is famous for its geographical blocks, so relying on it is pointless.
And since Tailscale doesn't do this on a whim (I hope), there's no guarantee that other services won't do the same.Ideologically correct VPN
This point exists here specifically for Headscale and ZeroTier.
Creating a crippled open-source product to advertise a commercial one is a vicious practice and I personally don't approve this.Not Wireguard
For obvious reasons. Signature-based blocking.Packaged in nixpkgs
This one's even more obvious. I'm not going to package a VPN into nix myself.
Test subjects
EasyTier
This is probably the simplest way to create a p2p network. So simple that there isn't even a module in nixpkgs to run it.
For security, there's only a password in --network-secret
, which is used for traffic encryption.
To work, it immediately opens TCP, UDP, WG, WS, WSS and whatever Lucifer's IT department cooked up. If one gets blocked, it'll break through via another.
Essentially all nodes in the network are identical and you can specify multiple peers for initial connection establishment.
You can use either public ones, which can be viewed here, or specify one of your own nodes.
It doesn't require any additional configuration.
By the way, it has clients for Android, Windows and Mac OS, so it's a good time to dig out those old games you didn't finish in childhood and organize LAN party with friends who aren't very tech-savvy.
The main disadvantage is that you can't bind IP addresses to specific machines.
And yes, this is a project from China, which might not appeal to some for ideological reasons, but personally I hope it was created by enthusiasts specifically for breaking through the Great Firewall of China.Configuration example
{
networking.firewall = {
allowedTCPPorts = [ 11010 11011 11012 ];
allowedUDPPorts = [ 11010 11011 11012 ];
};
environment.systemPackages = [ pkgs.easytier ];
systemd.services."easytier" = {
enable = true;
script = "easytier-core -d --network-name sumeragi --network-secret changeme -p tcp://public.easytier.cn:11010 --dev-name et0 --multi-thread";
serviceConfig = {
Restart = "always";
RestartMaxDelaySec = "1m";
RestartSec = "100ms";
RestartSteps = 9;
User = "root";
};
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = with pkgs; [
easytier
iproute2
bash
];
};
}
Nebula
This is a more pompous commercial solution from the creators of Slack.
It has elliptic curve encryption, suggests using its own PKI and looks generally reliable.
Though the prospect of manually distributing certificates to machines doesn't thrill me.
For its operation it requires "lighthouses" that will connect all other nodes.
Inside, everything works on Noise Protocol.
On the outside it exposes only a single UDP port.
Among the nice features there's a firewall and zoning, to build slightly more complex networks than "everyone with everyone."
And also Nebula's interface is absolutely shit.
Instead of a normal CLI, you need to configure an internal sshd and connect via SSH to localhost.
Maybe it's more secure, but it's utterly disgusting.ConfigurationExample
let
isLighthouse = if (config.networking.hostName == "lighthouse") then true else false;
in
{
services.nebula.networks.sumeragi = {
enable = true;
ca = "/etc/nebula/ca.crt";
cert = "/etc/nebula/node.crt";
key = "/etc/nebula/node.key";
isLighthouse = isLighthouse;
lighthouses = if (isLighthouse) then [] else [ "10.1.0.1" ];
listen = {
host = "0.0.0.0";
port = 4242;
};
staticHostMap = {
"10.1.0.1" = [ "266.266.266.266:4242" ];
};
settings = if (isLighthouse) then {
sshd = {
enabled = true;
listen = "127.0.0.1:2222";
host_key = "/etc/nebula/id_ed25519";
authorized_users = [
{
user = "nommy";
keys = [
"ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
];
}
];
};
} else {
};
firewall = {
outbound = [
{ port = "any"; proto = "any"; host = "any"; }
];
inbound = [
{ port = "any"; proto = "any"; host = "any"; }
];
};
};
networking.firewall.allowedUDPPorts = [ 4242 ];
}
Tinc
When I found this, my first thought was "The fuck is this?"
The project is over 10 years old and is still in an unstable state.
The current version is 1.1pre18
, released way back in 2021.
The last commit to the 1.1
branch was over a year ago.
It's packaged in Nix as Lucy knows what.
How is this even a thing?
But actually, Tinc can surprise you quite a bit.
Under the hood it uses its own protocol over UDP, elliptic curves and a ton of black magic (which, by the way, is properly documented) that makes it all work.
Of course, it still needs a node for initial connection bootstrapping, but there's no special setup required — any node can do it, and afterwards it's all direct node-to-node communication.
It has a relatively normal CLI, can show a graph of the entire network, has other tasty features, but really lacks some kind of TUI, or at least ASCII art for rendering that graph. For obvious reasons, the configuration was assembled in a dendrofecal manner, I strongly advise not copying it as-is, but rewriting it yourself. Yes, interface and route configuration is done through Example of not very good configuration
tinc-up
and tinc-down
.
This is the intended waylet
hostName = config.networking.hostName;
in
{
networking.firewall.allowedTCPPorts = [ 655 ];
networking.firewall.allowedUDPPorts = [ 655 ];
services.tinc = {
networks = {
sumeragi = {
name = hostName;
ed25519PrivateKeyFile = "/etc/tinc/sumeragi/ed25519_key.priv";
interfaceType = "tun";
debugLevel = 3;
hostSettings = {
lighthouse = {
settings.Ed25519PublicKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
subnets = [
{ address = "10.2.0.1/32"; }
];
addresses = [
{ address = "266.266.266.266"; port = 655; }
];
};
laptop = {
settings.Ed25519PublicKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
subnets = [
{ address = "10.2.0.2/32"; }
];
};
rpi = {
settings.Ed25519PublicKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
subnets = [
{ address = "10.2.0.3/32"; }
];
};
};
};
};
};
environment.etc = {
"tinc/sumeragi/tinc-up".source = pkgs.writeScript "tinc-up-sumeragi" ''
#!${pkgs.stdenv.shell}
${pkgs.nettools}/bin/ifconfig $INTERFACE ${(builtins.elemAt config.services.tinc.networks.sumeragi.hostSettings."${hostName}".subnets 0).address} netmask 255.255.255.0
/run/current-system/sw/bin/ip r add 10.2.0.0/24 dev tinc.sumeragi
'';
"tinc/sumeragi/tinc-down".source = pkgs.writeScript "tinc-down-sumeragi" ''
#!${pkgs.stdenv.shell}
${pkgs.nettools}/bin/ifconfig $INTERFACE down
/run/current-system/sw/bin/ip r del 10.2.0.0/24 dev tinc.sumeragi
'';
};
}
Methodology of measurment
This is actually a huge topic and you could write a whole book about it, but the most important thing is — IPerf lies.
Different versions of IPerf show different numbers, use different measurement methodologies by default, have many tuning options that affect results, and sometimes their readings differ significantly from reality.
So along with two versions of IPerf, it's worth adding some real-world network usage cases.
Internet speeds in both directions are roughly the same for all nodes, so I'll take numbers from the first direction that comes up, since the difference will be within the margin of error.
Infrastructure
For realistic measurements I'll use three machines:
- Home laptop (Laptop) in Spain
- Intermediate server with public IP (Lighthouse) in Finland
- Raspberry Pi (RPi) behind the Russian firewall
The mesh network coordinators are hosted on Lighthouse, while speed is measured between Laptop and RPi.
Ping
ping -c 300 10.1.0.3
We send ICMP packets, wait for the response to arrive, measure the time it took to get the response.
Here we can check latency, jittering and the number of lost packets.
Latency is the average ping response time.
Jittering is how much the response time "wanders" relative to the average. Measured in ms.
The number of lost packets is self-explanatory.
For more or less stable results, 300 packets should be enough.
/dev/zero through SSH
ssh 10.1.0.3 'dd if=/dev/zero bs=128M count=3 2>/dev/null' | dd of=/dev/null status=progress
We read three times 128 MB of zeros through SSH, then look at the reading speed.
Generally not a bad way to determine data transfer speed inside an SSH tunnel.
The main reason for using this test is that through some solutions SSH works so hellishly slow that more than a second can pass between pressing a key and the character appearing on screen, which is completely unacceptable.
And sometimes it doesn't work at all.
Wget
wget 10.1.0.3:5201/testfile
As a test file — the same 384 MB of zeros from /dev/null.
As a server I use simple-http-server, setting the number of threads equal to the number of CPU cores (8).
Of course, with compression disabled, otherwise megabytes of zeros risk turning into kilobytes of headers.
iperf2 and iperf3
Yes, they show orange prices in Africa. Hell knows how to tune this.
So we just run them with standard configuration and then normalize the results from megabits to megabytes.
Reference values
Measuring exact values for speed, ping and all this stuff that we could use as a baseline is somewhat impossible, since both machines are behind NAT.
But since the infrastructure includes a Lighthouse with a public IP, we can run a few tests and fantasize about some results.
ICMP packet loss | ICMP Latency | ICMP Jittering | /dev/zero through SSH | Wget | iperf2 | iperf3 | |
---|---|---|---|---|---|---|---|
Laptop -> Lighthouse | 0% | 71.879 ms | 1.422 ms | 25.5 MB/s | 23.3 MB/s | 18 MB/s | 24.375 MB/s |
RPi -> Lighthouse | 0% | 51.872 ms | 1.011 ms | 9.0 MB/s | Timeout | 9.963 MB/s | 11.112 MB/s |
Now we can start fantasizing.
Speed between nodes is limited by the slowest link, so we use the minimum values as our reference.
Latencies can simply be added together. But what to do with jittering isn't entirely clear.
Supposedly you can't add such values,
I don't want to recalculate every packet manually, so I'll just take the maximum value.
And it's time for the final results.
Results
All speeds are normalized in bytes. To convert to bits, multiply by 8.
ICMP packet loss | ICMP Latency | ICMP Jittering | /dev/zero through SSH | Wget | iperf2 | iperf3 | |
---|---|---|---|---|---|---|---|
Reference | 0% | 123.751 ms | 1.422 ms | 9.0 MB/s | Timeout | 9.963 MB/s | 11.112 MB/s |
Wireguard + udp2raw | 49.6% | 108.806 ms | 3.724 ms | Timeout | Timeout | 3.175 KB/s | 0.00 B/s |
EasyTier | 0% | 153.163 ms | 36.290 ms | 2.7 MB/s | 8.09 MB/s | 6.15 KB/s | 0.00 B/s |
Nebula | 0% | 122.173 ms | 15.054 ms | 2.7 MB/s | 3.40 MB/s | 5.975 KB/s | 0.00 B/s |
Tinc | 2.3% | 115.065 ms | 3.393 ms | 14.7 MB/s | 5.16 MB/s | 6.488 MB/s | 4.175 MB/s |
Egyptian power of those iperfs...
Tinc, as I already said, is very capable of surprising.
EasyTier can be forgiven for such overheads, it's ad-hoc after all and generally "be thankful there's any connection at all."
But Nebula frankly disappointed me. Here I really want to crack a joke about the Slack client on Electron, but... I expected better, seriously.
So if you want to get something like this — Tinc is the best choice performance-wise.
I'll keep all of them at once for myself.
I don't like launching Mars rovers unnecessarily.
That's all, folks.
And it all started when mom asked me to fix the robot vacuum...