
* Add allowed DNS list for killswitch * Windows killswitch strict mode backend part * Killswitch strict mode for Linux and MacOS * Windows fixes * feature: Add Kill Switch settings page with strict mode option * fix windows build after merge * Refresh killswitch mode when it toggled * Use HLM to store strictMode flag * Some Linux updates * feat: Enhance VerticalRadioButton with improved styling and disabled states * Refresh killSwitch state update * Fix build * refactor: Modularize header components * Change kill switch radio button styling * Fix strict kill switch mode handling * Refactor: Replace HeaderType with new Types for headers in QML pages * Remove deprecated HeaderType QML component * Refresh strict mode killswitch after global toggle change * Implement model, controller and UI for killswitch dns exceptions * Connect backend part and UI * Change label text to DNS exceptions * Remove HeaderType from PageSettingsApiDevices * Some pretty fixes * Fix problem with definition sequence of PageSettingsKillSwitchExceptions.pml elements * Add exclusion method for Windows firewall * Change ubuntu version in deploy script * Update ubuntu version in GH actions * Add confirmation popup for strict killswitch mode * Add qt standard path for build script * Add method to killswitch for expanding strickt mode exceptions list and fix allowTrafficTo() for Windows. Also Added cache in KillSwitch class for exceptions * Add insertion of gateway address to strict killswitch exceptions * Review fixes * buildfix and naming --------- Co-authored-by: aiamnezia <ai@amnezia.org>
995 lines
33 KiB
C++
995 lines
33 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "windowsfirewall.h"
|
|
|
|
#include <comdef.h>
|
|
#include <fwpmu.h>
|
|
#include <guiddef.h>
|
|
#include <initguid.h>
|
|
#include <netfw.h>
|
|
#include <qaccessible.h>
|
|
#include <qassert.h>
|
|
#include <stdio.h>
|
|
#include <windows.h>
|
|
#include <Ws2tcpip.h>
|
|
#include "winsock.h"
|
|
|
|
#include <QApplication>
|
|
#include <QFileInfo>
|
|
#include <QHostAddress>
|
|
#include <QNetworkInterface>
|
|
#include <QObject>
|
|
#include <QScopeGuard>
|
|
#include <QtEndian>
|
|
|
|
#include "ipaddress.h"
|
|
#include "leakdetector.h"
|
|
#include "logger.h"
|
|
#include "platforms/windows/windowsutils.h"
|
|
|
|
#include "killswitch.h"
|
|
|
|
#define IPV6_ADDRESS_SIZE 16
|
|
|
|
// ID for the Firewall Sublayer
|
|
DEFINE_GUID(ST_FW_WINFW_BASELINE_SUBLAYER_KEY, 0xc78056ff, 0x2bc1, 0x4211, 0xaa,
|
|
0xdd, 0x7f, 0x35, 0x8d, 0xef, 0x20, 0x2d);
|
|
// ID for the Mullvad Split-Tunnel Sublayer Provider
|
|
DEFINE_GUID(ST_FW_PROVIDER_KEY, 0xe2c114ee, 0xf32a, 0x4264, 0xa6, 0xcb, 0x3f,
|
|
0xa7, 0x99, 0x63, 0x56, 0xd9);
|
|
|
|
namespace {
|
|
Logger logger("WindowsFirewall");
|
|
WindowsFirewall* s_instance = nullptr;
|
|
|
|
// Note Filter Weight may be between 0-15!
|
|
constexpr uint8_t LOW_WEIGHT = 0;
|
|
constexpr uint8_t MED_WEIGHT = 7;
|
|
constexpr uint8_t HIGH_WEIGHT = 13;
|
|
constexpr uint8_t MAX_WEIGHT = 15;
|
|
} // namespace
|
|
|
|
WindowsFirewall* WindowsFirewall::create(QObject* parent) {
|
|
if (s_instance != nullptr) {
|
|
// Only one instance of the firewall is allowed
|
|
// Q_ASSERT(false);
|
|
return s_instance;
|
|
}
|
|
HANDLE engineHandle = nullptr;
|
|
DWORD result = ERROR_SUCCESS;
|
|
// Use dynamic sessions for efficiency and safety:
|
|
// -> Filtering policy objects are deleted even when the application crashes/
|
|
// deamon goes down
|
|
FWPM_SESSION0 session;
|
|
memset(&session, 0, sizeof(session));
|
|
session.flags = FWPM_SESSION_FLAG_DYNAMIC;
|
|
|
|
logger.debug() << "Opening the filter engine.";
|
|
|
|
result = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, &session,
|
|
&engineHandle);
|
|
|
|
if (result != ERROR_SUCCESS) {
|
|
WindowsUtils::windowsLog("FwpmEngineOpen0 failed");
|
|
return nullptr;
|
|
}
|
|
logger.debug() << "Filter engine opened successfully.";
|
|
if (!initSublayer()) {
|
|
return nullptr;
|
|
}
|
|
s_instance = new WindowsFirewall(engineHandle, parent);
|
|
return s_instance;
|
|
}
|
|
|
|
WindowsFirewall::WindowsFirewall(HANDLE session, QObject* parent)
|
|
: QObject(parent), m_sessionHandle(session) {
|
|
MZ_COUNT_CTOR(WindowsFirewall);
|
|
}
|
|
|
|
WindowsFirewall::~WindowsFirewall() {
|
|
MZ_COUNT_DTOR(WindowsFirewall);
|
|
if (m_sessionHandle != INVALID_HANDLE_VALUE) {
|
|
CloseHandle(m_sessionHandle);
|
|
}
|
|
}
|
|
|
|
// static
|
|
bool WindowsFirewall::initSublayer() {
|
|
// If we were not able to aquire a handle, this will fail anyway.
|
|
// We need to open up another handle because of wfp rules:
|
|
// If a wfp resource was created with SESSION_DYNAMIC,
|
|
// the session exlusively owns the resource, meaning the driver can't add
|
|
// filters to the sublayer. So let's have non dynamic session only for the
|
|
// sublayer creation. This means the Layer exists until the next Reboot.
|
|
DWORD result = ERROR_SUCCESS;
|
|
HANDLE wfp = INVALID_HANDLE_VALUE;
|
|
FWPM_SESSION0 session;
|
|
memset(&session, 0, sizeof(session));
|
|
|
|
logger.debug() << "Opening the filter engine";
|
|
result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &wfp);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmEngineOpen0 failed. Return value:.\n" << result;
|
|
return false;
|
|
}
|
|
auto cleanup = qScopeGuard([&] { FwpmEngineClose0(wfp); });
|
|
|
|
// Check if the Layer Already Exists
|
|
FWPM_SUBLAYER0* maybeLayer;
|
|
result = FwpmSubLayerGetByKey0(wfp, &ST_FW_WINFW_BASELINE_SUBLAYER_KEY,
|
|
&maybeLayer);
|
|
if (result == ERROR_SUCCESS) {
|
|
logger.debug() << "The Sublayer Already Exists!";
|
|
FwpmFreeMemory0((void**)&maybeLayer);
|
|
return true;
|
|
}
|
|
|
|
// Step 1: Start Transaction
|
|
result = FwpmTransactionBegin(wfp, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
|
|
<< result;
|
|
return false;
|
|
}
|
|
|
|
// Step 3: Add Sublayer
|
|
FWPM_SUBLAYER0 subLayer;
|
|
memset(&subLayer, 0, sizeof(subLayer));
|
|
subLayer.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
subLayer.displayData.name = (PWSTR)L"Amnezia-SplitTunnel-Sublayer";
|
|
subLayer.displayData.description =
|
|
(PWSTR)L"Filters that enforce a good baseline";
|
|
subLayer.weight = 0xFFFF;
|
|
|
|
result = FwpmSubLayerAdd0(wfp, &subLayer, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmSubLayerAdd0 failed. Return value:.\n" << result;
|
|
return false;
|
|
}
|
|
// Step 4: Commit!
|
|
result = FwpmTransactionCommit0(wfp);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
|
|
<< result;
|
|
return false;
|
|
}
|
|
logger.debug() << "Initialised Sublayer";
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::enableInterface(int vpnAdapterIndex) {
|
|
// Checks if the FW_Rule was enabled succesfully,
|
|
// disables the whole killswitch and returns false if not.
|
|
#define FW_OK(rule) \
|
|
{ \
|
|
auto result = FwpmTransactionBegin(m_sessionHandle, NULL); \
|
|
if (result != ERROR_SUCCESS) { \
|
|
disableKillSwitch(); \
|
|
return false; \
|
|
} \
|
|
if (!rule) { \
|
|
FwpmTransactionAbort0(m_sessionHandle); \
|
|
disableKillSwitch(); \
|
|
return false; \
|
|
} \
|
|
result = FwpmTransactionCommit0(m_sessionHandle); \
|
|
if (result != ERROR_SUCCESS) { \
|
|
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n" \
|
|
<< result; \
|
|
return false; \
|
|
} \
|
|
}
|
|
|
|
logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex;
|
|
if (vpnAdapterIndex < 0)
|
|
{
|
|
IPAddress allv4("0.0.0.0/0");
|
|
if (!blockTrafficTo(allv4, MED_WEIGHT,
|
|
"Block Internet", "killswitch")) {
|
|
return false;
|
|
}
|
|
IPAddress allv6("::/0");
|
|
if (!blockTrafficTo(allv6, MED_WEIGHT,
|
|
"Block Internet", "killswitch")) {
|
|
return false;
|
|
}
|
|
} else
|
|
FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT,
|
|
"Allow usage of VPN Adapter"));
|
|
FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic"));
|
|
FW_OK(allowHyperVTraffic(MAX_WEIGHT, "Allow Hyper-V Traffic"));
|
|
FW_OK(allowTrafficForAppOnAll(getCurrentPath(), MAX_WEIGHT,
|
|
"Allow all for AmneziaVPN.exe"));
|
|
FW_OK(blockTrafficOnPort(53, MED_WEIGHT, "Block all DNS"));
|
|
FW_OK(allowLoopbackTraffic(MED_WEIGHT,
|
|
"Allow Loopback traffic on device %1"));
|
|
|
|
logger.debug() << "Killswitch on! Rules:" << m_activeRules.length();
|
|
return true;
|
|
#undef FW_OK
|
|
}
|
|
|
|
// Allow unprotected traffic sent to the following local address ranges.
|
|
bool WindowsFirewall::enableLanBypass(const QList<IPAddress>& ranges) {
|
|
// Start the firewall transaction
|
|
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
disableKillSwitch();
|
|
return false;
|
|
}
|
|
auto cleanup = qScopeGuard([&] {
|
|
FwpmTransactionAbort0(m_sessionHandle);
|
|
disableKillSwitch();
|
|
});
|
|
|
|
// Blocking unprotected traffic
|
|
for (const IPAddress& prefix : ranges) {
|
|
if (!allowTrafficTo(prefix, LOW_WEIGHT + 1, "Allow LAN bypass traffic")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result = FwpmTransactionCommit0(m_sessionHandle);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionCommit0 failed with error:" << result;
|
|
return false;
|
|
}
|
|
|
|
cleanup.dismiss();
|
|
return true;
|
|
}
|
|
|
|
// Allow unprotected traffic sent to the following address ranges.
|
|
bool WindowsFirewall::allowTrafficRange(const QStringList& ranges) {
|
|
// Start the firewall transaction
|
|
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
disableKillSwitch();
|
|
return false;
|
|
}
|
|
auto cleanup = qScopeGuard([&] {
|
|
FwpmTransactionAbort0(m_sessionHandle);
|
|
disableKillSwitch();
|
|
});
|
|
|
|
for (const QString& addr : ranges) {
|
|
logger.debug() << "Allow killswitch exclude: " << addr;
|
|
if (!allowTrafficTo(QHostAddress(addr), LOW_WEIGHT + 1, "Allow killswitch bypass traffic")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result = FwpmTransactionCommit0(m_sessionHandle);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionCommit0 failed with error:" << result;
|
|
return false;
|
|
}
|
|
|
|
cleanup.dismiss();
|
|
return true;
|
|
}
|
|
|
|
|
|
bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) {
|
|
// Start the firewall transaction
|
|
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
|
|
if (result != ERROR_SUCCESS) {
|
|
disableKillSwitch();
|
|
return false;
|
|
}
|
|
auto cleanup = qScopeGuard([&] {
|
|
FwpmTransactionAbort0(m_sessionHandle);
|
|
disableKillSwitch();
|
|
});
|
|
|
|
// Build the firewall rules for this peer.
|
|
logger.info() << "Enabling traffic for peer"
|
|
<< config.m_serverPublicKey;
|
|
if (!blockTrafficTo(config.m_allowedIPAddressRanges, LOW_WEIGHT,
|
|
"Block Internet", config.m_serverPublicKey)) {
|
|
return false;
|
|
}
|
|
if (!config.m_dnsServer.isEmpty()) {
|
|
if (!allowTrafficTo(QHostAddress(config.m_dnsServer), 53, HIGH_WEIGHT,
|
|
"Allow DNS-Server", config.m_serverPublicKey)) {
|
|
return false;
|
|
}
|
|
// In some cases, we might configure a 2nd DNS server for IPv6, however
|
|
// this should probably be cleaned up by converting m_dnsServer into
|
|
// a QStringList instead.
|
|
if (config.m_dnsServer == config.m_serverIpv4Gateway) {
|
|
if (!allowTrafficTo(QHostAddress(config.m_serverIpv6Gateway), 53,
|
|
HIGH_WEIGHT, "Allow extra IPv6 DNS-Server",
|
|
config.m_serverPublicKey)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const QString& dns : config.m_allowedDnsServers) {
|
|
logger.debug() << "Allow DNS: " << dns;
|
|
if (!allowTrafficTo(QHostAddress(dns), 53, HIGH_WEIGHT,
|
|
"Allow DNS-Server", config.m_serverPublicKey)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!config.m_excludedAddresses.empty()) {
|
|
for (const QString& i : config.m_excludedAddresses) {
|
|
logger.debug() << "excludedAddresses range: " << i;
|
|
|
|
if (!allowTrafficTo(i, HIGH_WEIGHT,
|
|
"Allow Ecxlude route", config.m_serverPublicKey)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
result = FwpmTransactionCommit0(m_sessionHandle);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionCommit0 failed with error:" << result;
|
|
return false;
|
|
}
|
|
|
|
cleanup.dismiss();
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::disablePeerTraffic(const QString& pubkey) {
|
|
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
|
|
auto cleanup = qScopeGuard([&] {
|
|
if (result != ERROR_SUCCESS) {
|
|
FwpmTransactionAbort0(m_sessionHandle);
|
|
}
|
|
});
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
|
|
<< result;
|
|
return false;
|
|
}
|
|
|
|
logger.info() << "Disabling traffic for peer" << pubkey;
|
|
for (const auto& filterID : m_peerRules.values(pubkey)) {
|
|
FwpmFilterDeleteById0(m_sessionHandle, filterID);
|
|
m_peerRules.remove(pubkey, filterID);
|
|
}
|
|
|
|
// Commit!
|
|
result = FwpmTransactionCommit0(m_sessionHandle);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
|
|
<< result;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::disableKillSwitch() {
|
|
return KillSwitch::instance()->disableKillSwitch();
|
|
}
|
|
|
|
bool WindowsFirewall::allowAllTraffic() {
|
|
auto result = FwpmTransactionBegin(m_sessionHandle, NULL);
|
|
auto cleanup = qScopeGuard([&] {
|
|
if (result != ERROR_SUCCESS) {
|
|
FwpmTransactionAbort0(m_sessionHandle);
|
|
}
|
|
});
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionBegin0 failed. Return value:.\n"
|
|
<< result;
|
|
return false;
|
|
}
|
|
|
|
for (const auto& filterID : m_peerRules.values()) {
|
|
FwpmFilterDeleteById0(m_sessionHandle, filterID);
|
|
}
|
|
|
|
for (const auto& filterID : qAsConst(m_activeRules)) {
|
|
FwpmFilterDeleteById0(m_sessionHandle, filterID);
|
|
}
|
|
|
|
// Commit!
|
|
result = FwpmTransactionCommit0(m_sessionHandle);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "FwpmTransactionCommit0 failed. Return value:.\n"
|
|
<< result;
|
|
return false;
|
|
}
|
|
m_peerRules.clear();
|
|
m_activeRules.clear();
|
|
logger.debug() << "Firewall Disabled!";
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::allowTrafficForAppOnAll(const QString& exePath,
|
|
int weight,
|
|
const QString& title) {
|
|
DWORD result = ERROR_SUCCESS;
|
|
Q_ASSERT(weight <= 15);
|
|
|
|
// Get the AppID for the Executable;
|
|
QString appName = QFileInfo(exePath).baseName();
|
|
std::wstring wstr = exePath.toStdWString();
|
|
PCWSTR appPath = wstr.c_str();
|
|
FWP_BYTE_BLOB* appID = NULL;
|
|
result = FwpmGetAppIdFromFileName0(appPath, &appID);
|
|
if (result != ERROR_SUCCESS) {
|
|
WindowsUtils::windowsLog("FwpmGetAppIdFromFileName0 failure");
|
|
return false;
|
|
}
|
|
// Condition: Request must come from the .exe
|
|
FWPM_FILTER_CONDITION0 conds;
|
|
conds.fieldKey = FWPM_CONDITION_ALE_APP_ID;
|
|
conds.matchType = FWP_MATCH_EQUAL;
|
|
conds.conditionValue.type = FWP_BYTE_BLOB_TYPE;
|
|
conds.conditionValue.byteBlob = appID;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = &conds;
|
|
filter.numFilterConditions = 1;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT; // Make this decision
|
|
// only blockable by veto
|
|
// Build and add the Filters
|
|
// #1 Permit outbound IPv4 traffic.
|
|
{
|
|
QString desc("Permit (out) IPv4 Traffic of: " + appName);
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
|
|
if (!enableFilter(&filter, title, desc)) {
|
|
return false;
|
|
}
|
|
}
|
|
// #2 Permit inbound IPv4 traffic.
|
|
{
|
|
QString desc("Permit (in) IPv4 Traffic of: " + appName);
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
|
|
if (!enableFilter(&filter, title, desc)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight,
|
|
const QString& title) {
|
|
FWPM_FILTER_CONDITION0 conds;
|
|
// Condition: Request must be targeting the TUN interface
|
|
conds.fieldKey = FWPM_CONDITION_INTERFACE_INDEX;
|
|
conds.matchType = FWP_MATCH_EQUAL;
|
|
conds.conditionValue.type = FWP_UINT32;
|
|
conds.conditionValue.uint32 = networkAdapter;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = &conds;
|
|
filter.numFilterConditions = 1;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
|
|
QString description("Allow %0 traffic on Adapter %1");
|
|
// #1 Permit outbound IPv4 traffic.
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("out").arg(networkAdapter))) {
|
|
return false;
|
|
}
|
|
// #2 Permit inbound IPv4 traffic.
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("in").arg(networkAdapter))) {
|
|
return false;
|
|
}
|
|
// #3 Permit outbound IPv6 traffic.
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("out").arg(networkAdapter))) {
|
|
return false;
|
|
}
|
|
// #4 Permit inbound IPv6 traffic.
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("in").arg(networkAdapter))) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::allowTrafficTo(const IPAddress& addr, int weight,
|
|
const QString& title,
|
|
const QString& peer) {
|
|
GUID layerKeyOut;
|
|
GUID layerKeyIn;
|
|
if (addr.type() == QAbstractSocket::IPv4Protocol) {
|
|
layerKeyOut = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
|
|
layerKeyIn = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
|
|
} else {
|
|
layerKeyOut = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
|
layerKeyIn = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
}
|
|
|
|
// Match the IP address range.
|
|
FWPM_FILTER_CONDITION0 cond[1] = {};
|
|
FWP_RANGE0 ipRange;
|
|
QByteArray lowIpV6Buffer;
|
|
QByteArray highIpV6Buffer;
|
|
|
|
importAddress(addr.address(), ipRange.valueLow, &lowIpV6Buffer);
|
|
importAddress(addr.broadcastAddress(), ipRange.valueHigh, &highIpV6Buffer);
|
|
|
|
cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS;
|
|
cond[0].matchType = FWP_MATCH_RANGE;
|
|
cond[0].conditionValue.type = FWP_RANGE_TYPE;
|
|
cond[0].conditionValue.rangeValue = &ipRange;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
filter.numFilterConditions = 1;
|
|
filter.filterCondition = cond;
|
|
|
|
// Send the filters down to the firewall.
|
|
QString description = "Permit traffic %1 " + addr.toString();
|
|
filter.layerKey = layerKeyOut;
|
|
if (!enableFilter(&filter, title, description.arg("to"), peer)) {
|
|
return false;
|
|
}
|
|
filter.layerKey = layerKeyIn;
|
|
if (!enableFilter(&filter, title, description.arg("from"), peer)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port,
|
|
int weight, const QString& title,
|
|
const QString& peer) {
|
|
bool isIPv4 = targetIP.protocol() == QAbstractSocket::IPv4Protocol;
|
|
GUID layerOut =
|
|
isIPv4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
|
GUID layerIn = isIPv4 ? FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4
|
|
: FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
|
|
quint32_be ipBigEndian;
|
|
quint32 ip = targetIP.toIPv4Address();
|
|
qToBigEndian(ip, &ipBigEndian);
|
|
|
|
// Allow Traffic to IP with PORT using any protocol
|
|
FWPM_FILTER_CONDITION0 conds[4];
|
|
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[0].matchType = FWP_MATCH_EQUAL;
|
|
conds[0].conditionValue.type = FWP_UINT8;
|
|
conds[0].conditionValue.uint8 = (IPPROTO_UDP);
|
|
|
|
conds[1].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[1].matchType = FWP_MATCH_EQUAL;
|
|
conds[1].conditionValue.type = FWP_UINT8;
|
|
conds[1].conditionValue.uint16 = (IPPROTO_TCP);
|
|
|
|
conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
|
|
conds[2].matchType = FWP_MATCH_EQUAL;
|
|
conds[2].conditionValue.type = FWP_UINT16;
|
|
conds[2].conditionValue.uint16 = port;
|
|
|
|
conds[3].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS;
|
|
conds[3].matchType = FWP_MATCH_EQUAL;
|
|
QByteArray buffer;
|
|
// will hold v6 Addess bytes if present
|
|
importAddress(targetIP, conds[3].conditionValue, &buffer);
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = conds;
|
|
filter.numFilterConditions = 4;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
filter.flags = FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT; // Hard Permit!
|
|
|
|
QString description("Permit traffic %1 %2 on port %3");
|
|
filter.layerKey = layerOut;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("to").arg(targetIP.toString()).arg(port),
|
|
peer)) {
|
|
return false;
|
|
}
|
|
filter.layerKey = layerIn;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("from").arg(targetIP.toString()).arg(port),
|
|
peer)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) {
|
|
// Allow outbound DHCPv4
|
|
{
|
|
FWPM_FILTER_CONDITION0 conds[4];
|
|
// Condition: Request must be targeting the TUN interface
|
|
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[0].matchType = FWP_MATCH_EQUAL;
|
|
conds[0].conditionValue.type = FWP_UINT8;
|
|
conds[0].conditionValue.uint8 = (IPPROTO_UDP);
|
|
|
|
conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT;
|
|
conds[1].matchType = FWP_MATCH_EQUAL;
|
|
conds[1].conditionValue.type = FWP_UINT16;
|
|
conds[1].conditionValue.uint16 = (68);
|
|
|
|
conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
|
|
conds[2].matchType = FWP_MATCH_EQUAL;
|
|
conds[2].conditionValue.type = FWP_UINT16;
|
|
conds[2].conditionValue.uint16 = 67;
|
|
|
|
conds[3].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS;
|
|
conds[3].matchType = FWP_MATCH_EQUAL;
|
|
conds[3].conditionValue.type = FWP_UINT32;
|
|
conds[3].conditionValue.uint32 = (0xffffffff);
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = conds;
|
|
filter.numFilterConditions = 4;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
|
|
|
|
if (!enableFilter(&filter, title, "Allow Outbound DHCP")) {
|
|
return false;
|
|
}
|
|
}
|
|
// Allow inbound DHCPv4
|
|
{
|
|
FWPM_FILTER_CONDITION0 conds[3];
|
|
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[0].matchType = FWP_MATCH_EQUAL;
|
|
conds[0].conditionValue.type = FWP_UINT8;
|
|
conds[0].conditionValue.uint8 = (IPPROTO_UDP);
|
|
|
|
conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT;
|
|
conds[1].matchType = FWP_MATCH_EQUAL;
|
|
conds[1].conditionValue.type = FWP_UINT16;
|
|
conds[1].conditionValue.uint16 = (68);
|
|
|
|
conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
|
|
conds[2].matchType = FWP_MATCH_EQUAL;
|
|
conds[2].conditionValue.type = FWP_UINT16;
|
|
conds[2].conditionValue.uint16 = 67;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = conds;
|
|
filter.numFilterConditions = 3;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
|
|
|
|
if (!enableFilter(&filter, title, "Allow inbound DHCP")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Allow outbound DHCPv6
|
|
{
|
|
FWPM_FILTER_CONDITION0 conds[3];
|
|
// Condition: Request must be targeting the TUN interface
|
|
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[0].matchType = FWP_MATCH_EQUAL;
|
|
conds[0].conditionValue.type = FWP_UINT8;
|
|
conds[0].conditionValue.uint8 = (IPPROTO_UDP);
|
|
|
|
conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT;
|
|
conds[1].matchType = FWP_MATCH_EQUAL;
|
|
conds[1].conditionValue.type = FWP_UINT16;
|
|
conds[1].conditionValue.uint16 = (68);
|
|
|
|
conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
|
|
conds[2].matchType = FWP_MATCH_EQUAL;
|
|
conds[2].conditionValue.type = FWP_UINT16;
|
|
conds[2].conditionValue.uint16 = 67;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = conds;
|
|
filter.numFilterConditions = 3;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
|
|
|
if (!enableFilter(&filter, title, "Allow outbound DHCPv6")) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Allow inbound DHCPv6
|
|
{
|
|
FWPM_FILTER_CONDITION0 conds[3];
|
|
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[0].matchType = FWP_MATCH_EQUAL;
|
|
conds[0].conditionValue.type = FWP_UINT8;
|
|
conds[0].conditionValue.uint8 = (IPPROTO_UDP);
|
|
|
|
conds[1].fieldKey = FWPM_CONDITION_IP_LOCAL_PORT;
|
|
conds[1].matchType = FWP_MATCH_EQUAL;
|
|
conds[1].conditionValue.type = FWP_UINT16;
|
|
conds[1].conditionValue.uint16 = (68);
|
|
|
|
conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
|
|
conds[2].matchType = FWP_MATCH_EQUAL;
|
|
conds[2].conditionValue.type = FWP_UINT16;
|
|
conds[2].conditionValue.uint16 = 67;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = conds;
|
|
filter.numFilterConditions = 3;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
if (!enableFilter(&filter, title, "Allow inbound DHCPv6")) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Allows the internal Hyper-V Switches to work.
|
|
bool WindowsFirewall::allowHyperVTraffic(uint8_t weight, const QString& title) {
|
|
FWPM_FILTER_CONDITION0 cond;
|
|
// Condition: Request must be targeting the TUN interface
|
|
cond.fieldKey = FWPM_CONDITION_L2_FLAGS;
|
|
cond.matchType = FWP_MATCH_EQUAL;
|
|
cond.conditionValue.type = FWP_UINT32;
|
|
cond.conditionValue.uint32 = FWP_CONDITION_L2_IS_VM2VM;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = &cond;
|
|
filter.numFilterConditions = 1;
|
|
filter.action.type = FWP_ACTION_PERMIT;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
|
|
// #1 Permit Hyper-V => Hyper-V outbound.
|
|
filter.layerKey = FWPM_LAYER_OUTBOUND_MAC_FRAME_NATIVE;
|
|
if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V outbound")) {
|
|
return false;
|
|
}
|
|
// #2 Permit Hyper-V => Hyper-V inbound.
|
|
filter.layerKey = FWPM_LAYER_INBOUND_MAC_FRAME_NATIVE;
|
|
if (!enableFilter(&filter, title, "Permit Hyper-V => Hyper-V inbound")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight,
|
|
const QString& title,
|
|
const QString& peer) {
|
|
QString description("Block traffic %1 %2 ");
|
|
|
|
auto lower = addr.address();
|
|
auto upper = addr.broadcastAddress();
|
|
|
|
const bool isV4 = addr.type() == QAbstractSocket::IPv4Protocol;
|
|
const GUID layerKeyOut =
|
|
isV4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
|
const GUID layerKeyIn = isV4 ? FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4
|
|
: FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.action.type = FWP_ACTION_BLOCK;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
|
|
FWPM_FILTER_CONDITION0 cond[1] = {};
|
|
FWP_RANGE0 ipRange;
|
|
QByteArray lowIpV6Buffer;
|
|
QByteArray highIpV6Buffer;
|
|
|
|
importAddress(lower, ipRange.valueLow, &lowIpV6Buffer);
|
|
importAddress(upper, ipRange.valueHigh, &highIpV6Buffer);
|
|
|
|
cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS;
|
|
cond[0].matchType = FWP_MATCH_RANGE;
|
|
cond[0].conditionValue.type = FWP_RANGE_TYPE;
|
|
cond[0].conditionValue.rangeValue = &ipRange;
|
|
|
|
filter.numFilterConditions = 1;
|
|
filter.filterCondition = cond;
|
|
|
|
filter.layerKey = layerKeyOut;
|
|
if (!enableFilter(&filter, title, description.arg("to").arg(addr.toString()),
|
|
peer)) {
|
|
return false;
|
|
}
|
|
filter.layerKey = layerKeyIn;
|
|
if (!enableFilter(&filter, title,
|
|
description.arg("from").arg(addr.toString()), peer)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::blockTrafficTo(const QList<IPAddress>& rangeList,
|
|
uint8_t weight, const QString& title,
|
|
const QString& peer) {
|
|
for (auto range : rangeList) {
|
|
if (!blockTrafficTo(range, weight, title, peer)) {
|
|
logger.info() << "Setting Range of" << range.toString() << "failed";
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Returns the Path of the Current Executable this runs in
|
|
QString WindowsFirewall::getCurrentPath() {
|
|
const unsigned char initValue = 0xff;
|
|
QByteArray buffer(2048, initValue);
|
|
auto ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size());
|
|
|
|
if (ok == ERROR_INSUFFICIENT_BUFFER) {
|
|
buffer.resize(buffer.size() * 2);
|
|
ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size());
|
|
}
|
|
if (ok == 0) {
|
|
WindowsUtils::windowsLog("Err fetching dos path");
|
|
return "";
|
|
}
|
|
|
|
return QString::fromLocal8Bit(buffer);
|
|
}
|
|
|
|
void WindowsFirewall::importAddress(const QHostAddress& addr,
|
|
OUT FWP_VALUE0_& value,
|
|
OUT QByteArray* v6DataBuffer) {
|
|
const bool isV4 = addr.protocol() == QAbstractSocket::IPv4Protocol;
|
|
if (isV4) {
|
|
value.type = FWP_UINT32;
|
|
value.uint32 = addr.toIPv4Address();
|
|
return;
|
|
}
|
|
auto v6bytes = addr.toIPv6Address();
|
|
v6DataBuffer->append((const char*)v6bytes.c, IPV6_ADDRESS_SIZE);
|
|
value.type = FWP_BYTE_ARRAY16_TYPE;
|
|
value.byteArray16 = (FWP_BYTE_ARRAY16*)v6DataBuffer->data();
|
|
}
|
|
void WindowsFirewall::importAddress(const QHostAddress& addr,
|
|
OUT FWP_CONDITION_VALUE0_& value,
|
|
OUT QByteArray* v6DataBuffer) {
|
|
const bool isV4 = addr.protocol() == QAbstractSocket::IPv4Protocol;
|
|
if (isV4) {
|
|
value.type = FWP_UINT32;
|
|
value.uint32 = addr.toIPv4Address();
|
|
return;
|
|
}
|
|
auto v6bytes = addr.toIPv6Address();
|
|
v6DataBuffer->append((const char*)v6bytes.c, IPV6_ADDRESS_SIZE);
|
|
value.type = FWP_BYTE_ARRAY16_TYPE;
|
|
value.byteArray16 = (FWP_BYTE_ARRAY16*)v6DataBuffer->data();
|
|
}
|
|
|
|
bool WindowsFirewall::blockTrafficOnPort(uint port, uint8_t weight,
|
|
const QString& title) {
|
|
// Allow Traffic to IP with PORT using any protocol
|
|
FWPM_FILTER_CONDITION0 conds[3];
|
|
conds[0].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[0].matchType = FWP_MATCH_EQUAL;
|
|
conds[0].conditionValue.type = FWP_UINT8;
|
|
conds[0].conditionValue.uint8 = (IPPROTO_UDP);
|
|
|
|
conds[1].fieldKey = FWPM_CONDITION_IP_PROTOCOL;
|
|
conds[1].matchType = FWP_MATCH_EQUAL;
|
|
conds[1].conditionValue.type = FWP_UINT8;
|
|
conds[1].conditionValue.uint8 = (IPPROTO_TCP);
|
|
|
|
conds[2].fieldKey = FWPM_CONDITION_IP_REMOTE_PORT;
|
|
conds[2].matchType = FWP_MATCH_EQUAL;
|
|
conds[2].conditionValue.type = FWP_UINT16;
|
|
conds[2].conditionValue.uint16 = port;
|
|
|
|
// Assemble the Filter base
|
|
FWPM_FILTER0 filter;
|
|
memset(&filter, 0, sizeof(filter));
|
|
filter.filterCondition = conds;
|
|
filter.numFilterConditions = 3;
|
|
filter.action.type = FWP_ACTION_BLOCK;
|
|
filter.weight.type = FWP_UINT8;
|
|
filter.weight.uint8 = weight;
|
|
filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY;
|
|
|
|
QString description("Block %1 on Port %2");
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
|
|
if (!enableFilter(&filter, title, description.arg("outgoing v6").arg(port))) {
|
|
return false;
|
|
}
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
|
|
if (!enableFilter(&filter, title, description.arg("outgoing v4").arg(port))) {
|
|
return false;
|
|
}
|
|
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4;
|
|
if (!enableFilter(&filter, title, description.arg("incoming v4").arg(port))) {
|
|
return false;
|
|
}
|
|
filter.layerKey = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
|
|
if (!enableFilter(&filter, title, description.arg("incoming v6").arg(port))) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::enableFilter(FWPM_FILTER0* filter, const QString& title,
|
|
const QString& description,
|
|
const QString& peer) {
|
|
uint64_t filterID = 0;
|
|
auto name = title.toStdWString();
|
|
auto desc = description.toStdWString();
|
|
filter->displayData.name = (PWSTR)name.c_str();
|
|
filter->displayData.description = (PWSTR)desc.c_str();
|
|
auto result = FwpmFilterAdd0(m_sessionHandle, filter, NULL, &filterID);
|
|
if (result != ERROR_SUCCESS) {
|
|
logger.error() << "Failed to enable filter: " << title << " "
|
|
<< description;
|
|
return false;
|
|
}
|
|
logger.info() << "Filter added: " << title << ":" << description;
|
|
if (peer.isEmpty()) {
|
|
m_activeRules.append(filterID);
|
|
} else {
|
|
m_peerRules.insert(peer, filterID);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WindowsFirewall::allowLoopbackTraffic(uint8_t weight,
|
|
const QString& title) {
|
|
QList<QNetworkInterface> networkInterfaces =
|
|
QNetworkInterface::allInterfaces();
|
|
for (const auto& iface : networkInterfaces) {
|
|
if (iface.type() != QNetworkInterface::Loopback) {
|
|
continue;
|
|
}
|
|
if (!allowTrafficOfAdapter(iface.index(), weight,
|
|
title.arg(iface.name()))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|