config

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: GPL-3.0 Imports: 19 Imported by: 0

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

View Source
const (
	AppDir   = "qwdtt-cli"
	FileName = "profiles.toml"
)

Имя каталога и файла настроек. Каталог общий для всей программы: рядом с profiles.toml задача C2 положит хранилище секретов.

View Source
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.

View Source
const DefaultDatapath = DatapathTun

DefaultDatapath — режим профиля без поля datapath: весь трафик системы через свой адаптер (решение пользователя 2026-10-14: клиенту нужен туннель для всего, а не прокси, который ещё надо настраивать в программах). Права администратора (root) tt при подключении из меню просит сам; консольный tt connect без них отказывает с подсказкой про -datapath socks.

View Source
const DefaultPeerPort = "56000"

DefaultPeerPort — порт сервера qWDTT, если в адресе его не назвали. Тот же, что дописывает Android-клиент (PeerAddress.ensurePort).

View Source
const SecretsFileName = "secrets.json"

SecretsFileName — имя файла хранилища секретов. Лежит в каталоге настроек, рядом с profiles.toml.

Variables

This section is empty.

Functions

func CheckPeer

func CheckPeer(peer string) error

CheckPeer проверяет адрес сервера так же, как поле peer профиля.

func CheckProfileName

func CheckProfileName(name string) error

CheckProfileName проверяет имя профиля по тем же правилам, что и файл. Нужна тем, кто спрашивает имя у человека до записи (меню, plan-e5.md).

func EffectiveDatapath

func EffectiveDatapath(value string) string

EffectiveDatapath — каким режимом профиль подключится: значение поля после NormalizeDatapath, а пустое — DefaultDatapath. Умолчание читается только здесь, чтобы выбор реализации, проверка прав и показ профиля не разошлись.

func NormalizeDatapath

func NormalizeDatapath(value string) string

NormalizeDatapath приводит значение datapath к каноническому: без пробелов по краям, синоним wintun — к tun. Остальное не трогает: неизвестные значения отвергает Validate.

func Path

func Path() (string, error)

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

func PeerWithPort(peer string) string

PeerWithPort дописывает к адресу без порта DefaultPeerPort. Адрес с портом, IPv6 в скобках с портом и всё, где двоеточие уже есть, остаются как были: угадывать, где в голом IPv6 кончается адрес, нельзя, а проверка потом скажет, что не так.

func ProfileName

func ProfileName(raw string) string

ProfileName превращает имя из ссылки в имя профиля tt: латиница, цифры, дефис и подчёркивание (см. profileNameProblem). Кириллица транслитерируется, пробелы и прочие разделители становятся дефисом, всё остальное выбрасывается. Пусто — из имени ничего не осталось.

func Save

func Save(path string, f *File) error

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

func Load(path string) (*File, error)

Load читает и разбирает файл настроек.

Отсутствие файла — ошибка, обёрнутая вокруг os.ErrNotExist: вызывающий отличает «настроек ещё нет» от «настройки испорчены» через errors.Is, а не разбором текста.

func Parse

func Parse(data []byte, source string) (*File, error)

Parse разбирает содержимое файла настроек и проверяет его.

source попадает в сообщения об ошибках — это путь к файлу или любое другое название источника, по которому пользователь поймёт, что чинить.

func (*File) Names

func (f *File) Names() []string

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

func (*File) Profile

func (f *File) Profile(name string) (*Profile, error)

Profile возвращает профиль по имени. Пустое имя означает «профиль по умолчанию»: сначала default_profile, а если его нет — единственный профиль в файле.

Сообщения об ошибках перечисляют, что в файле есть: чаще всего промах с именем — это опечатка, и список избавляет от похода в файл.

func (*File) Validate

func (f *File) Validate() error

Validate проверяет файл целиком. Вызывается при разборе; отдельно нужен тем, кто собрал File в памяти — например, `tt profile set` перед записью.

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

func (p *Profile) Core() core.Config

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

func (p *Profile) KillSwitchOn() bool

KillSwitchOn — включён ли kill-switch: да, если в профиле не сказано обратного.

func (*Profile) Validate

func (p *Profile) Validate() error

Validate проверяет один профиль — тот же набор правил, что и у файла. Нужен `tt connect`: флаги командной строки перекрывают поля профиля, и собранное из двух источников значение обязано пройти ту же проверку, что и записанное в файл.

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 struct {
	// Name — имя профиля, как его назвали в приложении: бывают кириллица и
	// пробелы. Имя для нашего файла — ProfileName.
	Name string
	// Peer — адрес сервера, с портом.
	Peer string
	// Password — пароль подключения. Пусто — в ссылке его не было.
	Password string
	// Hashes — хеши звонков VK строкой, как в ссылке: через запятую, перевод
	// строки, бывают ссылки на звонки целиком. Пусто — в ссылке хешей нет
	// (у Android это «общие хеши» приложения, у нас их нет).
	Hashes string
	// Workers — число воркеров, если оно в ссылке есть и отличается от
	// умолчания ядра; иначе 0.
	Workers int
}

ShareLink — то, что прочитано из ссылки.

func ParseShareLink(text string) (*ShareLink, error)

ParseShareLink разбирает ссылку qwdtt://config. Сообщения об ошибках не повторяют саму ссылку: в ней пароль.

func (*ShareLink) HashCount

func (l *ShareLink) HashCount() int

HashCount — сколько хешей разобрано из ссылки.

type SocksSection

type SocksSection struct {
	Listen string `toml:"listen"`
}

SocksSection — настройки локального SOCKS5. Учитываются при datapath = "socks".

Логина и пароля здесь нет намеренно: SOCKS5 слушает петлю, а пароль к нему был бы вторым секретом в файле. Появятся вместе с задачей, которой они понадобятся.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL