mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user