datapath

package
v1.0.0 Latest Latest
Warning

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

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

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

View Source
const DefaultProgramTimeout = 30 * time.Second

DefaultProgramTimeout — сколько ждать wireguard.exe на установку и снятие службы. Штатно это секунды; дольше значит, что программа встала (например, показала окно с ошибкой), и ждать её дальше незачем.

View Source
const DefaultSocksListen = "127.0.0.1:1080"

DefaultSocksListen — адрес SOCKS5 по умолчанию, тот же, что был у флага ядра.

View Source
const DefaultTunnelName = app.Name

DefaultTunnelName — имя туннеля у штатного WireGuard и файла конфига (qwdtt-cli.conf → служба WireGuardTunnel$qwdtt-cli, адаптер «qwdtt-cli»).

View Source
const WintunDLL = "wintun.dll"

WintunDLL — библиотека драйвера Wintun. Ищется только рядом с бинарём и в System32 (LoadLibraryEx в golang.zx2c4.com/wintun), поэтому в Requires она стоит как файл рядом с qwdtt-cli, а не как программа из PATH.

Variables

This section is empty.

Functions

This section is empty.

Types

type DNSGuard

type DNSGuard interface {
	// Enable пускает DNS только к резолверам dns (и запросы самого qwdtt-cli).
	Enable(dns []netip.Addr) error
	// Disable снимает запрет.
	Disable() error
}

DNSGuard — защита от утечки DNS, общая на весь qwdtt-cli 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

func (e *External) Close() error

Close снимает службу туннеля и удаляет файл конфига. Безопасен до Start и при повторном вызове: супервизор зовёт его из defer.

Служба обязана быть снята, иначе после выхода qwdtt-cli в системе останется туннель в никуда; поэтому снятие идёт со своим сроком (см. uninstall).

Снятие делается один раз, а все вызовы ждут его конца и возвращают его ошибку. Иначе была гонка (найдена живым прогоном N4 Э2): по отмене ctx первым в Close входит сторож из Start, defer супервизора получал nil сразу, процесс выходил, а горутина сторожа гибла между снятием службы и удалением файла — служба снята, а файл с приватным ключом остался. Ошибку Close пишет в лог сам: супервизор её отбрасывает, а человеку нужна подсказка, что снимать вручную.

func (*External) Endpoint

func (e *External) Endpoint() netip.AddrPort

Endpoint — адрес из конфига, на который штатный WireGuard шлёт UDP; на нём слушает ядро.

func (*External) Requires

func (e *External) Requires() Requirements

Requires: права администратора (службу ставит только он), wireguard.exe, netguard — туннель забирает весь трафик системы — и защита DNS. Programs называет найденный путь, а если найти не вышло — голое имя, чтобы вызывающий сказал «не найдена программа». Вне Windows — Unsupported: служб WireGuard для Windows там нет.

func (*External) Start

func (e *External) Start(ctx context.Context, conf *WGConfig) error

Start записывает конфиг, ставит службу туннеля, ждёт, пока она заработает, и включает защиту DNS. При любом отказе служба снимается, а файл удаляется: следов не остаётся.

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: реализация, которая живёт весь qwdtt-cli 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) BatchSize

func (p *Pipe) BatchSize() int

BatchSize — conn.Bind.

func (*Pipe) Close

func (p *Pipe) Close() error

Close — conn.Bind: функции приёма, выданные Open, возвращают net.ErrClosed. Повторный вызов безвреден.

func (*Pipe) CoreConn

func (p *Pipe) CoreConn() net.PacketConn

CoreConn отдаёт новую сторону ядра для core.Config.PacketConn. Прежняя закрывается: две попытки разом ядро не держит, а пакет wireguard-go должен уйти той, что жива.

func (*Pipe) Dropped

func (p *Pipe) Dropped() (up, down uint64)

Dropped — сколько пакетов сброшено из-за полной очереди: up — от wireguard-go к ядру, down — обратно.

func (*Pipe) Open

func (p *Pipe) Open(port uint16) ([]conn.ReceiveFunc, uint16, error)

Open — conn.Bind. Порт ни к чему не привязывается и возвращается как есть.

func (*Pipe) ParseEndpoint

func (p *Pipe) ParseEndpoint(s string) (conn.Endpoint, error)

ParseEndpoint — conn.Bind. Endpoint конфига — номинальный loopback ядра (сервер возвращает порт, который ядро прислало в GETCONF); он нужен только для IpcGet и строк лога.

func (*Pipe) Send

func (p *Pipe) Send(bufs [][]byte, _ conn.Endpoint) error

Send — conn.Bind: пакеты уходят текущей стороне ядра, адрес ep не нужен — пир один. Ядра нет — пакеты молча пропадают, как UDP в закрытый порт.

func (*Pipe) SetMark

func (p *Pipe) SetMark(uint32) error

SetMark — conn.Bind. Сокета нет, метить нечего.

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

func (s *Socks) Close() error

Close останавливает SOCKS5 и WireGuard. Безопасен до Start и при повторном вызове: супервизор зовёт его из defer. Одновременный вызов ждёт конца первого (контракт Datapath.Close).

func (*Socks) CoreConn

func (s *Socks) CoreConn() net.PacketConn

CoreConn — Piped: сторона ядра канала в памяти.

func (*Socks) Endpoint

func (s *Socks) Endpoint() netip.AddrPort

Endpoint — адрес из конфига; номинальный: пакеты ядру идут каналом в памяти.

func (*Socks) ListenAddr

func (s *Socks) ListenAddr() net.Addr

ListenAddr — фактический адрес SOCKS5-сервера. Отличается от заказанного, если в настройках стоял нулевой порт; до Start — nil.

func (*Socks) Requires

func (s *Socks) Requires() Requirements

Requires: ничего. В этом весь смысл режима — он работает от обычного пользователя и ничего не требует установленным.

func (*Socks) Start

func (s *Socks) Start(ctx context.Context, conf *WGConfig) error

Start поднимает WireGuard в userspace и SOCKS5 над ним.

Возвращается, когда сервер уже слушает: занятый порт обязан выясниться здесь, а не в фоне через секунду после того, как CLI сказал «готово». Обслуживание продолжается в фоне до Close или отмены ctx.

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: адаптер живёт весь qwdtt-cli 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

func (w *Tun) AdapterID() uint64

AdapterID — идентификатор адаптера в системе (на Windows — LUID, на Linux — индекс интерфейса); 0 до Start и после Close. Нужен тем, кто исключает свой адаптер из выбора физического интерфейса (netguard, смена сети).

func (*Tun) Close

func (w *Tun) Close() error

Close останавливает wireguard-go и убирает адаптер. Безопасен до Start и при повторном вызове; одновременный вызов ждёт конца первого (контракт Datapath.Close).

func (*Tun) CoreConn

func (w *Tun) CoreConn() net.PacketConn

CoreConn — Piped: сторона ядра канала в памяти на эту попытку.

func (*Tun) Endpoint

func (w *Tun) Endpoint() netip.AddrPort

Endpoint — адрес из конфига; номинальный: пакеты ядру идут каналом в памяти.

func (*Tun) Requires

func (w *Tun) Requires() Requirements

Requires: права администратора (root на Linux), файлы платформы (на Windows — wintun.dll рядом с бинарём), netguard — туннель забирает весь трафик системы — и защита DNS. Где своего адаптера нет — Unsupported.

func (*Tun) Reserve

func (w *Tun) Reserve() (release func(), err error)

Reserve занимает адаптер на машине до подключения (Reserver): второй qwdtt-cli с datapath tun узнаёт о первом сразу, а не после похода в VK за кредами, и не уходит в переподключения (найдено живым прогоном W2). Держать резерв — весь сеанс; первый Start берёт ту же блокировку ещё раз, внутри процесса это не конфликт.

func (*Tun) Start

func (w *Tun) Start(_ context.Context, conf *WGConfig) error

Start поднимает туннель по конфигу попытки и возвращается, когда через него идёт весь трафик системы: стоят маршруты fullTunnelRoutes и включена защита DNS.

Первый вызов создаёт адаптер (адреса, MTU, DNS) и wireguard-go над ним. Следующие — перенастройка (Persistent): устройство получает ключ, пира и endpoint новой попытки, адаптер — новые адреса и DNS, если сервер их сменил. Отказ перенастройки адаптер не снимает: это отказ попытки, а следующая попробует снова.

ctx Start не хранит: адаптер переживает попытку и снимается только Close.

func (*Tun) Suspend

func (w *Tun) Suspend() error

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

func ParseWgQuick(conf string) (*WGConfig, error)

ParseWgQuick разбирает конфиг в формате wg-quick.

Разбор намеренно снисходителен ровно там, где снисходителен сам wg-quick: незнакомые ключи и секции пропускаются, строка без «=» пропускается, комментарии (# и ;) игнорируются. Строгость — только к тому, без чего туннель не поднимется: ключи, адрес и endpoint обязаны быть на месте, ключ обязан быть 32 байтами base64.

func (*WGConfig) EndpointAddrPort

func (c *WGConfig) EndpointAddrPort() (netip.AddrPort, bool)

EndpointAddrPort разбирает Endpoint в адрес с портом — то, что реализация возвращает из Datapath.Endpoint.

Второе значение — false, если в конфиге не адрес, а имя хоста: сервер возвращает 127.0.0.1:<порт>, но формат wg-quick имя допускает, и молча подставлять нули вместо адреса нельзя.

func (*WGConfig) IPCRequest

func (c *WGConfig) IPCRequest() string

IPCRequest собирает конфиг в текстовый протокол wireguard-go (device.IpcSet).

Jump to

Keyboard shortcuts

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