mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-20 02:00:55 +07:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89b94d2588 | |||
| 9166e17a23 | |||
| 37d2b8716d | |||
| 92b168100a | |||
| 0f6db8a238 | |||
| 7c075625d2 | |||
| c6a40b09c9 | |||
| 560c4070b4 | |||
| f6806459fd | |||
| 646b1561f8 | |||
| 6d1e10a2e3 | |||
| af0d561c5c | |||
| 214b18f65f | |||
| bd554fb730 | |||
| 316e64122e | |||
| bf3d11e5c4 | |||
| 9a0222aee3 | |||
| f0f0f7c5be | |||
| 36b1a863bf | |||
| 4103c5bbcf | |||
| fa69da6d56 | |||
| aaf2c9ddeb | |||
| dbbc7119ec | |||
| c57162c4cc | |||
| 40e39895c9 | |||
| ec3ab2a03c | |||
| ddecfcad26 | |||
| 67bd880cdf |
+1
-1
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2117)
|
||||
set(APP_ANDROID_VERSION_CODE 2118)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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
|
||||
+1
-1
Submodule client/3rd-prebuilt updated: 568b8d720d...51bb4703a4
@@ -109,6 +109,16 @@ void AmneziaApplication::init()
|
||||
// install filter on main window
|
||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||
win->installEventFilter(this);
|
||||
#ifdef Q_OS_ANDROID
|
||||
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||
qWarning() << "Scene graph error (suppressed):" << msg;
|
||||
});
|
||||
// Keep graphics context alive across hide/show cycles to avoid
|
||||
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
win->show();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -296,9 +296,25 @@ class AmneziaActivity : QtActivity() {
|
||||
hasWindowFocus = hasFocus
|
||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||
|
||||
// Cancel pending operations if window loses focus
|
||||
if (!hasFocus) {
|
||||
// Cancel pending operations if window loses focus
|
||||
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)
|
||||
|
||||
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()
|
||||
isActivityResumed = false
|
||||
// Cancel all pending operations when activity pauses
|
||||
@@ -349,6 +372,9 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onResume()
|
||||
isActivityResumed = true
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
if (qtInitialized.isCompleted) {
|
||||
QtAndroidController.onActivityResumed()
|
||||
}
|
||||
|
||||
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
||||
val uri = pendingOpenFileUri!!
|
||||
@@ -816,7 +842,7 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun getFd(fileName: String): Int {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall {
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
pfd?.fd ?: -1
|
||||
|
||||
@@ -31,4 +31,7 @@ object QtAndroidController {
|
||||
|
||||
external fun onImeInsetsChanged(heightDp: Int)
|
||||
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
|
||||
|
||||
external fun onActivityPaused()
|
||||
external fun onActivityResumed()
|
||||
}
|
||||
@@ -10,8 +10,10 @@ namespace apiDefs
|
||||
AmneziaFreeV3,
|
||||
AmneziaPremiumV1,
|
||||
AmneziaPremiumV2,
|
||||
AmneziaTrialV2,
|
||||
SelfHosted,
|
||||
ExternalPremium
|
||||
ExternalPremium,
|
||||
ExternalTrial
|
||||
};
|
||||
|
||||
enum ConfigSource {
|
||||
@@ -32,6 +34,7 @@ namespace apiDefs
|
||||
constexpr QLatin1String stackType("stack_type");
|
||||
constexpr QLatin1String serviceType("service_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
|
||||
constexpr QLatin1String vpnKey("vpn_key");
|
||||
|
||||
@@ -58,18 +58,24 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
};
|
||||
case apiDefs::ConfigSource::AmneziaGateway: {
|
||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||
constexpr QLatin1String serviceTrial("amnezia-trial");
|
||||
constexpr QLatin1String serviceFree("amnezia-free");
|
||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||
|
||||
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
||||
|
||||
if (serviceType == servicePremium) {
|
||||
return apiDefs::ConfigType::AmneziaPremiumV2;
|
||||
} else if (serviceType == serviceTrial) {
|
||||
return apiDefs::ConfigType::AmneziaTrialV2;
|
||||
} else if (serviceType == serviceFree) {
|
||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||
} else if (serviceType == serviceExternalPremium) {
|
||||
return apiDefs::ConfigType::ExternalPremium;
|
||||
} else if (serviceType == serviceExternalTrial) {
|
||||
return apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
@@ -90,6 +96,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
const int httpStatusCodeNotImplemented = 501;
|
||||
const int httpStatusCodeUnprocessableEntity = 422;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
@@ -122,6 +129,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
} else if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
@@ -133,7 +142,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -177,7 +187,9 @@ QString apiUtils::getPremiumV1VpnKey(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 {};
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ void CoreController::initControllers()
|
||||
|
||||
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
|
||||
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_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace
|
||||
constexpr int httpStatusCodeConflict = 409;
|
||||
|
||||
constexpr int httpStatusCodeNotImplemented = 501;
|
||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
} else if (httpStatus == httpStatusCodeUnprocessableEntity) {
|
||||
return false;
|
||||
} else if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << replyError;
|
||||
return true;
|
||||
|
||||
@@ -292,117 +292,103 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
constexpr int BUFFER_SIZE = 100;
|
||||
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;
|
||||
static constexpr size_t BUFFER_SIZE = 8192;
|
||||
|
||||
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");
|
||||
return {};
|
||||
}
|
||||
|
||||
memset(msgbuf, 0, sizeof(msgbuf));
|
||||
memset(gateway_address, 0, sizeof(gateway_address));
|
||||
memset(interface, 0, sizeof(interface));
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
struct timeval tv { 1, 0 };
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
/* point the header and the msg structure pointers into the buffer */
|
||||
nlmsg = (struct nlmsghdr *)msgbuf;
|
||||
struct {
|
||||
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*/
|
||||
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) {
|
||||
if (send(sock, &req, req.hdr.nlmsg_len, 0) < 0) {
|
||||
perror("send failed");
|
||||
close(sock);
|
||||
return {};
|
||||
}
|
||||
|
||||
/* receive response */
|
||||
do
|
||||
{
|
||||
received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0);
|
||||
if (received_bytes < 0) {
|
||||
perror("Error in recv");
|
||||
return {};
|
||||
}
|
||||
char buffer[BUFFER_SIZE];
|
||||
size_t total_len = 0;
|
||||
bool done = false;
|
||||
|
||||
nlh = (struct nlmsghdr *) ptr;
|
||||
|
||||
/* Check if the header is valid */
|
||||
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)
|
||||
while (!done && total_len < BUFFER_SIZE) {
|
||||
ssize_t n = recv(sock, buffer + total_len, BUFFER_SIZE - total_len, 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
else {
|
||||
ptr += received_bytes;
|
||||
msg_len += received_bytes;
|
||||
}
|
||||
|
||||
/* Break if its not a multi part message */
|
||||
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
break;
|
||||
}
|
||||
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))
|
||||
int scan_len = static_cast<int>(n);
|
||||
for (struct nlmsghdr *h = reinterpret_cast<struct nlmsghdr *>(buffer + total_len);
|
||||
NLMSG_OK(h, scan_len);
|
||||
h = NLMSG_NEXT(h, scan_len))
|
||||
{
|
||||
switch(route_attribute->rta_type) {
|
||||
case RTA_OIF:
|
||||
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:
|
||||
if (h->nlmsg_type == NLMSG_DONE || h->nlmsg_type == NLMSG_ERROR) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((*gateway_address) && (*interface)) {
|
||||
qDebug() << "Gateway " << gateway_address << " for interface " << interface;
|
||||
break;
|
||||
}
|
||||
total_len += static_cast<size_t>(n);
|
||||
}
|
||||
|
||||
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);
|
||||
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||
return { resultGw, QNetworkInterface::interfaceFromName(resultIf) };
|
||||
#endif
|
||||
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
QString gateway;
|
||||
|
||||
@@ -142,10 +142,6 @@ bool Daemon::activate(const InterfaceConfig& config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!maybeUpdateResolvers(config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// set routing
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
if (!wgutils()->updateRoutePrefix(ip)) {
|
||||
@@ -215,6 +211,11 @@ bool Daemon::addExclusionRoute(const IPAddress& prefix) {
|
||||
return true;
|
||||
}
|
||||
if (!wgutils()->addExclusionRoute(prefix)) {
|
||||
if (!m_pendingExclusionRoutes.contains(prefix)) {
|
||||
logger.warning() << "Exclusion route deferred (no gateway):"
|
||||
<< prefix.toString();
|
||||
m_pendingExclusionRoutes.append(prefix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
m_excludedAddrSet[prefix] = 1;
|
||||
@@ -480,6 +481,7 @@ bool Daemon::deactivate(bool emitSignals) {
|
||||
wgutils()->deleteExclusionRoute(iterator.key());
|
||||
}
|
||||
m_excludedAddrSet.clear();
|
||||
m_pendingExclusionRoutes.clear();
|
||||
|
||||
m_connections.clear();
|
||||
// Delete the interface
|
||||
@@ -589,6 +591,19 @@ void Daemon::checkHandshake() {
|
||||
|
||||
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;
|
||||
QList<WireguardUtils::PeerStatus> peers = wgutils()->getPeerStatus();
|
||||
for (ConnectionState& connection : m_connections) {
|
||||
@@ -605,6 +620,7 @@ void Daemon::checkHandshake() {
|
||||
}
|
||||
if (status.m_handshake != 0) {
|
||||
connection.m_date.setMSecsSinceEpoch(status.m_handshake);
|
||||
maybeUpdateResolvers(config);
|
||||
emit connected(status.m_pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ class Daemon : public QObject {
|
||||
};
|
||||
QMap<InterfaceConfig::HopType, ConnectionState> m_connections;
|
||||
QHash<IPAddress, int> m_excludedAddrSet;
|
||||
QList<IPAddress> m_pendingExclusionRoutes;
|
||||
QTimer m_handshakeTimer;
|
||||
};
|
||||
|
||||
|
||||
@@ -101,7 +101,9 @@ bool AndroidController::initialize()
|
||||
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
|
||||
{"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;
|
||||
@@ -558,3 +560,22 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
|
||||
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 imeInsetsChanged(int heightDp);
|
||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||
void activityPaused();
|
||||
void activityResumed();
|
||||
|
||||
private:
|
||||
bool isWaitingStatus = true;
|
||||
@@ -105,6 +107,8 @@ private:
|
||||
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
||||
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
|
||||
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>
|
||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||
|
||||
@@ -126,8 +126,7 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
|
||||
vpnReachability.startTracking { [weak self] status in
|
||||
guard status == .reachableViaWiFi else { return }
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
||||
self?.handleOpenVPNReachabilityChange(status)
|
||||
}
|
||||
|
||||
startHandler = completionHandler
|
||||
|
||||
@@ -21,6 +21,44 @@ extension Constants {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
// Xray configuration
|
||||
@@ -72,6 +110,7 @@ extension PacketTunnelProvider {
|
||||
settings.dnsSettings = !dnsArray.isEmpty
|
||||
? NEDNSSettings(servers: dnsArray)
|
||||
: NEDNSSettings(servers: ["1.1.1.1"])
|
||||
applyXraySplitTunnel(xrayConfig, settings: settings)
|
||||
|
||||
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
||||
|
||||
|
||||
@@ -41,10 +41,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var ovpnAdapter: OpenVPNAdapter?
|
||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
||||
private let pathMonitor = NWPathMonitor()
|
||||
private var didReceiveInitialPathUpdate = false
|
||||
private var currentPath: Network.NWPath?
|
||||
private var currentPathSignature: String?
|
||||
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
|
||||
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
||||
private var isApplyingNetworkChange = false
|
||||
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
|
||||
|
||||
var splitTunnelType: Int?
|
||||
var splitTunnelSites: [String]?
|
||||
@@ -78,14 +83,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { _ in }
|
||||
if proto == .openvpn {
|
||||
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)
|
||||
|
||||
@@ -197,6 +210,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
cancelPendingOpenVPNReconnect()
|
||||
cancelPendingNetworkChangeHandling()
|
||||
didReceiveInitialPathUpdate = false
|
||||
updateActiveInterfaceIndexForCurrentPath()
|
||||
|
||||
@@ -215,6 +230,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
cancelPendingOpenVPNReconnect()
|
||||
cancelPendingNetworkChangeHandling()
|
||||
|
||||
guard let protoType else {
|
||||
completionHandler()
|
||||
return
|
||||
@@ -259,9 +277,111 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||
guard protoType == .xray else {
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
wg_log(.info, message: "Tunnel restarted.")
|
||||
startTunnel(options: nil, completionHandler: completion)
|
||||
reasserting = true
|
||||
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.isConstrained ? "con" : "nocon")
|
||||
|
||||
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
|
||||
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
|
||||
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not
|
||||
// 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 {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
@@ -293,8 +419,8 @@ private extension PacketTunnelProvider {
|
||||
case .wiredEthernet: typeName = "ethernet"
|
||||
case .wifi: typeName = "wifi"
|
||||
case .cellular: typeName = "cellular"
|
||||
case .loopback: typeName = "loopback"
|
||||
case .other: typeName = "other"
|
||||
case .loopback, .other:
|
||||
continue
|
||||
@unknown default: typeName = "unknown"
|
||||
}
|
||||
signatureComponents.append("\(typeName):\(interface.index)")
|
||||
|
||||
@@ -3,5 +3,7 @@ import Foundation
|
||||
struct XrayConfig: Decodable {
|
||||
let dns1: String?
|
||||
let dns2: String?
|
||||
let splitTunnelType: Int?
|
||||
let splitTunnelSites: [String]?
|
||||
let config: String
|
||||
}
|
||||
|
||||
@@ -684,6 +684,15 @@ bool IosController::setupXray()
|
||||
QJsonObject finalConfig;
|
||||
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::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);
|
||||
|
||||
QJsonDocument finalConfigDoc(finalConfig);
|
||||
|
||||
@@ -4,20 +4,14 @@
|
||||
|
||||
#include "dnsutilslinux.h"
|
||||
|
||||
#include <net/if.h>
|
||||
|
||||
#include <QDBusVariant>
|
||||
#include <QtDBus/QtDBus>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
|
||||
#include "leakdetector.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 {
|
||||
Logger logger("DnsUtilsLinux");
|
||||
}
|
||||
@@ -25,188 +19,147 @@ Logger logger("DnsUtilsLinux");
|
||||
DnsUtilsLinux::DnsUtilsLinux(QObject* parent) : DnsUtils(parent) {
|
||||
MZ_COUNT_CTOR(DnsUtilsLinux);
|
||||
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() {
|
||||
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.";
|
||||
}
|
||||
|
||||
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,
|
||||
const QList<QHostAddress>& resolvers) {
|
||||
m_ifindex = if_nametoindex(qPrintable(ifname));
|
||||
if (m_ifindex <= 0) {
|
||||
logger.error() << "Unable to resolve ifindex for" << ifname;
|
||||
return false;
|
||||
}
|
||||
m_stateFilePath = QStringLiteral("/run/amnezia-dns-%1").arg(ifname);
|
||||
|
||||
setLinkDNS(m_ifindex, resolvers);
|
||||
setLinkDefaultRoute(m_ifindex, true);
|
||||
updateLinkDomains();
|
||||
writeResolvConf(resolvers);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DnsUtilsLinux::restoreResolvers() {
|
||||
for (auto iterator = m_linkDomains.constBegin();
|
||||
iterator != m_linkDomains.constEnd(); ++iterator) {
|
||||
setLinkDomains(iterator.key(), iterator.value());
|
||||
|
||||
if (m_resolvConfOriginal.isEmpty()) {
|
||||
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 */
|
||||
if (m_ifindex > 0) {
|
||||
QList<QVariant> argumentList = {QVariant::fromValue(m_ifindex)};
|
||||
QDBusPendingReply<> reply = m_resolver->asyncCallWithArgumentList(
|
||||
QStringLiteral("RevertLink"), argumentList);
|
||||
const bool hadDnsState = !m_resolvConfOriginal.isEmpty();
|
||||
restoreResolvConf();
|
||||
|
||||
QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this,
|
||||
SLOT(dnsCallCompleted(QDBusPendingCallWatcher*)));
|
||||
|
||||
m_ifindex = 0;
|
||||
if (hadDnsState && QFile::exists(QStringLiteral("/run/systemd/resolve"))) {
|
||||
QProcess::startDetached("systemctl", {"restart", "systemd-resolved"});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
#ifndef DNSUTILSLINUX_H
|
||||
#define DNSUTILSLINUX_H
|
||||
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QHostAddress>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
#include "daemon/dnsutils.h"
|
||||
#include "dbustypeslinux.h"
|
||||
|
||||
class DnsUtilsLinux final : public DnsUtils {
|
||||
Q_OBJECT
|
||||
@@ -23,19 +23,14 @@ class DnsUtilsLinux final : public DnsUtils {
|
||||
bool restoreResolvers() override;
|
||||
|
||||
private:
|
||||
void setLinkDNS(int ifindex, const QList<QHostAddress>& resolvers);
|
||||
void setLinkDomains(int ifindex, const QList<DnsLinkDomain>& domains);
|
||||
void setLinkDefaultRoute(int ifindex, bool enable);
|
||||
void updateLinkDomains();
|
||||
|
||||
private slots:
|
||||
void dnsCallCompleted(QDBusPendingCallWatcher*);
|
||||
void dnsDomainsReceived(QDBusPendingCallWatcher*);
|
||||
void writeResolvConf(const QList<QHostAddress>& resolvers);
|
||||
void restoreResolvConf();
|
||||
|
||||
private:
|
||||
int m_ifindex = 0;
|
||||
QMap<int, DnsLinkDomainList> m_linkDomains;
|
||||
QDBusInterface* m_resolver = nullptr;
|
||||
|
||||
QString m_resolvConfOriginal;
|
||||
QString m_resolvConfSavedContent;
|
||||
QString m_stateFilePath;
|
||||
};
|
||||
|
||||
#endif // DNSUTILSLINUX_H
|
||||
#endif
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "linuxfirewall.h"
|
||||
#include "logger.h"
|
||||
#include <QHostAddress>
|
||||
#include <QProcess>
|
||||
|
||||
#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)
|
||||
// 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..
|
||||
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
|
||||
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;
|
||||
for (const QString& server : servers)
|
||||
{
|
||||
result << QStringLiteral("-o amn0+ -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("-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);
|
||||
result << QStringLiteral("-d %1 -p udp --dport 53 -j ACCEPT").arg(server);
|
||||
result << QStringLiteral("-d %1 -p tcp --dport 53 -j ACCEPT").arg(server);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -245,7 +242,7 @@ void LinuxFirewall::install()
|
||||
QStringLiteral("-o lo+ -j ACCEPT"),
|
||||
});
|
||||
|
||||
installAnchor(IPv4, QStringLiteral("320.allowDNS"), {});
|
||||
installAnchor(Both, QStringLiteral("320.allowDNS"), {});
|
||||
|
||||
installAnchor(Both, QStringLiteral("310.blockDNS"), {
|
||||
QStringLiteral("-p udp --dport 53 -j REJECT"),
|
||||
@@ -289,6 +286,7 @@ void LinuxFirewall::install()
|
||||
installAnchor(Both, QStringLiteral("100.blockAll"), {
|
||||
QStringLiteral("-j REJECT"),
|
||||
});
|
||||
installAnchor(Both, QStringLiteral("400.allowPIA"), {});
|
||||
// NAT rules
|
||||
installAnchor(Both, QStringLiteral("100.transIp"), {
|
||||
|
||||
@@ -351,7 +349,7 @@ void LinuxFirewall::uninstall()
|
||||
// Remove filter anchors
|
||||
uninstallAnchor(Both, QStringLiteral("000.allowLoopback"));
|
||||
uninstallAnchor(Both, QStringLiteral("400.allowPIA"));
|
||||
uninstallAnchor(IPv4, QStringLiteral("320.allowDNS"));
|
||||
uninstallAnchor(Both, QStringLiteral("320.allowDNS"));
|
||||
uninstallAnchor(Both, QStringLiteral("310.blockDNS"));
|
||||
uninstallAnchor(Both, QStringLiteral("300.allowLAN"));
|
||||
uninstallAnchor(Both, QStringLiteral("290.allowDHCP"));
|
||||
@@ -372,6 +370,8 @@ void LinuxFirewall::uninstall()
|
||||
|
||||
teardownTrafficSplitting();
|
||||
|
||||
anchorCallbacks.clear();
|
||||
|
||||
logger.debug() << "LinuxFirewall::uninstall() complete";
|
||||
}
|
||||
|
||||
@@ -445,12 +445,17 @@ void LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPVersion ip, const QString
|
||||
|
||||
void LinuxFirewall::updateDNSServers(const QStringList& servers)
|
||||
{
|
||||
static QStringList existingServers {};
|
||||
|
||||
existingServers = servers;
|
||||
execute(QStringLiteral("iptables -F %1.320.allowDNS").arg(kAnchorName));
|
||||
for (const QString& rule : getDNSRules(servers))
|
||||
execute(QStringLiteral("iptables -A %1.320.allowDNS %2").arg(kAnchorName, rule));
|
||||
execute(QStringLiteral("ip6tables -F %1.320.allowDNS").arg(kAnchorName));
|
||||
|
||||
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)
|
||||
@@ -495,13 +500,20 @@ int LinuxFirewall::execute(const QString &command, bool ignoreErrors)
|
||||
logger.debug() << "(" << exitCode << ") $ " << command;
|
||||
if (!out.isEmpty())
|
||||
logger.info() << out;
|
||||
if (!err.isEmpty())
|
||||
if (!err.isEmpty() && !ignoreErrors)
|
||||
logger.warning() << err;
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
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/";
|
||||
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));
|
||||
@@ -513,6 +525,7 @@ void LinuxFirewall::teardownTrafficSplitting()
|
||||
{
|
||||
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("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("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) {
|
||||
QString gateway = NetworkUtilities::getGatewayAndIface().first;
|
||||
if (gateway.isEmpty()) {
|
||||
logger.warning() << "No default gateway available, skipping exclusion route";
|
||||
return false;
|
||||
}
|
||||
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_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
|
||||
rtm->rtm_type = RTN_UNICAST;
|
||||
|
||||
@@ -44,6 +44,9 @@ void LinuxNetworkWatcher::initialize() {
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
|
||||
&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.
|
||||
// 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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "linuxnetworkwatcherworker.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QtDBus/QtDBus>
|
||||
|
||||
#include "leakdetector.h"
|
||||
@@ -37,6 +38,7 @@
|
||||
enum NMState {
|
||||
NM_STATE_UNKNOWN = 0,
|
||||
NM_STATE_ASLEEP = 10,
|
||||
NM_STATE_DISABLED = 10,
|
||||
NM_STATE_DISCONNECTED = 20,
|
||||
NM_STATE_DISCONNECTING = 30,
|
||||
NM_STATE_CONNECTING = 40,
|
||||
@@ -122,6 +124,12 @@ void LinuxNetworkWatcherWorker::initialize() {
|
||||
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,
|
||||
DBUS_NETWORKMANAGER_PATH,
|
||||
DBUS_NETWORKMANAGER,
|
||||
@@ -199,10 +207,13 @@ void LinuxNetworkWatcherWorker::checkDevices() {
|
||||
|
||||
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
||||
{
|
||||
if (state == NM_STATE_ASLEEP) {
|
||||
emit wakeup();
|
||||
}
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
|
||||
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:
|
||||
void unsecuredNetwork(const QString& networkName, const QString& networkId);
|
||||
void wakeup();
|
||||
void networkChanged();
|
||||
|
||||
public slots:
|
||||
void initialize();
|
||||
@@ -34,10 +35,12 @@ class LinuxNetworkWatcherWorker final : public QObject {
|
||||
void NMStateChanged(quint32 state);
|
||||
|
||||
private:
|
||||
|
||||
// We collect the list of DBus wifi network device paths during the
|
||||
// initialization. When a property of them changes, we check if the access
|
||||
// point is active and unsecure.
|
||||
QStringList m_devicePaths;
|
||||
quint32 m_previousNMState = 0;
|
||||
};
|
||||
|
||||
#endif // LINUXNETWORKWATCHERWORKER_H
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
||||
<file>ui/qml/Components/QuestionDrawer.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/SettingsContainersListView.qml</file>
|
||||
<file>ui/qml/Components/TransportProtoSelector.qml</file>
|
||||
|
||||
@@ -366,6 +366,8 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
{
|
||||
QJsonObject apiPayload;
|
||||
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();
|
||||
|
||||
QByteArray responseBody;
|
||||
@@ -447,7 +449,7 @@ bool ApiConfigsController::importService()
|
||||
importSerivceFromAppStore();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
||||
importServiceFromGateway();
|
||||
return true;
|
||||
}
|
||||
@@ -721,6 +723,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
||||
}
|
||||
|
||||
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
|
||||
QByteArray responseBody;
|
||||
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);
|
||||
}
|
||||
m_serversModel->editServer(newServerConfig, serverIndex);
|
||||
|
||||
if (wasSubscriptionExpired) {
|
||||
emit subscriptionRefreshNeeded();
|
||||
}
|
||||
|
||||
if (reloadServiceConfig) {
|
||||
emit reloadServerFromApiFinished(tr("API config reloaded"));
|
||||
} else if (newCountryName.isEmpty()) {
|
||||
@@ -756,7 +764,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
|
||||
emit subscriptionExpiredOnServer();
|
||||
} else {
|
||||
emit errorOccurred(errorCode);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ public slots:
|
||||
|
||||
signals:
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
void subscriptionExpiredOnServer();
|
||||
void subscriptionRefreshNeeded();
|
||||
|
||||
void installServerFromApiFinished(const QString &message);
|
||||
void changeApiCountryFinished(const QString &message);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "apiSettingsController.h"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/api/apiUtils.h"
|
||||
@@ -77,6 +78,13 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object();
|
||||
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) {
|
||||
updateApiCountryModel();
|
||||
updateApiDevicesModel();
|
||||
@@ -85,6 +93,42 @@ bool ApiSettingsController::getAccountInfo(bool reload)
|
||||
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()
|
||||
{
|
||||
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
|
||||
|
||||
@@ -21,9 +21,11 @@ public slots:
|
||||
bool getAccountInfo(bool reload);
|
||||
void updateApiCountryModel();
|
||||
void updateApiDevicesModel();
|
||||
void getRenewalLink();
|
||||
|
||||
signals:
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
void renewalLinkReceived(const QString &url);
|
||||
|
||||
private:
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
|
||||
@@ -45,6 +45,8 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
|
||||
emit safeAreaBottomMarginChanged();
|
||||
emit safeAreaTopMarginChanged();
|
||||
});
|
||||
connect(AndroidController::instance(), &AndroidController::activityPaused, this, &SettingsController::activityPaused);
|
||||
connect(AndroidController::instance(), &AndroidController::activityResumed, this, &SettingsController::activityResumed);
|
||||
#endif
|
||||
|
||||
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
|
||||
|
||||
@@ -141,6 +141,9 @@ signals:
|
||||
void safeAreaTopMarginChanged();
|
||||
void safeAreaBottomMarginChanged();
|
||||
|
||||
void activityPaused();
|
||||
void activityResumed();
|
||||
|
||||
void isHomeAdLabelVisibleChanged(bool visible);
|
||||
void startMinimizedChanged();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "apiAccountInfoModel.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
|
||||
#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>")
|
||||
: tr("Active");
|
||||
: tr("<p><a style=\"color: #28c840;\">Active</a>");
|
||||
}
|
||||
case EndDateRole: {
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
|
||||
@@ -52,7 +53,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case IsComponentVisibleRole: {
|
||||
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: {
|
||||
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
|
||||
@@ -73,6 +76,18 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
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();
|
||||
@@ -164,6 +179,8 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
|
||||
roles[IsComponentVisibleRole] = "isComponentVisible";
|
||||
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
|
||||
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ public:
|
||||
EndDateRole,
|
||||
IsComponentVisibleRole,
|
||||
HasExpiredWorkerRole,
|
||||
IsProtocolSelectionSupportedRole
|
||||
IsProtocolSelectionSupportedRole,
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole
|
||||
};
|
||||
|
||||
explicit ApiAccountInfoModel(QObject *parent = nullptr);
|
||||
@@ -31,7 +33,6 @@ public:
|
||||
public slots:
|
||||
void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig);
|
||||
QVariant data(const QString &roleString);
|
||||
|
||||
QJsonArray getAvailableCountries();
|
||||
QJsonArray getIssuedConfigsInfo();
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace
|
||||
{
|
||||
constexpr char amneziaFree[] = "amnezia-free";
|
||||
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: {
|
||||
auto speed = apiServiceData.serviceInfo.speed;
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
|
||||
return apiServiceData.serviceInfo.cardDescription.arg(speed);
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
QString description = apiServiceData.serviceInfo.cardDescription;
|
||||
@@ -124,8 +125,10 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
case OrderRole: {
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
return 0;
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
} else if (serviceType == serviceType::amneziaTrial) {
|
||||
return 1;
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,20 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
case AdEndpointRole: {
|
||||
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();
|
||||
@@ -443,6 +457,9 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
roles[AdDescriptionRole] = "adDescription";
|
||||
roles[AdEndpointRole] = "adEndpoint";
|
||||
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ public:
|
||||
AdDescriptionRole,
|
||||
AdEndpointRole,
|
||||
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
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.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import Style 1.0
|
||||
|
||||
@@ -37,6 +38,7 @@ Item {
|
||||
property int borderFocusedWidth: 1
|
||||
|
||||
property string rightImageColor: AmneziaStyle.color.paleGray
|
||||
property string leftImageColor: ""
|
||||
|
||||
property bool descriptionOnTop: false
|
||||
property bool hideDescription: true
|
||||
@@ -140,6 +142,14 @@ Item {
|
||||
|
||||
anchors.centerIn: parent
|
||||
source: leftImageSource
|
||||
visible: leftImageColor === ""
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: leftImage
|
||||
source: leftImage
|
||||
color: leftImageColor
|
||||
visible: leftImageColor !== ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,23 @@ PageType {
|
||||
id: root
|
||||
|
||||
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 {
|
||||
target: ServersModel
|
||||
|
||||
function onProcessedServerChanged() {
|
||||
root.processedServer = proxyServersModel.get(0)
|
||||
root.updateSubscriptionState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +87,11 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 10
|
||||
Layout.bottomMargin: 4
|
||||
|
||||
actionButtonImage: "qrc:/images/controls/settings.svg"
|
||||
|
||||
headerText: root.processedServer.name
|
||||
descriptionText: qsTr("Location for connection")
|
||||
|
||||
actionButtonFunction: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
@@ -94,6 +104,50 @@ PageType {
|
||||
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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
@@ -52,6 +53,26 @@ PageType {
|
||||
|
||||
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 {
|
||||
target: ServersModel
|
||||
|
||||
@@ -108,12 +129,66 @@ PageType {
|
||||
actionButtonImage: "qrc:/images/controls/edit-3.svg"
|
||||
|
||||
headerText: root.processedServer.name
|
||||
descriptionText: ApiAccountInfoModel.data("serviceDescription")
|
||||
|
||||
actionButtonFunction: function() {
|
||||
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 {
|
||||
@@ -151,6 +226,54 @@ PageType {
|
||||
|
||||
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 {
|
||||
id: switcher
|
||||
|
||||
@@ -177,10 +300,14 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: footer.isVisibleForAmneziaFree
|
||||
}
|
||||
|
||||
WarningType {
|
||||
id: warning
|
||||
|
||||
Layout.topMargin: 32
|
||||
Layout.topMargin: 24
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.fillWidth: true
|
||||
@@ -204,7 +331,7 @@ PageType {
|
||||
id: vpnKey
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: warning.visible ? 16 : 32
|
||||
Layout.topMargin: warning.visible ? 16 : 0
|
||||
|
||||
visible: footer.isVisibleForAmneziaFree
|
||||
|
||||
|
||||
@@ -1,226 +1,226 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
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.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
var result = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
if (!result) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
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)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
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.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : qsTr("Connect"))
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
var result = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
if (!result) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
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)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,23 @@ PageType {
|
||||
}
|
||||
|
||||
textField.onTextChanged: {
|
||||
if (headerText == qsTr("Password or SSH private key")) {
|
||||
if (headerText === qsTr("Password or SSH private key")) {
|
||||
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 doesn’t work, generate a compatible one.")
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
|
||||
+43
-3
@@ -21,15 +21,27 @@ Window {
|
||||
function onStateChanged() {
|
||||
if (Qt.platform.os === "android") {
|
||||
if (Qt.application.state === Qt.ApplicationActive) {
|
||||
root.visible = true
|
||||
refreshTimer.restart()
|
||||
} else if (Qt.application.state === Qt.ApplicationSuspended ||
|
||||
Qt.application.state === Qt.ApplicationInactive) {
|
||||
console.log("QML: Application going to background, state:", Qt.application.state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
id: refreshTimer
|
||||
interval: 150
|
||||
@@ -276,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 {
|
||||
objectName: "busyIndicatorItem"
|
||||
|
||||
|
||||
+46
-26
@@ -57,11 +57,15 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled)
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([enabled](QSharedPointer<IpcInterfaceReplica> iface){
|
||||
QRemoteObjectPendingReply<bool> reply = iface->refreshKillSwitch(enabled);
|
||||
if (reply.waitForFinished() && reply.returnValue())
|
||||
qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed";
|
||||
else
|
||||
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call";
|
||||
auto reply = iface->refreshKillSwitch(enabled);
|
||||
auto *watcher = new QRemoteObjectPendingCallWatcher(reply);
|
||||
QObject::connect(watcher, &QRemoteObjectPendingCallWatcher::finished, [watcher]() {
|
||||
if (watcher->returnValue().toBool())
|
||||
qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed";
|
||||
else
|
||||
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to refresh killswitch";
|
||||
watcher->deleteLater();
|
||||
});
|
||||
});
|
||||
#endif
|
||||
}
|
||||
@@ -76,15 +80,19 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
case Vpn::ConnectionState::Connected: {
|
||||
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) &&
|
||||
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 dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
|
||||
|
||||
@@ -109,17 +117,25 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
} break;
|
||||
case Vpn::ConnectionState::Disconnected:
|
||||
case Vpn::ConnectionState::Error: {
|
||||
auto flushDns = iface->flushDns();
|
||||
if (flushDns.waitForFinished() && flushDns.returnValue())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
|
||||
auto flushDnsReply = iface->flushDns();
|
||||
auto *flushDnsWatcher = new QRemoteObjectPendingCallWatcher(flushDnsReply);
|
||||
QObject::connect(flushDnsWatcher, &QRemoteObjectPendingCallWatcher::finished, [flushDnsWatcher]() {
|
||||
if (flushDnsWatcher->returnValue().toBool())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
|
||||
flushDnsWatcher->deleteLater();
|
||||
});
|
||||
|
||||
auto clearSavedRoutes = iface->clearSavedRoutes();
|
||||
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
|
||||
auto clearSavedRoutesReply = iface->clearSavedRoutes();
|
||||
auto *clearRoutesWatcher = new QRemoteObjectPendingCallWatcher(clearSavedRoutesReply);
|
||||
QObject::connect(clearRoutesWatcher, &QRemoteObjectPendingCallWatcher::finished, [clearRoutesWatcher]() {
|
||||
if (clearRoutesWatcher->returnValue().toBool())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
|
||||
clearRoutesWatcher->deleteLater();
|
||||
});
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
@@ -182,8 +198,12 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode)
|
||||
}
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto reply = iface->flushDns();
|
||||
if (reply.waitForFinished() || !reply.returnValue())
|
||||
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
|
||||
auto *w = new QRemoteObjectPendingCallWatcher(reply);
|
||||
QObject::connect(w, &QRemoteObjectPendingCallWatcher::finished, [w]() {
|
||||
if (!w->returnValue().toBool())
|
||||
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
|
||||
w->deleteLater();
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -219,8 +239,8 @@ ErrorCode VpnConnection::lastError() const
|
||||
return m_vpnProtocol.data()->lastError();
|
||||
}
|
||||
|
||||
void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container,
|
||||
const QJsonObject &vpnConfiguration)
|
||||
void VpnConnection::connectToVpn(int serverIndex, const amnezia::ServerCredentials &credentials,
|
||||
amnezia::DockerContainer container, const QJsonObject &vpnConfiguration)
|
||||
{
|
||||
qDebug() << QString("Trying to connect to VPN, server index is %1, container is %2, route mode is")
|
||||
.arg(serverIndex)
|
||||
|
||||
@@ -44,7 +44,8 @@ public:
|
||||
#endif
|
||||
|
||||
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 disconnectFromVpn();
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ bool KillSwitch::refresh(bool enabled)
|
||||
#endif
|
||||
|
||||
#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
|
||||
|
||||
if (isStrictKillSwitchEnabled()) {
|
||||
@@ -70,6 +72,11 @@ bool KillSwitch::isStrictKillSwitchEnabled()
|
||||
QSettings RegHLM("HKEY_LOCAL_MACHINE\\Software\\" + QString(ORGANIZATION_NAME)
|
||||
+ "\\" + QString(APPLICATION_NAME), QSettings::NativeFormat);
|
||||
return RegHLM.value("strictKillSwitchEnabled", false).toBool();
|
||||
#endif
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
|
||||
if (m_appSettigns.isNull()) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
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("300.allowLAN"), 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);
|
||||
} else {
|
||||
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("300.allowLAN"), true);
|
||||
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::uninstall();
|
||||
}
|
||||
@@ -335,7 +342,7 @@ bool KillSwitch::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIn
|
||||
}
|
||||
|
||||
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);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public:
|
||||
bool isStrictKillSwitchEnabled();
|
||||
|
||||
private:
|
||||
KillSwitch(QObject* parent) {};
|
||||
explicit KillSwitch(QObject* parent) : QObject(parent) {}
|
||||
QStringList m_allowedRanges;
|
||||
QSharedPointer<SecureQSettings> m_appSettigns;
|
||||
|
||||
|
||||
@@ -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_addr.s_addr = inet_addr(gw.toStdString().c_str());
|
||||
((struct sockaddr_in *)&route.rt_gateway)->sin_port = 0;
|
||||
|
||||
// set host rejecting
|
||||
((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_port = 0;
|
||||
|
||||
// set mask
|
||||
((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_port = 0;
|
||||
|
||||
route.rt_flags = RTF_UP | RTF_GATEWAY;
|
||||
route.rt_metric = 0;
|
||||
//route.rt_dev = "ens33";
|
||||
route.rt_metric = 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) {
|
||||
QProcess process;
|
||||
process.start("systemctl", { "is-active", "--quiet", serviceName });
|
||||
process.waitForFinished();
|
||||
|
||||
if (!process.waitForFinished(2000)) {
|
||||
process.kill();
|
||||
process.waitForFinished(500);
|
||||
}
|
||||
return process.exitCode() == 0;
|
||||
}
|
||||
|
||||
bool RouterLinux::flushDns()
|
||||
{
|
||||
QProcess p;
|
||||
p.setProcessChannelMode(QProcess::MergedChannels);
|
||||
|
||||
//check what the dns manager use
|
||||
if (isServiceActive("nscd.service")) {
|
||||
qDebug() << "Restarting nscd.service";
|
||||
p.start("systemctl", { "restart", "nscd" });
|
||||
qDebug() << "Flushing nscd cache";
|
||||
QProcess::startDetached("nscd", { "--invalidate=hosts" });
|
||||
return true;
|
||||
} else if (isServiceActive("systemd-resolved.service")) {
|
||||
qDebug() << "Restarting systemd-resolved.service";
|
||||
p.start("systemctl", { "restart", "systemd-resolved" });
|
||||
qDebug() << "Flushing systemd-resolved cache";
|
||||
QProcess::startDetached("resolvectl", { "flush-caches" });
|
||||
qDebug().noquote() << "Flush dns requested (non-blocking)";
|
||||
return true;
|
||||
} else {
|
||||
qDebug() << "No suitable DNS manager found.";
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user