README.md

PM-NetLink Client

PM-NetLink Client - Linux-контроллер раздельного туннелирования для Xray-core. Он не реализует VPN-протокол, не поднимает tun-интерфейс и не подменяет сертификаты. Вместо этого проект собирает в один управляемый контур:

  • Xray transparent inbound dokodemo-door с TProxy;
  • nftables table inet pmnlc;
  • policy routing через ip rule и отдельную route table;
  • systemd units для routing/nftables и Xray daemon;
  • CLI и дополнительный Qt GUI.

Основная идея: в режиме whitelist только выбранные домены, доменные списки и будущие process-aware правила идут через proxy outbound Xray. Остальной трафик остается прямым. В режиме blacklist GUI показывает это как full tunnel: неподходящий под правила трафик идет через proxy, но локальные/private сети по-прежнему обходятся.

Статус и безопасность

MVP ориентирован на Arch Linux и AUR/yay, но большая часть кода подходит для обычных Linux-систем с systemd, nftables и iproute2.

PM-NetLink Client:

  • не читает содержимое HTTPS;
  • не делает MITM;
  • не устанавливает пользовательские CA-сертификаты;
  • не меняет глобальные DNS-настройки;
  • не вызывает flush ruleset;
  • создает только свою nftables table inet pmnlc;
  • пишет runtime Xray JSON с приватными правами 0600;
  • хранит профили в ~/.config/pm-netlink-client/profiles с правами 0600;
  • хранит GUI state, включая URL подписки, в ~/.config/pm-netlink-client/gui-state.yaml с правами 0600;
  • передает URL подписок в privileged GUI helper через stdin, а не через argv.
  • загружает подписки и remote split-lists только по http/https и отклоняет локальные, private и link-local адреса.

Маршрутизация по доменам зависит от метаданных, которые может увидеть Xray sniffing: SNI, HTTP Host, видимые QUIC-метаданные, IP, порт и протокол. ECH, DoH, некоторые QUIC-сценарии и CDN могут снижать точность domain-aware routing.

Возможности

  • CLI-first управление через pmnlc (pm-netlink-client остается legacy alias).
  • Нативный Qt GUI через pm-netlink-client-gui.
  • Импорт VLESS Reality, VLESS TLS и VLESS plain URI.
  • YAML-профили для VLESS Reality, VLESS TLS, VLESS plain и Shadowsocks.
  • VLESS TCP/raw/WebSocket/gRPC transports при импорте URI и генерации Xray.
  • Подписки с несколькими VLESS-профилями, обычные и base64 payload.
  • Чтение subscription-userinfo quota header: upload, download, total, expire, unlimited/lifetime.
  • Раздельные пользовательские и удаленные split-rules.
  • Автообновление подписки в GUI.
  • Обновление удаленного списка доменов в GUI.
  • Переключение whitelist split tunneling и blacklist/full tunnel.
  • Светлая/темная тема GUI.
  • Просмотр systemd logs и GUI-событий.
  • Автоочистка GUI-событий.
  • Dry-run для системных операций CLI.
  • Диагностика профиля через временный локальный Xray HTTP proxy.
  • Redaction proxy URI и секретов в логах.

Лицензия

Copyright (C) 2026 PM-NetLink Client contributors.

PM-NetLink Client - свободное ПО: вы можете распространять и/или изменять его на условиях GNU General Public License, опубликованной Free Software Foundation, версии 3 этой лицензии или любой более поздней версии на ваш выбор.

PM-NetLink Client распространяется в надежде, что он будет полезен, но БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ; даже без подразумеваемых гарантий ТОВАРНОЙ ПРИГОДНОСТИ или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ. Подробности см. в GNU General Public License.

Копия GNU General Public License находится в файле LICENSE.

Зависимости

CLI на Arch Linux:

sudo pacman -S python polkit xray nftables iproute2 systemd

GUI дополнительно:

sudo pacman -S pyside6

Python-зависимости CLI:

  • Python 3.11+
  • pydantic
  • pyyaml
  • rich
  • typer

Dev/test зависимости:

  • build
  • pytest

GUI-зависимость PySide6 вынесена в optional extra gui, поэтому CLI-only установка не подтягивает Qt.

Установка через yay/AUR

После публикации релиза v0.1.5:

yay -S pm-netlink-client

GUI-дополнение:

yay -S pm-netlink-client-gui

pm-netlink-client-gui зависит от точной версии pm-netlink-client, поэтому CLI будет установлен автоматически.

AUR-пакет устанавливает:

  • /usr/bin/pmnlc;
  • /usr/bin/pm-netlink-client как legacy alias;
  • /usr/bin/pm-netlink-client-gui только в GUI-пакете;
  • /usr/bin/pm-netlink-client-gui-helper для privileged GUI RPC;
  • /usr/lib/systemd/system/pm-netlink-client-routing.service;
  • /usr/lib/systemd/system/pm-netlink-clientd.service;
  • polkit action/rule для GUI-helper;
  • man pages для CLI и GUI;
  • документацию и examples в /usr/share/doc.

Службы не включаются автоматически. После установки подготовьте пользовательский config-dir и systemd units с явным путем к нему, затем импортируйте профиль, выберите его и включите сервисы:

sudo pmnlc install
sudo pmnlc import-uri my-profile 'vless://...'
sudo pmnlc use-profile my-profile
sudo pmnlc add-domain claude.ai
sudo pmnlc autostart enable
sudo pmnlc start

Или используйте встроенный lifecycle:

sudo pmnlc start
sudo pmnlc status

Для одного root-сеанса без повторения sudo можно открыть интерактивную оболочку:

sudo pmnlc shell

Установка из checkout

CLI:

pipx install .

или:

uv tool install .

CLI + GUI:

pipx install '.[gui]'

или:

uv tool install '.[gui]'

Затем подготовьте системные файлы:

sudo pmnlc install

Команда install:

  • проверяет наличие xray, nft, ip, systemctl;
  • создает ~/.config/pm-netlink-client/config.yaml, если его еще нет;
  • создает ~/.config/pm-netlink-client/profiles;
  • пишет начальный nftables config в ~/.config/pm-netlink-client/pmnlc.nft;
  • устанавливает systemd units в /etc/systemd/system;
  • прописывает в units абсолютный путь к пользовательскому config.yaml;
  • вызывает systemctl daemon-reload;
  • не включает службы без --enable.

Если CLI установлен в пользовательский путь, передайте абсолютный путь:

sudo pmnlc install --cli-path "$(command -v pmnlc)"

Для проверки без изменений:

sudo pmnlc install --dry-run

Быстрый старт CLI

sudo pmnlc install
sudo pmnlc import-uri my-profile 'vless://00000000-0000-4000-8000-000000000000@example.com:443?encryption=none&security=tls&type=tcp#Demo'
sudo pmnlc use-profile my-profile
sudo pmnlc add-domain claude.ai
sudo pmnlc generate
sudo pmnlc test
sudo pmnlc test --profile my-profile --probe-url https://www.google.com/generate_204
sudo pmnlc autostart enable
sudo pmnlc start
sudo pmnlc status

Интерактивная альтернатива после установки и импорта профиля:

sudo pmnlc shell
PM-NetLink Client shell
For help, type "help".
Type "apropos word" to search for commands related to "word".
(pmnlc) status
(pmnlc) add-domain claude.ai
(pmnlc) restart
(pmnlc) quit

add-domain в CLI добавляет точное доменное правило в rules.proxy_domains. Для правила “домен и поддомены” используйте GUI, удаленный split-list или добавьте домен в rules.proxy_domain_suffixes вручную.

Первый запуск GUI

Запуск:

pm-netlink-client-gui

GUI работает от обычного desktop user. Для privileged-операций с systemd, nftables и с файлами ~/.config/pm-netlink-client, если helper запущен через pkexec, он пишет в config-dir исходного desktop-пользователя, а не в /root:

pm-netlink-client-gui-helper

В packaged GUI установлен polkit action/rule для org.pm-netlink-client.gui-helper: активный локальный desktop-пользователь может запускать только этот helper через pkexec без ввода пароля. Inactive, remote и другие вызовы остаются на обычном polkit prompt. В рамках одной сессии окна GUI держит privileged helper открытым и отправляет newline-delimited JSON через stdin.

При первом запуске GUI попросит URL подписки. После импорта можно:

  • выбрать активный профиль;
  • включить/выключить VPN;
  • переключить split tunneling/full tunnel;
  • обновить подписку;
  • настроить автообновление;
  • обновить удаленный список доменов;
  • добавить свои сайты отдельно от remote list;
  • посмотреть и очистить GUI/systemd logs;
  • выбрать светлую или темную тему.

Архитектура

Высокоуровневый поток:

CLI/GUI
  -> ~/.config/pm-netlink-client/config.yaml
  -> /run/pm-netlink-client/xray.json
  -> /run/pm-netlink-client/pmnlc.nft
  -> systemd starts routing/nftables and Xray daemon
  -> nftables marks tcp/udp traffic
  -> policy routing sends marked packets to loopback
  -> Xray TProxy inbound receives original destination
  -> Xray routing chooses proxy or direct outbound

Systemd units:

  • pm-netlink-client-routing.service - oneshot service, применяет policy routing и nftables, снимает их в ExecStop.
  • pm-netlink-clientd.service - long-running daemon, генерирует Xray config и запускает xray run -config.

Runtime-файлы:

/run/pm-netlink-client/xray.json
/run/pm-netlink-client/pmnlc.nft

Конфигурация и состояние:

~/.config/pm-netlink-client/config.yaml
~/.config/pm-netlink-client/gui-state.yaml
~/.config/pm-netlink-client/profiles/*.yaml

Конфигурация

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

mode: whitelist
profiles_dir: ~/.config/pm-netlink-client/profiles
xray:
  binary: /usr/bin/xray
  active_profile: null
  generated_config: /run/pm-netlink-client/xray.json
  tproxy_port: 12345
  loglevel: warning
  bypass_mark: 255
  exempt_uid: null
routing:
  fwmark: 1
  table: 100
  enable_ipv6: true
  bypass_private_networks: true
  bypass_loopback: true
dns:
  enabled: false
  listen: 127.0.0.1
  port: 5353
rules:
  proxy_domains: []
  proxy_domain_suffixes: []
  remote_proxy_domains: []
  remote_proxy_domain_suffixes: []
  proxy_apps: []
  direct_domains: []
  direct_ips:
    - 127.0.0.0/8
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16
    - ::1/128
    - fc00::/7
    - fe80::/10

Важные поля:

  • mode: whitelist - fallback DIRECT, proxy только по правилам.
  • mode: blacklist - fallback PROXY, локальные/private сети остаются DIRECT.
  • profiles_dir - абсолютный путь или путь относительно файла config.
  • xray.generated_config - куда писать generated Xray JSON.
  • xray.tproxy_port - порт transparent inbound.
  • xray.bypass_mark - SO_MARK для outbound sockets Xray, чтобы не поймать Xray повторно в nftables.
  • xray.exempt_uid - необязательный UID, чей traffic не надо перенаправлять.
  • routing.fwmark - mark для policy routing.
  • routing.table - route table для local route на loopback.
  • rules.direct_ips - сети, которые обходят proxy на уровне Xray и nftables.
  • rules.proxy_apps - экспериментальное поле MVP; сейчас сохраняется, но надежное process-aware применение еще не реализовано.
  • dns.enabled - зарезервировано для будущей DNS-aware маршрутизации.

Профили

Профили лежат в profiles_dir как YAML-файлы. Новые сохранения используют служебное имя файла вида profile-<hash>.yaml; отображаемое имя хранится внутри YAML, поэтому пробелы, скобки и / в имени не теряются. Старый плоский формат <name>.yaml продолжает читаться для совместимости.

version: 1
name: Demo/Profile (RU)
profile:
  type: vless-tls
  server: example.com
  uuid: 00000000-0000-4000-8000-000000000000

VLESS Reality:

type: vless-reality
server: example.com
port: 443
uuid: PUT_UUID_HERE
encryption: none
flow: null
network: tcp
header_type: none
sni: www.microsoft.com
public_key: PUT_PUBLIC_KEY_HERE
short_id: PUT_SHORT_ID_HERE
fingerprint: chrome
spider_x: /
label: Example VLESS Reality

VLESS TLS:

type: vless-tls
server: example.com
port: 443
uuid: PUT_UUID_HERE
encryption: none
security: tls
network: tcp
header_type: none
sni: example.com
flow: null
fingerprint: chrome
label: Example VLESS TLS

VLESS plain:

type: vless-plain
server: example.com
port: 80
uuid: PUT_UUID_HERE
encryption: none
security: none
network: tcp
header_type: none
flow: null
fingerprint: null
label: Example VLESS Plain

Shadowsocks:

type: shadowsocks
server: example.com
port: 8388
method: 2022-blake3-aes-128-gcm
password: PUT_PASSWORD_HERE
label: Example Shadowsocks

Импорт URI:

sudo pmnlc import-uri my-profile 'vless://...'
sudo pmnlc use-profile my-profile

Поддерживаются VLESS URI с security=none, security=tls и security=reality, а также transports type=tcp, type=raw, type=ws и type=grpc. Для Reality flow сохраняется только если он явно есть в URI: профили без flow не получают xtls-rprx-vision автоматически. Другие URI scheme и неподдержанные network types отклоняются.

Подписки

GUI умеет импортировать VLESS-подписки:

  • plain text со строками URI;
  • base64/base64url payload;
  • комментарии и пустые строки игнорируются;
  • имена профилей берутся из URI fragment, hostname или fallback profile-N;
  • дубли имен получают суффикс -2, -3 и далее;
  • неподдержанные строки попадают в warnings, а не ломают весь импорт.

URL подписки должен использовать http или https. При загрузке helper отклоняет localhost, loopback, private, link-local, multicast и другие неподходящие для remote-fetch адреса, чтобы passwordless GUI helper не был каналом доступа к локальным сервисам root.

Если сервер возвращает header subscription-userinfo, GUI сохраняет quota metadata:

upload=1073741824; download=2147483648; total=10737418240; expire=1893456000

total=0, total=-1, total=unlimited и похожие значения считаются безлимитом. expire=0, expire=-1, expire=lifetime и похожие значения считаются бессрочной подпиской.

Split Rules

Удаленный split-list - обычный text file:

# комментарии и пустые строки игнорируются
example.com
.telegram.org
domain:youtube.com
full:login.example.com
https://chat.openai.com/path

URL удаленного split-list проходит ту же проверку, что и URL подписки: разрешены только remote http/https endpoints, локальные/private адреса отклоняются.

Правила:

  • example.com, .example.com и domain:example.com добавляются как suffix domain и матчят домен с поддоменами;
  • full:login.example.com добавляется как exact domain;
  • URL нормализуется до hostname;
  • домены приводятся к lower-case;
  • remote rules сохраняются в remote_proxy_domains и remote_proxy_domain_suffixes;
  • пользовательские сайты GUI сохраняются отдельно в proxy_domains и proxy_domain_suffixes, поэтому обновление remote list их не стирает.

Команды CLI

Глобальные опции:

pmnlc --version
pmnlc --verbose ...
pmnlc --debug ...
pmnlc --log-level info ...

Лог-уровень также можно задать через:

PMNLC_LOG_LEVEL=debug pmnlc status

Команды:

install [--config PATH] [--enable] [--force] [--cli-path PATH] [--dry-run]
uninstall [--config PATH] [--purge] [--dry-run]
start [--config PATH] [--dry-run]
stop [--config PATH] [--dry-run]
restart [--config PATH] [--dry-run]
status [--config PATH]
shell [--config PATH]
autostart enable [--dry-run]
autostart disable [--dry-run]
autostart status
add-domain DOMAIN [--config PATH]
remove-domain DOMAIN [--config PATH]
list-domains [--config PATH]
add-app PROCESS_NAME [--config PATH]
remove-app PROCESS_NAME [--config PATH]
list-apps [--config PATH]
add-profile NAME PATH [--config PATH] [--overwrite]
use-profile NAME [--config PATH]
list-profiles [--config PATH]
import-uri NAME URI [--config PATH] [--overwrite]
subscription set URL [--config PATH] [--name NAME]
subscription update [URL] [--config PATH] [--name NAME]
subscription status [--config PATH]
subscription clear [--config PATH]
generate [--config PATH] [--dry-run]
test [--config PATH] [--profile NAME] [--probe-url URL] [--timeout SECONDS]
diagnose [--config PATH] [--target HOST] [--count N] [--timeout SECONDS] [--connectivity-url URL]
pm-netlink-client-gui

Root нужен для команд, которые меняют /etc, /run, nftables, policy routing или systemd: install, uninstall, start, stop, restart, autostart enable, autostart disable, generate если target path находится в /run, импорт/изменение профилей в /etc, а также действия GUI через privileged helper.

sudo pmnlc shell [--config PATH] открывает постоянную интерактивную оболочку с prompt (pmnlc). Внутри доступны публичные команды CLI, help, help COMMAND, apropos WORD, exit и quit; hidden/internal команды остаются недоступны.

Hidden/internal команды используются systemd и GUI:

internal apply-routing
internal remove-routing
internal apply-system
internal remove-system
internal gui-rpc
daemon
print-xray

Обычно их не нужно запускать вручную.

Lifecycle служб

Запуск:

sudo pmnlc start

Или один раз войти в интерактивный root-сеанс:

sudo pmnlc shell

Остановка:

sudo pmnlc stop

Включить автозапуск:

sudo pmnlc autostart enable
pmnlc autostart status

autostart enable включает systemd services при загрузке, но не запускает VPN немедленно. Для текущего сеанса выполните sudo pmnlc start.

Отключить автозапуск, не останавливая текущий VPN-сеанс:

sudo pmnlc autostart disable

start генерирует Xray/nft runtime files и запускает systemd services. stop останавливает services, удаляет table inet pmnlc и policy routing rules/routes для выбранной config.

Диагностика

Проверить конфиг приложения и профиля:

sudo pmnlc test

Проверить неактивный профиль или сам outbound выбранного профиля через временный локальный Xray HTTP proxy:

sudo pmnlc test --profile my-profile
sudo pmnlc test --profile my-profile --probe-url https://www.google.com/generate_204

--probe-url не меняет nftables, systemd services или активный профиль: команда запускает временный Xray process с HTTP inbound на 127.0.0.1 и проверяет URL через этот proxy.

Проверить TCP-доступность сервера активного профиля, ICMP ping, состояние VPN services, nftables/policy routing и внешний доступ:

pmnlc diagnose

Если ICMP ping не проходит, но profile server tcp и остальные обязательные проверки ok, сервер доступен: многие провайдеры блокируют ping.

Проверить сгенерированный Xray config:

sudo pmnlc generate
sudo xray run -test -config /run/pm-netlink-client/xray.json

Проверить nftables config:

sudo nft -c -f /run/pm-netlink-client/pmnlc.nft
sudo nft list table inet pmnlc

Проверить policy routing:

ip rule show
ip route show table 100
ip -6 rule show
ip -6 route show table 100

Логи:

journalctl -u pm-netlink-clientd -e
journalctl -u pm-netlink-client-routing -e

Если unit, установленный из checkout, пишет env: 'pmnlc': No such file or directory, переустановите unit files с абсолютным CLI path:

sudo pmnlc install --force --cli-path "$(command -v pmnlc)"
sudo systemctl daemon-reload
sudo systemctl restart pm-netlink-client-routing.service pm-netlink-clientd.service

Экстренный откат:

sudo pmnlc stop
sudo nft delete table inet pmnlc
sudo ip rule del fwmark 1 table 100
sudo ip route flush table 100
sudo ip -6 rule del fwmark 1 table 100
sudo ip -6 route flush table 100
sudo systemctl stop pm-netlink-clientd

Разработка

Установить dev-зависимости:

uv sync --extra dev

Тесты:

PYTHONPATH=. uv run pytest

pyproject.toml также содержит pythonpath = ["."], чтобы pytest не импортировал случайно установленный системный пакет вместо checkout.

Компиляционная проверка:

python -m compileall -q pm_netlink_client tests

Wheel:

python -m build --wheel --no-isolation

GUI smoke test пропускается автоматически, если PySide6 не установлен.

Локальная проверка PKGBUILD

Для проверки packaging без git tag используйте локальный PKGBUILD, который собирает текущий checkout вместе с незакоммиченными изменениями:

makepkg -p packaging/aur/PKGBUILD.local -Ccf

Если запускаете makepkg из packaging/aur, передайте путь к checkout явно:

cd packaging/aur
PMNLC_LOCAL_SRC=/home/dragonnp/programming/pm-netlink-client makepkg -p PKGBUILD.local -Ccf

Чтобы установить собранные пакеты локально:

sudo pacman -U pm-netlink-client-*.pkg.tar.zst pm-netlink-client-gui-*.pkg.tar.zst

Ограничения MVP

  • QUIC может скрывать полезные metadata.
  • ECH может скрывать TLS SNI.
  • DoH может обходить domain-aware routing.
  • CDN-сервисы могут делить IP-адреса с несвязанными доменами.
  • CLI add-app пока только сохраняет process names; надежное enforcement планируется через cgroup или nftables integration.
  • DNS-aware routing зарезервирован для будущих версий.
  • GUI ориентирован на desktop-сессии с polkit/pkexec.
Описание
Релизы
2026-05-22
последний
Конвейеры
0 успешных
0 с ошибкой
Разработчики