Compare commits

...

3 Commits

Author SHA1 Message Date
NickVs2015
b337f06c0b feat: add macOS support to network diagnostics 2026-08-17 23:52:23 +03:00
NickVs2015
6498c55262 fix: support BOM encode Windows 2026-08-17 22:26:52 +03:00
NickVs2015
f4dab94b21 feat: add network diagnostics to Settings 2026-08-17 21:48:56 +03:00
15 changed files with 1115 additions and 7 deletions

View File

@@ -131,6 +131,18 @@ void SettingsUiController::clearLogs()
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
}
bool SettingsUiController::runNetworkDiagnostics()
{
return Logger::runNetworkDiagnostics();
}
void SettingsUiController::exportNetworkDiagnosticsFile(const QString &fileName)
{
if (!SystemController::saveFile(fileName, Logger::getNetworkDiagnosticsFile())) {
qInfo() << "SettingsUiController::exportNetworkDiagnosticsFile: save or share was cancelled or failed";
}
}
void SettingsUiController::backupAppConfig(const QString &fileName)
{
QByteArray data = m_settingsController->backupAppConfig();

View File

@@ -51,6 +51,9 @@ public slots:
void exportServiceLogsFile(const QString &fileName);
void clearLogs();
bool runNetworkDiagnostics();
void exportNetworkDiagnosticsFile(const QString &fileName);
void backupAppConfig(const QString &fileName);
void restoreAppConfig(const QString &fileName);
void restoreAppConfigFromData(const QByteArray &data);

View File

@@ -100,6 +100,45 @@ PageType {
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
DividerType {
visible: networkDiagnosticsAvailable
}
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: -8
visible: networkDiagnosticsAvailable
text: qsTr("Run network diagnostics")
leftImageSource: "qrc:/images/controls/scan-line.svg"
isSmallLeftImage: true
clickedFunction: function() {
var headerText = qsTr("Run network diagnostics?")
var descriptionText = qsTr("This collects local network configuration (adapters, routes, DNS, firewall rules and similar) through the background service and saves it as a new Amnezia-network log file, named with the current date and time. It takes a few seconds and does not open any window.")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
PageController.showBusyIndicator(true)
var success = SettingsController.runNetworkDiagnostics()
PageController.showBusyIndicator(false)
if (success) {
PageController.showNotificationMessage(qsTr("Network diagnostics saved"))
} else {
PageController.showNotificationMessage(qsTr("Network diagnostics failed"))
}
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}
model: logTypes
@@ -165,15 +204,20 @@ PageType {
}
}
readonly property bool networkDiagnosticsAvailable: Qt.platform.os === "windows" || Qt.platform.os === "linux" || Qt.platform.os === "osx"
// Show service logs only if this is NOT a macOS build with
// Network-Extension (IsMacOsNeBuild is injected from C++ at run-time)
// or if this is NOT a mobile build
property list<QtObject> logTypes: (IsMacOsNeBuild || GC.isMobile()) ? [
clientLogs
] : [
clientLogs,
serviceLogs
]
property list<QtObject> logTypes: {
if (IsMacOsNeBuild || GC.isMobile()) {
return [clientLogs]
} else if (networkDiagnosticsAvailable) {
return [clientLogs, serviceLogs, networkDiagnosticsLog]
} else {
return [clientLogs, serviceLogs]
}
}
QtObject {
id: clientLogs
@@ -228,4 +272,29 @@ PageType {
}
}
}
QtObject {
id: networkDiagnosticsLog
readonly property string title: qsTr("Network diagnostics")
readonly property string description: qsTr("Local network configuration snapshot collected via the background service")
readonly property bool isVisible: true
readonly property var openLogsHandler: function() {
SettingsController.openLogsFolder()
}
readonly property var exportLogsHandler: function() {
var timestamp = Qt.formatDateTime(new Date(), "yyyy-MM-dd_HH-mm-ss")
var fileName = SystemController.getFileName(qsTr("Save"),
qsTr("Logs files (*.log)"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/Amnezia-network-" + timestamp,
true,
".log")
if (fileName !== "") {
PageController.showBusyIndicator(true)
SettingsController.exportNetworkDiagnosticsFile(fileName)
PageController.showBusyIndicator(false)
PageController.showNotificationMessage(qsTr("Logs file saved"))
}
}
}
}

View File

@@ -7,6 +7,7 @@
#include <QJsonDocument>
#include <QMetaEnum>
#include <QStandardPaths>
#include <QSysInfo>
#include <QUrl>
#include "core/utils/utilities.h"
@@ -104,6 +105,50 @@ bool Logger::setServiceLogsEnabled(bool enabled)
return true;
}
bool Logger::runNetworkDiagnostics()
{
#ifdef AMNEZIA_DESKTOP
return IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<QString> reply = iface->runNetworkDiagnostics();
// Must exceed the service-side script timeout (30s, networkdiagnostics.cpp)
// plus margin for the IPC round-trip.
if (!reply.waitForFinished(35000)) {
qWarning() << "Logger::runNetworkDiagnostics(): IPC call timed out";
return false;
}
const QString result = reply.returnValue();
if (result.startsWith(QLatin1String("ERROR:"))) {
qWarning() << "Logger::runNetworkDiagnostics():" << result;
return false;
}
QDir().mkpath(userLogsDir());
const QString path = newNetworkDiagnosticsFilePath();
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "Logger::runNetworkDiagnostics(): failed to open" << path;
return false;
}
QTextStream ts(&file);
ts << QString("===== Amnezia network diagnostics - %1 =====\n%2 (%3)\n%4 %5 %6\n\n")
.arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-dd hh:mm:ss.zzz'Z'"),
QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture(),
APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
ts << result << "\n\n";
qDebug() << "Logger::runNetworkDiagnostics(): saved to" << path;
return true;
}, []() {
qWarning() << "Logger::runNetworkDiagnostics(): Service is not running";
return false;
});
#else
return false;
#endif
}
QString Logger::userLogsDir()
{
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/log";
@@ -135,6 +180,24 @@ QString Logger::serviceLogsFilePath()
return systemLogDir() + QDir::separator() + m_serviceLogFileName;
}
QString Logger::newNetworkDiagnosticsFilePath()
{
const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss");
return userLogsDir() + QDir::separator() + QStringLiteral("Amnezia-network-%1.log").arg(timestamp);
}
QString Logger::latestNetworkDiagnosticsFilePath()
{
QDir dir(userLogsDir());
// "yyyy-MM-dd_HH-mm-ss" sorts lexicographically in chronological order,
// so the last entry in a name-sorted listing is the most recent run.
const QStringList files = dir.entryList({ QStringLiteral("Amnezia-network-*.log") }, QDir::Files, QDir::Name);
if (files.isEmpty()) {
return QString();
}
return dir.filePath(files.last());
}
QString Logger::getLogFile()
{
if (m_file.isOpen()) {
@@ -169,6 +232,17 @@ QString Logger::getServiceLogFile()
#endif
}
QString Logger::getNetworkDiagnosticsFile()
{
const QString path = latestNetworkDiagnosticsFilePath();
if (path.isEmpty()) {
return QString();
}
QFile file(path);
file.open(QIODevice::ReadOnly);
return QString::fromUtf8(file.readAll());
}
bool Logger::openLogsFolder(bool isServiceLogger)
{
QString path = isServiceLogger ? systemLogDir() : userLogsDir();

View File

@@ -20,6 +20,7 @@ public:
static void deInit();
static bool setServiceLogsEnabled(bool enabled);
static bool runNetworkDiagnostics();
static bool openLogsFolder(bool isServiceLogger);
@@ -29,10 +30,13 @@ public:
static QString userLogsFilePath();
static QString serviceLogsFilePath();
static QString newNetworkDiagnosticsFilePath();
static QString latestNetworkDiagnosticsFilePath();
static QString systemLogDir();
static QString getLogFile();
static QString getServiceLogFile();
static QString getNetworkDiagnosticsFile();
// compat with Mozilla logger
Logger(const QString &className)

View File

@@ -44,6 +44,10 @@ class IpcInterface
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
SLOT( bool stopNetworkCheck() );
// Runs the bundled platform diagnostics script and returns the concatenated
// section output, or "ERROR: <reason>" on failure.
SLOT( QString runNetworkDiagnostics() );
SIGNAL( connectionLose() );
SIGNAL( wakeup() );
SIGNAL( networkChanged() );

View File

@@ -16,6 +16,7 @@
#include "logger.h"
#include "router.h"
#include "killswitch.h"
#include "networkdiagnostics.h"
#include "xray.h"
#ifdef Q_OS_WIN
@@ -241,6 +242,15 @@ bool IpcServer::stopNetworkCheck()
return true;
}
QString IpcServer::runNetworkDiagnostics()
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::runNetworkDiagnostics";
#endif
return NetworkDiagnostics::run();
}
bool IpcServer::resetKillSwitchAllowedRange(QStringList ranges)
{
#ifdef MZ_DEBUG

View File

@@ -46,6 +46,7 @@ public:
virtual bool xrayStop() override;
virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override;
virtual bool stopNetworkCheck() override;
virtual QString runNetworkDiagnostics() override;
private:
int m_localpid = 0;

View File

@@ -32,6 +32,7 @@ set(HEADERS
${CMAKE_CURRENT_LIST_DIR}/killswitch.h
${CMAKE_CURRENT_LIST_DIR}/systemservice.h
${CMAKE_CURRENT_LIST_DIR}/xray.h
${CMAKE_CURRENT_LIST_DIR}/networkdiagnostics.h
${CMAKE_CURRENT_BINARY_DIR}/version.h
)
@@ -49,6 +50,7 @@ set(SOURCES
${CMAKE_CURRENT_LIST_DIR}/killswitch.cpp
${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp
${CMAKE_CURRENT_LIST_DIR}/xray.cpp
${CMAKE_CURRENT_LIST_DIR}/networkdiagnostics.cpp
)
# Mozilla headres
@@ -275,6 +277,10 @@ if(LINUX)
endif()
qt6_add_resources(QRC ${QRC}
${CMAKE_CURRENT_LIST_DIR}/network_diagnostics/networkDiagnostics.qrc
)
include(${CMAKE_CURRENT_LIST_DIR}/../src/qtservice.cmake)
include_directories(
@@ -287,7 +293,7 @@ include_directories(
)
add_executable(${PROJECT} ${SOURCES} ${HEADERS} ${RESOURCES})
add_executable(${PROJECT} ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC})
target_link_libraries(${PROJECT} PRIVATE Qt6::Core Qt6::Widgets Qt6::Network Qt6::RemoteObjects Qt6::Core5Compat Qt6::DBus Qt6::Concurrent ${LIBS})
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")

View File

@@ -0,0 +1,295 @@
#!/usr/bin/env bash
# amnezia-baseline-snapshot.sh
# Linux network state snapshot for establishing a baseline.
# Run with sudo — otherwise some data (socket owners, iptables,
# some modules) will be incomplete.
#
# Usage (three runs):
# sudo ./amnezia-baseline-snapshot.sh --label clean
# sudo ./amnezia-baseline-snapshot.sh --label awg --endpoint <server IP>
# sudo ./amnezia-baseline-snapshot.sh --label xray --endpoint <server IP>
#
# Then compare the folders, e.g.:
# diff snapshot-clean/routes.txt snapshot-awg/routes.txt
set -uo pipefail
LABEL="snapshot"
ENDPOINT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--label) LABEL="$2"; shift 2 ;;
--label=*) LABEL="${1#*=}"; shift ;;
--endpoint) ENDPOINT="$2"; shift 2 ;;
--endpoint=*) ENDPOINT="${1#*=}"; shift ;;
-h|--help)
grep '^#' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
OUT="$(pwd)/snapshot-${LABEL}"
mkdir -p "$OUT"
have() { command -v "$1" >/dev/null 2>&1; }
# Runs block function $2, writes a timestamped header plus its output
# (stdout+stderr) to $OUT/$1.txt. Errors inside a block don't abort the script.
save() {
local name="$1" fn="$2" path
path="$OUT/$name.txt"
{
echo "=== $name === $(date -Is)"
"$fn" 2>&1
} > "$path"
echo " [ok] $name"
}
echo "Snapshot '$LABEL' -> $OUT"
[ "$(id -u)" -eq 0 ] || echo " [warn] not running as root — some data will be incomplete"
# --- 1. System ---------------------------------------------------------
system_block() {
echo "--- OS release ---"
cat /etc/os-release 2>/dev/null
echo
echo "--- Kernel / hostname ---"
uname -a
have hostnamectl && hostnamectl 2>&1
echo
echo "IsRoot: $([ "$(id -u)" -eq 0 ] && echo True || echo False)"
echo "Init system: $(ps -p 1 -o comm= 2>/dev/null)"
}
save "system" system_block
# --- 2. Adapters, including hidden and disabled -----------------------------
# Key for detecting an "overwritten driver" and leftover interfaces.
adapters_block() {
echo "--- ip -d link (all interfaces, including down) ---"
ip -d link show
echo
echo "--- ip addr ---"
ip addr show
echo
echo "--- driver info (ethtool -i) for each interface ---"
for p in /sys/class/net/*; do
ifc=$(basename "$p")
[ "$ifc" = "lo" ] && continue
echo "-- $ifc --"
if have ethtool; then
ethtool -i "$ifc" 2>&1
else
echo "ethtool not installed"
fi
done
echo
echo "--- nmcli device (if NetworkManager is present) ---"
have nmcli && nmcli -f all device show 2>&1
}
save "adapters" adapters_block
# --- 3. Connection type (cellular / wifi / ethernet) ------------------------
link_type_block() {
echo "--- type detection via /sys and iw ---"
for p in /sys/class/net/*; do
ifc=$(basename "$p")
[ "$ifc" = "lo" ] && continue
t="other"
if [ -d "$p/wireless" ] || { have iw && iw dev "$ifc" info >/dev/null 2>&1; }; then
t="wifi"
elif [[ "$ifc" == wwan* || "$ifc" == ww* || "$ifc" == usb* ]]; then
t="cellular"
elif [[ "$ifc" == en* || "$ifc" == eth* ]]; then
t="ethernet"
elif [[ "$ifc" == wg* || "$ifc" == tun* || "$ifc" == tap* ]]; then
t="vpn-tunnel"
fi
operstate=$(cat "$p/operstate" 2>/dev/null)
carrier=$(cat "$p/carrier" 2>/dev/null)
echo "$ifc: type=$t operstate=$operstate carrier=$carrier"
done
echo
echo "--- NetworkManager connection profiles ---"
have nmcli && nmcli connection show 2>&1
}
save "link-type" link_type_block
# --- 4. Drivers: kernel modules and network filters --------------------------
# Catches the wireguard module, tun/tap, and netfilter hooks used by
# split-tunnel solutions.
drivers_block() {
echo "--- lsmod (all loaded modules) ---"
lsmod
echo
echo "--- VPN/netfilter related modules ---"
lsmod | grep -E 'wireguard|^tun|^tap|nf_tables|ip_tables|ip6_tables|nfnetlink|xt_|nft_'
echo
echo "--- modinfo wireguard (if kernel module, not a userspace implementation) ---"
have modinfo && modinfo wireguard 2>&1
}
save "drivers" drivers_block
# --- 5. Routes ---------------------------------------------------------
routes_block() {
echo "--- ip route (main table) ---"
ip route show
echo
echo "--- ip route show table all ---"
ip route show table all
echo
echo "--- ip -6 route ---"
ip -6 route show table all
echo
echo "--- ip rule / ip -6 rule (policy routing) ---"
ip rule show
ip -6 rule show
echo
echo "--- default routes only ---"
ip route show default
ip -6 route show default
echo
echo "--- interfaces: addresses and indexes ---"
ip -o addr show
}
save "routes" routes_block
# --- 6. Actual routing stack resolution --------------------------------------
# What you can't see in `ip route show`: where the packet will ACTUALLY go.
route_resolution_block() {
targets=("8.8.8.8" "1.1.1.1")
if [ -n "$ENDPOINT" ]; then targets=("$ENDPOINT" "${targets[@]}"); fi
for t in "${targets[@]}"; do
echo "--- ip route get $t ---"
ip route get "$t" 2>&1
echo
done
}
save "route-resolution" route_resolution_block
# --- 7. DNS -----------------------------------------------------------------
dns_block() {
echo "--- /etc/resolv.conf ---"
cat /etc/resolv.conf 2>&1
echo
echo "--- resolvectl status (systemd-resolved) ---"
have resolvectl && resolvectl status 2>&1
echo
echo "--- resolvectl dns / domain ---"
have resolvectl && { resolvectl dns 2>&1; resolvectl domain 2>&1; }
echo
echo "--- NetworkManager DNS (VPN clients write here via nm) ---"
have nmcli && nmcli device show 2>&1 | grep -i 'DNS\|GENERAL.DEVICE'
echo
echo "--- /etc/nsswitch.conf (hosts line) ---"
grep '^hosts' /etc/nsswitch.conf 2>&1
}
save "dns" dns_block
# --- 8. IPv6 and localhost ---------------------------------------------
ipv6_localhost_block() {
echo "--- IP addresses (all families) ---"
ip -o addr show
echo
echo "--- IPv6 disabled via sysctl? ---"
sysctl net.ipv6.conf.all.disable_ipv6 net.ipv6.conf.default.disable_ipv6 2>&1
echo
echo "--- localhost resolution ---"
getent hosts localhost
getent ahosts localhost
echo
echo "--- hosts file ---"
cat /etc/hosts
}
save "ipv6-localhost" ipv6_localhost_block
# --- 9. Proxy: environment and system settings ------------------------------
# Key block for Xray: it often works through the system proxy.
proxy_block() {
echo "--- environment variables ---"
env | grep -i proxy
echo
echo "--- /etc/environment ---"
grep -i proxy /etc/environment 2>/dev/null
echo
echo "--- GNOME gsettings proxy (if present) ---"
have gsettings && gsettings get org.gnome.system.proxy mode 2>&1
echo
echo "--- apt proxy config (if present) ---"
cat /etc/apt/apt.conf.d/*proxy* 2>/dev/null
}
save "proxy" proxy_block
# --- 10. VPN/antiDPI services and processes ----------------------------
services_processes_block() {
local pattern='amnezia|wireguard|wg-quick|openvpn|tap|mullvad|tailscale|zapret|nfqws|winws|xray|v2ray|clash|outline|hiddify|proton|nord|express'
echo "--- systemd units matching pattern ---"
if have systemctl; then
systemctl list-units --all --type=service --no-legend 2>/dev/null | grep -Ei "$pattern"
fi
echo
echo "--- processes ---"
ps aux | grep -Ei "$pattern" | grep -v grep
echo
echo "--- listening TCP sockets (Xray inbounds) ---"
if have ss; then
ss -tlnp
else
netstat -tlnp
fi
echo
echo "--- UDP endpoints (AmneziaWG) ---"
if have ss; then
ss -ulnp
else
netstat -ulnp
fi
}
save "services-processes" services_processes_block
# --- 11. Firewall / netfilter ------------------------------------------------
firewall_block() {
echo "--- iptables -L -n -v ---"
have iptables && iptables -L -n -v 2>&1
echo
echo "--- ip6tables -L -n -v ---"
have ip6tables && ip6tables -L -n -v 2>&1
echo
echo "--- nft list ruleset (first ~400 lines) ---"
have nft && nft list ruleset 2>&1 | head -n 400
echo "(output truncated; see netfilter-full.txt for the full dump)"
echo
echo "--- ufw status ---"
have ufw && ufw status verbose 2>&1
echo
echo "--- firewalld ---"
if have firewall-cmd; then
firewall-cmd --state 2>&1
firewall-cmd --list-all 2>&1
fi
}
save "firewall" firewall_block
# --- 12. Full netfilter ruleset dump (heavy output, separate file) ----------
netfilter_full_block() {
echo "--- iptables-save ---"
have iptables-save && iptables-save 2>&1
echo
echo "--- ip6tables-save ---"
have ip6tables-save && ip6tables-save 2>&1
echo
echo "--- nft list ruleset (full) ---"
have nft && nft list ruleset 2>&1
}
save "netfilter-full" netfilter_full_block
echo
echo "Done. Folder: $OUT"
echo "Diff of two runs, e.g.:"
echo " diff snapshot-clean/routes.txt snapshot-awg/routes.txt"

View File

@@ -0,0 +1,288 @@
#!/usr/bin/env bash
# amnezia-baseline-snapshot.sh
# macOS network state snapshot for establishing a baseline.
# Run with sudo — otherwise some data (socket owners, pf rules,
# some kext/system_profiler details) will be incomplete.
#
# Usage (three runs):
# sudo ./amnezia-baseline-snapshot.sh --label clean
# sudo ./amnezia-baseline-snapshot.sh --label awg --endpoint <server IP>
# sudo ./amnezia-baseline-snapshot.sh --label xray --endpoint <server IP>
#
# Then compare the folders, e.g.:
# diff snapshot-clean/routes.txt snapshot-awg/routes.txt
set -uo pipefail
LABEL="snapshot"
ENDPOINT=""
while [[ $# -gt 0 ]]; do
case "$1" in
--label) LABEL="$2"; shift 2 ;;
--label=*) LABEL="${1#*=}"; shift ;;
--endpoint) ENDPOINT="$2"; shift 2 ;;
--endpoint=*) ENDPOINT="${1#*=}"; shift ;;
-h|--help)
grep '^#' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
OUT="$(pwd)/snapshot-${LABEL}"
mkdir -p "$OUT"
have() { command -v "$1" >/dev/null 2>&1; }
# Runs block function $2, writes a timestamped header plus its output
# (stdout+stderr) to $OUT/$1.txt. Errors inside a block don't abort the script.
save() {
local name="$1" fn="$2" path
path="$OUT/$name.txt"
{
echo "=== $name === $(date -Iseconds 2>/dev/null || date)"
"$fn" 2>&1
} > "$path"
echo " [ok] $name"
}
echo "Snapshot '$LABEL' -> $OUT"
[ "$(id -u)" -eq 0 ] || echo " [warn] not running as root — some data will be incomplete"
# --- 1. System ---------------------------------------------------------
system_block() {
echo "--- sw_vers ---"
sw_vers 2>&1
echo
echo "--- uname / hostname ---"
uname -a
hostname
echo
echo "IsRoot: $([ "$(id -u)" -eq 0 ] && echo True || echo False)"
}
save "system" system_block
# --- 2. Adapters, including hidden and disabled -----------------------------
# Key for detecting an "overwritten driver" and leftover interfaces.
adapters_block() {
echo "--- ifconfig -a (all interfaces, including down) ---"
ifconfig -a
echo
echo "--- networksetup -listallhardwareports ---"
have networksetup && networksetup -listallhardwareports 2>&1
echo
echo "--- networksetup -listallnetworkservices ---"
have networksetup && networksetup -listallnetworkservices 2>&1
}
save "adapters" adapters_block
# --- 3. Connection type (cellular / wifi / ethernet) ------------------------
link_type_block() {
echo "--- type detection via ifconfig media / networksetup ---"
for ifc in $(ifconfig -l 2>/dev/null); do
[ "$ifc" = "lo0" ] && continue
t="other"
case "$ifc" in
en*)
if have networksetup && networksetup -getairportnetwork "$ifc" >/dev/null 2>&1; then
t="wifi"
else
t="ethernet-or-wifi"
fi
;;
pdp_ip*|wwan*) t="cellular" ;;
utun*|ppp*|ipsec*) t="vpn-tunnel" ;;
awg*|wg*) t="vpn-tunnel" ;;
esac
status=$(ifconfig "$ifc" 2>/dev/null | awk '/status:/{print $2}')
echo "$ifc: type=$t status=$status"
done
echo
echo "--- current Wi-Fi network (if any) ---"
if have networksetup; then
for ifc in $(ifconfig -l 2>/dev/null); do
case "$ifc" in en*)
networksetup -getairportnetwork "$ifc" 2>&1
;; esac
done
fi
echo
echo "--- scutil --nwi (primary interface / reachability) ---"
have scutil && scutil --nwi 2>&1
}
save "link-type" link_type_block
# --- 4. Drivers: kernel extensions and system network config ----------------
# Catches the WireGuard/tun kext or sysex, and split-tunnel related extensions.
drivers_block() {
echo "--- kextstat (legacy kexts, if any) ---"
have kextstat && kextstat 2>&1
echo
echo "--- systemextensionsctl list (modern DriverKit/NetworkExtension sysexes) ---"
have systemextensionsctl && systemextensionsctl list 2>&1
echo
echo "--- VPN/netfilter related kexts ---"
have kextstat && kextstat 2>&1 | grep -iE 'wireguard|tun|tap|utun|amnezia'
}
save "drivers" drivers_block
# --- 5. Routes ---------------------------------------------------------
routes_block() {
echo "--- netstat -nr (IPv4 + IPv6 routing table) ---"
netstat -nr
echo
echo "--- netstat -nr -f inet (IPv4 only) ---"
netstat -nr -f inet
echo
echo "--- netstat -nr -f inet6 (IPv6 only) ---"
netstat -nr -f inet6
echo
echo "--- default routes only ---"
netstat -nr | awk 'NR==1 || /^default/'
echo
echo "--- interfaces: addresses ---"
ifconfig -a | grep -E '^[a-z]|inet '
}
save "routes" routes_block
# --- 6. Actual routing stack resolution --------------------------------------
# What you can't see in `netstat -nr`: where the packet will ACTUALLY go.
route_resolution_block() {
targets=("8.8.8.8" "1.1.1.1")
if [ -n "$ENDPOINT" ]; then targets=("$ENDPOINT" "${targets[@]}"); fi
for t in "${targets[@]}"; do
echo "--- route -n get $t ---"
route -n get "$t" 2>&1
echo
done
}
save "route-resolution" route_resolution_block
# --- 7. DNS -----------------------------------------------------------------
dns_block() {
echo "--- scutil --dns (effective per-interface resolver config) ---"
have scutil && scutil --dns 2>&1
echo
echo "--- /etc/resolv.conf (usually managed/symlinked on macOS) ---"
cat /etc/resolv.conf 2>&1
echo
echo "--- networksetup -getdnsservers per network service ---"
if have networksetup; then
while IFS= read -r svc; do
[[ "$svc" == "An asterisk"* ]] && continue
echo "-- $svc --"
networksetup -getdnsservers "$svc" 2>&1
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
fi
}
save "dns" dns_block
# --- 8. IPv6 and localhost ---------------------------------------------
ipv6_localhost_block() {
echo "--- IP addresses (all families) ---"
ifconfig -a | grep -E '^[a-z]|inet '
echo
echo "--- IPv6 disabled? (per-interface sysctl / networksetup) ---"
sysctl net.inet6.ip6.forwarding 2>&1
if have networksetup; then
while IFS= read -r svc; do
[[ "$svc" == "An asterisk"* ]] && continue
echo "-- $svc --"
networksetup -getinfo "$svc" 2>&1 | grep -i ipv6
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
fi
echo
echo "--- localhost resolution ---"
dscacheutil -q host -a name localhost 2>&1
echo
echo "--- hosts file ---"
cat /etc/hosts
}
save "ipv6-localhost" ipv6_localhost_block
# --- 9. Proxy: environment and system settings ------------------------------
# Key block for Xray: it often works through the system proxy.
proxy_block() {
echo "--- environment variables ---"
env | grep -i proxy
echo
echo "--- scutil --proxy (system-wide proxy configuration) ---"
have scutil && scutil --proxy 2>&1
echo
echo "--- networksetup proxy settings per network service ---"
if have networksetup; then
while IFS= read -r svc; do
[[ "$svc" == "An asterisk"* ]] && continue
echo "-- $svc --"
networksetup -getwebproxy "$svc" 2>&1
networksetup -getsecurewebproxy "$svc" 2>&1
networksetup -getsocksfirewallproxy "$svc" 2>&1
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
fi
}
save "proxy" proxy_block
# --- 10. VPN/antiDPI services and processes ----------------------------
services_processes_block() {
local pattern='amnezia|wireguard|wg-quick|openvpn|tap|mullvad|tailscale|zapret|nfqws|winws|xray|v2ray|clash|outline|hiddify|proton|nord|express'
echo "--- launchctl list matching pattern ---"
have launchctl && launchctl list 2>/dev/null | grep -Ei "$pattern"
echo
echo "--- processes ---"
ps aux | grep -Ei "$pattern" | grep -v grep
echo
echo "--- listening TCP sockets (Xray inbounds) ---"
if have lsof; then
lsof -nP -iTCP -sTCP:LISTEN 2>&1
else
netstat -anp tcp 2>&1
fi
echo
echo "--- UDP endpoints (AmneziaWG) ---"
if have lsof; then
lsof -nP -iUDP 2>&1
else
netstat -anp udp 2>&1
fi
}
save "services-processes" services_processes_block
# --- 11. Firewall / packet filter ------------------------------------------------
# Amnezia loads its rules under the pf anchor "amn" (see macosfirewall.cpp).
firewall_block() {
echo "--- pfctl -s info (pf enabled/disabled, stats) ---"
have pfctl && pfctl -s info 2>&1
echo
echo "--- pfctl -s Anchors (all loaded anchors) ---"
have pfctl && pfctl -s Anchors 2>&1
echo
echo "--- pfctl -a amn -s rules (Amnezia's own anchor + sub-anchors) ---"
if have pfctl; then
anchors=$(pfctl -s Anchors 2>/dev/null | awk '/^amn/ {sub(/\*$/, "", $1); print $1}')
for anc in $anchors; do
echo "-- anchor: $anc --"
pfctl -a "$anc" -s rules 2>&1
done
fi
echo
echo "--- pfctl -s rules (rules in the main ruleset, not under any anchor) ---"
have pfctl && pfctl -s rules 2>&1
echo
echo "--- socketfilterfw (Application Firewall) ---"
if [ -x /usr/libexec/ApplicationFirewall/socketfilterfw ]; then
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>&1
/usr/libexec/ApplicationFirewall/socketfilterfw --listapps 2>&1
fi
}
save "firewall" firewall_block
echo
echo "Done. Folder: $OUT"
echo "Diff of two runs, e.g.:"
echo " diff snapshot-clean/routes.txt snapshot-awg/routes.txt"

View File

@@ -0,0 +1,219 @@
# network-diagnostics-windows.ps1
# Windows network state snapshot for establishing a baseline.
# Run in PowerShell AS ADMINISTRATOR.
#
# Usage (three runs):
# .\network-diagnostics-windows.ps1 -Label clean
# .\network-diagnostics-windows.ps1 -Label awg -Endpoint <server IP>
# .\network-diagnostics-windows.ps1 -Label xray -Endpoint <server IP>
#
# Then compare the folders, e.g.:
# Compare-Object (gc .\snapshot-clean\routes.txt) (gc .\snapshot-awg\routes.txt)
param(
[string]$Label = "snapshot",
[string]$Endpoint = ""
)
$out = Join-Path (Get-Location) "snapshot-$Label"
New-Item -ItemType Directory -Path $out -Force | Out-Null
function Save($name, $block) {
$path = Join-Path $out "$name.txt"
"=== $name === $(Get-Date -Format o)" | Out-File $path -Encoding utf8
try { & $block *>&1 | Out-File $path -Append -Encoding utf8 }
catch { "ERROR: $($_.Exception.Message)" | Out-File $path -Append -Encoding utf8 }
Write-Host " [ok] $name"
}
Write-Host "Snapshot '$Label' -> $out"
# --- 1. System ---------------------------------------------------------
Save "system" {
Get-ComputerInfo -Property OsName, OsVersion, OsBuildNumber, CsName |
Format-List
"IsAdmin: " + ([Security.Principal.WindowsPrincipal]`
[Security.Principal.WindowsIdentity]::GetCurrent()`
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
# --- 2. Adapters, including hidden and disabled -----------------------------
# Key for detecting an "overwritten driver" and leftover interfaces.
Save "adapters" {
Get-NetAdapter -IncludeHidden |
Select-Object Name, InterfaceDescription, ifIndex, Status,
AdminStatus, MacAddress, LinkSpeed,
DriverProvider, DriverVersion, DriverDate,
DriverFileName, DriverInformation, Virtual, Hidden |
Format-List
}
# --- 3. Connection type (cellular / wifi / ethernet) ------------------------
Save "link-type" {
"--- NdisPhysicalMedium (802.3=ethernet, Native802.11=wifi, WirelessWan=cellular) ---"
Get-NetAdapter -IncludeHidden |
Select-Object Name, InterfaceType, NdisPhysicalMedium, MediaConnectionState |
Format-Table -AutoSize
"--- Connection profiles ---"
Get-NetConnectionProfile |
Select-Object Name, InterfaceAlias, NetworkCategory,
IPv4Connectivity, IPv6Connectivity |
Format-List
}
# --- 4. Drivers: network filters and tunnels --------------------------------
# Catches WinDivert-style filters, TAP/Wintun, and split-tunnel drivers.
Save "drivers-system" {
Get-CimInstance Win32_SystemDriver |
Where-Object { $_.PathName -match 'drivers' } |
Select-Object Name, DisplayName, State, StartMode, PathName |
Sort-Object Name | Format-Table -AutoSize
}
Save "drivers-store" {
pnputil /enum-drivers
}
Save "drivers-net-class" {
Get-CimInstance Win32_PnPSignedDriver |
Where-Object { $_.DeviceClass -eq 'NET' } |
Select-Object DeviceName, DriverProviderName, DriverVersion,
DriverDate, InfName, IsSigned, Signer |
Format-List
}
# --- 5. Routes ---------------------------------------------------------
Save "routes" {
"--- route print ---"
route print
"`n--- Get-NetRoute (IPv4+IPv6) ---"
Get-NetRoute |
Select-Object DestinationPrefix, NextHop, ifIndex, InterfaceAlias,
RouteMetric, InterfaceMetric, Protocol, AddressFamily |
Sort-Object AddressFamily, DestinationPrefix | Format-Table -AutoSize
"`n--- default routes only ---"
Get-NetRoute -DestinationPrefix '0.0.0.0/0','::/0' -ErrorAction SilentlyContinue |
Format-Table -AutoSize
"`n--- interface metrics ---"
Get-NetIPInterface |
Select-Object ifIndex, InterfaceAlias, AddressFamily,
InterfaceMetric, Dhcp, ConnectionState |
Sort-Object ifIndex | Format-Table -AutoSize
}
# --- 6. Actual routing stack resolution --------------------------------------
# What you can't see in route print: where the packet will ACTUALLY go.
Save "route-resolution" {
$targets = @('8.8.8.8', '1.1.1.1')
if ($Endpoint) { $targets = @($Endpoint) + $targets }
foreach ($t in $targets) {
"--- Find-NetRoute -RemoteIPAddress $t ---"
Find-NetRoute -RemoteIPAddress $t -ErrorAction SilentlyContinue |
Select-Object IPAddress, InterfaceAlias, ifIndex,
DestinationPrefix, NextHop, RouteMetric |
Format-List
""
}
}
# --- 7. DNS -----------------------------------------------------------------
Save "dns" {
"--- configured servers ---"
Get-DnsClientServerAddress |
Select-Object InterfaceAlias, ifIndex, AddressFamily, ServerAddresses |
Format-Table -AutoSize
"`n--- global settings ---"
Get-DnsClientGlobalSetting | Format-List
"`n--- NRPT rules (VPN clients write here) ---"
Get-DnsClientNrptPolicy -ErrorAction SilentlyContinue | Format-List
Get-DnsClientNrptRule -ErrorAction SilentlyContinue | Format-List
"`n--- DoH configuration ---"
Get-DnsClientDohServerAddress -ErrorAction SilentlyContinue | Format-Table -AutoSize
}
# --- 8. IPv6 and localhost ---------------------------------------------
Save "ipv6-localhost" {
"--- IP addresses ---"
Get-NetIPAddress |
Select-Object InterfaceAlias, IPAddress, PrefixLength,
AddressFamily, AddressState, SkipAsSource |
Sort-Object AddressFamily | Format-Table -AutoSize
"`n--- IPv6 global config ---"
netsh interface ipv6 show global
"`n--- IPv6 disabled via registry (DisabledComponents) ---"
Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters' `
-Name DisabledComponents -ErrorAction SilentlyContinue |
Select-Object DisabledComponents | Format-List
"`n--- localhost resolution ---"
Resolve-DnsName localhost -ErrorAction SilentlyContinue | Format-Table -AutoSize
"`n--- hosts file ---"
Get-Content "$env:SystemRoot\System32\drivers\etc\hosts" -ErrorAction SilentlyContinue
}
# --- 9. Proxy: system and browser (WinINET) ----------------------------
# Key block for Xray: it often works through the system proxy.
Save "proxy" {
"--- env vars ---"
Get-ChildItem Env: |
Where-Object { $_.Name -match 'PROXY|proxy' } |
Format-Table -AutoSize
"`n--- WinINET (HKCU, what the browser sees) ---"
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' `
-ErrorAction SilentlyContinue |
Select-Object ProxyEnable, ProxyServer, ProxyOverride, AutoConfigURL |
Format-List
"`n--- WinHTTP ---"
netsh winhttp show proxy
}
# --- 10. VPN/antiDPI services and processes ----------------------------
Save "services-processes" {
"--- services matching known patterns ---"
Get-Service | Where-Object {
$_.Name -match 'amnezia|wireguard|wg|openvpn|tap|mullvad|tailscale|zapret|nfqws|winws|xray|v2ray|clash|outline|hiddify|proton|nord|express'
} | Select-Object Name, DisplayName, Status, StartType | Format-Table -AutoSize
"`n--- processes ---"
Get-Process | Where-Object {
$_.ProcessName -match 'amnezia|wireguard|openvpn|mullvad|tailscale|nfqws|winws|goodbyedpi|byedpi|xray|v2ray|clash|tun2socks|sing-box'
} | Select-Object ProcessName, Id, Path | Format-Table -AutoSize
"`n--- listening sockets on loopback (Xray inbounds) ---"
Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $_.LocalAddress -in '127.0.0.1','::1','0.0.0.0','::' } |
Select-Object LocalAddress, LocalPort, OwningProcess,
@{n='Process';e={(Get-Process -Id $_.OwningProcess -EA SilentlyContinue).ProcessName}} |
Sort-Object LocalPort | Format-Table -AutoSize
"`n--- UDP endpoints (AmneziaWG) ---"
Get-NetUDPEndpoint -ErrorAction SilentlyContinue |
Select-Object LocalAddress, LocalPort, OwningProcess,
@{n='Process';e={(Get-Process -Id $_.OwningProcess -EA SilentlyContinue).ProcessName}} |
Sort-Object LocalPort | Format-Table -AutoSize
}
# --- 11. Firewall / WFP -----------------------------------------------------
Save "firewall" {
"--- profiles ---"
Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction,
DefaultOutboundAction | Format-Table -AutoSize
"`n--- enabled block rules (short list) ---"
Get-NetFirewallRule -Enabled True -ErrorAction SilentlyContinue |
Where-Object { $_.Action -eq 'Block' } |
Select-Object DisplayName, Direction, Profile, Owner |
Format-Table -AutoSize
"`n--- third-party security products ---"
Get-CimInstance -Namespace root\SecurityCenter2 -ClassName AntiVirusProduct `
-ErrorAction SilentlyContinue |
Select-Object displayName, productState | Format-List
}
# --- 12. WFP filters (heavy output, separate file) --------------------------
Save "wfp-filters" {
netsh wfp show filters file=- 2>$null | Select-Object -First 400
"(output truncated; for a full dump: netsh wfp show filters file=wfp.xml)"
}
Write-Host "`nDone. Folder: $out"
Write-Host "Diff of two runs, e.g.:"
Write-Host " Compare-Object (gc .\snapshot-clean\routes.txt) (gc .\snapshot-awg\routes.txt)"

View File

@@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/network_diagnostics">
<file>network-diagnostics-linux.sh</file>
<file>network-diagnostics-macos.sh</file>
<file>network-diagnostics-windows.ps1</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,101 @@
#include "networkdiagnostics.h"
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QTemporaryDir>
#include <QTextStream>
namespace {
constexpr int kProcessTimeoutMsec = 30000;
const QString kSnapshotLabel = QStringLiteral("diagnostics");
#if defined(Q_OS_WIN)
const QStringList kSectionOrder = { "system", "adapters", "link-type",
"drivers-system", "drivers-store", "drivers-net-class",
"routes", "route-resolution", "dns",
"ipv6-localhost", "proxy", "services-processes",
"firewall", "wfp-filters" };
#elif defined(Q_OS_LINUX)
const QStringList kSectionOrder = { "system", "adapters", "link-type", "drivers",
"routes", "route-resolution", "dns", "ipv6-localhost",
"proxy", "services-processes", "firewall", "netfilter-full" };
#elif defined(Q_OS_MACOS)
const QStringList kSectionOrder = { "system", "adapters", "link-type", "drivers",
"routes", "route-resolution", "dns", "ipv6-localhost",
"proxy", "services-processes", "firewall" };
#endif
}
QString NetworkDiagnostics::run()
{
#if !defined(Q_OS_WIN) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
return QStringLiteral("ERROR: network diagnostics is not supported on this platform");
#else
QTemporaryDir tempDir;
if (!tempDir.isValid()) {
return QStringLiteral("ERROR: failed to create secure temp directory");
}
#if defined(Q_OS_WIN)
const QString resourcePath = QStringLiteral(":/network_diagnostics/network-diagnostics-windows.ps1");
const QString scriptPath = tempDir.filePath(QStringLiteral("network-diagnostics-windows.ps1"));
#elif defined(Q_OS_MACOS)
const QString resourcePath = QStringLiteral(":/network_diagnostics/network-diagnostics-macos.sh");
const QString scriptPath = tempDir.filePath(QStringLiteral("network-diagnostics-macos.sh"));
#else
const QString resourcePath = QStringLiteral(":/network_diagnostics/network-diagnostics-linux.sh");
const QString scriptPath = tempDir.filePath(QStringLiteral("network-diagnostics-linux.sh"));
#endif
if (!QFile::copy(resourcePath, scriptPath)) {
return QStringLiteral("ERROR: failed to extract diagnostics script");
}
QProcess process;
process.setWorkingDirectory(tempDir.path()); // scripts write ./snapshot-<label>/ relative to CWD
#if defined(Q_OS_WIN)
process.start(QStringLiteral("powershell"),
{ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden",
"-File", scriptPath, "-Label", kSnapshotLabel });
#else
process.start(QStringLiteral("/bin/bash"), { scriptPath, "--label", kSnapshotLabel });
#endif
if (!process.waitForStarted(5000)) {
return QStringLiteral("ERROR: failed to start diagnostics script (%1)").arg(process.errorString());
}
if (!process.waitForFinished(kProcessTimeoutMsec)) {
process.kill();
process.waitForFinished(3000);
return QStringLiteral("ERROR: diagnostics script timed out after %1 s").arg(kProcessTimeoutMsec / 1000);
}
const QString snapshotDir = tempDir.filePath(QStringLiteral("snapshot-%1").arg(kSnapshotLabel));
QString combined;
QTextStream out(&combined);
int sectionsFound = 0;
for (const QString &section : kSectionOrder) {
QFile sectionFile(QStringLiteral("%1/%2.txt").arg(snapshotDir, section));
if (!sectionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
out << "=== " << section << " === (section missing)\n\n";
continue;
}
QString sectionText = QString::fromUtf8(sectionFile.readAll());
if (sectionText.startsWith(QChar(0xFEFF))) {
sectionText.remove(0, 1);
}
out << sectionText << "\n";
sectionsFound++;
}
if (sectionsFound == 0) {
return QStringLiteral("ERROR: diagnostics script produced no output (exit code %1)").arg(process.exitCode());
}
return combined;
#endif
}

View File

@@ -0,0 +1,15 @@
#ifndef NETWORKDIAGNOSTICS_H
#define NETWORKDIAGNOSTICS_H
#include <QString>
// Runs the bundled per-platform network diagnostics script (extracted from a
// Qt resource into a securely-created temp dir) and returns its concatenated
// section output, or an "ERROR: ..." string on failure.
class NetworkDiagnostics
{
public:
static QString run();
};
#endif // NETWORKDIAGNOSTICS_H