Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions 3rdparty/WinCommander.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@

class WinCommander {
public:
static const int SW_HIDE = 0;
static const int SW_NORMAL = 1;
static const int SW_SHOWMINIMIZED = 2;
// Do not reuse the Win32 SW_* names here: windows.h defines them as
// preprocessor macros, which also expand in qualified expressions such as
// WinCommander::SW_HIDE when CMake unity builds combine translation units.
static constexpr int WindowHidden = 0;
static constexpr int WindowNormal = 1;
static constexpr int WindowMinimized = 2;

static uint runProcessElevated(const QString &path,
const QStringList &parameters = QStringList(),
const QString &workingDir = QString(),
int nShow = SW_SHOWMINIMIZED, bool aWait = true);
};
int nShow = WindowMinimized, bool aWait = true);
};
30 changes: 30 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ set(PROJECT_SOURCES

include/sys/Process.hpp
src/sys/Process.cpp
include/sys/KillSwitchController.hpp
src/sys/KillSwitchController.cpp

include/sys/ProcessMetrics.hpp
src/sys/ProcessMetrics.cpp
Expand All @@ -162,6 +164,7 @@ set(PROJECT_SOURCES
src/ui/mainWindow/TestRunner.cpp
include/ui/mainWindow/MainWindowInternal.h
include/ui/mainWindow/TestRunner.h
src/ui/mainwindow_killswitch.cpp
include/ui/mainwindow.h
include/ui/mainwindow.ui
include/ui/widget/StartStopButton.hpp
Expand Down Expand Up @@ -358,6 +361,8 @@ set(PROJECT_SOURCES
src/configs/outbounds/vmess.cpp
src/configs/outbounds/wireguard.cpp
src/configs/outbounds/custom.cpp
include/configs/GeneratorUtils.h
src/configs/GeneratorUtils.cpp
include/configs/generate.h
src/configs/generate.cpp
include/configs/common/utils.h
Expand Down Expand Up @@ -512,3 +517,28 @@ target_link_libraries(Throne PRIVATE
)

qt_finalize_executable(Throne)

include(CTest)
if (BUILD_TESTING)
add_executable(KillSwitchControllerTest
tests/KillSwitchControllerTest.cpp
include/sys/KillSwitchController.hpp
src/sys/KillSwitchController.cpp
)
target_include_directories(KillSwitchControllerTest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(KillSwitchControllerTest PRIVATE Qt6::Core)
add_test(NAME KillSwitchControllerTest COMMAND KillSwitchControllerTest)

add_executable(GeneratorUtilsTest
tests/GeneratorUtilsTest.cpp
include/configs/GeneratorUtils.h
src/configs/GeneratorUtils.cpp
)
target_include_directories(GeneratorUtilsTest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(GeneratorUtilsTest PRIVATE Qt6::Core Qt6::Network)
add_test(NAME GeneratorUtilsTest COMMAND GeneratorUtilsTest)
endif ()
4 changes: 2 additions & 2 deletions cmake/windows/windows.cmake
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
set(PLATFORM_SOURCES 3rdparty/WinCommander.cpp src/sys/windows/guihelper.cpp src/sys/windows/MiniDump.cpp src/sys/windows/eventHandler.cpp src/sys/windows/WinVersion.cpp src/sys/windows/AutoRun.cpp src/sys/windows/UrlScheme.cpp)
set(PLATFORM_LIBRARIES wininet wsock32 ws2_32 user32 rasapi32 iphlpapi ntdll wbemuuid psapi shell32)
set(PLATFORM_SOURCES 3rdparty/WinCommander.cpp src/sys/windows/guihelper.cpp src/sys/windows/MiniDump.cpp src/sys/windows/eventHandler.cpp src/sys/windows/WinVersion.cpp src/sys/windows/AutoRun.cpp src/sys/windows/UrlScheme.cpp src/sys/windows/WindowsWfpKillSwitchBackend.cpp)
set(PLATFORM_LIBRARIES wininet wsock32 ws2_32 user32 rasapi32 iphlpapi ntdll wbemuuid psapi shell32 fwpuclnt uuid)

include(cmake/windows/generate_product_version.cmake)
generate_product_version(
Expand Down
24 changes: 24 additions & 0 deletions include/configs/GeneratorUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <QString>
#include <QtGlobal>

namespace Configs::GeneratorUtils {

struct ParsedHostPort {
QString host;
quint16 port = 0;
};

// Parse host:port without treating the final hextet of a raw IPv6 literal as a
// port. IPv6 ports therefore require the standard [address]:port form.
[[nodiscard]] ParsedHostPort ParseHostPort(const QString &endpoint,
quint16 defaultPort);

// Return the independent XHTTP download endpoint only when it is a hostname
// that needs bootstrap DNS. Numeric IPv4/IPv6 addresses and malformed objects
// do not need (or cannot safely receive) such a DNS rule.
[[nodiscard]] QString ExtractXrayXhttpDownloadDomain(
const QString &downloadSettings);

} // namespace Configs::GeneratorUtils
3 changes: 3 additions & 0 deletions include/configs/generate.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace Configs
QJsonObject coreConfig;
QString tunIPv4CIDR;
bool isXrayNeeded = false;
// True when a child process or generated outbound can create network
// traffic whose routing and DNS behavior Throne cannot constrain.
bool hasUnverifiableNetworkConfig = false;
QJsonObject xrayConfig;
// Opaque full configs, one instance each; never merged into xrayConfig.
QStringList xrayFullConfigs;
Expand Down
1 change: 1 addition & 0 deletions include/database/SettingsRepo.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ namespace Configs {
int vpn_mtu = 1500;
bool disable_private_range_bypass = false;
bool vpn_ipv6 = false;
bool kill_switch_enabled = false;
QString vpn_tun_ipv4_cidr = "172.19.0.1/24";
QString vpn_tun_ipv6_cidr = "fdfe:dcba:9876::1/96";
bool disable_privilege_req = false;
Expand Down
223 changes: 223 additions & 0 deletions include/sys/KillSwitchController.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
#pragma once

#include <QMutex>
#include <QString>
#include <QStringList>
#include <QtTypes>

#include <optional>

namespace Configs_sys {

// The controller deliberately knows nothing about WFP, Windows Firewall, or
// another platform implementation. In particular, it cannot make a blocking
// policy safe by itself: backend operations which fail must leave the OS in the
// previous state or in a stricter state, never in a less restrictive state.
struct KillSwitchResult {
bool ok = false;
QString error;

[[nodiscard]] static KillSwitchResult Success();
[[nodiscard]] static KillSwitchResult Failure(QString error);
[[nodiscard]] explicit operator bool() const { return ok; }
};

struct KillSwitchTrustedCorePlan {
// The baseline deliberately exempts only these trusted core executables so
// they can establish and carry the tunnel. Current profile formats do not
// expose a complete, static endpoint set (custom cores, Tailscale/DERP and
// domain rotation are examples), so this is an application-scoped permit,
// not an endpoint-scoped one. Keep this list minimal and canonicalized.
QStringList executablePaths;

[[nodiscard]] bool isValid() const;
friend bool operator==(const KillSwitchTrustedCorePlan &,
const KillSwitchTrustedCorePlan &) = default;
};

struct KillSwitchTunInterface {
QString name;
// Platform interface index, not a process-owned handle. A name is retained
// as a diagnostic/fallback identity for platforms without numeric indices.
quint64 interfaceIndex = 0;
bool ipv4 = true;
bool ipv6 = false;

[[nodiscard]] bool isValid() const;
friend bool operator==(const KillSwitchTunInterface &,
const KillSwitchTunInterface &) = default;
};

struct KillSwitchBackendState {
bool baselineActive = false;
bool dynamicCoreActive = false;
bool tunAllowanceActive = false;

[[nodiscard]] bool anyActive() const {
return baselineActive || dynamicCoreActive || tunAllowanceActive;
}
};

struct KillSwitchReconcileResult {
KillSwitchResult result;
KillSwitchBackendState state;
};

class KillSwitchBackend {
public:
virtual ~KillSwitchBackend() = default;

// Discover/reconcile only Throne-owned state. The implementation must not
// touch unrelated firewall configuration. Any stale transient session or
// TUN allow should be made safe before returning its observed state.
[[nodiscard]] virtual KillSwitchReconcileResult reconcile() = 0;

// Install or verify the persistent, dual-stack fail-closed baseline.
[[nodiscard]] virtual KillSwitchResult ensureBaseline() = 0;

// Create or replace the trusted application permits needed by the core.
// This is normally installed once at application startup and remains valid
// across profile changes. The persistent baseline remains installed.
[[nodiscard]] virtual KillSwitchResult startDynamicCore(
const KillSwitchTrustedCorePlan &plan) = 0;

// These calls are idempotent. removeTunAllowance must be safe when the TUN
// has already vanished, and addTunAllowance must never weaken the baseline
// for another interface.
[[nodiscard]] virtual KillSwitchResult removeTunAllowance() = 0;
[[nodiscard]] virtual KillSwitchResult addTunAllowance(
const KillSwitchTunInterface &tunInterface) = 0;

// Remove only Throne-owned persistent and transient objects.
[[nodiscard]] virtual KillSwitchResult disable() = 0;
};

class KillSwitchController {
public:
enum class State {
Disabled,
Connecting,
Connected,
Switching,
Reconnecting,
Stopping,
Disconnected,
Error,
Exiting,
};

enum class StartIntent {
Connect,
Switch,
Reconnect,
};

struct PrepareResult {
// Callers must not stop a working profile unless this is true.
bool mayTearDownCurrentProfile = false;
quint64 operationId = 0;
State state = State::Disabled;
QString error;

[[nodiscard]] explicit operator bool() const {
return mayTearDownCurrentProfile;
}
};

struct Snapshot {
bool initialized = false;
bool enabled = false;
bool recoveredStaleProtection = false;
State state = State::Disabled;
KillSwitchBackendState backend;
KillSwitchTunInterface allowedTun;
quint64 activeOperationId = 0;
QString lastError;
};

struct InitializationResult {
KillSwitchResult result;
bool enabled = false;
bool recoveredStaleProtection = false;
State state = State::Disabled;

[[nodiscard]] explicit operator bool() const {
return static_cast<bool>(result);
}
};

explicit KillSwitchController(KillSwitchBackend &backend);

// Must be called once after settings are loaded. Reconciliation runs even
// when shouldEnable is false. Discovered Throne protection is retained and
// promoted to enabled; only an explicit disable() removes persistent rules.
[[nodiscard]] InitializationResult initialize(
bool shouldEnable, KillSwitchTrustedCorePlan trustedCorePlan);
[[nodiscard]] KillSwitchResult enable();
[[nodiscard]] KillSwitchResult disable();

// The successful return is the prepare-before-stop security boundary:
// baseline -> constrained core session -> remove old TUN allow. No caller
// may tear down the old profile before it receives success.
[[nodiscard]] PrepareResult prepareForProfileStart(
StartIntent intent);

// operationId rejects late readiness/failure callbacks from an older start.
// System Proxy profiles pass std::nullopt; TUN profiles pass the ready
// interface including the IP families it carries.
[[nodiscard]] KillSwitchResult profileBecameReady(
quint64 operationId,
std::optional<KillSwitchTunInterface> tunInterface = std::nullopt);
[[nodiscard]] KillSwitchResult profileStartFailed(
quint64 operationId, QString error);

[[nodiscard]] PrepareResult prepareForProfileStop();
[[nodiscard]] KillSwitchResult profileStopped();

// Rolls back a prepared stop when the stop RPC failed and the old profile
// is still operational. TUN profiles pass the still-live interface so its
// allowance can be restored; System Proxy profiles pass std::nullopt. A
// failed add is safe to retry and leaves the connection blocked meanwhile.
[[nodiscard]] KillSwitchResult profileStopFailed(
std::optional<KillSwitchTunInterface> stillActiveTun = std::nullopt,
QString error = {});

// Called after an unplanned daemon/core exit. The baseline is re-verified
// and the obsolete TUN permission is removed; it is never disabled.
[[nodiscard]] KillSwitchResult coreTerminatedUnexpectedly(
bool reconnectPlanned);

// With the kill switch enabled, normal application exit intentionally keeps
// the persistent baseline for fail-closed crash/exit semantics.
[[nodiscard]] PrepareResult prepareForExit();

[[nodiscard]] Snapshot snapshot() const;
[[nodiscard]] bool invariantHolds(QString *reason = nullptr) const;
[[nodiscard]] static QString stateName(State state);

private:
[[nodiscard]] KillSwitchResult ensureBaselineLocked();
[[nodiscard]] KillSwitchResult removeTunAllowanceLocked();
[[nodiscard]] KillSwitchResult backendFailureLocked(
const QString &action, const KillSwitchResult &result);
[[nodiscard]] PrepareResult prepareFailureLocked(
State originalState, const QString &error) const;
[[nodiscard]] bool invariantHoldsLocked(QString *reason) const;
[[nodiscard]] bool startAllowedLocked(StartIntent intent) const;
[[nodiscard]] quint64 nextOperationIdLocked();

KillSwitchBackend &backend_;
mutable QMutex mutex_;
bool initialized_ = false;
bool enabled_ = false;
bool recoveredStaleProtection_ = false;
State state_ = State::Disabled;
KillSwitchBackendState backendState_;
KillSwitchTunInterface allowedTun_;
quint64 operationCounter_ = 0;
quint64 activeOperationId_ = 0;
KillSwitchTrustedCorePlan trustedCorePlan_;
QString lastError_;
};

} // namespace Configs_sys
Loading