[0.9.0] - 2026-06-17
Added
- Device Binding (JIT Device Enrollment) — one-time client slots that auto-bind to the first device connecting; subsequent connections from a different X25519 static key are rejected (Shutdown reason 4). Enrollment uses a DH proof
X25519(static_priv, server_static_pub)so the server verifies key ownership without the private key ever leaving the client. New CLI commands:--add-client-one-time <NAME>,--reset-device <NAME_OR_ID>. NewClientConfigfields:one_time: bool,device_pubkey: Option<[u8;32]>. Static key storage by platform: Linux/macOS~/.config/aivpn/device.key(mode 0600, atomic create); Windows%APPDATA%\aivpn\device.key; Android —EncryptedSharedPreferences(Android Keystore); iOS — Keychain (kSecAttrAccessibleAfterFirstUnlock). - Connection Quality Score (0–100) — per-session EWMA tracker computing RTT (40 pts), jitter (20 pts), packet loss (30 pts), neural MSE (10 pts). Exposed via new
QualityReportcontrol payload; server receives telemetry from each client on every keepalive exchange. - Adaptive Mode auto-tuning — quality score drives
AdaptiveLevel(Off/Light/Aggressive/Satellite) automatically. Each level adjusts keepalive interval (8/6/4/15 s) and FEC group size (disabled/16/8/4). Server can also pushAdaptiveHintto override the client-computed level. - KeepaliveAck RTT measurement — server echoes client keepalive timestamp; client computes RTT on receipt and feeds it into the quality tracker.
- XOR Forward Error Correction (upload path) — new
InnerType::FecRepair(0x0005) andFecEncoder/FecDecoderinaivpn-common::fec. Every N data packets, one repair packet (XOR of the group) is emitted on the client upload path. Group size N controlled byAdaptiveLevel::fec_n(). - Client-to-Client Relay — new
--allow-peer-routingserver flag (envAIVPN_ALLOW_PEER_ROUTING); when set, the TUN read loop forwards packets whose source IP belongs to a VPN client session directly to the destination VPN client session, enabling intra-VPN unicast routing. Disabled by default to preserve client isolation. - Local DNS Proxy — new
aivpn-client::dns_proxymodule;--dns-proxy <bind_addr> --dns-upstream <resolver>starts a lightweight UDP forwarder that tunnels all DNS queries through the active VPN path, preventing DNS leaks on platforms without per-app DNS routing. - New protocol control subtypes —
DeviceEnrollment(0x17),KeepaliveAck(0x18),QualityReport(0x19),AdaptiveHint(0x1A) with full encode/decode inprotocol.rs. - Device Key display (CLI / Windows / macOS) —
--show-device-keyCLI flag prints the device's X25519 public key (base64) and exits, enabling GUI clients to surface the key via subprocess. Windows GUI shows a truncated key in the Connection Keys panel with a copy button. macOS menu bar loads the key via helper action and displays it in the status view. - Traffic Mimicry Engine for iOS and Android —
MimicryEncryptorfromaivpn-commonis now used on the upload path of both iOS (aivpn-ios-core) and Android (aivpn-android-core). A bootstrap mask is derived from the PSK viabootstrap_mask_for_psk()so the very first packet already uses traffic shaping; subsequentMaskUpdatemessages hot-swap the active profile. Traffic Mimicry is now ✅ on all five platforms. - Feature Capability Matrix — formal table added after the Platform Support section in all three READMEs with verified ✅/❌ status for 13 features across 5 platforms.
Fixed
- FEC:
FecEncodercount overflow —count += 1in hot path could panic on overflow in debug builds; replaced withwrapping_add(1). - FEC: stale XOR buffer injected after lost repair packet — server FEC accumulator now tracks
group_seq; when a new group begins before the previous repair arrived, the stale XOR buffer is detected and discarded instead of being applied to the new group. - FEC:
FecDecoder::recorddivision by zero —FecRepair::decodenow returnsNoneforgroup_size == 0;FecDecoderguards against malformed repair packets that would cause a div-by-zero panic. - iOS FFI:
static_privkey_lennot validated —aivpn_run_tunnelFFI entry point now checksstatic_privkey_len == 32beforefrom_raw_parts; mismatched lengths are rejected with an error instead of causing undefined behavior. - Keepalive RTT skew —
ControlPayload::Keepalivenow carriessend_ts(client's own monotonic clock); server echoes it inKeepaliveAck; RTT is computed without client/server clock synchronization. - Server: non-IPv4 payload bypasses anti-spoof check — the data handler now rejects non-IPv4 inner payloads before the source-address enforcement step, preventing clients from bypassing the VPN IP ownership check via crafted packets.
- Android:
CtrlTxGuard::dropsilent failure on poisoned mutex — when the async control-tx channel mutex was poisoned,drop()silently skipped cleanup; now recovers from poison withinto_inner()and completes the cleanup. - Android: adaptive hint leaks across reconnects —
ACTIVE_ADAPTIVE_LEVELwas not reset at session start; the previous session's server-pushed level could influence the new session before the firstAdaptiveHintarrived; reset to0on session entry. - Android JNI: recording service name unbounded —
startRecordingservice name string was passed through JNI without length validation; capped to 128 UTF-8 bytes at the boundary to prevent allocation abuse. - macOS helper: DNS proxy port range not validated —
dnsProxyvalue in helper requests was checked for HOST:PORT format but not for port in range 1–65535; added explicit range check. - Security: quality sidecar written to world-readable
/tmp—write_quality_file()wroteaivpn-quality.jsontostd::env::temp_dir()which is world-readable; moved to/var/run/aivpn/(mode 0750, root:root) so other local users cannot read connection quality metrics. - iOS:
ControlPayload::Keepaliveused as unit variant (critical) — three call sites inios_tunnel.rsusedControlPayload::Keepalive.encode()which would fail to compile on iOS; corrected toControlPayload::Keepalive { send_ts: 0 }.encode(). - Android:
transition_recv_win.reset()discards in-flight window — during inline PFS rekey the old receive window was cleared instead of moved to the transition slot; corrected tostd::mem::take(&mut recv_win). - Android: dead load of
keepalive_sent_ms_rx— theArcclone was only used in a discardedload()call in theKeepaliveAckhandler; removed. - iOS: quality tracker not updated when
echo_ts == 0—record_received()and score update were inside theecho_ts > 0guard; moved outside so quality tracks liveness even without RTT. aivpn-common: clippymanual_abs_diffinquality.rs— manual branch replaced withsample_us.abs_diff(self.rtt_us).aivpn-common: clippy warnings inkernel_accel.rs—ioctl_ref(&mut v)corrected toioctl_ref(&v);io::Error::new(Other, …)replaced withio::Error::other(…).aivpn-client:send_controlsilently swallowed upload channel errors — send error was logged butOk(())was returned; now propagatesError::Channel(…).aivpn-client:Shutdownhandler returnedOk(())after disconnect — now returnsErr(Error::Session("server shutdown: …"))to cleanly break the run loop.- Windows: DNS proxy address validated with Unicode en-dash — the allowlist
":.[]−-"contained U+2212 instead of ASCII hyphen; replaced withaddr.parse::<SocketAddr>().is_err(). - Windows: manual JSON parsing in
read_quality_json— fragile comma-split parser replaced withserde_json::from_str::<serde_json::Value>. - Windows:
child.kill()without graceful wait —disconnect()now pollstry_wait()for up to 500 ms before force-killing. - CLI:
unwrap()inrecord start/stopsubcommands —UdpSocket::bindandsend_topanics replaced witheprintln!error messages.
Changed
- Version bumped 0.8.5 → 0.9.0 across workspace
Cargo.toml, all crateCargo.tomlfiles, macOSInfo.plist, iOSApp/Info.plistandTunnel/Info.plist.
[0.9.0] — 2026-06-17
Добавлено
- Привязка устройства (JIT Device Enrollment) — одноразовые конфиги, автоматически привязывающиеся к первому подключившемуся устройству. Последующие подключения с другим статическим X25519-ключом отклоняются (Shutdown, причина 4). Регистрация использует DH-доказательство
X25519(static_priv, server_static_pub)— приватный ключ никогда не покидает клиент. Новые команды CLI:--add-client-one-time <ИМЯ>,--reset-device <ИМЯ_ИЛИ_ID>. Новые поляClientConfig:one_time: bool,device_pubkey: Option<[u8;32]>. Хранение ключа по платформам: Linux/macOS —~/.config/aivpn/device.key(права 0600, атомарное создание); Windows —%APPDATA%\aivpn\device.key; Android —EncryptedSharedPreferences(Android Keystore); iOS — Keychain (kSecAttrAccessibleAfterFirstUnlock). - Оценка качества соединения (0–100) — EWMA-трекер на сессию, вычисляющий RTT (40 очков), джиттер (20 очков), потери пакетов (30 очков) и нейронный MSE (10 очков). Передаётся серверу через новый control payload
QualityReportпри каждом keepalive. - Автоматическая настройка Adaptive Mode — оценка качества управляет
AdaptiveLevel(Off/Light/Aggressive/Satellite) автоматически. Каждый уровень задаёт интервал keepalive (8/6/4/15 с) и группу FEC (отключено/16/8/4). Сервер может принудительно задать уровень черезAdaptiveHint. - Измерение RTT через KeepaliveAck — сервер эхирует временную метку keepalive клиента; клиент вычисляет RTT при получении и передаёт его в трекер качества.
- XOR Forward Error Correction (upload-путь) — новый
InnerType::FecRepair(0x0005) иFecEncoder/FecDecoderвaivpn-common::fec. Каждые N пакетов данных клиент отправляет один repair-пакет (XOR группы). Размер группы N задаётсяAdaptiveLevel::fec_n(). - Маршрутизация клиент-клиент — новый флаг сервера
--allow-peer-routing(envAIVPN_ALLOW_PEER_ROUTING): TUN read loop перенаправляет пакеты, исходный IP которых принадлежит сессии VPN-клиента, напрямую к целевой клиентской сессии — без выхода в интернет. По умолчанию отключено для изоляции клиентов. - Локальный DNS-прокси — новый модуль
aivpn-client::dns_proxy; флаги--dns-proxy <адрес> --dns-upstream <резолвер>запускают лёгкий UDP-форвардер, туннелирующий DNS-запросы через активный VPN-путь и предотвращающий DNS-утечки. - Новые control subtype протокола —
DeviceEnrollment(0x17),KeepaliveAck(0x18),QualityReport(0x19),AdaptiveHint(0x1A) с полным encode/decode вprotocol.rs. - Отображение Device Key (CLI / Windows / macOS) — флаг CLI
--show-device-keyвыводит X25519-публичный ключ устройства в base64 и завершает работу; используется GUI-клиентами. Windows GUI показывает усечённый ключ в панели «Ключи подключения» с кнопкой копирования. macOS получает ключ через helper action и отображает его в статусном окне. - Mimicry Engine для iOS и Android —
MimicryEncryptorизaivpn-commonтеперь используется на upload-пути iOS (aivpn-ios-core) и Android (aivpn-android-core). Начальная маска формируется из PSK черезbootstrap_mask_for_psk(), поэтому первый пакет уже маскирован;MaskUpdateзаменяет профиль без переподключения. Маскировка трафика теперь ✅ на всех пяти платформах. - Таблица функциональных возможностей — добавлена после раздела «Поддерживаемые платформы» во всех трёх README с проверенными статусами ✅/❌ для 13 функций на 5 платформах.
Исправлено
- FEC: переполнение счётчика
FecEncoder—count += 1в горячем пути мог вызвать панику в debug-сборке; заменено наwrapping_add(1). - FEC: устаревший XOR-буфер из потерянного repair-пакета — FEC-аккумулятор сервера теперь отслеживает
group_seq; когда новая группа начинается до получения repair-пакета предыдущей, устаревший XOR-буфер обнаруживается и отбрасывается вместо применения к новой группе. - FEC: деление на ноль в
FecDecoder::record—FecRepair::decodeтеперь возвращаетNoneприgroup_size == 0; добавлена защита от повреждённых repair-пакетов, вызывавших панику. - iOS FFI: параметр
static_privkey_lenне валидировался — точка входа FFIaivpn_run_tunnelтеперь проверяетstatic_privkey_len == 32доfrom_raw_parts; несоответствие длины возвращает ошибку вместо неопределённого поведения. - Keepalive: смещение RTT из-за рассинхронизации часов —
ControlPayload::Keepaliveтеперь несётsend_ts(монотонные часы клиента); сервер отражает его вKeepaliveAck; RTT вычисляется без синхронизации часов клиента и сервера. - Сервер: не-IPv4 payload обходил проверку anti-spoof — обработчик данных теперь отклоняет не-IPv4 внутренние payload до шага проверки владельца IP-адреса, предотвращая обход VPN IP ownership check через сформированные пакеты.
- Android:
CtrlTxGuard::dropмолчал при отравленном мьютексе — когда мьютекс async control-tx канала был отравлен,drop()тихо пропускал очистку; теперь восстанавливается из отравления черезinto_inner()и завершает очистку. - Android: утечка adaptive hint между переподключениями —
ACTIVE_ADAPTIVE_LEVELне сбрасывался при начале сессии; уровень, заданный сервером в прошлой сессии, мог влиять на новую до прихода первогоAdaptiveHint; сбрасывается в0при входе в сессию. - Android JNI: имя сервиса записи не ограничено по длине — строка имени в
startRecordingпередавалась через JNI без проверки длины; ограничена 128 байтами UTF-8 на границе JNI. - macOS helper: диапазон порта DNS-прокси не проверялся — значение
dnsProxyв запросах к helper проверялось на формат HOST:PORT, но не на диапазон порта 1–65535; добавлена явная проверка диапазона. - Безопасность: quality sidecar записывался в мировой
/tmp—write_quality_file()писалaivpn-quality.jsonвstd::env::temp_dir(), доступный всем локальным пользователям; перемещён в/var/run/aivpn/(режим 0750, root:root). - iOS:
ControlPayload::Keepaliveиспользовался как unit-вариант (критическое) — три точки вios_tunnel.rsиспользовалиControlPayload::Keepalive.encode(), что не компилируется; исправлено наControlPayload::Keepalive { send_ts: 0 }.encode(). - Android:
transition_recv_win.reset()уничтожал окно в полёте — при inline PFS rekey старое receive-окно очищалось вместо переноса в transition-слот; исправлено наstd::mem::take(&mut recv_win). - Android: мёртвая загрузка
keepalive_sent_ms_rx—Arc-клон использовался только в выброшенномload()в обработчикеKeepaliveAck; удалён. - iOS: трекер качества не обновлялся при
echo_ts == 0—record_received()был внутри проверкиecho_ts > 0; вынесен наружу. aivpn-common: clippy-предупреждения вquality.rsиkernel_accel.rs—manual_abs_diff,needless_pass_by_ref_mut,io_error_other— все исправлены.aivpn-client:send_controlмолча проглатывал ошибки канала — теперь пробрасываетError::Channel(…).aivpn-client: обработчикShutdownвозвращалOk(())после отключения — теперь возвращаетErr(Error::Session("server shutdown: …"))для выхода из run loop.- Windows: валидация адреса DNS-прокси с Unicode en-dash —
":.[]−-"содержал U+2212; заменено наaddr.parse::<SocketAddr>().is_err(). - Windows: ручной разбор JSON в
read_quality_json— хрупкий парсер заменён наserde_json::from_str::<serde_json::Value>. - Windows:
child.kill()без мягкого ожидания —disconnect()опрашиваетtry_wait()до 500 мс передkill(). - CLI:
unwrap()в подкомандахrecord start/stop— заменены на вывод ошибки черезeprintln!.