Skip to content

Repository files navigation

vpnctl — менеджер AmneziaWG/WireGuard для Ubuntu

Компактный локальный инструмент для управления VPN-туннелем AmneziaWG / WireGuard на рабочих станциях Ubuntu. Позволяет загрузить пользовательский .conf-файл, включить/выключить VPN одной кнопкой и следить за статусом подключения.

Инструмент работает локально: нет облака, нет сервера, нет регистрации. Все данные (включая приватный ключ) остаются на машине, под контролем root.

Go Version License Go Report Card Tests


Демонстрация

$ ./vpnctl import WARPv2_54.conf
импортировано: WARPv2_54.conf (interface=warpv2_54, Endpoint=1.2.3.4:51820)
подключение: vpnctl up warpv2_54

$ ./vpnctl up
поднимаю awg0 (сервер 1.2.3.4:51820)...
VPN поднят.

$ ./vpnctl status
интерфейс: awg0  (включен)
внешний IP: 203.0.113.7

Те же действия доступны в графическом окне: выбрать .conf, нажать «Загрузить», затем «ВКЛЮЧИТЬ VPN» — и следить за статусом в реальном времени.


Начало работы

Требования

  • Ubuntu (проверялось на Ubuntu с GNOME), Linux x86_64.
  • Go 1.26+ для сборки из исходников.
  • Утилиты AmneziaWG: awg, awg-quick (это инструменты AmneziaWG, а не стандартного WireGuard).
  • pkexec (входит в policykit-1) — графический запрос пароля там, где в sudoers нет правила NOPASSWD.
  • Для графического интерфейса — GTK3 и dev-заголовки при сборке.

Установка системных библиотек (для сборки GUI)

sudo apt install build-essential pkg-config libgtk-3-dev

Dev-заголовки нужны только на этапе компиляции. Уже собранный бинарник требует только библиотеку рантайма libgtk-3.

Сборка из исходников

cd vpnctl
go build -mod=vendor -o vpnctl .

Результат — один исполняемый файл vpnctl (~12 МБ).

Сборка использует каталог vendor/ (зависимость gotk3 уже зафиксирована и, при необходимости, пропатчена — см. раздел «Известные проблемы»).

Установка готового бинарника

Собранный пакет (например, dist/vpnctl_0.1.0_linux_amd64.tar.gz):

tar -xzf vpnctl_0.1.0_linux_amd64.tar.gz
sudo install -m 0755 vpnctl /usr/local/bin/vpnctl

Либо собрать и установить из исходников:

cd vpnctl
go install -mod=vendor .

Как пользоваться

Графический интерфейс

./vpnctl tray

Откроется окно «AmneziaWG VPN» в тёмной теме:

  • Шапка — иконка, название и бейдж с именем текущего интерфейса.
  • Карточка статуса — цветной индикатор и слово ПОДКЛЮЧЕНО / ОТКЛЮЧЕНО, внешний IP и строка сервер <host:port>.
  • Конфигурация — кнопка выбора .conf и кнопка «Загрузить».
  • Основная кнопка — «ВКЛЮЧИТЬ VPN» / «ВЫКЛЮЧИТЬ VPN» на всю ширину.
  • Строка лога — последнее событие или ошибка.

Порядок работы:

  1. Нажмите «Выбрать конфиг…» и укажите ваш файл *.conf.
  2. Нажмите «Загрузить». Откроется графический запрос пароля (pkexec) — введите пароль. Конфиг будет скопирован в системный каталог AmneziaWG. Если такой конфиг уже установлен, импорт ничего не делает и сообщает об этом.
  3. Нажмите «ВКЛЮЧИТЬ VPN». Произойдёт подъём туннеля; окно дождётся, что интернет через туннель действительно появился.
  4. Статус и внешний IP обновляются автоматически. Кнопка меняется на «ВЫКЛЮЧИТЬ VPN», когда туннель активен.

Окно запоминает, какой интерфейс был импортирован последним, и дальше кнопка управляет именно им — жёсткой привязки к awg0 нет.

Имена tray, ui и gui равнозначны и запускают одно и то же окно.

Командная строка

Команда Описание
./vpnctl status [iface] Показать статус туннеля и внешний IP
./vpnctl up [iface] Поднять VPN с проверкой и автооткатом
./vpnctl down [iface] Опустить VPN
./vpnctl import <файл.conf> Импортировать конфиг в систему
./vpnctl tray (ui/gui — синонимы) Открыть графический интерфейс

iface по умолчанию — awg0.

Примеры:

# импорт конфига с проверкой
./vpnctl import ~/Загрузки/WARPv2_54.conf

# поднять / опустить VPN
./vpnctl up
./vpnctl down

# посмотреть состояние
./vpnctl status

Возможности

  • Форма загрузки конфигурации в собственном графическом окне (выбор файла через системный диалог, фильтр *.conf).
  • Импорт конфига в системный каталог AmneziaWG с правильными правами (владелец root, режим 0600) — одним привилегированным вызовом, то есть одним запросом пароля.
  • Кнопка включения / выключения VPN: подъём с проверкой, что интернет через туннель действительно появился, и автооткатом, если не появился.
  • Мониторинг статуса — обновляется автоматически каждые 3 секунды: состояние интерфейса, внешний IP, сервер (Endpoint).
  • Тёмная терминальная тема окна, не зависящая от оформления рабочего стола.
  • Ярлык на рабочем столе и в меню приложений со своей иконкой.
  • CLI с теми же командами для автоматизации и скриптов.

Ярлык на рабочем столе

В репозитории ярлыка нет — он ставится в домашний каталог пользователя тремя файлами и не требует прав root.

1. Иконка

Иконка лежит в репозитории, в assets/icons/hicolor/:

assets/icons/hicolor/
├── scalable/apps/vpnctl.svg
├── 48x48/apps/vpnctl.png
├── 64x64/apps/vpnctl.png
├── 128x128/apps/vpnctl.png
├── 256x256/apps/vpnctl.png
└── 512x512/apps/vpnctl.png

SVG — исходник, PNG-размеры отрендерены из него: часть шеллов не умеет тянуть SVG напрямую. Чтобы иконка стала видна системе, скопируйте набор в домашний каталог тем:

cp -r assets/icons/hicolor/* ~/.local/share/icons/hicolor/
gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor

Каталог намеренно без index.theme — он дополняет системный hicolor; поиск по имени vpnctl работает и без него (проверено Gtk.IconTheme.lookup_icon). Если какой-то шелл иконку всё же не видит, скопируйте туда /usr/share/icons/hicolor/index.theme.

2. Ярлык в меню приложений

~/.local/share/applications/vpnctl.desktop:

[Desktop Entry]
Version=1.0
Type=Application
Name=AmneziaWG VPN
Comment=Управление туннелем AmneziaWG (awg0)
Exec=/home/robert/go-projects/vpnctl/vpnctl tray
Icon=vpnctl
Terminal=false
Categories=Network;Security;System;
Keywords=vpn;wireguard;amneziawg;awg;vpnctl;
StartupNotify=true
StartupWMClass=vpnctl

StartupWMClass=vpnctl должен совпадать с WM_CLASS окна — иначе GNOME не свяжет окно с иконкой и нарисует дефолтную. Проверить:

xprop -id "$(xdotool search --name 'AmneziaWG VPN' | head -1)" WM_CLASS
# → WM_CLASS(STRING) = "vpnctl", "Vpnctl"

3. Ярлык на рабочем столе

cp ~/.local/share/applications/vpnctl.desktop ~/Рабочий\ стол/"AmneziaWG VPN.desktop"
chmod 755 ~/Рабочий\ стол/"AmneziaWG VPN.desktop"
gio set ~/Рабочий\ стол/"AmneziaWG VPN.desktop" metadata::trusted true

metadata::trusted true обязателен: без него GNOME помечает ярлык как «Ненадёжный» и не запускает его по клику.

4. Обновить кэши

gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor
update-desktop-database ~/.local/share/applications

Проверка

desktop-file-validate ~/.local/share/applications/vpnctl.desktop
gtk-launch vpnctl

Exec указывает на бинарник в каталоге проекта. Если перенести репозиторий или установить vpnctl в /usr/local/bin, поправьте путь в обоих .desktop-файлах — иначе ярлык сломается. Пересборка бинаря на месте ярлык не ломает.

Если папки на рабочем столе перестали открываться

Ярлык vpnctl заявляется только для application/x-desktop и обработчиком каталогов не является. Но в ~/.config/mimeapps.list может оказаться чужая запись вида:

[Added Associations]
inode/directory=dev.warp.Warp.desktop;org.gnome.Nautilus.desktop;

Терминал Warp принимает только URI и падает на голом пути:

error: invalid value '/home/robert' for '[URLS]...': relative URL without a base

Если папки не открываются — уберите Warp из этой строки, оставив:

inode/directory=org.gnome.Nautilus.desktop;

Проверить, кто обработчик сейчас:

gio mime inode/directory          # список кандидатов
xdg-mime query default inode/directory

Что делает «импорт»

vpnctl import (и кнопка «Загрузить» в окне) выполняет:

  1. Проверку файла — читается, не пуст, размер ≤ 1 МБ, содержит секции [Interface] и [Peer].
  2. Определение имени интерфейса из имени файла: WARPv2_54.conf → интерфейс warpv2_54. Имя приводится к нижнему регистру и к безопасному формату (только буквы, цифры, _, -; недопустимые символы заменяются на _), иначе берётся awg0. Разрешённые символы сохраняются как есть — то есть _ в имени файла остаётся и в имени интерфейса.
  3. Один привилегированный вызов sh -c со скриптом:
    • если <каталог>/<имя>.conf уже существует — печатает EXISTS и выходит с кодом 2;
    • mkdir -p целевого каталога;
    • install -o root -g root -m 600 <источник> .<имя>.tmp — копия с владельцем root и режимом 0600;
    • mv .<имя>.tmp <имя>.conf — атомарная замена;
    • при любой ошибке — rm -f .<имя>.tmp.

Всё это одна команда, поэтому один импорт = максимум один запрос пароля, и неудачный импорт не оставляет временных файлов.

Эскалация: сначала пробуется беспарольный sudo -n (если в sudoers есть правило на нужные команды), и только если он недоступен — pkexec с графическим запросом.

Повторный импорт

Если конфиг с таким именем уже установлен, conf.Import возвращает ErrExists, и приложение сообщает: «конфиг уже установлен (…) — повторно загружать не нужно». Рабочий конфиг не перезаписывается. CLI дополнительно печатает подсказку подключение: vpnctl up <iface>.


Что команда «up» делает безопасно

  1. Если интерфейс уже поднят — сразу возвращает успех, туннель не пересоздаётся.
  2. Запускает awg-quick up <iface>.
  3. Ждёт появления интернета через туннель (до 20 секунд). Проверка идёт по очереди по нескольким хостам, и каждый запрос открывает новое соединение.
  4. Если интернет не появился — автоматически откатывается (awg-quick down) и возвращает ошибку с указанием сервера и последней ошибки проверки.

Отдельной предварительной проверки сервера по TCP нет: AmneziaWG/WireGuard работают по UDP, поэтому TCP-коннект к Endpoint ничего не говорит о работоспособности туннеля. Единственный честный критерий — «появился ли интернет после подъёма».


Оформление

Тема окна задаётся целиком в internal/ui/style.go (CSS через CssProvider + AddProviderForScreen, плюс gtk-application-prefer-dark-theme). Окно не наследует оформление рабочего стола: фон #14161a, карточка #1b1f26, акцент состояния зелёный #46d17a / серый #6b7480, основная кнопка синяя #2f6feb, адреса — моноширинным шрифтом.

CSS-классы вешаются через StyleContext.AddClass (хелпер cls), а не через Widget.SetName: SetName задаёт CSS-имя (#name) и с селекторами .class молча не совпадает — виджеты остаются в дефолтной теме без единой ошибки.


Архитектура

Проект построен по принципам KISS, YAGNI, DRY: минимальный набор пакетов, по одной реализации каждого действия, которую используют и CLI, и GUI.

vpnctl/
├── go.mod                          # модуль, одна внешняя зависимость (gotk3)
├── main.go                         # точка входа, диспетчер подкоманд CLI
├── internal/
│   ├── priv/   priv.go             # ЕДИНСТВЕННОЕ место повышения привилегий
│   ├── conf/   conf.go             # валидация и импорт .conf
│   ├── vpn/    vpn.go              # up/down/status + внешний IP
│   └── ui/     style.go            # тёмная тема (CSS) и хелперы классов
│               ui.go               # окно: конструктор и раскладка
│               actions.go          # обработчики «Загрузить» и «ВКЛ/ВЫКЛ»
│               render.go           # мониторинг и отрисовка статуса
└── vendor/                         # зафиксированные зависимости (gotk3)

Поток данных

пользователь → форма/CLI → conf.Validate → conf.Import → priv.RunWG → sh -c
                                                          │  install 0600 + mv
                                                          └─► /etc/amnezia/amneziawg/*.conf

пользователь → кнопка/CLI → vpn.Up/Down ──► priv.RunWG ──► awg-quick up/down <iface>

мониторинг → vpn.Status.StatusAt → /sys/class/net/<iface>   (без прав)
                                 + внешний IP (HTTPS)        (без прав)
           → conf.InstalledEndpoint → awg-quick strip        (sudo -n, без окна)

Ключевые решения

Решение Обоснование
Единая точка повышения привилегий — internal/priv (Run / RunWG / TryWG) DRY; RunWG сначала пробует беспарольный sudo -n и только потом pkexec, а TryWG вообще не показывает окно
Импорт — один sh -c на весь сценарий Один запрос пароля на импорт; неудача не оставляет временных файлов
Проверка статуса почти без прав Опрос каждые 3 с не должен требовать пароль
Приватный ключ — только в системном конфиге Не дублируем секреты в хранилище приложения
Атомарный импорт (installmv) Никогда не оставляем наполовину записанного конфига
Отказ импорта при существующем конфиге Не затираем рабочее подключение
Up не трогает уже поднятый туннель Не «дёргаем» подключение, включённое ранее
Проверка связи — новым соединением на каждый хост Пуловый keep-alive сокет со старого маршрута после подъёма туннеля мёртв и съедает весь таймаут
refresh() без ввода-вывода, первый опрос — в monitor() Иначе главный цикл блокируется до gtk.Main(), и при недоступной сети окно не появляется

Структура пакетов

  • internal/priv — единственное место повышения привилегий.

    • Run(name, args...)pkexec (графический запрос пароля).
    • RunWG(name, args...) — сначала беспарольный sudo -n, при неудаче pkexec.
    • TryWG(name, args...) — только беспарольный sudo -n, никогда не показывает окно.

    Ошибки оборачиваются в *Error с режимом, командой, аргументами и выводом.

  • internal/confValidate(path) (проверка файла, извлечение имени интерфейса и Endpoint), InterfaceName(path), Import(path) (один привилегированный sh -c: guard → mkdir -pinstall 0600mv → очистка; при существующем конфиге возвращает ErrExists), InstalledEndpoint(iface) (Endpoint установленного конфига через awg-quick strip, без запроса пароля). Путь каталога переопределяется переменной VPNCTL_CONF_DIR.

  • internal/vpnStatus, StatusAt() (состояние интерфейса + внешний IP), RemoteIP() / probe() (внешний IP; список хостов, VPNCTL_IP_URL первым, каждый запрос — новым соединением), Up() / Down() (подъём с ожиданием интернета и автооткатом). Отдельной функции проверки Endpoint по TCP нет.

  • internal/ui — GTK3-окно: тёмная тема (style.go), раскладка (buildContent), форма загрузки (FileChooserButton + кнопка «Загрузить»), ToggleButton «ВКЛЮЧИТЬ/ВЫКЛЮЧИТЬ VPN», карточка статуса. Мониторинг — тикер каждые 3 с, обновления перекидываются в главный цикл через glib.IdleAdd.

GUI и таймер: важный нюанс

renderStatus синхронизирует состояние ToggleButton вызовом SetActive, что генерирует сигнал toggled. Чтобы опрос статуса не включал/выключал VPN сам по себе, обработчик onToggle пропускает программные изменения через флаг syncing и пропускает операции, пока активен флаг busy.

Endpoint для интерфейса читается один раз и кэшируется (endpointResolved / setEndpoint): это привилегированный вызов, а опрос идёт каждые 3 секунды.


Безопасность

  • Приватный ключ хранится в файле с правами 0600, владелец — root. Сторонние приложения и обычный пользователь его не прочитают.
  • В системный каталог пишется только через привилегированный вызов; имя интерфейса проверяется регулярным выражением (защита от инъекции пути), путь экранируется (shquote).
  • Проверка файла (размер, секции, Endpoint) выполняется до копирования.
  • Импорт не перезаписывает существующий конфиг — рабочее подключение защищено от случайного затирания.
  • «ВКЛ» не перезапускает уже поднятый туннель — активное подключение не сбрасывается.
  • Приложение работает от обычного пользователя и повышает привилегии только в момент команд up / down / import.

Тестирование

Единицы логики покрыты table-driven тестами с детектором гонок и рандомизированным фаззингом парсера конфигов:

# юнит-тесты (не требуют прав root, sudo или pkexec)
go test -mod=vendor ./...

# с детектором гонок
go test -mod=vendor -race ./...

# рандомизированные проверки парсера (по 10 секунд на цель)
go test -mod=vendor ./internal/conf/ -run '^$' -fuzz=FuzzInterfaceName -fuzztime=10s
go test -mod=vendor ./internal/conf/ -run '^$' -fuzz=FuzzValidate -fuzztime=10s

# отчёт о покрытии
go test -mod=vendor ./internal/conf/ ./internal/priv/ ./internal/vpn/ -cover

# линтер (v1.64.x; настройка — в .golangci.yml)
golangci-lint run ./...

Привилегированные действия (up, down, import) в тестах подменяются фейковым исполнителем команд (priv.Command), поэтому тесты безопасно гонять без sudo, pkexec и installed AmneziaWG. GUI (internal/ui) юнит-тестами не покрывается — для него нужен дисплей.

Ручная проверка (как раньше):

go build -mod=vendor -o vpnctl .
./vpnctl status

Для повторяемых ручных проверок можно переопределить каталог конфигов переменной VPNCTL_CONF_DIR, а адрес проверки внешнего IP — переменной VPNCTL_IP_URL.


Известные проблемы и ограничения

  1. Трей-иконка в GNOME/Wayland не отображается. По умолчанию GNOME и Wayland-сессии не показывают legacy-трей (Gtk.StatusIcon). Используется обычное окно. Для трея нужен отдельный компонент на libayatana-appindicator (стандарт AppIndicator), который в этой версии не включён (YAGNI).
  2. gtk.StatusIcon — deprecated и ломает сборку. Включение трея через -tags gtk_deprecated втягивает неиспользуемый C-колбэк (Seat.Grab), который не компилируется на свежих GTK (undefined: callback). В vendor/ этот колбэк закомментирован, поэтому используется только окно.
  3. Snap-окружение может ронять запуск GUI из-за несовпадения версий libc/libpthread (ошибка undefined symbol: __libc_pthread_init). Запускайте vpnctl tray в обычной сессии рабочего стола, а не в snap-sandbox.
  4. Handshake и счётчики трафика в окне не показываются намеренно. Для них нужен awg show, а он требует root и давал бы запрос пароля на каждом опросе (раз в 3 с). В окне есть состояние интерфейса и внешний IP — их можно читать без прав.
  5. Ярлык и чужие ассоциации каталогов. Если папки на рабочем столе перестали открываться, проверьте ~/.config/mimeapps.list — см. раздел «Ярлык на рабочем столе», подраздел про [Added Associations].

Планы (YAGNI — реализуется только по запросу)

  • Системный трей через libayatana-appindicator (AppIndicator) для GNOME/Wayland.
  • Несколько сохранённых конфигов с переключением в окне.
  • Автостарт при входе в систему.

Вклад

Проект открыт для вклада — правила и процесс описаны в CONTRIBUTING.md: требования, сборка, архитектурные ограничения (привилегии — только через internal/priv) и стиль коммитов. История изменений ведётся в CHANGELOG.md.

Участники

Автор и основной мейнтейнер — turkprogrammer. Спасибо всем, кто открывает issues и отправляет pull requests.


Лицензия

Распространяется под лицензией MIT. Зависимости — под своими лицензиями (gotk3 — MIT).

About

vpnctl — менеджер AmneziaWG/WireGuard для Ubuntu

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages