Documentation
¶
Overview ¶
Package datapath отвечает за то, каким способом трафик приложений попадает в туннель.
Ядро всегда делает одно и то же: принимает пакеты WireGuard и уносит их через TURN. Различается только то, что их подаёт, — userspace WireGuard с локальным SOCKS5, штатный wireguard.exe или (с Э2) собственный адаптер tun. Это различие и отделено интерфейсом Datapath (architecture.md §04, решение Р2). Свой wireguard-go (socks, tun) подаёт их каналом в памяти (Pipe, Piped), wireguard.exe — UDP на loopback ядра.
Index ¶
- Constants
- type DNSGuard
- type Datapath
- type External
- type ExternalOptions
- type IPv6Blocker
- type Persistent
- type Pipe
- func (p *Pipe) BatchSize() int
- func (p *Pipe) Close() error
- func (p *Pipe) CoreConn() net.PacketConn
- func (p *Pipe) Dropped() (up, down uint64)
- func (p *Pipe) Open(port uint16) ([]conn.ReceiveFunc, uint16, error)
- func (p *Pipe) ParseEndpoint(s string) (conn.Endpoint, error)
- func (p *Pipe) Send(bufs [][]byte, _ conn.Endpoint) error
- func (p *Pipe) SetMark(uint32) error
- type Piped
- type Requirements
- type Reserver
- type Socks
- type SocksOptions
- type Tun
- func (w *Tun) AdapterID() uint64
- func (w *Tun) Close() error
- func (w *Tun) CoreConn() net.PacketConn
- func (w *Tun) Endpoint() netip.AddrPort
- func (w *Tun) Requires() Requirements
- func (w *Tun) Reserve() (release func(), err error)
- func (w *Tun) Start(_ context.Context, conf *WGConfig) error
- func (w *Tun) Suspend() error
- type TunOptions
- type WGConfig
Constants ¶
const DefaultProgramTimeout = 30 * time.Second
DefaultProgramTimeout — сколько ждать wireguard.exe на установку и снятие службы. Штатно это секунды; дольше значит, что программа встала (например, показала окно с ошибкой), и ждать её дальше незачем.
const DefaultSocksListen = "127.0.0.1:1080"
DefaultSocksListen — адрес SOCKS5 по умолчанию, тот же, что был у флага ядра.
const DefaultTunnelName = "tt"
DefaultTunnelName — имя туннеля у штатного WireGuard и файла конфига (tt.conf → служба WireGuardTunnel$tt, адаптер «tt»).
const WintunDLL = "wintun.dll"
WintunDLL — библиотека драйвера Wintun. Ищется только рядом с бинарём и в System32 (LoadLibraryEx в golang.zx2c4.com/wintun), поэтому в Requires она стоит как файл рядом с tt, а не как программа из PATH.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DNSGuard ¶
type DNSGuard interface {
// Enable пускает DNS только к резолверам dns (и запросы самого tt).
Enable(dns []netip.Addr) error
// Disable снимает запрет.
Disable() error
}
DNSGuard — защита от утечки DNS, общая на весь tt connect (на Windows — winsys.DNSGuard, на Linux — linuxsys.DNSGuard). Datapath включает её, когда туннель поднят, и выключает в Close; оба вызова безвредны повторно.
type Datapath ¶
type Datapath interface {
// Start поднимает datapath по конфигу WireGuard, который сервер вернул
// в GETCONF. Возвращается, когда трафик можно подавать, и не ждёт
// завершения: остановка — это Close или отмена ctx.
//
// ctx живёт не дольше туннеля: его отмена равносильна Close. Исключение —
// Persistent: там ctx ограничивает только сам запуск.
Start(ctx context.Context, conf *WGConfig) error
// Endpoint — адрес из поля Endpoint конфига: порт loopback выбирает ядро,
// а сервер возвращает его в конфиге обратно. Слушает на нём ядро только
// без канала в памяти (external); у Piped он номинальный.
//
// Осмысленно только после успешного Start; до него — нулевое значение.
Endpoint() netip.AddrPort
// Requires — что нужно для запуска: права, файлы рядом с бинарём, внешние
// программы. Спрашивается до Start, чтобы объяснить нехватку одной строкой,
// а не падением на середине подключения (architecture.md §09).
Requires() Requirements
// Close останавливает datapath и убирает за собой всё, что он создал:
// адаптер, службу, временный файл конфига с приватным ключом.
//
// Close обязан быть безопасным до Start и при повторном вызове: супервизор
// зовёт его из defer, а Э1 требует снятия datapath даже при панике в ядре
// (задача F3). Одновременные вызовы — норма (сторож отмены ctx внутри
// реализации и defer супервизора): каждый вызов возвращается только
// после того, как уборка закончена, иначе процесс выйдет посреди неё.
Close() error
}
Datapath — способ доставки трафика в туннель. Реализации живут в соседних файлах пакета: socks.go, external.go и (с Э2) tun.go.
Порядок работы один для всех: вызывающий строит нужную реализацию, спрашивает Requires — хватает ли прав и на месте ли зависимости, — затем по событию core.EventConfig зовёт Start и в конце обязательно Close. Настройки самой реализации (адрес SOCKS5, учётные данные, пути) передаются её конструктору, а не через интерфейс: у трёх реализаций они не пересекаются.
type External ¶
type External struct {
// contains filtered or unexported fields
}
External — datapath поверх штатного WireGuard для Windows: конфиг от сервера записывается в файл, wireguard.exe ставит по нему службу туннеля, при остановке служба снимается.
Файл с приватным ключом живёт столько же, сколько служба: служба хранит путь к нему и читает его при каждом запуске, копии у себя не делает. Удаляется файл в Close, после снятия службы, и при любом отказе Start. (Первая версия D3 удаляла файл сразу после установки — служба падала с «файл не найден», а туннель числился работающим; найдено живой проверкой H2.)
func NewExternal ¶
func NewExternal(opts ExternalOptions) *External
NewExternal готовит datapath. Программа ищется сразу, сеть и служба не трогаются до Start.
func (*External) Close ¶
Close снимает службу туннеля и удаляет файл конфига. Безопасен до Start и при повторном вызове: супервизор зовёт его из defer.
Служба обязана быть снята, иначе после выхода tt в системе останется туннель в никуда; поэтому снятие идёт со своим сроком (см. uninstall).
Снятие делается один раз, а все вызовы ждут его конца и возвращают его ошибку. Иначе была гонка (найдена живым прогоном N4 Э2): по отмене ctx первым в Close входит сторож из Start, defer супервизора получал nil сразу, процесс выходил, а горутина сторожа гибла между снятием службы и удалением файла — служба снята, а файл с приватным ключом остался. Ошибку Close пишет в лог сам: супервизор её отбрасывает, а человеку нужна подсказка, что снимать вручную.
func (*External) Endpoint ¶
Endpoint — адрес из конфига, на который штатный WireGuard шлёт UDP; на нём слушает ядро.
func (*External) Requires ¶
func (e *External) Requires() Requirements
Requires: права администратора (службу ставит только он), wireguard.exe, netguard — туннель забирает весь трафик системы — и защита DNS. Programs называет найденный путь, а если найти не вышло — голое имя, чтобы вызывающий сказал «не найдена программа». Вне Windows — Unsupported: служб WireGuard для Windows там нет.
type ExternalOptions ¶
type ExternalOptions struct {
// Program — путь к wireguard.exe. Пусто — искать самим: реестр, каталог
// Program Files, затем PATH (см. locateWireGuard).
Program string
// Name — имя туннеля. Пусто — DefaultTunnelName.
Name string
// Dir — каталог для файла конфига. Пусто — каталог настроек пользователя
// (qwdtt-cli в os.UserConfigDir). Рядом с бинарём файл не кладётся ни
// при каких настройках: в нём приватный ключ.
Dir string
// Timeout — сколько ждать wireguard.exe. Ноль — DefaultProgramTimeout.
Timeout time.Duration
// Logf — куда писать; nil означает молчать. Datapath сам в stdout не
// пишет — вывод принадлежит CLI.
Logf func(format string, args ...any)
// DNSGuard — защита от утечки DNS: включается, когда служба туннеля
// работает, выключается в Close. Нужна и здесь: с половинами /1 вместо
// /0 (splitDefaultRoutes) WireGuard свой запрет DNS не включает
// (plan-e2.md, решение 5). nil — без неё.
DNSGuard DNSGuard
}
ExternalOptions — настройки datapath external.
type IPv6Blocker ¶
type IPv6Blocker interface {
// BlockIPv6 задаёт, закрывать ли IPv6 при следующем Enable.
BlockIPv6(block bool)
}
IPv6Blocker — необязательная часть DNSGuard (Linux): защита, которая умеет заодно закрыть IPv6 системы, когда туннель его не везёт.
type Persistent ¶
type Persistent interface {
// Suspend снимает то, из-за чего трафик системы идёт в туннель, —
// маршруты и запрет утечки DNS, — и оставляет остальное. Вернёт их
// следующий успешный Start.
//
// Супервизор зовёт его сразу после неудачной попытки, до задержки: пока
// маршруты ведут в мёртвый туннель, страница капчи VK не загрузится, а
// капчу ядро просит как раз при переподключении. Безвреден до Start
// и повторно.
Suspend() error
}
Persistent — необязательная часть Datapath: реализация, которая живёт весь tt connect, а не одну попытку (plan-e2.md, решение 3). У tun это адаптер: пересоздавать его на каждое переподключение значило бы, что он исчезает и появляется заново, а Windows заводит «Сеть 2», «Сеть 3».
Супервизор строит такую реализацию один раз и закрывает при выходе, а Start зовёт на каждую попытку. Повторный Start — перенастройка новым конфигом сервера, а не отказ «уже запущен». ctx у Start ограничивает только сам запуск: попытка кончается, datapath остаётся.
type Pipe ¶
type Pipe struct {
// contains filtered or unexported fields
}
Pipe — канал в памяти между wireguard-go и ядром вместо UDP на loopback (решение Р4, plan-e3.md, блок B).
Сам Pipe — conn.Bind для wireguard-go; сторону ядра отдаёт CoreConn. Через loopback порт ядра был открыт любому процессу машины, а диспетчер ядра отвечает последнему отправителю: один посторонний датаграм уводил ответы сервера. В памяти постороннему писать некуда.
Время жизни сторон разное. Bind живёт вместе с устройством wireguard-go — у tun это весь сеанс, через все попытки; Close и Open он переживает (BindUpdate в reconfigureDevice). Сторона ядра — одна попытка: core.Run закрывает её на выходе, следующая попытка берёт новую.
Семантика — как у UDP: очередь переполнена или ядра нет — пакет сбрасывается, отправитель не ждёт. Сброс от переполнения считается (Dropped).
func NewPipe ¶
func NewPipe() *Pipe
NewPipe возвращает Pipe без стороны ядра и с закрытым Bind: открывает его wireguard-go (device.Up).
func (*Pipe) Close ¶
Close — conn.Bind: функции приёма, выданные Open, возвращают net.ErrClosed. Повторный вызов безвреден.
func (*Pipe) CoreConn ¶
func (p *Pipe) CoreConn() net.PacketConn
CoreConn отдаёт новую сторону ядра для core.Config.PacketConn. Прежняя закрывается: две попытки разом ядро не держит, а пакет wireguard-go должен уйти той, что жива.
func (*Pipe) Dropped ¶
Dropped — сколько пакетов сброшено из-за полной очереди: up — от wireguard-go к ядру, down — обратно.
func (*Pipe) ParseEndpoint ¶
ParseEndpoint — conn.Bind. Endpoint конфига — номинальный loopback ядра (сервер возвращает порт, который ядро прислало в GETCONF); он нужен только для IpcGet и строк лога.
type Piped ¶
type Piped interface {
CoreConn() net.PacketConn
}
Piped — необязательная часть Datapath: реализация, у которой свой wireguard-go и пакеты ядру идут каналом в памяти (Pipe), а не UDP на loopback: порт ядра не открыт посторонним процессам (plan-e3.md, блок B).
Супервизор зовёт CoreConn перед каждым запуском ядра и отдаёт результат в core.Config.PacketConn; прежняя сторона ядра при этом закрывается. Ядро стартует раньше Start — канал готов с постройки datapath.
type Requirements ¶
type Requirements struct {
// Unsupported — почему реализация не работает на этой системе; nil —
// работает. Если задано, остальные поля вызывающий не проверяет: права
// и файлы для того, что здесь всё равно не запустится, советовать незачем
// (найдено первым прогоном на Linux).
Unsupported error
// Admin — нужны права администратора (root). Проверяет их вызывающий:
// сам datapath о токене процесса ничего не знает.
Admin bool
// Files — файлы, которые должны лежать рядом с бинарём, например
// wintun.dll. Имена, а не пути: искать их — дело вызывающего.
Files []string
// Programs — внешние программы, которые должны быть установлены,
// например wireguard.exe.
Programs []string
// Netguard — datapath забирает весь трафик системы, и собственные сокеты
// ядра нужно увести мимо туннеля, иначе туннель зависит от самого себя
// (architecture.md §05). Включает netguard вызывающий: он даёт ядру
// core.Config.DialControl.
Netguard bool
// DNSGuard — datapath ставит правила защиты от утечки DNS и ждёт их
// сессию в своих опциях (поле DNSGuard). Открывает её вызывающий, до
// подключения: отказ WFP — понятная ошибка сразу, а не цикл
// переподключений (plan-e2.md, W4).
DNSGuard bool
// Warning — ограничение, о котором нужно предупредить пользователя при
// запуске, даже когда всё остальное на месте. Пустая строка — сказать
// нечего. CLI печатает его строкой «Предупреждение: …» до подключения.
//
// Заведено ради external в Э1 (до netguard он работал только со
// сплит-конфигом); в Э2 это ограничение снято, и сейчас поле никто
// не заполняет.
Warning string
}
Requirements — условия, на которых реализация может работать. Пустое значение означает «ничего не нужно»: так выглядит socks, работающий от обычного пользователя и без внешних зависимостей.
type Reserver ¶
type Reserver interface {
Reserve() (release func(), err error)
}
Reserver — необязательная часть Datapath: реализация, которой до подключения нужно занять что-то общее на машину (у tun — адаптер). CLI зовёт Reserve после проверки Requires, до первой попытки, и держит резерв весь сеанс. Отказ здесь — понятная ошибка до похода в сеть; без резерва конфликт выяснился бы только в Start, после VK и TURN, и супервизор принял бы его за сбой попытки.
type Socks ¶
type Socks struct {
// contains filtered or unexported fields
}
Socks — datapath без прав администратора: userspace WireGuard поверх gVisor netstack и локальный SOCKS5-сервер над ним.
Охват у него уже, чем у системного туннеля: только TCP CONNECT и только те приложения, которые умеют ходить через прокси. IPv6 отклоняется намеренно — маршрутов на v6 в туннеле нет, и попытка клиента сходить по AAAA должна заканчиваться отказом сразу, а не таймаутом (architecture.md §04).
func NewSocks ¶
func NewSocks(opts SocksOptions) *Socks
NewSocks готовит datapath. Сеть не трогается до Start.
func (*Socks) Close ¶
Close останавливает SOCKS5 и WireGuard. Безопасен до Start и при повторном вызове: супервизор зовёт его из defer. Одновременный вызов ждёт конца первого (контракт Datapath.Close).
func (*Socks) CoreConn ¶
func (s *Socks) CoreConn() net.PacketConn
CoreConn — Piped: сторона ядра канала в памяти.
func (*Socks) Endpoint ¶
Endpoint — адрес из конфига; номинальный: пакеты ядру идут каналом в памяти.
func (*Socks) ListenAddr ¶
ListenAddr — фактический адрес SOCKS5-сервера. Отличается от заказанного, если в настройках стоял нулевой порт; до Start — nil.
func (*Socks) Requires ¶
func (s *Socks) Requires() Requirements
Requires: ничего. В этом весь смысл режима — он работает от обычного пользователя и ничего не требует установленным.
type SocksOptions ¶
type SocksOptions struct {
// Listen — адрес SOCKS5-сервера. Пусто — DefaultSocksListen.
Listen string
// Auth включает проверку логина и пароля (метод 0x02).
Auth bool
Username string
Password string
// Logf — куда писать; nil означает молчать. Через него же проходит вывод
// wireguard-go и библиотеки SOCKS5: datapath, как и ядро, сам в stdout
// ничего не пишет — вывод принадлежит CLI.
Logf func(format string, args ...any)
}
SocksOptions — настройки datapath socks.
type Tun ¶
type Tun struct {
// contains filtered or unexported fields
}
Tun — datapath с собственным адаптером: wireguard-go в процессе поверх TUN системы (на Windows — от драйвера Wintun). От socks отличается только TUN (architecture.md §04), общая часть — startDevice.
Tun — Persistent: адаптер живёт весь tt connect. Первый Start создаёт его, следующие перенастраивают устройство конфигом новой попытки, Suspend между попытками снимает маршруты и запрет утечки DNS (plan-e2.md, решение 3).
Адаптер живёт, пока открыт его дескриптор: Close убирает его штатно, а при падении процесса систему чистит сам драйвер (plan-e2.md, врезка N1, п. 3). Адреса и маршруты висят на адаптере и уходят вместе с ним.
func NewTun ¶
func NewTun(opts TunOptions) *Tun
NewTun готовит datapath. Ни драйвер, ни сеть не трогаются до Start: CLI строит datapath и ради одного Requires.
func (*Tun) AdapterID ¶
AdapterID — идентификатор адаптера в системе (на Windows — LUID, на Linux — индекс интерфейса); 0 до Start и после Close. Нужен тем, кто исключает свой адаптер из выбора физического интерфейса (netguard, смена сети).
func (*Tun) Close ¶
Close останавливает wireguard-go и убирает адаптер. Безопасен до Start и при повторном вызове; одновременный вызов ждёт конца первого (контракт Datapath.Close).
func (*Tun) CoreConn ¶
func (w *Tun) CoreConn() net.PacketConn
CoreConn — Piped: сторона ядра канала в памяти на эту попытку.
func (*Tun) Requires ¶
func (w *Tun) Requires() Requirements
Requires: права администратора (root на Linux), файлы платформы (на Windows — wintun.dll рядом с бинарём), netguard — туннель забирает весь трафик системы — и защита DNS. Где своего адаптера нет — Unsupported.
func (*Tun) Reserve ¶
Reserve занимает адаптер на машине до подключения (Reserver): второй tt с datapath tun узнаёт о первом сразу, а не после похода в VK за кредами, и не уходит в переподключения (найдено живым прогоном W2). Держать резерв — весь сеанс; первый Start берёт ту же блокировку ещё раз, внутри процесса это не конфликт.
func (*Tun) Start ¶
Start поднимает туннель по конфигу попытки и возвращается, когда через него идёт весь трафик системы: стоят маршруты fullTunnelRoutes и включена защита DNS.
Первый вызов создаёт адаптер (адреса, MTU, DNS) и wireguard-go над ним. Следующие — перенастройка (Persistent): устройство получает ключ, пира и endpoint новой попытки, адаптер — новые адреса и DNS, если сервер их сменил. Отказ перенастройки адаптер не снимает: это отказ попытки, а следующая попробует снова.
ctx Start не хранит: адаптер переживает попытку и снимается только Close.
func (*Tun) Suspend ¶
Suspend снимает защиту DNS и маршруты, оставляя адаптер (Persistent): трафик системы идёт мимо туннеля, пока следующий Start их не вернёт. Заодно у устройства убирается пир — иначе wireguard-go слал бы рукопожатия старым ключом, пока ядра между попытками нет.
Защита — раньше маршрутов: с kill-switch её Disable закрывает трафик системы (plan-e3.md, K3), и мига, когда маршрутов уже нет, а закрыть ещё не успели, быть не должно. Без kill-switch порядок безвреден: DNS без запрета на этот миг уходит в туннель.
type TunOptions ¶
type TunOptions struct {
// Name — имя адаптера, как его покажет Get-NetAdapter. Пусто —
// DefaultTunnelName.
Name string
// Logf — куда писать; nil означает молчать. Сюда же уходит вывод
// wireguard-go и самого wintun.dll: datapath сам в stdout не пишет.
Logf func(format string, args ...any)
// DNSGuard — защита от утечки DNS: включается, когда маршруты стоят,
// выключается в Suspend и Close. nil — без неё (CLI с -allow-leak, тесты).
DNSGuard DNSGuard
}
TunOptions — настройки datapath tun.
type WGConfig ¶
type WGConfig struct {
// Raw — текст конфига, как он пришёл. Нужен external: штатный WireGuard
// читает файл сам, и пересобирать конфиг из полей значило бы рисковать
// потерей того, что разбор не понял.
Raw string
// PrivateKeyHex и PeerPublicHex — ключи в том виде, в каком их принимает
// протокол IPC wireguard-go: hex, а не base64 из файла конфига.
PrivateKeyHex string
PeerPublicHex string
// Addresses — адреса интерфейса, уже без длины префикса: netstack.CreateNetTUN
// принимает именно адреса.
Addresses []netip.Addr
// DNS — резолверы туннеля. Если сервер их не прислал, остаётся 1.1.1.1.
DNS []netip.Addr
// MTU — по умолчанию 1280: минимум, гарантированный IPv6, и запас под все
// заголовки туннеля.
MTU int
// AllowedIPs — префиксы строками, как в конфиге: дальше они уходят в IPC
// без разбора. Пустой список читается как 0.0.0.0/0.
AllowedIPs []string
// Endpoint — куда слать UDP. Строка, а не netip.AddrPort: wireguard-go
// принимает и имя хоста и резолвит его сам. Разбор адреса — EndpointAddrPort.
Endpoint string
// Keepalive — PersistentKeepalive в секундах, по умолчанию 25.
Keepalive uint16
}
WGConfig — разобранный конфиг WireGuard в формате wg-quick, тот самый, что сервер возвращает на GETCONF и что ядро отдаёт наружу событием core.EventConfig.
Разбор нужен обеим реализациям Э1, поэтому живёт здесь, а не в одной из них: socks собирает из полей запрос к wireguard-go (IPCRequest), external пишет в файл исходный текст (Raw), но обе обязаны понять, что прислал сервер, до того, как поднимать туннель.
func ParseWgQuick ¶
ParseWgQuick разбирает конфиг в формате wg-quick.
Разбор намеренно снисходителен ровно там, где снисходителен сам wg-quick: незнакомые ключи и секции пропускаются, строка без «=» пропускается, комментарии (# и ;) игнорируются. Строгость — только к тому, без чего туннель не поднимется: ключи, адрес и endpoint обязаны быть на месте, ключ обязан быть 32 байтами base64.
func (*WGConfig) EndpointAddrPort ¶
EndpointAddrPort разбирает Endpoint в адрес с портом — то, что реализация возвращает из Datapath.Endpoint.
Второе значение — false, если в конфиге не адрес, а имя хоста: сервер возвращает 127.0.0.1:<порт>, но формат wg-quick имя допускает, и молча подставлять нули вместо адреса нельзя.
func (*WGConfig) IPCRequest ¶
IPCRequest собирает конфиг в текстовый протокол wireguard-go (device.IpcSet).