feat: awg connection states (#2091)

* Submodule amneziawg-apple updated

* feat: add support for controlled junk and special handshake timeout in AWG configurator

* refactor: improve AWG configurator and iOS controller logic

* awg_configurator.cpp reverted
This commit is contained in:
yyy-amnezia
2025-12-30 04:45:32 +02:00
committed by GitHub
parent 52031efc48
commit d859b111ca
7 changed files with 216 additions and 49 deletions

View File

@@ -94,15 +94,24 @@ extension PacketTunnelProvider {
}
} catch {
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
completionHandler(nil)
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
}
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter?.getRuntimeConfiguration { settings in
let components = settings!.components(separatedBy: "\n")
guard let wgAdapter = wgAdapter else {
completionHandler(nil)
return
}
wgAdapter.getRuntimeConfiguration { settings in
guard let settings = settings else {
completionHandler(nil)
return
}
let components = settings.components(separatedBy: "\n")
var settingsDictionary: [String: String] = [:]
for component in components {
@@ -131,7 +140,7 @@ extension PacketTunnelProvider {
}
}
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter?.getRuntimeConfiguration { settings in

View File

@@ -76,7 +76,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
guard hasMeaningfulChange, self.protoType != nil else { return }
guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
if proto == .wireguard {
return
}
DispatchQueue.main.async {
self.handle(networkChange: path) { _ in }
@@ -123,6 +128,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
if messageData.count == 1 && messageData[0] == 0 {
guard let completionHandler else { return }
if protoType == .wireguard {
handleWireguardAppMessage(messageData, completionHandler: completionHandler)
} else {
completionHandler(nil)
}
return
}
guard let message = String(data: messageData, encoding: .utf8) else {
if let completionHandler {
completionHandler(nil)
@@ -133,6 +148,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
neLog(.info, title: "App said: ", message: message)
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
if protoType == .wireguard {
handleWireguardAppMessage(messageData, completionHandler: completionHandler)
return
}
neLog(.error, message: "Failed to serialize message from app")
return
}

View File

@@ -42,46 +42,64 @@ struct WGConfig: Decodable {
}
var settings: String {
guard junkPacketCount != nil else { return "" }
func trimmed(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty else {
return nil
}
return value
}
guard
let junkPacketCount = trimmed(junkPacketCount),
let junkPacketMinSize = trimmed(junkPacketMinSize),
let junkPacketMaxSize = trimmed(junkPacketMaxSize),
let initPacketJunkSize = trimmed(initPacketJunkSize),
let responsePacketJunkSize = trimmed(responsePacketJunkSize),
let initPacketMagicHeader = trimmed(initPacketMagicHeader),
let responsePacketMagicHeader = trimmed(responsePacketMagicHeader),
let underloadPacketMagicHeader = trimmed(underloadPacketMagicHeader),
let transportPacketMagicHeader = trimmed(transportPacketMagicHeader)
else { return "" }
var settingsLines: [String] = []
// Required parameters when junkPacketCount is present
settingsLines.append("Jc = \(junkPacketCount!)")
settingsLines.append("Jmin = \(junkPacketMinSize!)")
settingsLines.append("Jmax = \(junkPacketMaxSize!)")
settingsLines.append("S1 = \(initPacketJunkSize!)")
settingsLines.append("S2 = \(responsePacketJunkSize!)")
settingsLines.append("H1 = \(initPacketMagicHeader!)")
settingsLines.append("H2 = \(responsePacketMagicHeader!)")
settingsLines.append("H3 = \(underloadPacketMagicHeader!)")
settingsLines.append("H4 = \(transportPacketMagicHeader!)")
settingsLines.append("Jc = \(junkPacketCount)")
settingsLines.append("Jmin = \(junkPacketMinSize)")
settingsLines.append("Jmax = \(junkPacketMaxSize)")
settingsLines.append("S1 = \(initPacketJunkSize)")
settingsLines.append("S2 = \(responsePacketJunkSize)")
settingsLines.append("H1 = \(initPacketMagicHeader)")
settingsLines.append("H2 = \(responsePacketMagicHeader)")
settingsLines.append("H3 = \(underloadPacketMagicHeader)")
settingsLines.append("H4 = \(transportPacketMagicHeader)")
// Optional parameters - only add if not nil and not empty
if let s3 = cookieReplyPacketJunkSize, !s3.isEmpty {
if let s3 = trimmed(cookieReplyPacketJunkSize) {
settingsLines.append("S3 = \(s3)")
}
if let s4 = transportPacketJunkSize, !s4.isEmpty {
if let s4 = trimmed(transportPacketJunkSize) {
settingsLines.append("S4 = \(s4)")
}
if let i1 = specialJunk1, !i1.isEmpty {
if let i1 = trimmed(specialJunk1) {
settingsLines.append("I1 = \(i1)")
}
if let i2 = specialJunk2, !i2.isEmpty {
if let i2 = trimmed(specialJunk2) {
settingsLines.append("I2 = \(i2)")
}
if let i3 = specialJunk3, !i3.isEmpty {
if let i3 = trimmed(specialJunk3) {
settingsLines.append("I3 = \(i3)")
}
if let i4 = specialJunk4, !i4.isEmpty {
if let i4 = trimmed(specialJunk4) {
settingsLines.append("I4 = \(i4)")
}
if let i5 = specialJunk5, !i5.isEmpty {
if let i5 = trimmed(specialJunk5) {
settingsLines.append("I5 = \(i5)")
}
return settingsLines.joined(separator: "\n")
}

View File

@@ -7,6 +7,8 @@
#include <QVariantMap>
#include <QStringList>
#include <QList>
#include <QElapsedTimer>
#include <atomic>
#ifdef __OBJC__
#import <Foundation/Foundation.h>
@@ -103,6 +105,7 @@ private:
bool startXray(const QString &jsonConfig);
void startTunnel();
void emitConnectionStateIfChanged(Vpn::ConnectionState state);
private:
void *m_iosControllerWrapper {};
@@ -116,8 +119,13 @@ private:
amnezia::Proto m_proto;
QJsonObject m_rawConfig;
QString m_tunnelId;
uint64_t m_txBytes;
uint64_t m_rxBytes;
uint64_t m_txBytes = 0;
uint64_t m_rxBytes = 0;
bool m_handshakeAwaiting = false;
bool m_handshakeConfirmed = false;
QElapsedTimer m_handshakeTimer;
Vpn::ConnectionState m_lastEmittedState = Vpn::ConnectionState::Unknown;
std::atomic_bool m_statusRequestInFlight { false };
};
#endif // IOS_CONTROLLER_H

View File

@@ -93,6 +93,48 @@ Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
}
}
namespace {
constexpr int kHandshakeTimeoutMs = 12000;
constexpr uint64_t kHandshakeRxThreshold = 4096;
bool isWireGuardBasedProto(amnezia::Proto proto) {
return proto == amnezia::Proto::WireGuard || proto == amnezia::Proto::Awg;
}
uint64_t uint64FromResponse(NSDictionary *response, NSString *key, uint64_t fallback = 0) {
id value = response[key];
if (!value || value == [NSNull null]) {
return fallback;
}
if ([value isKindOfClass:[NSNumber class]]) {
return [(NSNumber *)value unsignedLongLongValue];
}
if ([value isKindOfClass:[NSString class]]) {
const char *str = [(NSString *)value UTF8String];
if (str && *str) {
return strtoull(str, nullptr, 10);
}
}
return fallback;
}
long long int64FromResponse(NSDictionary *response, NSString *key, long long fallback = 0) {
id value = response[key];
if (!value || value == [NSNull null]) {
return fallback;
}
if ([value isKindOfClass:[NSNumber class]]) {
return [(NSNumber *)value longLongValue];
}
if ([value isKindOfClass:[NSString class]]) {
const char *str = [(NSString *)value UTF8String];
if (str && *str) {
return strtoll(str, nullptr, 10);
}
}
return fallback;
}
}
namespace {
IosController* s_instance = nullptr;
}
@@ -114,6 +156,15 @@ IosController::IosController() : QObject()
}
void IosController::emitConnectionStateIfChanged(Vpn::ConnectionState state)
{
if (m_lastEmittedState == state) {
return;
}
m_lastEmittedState = state;
emit connectionStateChanged(state);
}
IosController* IosController::Instance() {
if (!s_instance) {
s_instance = new IosController();
@@ -280,33 +331,65 @@ void IosController::disconnectVpn()
void IosController::checkStatus()
{
if (!m_currentTunnel) {
return;
}
if (m_currentTunnel.connection.status != NEVPNStatusConnected) {
return;
}
if (m_statusRequestInFlight.exchange(true)) {
return;
}
NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action];
NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus];
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sendVpnExtensionMessage(message, [&](NSDictionary* response){
uint64_t txBytes = [response[@"tx_bytes"] intValue];
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
uint64_t last_handshake_time_sec = 0;
#if !MACOS_NE
if (response[@"last_handshake_time_sec"] && ![response[@"last_handshake_time_sec"] isKindOfClass:[NSNull class]]) {
last_handshake_time_sec = [response[@"last_handshake_time_sec"] intValue];
} else {
qDebug() << "Key last_handshake_time_sec is missing or null";
if (!response) {
QMetaObject::invokeMethod(this, [this]() {
m_statusRequestInFlight = false;
}, Qt::QueuedConnection);
return;
}
if (last_handshake_time_sec < 0) {
disconnectVpn();
qDebug() << "Invalid handshake time, disconnecting VPN.";
}
#endif
const uint64_t txBytes = uint64FromResponse(response, @"tx_bytes");
const uint64_t rxBytes = uint64FromResponse(response, @"rx_bytes");
const long long last_handshake_time_sec = int64FromResponse(response, @"last_handshake_time_sec");
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
m_rxBytes = rxBytes;
m_txBytes = txBytes;
QMetaObject::invokeMethod(this, [this, txBytes, rxBytes, last_handshake_time_sec]() {
if (isWireGuardBasedProto(m_proto) && m_handshakeAwaiting) {
const bool hasHandshakeData = (last_handshake_time_sec >= 0);
const bool hasFreshHandshake = hasHandshakeData &&
((last_handshake_time_sec > 0) ||
(rxBytes >= kHandshakeRxThreshold) ||
(txBytes >= kHandshakeRxThreshold));
if (hasFreshHandshake) {
m_handshakeConfirmed = true;
m_handshakeAwaiting = false;
m_handshakeTimer.invalidate();
qDebug() << "IosController::checkStatus : handshake confirmed";
emitConnectionStateIfChanged(Vpn::ConnectionState::Connected);
} else if (m_handshakeTimer.isValid() &&
m_handshakeTimer.elapsed() > kHandshakeTimeoutMs) {
m_handshakeTimer.restart();
qDebug() << "IosController::checkStatus : handshake timed out, keeping tunnel alive";
emitConnectionStateIfChanged(Vpn::ConnectionState::Reconnecting);
}
}
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
m_rxBytes = rxBytes;
m_txBytes = txBytes;
m_statusRequestInFlight = false;
}, Qt::QueuedConnection);
});
});
}
@@ -413,7 +496,22 @@ void IosController::vpnStatusDidChange(void *pNotification)
}
}
emit connectionStateChanged(iosStatusToState(session.status));
Vpn::ConnectionState nextState = iosStatusToState(session.status);
if (session.status == NEVPNStatusConnected && isWireGuardBasedProto(m_proto)) {
if (!m_handshakeConfirmed) {
nextState = Vpn::ConnectionState::Connecting;
if (!m_handshakeAwaiting) {
m_handshakeAwaiting = true;
m_handshakeTimer.restart();
}
}
} else if (session.status != NEVPNStatusConnected) {
m_handshakeAwaiting = false;
m_handshakeConfirmed = false;
m_handshakeTimer.invalidate();
m_statusRequestInFlight = false;
}
emitConnectionStateIfChanged(nextState);
}
}
@@ -799,6 +897,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
{
if (!m_currentTunnel) {
qDebug() << "Cannot set an extension callback without a tunnel manager";
if (callback) {
callback(nil);
}
return;
}
@@ -808,6 +909,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
if (!data || error) {
qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:"
<< [error.localizedDescription UTF8String];
if (callback) {
callback(nil);
}
return;
}
@@ -838,11 +942,18 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
} else {
qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist";
if (callback) {
callback(nil);
}
return;
}
if (sendError) {
qDebug() << "Failed to send message to VpnExtension. Error:"
<< [sendError.localizedDescription UTF8String];
if (callback) {
callback(nil);
}
}
}