Documentation
¶
Overview ¶
lifecycle.go — цикл подключения (задача F3): то единственное место в супервизоре, где есть время, горутины и внешний мир.
Автомат (state.go) помнит, где мы; политика (restart.go) считает задержки и годность хешей; здесь они связываются с ядром и datapath. Цикл запускает попытку, раздаёт события ядра всем троим, поднимает datapath по конфигу от сервера, опрашивает watchdog своим тикером и ждёт назначенную задержку перед следующей попыткой.
Главное обязательство файла — datapath снимается всегда: при обычном возврате из ядра, при остановке по Ctrl+C и при панике внутри ядра (architecture.md §07).
restart.go — политика перезапуска (задача F2): задержки между попытками, watchdog по трафику и учёт годных хешей звонка.
Как и автомат в state.go, всё здесь бесчасовое: текущее время приходит аргументом, а ждать задержку и опрашивать watchdog — дело цикла подключения (lifecycle.go, F3). Иначе тесты спали бы по минуте и зависели от планировщика.
Package supervisor сводит ядро, datapath и решатель капчи в один туннель: держит состояние подключения, решает, когда перезапускать ядро, и гарантирует, что datapath снимется при остановке (architecture.md §07).
В Android этим занимался TunnelManager.kt — 2162 строки с watchdog, разбором лога и парой десятков состояний UI. Здесь остаётся конечный автомат (этот файл, задача F1), политика перезапуска (restart.go, F2) и завершение (lifecycle.go, F3).
state.go — сам автомат. Он ничего не запускает и не ждёт: принимает события ядра и решения супервизора, а отдаёт состояние и переходы. Таймеров внутри нет намеренно — автомат ведётся событиями, а время живёт в restart.go.
Index ¶
- Constants
- Variables
- func Backoff(attempt int) time.Duration
- func Interrupt(parent context.Context) (context.Context, context.CancelFunc)
- func ShortHash(hash string) string
- type Decision
- type HashPool
- type Machine
- func (m *Machine) BeginAttempt() (Transition, bool)
- func (m *Machine) Degrade() (Transition, bool)
- func (m *Machine) FailedStage() core.Stage
- func (m *Machine) LastError() error
- func (m *Machine) Observe(ev core.Event) (Transition, bool)
- func (m *Machine) Reconnect(cause error) (Transition, bool)
- func (m *Machine) Recover() (Transition, bool)
- func (m *Machine) StageStatus(stage core.Stage) core.StageStatus
- func (m *Machine) State() State
- func (m *Machine) Stop() (Transition, bool)
- type Options
- type Restarter
- type StallReason
- type State
- type Transition
- type Tunnel
- type Watchdog
Constants ¶
const DefaultStallBytes = 1024
DefaultStallBytes — сколько восходящих байт без единого байта в ответ нужно, чтобы watchdog вообще начал отсчёт молчания.
Порог нужен из-за WireGuard: раз в 25 секунд он шлёт keepalive в 32 байта, на который сервер по протоколу не отвечает, а отвечает только на рукопожатие раз в две минуты. Без порога простаивающий туннель выглядел бы молчащим через 25 секунд и перезапускался бы каждые 50 — так и было на живой проверке G2. Между двумя рукопожатиями keepalive набирает не больше 160 байт; живой запрос приложения — SYN с повторами, DNS — набирает килобайт за секунды. Android этого не замечал: там счётчики читались из лога с точностью до 0,01 МБ, и keepalive в них не был виден вовсе.
const DefaultStallPoll = time.Second
DefaultStallPoll — как часто цикл спрашивает watchdog, жив ли туннель. Чаще секунды незачем: ядро присылает статистику раз в три секунды, а окно терпения измеряется десятками секунд.
const DefaultStallWindow = 25 * time.Second
DefaultStallWindow — сколько ждать ответа, прежде чем считать туннель неработающим. Та же величина, что у hasRecentTransportActivity на Android.
Variables ¶
var ErrHashesExhausted = errors.New("годных хешей звонка не осталось")
ErrHashesExhausted — годных хешей звонка не осталось, пробовать больше нечем. Единственная причина, по которой цикл заканчивается сам, а не по просьбе.
Functions ¶
func Backoff ¶
Backoff — задержка перед повтором после неудачной попытки номер attempt (нумерация с нуля: первая неудача даёт секунду).
Последовательность перенесена из Android дословно (TunnelManager.kt:1268): 1, 2, 4, 8, 16, 32, 64 секунды и дальше по 64.
func Interrupt ¶
Interrupt возвращает контекст, отменяемый по первому Ctrl+C (SIGINT) или SIGTERM, и функцию снятия обработчика.
Второй Ctrl+C убивает процесс немедленно: по первому сигналу обработчик снимается, и следующий уходит системе, у которой поведение по умолчанию — завершить процесс. Ждать корректной остановки дважды пользователь не обязан, а собственного os.Exit это не требует — выход из библиотеки дело плохое, да и на зависшем ядре он бы не помог: горутина, печатающая «выхожу», может стоять в том же замке, что и всё остальное.
Возвращённую функцию нужно вызвать при выходе: она снимает обработчик и отменяет контекст.
Types ¶
type Decision ¶
type Decision struct {
// Stop — пробовать больше нечем: годных хешей не осталось. Автомат при
// этом уже переведён в StateStopping.
Stop bool
// Delay — сколько ждать до следующей попытки. Ноль при Stop.
Delay time.Duration
// Attempt — номер кончившейся попытки, начиная с единицы. Для строки
// вида «попытка 3, следующая через 4 с».
Attempt int
// Transitions — переходы автомата, которые сделало это решение: Reconnect,
// а при исчерпанных хешах следом Stop. Пусто, когда автомат уже стоял там,
// куда его переводили.
//
// Поле нужно потому, что автомат переводит Restarter, а печатает переходы
// цикл подключения (lifecycle.go): без него он узнавал бы о смене состояния
// сравнением State() до и после вызова.
Transitions []Transition
}
Decision — что делать после того, как попытка кончилась.
type HashPool ¶
type HashPool struct {
// contains filtered or unexported fields
}
HashPool — хеши звонков VK и их годность.
Группы воркеров ядро раздаёт по хешам: группа, не получившая креды, молча возвращается, а остальные продолжают работать — туннель от одного негодного хеша не падает. Отменять группы пулу поэтому незачем, это ядро уже сделало. Его работа — помнить, какие хеши предлагать следующей попытке, и сказать, когда предлагать стало нечего (architecture.md §07).
Безопасен для параллельного вызова: Observe зовётся из Sink, то есть из горутин воркеров ядра.
func NewHashPool ¶
NewHashPool заводит пул из хешей профиля. Порядок сохраняется: ядро раздаёт хеши группам по индексу, и менять их местами между попытками незачем.
func (*HashPool) DeadReason ¶
DeadReason — почему хеш исключён. Пустая строка означает, что он годен.
func (*HashPool) Exhausted ¶
Exhausted — годных хешей не осталось. Пустой пул исчерпан с самого начала: подключаться без хешей не к чему.
func (*HashPool) MarkDead ¶
MarkDead исключает хеш. Возвращает false, если хеш уже исключён или пулу не принадлежит: пришедший ниоткуда хеш не повод считать, что туннелю хуже.
func (*HashPool) Observe ¶
Observe применяет событие ядра. Исключает хеш только core.HashCheckDead — звонок не найден или закрыт, и ни повтор, ни перезапуск его не вернут. Капча, лимит запросов и сетевая ошибка временные: по ним хеш остаётся в игре, иначе первая же неурядица со связью срезала бы все хеши разом и остановила клиент вместо того, чтобы переподключиться.
Возвращает true, если хеш именно этим событием и исключён — вызывающему есть о чём напечатать строку.
type Machine ¶
type Machine struct {
// contains filtered or unexported fields
}
Machine — конечный автомат подключения.
Безопасен для параллельного вызова: Observe зовётся из Sink, то есть из горутин воркеров ядра, а остальные методы — из супервизора.
func (*Machine) BeginAttempt ¶
func (m *Machine) BeginAttempt() (Transition, bool)
BeginAttempt открывает попытку подключения: StateIdle или StateReconnecting переходят в StateResolving, таблица шагов очищается.
Из остальных состояний — ничего: попытка уже идёт, а из StateStopping переходов нет вовсе.
func (*Machine) Degrade ¶
func (m *Machine) Degrade() (Transition, bool)
Degrade отмечает, что нисходящего трафика нет дольше разрешённого: туннель формально поднят, но не работает. Осмысленно только из StateConnected — остальные состояния и так не обещают трафика.
func (*Machine) FailedStage ¶
FailedStage — шаг, отказ которого ещё не перекрыт успехом. Пусто, если такого нет. Шаг считается перекрытым, когда по нему приходит core.StageOK: TURN и DTLS каждый воркер проходит самостоятельно, и отказ одного из ста не означает, что шаг не прошёл.
func (*Machine) LastError ¶
LastError — причина последнего ухода с линии, переданная в Reconnect. Сбрасывается началом новой попытки.
func (*Machine) Observe ¶
func (m *Machine) Observe(ev core.Event) (Transition, bool)
Observe применяет событие ядра. События, кроме core.EventStage, автомат не трогают — их разбирают другие части супервизора.
Шаг двигает состояние только вперёд по линии и только пока автомат на ней: в StateDegraded и StateReconnecting событие попадёт в таблицу шагов, но состояния не изменит. Событие, пришедшее в StateStopping, отбрасывается целиком: попытка кончилась, и умирающие воркеры не должны перерисовывать картину.
Отказ шага состояния тоже не меняет. Ядро повторяет шаги само (капчу — трижды, TURN и DTLS — каждым воркером), и закончилась попытка или нет, знает не шаг, а возврат из core.Run; супервизор скажет об этом через Reconnect.
func (*Machine) Reconnect ¶
func (m *Machine) Reconnect(cause error) (Transition, bool)
Reconnect уводит автомат с линии: попытка кончилась, ядро останавливается, следующая попытка ждёт задержки. Когда начинать её — решает restart.go (F2), автомат остаётся в StateReconnecting до BeginAttempt.
cause может быть нулевым: перезапуск бывает намеренным, например при смене сети. Из StateIdle и StateStopping — ничего: в первом нечего переподключать, второе конечное.
func (*Machine) Recover ¶
func (m *Machine) Recover() (Transition, bool)
Recover возвращает туннель из StateDegraded в StateConnected: трафик пошёл сам, перезапускать нечего.
Схема в §07 такой стрелки не рисует — там из Degraded только перезапуск или остановка. Но watchdog судит по окну в 25 секунд, а молчание такой длины бывает и у живого туннеля; считать его отказом значило бы рвать рабочее соединение.
func (*Machine) StageStatus ¶
func (m *Machine) StageStatus(stage core.Stage) core.StageStatus
StageStatus — чем кончился шаг в текущей попытке. Пустая строка означает, что о шаге ничего не известно: он не начинался или ядро о нём не сообщает (например, core.StageVK о неудаче молчит — молчание и есть неудача).
func (*Machine) Stop ¶
func (m *Machine) Stop() (Transition, bool)
Stop переводит автомат в конечное StateStopping: дальше только снятие datapath и выход (F3). Работает из любого состояния, включая StateIdle: Ctrl+C до первой попытки — тоже остановка.
type Options ¶
type Options struct {
// Config — настройки ядра. Поле Hashes заполняет сам цикл, из пула перед
// каждой попыткой: негодный хеш не должен попасть в следующую.
Config core.Config
// Hashes — хеши звонков VK, как их дал профиль. Порядок сохраняется.
Hashes []string
// NewDatapath строит datapath для одной попытки. Обязателен.
//
// Именно конструктор, а не готовый экземпляр: реализация одноразовая —
// Start у неё зовётся один раз, Close закрывает её навсегда, — а конфиг
// WireGuard сервер выдаёт новый на каждое подключение. Проверить Requires
// и объяснить нехватку прав нужно до входа в цикл, и это дело CLI.
//
// Исключение — datapath.Persistent (tun): такой экземпляр цикл
// строит один раз, на первой попытке, зовёт его Start на каждую
// попытку, Suspend — после каждой неудачной и Close — при выходе из Run.
NewDatapath func() (datapath.Datapath, error)
// DatapathStage — какой шаг закрывает подъём datapath: core.StageSocks
// или core.StageVPN. Пусто — core.StageSocks.
//
// Сам datapath своего имени не знает и знать не должен (D1); какой из двух
// шагов показать пользователю, известно тому, кто выбрал реализацию.
DatapathStage core.Stage
// ProbeDNS проверяет резолвер до запуска ядра и закрывает шаг «dns» —
// единственный шаг линии, который идёт до core.Run (core/events.go, «Кто
// какие шаги отправляет»). Отказ означает отказ попытки: без резолвера
// ядру не за чем идти, а сеть, где его нет, обычно возвращается — и
// следующая попытка это увидит. nil — шаг не проверяется и не отправляется.
//
// Срок ожидания задаёт сама функция: цикл передаёт ей контекст попытки,
// который живёт до её конца. Готовая реализация — core.ProbeDNS.
ProbeDNS func(ctx context.Context) error
// Control — ручка управления ядром: пауза, ответ на капчу, ответ на запрос
// TURN-кредов. nil допустим и означает, что управлять не понадобится.
Control *core.Control
// OnEvent получает все события ядра, включая те, что супервизор собрал сам
// (шаги datapath и done). nil — молчать.
//
// Зовётся из горутин воркеров ядра, но не одновременно: цикл сериализует
// OnEvent и OnTransition между собой, чтобы получателю — а это вывод в
// терминал — не понадобился собственный замок.
OnEvent func(core.Event)
// OnTransition получает состоявшуюся смену состояния: §07 требует печатать
// её строкой. nil — молчать.
OnTransition func(Transition)
// Logf получает строки самого цикла, которые не события ядра и не смена
// состояния: что маршруты сняты на время переподключения и что они
// возвращены (plan-e2.md, решение 3). Сериализован с OnEvent и
// OnTransition. nil — молчать.
Logf func(format string, args ...any)
// KillSwitch — стоит ли сейчас kill-switch, набор «закрыто» (plan-e3.md,
// K3): с ним datapath, снимая защиту DNS, закрывает трафик системы, а не
// отпускает его мимо туннеля. Цикл спрашивает после конца попытки, чтобы
// строка говорила правду: kill-switch мог и не встать (-allow-leak).
// nil — kill-switch нет.
KillSwitch func() bool
// StallWindow — сколько молчания терпит watchdog, прежде чем объявить
// туннель деградировавшим. 0 — DefaultStallWindow (25 с).
StallWindow time.Duration
// StallPoll — как часто опрашивается watchdog. 0 — DefaultStallPoll.
StallPoll time.Duration
// StallBytes — сколько восходящих байт без ответа считается отправкой,
// а не keepalive WireGuard. 0 — DefaultStallBytes.
StallBytes int64
// StallRestartAfter — сколько ещё длится молчание в состоянии «трафика
// нет», прежде чем попытка обрывается и начинается новая. 0 — столько же,
// сколько StallWindow: деградация в 25 с, перезапуск на пятидесятой.
//
// Две величины, а не одна, потому что молчание в двадцать пять секунд
// бывает и у живого туннеля: сначала мы говорим об этом пользователю
// и только потом рвём соединение.
StallRestartAfter time.Duration
}
Options — из чего собирается цикл подключения.
type Restarter ¶
type Restarter struct {
// contains filtered or unexported fields
}
Restarter — политика перезапуска: ведёт счётчик неудач и держит автомат с пулом хешей в согласии друг с другом.
Порядок вызовов у F3 такой: Failed, когда core.Run вернулся; Connected, когда туннель поднялся. Автомат переводит Failed — иначе счётчик попыток и состояние расходились бы при любом пропущенном вызове.
func NewRestarter ¶
NewRestarter связывает автомат с пулом хешей.
func (*Restarter) Connected ¶
func (r *Restarter) Connected()
Connected сбрасывает счётчик задержек: туннель, проработавший час, должен переподключаться через секунду, а не через минуту, заработанную прошлыми неудачами.
func (*Restarter) Failed ¶
Failed объявляет, что попытка кончилась: уводит автомат с линии и говорит, ждать ли следующую и сколько.
Когда годных хешей не осталось, автомат переводится в StateStopping: ждать нечего, подключаться не к чему.
func (*Restarter) Reset ¶
func (r *Restarter) Reset()
Reset обнуляет счётчик задержек без смены состояния: сеть сменилась, пока цикл ждал задержку.
type StallReason ¶
type StallReason string
StallReason — чем именно плох туннель. Пустая строка означает, что он здоров.
const ( // StallNone — претензий нет. StallNone StallReason = "" // StallNoDownlink — мы отправляли, ответа нет. StallNoDownlink StallReason = "нет ответа от сервера" // StallNoStats — ядро перестало присылать счётчики, хотя работает. StallNoStats StallReason = "ядро не присылает статистику" // StallNoUplink — datapath поднят, а в ядро от него не пришло ни байта. // Здоровый WireGuard так не молчит: сразу после подъёма он шлёт // рукопожатие, дальше keepalive раз в 25 секунд. Найдено живой проверкой // H2: служба штатного WireGuard упала при запуске, а туннель числился // работающим — неотвеченной отправки, по которой судит StallNoDownlink, // не было, потому что не было отправки вовсе. StallNoUplink StallReason = "datapath не передал в туннель ни байта" )
type State ¶
type State string
State — состояние туннеля. Набор повторяет схему из architecture.md §07.
const ( // StateIdle — ничего не запущено: до первой попытки подключения. StateIdle State = "idle" // StateResolving — проверяется резолвер до VK. Шаг наш, не ядра: он идёт // до core.Run (см. «Кто какие шаги отправляет» в core/events.go). StateResolving State = "resolving" // StateCaptcha — VK показал Smart Captcha и она решается. StateCaptcha State = "captcha" // StateVK — получены TURN-креды. StateVK State = "vk" // StateTURN — получен relay на TURN-сервере VK. StateTURN State = "turn" // StateHandshake — рукопожатие DTLS с сервером. StateHandshake State = "handshake" // StateConfiguring — воркеры на линии, поднимается datapath. StateConfiguring State = "configuring" // StateConnected — туннель работает. StateConnected State = "connected" // StateDegraded — воркеры живы, но нисходящего трафика нет дольше 25 с. // Выставляется watchdog'ом из restart.go, а не событием ядра. StateDegraded State = "degraded" // StateReconnecting — попытка закончилась, ядро останавливается, следующая // попытка ждёт своей задержки (F2). StateReconnecting State = "reconnecting" // StateStopping — останавливаемся окончательно: по Ctrl+C или потому, что // годных хешей не осталось. Состояние конечное, из него переходов нет. StateStopping State = "stopping" )
type Transition ¶
type Transition struct {
From State
To State
// Stage — шаг, вызвавший переход. Пусто, если переход сделал сам
// супервизор: начало попытки, watchdog, остановка.
Stage core.Stage
// Cause — причина ухода с линии. Заполнена только у перехода
// в StateReconnecting, и то не всегда: перезапуск бывает намеренным,
// например при смене сети.
Cause error
}
Transition — состоявшийся переход. Возвращается каждым методом, меняющим состояние, чтобы супервизор напечатал строку (§07) и не восстанавливал причину перехода из состояния постфактум.
type Tunnel ¶
type Tunnel struct {
// contains filtered or unexported fields
}
Tunnel — цикл подключения: ядро, datapath, автомат и политика перезапуска, связанные вместе.
Экземпляр одноразовый: Run зовётся один раз и возвращается, когда туннель остановлен окончательно.
func NewTunnel ¶
NewTunnel собирает цикл. Ошибку возвращает только на неполных настройках: сеть до Run не трогается.
func (*Tunnel) Machine ¶
Machine — автомат: таблица шагов текущей попытки и причина последнего отказа.
func (*Tunnel) Restart ¶
Restart обрывает идущую попытку с причиной reason и начинает новую сразу, без задержки и с обнулённым счётчиком задержек (Restarter.Restart). Если цикл ждёт задержку, ожидание прерывается. Зовётся при смене сети (S2 Э2) из любой горутины; до Run и после него ничего не делает.
Маршруты на время перезапуска снимаются, как после любой неудачной попытки: на новой сети может понадобиться капча.
func (*Tunnel) Run ¶
Run держит туннель, пока не отменят ctx или пока не кончатся годные хеши.
Остановка по ctx ошибкой не считается и даёт nil — как и у core.Run. Ошибка возвращается только вместе с ErrHashesExhausted: другого способа кончиться сам у цикла нет, отказ попытки означает следующую попытку.
К возврату datapath снят, горутины цикла закончились, а автомат стоит в StateStopping.
type Watchdog ¶
type Watchdog struct {
// contains filtered or unexported fields
}
Watchdog следит, что туннель не только поднят, но и работает.
Судит по core.EventStats — счётчикам, которые ядро присылает раз в три секунды. Времени внутри нет: текущее время приходит аргументом, а опрашивать Stall своим тикером — дело F3.
Безопасен для параллельного вызова: Observe зовётся из Sink.
func NewWatchdog ¶
NewWatchdog заводит watchdog с окном терпения. Ноль означает DefaultStallWindow.
func (*Watchdog) Observe ¶
Observe применяет событие ядра. Нужны ему два: core.EventStats и шаг «туннель готов» (core.StageDone), который отправляет сам супервизор; остальные события он не трогает.
func (*Watchdog) Reset ¶
func (w *Watchdog) Reset()
Reset забывает всё: вызывается перед новой попыткой, иначе счётчики прошлой выглядели бы как молчание этой.
func (*Watchdog) Stall ¶
func (w *Watchdog) Stall(now time.Time) StallReason
Stall — что не так с туннелем прямо сейчас.
Молчание считается не от последнего нисходящего байта, а от начала неотвеченной отправки. Оба других способа врут: от последнего нисходящего — туннель, через который полчаса никто ничего не посылал, объявлялся бы сломанным первым же запросом; от последнего восходящего — пока приложение продолжает отправлять, отсчёт сбрасывался бы каждые три секунды и молчание сервера не обнаружилось бы никогда. В Android складывали вторым способом, прикрывшись отдельной проверкой на живость статистики (TunnelManager.kt:1345).
Простаивающий туннель здоров: не отправляли — нечего и ждать. Keepalive WireGuard за отправку не считается — см. DefaultStallBytes.