Documentation
¶
Overview ¶
Package config читает profiles.toml — файл с настройками подключения.
Двадцать пять флагов ядра заменяются профилем: набором именованных настроек, между которыми переключаются одним словом в командной строке (решение Р6 в doc/architecture.md, формат — там же, §10).
Секретов в файле нет. Пароль подключения — это одновременно ключ маскировки трафика (HKDF из него выводит WRAP-ключ, см. internal/core/wrap.go), поэтому он и хеши звонков лежат в хранилище секретов, а в профиле остаются только ссылки вида "dpapi:…". Разворачивает их задача C2; здесь ссылка — обычная строка, и её содержимое пакету неизвестно.
sharelink.go — профиль из ссылки qwdtt://config?… (plan-e5.md, I8).
Такую ссылку (и QR-код с ней) выдают Android-клиент qWDTT («Экспорт профиля») и бот сервера. В ней всё, что нужно для подключения:
qwdtt://config?name=<имя>&peer=<host:port>&hashes=<хеши>&workers=<N>&port=<порт>&pass=<пароль>
Значения закодированы URLEncoder'ом Java: UTF-8, пробел — «+». Разбор повторяет parseQwdttUri апстрима: принимается и qwdtt:config?… без «//», ссылка в base64, порт сервера по умолчанию дописывается, неизвестные параметры пропускаются.
В ссылке пароль и хеши — она сама секрет. Принимать её аргументом командной строки нельзя по той же причине, что и пароль (см. internal/cli/profile.go).
Index ¶
- Constants
- func CheckPeer(peer string) error
- func CheckProfileName(name string) error
- func EffectiveDatapath(value string) string
- func NormalizeDatapath(value string) string
- func Path() (string, error)
- func PeerWithPort(peer string) string
- func ProfileName(raw string) string
- func Save(path string, f *File) error
- type File
- type Profile
- type SecretStore
- type ShareLink
- type SocksSection
Constants ¶
const ( AppDir = "qwdtt-cli" FileName = "profiles.toml" )
Имя каталога и файла настроек. Каталог общий для всей программы: рядом с profiles.toml задача C2 положит хранилище секретов.
const ( // DatapathSocks — userspace WireGuard и локальный SOCKS5. Прав администратора // не требует; по умолчанию — только на macOS, где других режимов нет // (DefaultDatapath). DatapathSocks = "socks" // DatapathExternal — конфиг отдаётся штатному WireGuard, тот поднимает // туннель сам. Нужны права администратора и установленный WireGuard. DatapathExternal = "external" // DatapathTun — свой адаптер tt, весь трафик системы (Э2). На Windows // адаптер даёт драйвер Wintun: нужны права администратора и wintun.dll // рядом с tt. DatapathTun = "tun" // DatapathWintun — прежнее имя tun (до Э4); принимается как синоним, // чтобы старые профили не ломались. См. NormalizeDatapath. DatapathWintun = "wintun" )
Способы отдать трафик наружу. Значения — те же слова, что в architecture.md §07.
const DefaultDatapath = DatapathTun
DefaultDatapath — режим профиля без поля datapath: весь трафик системы через свой адаптер (решение пользователя 2026-10-14: клиенту нужен туннель для всего, а не прокси, который ещё надо настраивать в программах). Права администратора (root) tt при подключении из меню просит сам; консольный tt connect без них отказывает с подсказкой про -datapath socks.
const DefaultPeerPort = "56000"
DefaultPeerPort — порт сервера qWDTT, если в адресе его не назвали. Тот же, что дописывает Android-клиент (PeerAddress.ensurePort).
const SecretsFileName = "secrets.json"
SecretsFileName — имя файла хранилища секретов. Лежит в каталоге настроек, рядом с profiles.toml.
Variables ¶
This section is empty.
Functions ¶
func CheckProfileName ¶
CheckProfileName проверяет имя профиля по тем же правилам, что и файл. Нужна тем, кто спрашивает имя у человека до записи (меню, plan-e5.md).
func EffectiveDatapath ¶
EffectiveDatapath — каким режимом профиль подключится: значение поля после NormalizeDatapath, а пустое — DefaultDatapath. Умолчание читается только здесь, чтобы выбор реализации, проверка прав и показ профиля не разошлись.
func NormalizeDatapath ¶
NormalizeDatapath приводит значение datapath к каноническому: без пробелов по краям, синоним wintun — к tun. Остальное не трогает: неизвестные значения отвергает Validate.
func Path ¶
Path возвращает путь к profiles.toml по правилам платформы: %APPDATA%\qwdtt-cli\profiles.toml на Windows и $XDG_CONFIG_HOME/qwdtt-cli/profiles.toml (по умолчанию ~/.config) на прочих. Под sudo — ~/.config/qwdtt-cli того, кто вызвал sudo, а не root.
os.UserConfigDir здесь не годится: на macOS он даёт ~/Library/Application Support, а клиент консольный, и его настройки логичнее держать там же, где у остальных консольных программ.
func PeerWithPort ¶
PeerWithPort дописывает к адресу без порта DefaultPeerPort. Адрес с портом, IPv6 в скобках с портом и всё, где двоеточие уже есть, остаются как были: угадывать, где в голом IPv6 кончается адрес, нельзя, а проверка потом скажет, что не так.
func ProfileName ¶
ProfileName превращает имя из ссылки в имя профиля tt: латиница, цифры, дефис и подчёркивание (см. profileNameProblem). Кириллица транслитерируется, пробелы и прочие разделители становятся дефисом, всё остальное выбрасывается. Пусто — из имени ничего не осталось.
func Save ¶
Save записывает файл настроек целиком.
Комментарии и порядок ключей исходного файла не сохраняются: разбор отдаёт структуру, а не дерево с комментариями, и класть их обратно неоткуда. Переписать файл целиком — честнее, чем править его построчно регулярками и однажды испортить чужую разметку.
Перед записью настройки проверяются: писать файл, который сами же откажемся читать, нельзя. Запись идёт во временный файл с последующим Rename, чтобы обрыв не оставил профили наполовину записанными.
Types ¶
type File ¶
type File struct {
// DefaultProfile — какой профиль брать, если в командной строке имя
// не названо. Может быть пустым: когда профиль в файле один, выбирать
// не из чего.
DefaultProfile string `toml:"default_profile"`
// Profiles — секции [profile.<имя>].
Profiles map[string]*Profile `toml:"profile"`
}
File — разобранный profiles.toml целиком.
func Load ¶
Load читает и разбирает файл настроек.
Отсутствие файла — ошибка, обёрнутая вокруг os.ErrNotExist: вызывающий отличает «настроек ещё нет» от «настройки испорчены» через errors.Is, а не разбором текста.
func Parse ¶
Parse разбирает содержимое файла настроек и проверяет его.
source попадает в сообщения об ошибках — это путь к файлу или любое другое название источника, по которому пользователь поймёт, что чинить.
func (*File) Names ¶
Names возвращает имена профилей по алфавиту — для выбора и для сообщений об ошибках, где порядок обязан быть устойчивым.
func (*File) Profile ¶
Profile возвращает профиль по имени. Пустое имя означает «профиль по умолчанию»: сначала default_profile, а если его нет — единственный профиль в файле.
Сообщения об ошибках перечисляют, что в файле есть: чаще всего промах с именем — это опечатка, и список избавляет от похода в файл.
type Profile ¶
type Profile struct {
// Name — имя секции. Заполняется при разборе, в файле его нет.
Name string `toml:"-"`
// Peer — адрес VPS в виде host:port.
Peer string `toml:"peer"`
// PasswordRef — ссылка на пароль подключения в хранилище секретов.
PasswordRef string `toml:"password_ref"`
// VkHashesRef — ссылка на список хешей звонков VK.
VkHashesRef string `toml:"vk_hashes_ref"`
// Workers — число воркеров. Кратно девяти, потолок 108; округляет
// и обрезает ядро (core.Config.Normalize), здесь значение не трогают.
Workers int `toml:"workers"`
// Obfs — режим RTP-маскировки: audio или video.
Obfs string `toml:"obfs"`
// TurnTransport — udp или tcp. TCP помогает в сетях, где душат UDP.
TurnTransport string `toml:"turn_transport"`
// DTLS — поднимать ли DTLS поверх TURN. Указатель, потому что «не задано»
// и «задано false» здесь разные вещи: пропущенное поле означает обычный
// режим с DTLS, а false — прямой RTP-obfs AEAD, который требует сервера,
// запущенного с -listen-direct.
DTLS *bool `toml:"dtls"`
// DNS — резолвер для доменов VK.
DNS string `toml:"dns"`
// Datapath — как отдавать трафик: socks, tun или external.
Datapath string `toml:"datapath"`
// KillSwitch — блокировать ли трафик системы, пока туннель не поднят
// (tun и external, plan-e3.md). Указатель: пропущенное поле значит
// «включён», выключает только явное false. Читать — KillSwitchOn.
KillSwitch *bool `toml:"kill_switch"`
// Socks — секция [profile.<имя>.socks].
Socks SocksSection `toml:"socks"`
}
Profile — одна секция [profile.<имя>].
Пустое поле означает «не задано»: значение по умолчанию подставляется в Core, и берётся оно из core.DefaultConfig, чтобы умолчания не разъезжались с ядром. Исключение — datapath, см. Core.
func (*Profile) Core ¶
Core переводит профиль в настройки ядра.
Секреты не подставляются: Password и Hashes остаются пустыми, их заполняет вызывающий, развернув PasswordRef и VkHashesRef через хранилище (задача C2). Поэтому core.Config.Validate после Core ещё не проходит — и не должен.
Умолчания берутся из core.DefaultConfig целиком.
Полей datapath здесь нет: ядру всё равно, кто заберёт у него конфиг. Выбор реализации (Profile.Datapath, умолчание — EffectiveDatapath) и её настройки (Profile.Socks) читает CLI и передаёт конструктору из internal/datapath, минуя ядро (задача D2).
func (*Profile) KillSwitchOn ¶
KillSwitchOn — включён ли kill-switch: да, если в профиле не сказано обратного.
type SecretStore ¶
type SecretStore interface {
// Put кладёт значение под именем name и возвращает ссылку на него.
// Повторный вызов с тем же name перезаписывает прежнее значение и
// возвращает ту же ссылку — чтобы `tt profile set` не плодил осиротевшие
// записи при каждом запуске.
Put(name, value string) (ref string, err error)
// Get разворачивает ссылку обратно в значение.
Get(ref string) (value string, err error)
// Delete убирает значение из хранилища. Ссылка, которой в хранилище нет,
// ошибкой не считается: `tt profile rm` должен доводить удаление до конца,
// даже если запись пропала раньше.
Delete(ref string) error
}
SecretStore держит секреты — пароль подключения и хеши звонков VK — вне profiles.toml. В профиль попадает только ссылка вида <хранилище>:<идентификатор>, которую возвращает Put и разворачивает Get.
Что означает «хранилище», решает платформа. На Windows это DPAPI: значение шифруется CryptProtectData и привязано к учётной записи, схема ссылки — dpapi. На прочих системах шифрования нет, защита — права 0600 на самом файле хранилища, схема — file. Ссылка одной платформы на другой не разворачивается: Get отвергает чужую схему, а не отдаёт мусор (см. secret_windows.go, secret_unix.go).
func NewSecretStore ¶
func NewSecretStore() (SecretStore, error)
NewSecretStore открывает хранилище в каталоге настроек платформы — там же, где Path ищет profiles.toml.
func NewSecretStoreFor ¶
func NewSecretStoreFor(configPath string) SecretStore
NewSecretStoreFor открывает хранилище рядом с указанным файлом настроек.
Нужен команде с флагом -config: секреты обязаны лежать в том же каталоге, что и профили, которые на них ссылаются, иначе перенесённый каталог настроек окажется наполовину рабочим.
type ShareLink ¶
type ShareLink struct {
// пробелы. Имя для нашего файла — ProfileName.
Name string
Peer string
Password string
// строки, бывают ссылки на звонки целиком. Пусто — в ссылке хешей нет
// (у Android это «общие хеши» приложения, у нас их нет).
Hashes string
// умолчания ядра; иначе 0.
Workers int
}
ShareLink — то, что прочитано из ссылки.
func ParseShareLink ¶
ParseShareLink разбирает ссылку qwdtt://config. Сообщения об ошибках не повторяют саму ссылку: в ней пароль.
type SocksSection ¶
type SocksSection struct {
Listen string `toml:"listen"`
}
SocksSection — настройки локального SOCKS5. Учитываются при datapath = "socks".
Логина и пароля здесь нет намеренно: SOCKS5 слушает петлю, а пароль к нему был бы вторым секретом в файле. Появятся вместе с задачей, которой они понадобятся.