С чего всё начал лось

Мой стандартное решение "на каждый день", когда дело доходит до связаывания компьютеров в одну сеть — Wireguard.
Wireguard хорош, умеет в p2p, и в целом так-то никаких минусов не имеет.

Минусы имеет нахождение части моей домашней инфраструктуры на территории под контролем страны, которая заблокировала Wireguard по сигнатурам.
Что, конечно же, максимально отвратительно, а ещё отвратительнее то, что блокировки уже давным-давно перестали подчиняться хоть какому-то законодательству.
В итоге получается непонятный чёрный ящик, который может делать что угодно, вести себя как угодно и никто уже не знает, как эта шайтан-железка вообще работает.

А значит настало время пенетрации.

Почему не обфускация?

На самом деле есть несколько проектов, которые позволяют обфусцировать Wireguard трафик и пробить фаерволл.
udp2raw, wstunnel и прочие отлично с этим справляются.
И ещё Amnezia VPN сделали свой собственный форк Wireguard'а, специально для пробития государственной цензуры.

Но главная проблема обфускации — это уменьшение эффективного MTU пакета. Ну, потому что один пакет мы оборачиваем в другой пакет, и вот этот оверхед место занимает.
A это не хорошо.

Чего я хочу от VPN

  • p2p mesh сеть
    Wireguard — это конечно хорошо, но пропускать весь трафик через один сервер чревато последствиями.
    Последствия обычно включают запуск марсохода, чтобы переключить VPN на другой сервер в случае блокировки по IP или просто потому что серверу стало нехорошо.
    Да и гонять трафик через половину планеты только чтобы постучаться в машину, которая находится на расстоянии вытянутой руки — это что-то неправильное.

  • Open source and selfhosted
    В таких делах полагаться на third-party провайдера либо опасно, либо бесполезно.
    Тот же Tailscale, например, знаменит своими географическими блокировками, так что полагаться на него бессмысленно.
    А так как Tailscale делает это не по желанию левой пятки (надеюсь), то нет никаких гарантий, что другие сервисы не поступят так же.

  • Идеологически правильный VPN
    Этот пункт тут существует специально для Headscale и ZeroTier.
    Создание урезанного опенсорсного продукта для рекламы коммерческого — это порочная практика и лично я такое не одобряю.

  • Не Wireguard
    По очевидным причинам. Блокировки по сигнатурам.

  • Упаковано в nixpkgs
    Тут ещё очевиднее. Сношала я VPN в nix паковать.

Подопытные

EasyTier

GitHub, Official site

Это, наверное, самый простой способ создать p2p сеть.
Настолько простой, что в nixpkgs даже нет модуля для его запуска.

Из безопасности тут только строка с паролем в --network-secret, которая и используется для шифрования трафика.

Для работы сразу открывает TCP, UDP, WG, WS, WSS и черта лысого. Одно заблокировано, так через другое пробьётся.

Фактически все ноды в сети одинаковы и можно указать несколько пиров для первоначальной установки связи.
Можно использовать как публичные, которые можно посмотреть здесь, так и указать одну из своих нод.
Никакой дополнительной настройки оно не требует.

К слову, имеет клиенты для Android, Windows и Mac OS, так что самое время доставать те старые игры, в которые недоиграли в детстве и устраивать LAN-пати с не сильно сведующими в технологиях друзьями.

Главный недостаток — нельзя забиндить IP адреса к конкретным машинам.

И да, это проект из Китая, может кому-то не понравится по идеологическим причинам, но лично я надеюсь, что оно создано энтузиастами специально для пробития Большого Китайского Фаерволла.

Пример конфигурации
{
  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

GitHub, Official site

Это уже более пафосное коммерческое решение от создателей Slack.

Имеет шифрование на эллиптических кривых, предлагает использовать свой PKI и выглядит в целом надёжненько.
Хотя перспектива раскидывать сертификаты на машины ручками меня не радует.

Для своей работы требует "маяки", которые будут соединять все остальные ноды.
Внутри всё работает вроде как на Noise Protocol.
Наружу торчит только одним UDP портом.

Из приятных фишек есть фаерволл и зонирование, чтобы строить чуть более сложные сети, чем "все со всеми".

А ещё интерфейс у Nebula максимально всратый.
Вместо нормального cli, нужно настроить внутренний sshd и по ssh, соответственно, подключаться на локалхост.
Может и безопаснее, но вообще отвратительно.

Пример конфигурации
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

GitHub, Official site

Когда я нашла это чудо, первой мыслью было "Что это за херня?"
Проекту больше 10 лет и он до сих пор находится в нестабильном состоянии.
Текущая версия 1.1pre18, вышла аж в 2021-м году.
Последний коммит в 1.1 ветку был больше года назад.
В Nix запакована как чёрт пойми что.
Это вот как оно вообще?

Но на самом деле Tinc способен очень даже сильно удивить.

Под капотом использует свой собственный протокол поверх UDP, эллиптические кривые и тонну чёрной магии (к слову, нормально задокументированной), на которой это всё работает.

Разумеется, всё ещё требует внешнюю ноду, через которую будет инициализировать все соединения, но в остальном сеть полностью одноранговая.

Имеет относительно нормальный cli, умеет показывать граф всей сети, есть прочие вкусные штуки, но вот очень не хватает какого-то tui, или хотя бы ASCII-арта для отрисовки этого самого графа.

Пример упоротой конфигурации

По очевидным причинам конфигурация собрана дендрофекальным способом, очень советую не копировать as is, а всё же переписать самостоятельно.

Да, конфигурация интерфейса и роутов делается через tinc-up и tinc-down скрипты.
Это intended way

let
  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
    '';
  };
}

Методология измерения

Это вообще очень большая тема и по ней можно целую книгу написать, но самое главное — IPerf врёт.
Разные версии IPerf показывают разные цифры, используют разные методики измерения по-умолчанию, имеют много вариантов тюнинга, которые влияют на результаты и порой их показания сильно отличаются от реальности.

Так что вместе с двумя версиями iperf стоит добавить немного реальных кейсов использования сети.

Скорость интернетов в обе стороны у всех нод примерно одинаковая, поэтому я буду брать цифры с первого попавшегося направления, ибо разница будет в пределах погрешности.

Инфраструктура

Для реалистичных измерений я буду использовать три машины:

  • Домашний ноутбук (Laptop) в Испании
  • Промежуточный сервер с публичным IP (Lighthouse) в Финляндии
  • Raspberry Pi (RPi) за российским фаерволлом

При этом на Lighthouse размещены координаторы mesh-сетей, а скорость измеряется между Laptop и RPi.

Ping

ping -c 300 10.1.0.3

Отправляем ICMP пакетики, ждём пока прилетит ответ, меряем время за которое ответ был получен.
Тут можно проверить latency, jittering и количество потерянных пакетов.

Latency — это среднее время ответа на пинг.
Jittering — это насколько сильно время ответа "гуляет" относительно среднего. Измеряется в ms.
С количеством потерянных пакетов и так всё понятно.

Для плюс-минус стабильных результатов 300 пакетов должно хватить.

/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

Читаем три раза по 128 Mb нулей через SSH, потом смотрим на скорость чтения.
В целом не самый плохой способ определить скорость передачи данных внутри SSH туннеля.

Главная причина использования этого теста — через некоторые решения SSH работает настолько адски медленно, что между нажатием клавиши и появлением символа на экране может пройти больше секунды, а это никуда не годится.
А иногда так вообще не работает.

Wget

wget 10.1.0.3:5201/testfile

В качестве тестового файла — всё те же 384 Mb нулей из /dev/null.

В качестве сервера использую simple-http-server, выставив количество потоков равное количеству ядер CPU (8).
Разумеется, с выключенным сжатием, иначе мегабайты нулей рискуют превратиться в килобайт хедеров.

iperf2 и iperf3

Да, они показывают цены на апельсины в Африке. Чёрт его знает, как это тюнить.
Поэтому просто запускаем с стандартной конфигурацией и потом нормализуем результаты из мегабитов в мегабайты.

Референсные значения

Измерить точные значения скорости, пинга и вот этого вот всего, от чего можно было бы отталкиваться немножечко невозможно, ибо обе машины находятся за NAT.
Но так как в инфраструктуре имеется Lighthouse с публичным IP, можно сделать несколько тестов и нафантазировать какие-то результаты.

ICMP packet lossICMP LatencyICMP Jittering/dev/zero through SSHWgetiperf2iperf3
Laptop -> Lighthouse0%71.879 ms1.422 ms25.5 MB/s23.3MB/s18 MB/s24.375 MB/s
RPi -> Lighthouse0%51.872 ms1.011 ms9.0 MB/sTimeout9.963 MB/s11.112 MB/s

А теперь можно начать фантазировать.

Скорость между нодами не может быть больше минимального участка, поэтому в референсные значения тоже берём минимальную.
Latency можно просто сложить.
А вот что делать с jittering'ом — не до конца понятно. Вроде бы складывать такие значения нельзя, с нуля пересчитывать каждый пакет я не хочу, поэтому просто возьму максимальное значение.

И настало время итоговых результатов

Результаты

Вся скорость нормализована в байтах. Для перевода в биты нужно умножить на 8.

ICMP packet lossICMP LatencyICMP Jittering/dev/zero through SSHWgetiperf2iperf3
Reference0%123.751 ms1.422 ms9.0 MB/sTimeout9.963 MB/s11.112 MB/s
Wireguard + udp2raw49.6%108.806 ms3.724 msTimeoutTimeout3.175 KB/s0.00 B/s
EasyTier0%153.163 ms36.290 ms2.7 MB/s8.09 MB/s6.15 KB/s0.00 B/s
Nebula0%122.173 ms15.054 ms2.7 MB/s3.40 MB/s5.975 KB/s0.00 B/s
Tinc2.3%115.065 ms3.393 ms14.7 MB/s5.16 MB/s6.488 MB/s4.175 MB/s

Египетская сила тех iperf'ов...

Tinc, как уже говорила, очень способен удивить.

EasyTier простительны такие оверхеды, он всё таки Ad-hoc и вообще "скажи спасибо, что связь в принципе есть".

А вот Nebula меня откровенно разочаровала. Тут так и хочется ввернуть шуточку про клиент Слака на Электроне, но... Я ожидала лучшего, серьёзно.

Так что если хочется заиметь что-то такое — Tinc лучший выбор в плане производительности.
Я же себе оставлю все и сразу.
Не люблю марсоход запускать без надобности.

На этом у меня всё.





А началось-то всё с того, что мама попросила починить робот-пылесос...