Compare commits

...

27 Commits

Author SHA1 Message Date
NickVs2015 89b94d2588 fix: resolve flush error and fix comment 2026-04-11 00:12:58 +03:00
NickVs2015 9166e17a23 fix: add support system with nscd service 2026-04-10 23:08:32 +03:00
NickVs2015 37d2b8716d fix: infinite reconnection when NM up 2026-04-10 23:06:16 +03:00
NickVs2015 92b168100a fix: Linux killswitch changes 2026-04-10 23:06:16 +03:00
NickVs2015 0f6db8a238 fix: Linux dns without dbus 2026-04-10 23:06:16 +03:00
NickVs2015 7c075625d2 fix: revert wireguardutilslinux 2026-04-03 20:21:00 +03:00
NickVs2015 c6a40b09c9 fix: reconnect 2026-04-03 20:11:28 +03:00
NickVs2015 560c4070b4 fix: add NM_STATE_DISABLED and check getGatewayAndIface more carefully 2026-04-03 08:47:15 +03:00
NickVs2015 f6806459fd fix: restore reconnect time 2026-04-02 10:26:16 +03:00
NickVs2015 646b1561f8 revert: restore linuxfirewall.cpp 2026-04-01 00:57:11 +03:00
NickVs2015 6d1e10a2e3 feat: catch state changed via check gateway 2026-04-01 00:43:46 +03:00
NickVs2015 af0d561c5c fix: add dns load/unload 2026-03-31 23:14:17 +03:00
NickVs2015 214b18f65f fix: improve reeconnection 2026-03-31 14:08:04 +03:00
NickVs2015 bd554fb730 fix: Dbus error, fix race conditional 2026-03-31 10:35:04 +03:00
NickVs2015 316e64122e fix: add linux reconnection 2026-03-26 22:31:26 +03:00
NickVs2015 bf3d11e5c4 feat: renewal new status logic (#2409)
* fix: renewal add status logic

* fix: wakeup activity resumed android
2026-03-25 19:48:32 +08:00
NickVs2015 9a0222aee3 fix: ui fixes for renewal subscription (#2406) 2026-03-25 12:34:42 +08:00
NickVs2015 f0f0f7c5be feat: add subscription renewal (#2389)
* feat: add renewal subsribe

* fix: after review
2026-03-24 22:45:02 +08:00
NickVs2015 36b1a863bf fix: black screen resume / pause (#2400) 2026-03-24 22:13:31 +08:00
yyy-amnezia 4103c5bbcf refactor: extract and simplify OpenVPN reachability and network change handling logic (#2402) 2026-03-24 22:12:59 +08:00
vkamn fa69da6d56 chore: send app version in services request (#2403) 2026-03-24 20:25:04 +08:00
yyy-amnezia aaf2c9ddeb feat: add Xray split tunnel support for iOS PacketTunnelProvider (#2332) 2026-03-24 16:07:36 +08:00
Mitternacht822 dbbc7119ec feat: add warning info for ssh keys (#2252)
* fix: fixed da typo

* feat: added warning about available ssh keys info
2026-03-24 16:06:40 +08:00
vkamn c57162c4cc feat: add base amnezia trial support (#2366)
* feat: add base amnezia trial support

* feat: add external-trial
2026-03-24 10:29:51 +08:00
NickVs2015 40e39895c9 fix openfile deadlock (#2373) 2026-03-21 11:46:46 +08:00
vkamn ec3ab2a03c chore: update licnese file (#2376) 2026-03-20 21:04:13 +08:00
yyy-amnezia ddecfcad26 fix: apple platform network switch fix (#2359)
* Apple platform network switch fix

* macos_ne exclusion fixed
2026-03-20 20:51:36 +08:00
51 changed files with 1480 additions and 622 deletions
+1 -1
View File
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
## License ## License
GPL v3.0 This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
## Donate ## Donate
+149
View File
@@ -0,0 +1,149 @@
# Third-Party Licenses
This project is licensed under the GNU General Public License v3.0.
This file lists third-party software components used by this repository.
Each component is distributed under its own license as linked below.
---
## QtKeychain
- Source: https://github.com/frankosterfeld/qtkeychain
- License: BSD License
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
---
## QSimpleCrypto
- Source: https://github.com/n1flh31mur/QSimpleCrypto
- License: Apache License 2.0
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
---
## SortFilterProxyModel
- Source: https://github.com/oKcerG/SortFilterProxyModel
- License: MIT License
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
---
## QJsonStruct
- Source: https://github.com/Qv2ray/QJsonStruct
- License: MIT License
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
---
## QR Code Generator (qrcodegen)
- Source: https://github.com/nayuki/QR-Code-generator
- License: MIT License
- License Text: https://www.nayuki.io/page/qr-code-generator-library
---
## Qt Gamepad
- Source: https://github.com/qt/qtgamepad
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
---
## AmneziaWG Apple (WireGuard)
- Source: https://github.com/amnezia-vpn/amneziawg-apple
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
---
## AmneziaWG Android
- Source: https://github.com/amnezia-vpn/amneziawg-go
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
---
## Xray Core
- Source: https://github.com/XTLS/Xray-core
- License: Mozilla Public License 2.0 (MPL-2.0)
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
---
## Cloak
- Source: https://github.com/cbeuw/Cloak
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
---
## Shadowsocks
- Source: https://github.com/shadowsocks/shadowsocks-libev
- License: GPL-3.0-or-later
- License Text: http://www.gnu.org/licenses/
---
## OpenSSL
- Source: https://github.com/openssl/openssl
- License: Apache License 2.0
- License Text: https://www.openssl.org/source/license.html
---
## libssh
- Source: https://www.libssh.org/
- License: GNU Lesser General Public License (LGPL)
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
---
## OpenVPNAdapter
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
---
## Wintun
- Source: https://www.wintun.net/
- License: Prebuilt Binaries License
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
---
## Mullvad Split Tunnel Driver
- Source: https://github.com/mullvad/win-split-tunnel
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
---
## tun2socks
- Source: https://github.com/eycorsican/go-tun2socks
- License: MIT License
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
---
## TAP-Windows Driver
- Source: https://github.com/OpenVPN/tap-windows6
- License: tap-windows6 license
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING
@@ -296,9 +296,25 @@ class AmneziaActivity : QtActivity() {
hasWindowFocus = hasFocus hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus") Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) { if (!hasFocus) {
// Cancel pending operations if window loses focus
resumeHandler.removeCallbacksAndMessages(null) resumeHandler.removeCallbacksAndMessages(null)
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 50)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
requestLayout()
invalidate()
}
}, 150)
}
} }
} }
@@ -337,6 +353,13 @@ class AmneziaActivity : QtActivity() {
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean) private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() { override fun onPause() {
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
// Using a coroutine here would be too late — the surface is gone by the time
// the coroutine runs. A direct synchronous call gives Qt's render thread the
// best chance to process visible=false before surface destruction.
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
super.onPause() super.onPause()
isActivityResumed = false isActivityResumed = false
// Cancel all pending operations when activity pauses // Cancel all pending operations when activity pauses
@@ -349,6 +372,9 @@ class AmneziaActivity : QtActivity() {
super.onResume() super.onResume()
isActivityResumed = true isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity") Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityResumed()
}
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) { if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!! val uri = pendingOpenFileUri!!
@@ -816,7 +842,7 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun getFd(fileName: String): Int { fun getFd(fileName: String): Int {
Log.v(TAG, "Get fd for $fileName") Log.v(TAG, "Get fd for $fileName")
return blockingCall { return blockingCall(Dispatchers.IO) {
try { try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r") pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1 pfd?.fd ?: -1
@@ -31,4 +31,7 @@ object QtAndroidController {
external fun onImeInsetsChanged(heightDp: Int) external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int) external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
external fun onActivityPaused()
external fun onActivityResumed()
} }
+4 -1
View File
@@ -10,8 +10,10 @@ namespace apiDefs
AmneziaFreeV3, AmneziaFreeV3,
AmneziaPremiumV1, AmneziaPremiumV1,
AmneziaPremiumV2, AmneziaPremiumV2,
AmneziaTrialV2,
SelfHosted, SelfHosted,
ExternalPremium ExternalPremium,
ExternalTrial
}; };
enum ConfigSource { enum ConfigSource {
@@ -32,6 +34,7 @@ namespace apiDefs
constexpr QLatin1String stackType("stack_type"); constexpr QLatin1String stackType("stack_type");
constexpr QLatin1String serviceType("service_type"); constexpr QLatin1String serviceType("service_type");
constexpr QLatin1String cliVersion("cli_version"); constexpr QLatin1String cliVersion("cli_version");
constexpr QLatin1String cliName("cli_name");
constexpr QLatin1String supportedProtocols("supported_protocols"); constexpr QLatin1String supportedProtocols("supported_protocols");
constexpr QLatin1String vpnKey("vpn_key"); constexpr QLatin1String vpnKey("vpn_key");
+14 -2
View File
@@ -58,18 +58,24 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
}; };
case apiDefs::ConfigSource::AmneziaGateway: { case apiDefs::ConfigSource::AmneziaGateway: {
constexpr QLatin1String servicePremium("amnezia-premium"); constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceTrial("amnezia-trial");
constexpr QLatin1String serviceFree("amnezia-free"); constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium"); constexpr QLatin1String serviceExternalPremium("external-premium");
constexpr QLatin1String serviceExternalTrial("external-trial");
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString(); auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
if (serviceType == servicePremium) { if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2; return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceTrial) {
return apiDefs::ConfigType::AmneziaTrialV2;
} else if (serviceType == serviceFree) { } else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3; return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) { } else if (serviceType == serviceExternalPremium) {
return apiDefs::ConfigType::ExternalPremium; return apiDefs::ConfigType::ExternalPremium;
} else if (serviceType == serviceExternalTrial) {
return apiDefs::ConfigType::ExternalTrial;
} }
} }
default: { default: {
@@ -90,6 +96,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeConflict = 409; const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404; const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501; const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) { if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors; qDebug().noquote() << sslErrors;
@@ -122,6 +129,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiNotFoundError; return amnezia::ErrorCode::ApiNotFoundError;
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) { } else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} else if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
} }
return amnezia::ErrorCode::ApiConfigDownloadError; return amnezia::ErrorCode::ApiConfigDownloadError;
} }
@@ -133,7 +142,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject) bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{ {
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2, static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::ExternalPremium }; apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
apiDefs::ConfigType::ExternalTrial };
return premiumTypes.contains(getConfigType(serverConfigObject)); return premiumTypes.contains(getConfigType(serverConfigObject));
} }
@@ -177,7 +187,9 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject) QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{ {
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) { auto configType = apiUtils::getConfigType(serverConfigObject);
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
return {}; return {};
} }
@@ -153,6 +153,8 @@ void CoreController::initControllers()
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get()); m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this)); m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get()); m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
@@ -46,6 +46,7 @@ namespace
constexpr int httpStatusCodeConflict = 409; constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501; constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodeUnprocessableEntity = 422;
} }
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -451,6 +452,8 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
} }
} else if (httpStatus == httpStatusCodeConflict) { } else if (httpStatus == httpStatusCodeConflict) {
return false; return false;
} else if (httpStatus == httpStatusCodeUnprocessableEntity) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) { } else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError; qDebug() << replyError;
return true; return true;
+77 -91
View File
@@ -292,117 +292,103 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) }; return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
#endif #endif
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
constexpr int BUFFER_SIZE = 100; static constexpr size_t BUFFER_SIZE = 8192;
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
int sock = -1, msgseq = 0;
struct nlmsghdr *nlh, *nlmsg;
struct rtmsg *route_entry;
// This struct contain route attributes (route type)
struct rtattr *route_attribute;
char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE];
char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE];
char *ptr = buffer;
struct timeval tv;
if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) { int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock < 0) {
perror("socket failed"); perror("socket failed");
return {}; return {};
} }
memset(msgbuf, 0, sizeof(msgbuf)); struct timeval tv { 1, 0 };
memset(gateway_address, 0, sizeof(gateway_address)); setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
memset(interface, 0, sizeof(interface));
memset(buffer, 0, sizeof(buffer));
/* point the header and the msg structure pointers into the buffer */ struct {
nlmsg = (struct nlmsghdr *)msgbuf; struct nlmsghdr hdr;
struct rtmsg rt;
} req {};
req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
req.hdr.nlmsg_type = RTM_GETROUTE;
req.hdr.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST;
req.hdr.nlmsg_seq = 1;
req.hdr.nlmsg_pid = static_cast<uint32_t>(getpid());
req.rt.rtm_family = AF_INET;
/* Fill in the nlmsg header*/ if (send(sock, &req, req.hdr.nlmsg_len, 0) < 0) {
nlmsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlmsg->nlmsg_type = RTM_GETROUTE; // Get the routes from kernel routing table .
nlmsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump.
nlmsg->nlmsg_seq = msgseq++; // Sequence of the message packet.
nlmsg->nlmsg_pid = getpid(); // PID of process sending the request.
/* 1 Sec Timeout to avoid stall */
tv.tv_sec = 1;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv, sizeof(struct timeval));
/* send msg */
if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) {
perror("send failed"); perror("send failed");
close(sock);
return {}; return {};
} }
/* receive response */ char buffer[BUFFER_SIZE];
do size_t total_len = 0;
{ bool done = false;
received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0);
if (received_bytes < 0) {
perror("Error in recv");
return {};
}
nlh = (struct nlmsghdr *) ptr; while (!done && total_len < BUFFER_SIZE) {
ssize_t n = recv(sock, buffer + total_len, BUFFER_SIZE - total_len, 0);
/* Check if the header is valid */ if (n <= 0)
if((NLMSG_OK(nlmsg, received_bytes) == 0) ||
(nlmsg->nlmsg_type == NLMSG_ERROR))
{
perror("Error in received packet");
return {};
}
/* If we received all data break */
if (nlh->nlmsg_type == NLMSG_DONE)
break; break;
else {
ptr += received_bytes;
msg_len += received_bytes;
}
/* Break if its not a multi part message */ int scan_len = static_cast<int>(n);
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0) for (struct nlmsghdr *h = reinterpret_cast<struct nlmsghdr *>(buffer + total_len);
break; NLMSG_OK(h, scan_len);
} h = NLMSG_NEXT(h, scan_len))
while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid()));
/* parse response */
for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes))
{
/* Get the route data */
route_entry = (struct rtmsg *) NLMSG_DATA(nlh);
/* We are just interested in main routing table */
if (route_entry->rtm_table != RT_TABLE_MAIN)
continue;
route_attribute = (struct rtattr *) RTM_RTA(route_entry);
route_attribute_len = RTM_PAYLOAD(nlh);
/* Loop through all attributes */
for ( ; RTA_OK(route_attribute, route_attribute_len);
route_attribute = RTA_NEXT(route_attribute, route_attribute_len))
{ {
switch(route_attribute->rta_type) { if (h->nlmsg_type == NLMSG_DONE || h->nlmsg_type == NLMSG_ERROR) {
case RTA_OIF: done = true;
if_indextoname(*(int *)RTA_DATA(route_attribute), interface);
break;
case RTA_GATEWAY:
inet_ntop(AF_INET, RTA_DATA(route_attribute),
gateway_address, sizeof(gateway_address));
break;
default:
break; break;
} }
} }
total_len += static_cast<size_t>(n);
if ((*gateway_address) && (*interface)) {
qDebug() << "Gateway " << gateway_address << " for interface " << interface;
break;
}
} }
QString resultGw;
QString resultIf;
int remaining = static_cast<int>(total_len);
for (struct nlmsghdr *nlh = reinterpret_cast<struct nlmsghdr *>(buffer);
NLMSG_OK(nlh, remaining);
nlh = NLMSG_NEXT(nlh, remaining))
{
if (nlh->nlmsg_type == NLMSG_DONE || nlh->nlmsg_type == NLMSG_ERROR)
break;
if (nlh->nlmsg_type != RTM_NEWROUTE)
continue;
struct rtmsg *rt = static_cast<struct rtmsg *>(NLMSG_DATA(nlh));
if (rt->rtm_table != RT_TABLE_MAIN || rt->rtm_family != AF_INET)
continue;
char route_gw[INET_ADDRSTRLEN] = {};
char route_if[IF_NAMESIZE] = {};
int attr_len = RTM_PAYLOAD(nlh);
for (struct rtattr *rta = RTM_RTA(rt);
RTA_OK(rta, attr_len);
rta = RTA_NEXT(rta, attr_len))
{
if (rta->rta_type == RTA_GATEWAY)
inet_ntop(AF_INET, RTA_DATA(rta), route_gw, sizeof(route_gw));
else if (rta->rta_type == RTA_OIF)
if_indextoname(*static_cast<int *>(RTA_DATA(rta)), route_if);
}
if (!route_gw[0] || !route_if[0])
continue;
QString ifStr(route_if);
if (ifStr.startsWith(QLatin1String("amn")) ||
ifStr.startsWith(QLatin1String("tun")))
continue;
resultGw = QString::fromLatin1(route_gw);
resultIf = QString::fromLatin1(route_if);
qDebug() << "Gateway " << route_gw << " for interface " << route_if;
break;
}
close(sock); close(sock);
return { gateway_address, QNetworkInterface::interfaceFromName(interface) }; return { resultGw, QNetworkInterface::interfaceFromName(resultIf) };
#endif #endif
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
QString gateway; QString gateway;
+20 -4
View File
@@ -142,10 +142,6 @@ bool Daemon::activate(const InterfaceConfig& config) {
return false; return false;
} }
if (!maybeUpdateResolvers(config)) {
return false;
}
// set routing // set routing
for (const IPAddress& ip : config.m_allowedIPAddressRanges) { for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
if (!wgutils()->updateRoutePrefix(ip)) { if (!wgutils()->updateRoutePrefix(ip)) {
@@ -215,6 +211,11 @@ bool Daemon::addExclusionRoute(const IPAddress& prefix) {
return true; return true;
} }
if (!wgutils()->addExclusionRoute(prefix)) { if (!wgutils()->addExclusionRoute(prefix)) {
if (!m_pendingExclusionRoutes.contains(prefix)) {
logger.warning() << "Exclusion route deferred (no gateway):"
<< prefix.toString();
m_pendingExclusionRoutes.append(prefix);
}
return false; return false;
} }
m_excludedAddrSet[prefix] = 1; m_excludedAddrSet[prefix] = 1;
@@ -480,6 +481,7 @@ bool Daemon::deactivate(bool emitSignals) {
wgutils()->deleteExclusionRoute(iterator.key()); wgutils()->deleteExclusionRoute(iterator.key());
} }
m_excludedAddrSet.clear(); m_excludedAddrSet.clear();
m_pendingExclusionRoutes.clear();
m_connections.clear(); m_connections.clear();
// Delete the interface // Delete the interface
@@ -589,6 +591,19 @@ void Daemon::checkHandshake() {
logger.debug() << "Checking for handshake..."; logger.debug() << "Checking for handshake...";
if (!m_pendingExclusionRoutes.isEmpty()) {
QList<IPAddress> stillPending;
for (const IPAddress& prefix : m_pendingExclusionRoutes) {
if (!wgutils()->addExclusionRoute(prefix)) {
stillPending.append(prefix);
} else {
logger.debug() << "Deferred exclusion route added:" << prefix.toString();
m_excludedAddrSet[prefix] = 1;
}
}
m_pendingExclusionRoutes = stillPending;
}
int pendingHandshakes = 0; int pendingHandshakes = 0;
QList<WireguardUtils::PeerStatus> peers = wgutils()->getPeerStatus(); QList<WireguardUtils::PeerStatus> peers = wgutils()->getPeerStatus();
for (ConnectionState& connection : m_connections) { for (ConnectionState& connection : m_connections) {
@@ -605,6 +620,7 @@ void Daemon::checkHandshake() {
} }
if (status.m_handshake != 0) { if (status.m_handshake != 0) {
connection.m_date.setMSecsSinceEpoch(status.m_handshake); connection.m_date.setMSecsSinceEpoch(status.m_handshake);
maybeUpdateResolvers(config);
emit connected(status.m_pubkey); emit connected(status.m_pubkey);
} }
} }
+1
View File
@@ -87,6 +87,7 @@ class Daemon : public QObject {
}; };
QMap<InterfaceConfig::HopType, ConnectionState> m_connections; QMap<InterfaceConfig::HopType, ConnectionState> m_connections;
QHash<IPAddress, int> m_excludedAddrSet; QHash<IPAddress, int> m_excludedAddrSet;
QList<IPAddress> m_pendingExclusionRoutes;
QTimer m_handshakeTimer; QTimer m_handshakeTimer;
}; };
@@ -101,7 +101,9 @@ bool AndroidController::initialize()
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)}, {"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}, {"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)}, {"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)} {"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
}; };
QJniEnvironment env; QJniEnvironment env;
@@ -558,3 +560,22 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp); emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
} }
// static
void AndroidController::onActivityPaused(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityPaused();
}
// static
void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityResumed();
}
@@ -75,6 +75,8 @@ signals:
void authenticationResult(bool result); void authenticationResult(bool result);
void imeInsetsChanged(int heightDp); void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp); void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused();
void activityResumed();
private: private:
bool isWaitingStatus = true; bool isWaitingStatus = true;
@@ -105,6 +107,8 @@ private:
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp); static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp); static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz);
template <typename Ret, typename ...Args> template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
@@ -126,8 +126,7 @@ extension PacketTunnelProvider {
} }
vpnReachability.startTracking { [weak self] status in vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return } self?.handleOpenVPNReachabilityChange(status)
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
} }
startHandler = completionHandler startHandler = completionHandler
@@ -21,6 +21,44 @@ extension Constants {
} }
extension PacketTunnelProvider { extension PacketTunnelProvider {
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
settings: NEPacketTunnelNetworkSettings) {
guard let splitTunnelType = xrayConfig.splitTunnelType else {
return
}
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
xrayLog(.error, message: "Split tunnel sites are not set")
return
}
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
for excludedIPString in splitTunnelSites {
if let excludedIP = IPAddressRange(from: excludedIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludedIP.address)",
subnetMask: "\(excludedIP.subnetMask())"))
}
}
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
func startXray(completionHandler: @escaping (Error?) -> Void) { func startXray(completionHandler: @escaping (Error?) -> Void) {
// Xray configuration // Xray configuration
@@ -72,6 +110,7 @@ extension PacketTunnelProvider {
settings.dnsSettings = !dnsArray.isEmpty settings.dnsSettings = !dnsArray.isEmpty
? NEDNSSettings(servers: dnsArray) ? NEDNSSettings(servers: dnsArray)
: NEDNSSettings(servers: ["1.1.1.1"]) : NEDNSSettings(servers: ["1.1.1.1"])
applyXraySplitTunnel(xrayConfig, settings: settings)
let xrayConfigData = xrayConfig.config.data(using: .utf8) let xrayConfigData = xrayConfig.config.data(using: .utf8)
+135 -9
View File
@@ -41,10 +41,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var ovpnAdapter: OpenVPNAdapter? var ovpnAdapter: OpenVPNAdapter?
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow) private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor") private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
private let pathMonitor = NWPathMonitor() private let pathMonitor = NWPathMonitor()
private var didReceiveInitialPathUpdate = false private var didReceiveInitialPathUpdate = false
private var currentPath: Network.NWPath? private var currentPath: Network.NWPath?
private var currentPathSignature: String? private var currentPathSignature: String?
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
private var isApplyingNetworkChange = false
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
var splitTunnelType: Int? var splitTunnelType: Int?
var splitTunnelSites: [String]? var splitTunnelSites: [String]?
@@ -78,14 +83,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard hasMeaningfulChange, let proto = self.protoType else { return } guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here. // WireGuard/AWG manages network changes internally in its own adapter.
if proto == .wireguard { if proto == .wireguard {
return return
} }
DispatchQueue.main.async { if proto == .openvpn {
self.handle(networkChange: path) { _ in } self.scheduleOpenVPNReconnect(reason: "NWPath changed")
return
} }
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
return
}
self.scheduleNetworkChangeHandling(for: proto, path: path)
} }
pathMonitor.start(queue: pathMonitorQueue) pathMonitor.start(queue: pathMonitorQueue)
@@ -197,6 +210,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return return
} }
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
didReceiveInitialPathUpdate = false didReceiveInitialPathUpdate = false
updateActiveInterfaceIndexForCurrentPath() updateActiveInterfaceIndexForCurrentPath()
@@ -215,6 +230,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
guard let protoType else { guard let protoType else {
completionHandler() completionHandler()
return return
@@ -259,9 +277,111 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} }
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) { private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
guard protoType == .xray else {
updateActiveInterfaceIndex(for: changePath)
completion(nil)
return
}
updateActiveInterfaceIndex(for: changePath) updateActiveInterfaceIndex(for: changePath)
wg_log(.info, message: "Tunnel restarted.") reasserting = true
startTunnel(options: nil, completionHandler: completion) xrayLog(.info, message: "Applying network change to xray tunnel")
stopXray { }
startXray { [weak self] error in
self?.reasserting = false
completion(error)
}
}
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
guard proto == .xray else { return }
pendingNetworkChangeWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingNetworkChangeWorkItem = nil
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
return
}
self.isApplyingNetworkChange = true
DispatchQueue.main.async {
self.handle(networkChange: path) { [weak self] _ in
self?.networkChangeQueue.async {
self?.isApplyingNetworkChange = false
}
}
}
}
pendingNetworkChangeWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
private func scheduleOpenVPNReconnect(reason: String) {
guard protoType == .openvpn else { return }
pendingOpenVPNReconnectWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingOpenVPNReconnectWorkItem = nil
guard self.protoType == .openvpn else { return }
if self.reasserting {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard !self.reasserting else {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
}
}
pendingOpenVPNReconnectWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
defer { lastOpenVPNReachabilityStatus = status }
guard let previousStatus = lastOpenVPNReachabilityStatus else {
return
}
guard previousStatus != status else {
return
}
switch status {
case .reachableViaWiFi, .reachableViaWWAN:
scheduleOpenVPNReconnect(reason: "Reachability changed")
default:
break
}
}
private func cancelPendingOpenVPNReconnect() {
pendingOpenVPNReconnectWorkItem?.cancel()
pendingOpenVPNReconnectWorkItem = nil
lastOpenVPNReachabilityStatus = nil
}
private func cancelPendingNetworkChangeHandling() {
pendingNetworkChangeWorkItem?.cancel()
pendingNetworkChangeWorkItem = nil
isApplyingNetworkChange = false
} }
} }
@@ -271,8 +391,14 @@ private extension PacketTunnelProvider {
signatureComponents.append(path.isExpensive ? "exp" : "noexp") signatureComponents.append(path.isExpensive ? "exp" : "noexp")
signatureComponents.append(path.isConstrained ? "con" : "nocon") signatureComponents.append(path.isConstrained ? "con" : "nocon")
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other] // Ignore loopback and tunnel-style `.other` interfaces so Xray does not
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in // react to its own utun lifecycle as if the physical uplink changed.
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
let externalInterfaces = path.availableInterfaces.filter { interface in
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
}
let sortedInterfaces = externalInterfaces.sorted { lhs, rhs in
if lhs.type == rhs.type { if lhs.type == rhs.type {
return lhs.index < rhs.index return lhs.index < rhs.index
} }
@@ -293,8 +419,8 @@ private extension PacketTunnelProvider {
case .wiredEthernet: typeName = "ethernet" case .wiredEthernet: typeName = "ethernet"
case .wifi: typeName = "wifi" case .wifi: typeName = "wifi"
case .cellular: typeName = "cellular" case .cellular: typeName = "cellular"
case .loopback: typeName = "loopback" case .loopback, .other:
case .other: typeName = "other" continue
@unknown default: typeName = "unknown" @unknown default: typeName = "unknown"
} }
signatureComponents.append("\(typeName):\(interface.index)") signatureComponents.append("\(typeName):\(interface.index)")
+2
View File
@@ -3,5 +3,7 @@ import Foundation
struct XrayConfig: Decodable { struct XrayConfig: Decodable {
let dns1: String? let dns1: String?
let dns2: String? let dns2: String?
let splitTunnelType: Int?
let splitTunnelSites: [String]?
let config: String let config: String
} }
+9
View File
@@ -684,6 +684,15 @@ bool IosController::setupXray()
QJsonObject finalConfig; QJsonObject finalConfig;
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString()); finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString()); finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
finalConfig.insert(config_key::config, xrayConfigStr); finalConfig.insert(config_key::config, xrayConfigStr);
QJsonDocument finalConfigDoc(finalConfig); QJsonDocument finalConfigDoc(finalConfig);
+127 -174
View File
@@ -4,20 +4,14 @@
#include "dnsutilslinux.h" #include "dnsutilslinux.h"
#include <net/if.h> #include <QDir>
#include <QFile>
#include <QDBusVariant> #include <QFileInfo>
#include <QtDBus/QtDBus> #include <QProcess>
#include "leakdetector.h" #include "leakdetector.h"
#include "logger.h" #include "logger.h"
constexpr const char* DBUS_RESOLVE_SERVICE = "org.freedesktop.resolve1";
constexpr const char* DBUS_RESOLVE_PATH = "/org/freedesktop/resolve1";
constexpr const char* DBUS_RESOLVE_MANAGER = "org.freedesktop.resolve1.Manager";
constexpr const char* DBUS_PROPERTY_INTERFACE =
"org.freedesktop.DBus.Properties";
namespace { namespace {
Logger logger("DnsUtilsLinux"); Logger logger("DnsUtilsLinux");
} }
@@ -25,188 +19,147 @@ Logger logger("DnsUtilsLinux");
DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) { DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) {
MZ_COUNT_CTOR(DnsUtilsLinux); MZ_COUNT_CTOR(DnsUtilsLinux);
logger.debug() << "DnsUtilsLinux created."; logger.debug() << "DnsUtilsLinux created.";
QDBusConnection conn = QDBusConnection::systemBus();
m_resolver = new QDBusInterface(DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH,
DBUS_RESOLVE_MANAGER, conn, this);
} }
DnsUtilsLinux::~DnsUtilsLinux() { DnsUtilsLinux::~DnsUtilsLinux() {
MZ_COUNT_DTOR(DnsUtilsLinux); MZ_COUNT_DTOR(DnsUtilsLinux);
for (auto iterator = m_linkDomains.constBegin();
iterator != m_linkDomains.constEnd(); ++iterator) {
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(iterator.key());
argumentList << QVariant::fromValue(iterator.value());
m_resolver->asyncCallWithArgumentList(QStringLiteral("SetLinkDomains"),
argumentList);
}
if (m_ifindex > 0) {
m_resolver->asyncCall(QStringLiteral("RevertLink"), m_ifindex);
}
logger.debug() << "DnsUtilsLinux destroyed."; logger.debug() << "DnsUtilsLinux destroyed.";
} }
void DnsUtilsLinux::writeResolvConf(const QList<QHostAddress>& resolvers) {
if (resolvers.isEmpty()) return;
static const QString kPath = QStringLiteral("/etc/resolv.conf");
if (m_resolvConfOriginal.isEmpty()) {
QFileInfo fi(kPath);
if (fi.isSymLink()) {
m_resolvConfOriginal = fi.symLinkTarget();
logger.debug() << "Saved resolv.conf symlink target:"
<< m_resolvConfOriginal;
if (!m_stateFilePath.isEmpty()) {
QFile sf(m_stateFilePath);
if (sf.open(QIODevice::WriteOnly | QIODevice::Text))
sf.write(m_resolvConfOriginal.toUtf8());
}
} else {
QFile orig(kPath);
if (orig.open(QIODevice::ReadOnly | QIODevice::Text)) {
QByteArray content = orig.readAll();
orig.close();
m_resolvConfOriginal = QStringLiteral("__file__");
logger.debug() << "resolv.conf is a regular file; saved content for restore";
if (!m_stateFilePath.isEmpty()) {
QFile sf(m_stateFilePath);
if (sf.open(QIODevice::WriteOnly | QIODevice::Text)) {
sf.write("__file__:");
sf.write(content);
}
}
} else {
m_resolvConfOriginal = QStringLiteral("__file__");
}
}
}
QFile::remove(kPath);
QFile f(kPath);
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
logger.warning() << "Failed to write" << kPath << ":" << f.errorString();
if (m_resolvConfOriginal != QStringLiteral("__file__"))
QFile::link(m_resolvConfOriginal, kPath);
m_resolvConfOriginal.clear();
if (!m_stateFilePath.isEmpty()) QFile::remove(m_stateFilePath);
return;
}
for (const auto& r : resolvers) {
if (r.protocol() == QAbstractSocket::IPv4Protocol)
f.write(
QStringLiteral("nameserver %1\n").arg(r.toString()).toUtf8());
}
f.close();
logger.debug() << "Wrote resolv.conf with" << resolvers.size()
<< "DNS servers";
}
void DnsUtilsLinux::restoreResolvConf() {
if (m_resolvConfOriginal.isEmpty()) return;
const QString original = m_resolvConfOriginal;
m_resolvConfOriginal.clear();
if (!m_stateFilePath.isEmpty())
QFile::remove(m_stateFilePath);
static const char* kPath = "/etc/resolv.conf";
QFile::remove(kPath);
if (original == QStringLiteral("__file__")) {
if (!m_resolvConfSavedContent.isEmpty()) {
QFile f(kPath);
if (f.open(QIODevice::WriteOnly | QIODevice::Text)) {
f.write(m_resolvConfSavedContent.toUtf8());
logger.debug() << "Restored resolv.conf from saved content";
}
m_resolvConfSavedContent.clear();
} else if (QFile::exists(QStringLiteral("/run/systemd/resolve/stub-resolv.conf"))) {
QFile::link(QStringLiteral("/run/systemd/resolve/stub-resolv.conf"), kPath);
logger.debug() << "Restored resolv.conf symlink to stub-resolv.conf";
}
} else {
QFile::link(original, kPath);
logger.debug() << "Restored resolv.conf symlink to" << original;
}
}
bool DnsUtilsLinux::updateResolvers(const QString& ifname, bool DnsUtilsLinux::updateResolvers(const QString& ifname,
const QList<QHostAddress>& resolvers) { const QList<QHostAddress>& resolvers) {
m_ifindex = if_nametoindex(qPrintable(ifname)); m_stateFilePath = QStringLiteral("/run/amnezia-dns-%1").arg(ifname);
if (m_ifindex <= 0) {
logger.error() << "Unable to resolve ifindex for" << ifname;
return false;
}
setLinkDNS(m_ifindex, resolvers); writeResolvConf(resolvers);
setLinkDefaultRoute(m_ifindex, true);
updateLinkDomains();
return true; return true;
} }
bool DnsUtilsLinux::restoreResolvers() { bool DnsUtilsLinux::restoreResolvers() {
for (auto iterator = m_linkDomains.constBegin();
iterator != m_linkDomains.constEnd(); ++iterator) { if (m_resolvConfOriginal.isEmpty()) {
setLinkDomains(iterator.key(), iterator.value()); QStringList candidates;
if (!m_stateFilePath.isEmpty()) {
candidates << m_stateFilePath;
} else {
QDir runDir(QStringLiteral("/run"));
for (const QString& name : runDir.entryList(
{QStringLiteral("amnezia-dns-*")}, QDir::Files)) {
candidates << runDir.filePath(name);
}
}
for (const QString& path : candidates) {
QFile sf(path);
if (sf.open(QIODevice::ReadOnly)) {
QByteArray data = sf.readAll();
sf.close();
m_stateFilePath = path;
if (data.startsWith("__file__:")) {
m_resolvConfOriginal = QStringLiteral("__file__");
m_resolvConfSavedContent = QString::fromUtf8(data.mid(9));
} else {
m_resolvConfOriginal = QString::fromUtf8(data).trimmed();
}
logger.debug() << "Recovered DNS original from" << path << ":"
<< m_resolvConfOriginal;
break;
}
}
} }
m_linkDomains.clear();
/* Revert the VPN interface's DNS configuration */ const bool hadDnsState = !m_resolvConfOriginal.isEmpty();
if (m_ifindex > 0) { restoreResolvConf();
QList<QVariant> argumentList = {QVariant::fromValue(m_ifindex)};
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
QStringLiteral("RevertLink"), argumentList);
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this); if (hadDnsState && QFile::exists(QStringLiteral("/run/systemd/resolve"))) {
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, QProcess::startDetached("systemctl", {"restart", "systemd-resolved"});
SLOT(dnsCallCompleted(QDBusPendingCallWatcher*)));
m_ifindex = 0;
} }
return true; return true;
} }
void DnsUtilsLinux::dnsCallCompleted(QDBusPendingCallWatcher* call) {
QDBusPendingReply<> reply = *call;
if (reply.isError()) {
logger.error() << "Error received from the DBus service";
}
delete call;
}
void DnsUtilsLinux::setLinkDNS(int ifindex,
const QList<QHostAddress>& resolvers) {
QList<DnsResolver> resolverList;
char ifnamebuf[IF_NAMESIZE];
const char* ifname = if_indextoname(ifindex, ifnamebuf);
for (const auto& ip : resolvers) {
resolverList.append(ip);
if (ifname) {
logger.debug() << "Adding DNS resolver" << ip.toString() << "via"
<< ifname;
}
}
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(ifindex);
argumentList << QVariant::fromValue(resolverList);
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
QStringLiteral("SetLinkDNS"), argumentList);
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this,
SLOT(dnsCallCompleted(QDBusPendingCallWatcher*)));
}
void DnsUtilsLinux::setLinkDomains(int ifindex,
const QList<DnsLinkDomain>& domains) {
char ifnamebuf[IF_NAMESIZE];
const char* ifname = if_indextoname(ifindex, ifnamebuf);
if (ifname) {
for (const auto& d : domains) {
// The DNS search domains often winds up revealing user's ISP which
// can correlate back to their location.
logger.debug() << "Setting DNS domain:" << logger.sensitive(d.domain)
<< "via" << ifname << (d.search ? "search" : "");
}
}
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(ifindex);
argumentList << QVariant::fromValue(domains);
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
QStringLiteral("SetLinkDomains"), argumentList);
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this,
SLOT(dnsCallCompleted(QDBusPendingCallWatcher*)));
}
void DnsUtilsLinux::setLinkDefaultRoute(int ifindex, bool enable) {
QList<QVariant> argumentList;
argumentList << QVariant::fromValue(ifindex);
argumentList << QVariant::fromValue(enable);
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
QStringLiteral("SetLinkDefaultRoute"), argumentList);
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this,
SLOT(dnsCallCompleted(QDBusPendingCallWatcher*)));
}
void DnsUtilsLinux::updateLinkDomains() {
/* Get the list of search domains, and remove any others that might conspire
* to satisfy DNS resolution. Unfortunately, this is a pain because Qt doesn't
* seem to be able to demarshall complex property types.
*/
QDBusMessage message = QDBusMessage::createMethodCall(
DBUS_RESOLVE_SERVICE, DBUS_RESOLVE_PATH, DBUS_PROPERTY_INTERFACE, "Get");
message << QString(DBUS_RESOLVE_MANAGER);
message << QString("Domains");
QDBusPendingReply<QVariant> reply =
m_resolver->connection().asyncCall(message);
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this,
SLOT(dnsDomainsReceived(QDBusPendingCallWatcher*)));
}
void DnsUtilsLinux::dnsDomainsReceived(QDBusPendingCallWatcher* call) {
QDBusPendingReply<QVariant> reply = *call;
if (reply.isError()) {
logger.error() << "Error retrieving the DNS domains from the DBus service";
delete call;
return;
}
/* Update the state of the DNS domains */
m_linkDomains.clear();
QDBusArgument args = qvariant_cast<QDBusArgument>(reply.value());
QList<DnsDomain> list = qdbus_cast<QList<DnsDomain>>(args);
for (const auto& d : list) {
if (d.ifindex == 0) {
continue;
}
m_linkDomains[d.ifindex].append(DnsLinkDomain(d.domain, d.search));
}
/* Drop any competing root search domains. */
DnsLinkDomain root = DnsLinkDomain(".", true);
for (auto iterator = m_linkDomains.constBegin();
iterator != m_linkDomains.constEnd(); ++iterator) {
if (!iterator.value().contains(root)) {
continue;
}
QList<DnsLinkDomain> newlist = iterator.value();
newlist.removeAll(root);
setLinkDomains(iterator.key(), newlist);
}
/* Add a root search domain for the new interface. */
QList<DnsLinkDomain> newlist = {root};
setLinkDomains(m_ifindex, newlist);
delete call;
}
static DnsMetatypeRegistrationProxy s_dnsMetatypeProxy;
+10 -15
View File
@@ -5,11 +5,11 @@
#ifndef DNSUTILSLINUX_H #ifndef DNSUTILSLINUX_H
#define DNSUTILSLINUX_H #define DNSUTILSLINUX_H
#include <QDBusInterface> #include <QHostAddress>
#include <QDBusPendingCallWatcher> #include <QList>
#include <QString>
#include "daemon/dnsutils.h" #include "daemon/dnsutils.h"
#include "dbustypeslinux.h"
class DnsUtilsLinux final : public DnsUtils { class DnsUtilsLinux final : public DnsUtils {
Q_OBJECT Q_OBJECT
@@ -23,19 +23,14 @@ class DnsUtilsLinux final : public DnsUtils {
bool restoreResolvers() override; bool restoreResolvers() override;
private: private:
void setLinkDNS(int ifindex, const QList<QHostAddress>& resolvers); void writeResolvConf(const QList<QHostAddress>& resolvers);
void setLinkDomains(int ifindex, const QList<DnsLinkDomain>& domains); void restoreResolvConf();
void setLinkDefaultRoute(int ifindex, bool enable);
void updateLinkDomains();
private slots:
void dnsCallCompleted(QDBusPendingCallWatcher*);
void dnsDomainsReceived(QDBusPendingCallWatcher*);
private: private:
int m_ifindex = 0;
QMap<int, DnsLinkDomainList> m_linkDomains; QString m_resolvConfOriginal;
QDBusInterface* m_resolver = nullptr; QString m_resolvConfSavedContent;
QString m_stateFilePath;
}; };
#endif // DNSUTILSLINUX_H #endif
+29 -16
View File
@@ -32,6 +32,7 @@
#include "linuxfirewall.h" #include "linuxfirewall.h"
#include "logger.h" #include "logger.h"
#include <QHostAddress>
#include <QProcess> #include <QProcess>
#define BRAND_CODE "amn" #define BRAND_CODE "amn"
@@ -108,7 +109,7 @@ int LinuxFirewall::linkChain(LinuxFirewall::IPVersion ip, const QString& chain,
// (we can't safely delete all rules at once since rule numbers change) // (we can't safely delete all rules at once since rule numbers change)
// TODO: occasionally this script results in warnings in logs "Bad rule (does a matching rule exist in the chain?)" - this happens when // TODO: occasionally this script results in warnings in logs "Bad rule (does a matching rule exist in the chain?)" - this happens when
// the e.g OUTPUT chain is empty but this script attempts to delete things from it anyway. It doesn't cause any problems, but we should still fix at some point.. // the e.g OUTPUT chain is empty but this script attempts to delete things from it anyway. It doesn't cause any problems, but we should still fix at some point..
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName)); return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs -r %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
} }
else else
return execute(QStringLiteral("if ! %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -A %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName)); return execute(QStringLiteral("if ! %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -A %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName));
@@ -192,12 +193,8 @@ QStringList LinuxFirewall::getDNSRules(const QStringList& servers)
QStringList result; QStringList result;
for (const QString& server : servers) for (const QString& server : servers)
{ {
result << QStringLiteral("-o amn0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server); result << QStringLiteral("-d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o amn0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server); result << QStringLiteral("-d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun2+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server);
result << QStringLiteral("-o tun2+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
} }
return result; return result;
} }
@@ -245,7 +242,7 @@ void LinuxFirewall::install()
QStringLiteral("-o lo+ -j ACCEPT"), QStringLiteral("-o lo+ -j ACCEPT"),
}); });
installAnchor(IPv4, QStringLiteral("320.allowDNS"), {}); installAnchor(Both, QStringLiteral("320.allowDNS"), {});
installAnchor(Both, QStringLiteral("310.blockDNS"), { installAnchor(Both, QStringLiteral("310.blockDNS"), {
QStringLiteral("-p udp --dport 53 -j REJECT"), QStringLiteral("-p udp --dport 53 -j REJECT"),
@@ -289,6 +286,7 @@ void LinuxFirewall::install()
installAnchor(Both, QStringLiteral("100.blockAll"), { installAnchor(Both, QStringLiteral("100.blockAll"), {
QStringLiteral("-j REJECT"), QStringLiteral("-j REJECT"),
}); });
installAnchor(Both, QStringLiteral("400.allowPIA"), {});
// NAT rules // NAT rules
installAnchor(Both, QStringLiteral("100.transIp"), { installAnchor(Both, QStringLiteral("100.transIp"), {
@@ -351,7 +349,7 @@ void LinuxFirewall::uninstall()
// Remove filter anchors // Remove filter anchors
uninstallAnchor(Both, QStringLiteral("000.allowLoopback")); uninstallAnchor(Both, QStringLiteral("000.allowLoopback"));
uninstallAnchor(Both, QStringLiteral("400.allowPIA")); uninstallAnchor(Both, QStringLiteral("400.allowPIA"));
uninstallAnchor(IPv4, QStringLiteral("320.allowDNS")); uninstallAnchor(Both, QStringLiteral("320.allowDNS"));
uninstallAnchor(Both, QStringLiteral("310.blockDNS")); uninstallAnchor(Both, QStringLiteral("310.blockDNS"));
uninstallAnchor(Both, QStringLiteral("300.allowLAN")); uninstallAnchor(Both, QStringLiteral("300.allowLAN"));
uninstallAnchor(Both, QStringLiteral("290.allowDHCP")); uninstallAnchor(Both, QStringLiteral("290.allowDHCP"));
@@ -372,6 +370,8 @@ void LinuxFirewall::uninstall()
teardownTrafficSplitting(); teardownTrafficSplitting();
anchorCallbacks.clear();
logger.debug() << "LinuxFirewall::uninstall() complete"; logger.debug() << "LinuxFirewall::uninstall() complete";
} }
@@ -445,12 +445,17 @@ void LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPVersion ip, const QString
void LinuxFirewall::updateDNSServers(const QStringList& servers) void LinuxFirewall::updateDNSServers(const QStringList& servers)
{ {
static QStringList existingServers {};
existingServers = servers;
execute(QStringLiteral("iptables -F %1.320.allowDNS").arg(kAnchorName)); execute(QStringLiteral("iptables -F %1.320.allowDNS").arg(kAnchorName));
for (const QString& rule : getDNSRules(servers)) execute(QStringLiteral("ip6tables -F %1.320.allowDNS").arg(kAnchorName));
execute(QStringLiteral("iptables -A %1.320.allowDNS %2").arg(kAnchorName, rule));
for (const QString& server : servers) {
if (server.isEmpty()) continue;
const QString cmd = (QHostAddress(server).protocol() == QAbstractSocket::IPv6Protocol)
? QStringLiteral("ip6tables")
: QStringLiteral("iptables");
execute(QStringLiteral("%1 -A %2.320.allowDNS -d %3 -p udp --dport 53 -j ACCEPT").arg(cmd, kAnchorName, server));
execute(QStringLiteral("%1 -A %2.320.allowDNS -d %3 -p tcp --dport 53 -j ACCEPT").arg(cmd, kAnchorName, server));
}
} }
void LinuxFirewall::updateAllowNets(const QStringList& servers) void LinuxFirewall::updateAllowNets(const QStringList& servers)
@@ -495,13 +500,20 @@ int LinuxFirewall::execute(const QString &command, bool ignoreErrors)
logger.debug() << "(" << exitCode << ") $ " << command; logger.debug() << "(" << exitCode << ") $ " << command;
if (!out.isEmpty()) if (!out.isEmpty())
logger.info() << out; logger.info() << out;
if (!err.isEmpty()) if (!err.isEmpty() && !ignoreErrors)
logger.warning() << err; logger.warning() << err;
return exitCode; return exitCode;
} }
void LinuxFirewall::setupTrafficSplitting() void LinuxFirewall::setupTrafficSplitting()
{ {
execute(QStringLiteral("grep -q '%1' /etc/iproute2/rt_tables || printf '200\\t%1\\n' >> /etc/iproute2/rt_tables").arg(kRtableName));
execute(QStringLiteral(
"if [ ! -d /sys/fs/cgroup/net_cls ] ; then "
"mkdir -p /sys/fs/cgroup/net_cls && "
"mount -t cgroup -o net_cls cgroup /sys/fs/cgroup/net_cls ; fi"));
auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/"; auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/";
logger.info() << "Should be setting up cgroup in" << cGroupDir << "for traffic splitting"; logger.info() << "Should be setting up cgroup in" << cGroupDir << "for traffic splitting";
execute(QStringLiteral("if [ ! -d %1 ] ; then mkdir %1 ; sleep 0.1 ; echo %2 > %1/net_cls.classid ; fi").arg(cGroupDir).arg(kCGroupId)); execute(QStringLiteral("if [ ! -d %1 ] ; then mkdir %1 ; sleep 0.1 ; echo %2 > %1/net_cls.classid ; fi").arg(cGroupDir).arg(kCGroupId));
@@ -513,6 +525,7 @@ void LinuxFirewall::teardownTrafficSplitting()
{ {
logger.info() << "Tearing down cgroup and routing rules"; logger.info() << "Tearing down cgroup and routing rules";
execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2> /dev/null ; fi").arg(kPacketTag, kRtableName)); execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2> /dev/null ; fi").arg(kPacketTag, kRtableName));
execute(QStringLiteral("ip route flush table %1").arg(kRtableName)); execute(QStringLiteral("ip route flush table %1").arg(kRtableName), true);
execute(QStringLiteral("ip route flush cache")); execute(QStringLiteral("ip route flush cache"));
execute(QStringLiteral("sed -i '/%1/d' /etc/iproute2/rt_tables").arg(kRtableName));
} }
@@ -164,8 +164,13 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
} }
if (rtm->rtm_type == RTN_THROW) { if (rtm->rtm_type == RTN_THROW) {
QString gateway = NetworkUtilities::getGatewayAndIface().first;
if (gateway.isEmpty()) {
logger.warning() << "No default gateway available, skipping exclusion route";
return false;
}
struct in_addr ip4; struct in_addr ip4;
inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().first.toUtf8(), &ip4); inet_pton(AF_INET, gateway.toUtf8(), &ip4);
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4)); nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0); nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
rtm->rtm_type = RTN_UNICAST; rtm->rtm_type = RTN_UNICAST;
@@ -44,6 +44,9 @@ void LinuxNetworkWatcher::initialize() {
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this, connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
&NetworkWatcherImpl::wakeup); &NetworkWatcherImpl::wakeup);
connect(m_worker, &LinuxNetworkWatcherWorker::networkChanged, this,
[this]() { emit networkChanged(""); });
// Let's wait a few seconds to allow the UI to be fully loaded and shown. // Let's wait a few seconds to allow the UI to be fully loaded and shown.
// This is not strictly needed, but it's better for user experience because // This is not strictly needed, but it's better for user experience because
// it makes the UI faster to appear, plus it gives a bit of delay between the // it makes the UI faster to appear, plus it gives a bit of delay between the
@@ -4,6 +4,7 @@
#include "linuxnetworkwatcherworker.h" #include "linuxnetworkwatcherworker.h"
#include <QTimer>
#include <QtDBus/QtDBus> #include <QtDBus/QtDBus>
#include "leakdetector.h" #include "leakdetector.h"
@@ -37,6 +38,7 @@
enum NMState { enum NMState {
NM_STATE_UNKNOWN = 0, NM_STATE_UNKNOWN = 0,
NM_STATE_ASLEEP = 10, NM_STATE_ASLEEP = 10,
NM_STATE_DISABLED = 10,
NM_STATE_DISCONNECTED = 20, NM_STATE_DISCONNECTED = 20,
NM_STATE_DISCONNECTING = 30, NM_STATE_DISCONNECTING = 30,
NM_STATE_CONNECTING = 40, NM_STATE_CONNECTING = 40,
@@ -122,6 +124,12 @@ void LinuxNetworkWatcherWorker::initialize() {
SLOT(propertyChanged(QString, QVariantMap, QStringList))); SLOT(propertyChanged(QString, QVariantMap, QStringList)));
} }
QVariant currentState = nm.property("State");
if (currentState.isValid()) {
m_previousNMState = currentState.toUInt();
logger.debug() << "Initial NM state:" << m_previousNMState;
}
QDBusConnection::systemBus().connect(DBUS_NETWORKMANAGER, QDBusConnection::systemBus().connect(DBUS_NETWORKMANAGER,
DBUS_NETWORKMANAGER_PATH, DBUS_NETWORKMANAGER_PATH,
DBUS_NETWORKMANAGER, DBUS_NETWORKMANAGER,
@@ -199,10 +207,13 @@ void LinuxNetworkWatcherWorker::checkDevices() {
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state) void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{ {
if (state == NM_STATE_ASLEEP) { logger.debug() << "NMStateChanged " << state;
emit wakeup();
}
logger.debug() << "NMStateChanged " << state; if (state == NM_STATE_ASLEEP || state == NM_STATE_DISABLED) {
} emit wakeup();
} else if (state >= NM_STATE_CONNECTED_SITE && m_previousNMState < NM_STATE_CONNECTED_SITE) {
emit networkChanged();
}
m_previousNMState = state;
}
@@ -24,6 +24,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
signals: signals:
void unsecuredNetwork(const QString& networkName, const QString& networkId); void unsecuredNetwork(const QString& networkName, const QString& networkId);
void wakeup(); void wakeup();
void networkChanged();
public slots: public slots:
void initialize(); void initialize();
@@ -34,10 +35,12 @@ class LinuxNetworkWatcherWorker final : public QObject {
void NMStateChanged(quint32 state); void NMStateChanged(quint32 state);
private: private:
// We collect the list of DBus wifi network device paths during the // We collect the list of DBus wifi network device paths during the
// initialization. When a property of them changes, we check if the access // initialization. When a property of them changes, we check if the access
// point is active and unsecure. // point is active and unsecure.
QStringList m_devicePaths; QStringList m_devicePaths;
quint32 m_previousNMState = 0;
}; };
#endif // LINUXNETWORKWATCHERWORKER_H #endif // LINUXNETWORKWATCHERWORKER_H
+1
View File
@@ -135,6 +135,7 @@
<file>ui/qml/Components/InstalledAppsDrawer.qml</file> <file>ui/qml/Components/InstalledAppsDrawer.qml</file>
<file>ui/qml/Components/QuestionDrawer.qml</file> <file>ui/qml/Components/QuestionDrawer.qml</file>
<file>ui/qml/Components/SelectLanguageDrawer.qml</file> <file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file> <file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file> <file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file> <file>ui/qml/Components/TransportProtoSelector.qml</file>
@@ -366,6 +366,8 @@ bool ApiConfigsController::fillAvailableServices()
{ {
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType(); apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first(); apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody; QByteArray responseBody;
@@ -447,7 +449,7 @@ bool ApiConfigsController::importService()
importSerivceFromAppStore(); importSerivceFromAppStore();
return true; return true;
} }
} else { } else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
importServiceFromGateway(); importServiceFromGateway();
return true; return true;
} }
@@ -721,6 +723,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
} }
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false); bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase); ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
@@ -747,6 +750,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(config_key::nameOverriddenByUser, true); newServerConfig.insert(config_key::nameOverriddenByUser, true);
} }
m_serversModel->editServer(newServerConfig, serverIndex); m_serversModel->editServer(newServerConfig, serverIndex);
if (wasSubscriptionExpired) {
emit subscriptionRefreshNeeded();
}
if (reloadServiceConfig) { if (reloadServiceConfig) {
emit reloadServerFromApiFinished(tr("API config reloaded")); emit reloadServerFromApiFinished(tr("API config reloaded"));
} else if (newCountryName.isEmpty()) { } else if (newCountryName.isEmpty()) {
@@ -756,7 +764,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
} }
return true; return true;
} else { } else {
emit errorOccurred(errorCode); if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
emit subscriptionExpiredOnServer();
} else {
emit errorOccurred(errorCode);
}
return false; return false;
} }
} }
@@ -43,6 +43,8 @@ public slots:
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
void subscriptionExpiredOnServer();
void subscriptionRefreshNeeded();
void installServerFromApiFinished(const QString &message); void installServerFromApiFinished(const QString &message);
void changeApiCountryFinished(const QString &message); void changeApiCountryFinished(const QString &message);
@@ -1,6 +1,7 @@
#include "apiSettingsController.h" #include "apiSettingsController.h"
#include <QEventLoop> #include <QEventLoop>
#include <QJsonDocument>
#include <QTimer> #include <QTimer>
#include "core/api/apiUtils.h" #include "core/api/apiUtils.h"
@@ -77,6 +78,13 @@ bool ApiSettingsController::getAccountInfo(bool reload)
QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object(); QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object();
m_apiAccountInfoModel->updateModel(accountInfo, serverConfig); m_apiAccountInfoModel->updateModel(accountInfo, serverConfig);
QString subscriptionEndDate = accountInfo.value(apiDefs::key::subscriptionEndDate).toString();
if (!subscriptionEndDate.isEmpty()) {
apiConfig.insert(apiDefs::key::subscriptionEndDate, subscriptionEndDate);
serverConfig.insert(configKey::apiConfig, apiConfig);
m_serversModel->editServer(serverConfig, processedIndex);
}
if (reload) { if (reload) {
updateApiCountryModel(); updateApiCountryModel();
updateApiDevicesModel(); updateApiDevicesModel();
@@ -85,6 +93,42 @@ bool ApiSettingsController::getAccountInfo(bool reload)
return true; return true;
} }
void ApiSettingsController::getRenewalLink()
{
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
QString url = responseJson.value("renewal_url").toString();
if (!url.isEmpty()) {
emit renewalLinkReceived(url);
}
});
}
void ApiSettingsController::updateApiCountryModel() void ApiSettingsController::updateApiCountryModel()
{ {
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
@@ -21,9 +21,11 @@ public slots:
bool getAccountInfo(bool reload); bool getAccountInfo(bool reload);
void updateApiCountryModel(); void updateApiCountryModel();
void updateApiDevicesModel(); void updateApiDevicesModel();
void getRenewalLink();
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
void renewalLinkReceived(const QString &url);
private: private:
QSharedPointer<ServersModel> m_serversModel; QSharedPointer<ServersModel> m_serversModel;
@@ -45,6 +45,8 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
emit safeAreaBottomMarginChanged(); emit safeAreaBottomMarginChanged();
emit safeAreaTopMarginChanged(); emit safeAreaTopMarginChanged();
}); });
connect(AndroidController::instance(), &AndroidController::activityPaused, this, &SettingsController::activityPaused);
connect(AndroidController::instance(), &AndroidController::activityResumed, this, &SettingsController::activityResumed);
#endif #endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv(); m_isDevModeEnabled = m_settings->isDevGatewayEnv();
@@ -141,6 +141,9 @@ signals:
void safeAreaTopMarginChanged(); void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged(); void safeAreaBottomMarginChanged();
void activityPaused();
void activityResumed();
void isHomeAdLabelVisibleChanged(bool visible); void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged(); void startMinimizedChanged();
+19 -2
View File
@@ -1,5 +1,6 @@
#include "apiAccountInfoModel.h" #include "apiAccountInfoModel.h"
#include <QDateTime>
#include <QJsonObject> #include <QJsonObject>
#include "core/api/apiUtils.h" #include "core/api/apiUtils.h"
@@ -32,7 +33,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
} }
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>") return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
: tr("Active"); : tr("<p><a style=\"color: #28c840;\">Active</a>");
} }
case EndDateRole: { case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
@@ -52,7 +53,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
} }
case IsComponentVisibleRole: { case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2 return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium; || m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
} }
case HasExpiredWorkerRole: { case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) { for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
@@ -73,6 +76,18 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
} }
return false; return false;
} }
case IsSubscriptionExpiredRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate);
}
case IsSubscriptionExpiringSoonRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
if (apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate)) return false;
QDateTime endDate = QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODateWithMs);
return endDate <= QDateTime::currentDateTimeUtc().addDays(10);
}
} }
return QVariant(); return QVariant();
@@ -164,6 +179,8 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[IsComponentVisibleRole] = "isComponentVisible"; roles[IsComponentVisibleRole] = "isComponentVisible";
roles[HasExpiredWorkerRole] = "hasExpiredWorker"; roles[HasExpiredWorkerRole] = "hasExpiredWorker";
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported"; roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
return roles; return roles;
} }
+3 -2
View File
@@ -19,7 +19,9 @@ public:
EndDateRole, EndDateRole,
IsComponentVisibleRole, IsComponentVisibleRole,
HasExpiredWorkerRole, HasExpiredWorkerRole,
IsProtocolSelectionSupportedRole IsProtocolSelectionSupportedRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole
}; };
explicit ApiAccountInfoModel(QObject *parent = nullptr); explicit ApiAccountInfoModel(QObject *parent = nullptr);
@@ -31,7 +33,6 @@ public:
public slots: public slots:
void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig); void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig);
QVariant data(const QString &roleString); QVariant data(const QString &roleString);
QJsonArray getAvailableCountries(); QJsonArray getAvailableCountries();
QJsonArray getIssuedConfigsInfo(); QJsonArray getIssuedConfigsInfo();
+5 -2
View File
@@ -41,6 +41,7 @@ namespace
{ {
constexpr char amneziaFree[] = "amnezia-free"; constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium"; constexpr char amneziaPremium[] = "amnezia-premium";
constexpr char amneziaTrial[] = "amnezia-trial";
} }
} }
@@ -69,7 +70,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
} }
case CardDescriptionRole: { case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed; auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) { if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
return apiServiceData.serviceInfo.cardDescription.arg(speed); return apiServiceData.serviceInfo.cardDescription.arg(speed);
} else if (serviceType == serviceType::amneziaFree) { } else if (serviceType == serviceType::amneziaFree) {
QString description = apiServiceData.serviceInfo.cardDescription; QString description = apiServiceData.serviceInfo.cardDescription;
@@ -124,8 +125,10 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
case OrderRole: { case OrderRole: {
if (serviceType == serviceType::amneziaPremium) { if (serviceType == serviceType::amneziaPremium) {
return 0; return 0;
} else if (serviceType == serviceType::amneziaFree) { } else if (serviceType == serviceType::amneziaTrial) {
return 1; return 1;
} else if (serviceType == serviceType::amneziaFree) {
return 2;
} }
} }
} }
+17
View File
@@ -179,6 +179,20 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
case AdEndpointRole: { case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString(); return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
} }
case IsSubscriptionExpiredRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
if (endDate.isEmpty()) return false;
return apiUtils::isSubscriptionExpired(endDate);
}
case IsSubscriptionExpiringSoonRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
if (endDate.isEmpty()) return false;
if (apiUtils::isSubscriptionExpired(endDate)) return false;
QDateTime endDateTime = QDateTime::fromString(endDate, Qt::ISODateWithMs);
return endDateTime <= QDateTime::currentDateTimeUtc().addDays(10);
}
} }
return QVariant(); return QVariant();
@@ -443,6 +457,9 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[AdDescriptionRole] = "adDescription"; roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint"; roles[AdEndpointRole] = "adEndpoint";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
return roles; return roles;
} }
+3
View File
@@ -52,6 +52,9 @@ public:
AdDescriptionRole, AdDescriptionRole,
AdEndpointRole, AdEndpointRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
HasAmneziaDns HasAmneziaDns
}; };
@@ -126,6 +126,18 @@ ListViewType {
} }
} }
CaptionTextType {
visible: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
Layout.fillWidth: true
Layout.leftMargin: 64
Layout.bottomMargin: 8
text: isSubscriptionExpired ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon.")
color: isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
wrapMode: Text.WordWrap
}
DividerType { DividerType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 0 Layout.leftMargin: 0
@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
DrawerType2 {
id: root
expandedStateContent: ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
onImplicitHeightChanged: {
root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
}
Item {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
implicitHeight: titleText.implicitHeight
Header2TextType {
id: titleText
anchors.left: parent.left
anchors.right: parent.right
text: qsTr("Amnezia Premium subscription has expired")
horizontalAlignment: Text.AlignLeft
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Renew your subscription to continue using VPN")
horizontalAlignment: Text.AlignLeft
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Renew")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
BasicButtonType {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
Layout.bottomMargin: 8
implicitHeight: 25
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.goldenApricot
text: qsTr("Support")
clickedFunc: function() {
root.closeTriggered()
PageController.goToPage(PageEnum.PageSettingsApiSupport)
}
}
}
}
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Style 1.0 import Style 1.0
@@ -37,6 +38,7 @@ Item {
property int borderFocusedWidth: 1 property int borderFocusedWidth: 1
property string rightImageColor: AmneziaStyle.color.paleGray property string rightImageColor: AmneziaStyle.color.paleGray
property string leftImageColor: ""
property bool descriptionOnTop: false property bool descriptionOnTop: false
property bool hideDescription: true property bool hideDescription: true
@@ -140,6 +142,14 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
source: leftImageSource source: leftImageSource
visible: leftImageColor === ""
}
ColorOverlay {
anchors.fill: leftImage
source: leftImage
color: leftImageColor
visible: leftImageColor !== ""
} }
} }
@@ -18,12 +18,23 @@ PageType {
id: root id: root
property var processedServer property var processedServer
property bool subscriptionExpired: false
property bool subscriptionExpiringSoon: false
function updateSubscriptionState() {
root.subscriptionExpired = ServersModel.getProcessedServerData("isSubscriptionExpired")
root.subscriptionExpiringSoon = ServersModel.getProcessedServerData("isSubscriptionExpiringSoon")
}
Component.onCompleted: {
root.updateSubscriptionState()
}
Connections { Connections {
target: ServersModel target: ServersModel
function onProcessedServerChanged() { function onProcessedServerChanged() {
root.processedServer = proxyServersModel.get(0) root.processedServer = proxyServersModel.get(0)
root.updateSubscriptionState()
} }
} }
@@ -76,12 +87,11 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.bottomMargin: 10 Layout.bottomMargin: 4
actionButtonImage: "qrc:/images/controls/settings.svg" actionButtonImage: "qrc:/images/controls/settings.svg"
headerText: root.processedServer.name headerText: root.processedServer.name
descriptionText: qsTr("Location for connection")
actionButtonFunction: function() { actionButtonFunction: function() {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
@@ -94,6 +104,50 @@ PageType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo) PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
} }
} }
CaptionTextType {
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 4
text: root.subscriptionExpired ? qsTr("Subscription expired") : qsTr("Subscription expiring soon")
color: root.subscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
}
BasicButtonType {
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 4
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
text: qsTr("Renew subscription")
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: (root.subscriptionExpired || root.subscriptionExpiringSoon) ? 8 : 4
Layout.bottomMargin: 8
text: qsTr("Location for connection")
color: AmneziaStyle.color.mutedGray
}
} }
delegate: ColumnLayout { delegate: ColumnLayout {
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Dialogs import QtQuick.Dialogs
import Qt5Compat.GraphicalEffects
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
@@ -52,6 +53,26 @@ PageType {
property var processedServer property var processedServer
property bool isSubscriptionExpired: false
property bool isSubscriptionExpiringSoon: false
function updateSubscriptionState() {
root.isSubscriptionExpired = ApiAccountInfoModel.data("isSubscriptionExpired")
root.isSubscriptionExpiringSoon = ApiAccountInfoModel.data("isSubscriptionExpiringSoon")
}
Component.onCompleted: {
root.updateSubscriptionState()
}
Connections {
target: ApiAccountInfoModel
function onModelReset() {
root.updateSubscriptionState()
}
}
Connections { Connections {
target: ServersModel target: ServersModel
@@ -108,12 +129,66 @@ PageType {
actionButtonImage: "qrc:/images/controls/edit-3.svg" actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer.name headerText: root.processedServer.name
descriptionText: ApiAccountInfoModel.data("serviceDescription")
actionButtonFunction: function() { actionButtonFunction: function() {
serverNameEditDrawer.openTriggered() serverNameEditDrawer.openTriggered()
} }
} }
Text {
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 4
text: root.isSubscriptionExpired
? qsTr("Subscription expired")
: qsTr("Subscription expiring soon")
color: root.isSubscriptionExpired
? AmneziaStyle.color.vibrantRed
: AmneziaStyle.color.goldenApricot
font.pixelSize: 14
font.weight: Font.Medium
wrapMode: Text.WordWrap
}
ParagraphTextType {
visible: ApiAccountInfoModel.data("serviceDescription") !== ""
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon ? 0 : 10
text: ApiAccountInfoModel.data("serviceDescription")
color: AmneziaStyle.color.mutedGray
}
BasicButtonType {
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 8
text: qsTr("Renew subscription")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
} }
delegate: ColumnLayout { delegate: ColumnLayout {
@@ -151,6 +226,54 @@ PageType {
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible") readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
Item {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
Layout.fillWidth: true
implicitHeight: renewRow.implicitHeight + 32
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: ApiSettingsController.getRenewalLink()
}
Row {
id: renewRow
anchors.centerIn: parent
spacing: 12
Item {
width: renewIcon.implicitWidth
height: renewIcon.implicitHeight
anchors.verticalCenter: parent.verticalCenter
Image {
id: renewIcon
source: "qrc:/images/controls/refresh-cw.svg"
}
ColorOverlay {
anchors.fill: renewIcon
source: renewIcon
color: AmneziaStyle.color.goldenApricot
}
}
Text {
text: qsTr("Renew subscription")
color: AmneziaStyle.color.goldenApricot
font.pixelSize: 18
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
DividerType {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
}
SwitcherType { SwitcherType {
id: switcher id: switcher
@@ -177,10 +300,14 @@ PageType {
} }
} }
DividerType {
visible: footer.isVisibleForAmneziaFree
}
WarningType { WarningType {
id: warning id: warning
Layout.topMargin: 32 Layout.topMargin: 24
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.fillWidth: true Layout.fillWidth: true
@@ -204,7 +331,7 @@ PageType {
id: vpnKey id: vpnKey
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: warning.visible ? 16 : 32 Layout.topMargin: warning.visible ? 16 : 0
visible: footer.isVisibleForAmneziaFree visible: footer.isVisibleForAmneziaFree
@@ -1,226 +1,226 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Dialogs import QtQuick.Dialogs
import PageEnum 1.0 import PageEnum 1.0
import Style 1.0 import Style 1.0
import "./" import "./"
import "../Controls2" import "../Controls2"
import "../Controls2/TextTypes" import "../Controls2/TextTypes"
import "../Config" import "../Config"
import "../Components" import "../Components"
PageType { PageType {
id: root id: root
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
listView.positionViewAtBeginning() listView.positionViewAtBeginning()
} }
} }
} }
ListViewType { ListViewType {
id: listView id: listView
anchors.top: backButton.bottom anchors.top: backButton.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
header: ColumnLayout { header: ColumnLayout {
width: listView.width width: listView.width
BaseHeaderType { BaseHeaderType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.bottomMargin: 32 Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name") headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription") descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
} }
} }
model: inputFields model: inputFields
spacing: 0 spacing: 0
delegate: ColumnLayout { delegate: ColumnLayout {
width: listView.width width: listView.width
LabelWithImageType { LabelWithImageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: 16 Layout.margins: 16
imageSource: imagePath imageSource: imagePath
leftText: lText leftText: lText
rightText: rText rightText: rText
visible: isVisible visible: isVisible
} }
} }
footer: ColumnLayout { footer: ColumnLayout {
width: listView.width width: listView.width
spacing: 0 spacing: 0
ParagraphTextType { ParagraphTextType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
onLinkActivated: function(link) { onLinkActivated: function(link) {
Qt.openUrlExternally(link) Qt.openUrlExternally(link)
} }
textFormat: Text.RichText textFormat: Text.RichText
text: { text: {
var text = ApiServicesModel.getSelectedServiceData("features") var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
} }
} }
ParagraphTextType { ParagraphTextType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium" visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray color: AmneziaStyle.color.mutedGray
font.pixelSize: 12 font.pixelSize: 12
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.") text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
} }
BasicButtonType { BasicButtonType {
id: continueButton id: continueButton
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 32 Layout.topMargin: 32
Layout.bottomMargin: 16 Layout.bottomMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect") text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : qsTr("Connect"))
clickedFunc: function() { clickedFunc: function() {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService() var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
if (!result) { if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint() var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint) Qt.openUrlExternally(endpoint)
PageController.closePage() PageController.closePage()
PageController.closePage() PageController.closePage()
} }
} }
} }
ParagraphTextType { ParagraphTextType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.bottomMargin: 32 Layout.bottomMargin: 32
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium" visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray color: AmneziaStyle.color.mutedGray
font.pixelSize: 12 font.pixelSize: 12
text: { text: {
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy") var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl) return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
} }
onLinkActivated: function(link) { onLinkActivated: function(link) {
Qt.openUrlExternally(link) Qt.openUrlExternally(link)
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
} }
} }
} }
} }
property list<QtObject> inputFields: [ property list<QtObject> inputFields: [
region, region,
price, price,
timeLimit, timeLimit,
speed, speed,
features features
] ]
QtObject { QtObject {
id: region id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg" readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region") readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region") readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true property bool isVisible: true
} }
QtObject { QtObject {
id: price id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg" readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price") readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price") readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true property bool isVisible: true
} }
QtObject { QtObject {
id: timeLimit id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg" readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period") readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit") readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== "" property bool isVisible: rText !== ""
} }
QtObject { QtObject {
id: speed id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg" readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed") readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed") readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true property bool isVisible: true
} }
QtObject { QtObject {
id: features id: features
readonly property string imagePath: "qrc:/images/controls/info.svg" readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features") readonly property string lText: qsTr("Features")
readonly property string rText: "" readonly property string rText: ""
property bool isVisible: true property bool isVisible: true
} }
} }
@@ -79,11 +79,23 @@ PageType {
} }
textField.onTextChanged: { textField.onTextChanged: {
if (headerText == qsTr("Password or SSH private key")) { if (headerText === qsTr("Password or SSH private key")) {
buttonImageSource = textField.text !== "" ? imageSource : "" buttonImageSource = textField.text !== "" ? imageSource : ""
} }
} }
} }
WarningType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: title === qsTr("Password or SSH private key")
backGroundColor: AmneziaStyle.color.translucentWhite
iconPath: "qrc:/images/controls/alert-circle.svg"
textString: qsTr("SSH key requirements: supported ED25519 or RSA in PEM. Paste the private key including BEGIN/END lines. If your key doesnt work, generate a compatible one.")
}
} }
footer: ColumnLayout { footer: ColumnLayout {
+42 -6
View File
@@ -23,17 +23,25 @@ Window {
if (Qt.application.state === Qt.ApplicationActive) { if (Qt.application.state === Qt.ApplicationActive) {
root.visible = true root.visible = true
refreshTimer.restart() refreshTimer.restart()
} else if (Qt.application.state === Qt.ApplicationSuspended) {
// Hide window to stop the Qt render loop and prevent
// eglSwapBuffers from being called on a lost EGL context.
// NOTE: Do NOT hide on ApplicationInactive that fires on any
// focus change (IME, notifications) and would blank the screen.
root.visible = false
} }
} }
} }
} }
// Hide the window immediately when Android Activity.onPause() fires so that
// Qt's render loop stops before the EGL surface is disconnected. This
// prevents "QRhiGles2: Failed to make context current" and the resulting
// black screen that appears after swiping home and returning.
Connections {
target: SettingsController
function onActivityPaused() {
if (Qt.platform.os === "android") root.visible = false
}
function onActivityResumed() {
if (Qt.platform.os === "android") root.visible = true
}
}
Timer { Timer {
id: refreshTimer id: refreshTimer
interval: 150 interval: 150
@@ -280,6 +288,34 @@ Window {
} }
} }
Item {
objectName: "subscriptionExpiredDrawerItem"
anchors.fill: parent
SubscriptionExpiredDrawer {
id: subscriptionExpiredDrawer
anchors.fill: parent
}
}
Connections {
target: ApiConfigsController
function onSubscriptionExpiredOnServer() {
subscriptionExpiredDrawer.openTriggered()
}
}
Connections {
target: ApiSettingsController
function onRenewalLinkReceived(url) {
Qt.openUrlExternally(url)
}
}
Item { Item {
objectName: "busyIndicatorItem" objectName: "busyIndicatorItem"
+46 -26
View File
@@ -57,11 +57,15 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled)
{ {
#ifdef AMNEZIA_DESKTOP #ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([enabled](QSharedPointer<IpcInterfaceReplica> iface){ IpcClient::withInterface([enabled](QSharedPointer<IpcInterfaceReplica> iface){
QRemoteObjectPendingReply<bool> reply = iface->refreshKillSwitch(enabled); auto reply = iface->refreshKillSwitch(enabled);
if (reply.waitForFinished() && reply.returnValue()) auto *watcher = new QRemoteObjectPendingCallWatcher(reply);
qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed"; QObject::connect(watcher, &QRemoteObjectPendingCallWatcher::finished, [watcher]() {
else if (watcher->returnValue().toBool())
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call"; qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed";
else
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to refresh killswitch";
watcher->deleteLater();
});
}); });
#endif #endif
} }
@@ -76,15 +80,19 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
case Vpn::ConnectionState::Connected: { case Vpn::ConnectionState::Connected: {
iface->resetIpStack(); iface->resetIpStack();
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
if (!ContainerProps::isAwgContainer(container) && if (!ContainerProps::isAwgContainer(container) &&
container != DockerContainer::WireGuard) { container != DockerContainer::WireGuard) {
auto flushDnsReply = iface->flushDns();
auto *flushDnsWatcher = new QRemoteObjectPendingCallWatcher(flushDnsReply, this);
QObject::connect(flushDnsWatcher, &QRemoteObjectPendingCallWatcher::finished, this,
[flushDnsWatcher]() {
if (flushDnsWatcher->returnValue().toBool())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
flushDnsWatcher->deleteLater();
});
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString(); QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString(); QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
@@ -109,17 +117,25 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
} break; } break;
case Vpn::ConnectionState::Disconnected: case Vpn::ConnectionState::Disconnected:
case Vpn::ConnectionState::Error: { case Vpn::ConnectionState::Error: {
auto flushDns = iface->flushDns(); auto flushDnsReply = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue()) auto *flushDnsWatcher = new QRemoteObjectPendingCallWatcher(flushDnsReply);
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS"; QObject::connect(flushDnsWatcher, &QRemoteObjectPendingCallWatcher::finished, [flushDnsWatcher]() {
else if (flushDnsWatcher->returnValue().toBool())
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS"; qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
flushDnsWatcher->deleteLater();
});
auto clearSavedRoutes = iface->clearSavedRoutes(); auto clearSavedRoutesReply = iface->clearSavedRoutes();
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue()) auto *clearRoutesWatcher = new QRemoteObjectPendingCallWatcher(clearSavedRoutesReply);
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes"; QObject::connect(clearRoutesWatcher, &QRemoteObjectPendingCallWatcher::finished, [clearRoutesWatcher]() {
else if (clearRoutesWatcher->returnValue().toBool())
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes"; qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
clearRoutesWatcher->deleteLater();
});
} break; } break;
default: default:
break; break;
@@ -182,8 +198,12 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode)
} }
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) { IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns(); auto reply = iface->flushDns();
if (reply.waitForFinished() || !reply.returnValue()) auto *w = new QRemoteObjectPendingCallWatcher(reply);
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS"; QObject::connect(w, &QRemoteObjectPendingCallWatcher::finished, [w]() {
if (!w->returnValue().toBool())
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
w->deleteLater();
});
}); });
break; break;
} }
@@ -219,8 +239,8 @@ ErrorCode VpnConnection::lastError() const
return m_vpnProtocol.data()->lastError(); return m_vpnProtocol.data()->lastError();
} }
void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, void VpnConnection::connectToVpn(int serverIndex, const amnezia::ServerCredentials &credentials,
const QJsonObject &vpnConfiguration) amnezia::DockerContainer container, const QJsonObject &vpnConfiguration)
{ {
qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is") qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is")
.arg(serverIndex) .arg(serverIndex)
+2 -1
View File
@@ -44,7 +44,8 @@ public:
#endif #endif
public slots: public slots:
void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration); void connectToVpn(int serverIndex, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, const QJsonObject &vpnConfiguration);
void reconnectToVpn(); void reconnectToVpn();
void disconnectFromVpn(); void disconnectFromVpn();
+11 -4
View File
@@ -53,7 +53,9 @@ bool KillSwitch::refresh(bool enabled)
#endif #endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
m_appSettigns->setValue("Conf/strictKillSwitchEnabled", enabled); if (!m_appSettigns.isNull()) {
m_appSettigns->setValue("Conf/strictKillSwitchEnabled", enabled);
}
#endif #endif
if (isStrictKillSwitchEnabled()) { if (isStrictKillSwitchEnabled()) {
@@ -70,6 +72,11 @@ bool KillSwitch::isStrictKillSwitchEnabled()
QSettings RegHLM("HKEY_LOCAL_MACHINE\\Software\\" + QString(ORGANIZATION_NAME) QSettings RegHLM("HKEY_LOCAL_MACHINE\\Software\\" + QString(ORGANIZATION_NAME)
+ "\\" + QString(APPLICATION_NAME), QSettings::NativeFormat); + "\\" + QString(APPLICATION_NAME), QSettings::NativeFormat);
return RegHLM.value("strictKillSwitchEnabled", false).toBool(); return RegHLM.value("strictKillSwitchEnabled", false).toBool();
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
if (m_appSettigns.isNull()) {
return false;
}
#endif #endif
return m_appSettigns->value("Conf/strictKillSwitchEnabled", false).toBool(); return m_appSettigns->value("Conf/strictKillSwitchEnabled", false).toBool();
} }
@@ -86,7 +93,7 @@ bool KillSwitch::disableKillSwitch() {
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), false);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), false);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), false);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("320.allowDNS"), false);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), false);
} else { } else {
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true);
@@ -98,7 +105,7 @@ bool KillSwitch::disableKillSwitch() {
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), false);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("320.allowDNS"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), false); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), false);
LinuxFirewall::uninstall(); LinuxFirewall::uninstall();
} }
@@ -335,7 +342,7 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
} }
LinuxFirewall::updateDNSServers(dnsServers); LinuxFirewall::updateDNSServers(dnsServers);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("320.allowDNS"), true);
LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true);
#endif #endif
+1 -1
View File
@@ -22,7 +22,7 @@ public:
bool isStrictKillSwitchEnabled(); bool isStrictKillSwitchEnabled();
private: private:
KillSwitch(QObject* parent) {}; explicit KillSwitch(QObject* parent) : QObject(parent) {}
QStringList m_allowedRanges; QStringList m_allowedRanges;
QSharedPointer<SecureQSettings> m_appSettigns; QSharedPointer<SecureQSettings> m_appSettigns;
+14 -19
View File
@@ -120,18 +120,20 @@ bool RouterLinux::routeDelete(const QString &ipWithSubnet, const QString &gw, co
((struct sockaddr_in *)&route.rt_gateway)->sin_family = AF_INET; ((struct sockaddr_in *)&route.rt_gateway)->sin_family = AF_INET;
((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr = inet_addr(gw.toStdString().c_str()); ((struct sockaddr_in *)&route.rt_gateway)->sin_addr.s_addr = inet_addr(gw.toStdString().c_str());
((struct sockaddr_in *)&route.rt_gateway)->sin_port = 0; ((struct sockaddr_in *)&route.rt_gateway)->sin_port = 0;
// set host rejecting // set host rejecting
((struct sockaddr_in *)&route.rt_dst)->sin_family = AF_INET; ((struct sockaddr_in *)&route.rt_dst)->sin_family = AF_INET;
((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr = inet_addr(ip.toStdString().c_str()); ((struct sockaddr_in *)&route.rt_dst)->sin_addr.s_addr = inet_addr(ip.toStdString().c_str());
((struct sockaddr_in *)&route.rt_dst)->sin_port = 0; ((struct sockaddr_in *)&route.rt_dst)->sin_port = 0;
// set mask // set mask
((struct sockaddr_in *)&route.rt_genmask)->sin_family = AF_INET; ((struct sockaddr_in *)&route.rt_genmask)->sin_family = AF_INET;
((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr = inet_addr(mask.toStdString().c_str()); ((struct sockaddr_in *)&route.rt_genmask)->sin_addr.s_addr = inet_addr(mask.toStdString().c_str());
((struct sockaddr_in *)&route.rt_genmask)->sin_port = 0; ((struct sockaddr_in *)&route.rt_genmask)->sin_port = 0;
route.rt_flags = RTF_UP | RTF_GATEWAY; route.rt_flags = RTF_UP | RTF_GATEWAY;
route.rt_metric = 0;
//route.rt_dev = "ens33"; //route.rt_dev = "ens33";
route.rt_metric = 0;
if (ioctl(sock, SIOCDELRT, &route) < 0) if (ioctl(sock, SIOCDELRT, &route) < 0)
{ {
@@ -155,36 +157,29 @@ bool RouterLinux::routeDeleteList(const QString &gw, const QStringList &ips)
bool RouterLinux::isServiceActive(const QString &serviceName) { bool RouterLinux::isServiceActive(const QString &serviceName) {
QProcess process; QProcess process;
process.start("systemctl", { "is-active", "--quiet", serviceName }); process.start("systemctl", { "is-active", "--quiet", serviceName });
process.waitForFinished(); if (!process.waitForFinished(2000)) {
process.kill();
process.waitForFinished(500);
}
return process.exitCode() == 0; return process.exitCode() == 0;
} }
bool RouterLinux::flushDns() bool RouterLinux::flushDns()
{ {
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
//check what the dns manager use //check what the dns manager use
if (isServiceActive("nscd.service")) { if (isServiceActive("nscd.service")) {
qDebug() << "Restarting nscd.service"; qDebug() << "Flushing nscd cache";
p.start("systemctl", { "restart", "nscd" }); QProcess::startDetached("nscd", { "--invalidate=hosts" });
return true;
} else if (isServiceActive("systemd-resolved.service")) { } else if (isServiceActive("systemd-resolved.service")) {
qDebug() << "Restarting systemd-resolved.service"; qDebug() << "Flushing systemd-resolved cache";
p.start("systemctl", { "restart", "systemd-resolved" }); QProcess::startDetached("resolvectl", { "flush-caches" });
qDebug().noquote() << "Flush dns requested (non-blocking)";
return true;
} else { } else {
qDebug() << "No suitable DNS manager found."; qDebug() << "No suitable DNS manager found.";
return false; return false;
} }
p.waitForFinished();
QByteArray output(p.readAll());
if (output.isEmpty())
qDebug().noquote() << "Flush dns completed";
else
qDebug().noquote() << "OUTPUT systemctl restart nscd/systemd-resolved: " + output;
return true;
} }
bool RouterLinux::createTun(const QString &dev, const QString &subnet) { bool RouterLinux::createTun(const QString &dev, const QString &subnet) {