Skip to content

Commit 2a514cc

Browse files
Ryanmello07claude
andcommitted
p1: close the six review findings on the rpc-only mode
The service-side fence held; every defect was on the app side or in the mode's headline promise. B1 (blocking) — an rpc-only request rewrote routes against a pre-mode service, and the log asserted the opposite. A v1 service has no `mode` handler, so it drops the field, runs all eight steps and calls netConfig_->Apply; the app then logged "the service is clamped. Nothing will be connected." while the routing table had just been rewritten. The code reasoned only about the downgrade direction. kProtocolVersion -> 2 with kFirstStartModeVersion, and BootstrapSession now refuses BEFORE start_tunnel when it asks for rpc-only and hello reports < 2. The two mismatch directions are handled separately: the safe one (asked tunnel, got rpc-only) refuses; the dangerous one (asked rpc-only, got tunnel) stops the tunnel it did not ask for and refuses. B2 (blocking) — the app rendered "Connected" in rpc-only mode. The previous audit was of the wrong signal: the user-visible connect status never read TunnelState. The real chain is getConnectionStatus() -> LiveStats::connectionStatus -> ParseConnectStatus -> ApplyConnectStatus, and in rpc-only the DeviceLocal negotiates providers normally, so picking a location showed "Connected", a green dot, a Disconnect button, "Connected to N providers" and a live rate with zero packets carried -- while the tray, reading TunnelState, stayed disconnected. Fixed at ONE chokepoint in SdkHost::ReadStats, not in the render path: when the session is rpc-only, connectionStatus is forced to "RPC_ONLY" and connected/providerCount/rates to zero. "RPC_ONLY" is deliberately a value the view does not recognise -- ParseConnectStatus documents that anything unrecognised reads as Disconnected precisely so an unknown status cannot leave the button claiming a connection the SDK never made. The true values are kept in rawConnectionStatus/rawConnected for the P2 developer surface. No view file is touched, so the P0 ConnectPage split does not collide with this. S1 — an rpc-only app could attach to a live production tunnel and then revert it via Logout/re-registration. Reattach now requires an exact mode match, and attaching to a live tunnel while asking rpc-only is refused. S2 — --rpc-only did delete routes and clear DNS on startup, and consumed the crash marker, while the banner claimed it wrote nothing. The startup sweep is now observe-only in rpc-only: SweepOrphanedTunnel(remove=false) reports orphans without touching them, and PeekActiveMarker reports the marker without eating it. Banners corrected. S3 — reply.ok now means "I did what you asked": IsSessionLive && mode matches. The error string is left empty on a pure mode mismatch, because ServiceClient::CallStatus overwrites state with Error whenever !ok carries one, which would erase the mode the caller needs. S4 — sessionMode_ defaults and resets to RpcOnly, the mode that claims less, matching the policy Protocol.h already stated. Also: URNETWORK_RPC_ONLY is now an explicit truthy allow-list. "off", "no" and "0 " (trailing space) all used to evaluate to ON; unrecognised values now mean OFF and say so. `console --rpc-only --bogus` no longer ignores argv[3]. Verified by running, unelevated: * B2 by SCREENSHOT, same build and same synthetic SDK inputs, only the env var differing: clamp off renders "Connected" / green dot / Disconnect / "Connected to 7 providers" / 12.3 Mbps; clamp on renders "Ready to connect" / idle dot / Connect / no count / no rate. * B1 both ways over the real pipe: a service reporting v1 makes the app refuse and NO start_tunnel is ever sent; a v2 service bootstraps -- "session bootstrapped (mode=rpc_only)", the DeviceRemote's first real RPC connection to the service. * S2 by planting a marker: an unelevated rpc-only run leaves it in place and says so; a normal console run consumes it and reports the crash. * S3: clamped process + mode=tunnel now replies ok=false with mode=rpc_only. * env var matrix (1/on/off/no/"0 "/banana/unset) and the argv rejection. * routes and DNS byte-identical before/during/after; no adapter, no marker, connectivity intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PT7KcWCPKfFwQUc7SM3oZY
1 parent bdc3b1e commit 2a514cc

9 files changed

Lines changed: 275 additions & 45 deletions

File tree

app/src/App/SdkHost.cpp

Lines changed: 137 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "SdkHost.h"
77

88
#include <algorithm>
9+
#include <cwctype>
910
#include <fstream>
1011
#include <random>
1112
#include <thread>
@@ -31,19 +32,37 @@ struct RpcSession {
3132
};
3233

3334
// URNETWORK_RPC_ONLY: ask the service for a session that stops before it would
34-
// touch the machine's routes or DNS (spec P1). Empty, "0" and "false" mean off;
35-
// anything else means on, so `set URNETWORK_RPC_ONLY=1` is enough.
35+
// touch the machine's routes or DNS (spec P1).
36+
//
37+
// Parsed as an explicit allow-list of truthy values rather than "anything that
38+
// is not falsy". The earlier version accepted anything unrecognised as ON, so
39+
// `URNETWORK_RPC_ONLY=off`, `=no` and `=0 ` (trailing space) all turned the
40+
// mode ON — a stray `setx` giving a client that silently refuses to connect.
41+
// Unrecognised now means OFF *and says so*, because the failure of guessing
42+
// wrong is a developer confused about why nothing connects.
3643
proto::StartMode StartModeFromEnvironment() {
3744
constexpr DWORD kMax = 64;
3845
wchar_t buf[kMax] = {0};
3946
const DWORD n = ::GetEnvironmentVariableW(L"URNETWORK_RPC_ONLY", buf, kMax);
40-
// n == 0: unset. n >= kMax: a value too long to be one of ours; treat it as
41-
// unset rather than truncating into a comparison.
47+
// n == 0: unset. n >= kMax: too long to be one of ours; treat as unset rather
48+
// than truncating into a comparison.
4249
if (n == 0 || n >= kMax) return proto::StartMode::Tunnel;
50+
4351
std::wstring v(buf, n);
44-
if (v == L"0" || v == L"false" || v == L"FALSE" || v == L"False")
52+
const size_t first = v.find_first_not_of(L" \t\r\n");
53+
const size_t last = v.find_last_not_of(L" \t\r\n");
54+
v = (first == std::wstring::npos) ? L"" : v.substr(first, last - first + 1);
55+
std::transform(v.begin(), v.end(), v.begin(),
56+
[](wchar_t c) { return static_cast<wchar_t>(::towlower(c)); });
57+
58+
if (v == L"1" || v == L"true" || v == L"yes" || v == L"on")
59+
return proto::StartMode::RpcOnly;
60+
if (v.empty() || v == L"0" || v == L"false" || v == L"no" || v == L"off")
4561
return proto::StartMode::Tunnel;
46-
return proto::StartMode::RpcOnly;
62+
LogWarn("sdkhost: URNETWORK_RPC_ONLY is set to an unrecognised value; "
63+
"ignoring it and using the normal tunnel mode. Use 1/true/yes/on to "
64+
"enable rpc-only.");
65+
return proto::StartMode::Tunnel;
4766
}
4867

4968
void SaveRpcSession(const RpcSession& s) {
@@ -771,18 +790,52 @@ bool SdkHost::BootstrapSession() {
771790
try {
772791
std::string clientPem, serverCertPem, hostPort;
773792

774-
// Reattach to a live session if the service reports one and we have the key
775-
// material for it. A live session is reusable when it is at LEAST as
776-
// capable as what we asked for: a tunnel serves an rpc-only request fine,
777-
// but an rpc-only session does not serve a tunnel request — and tearing
778-
// down a running tunnel because this process happens to be in rpc-only mode
779-
// would disconnect the user to make a developer's life easier.
780793
proto::TunnelStatus hello = service_.Hello();
794+
795+
if (requestedMode_ == proto::StartMode::RpcOnly) {
796+
// A service older than kFirstStartModeVersion has no `mode` handler: it
797+
// drops the field, runs all eight steps and rewrites this machine's
798+
// routes and DNS. Refuse BEFORE start_tunnel — after it the damage is
799+
// done and all we could do is revert. This check is the ONLY thing that
800+
// distinguishes "honours mode" from "ignores mode"; without it the
801+
// safest-looking configuration in the tree is the one that silently
802+
// builds a real tunnel.
803+
if (hello.protocol_version < proto::kFirstStartModeVersion) {
804+
LogError("sdkhost: REFUSING to start a session. URNETWORK_RPC_ONLY is "
805+
"set, but the running service speaks control protocol v{} and "
806+
"only v{}+ understands the start mode — it would ignore the "
807+
"field, build a REAL TUNNEL and rewrite this machine's routes "
808+
"and dns. Update the installed service, or run `urnetworkd "
809+
"console --rpc-only` from this build.",
810+
hello.protocol_version, proto::kFirstStartModeVersion);
811+
return false;
812+
}
813+
// A live TUNNEL when we asked for rpc-only. Attaching would be honestly
814+
// REPORTED, but it also hands this process the authority to tear that
815+
// tunnel down: TeardownSessionLocked -> StopTunnel -> NetworkConfig::
816+
// Revert, reachable from Logout and from re-registration. Somebody who
817+
// set the env var to guarantee "this run cannot touch my network" must
818+
// not find that Log out reverted the tunnel they were using.
819+
if (proto::IsSessionLive(hello.state) &&
820+
hello.mode == proto::StartMode::Tunnel) {
821+
LogError("sdkhost: REFUSING to attach. URNETWORK_RPC_ONLY is set, but "
822+
"the service is running a REAL TUNNEL (state={} "
823+
"routes_installed={}). This process would be able to stop it — "
824+
"a log out or a re-registration reverts its routes. Stop the "
825+
"tunnel first, or unset URNETWORK_RPC_ONLY.",
826+
proto::ToString(hello.state),
827+
hello.routes_installed ? "yes" : "no");
828+
return false;
829+
}
830+
}
831+
832+
// Reattach only when the live session's mode is EXACTLY the one we asked
833+
// for. "At least as capable" was wrong in the rpc-only direction: a tunnel
834+
// does carry rpc-only traffic, but attaching to it also confers the power
835+
// to revert it — refused above.
781836
auto saved = LoadRpcSession();
782837
const bool liveIsSufficient =
783-
proto::IsSessionLive(hello.state) &&
784-
!(requestedMode_ == proto::StartMode::Tunnel &&
785-
hello.mode == proto::StartMode::RpcOnly);
838+
proto::IsSessionLive(hello.state) && hello.mode == requestedMode_;
786839
if (liveIsSufficient && saved && hello.rpc_listen_hostport == saved->host_port) {
787840
clientPem = saved->client_pem;
788841
serverCertPem = saved->server_cert_pem;
@@ -827,10 +880,34 @@ bool SdkHost::BootstrapSession() {
827880
return false;
828881
}
829882
sessionMode_.store(st.mode);
883+
// A mode mismatch is never benign, and the two directions are NOT the
884+
// same event. The old code logged the harmless one's message for both.
830885
if (st.mode != cfg.mode) {
831-
LogWarn("sdkhost: asked the service for a {} session and got {} — the "
832-
"service is clamped. Nothing will be connected.",
833-
proto::ToString(cfg.mode), proto::ToString(st.mode));
886+
if (cfg.mode == proto::StartMode::RpcOnly) {
887+
// The dangerous direction. We asked for no network changes and the
888+
// service built a tunnel: routes and DNS have ALREADY been rewritten.
889+
// The version gate above should make this unreachable, so reaching it
890+
// means a peer is misreporting its version — give the routes back
891+
// rather than keep a tunnel nobody asked for.
892+
LogError("sdkhost: the service returned a TUNNEL for an rpc-only "
893+
"request (routes_installed={}). This machine's routes and "
894+
"dns have already been rewritten by a request that asked for "
895+
"the opposite. Stopping it and refusing the session.",
896+
st.routes_installed ? "yes" : "no");
897+
service_.StopTunnel();
898+
return false;
899+
}
900+
// The safe direction: we asked for a tunnel and the service is clamped
901+
// to rpc-only, so nothing was written. Refuse rather than silently run
902+
// a session the caller did not ask for.
903+
LogError("sdkhost: asked the service for a {} session and it served {} "
904+
"— the service is clamped (`urnetworkd console --rpc-only`). "
905+
"No tunnel was created and no routes were touched. Set "
906+
"URNETWORK_RPC_ONLY=1 to ask for rpc-only explicitly, or run "
907+
"an unclamped service.",
908+
proto::ToString(cfg.mode), proto::ToString(st.mode));
909+
service_.StopTunnel();
910+
return false;
834911
}
835912
if (st.mode == proto::StartMode::RpcOnly) {
836913
LogWarn("sdkhost: RPC-ONLY session at {} — the DeviceRemote is live and "
@@ -979,6 +1056,44 @@ LiveStats SdkHost::ReadStats() {
9791056
if (loc->country) s.countryName = *loc->country;
9801057
}
9811058
}
1059+
1060+
// ---- rpc-only: clamp the RENDERED connection state ----------------------
1061+
//
1062+
// LAST, so it covers every field the window renders, including the throughput
1063+
// rates filled in above. It belongs here rather than in the window for two
1064+
// reasons: this is where the fields the UI renders are produced, and it is
1065+
// the only place a fix can reach them without touching the UI layer.
1066+
//
1067+
// Clamping TunnelState was NOT enough, because the user-visible connect
1068+
// status never read TunnelState. The chain that reaches the pixels is
1069+
// getConnectionStatus() -> LiveStats::connectionStatus ->
1070+
// MainWindow::ParseConnectStatus -> ApplyConnectStatus. In rpc-only the
1071+
// DeviceLocal is live and negotiates provider transports normally — that is
1072+
// the POINT of the mode, and the spec puts connect controls inside it — so
1073+
// the moment a location is picked getConnectionStatus() returns CONNECTED and
1074+
// the window shows "Connected", a green dot, a Disconnect button, "Connected
1075+
// to N providers" and a live rate, with zero packets carried. The tray
1076+
// meanwhile reads TunnelState and stays disconnected, so the app contradicts
1077+
// itself.
1078+
//
1079+
// "RPC_ONLY" is deliberately a value the window does not recognise:
1080+
// ParseConnectStatus documents that anything unrecognised reads as
1081+
// Disconnected, precisely so an unknown status cannot leave the button
1082+
// claiming a connection the SDK never made. This uses that existing fail-safe
1083+
// rather than adding a parallel one.
1084+
s.rpcOnly = sessionMode_.load() == proto::StartMode::RpcOnly;
1085+
if (s.rpcOnly) {
1086+
// The true SDK values are kept, not discarded: the developer surface (P2)
1087+
// is the one place that SHOULD see them. Everything the connect page
1088+
// renders is clamped.
1089+
s.rawConnectionStatus = s.connectionStatus;
1090+
s.rawConnected = s.connected;
1091+
s.connectionStatus = "RPC_ONLY";
1092+
s.connected = false; // gates "Connected to N providers" and the rate line
1093+
s.providerCount = 0;
1094+
s.downBitsPerSecond = 0;
1095+
s.upBitsPerSecond = 0;
1096+
}
9821097
return s;
9831098
}
9841099

@@ -1712,9 +1827,10 @@ void SdkHost::TeardownSessionLocked() {
17121827
if (service_.IsConnected()) {
17131828
service_.StopTunnel();
17141829
}
1715-
// No session, so no session mode. Back to the default rather than leaving the
1716-
// last one to be read by a status built before the next bootstrap sets it.
1717-
sessionMode_.store(proto::StartMode::Tunnel);
1830+
// No session, so no tunnel — reset to the mode that claims less, not to
1831+
// Tunnel. A status built between this teardown and the next bootstrap must
1832+
// not be able to render "connected".
1833+
sessionMode_.store(proto::StartMode::RpcOnly);
17181834
ClearRpcSession();
17191835
}
17201836

app/src/App/SdkHost.h

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,24 @@ struct CreateNetworkParams {
6666
// Snapshot of live connection / throughput / provide stats. Pushed to the UI on
6767
// SDK listener callbacks (macOS parity: listener-push, not polling).
6868
struct LiveStats {
69-
std::string connectionStatus; // getConnectionStatus() (CONNECTED/CONNECTING/...)
70-
bool connected = false;
69+
// getConnectionStatus() (CONNECTED/CONNECTING/DESTINATION_SET/DISCONNECTED)
70+
// -- EXCEPT in an rpc-only session, where it is forced to the deliberately
71+
// unrecognised "RPC_ONLY" so the connect page renders as disconnected. There
72+
// is no tunnel in that mode and nothing may claim otherwise. See the clamp at
73+
// the end of SdkHost::ReadStats.
74+
std::string connectionStatus;
75+
bool connected = false; // forced false in an rpc-only session
7176
int64_t providerCount = 0; // grid window current size (providers in window)
7277
int64_t downBitsPerSecond = 0; // remote (tunneled) ingress bit rate
7378
int64_t upBitsPerSecond = 0; // remote (tunneled) egress bit rate
79+
// This snapshot came from an rpc-only session: no tunnel exists, nothing is
80+
// carried, and the four fields above have been clamped to say so.
81+
bool rpcOnly = false;
82+
// What the SDK actually reported before the clamp. For the developer surface
83+
// (P2), which is the one place that should see through it. Empty/false unless
84+
// rpcOnly.
85+
std::string rawConnectionStatus;
86+
bool rawConnected = false;
7487
bool insufficientBalance = false;
7588
bool provideEnabled = false;
7689
bool providePaused = false;
@@ -538,8 +551,12 @@ class SdkHost {
538551
ServiceClient service_;
539552
// Set once in Initialize() from URNETWORK_RPC_ONLY; never changes after.
540553
proto::StartMode requestedMode_ = proto::StartMode::Tunnel;
541-
// Set from the service's reply/hello whenever a session is established.
542-
std::atomic<proto::StartMode> sessionMode_{proto::StartMode::Tunnel};
554+
// Set from the service's reply/hello whenever a session is established, and
555+
// reset here when one is torn down. Defaults to RpcOnly — the mode that
556+
// CLAIMS LESS — matching the policy TunnelStatus::from_json states for an
557+
// unreadable mode. With no session there is certainly no tunnel, so a stray
558+
// read before or after one must not be able to render "connected".
559+
std::atomic<proto::StartMode> sessionMode_{proto::StartMode::RpcOnly};
543560
std::string appVersion_ = "0.0.1";
544561

545562
WalletConnect wallet_;

app/src/Common/Protocol.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,22 @@
2525
namespace urnw::proto {
2626

2727
// bump when the wire format changes incompatibly; hello negotiates it
28-
inline constexpr int kProtocolVersion = 1;
28+
//
29+
// 2: StartTunnel::mode / TunnelStatus::mode + TunnelState::RpcOnly.
30+
// This bump is load-bearing, not bookkeeping. A version-1 service has no
31+
// `mode` handler in its from_json, so it SILENTLY DROPS the field: an
32+
// rpc-only request arrives as a plain start_tunnel, all eight steps run, and
33+
// the machine's routes and DNS are rewritten by a request that asked for
34+
// exactly the opposite. `mode` cannot be made safe by its own absence — the
35+
// only thing that distinguishes "this peer honours mode" from "this peer
36+
// ignores mode" is the version. Anything requesting RpcOnly MUST refuse to
37+
// proceed against a peer reporting < kFirstStartModeVersion.
38+
// See SdkHost::BootstrapSession.
39+
inline constexpr int kProtocolVersion = 2;
40+
41+
// The first version that understands StartTunnel::mode. Below this, an absent
42+
// `mode` on the wire means "ignored", not "defaulted".
43+
inline constexpr int kFirstStartModeVersion = 2;
2944

3045
// ---- message type tags ----------------------------------------------------
3146

@@ -164,6 +179,11 @@ struct TunnelStatus {
164179
// `state` so it survives Starting/Stopping/Error, where `state` says nothing
165180
// about which kind of session was asked for. In a live session the two agree
166181
// by construction: the controller derives both from one stored mode.
182+
//
183+
// Defaults to Tunnel, and that is right even for an absent field: a peer old
184+
// enough not to send `mode` is a peer that only ever built real tunnels, so
185+
// reading its silence as "Tunnel" is honest. What is NOT safe is asking such
186+
// a peer for RpcOnly — see kFirstStartModeVersion.
167187
StartMode mode = StartMode::Tunnel;
168188
// True only when routes and DNS are actually installed right now. This is the
169189
// field to trust for "is my traffic going through the tunnel"; it is false for

app/src/Service/ControlServer.cpp

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ nlohmann::json ControlServer::Handle(const nlohmann::json& request) {
2626
} else if (type == proto::msg::kStartTunnel) {
2727
proto::StartTunnel cfg = request.get<proto::StartTunnel>();
2828
proto::TunnelStatus st = tunnel_.Start(cfg);
29-
// "ok" means "the session you asked for is live", which for an rpc-only
30-
// request is state rpc_only, not up. The caller still has to read
31-
// st.state / st.mode to know what it got — ok alone never implies a
32-
// tunnel. (An unknown mode string throws out of the get<> above and is
33-
// answered as a failed reply, so a garbled mode never starts anything.)
34-
reply.ok = proto::IsSessionLive(st.state);
29+
// "ok" means "I did what you asked" — live AND in the mode requested.
30+
// A clamped process serving a tunnel request produces a live session, but
31+
// not the one the caller asked for, and reporting ok for that is how a
32+
// caller ends up believing it has a tunnel. The status carries the mode
33+
// actually served, so the caller can see which way it differed.
34+
// (An unknown mode string throws out of the get<> above and is answered
35+
// as a failed reply, so a garbled mode never starts anything.)
36+
reply.ok = proto::IsSessionLive(st.state) && st.mode == cfg.mode;
37+
// Left EMPTY on a pure mode mismatch, deliberately: the status already
38+
// says what happened, and ServiceClient::CallStatus overwrites state with
39+
// Error whenever !ok carries an error string — which would erase the very
40+
// mode the caller needs in order to react correctly.
3541
reply.error = st.error;
3642
reply.status = st;
3743
PushState();

app/src/Service/NetworkConfig.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ void NetworkConfig::CrashRevert() {
268268
}
269269

270270
int NetworkConfig::SweepOrphanedTunnel(const GUID& tunGuid,
271-
const wchar_t* adapterName) {
271+
const wchar_t* adapterName, bool remove) {
272272
// Collect candidate LUIDs first, then sweep, so a rename or a GUID fallback
273273
// cannot make us miss the interface that is holding the machine's traffic.
274274
uint64_t candidates[8] = {0};
@@ -326,6 +326,15 @@ int NetworkConfig::SweepOrphanedTunnel(const GUID& tunGuid,
326326
row.InterfaceLuid = luid;
327327
std::string alias = (::GetIfEntry2(&row) == NO_ERROR) ? Narrow(row.Alias)
328328
: std::string("<gone>");
329+
if (!remove) {
330+
LogWarn("netcfg: ORPHANED tun interface \"{}\" (luid {:#x}) present at "
331+
"startup — a previous run did not revert. NOT cleaning it: this "
332+
"process is observe-only (rpc-only mode) and removing routes "
333+
"needs elevation. Run `urnetworkd revert` from an elevated prompt "
334+
"to take the routes back.",
335+
alias, luid.Value);
336+
continue;
337+
}
329338
int removed = DeleteTunnelRoutes(luid);
330339
ClearTunnelDns(luid);
331340
LogWarn(

app/src/Service/NetworkConfig.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,15 @@ class NetworkConfig {
9191
// exists when the service starts, a previous run died without unwinding (or
9292
// the adapter outlived it). Delete our route set and DNS from each and say so
9393
// loudly. Returns how many orphaned interfaces were found.
94-
static int SweepOrphanedTunnel(const GUID& tunGuid, const wchar_t* adapterName);
94+
//
95+
// remove=false makes it OBSERVE ONLY: it still finds and reports orphans but
96+
// deletes no route and clears no DNS. That is what the unelevated rpc-only
97+
// mode uses — the removal needs privilege it does not have, and a mode whose
98+
// entire promise is "this will not touch your network" must not open by
99+
// rewriting the route table. Reporting an orphan it cannot clean is still
100+
// worth doing: it tells the owner to run `urnetworkd revert` elevated.
101+
static int SweepOrphanedTunnel(const GUID& tunGuid, const wchar_t* adapterName,
102+
bool remove = true);
95103

96104
// Find the best default-route interface indices that are NOT the tun. Used to
97105
// set the SDK egress binding. Recomputed on every network change.

app/src/Service/TunnelController.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,11 @@ bool TunnelController::TakeActiveMarker() {
450450
return true;
451451
}
452452

453+
bool TunnelController::PeekActiveMarker() {
454+
std::error_code ec;
455+
return std::filesystem::exists(ActiveMarkerPath(), ec);
456+
}
457+
453458
void TunnelController::PushPhysicalAddressesLocked(const EgressInterfaces& egress) {
454459
uint8_t addr4[4] = {0};
455460
uint8_t addr6[16] = {0};

0 commit comments

Comments
 (0)