Skip to content

feat(interop): IVPN client interoperability#2135

Merged
stenya merged 13 commits intodevelopmentfrom
feature/ivpn_interoperability
Mar 10, 2026
Merged

feat(interop): IVPN client interoperability#2135
stenya merged 13 commits intodevelopmentfrom
feature/ivpn_interoperability

Conversation

@stenya
Copy link
Copy Markdown
Contributor

@stenya stenya commented Mar 6, 2026

Summary

Introduces a new service/interop module enabling Portmaster to interoperate with the IVPN desktop client.

Changes

New: service/interop module

  • Exposes a /interop/ping API endpoint so the IVPN client can trigger a connection attempt to Portmaster.

IVPN client integration (service/interop/ivpn)

  • Connects to the IVPN daemon via ivpnclient and subscribes to ConnectionStarting / ConnectionStopped events.
  • Provides smart firewall verdicts: automatically accepts IVPN VPN tunnel traffic (matched by destination IP/port/protocol) and IVPN service binary connections, bypassing Portmaster's normal filtering for those flows.
  • Sends the hello request with GetActiveRemoteEndpoint so the active VPN endpoint is known immediately on connect.

DNS coordination

  • When Portmaster has custom DNS servers configured and interception is active, it pushes its local DNS listen address to the IVPN client via SetTempPrioritizedDns, preventing DNS conflicts.
  • Automatically reverts IVPN DNS when interception is paused or custom servers are removed.

Firewall: external verdict handler hook

  • Added SetExternalVerdictHandler / ExtVerdictHandlerFunc in the firewall packet handler, providing a lock-free (atomic.Pointer) hot-path hook for external modules to inject Accept/Block verdicts without modifying core firewall logic.

Interception: start/stop events

  • Added EventStartStopState event emitter and IsStarted() accessor to Interception, allowing other modules to react to interception state changes.

Notes

⚠️ The ivpnclient package is currently referenced via a local replace directive in go.mod. This must be updated to a published module path before merging to a release branch.

Summary by CodeRabbit

  • New Features

    • IVPN interoperability for coordinated firewall/DNS behavior
    • External verdict handler to allow external firewall decisions
    • Interoperability API endpoint (ping)
    • Notification “Don’t show again” suppression with persistence
  • Improvements

    • Start/stop interception now emits state events
    • Notification visibility controls and updated notification UI labels
  • Bug Fixes

    • Reset notifications endpoint now clears related suppression state and updates messaging

stenya and others added 7 commits February 27, 2026 00:32
Allow Portmaster to cooperate with the IVPN client:
- Accept IVPN VPN tunnel and service process connections
- Delegate DNS control to Portmaster when custom DNS is configured
- Auto-connect to IVPN daemon on startup and on ping
- Hook into firewall verdict pipeline via new ExtVerdictHandler
Every connection verdict previously acquired an RWMutex to read IVPN
state. Replace it with atomic.Pointer[clientStatus] using an immutable
snapshot (copy-on-write) so reads are a single pointer load with no
locking on the per-packet hot path.

Apply the same pattern to the external verdict handler: replace a
data-racy plain function variable and two auxiliary atomic.Bool flags
with a single atomic.Pointer[ExtVerdictHandlerFunc]. Use CompareAndSwap
for set-once semantics. Move the load into the default branch of
filterHandler so pre-authenticated and DNS-redirect connections pay zero
cost.
…ception is paused

Add EventStartStopState event  manager and IsStarted() method
to the Interception module so other modules can react to start/stop state changes.

Update IVPN interop to subscribe to interception start/stop events
and skip applying custom DNS settings when interception is inactive,
ensuring correct behavior when Portmaster is in the Paused state.
Bugs fixed:
- Inbound UDP from VPN server incorrectly blocked
- Firewall verdicts possible before client status initialized

Also: move interop before interception in startup order, simplify DNS state tracking.
@stenya stenya self-assigned this Mar 6, 2026
@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 6, 2026

@stenya stenya marked this pull request as draft March 6, 2026 18:11
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 6, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a new Interoperability module (with IVPN integration) that can register external firewall verdicts and manage DNS synchronization; wires the module into service startup. Also introduces notification suppression persistence and visibility flags, updates firewall packet handling to accept external verdicts, plus dependency and editor config changes.

Changes

Cohort / File(s) Summary
Interop core
service/interop/module.go, service/interop/api.go, service/interop/verdict_handler.go
New Interoperability module type, lifecycle, API endpoint for "interop/ping", and verdict delegation across registered interop modules; lazy registration of external verdict handler with firewall.
IVPN integration
service/interop/ivpn/ivpn.go, service/interop/ivpn/notification.go
New IVPN interop implementing Start/Ping/VerdictHandler, DNS sync with Portmaster, VPN-connection tracking, and notification + persistence for user suppression. Large new file with event-driven logic.
Firewall packet handling
service/firewall/packet_handler.go, service/firewall/interception/module.go
Adds external verdict mechanism (ExtVerdictHandlerFunc, SetExternalVerdictHandler) integrated into filterHandler with tunnel control; interception module gains EventStartStopState and IsStarted() for state events.
Service wiring
service/instance.go
Wires Interoperability into Instance (field + initialization) and includes it in module startup order before interception.
Notifications & suppression
service/resolver/metrics.go, service/broadcasts/api.go, base/notifications/notification.go
Adds suppression persistence keys and marker storage for stale-cache and IVPN-detect notifications; notification action visibility field/type added and reset/reset-state endpoint extended to clear suppression markers.
Desktop UI & native clients
desktop/angular/src/app/.../navigation.*, desktop/angular/src/app/services/notifications.types.ts, desktop/angular/src/app/shared/.../notification-list.component.html, desktop/tauri/src-tauri/src/portapi/models/notification.rs, desktop/tauri/src-tauri/src/portmaster/notifications.rs
Propagates Action visibility field to frontends: new Visibility property in Angular types and UI gating so actions with non-empty visibility are withheld from in-app buttons; Tauri/Rust code respects action.visibility when building buttons.
Dev tooling & deps
go.mod, .vscode/launch.json, .vscode/settings.json, .gitignore
Updates Go module deps and adds ivpn client/faker requirement; adds Linux launch debug fields (console, asRoot), cSpell words and new gitignore patterns.

Sequence Diagram

sequenceDiagram
    participant ServiceInit as Service Init
    participant Interop as Interoperability
    participant IVPN as IVPN Interop
    participant IVPNClient as IVPN Client
    participant Firewall as Firewall
    participant Network as Packet Handler

    ServiceInit->>Interop: New(instance) / prep()
    Interop->>Interop: register modules (IVPN), register API
    Interop->>Firewall: EnsureVerdictHandlerRegistered()
    Firewall->>Firewall: SetExternalVerdictHandler(verdict_handler)

    Interop->>IVPN: Start()
    IVPN->>IVPNClient: connect & hello
    IVPNClient-->>IVPN: ConnectionStarting/Stopped events
    IVPN->>Interop: update DNS state via Interoperability config events

    Network->>Firewall: processPacket(conn)
    Firewall->>Firewall: filterHandler invokes external verdict
    Firewall->>Interop: external verdict handler(conn)
    Interop->>IVPN: call module.VerdictHandler(conn)
    IVPN-->>Interop: verdict (Accept/Block/Undecided, skipTunnel)
    Interop-->>Firewall: verdict result
    Firewall->>Network: apply verdict, maybe skip tunneling
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature: pause PM/SPN #2067 — touches pause/resume and server-side control logic; related to control/interop surface changes introduced here.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main feature addition—IVPN client interoperability—which is the primary change across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/ivpn_interoperability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
service/interop/api.go (2)

30-30: Inconsistent receiver naming.

The receiver is named c here but i is used in other methods in this package. Use consistent naming.

♻️ Consistent receiver name
-func (c *Interoperability) handlePing(r *api.Request) (msg string, err error) {
+func (i *Interoperability) handlePing(r *api.Request) (msg string, err error) {
 	// Received ping, possibly from IVPN client
 	// Try to connect to IVPN client if not already connected
 
-	for _, im := range c.interopModules {
+	for _, im := range i.interopModules {
 		if err := im.PingHandler(); err != nil {
-			c.mgr.Warn("Failed to handle ping for interoperability module: " + err.Error())
+			i.mgr.Warn("Failed to handle ping for interoperability module: " + err.Error())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/api.go` at line 30, The method receiver for Interoperability
is named `c` in `handlePing` but other methods use `i`; rename the receiver from
`c` to `i` in the function signature of `handlePing` (function name: handlePing,
type: Interoperability) so it matches the package-wide convention and avoids
inconsistent receiver naming.

11-13: Unused struct field.

The pingParams struct and its Message field are defined but never used in handlePing. Either remove the unused struct or implement the intended functionality.

🗑️ Remove unused struct
-type pingParams struct {
-	Message bool `json:"message"`
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/api.go` around lines 11 - 13, The pingParams struct (type
pingParams with field Message) is unused by handlePing; remove the dead
declaration to avoid unused field warnings or, if intended, update handlePing to
decode JSON into pingParams and use pingParams.Message in the response; locate
the type pingParams and either delete it or add JSON decoding and usage in the
handlePing function to consume Message.
service/interop/verdict_handler.go (1)

7-15: Method name should use camelCase per Go conventions.

The method verdict_handler uses snake_case which is unconventional in Go. Standard Go naming uses camelCase for unexported identifiers.

♻️ Rename to camelCase
-func (i *Interoperability) verdict_handler(conn *network.Connection) (verdict network.Verdict, reason string, skipTunnel bool) {
+func (i *Interoperability) verdictHandler(conn *network.Connection) (verdict network.Verdict, reason string, skipTunnel bool) {

Also update the reference in service/interop/module.go line 127:

-		firewall.SetExternalVerdictHandler(i.verdict_handler)
+		firewall.SetExternalVerdictHandler(i.verdictHandler)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/verdict_handler.go` around lines 7 - 15, Rename the method
verdict_handler to camelCase (verdictHandler) on the Interoperability receiver
and update all call sites (e.g., the call referenced in
service/interop/module.go at the previous line 127) to use verdictHandler;
ensure the method signature (func (i *Interoperability) verdictHandler(conn
*network.Connection) (network.Verdict, string, bool)) and any variable shadowing
inside the loop (verdict, reason, skipTunnel) remain correct and compile after
renaming.
service/interop/ivpn/ivpn.go (2)

279-281: Broad allowlist for IVPN service binary.

All network traffic from the IVPN service binary is unconditionally accepted. While this is likely necessary for IVPN to function, document this trust assumption clearly.

📝 Add comment clarifying the trust model
+	// Trust all traffic from the IVPN service binary.
+	// This is required for IVPN to establish and maintain VPN connections,
+	// including control plane traffic that may not match the VPN tunnel endpoint.
 	if status.serviceBinary != "" && conn.Process().Path == status.serviceBinary {
 		return network.VerdictAccept, "IVPN Service connection", false
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/ivpn/ivpn.go` around lines 279 - 281, Add a clear code
comment above the conditional that unconditionally accepts connections from the
IVPN service binary (the block checking status.serviceBinary != "" &&
conn.Process().Path == status.serviceBinary and returning network.VerdictAccept)
explaining the trust model: state that any process matching status.serviceBinary
is fully trusted by this filter, why that trust is required for IVPN to
function, and any limitations/assumptions (e.g., relies on path integrity, not
performing further validation like signature checking or user ownership);
reference the symbols status.serviceBinary, conn.Process().Path, and
network.VerdictAccept so reviewers know exactly which check the comment applies
to.

117-121: Consider debug logging for connection failures.

Silently ignoring errors when the IVPN client isn't running is intentional, but having debug-level logging would help troubleshoot cases where the client is running but connections fail for other reasons.

💡 Add debug logging
 	client, err := ivpnclient.NewClientAsRoot(nil, time.Second*10, ci)
 	if err != nil {
+		// Client not running or other connection issue - this is expected
+		// when IVPN is not installed or not active
 		return nil
 	}

Also applies to: 133-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/ivpn/ivpn.go` around lines 117 - 121, The
ivpnclient.NewClientAsRoot call currently swallows errors (client, err :=
ivpnclient.NewClientAsRoot(...); if err != nil { return nil })—add a debug-level
log that emits the error details before returning so connection failures are
visible during diagnosis; specifically log err (e.g., logger.Debugf or similar)
in the NewClientAsRoot error branch and apply the same change to the second
identical block later (the other NewClientAsRoot error check around lines noted
in the review) so both silent failures are recorded at debug level.
service/firewall/packet_handler.go (1)

507-520: Log message format could be clearer.

The log format at line 517 produces output like "filter: special verdict Accept "IVPN VPN connection" <conn> for connection" which reads awkwardly due to the trailing "for connection" and the connection object placement.

💡 Suggested log format improvement
-				log.Tracer(pkt.Ctx()).Infof("filter: special verdict %s %q %s for connection", verdict, reason, conn)
+				log.Tracer(pkt.Ctx()).Infof("filter: external verdict %s for %s: %q", verdict, conn, reason)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/firewall/packet_handler.go` around lines 507 - 520, The log message
is awkwardly formatted; update the Infof call (the one using
log.Tracer(pkt.Ctx()).Infof inside the externalVerdictHandler handling) to
produce a clearer, self-contained message by removing the trailing "for
connection" and placing the connection representation in a sensible spot (or
using conn.String()/%v explicitly) so the output reads like "filter: special
verdict <verdict> <reason> for <conn>" or similar; keep references to
externalVerdictHandler.Load(), conn.SetVerdict(...), and pkt.Ctx() so the change
is localized to that Infof invocation.
service/interop/module.go (2)

92-92: Minor typo in comment.

"Ot is ok" should be "It is ok".

📝 Fix typo
-	// Ot is ok to check it once at startup, since it can only be changed by restarting Portmaster with a different config.
+	// It is ok to check it once at startup, since it can only be changed by restarting Portmaster with a different config.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/module.go` at line 92, Fix the typo in the comment that
currently reads "Ot is ok to check it once at startup, since it can only be
changed by restarting Portmaster with a different config." — change "Ot is ok"
to "It is ok" in the comment containing that exact phrase so the comment reads
"It is ok to check it once at startup, since it can only be changed by
restarting Portmaster with a different config."

125-128: Error from SetExternalVerdictHandler is silently ignored.

firewall.SetExternalVerdictHandler returns an error if a handler is already set, but this error is discarded. While the atomic flag prevents double registration from this module, logging a warning would help diagnose issues if another component unexpectedly registers a handler first.

💡 Log the error
 func (i *Interoperability) EnsureVerdictHandlerRegistered() {
 	if i.verdictHandlerRegistered.CompareAndSwap(false, true) {
-		firewall.SetExternalVerdictHandler(i.verdict_handler)
+		if err := firewall.SetExternalVerdictHandler(i.verdictHandler); err != nil {
+			i.mgr.Warn("failed to register external verdict handler: " + err.Error())
+		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/module.go` around lines 125 - 128,
EnsureVerdictHandlerRegistered currently ignores the error returned by
firewall.SetExternalVerdictHandler; update the function to capture that error
and log a warning if non-nil. Specifically, after calling
firewall.SetExternalVerdictHandler(i.verdict_handler) inside
EnsureVerdictHandlerRegistered, check the returned error and emit a warning that
includes the error and context (e.g., another component may have already
registered a handler). Use the module's logger (e.g., i.logger.Warnf or similar)
or fallback to the package logger to record the warning while keeping the atomic
verdictHandlerRegistered logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vscode/launch.json:
- Around line 19-25: The linux debug configuration currently uses "asRoot": true
with the Go debugger's default "mode": "auto", which causes the build to run as
root; change the configuration to set "mode": "exec" and add a "preLaunchTask"
that runs the build (e.g., go build --output=<bin> ...) as your normal user
before launch, keep "asRoot": true only for the launched executable, and keep
existing properties like "console" and "args" in the "linux" configuration so
the debug session executes the already-built binary with root privileges without
performing the build as root.

In `@service/interop/api.go`:
- Around line 15-28: The /interop/ping endpoint currently registers
APIEndpointPing with api.PermitAnyone for Read and Write, allowing
unauthenticated callers to trigger handlePing/PingHandler (which calls
connHandler.Go()); change the permissions in registerAPIEndpoints to use
api.PermitUser for both Read and Write so only authenticated users can invoke
this endpoint, and verify handlePing/PingHandler enforce/expect authenticated
context (adjust any auth checks or documentation accordingly).

In `@service/interop/module.go`:
- Around line 77-79: The stop() function currently does nothing; add a Stop()
error method to the interopModule interface and implement it on the IVPN module
(the IVPN type that currently provides Start()), then update stop() to iterate
the interopModules slice and call each module's Stop(), aggregating or logging
errors and returning a combined error if any; ensure the IVPN implementation
closes network connections and other resources gracefully and mirrors
Start()/Stop() semantics.

---

Nitpick comments:
In `@service/firewall/packet_handler.go`:
- Around line 507-520: The log message is awkwardly formatted; update the Infof
call (the one using log.Tracer(pkt.Ctx()).Infof inside the
externalVerdictHandler handling) to produce a clearer, self-contained message by
removing the trailing "for connection" and placing the connection representation
in a sensible spot (or using conn.String()/%v explicitly) so the output reads
like "filter: special verdict <verdict> <reason> for <conn>" or similar; keep
references to externalVerdictHandler.Load(), conn.SetVerdict(...), and pkt.Ctx()
so the change is localized to that Infof invocation.

In `@service/interop/api.go`:
- Line 30: The method receiver for Interoperability is named `c` in `handlePing`
but other methods use `i`; rename the receiver from `c` to `i` in the function
signature of `handlePing` (function name: handlePing, type: Interoperability) so
it matches the package-wide convention and avoids inconsistent receiver naming.
- Around line 11-13: The pingParams struct (type pingParams with field Message)
is unused by handlePing; remove the dead declaration to avoid unused field
warnings or, if intended, update handlePing to decode JSON into pingParams and
use pingParams.Message in the response; locate the type pingParams and either
delete it or add JSON decoding and usage in the handlePing function to consume
Message.

In `@service/interop/ivpn/ivpn.go`:
- Around line 279-281: Add a clear code comment above the conditional that
unconditionally accepts connections from the IVPN service binary (the block
checking status.serviceBinary != "" && conn.Process().Path ==
status.serviceBinary and returning network.VerdictAccept) explaining the trust
model: state that any process matching status.serviceBinary is fully trusted by
this filter, why that trust is required for IVPN to function, and any
limitations/assumptions (e.g., relies on path integrity, not performing further
validation like signature checking or user ownership); reference the symbols
status.serviceBinary, conn.Process().Path, and network.VerdictAccept so
reviewers know exactly which check the comment applies to.
- Around line 117-121: The ivpnclient.NewClientAsRoot call currently swallows
errors (client, err := ivpnclient.NewClientAsRoot(...); if err != nil { return
nil })—add a debug-level log that emits the error details before returning so
connection failures are visible during diagnosis; specifically log err (e.g.,
logger.Debugf or similar) in the NewClientAsRoot error branch and apply the same
change to the second identical block later (the other NewClientAsRoot error
check around lines noted in the review) so both silent failures are recorded at
debug level.

In `@service/interop/module.go`:
- Line 92: Fix the typo in the comment that currently reads "Ot is ok to check
it once at startup, since it can only be changed by restarting Portmaster with a
different config." — change "Ot is ok" to "It is ok" in the comment containing
that exact phrase so the comment reads "It is ok to check it once at startup,
since it can only be changed by restarting Portmaster with a different config."
- Around line 125-128: EnsureVerdictHandlerRegistered currently ignores the
error returned by firewall.SetExternalVerdictHandler; update the function to
capture that error and log a warning if non-nil. Specifically, after calling
firewall.SetExternalVerdictHandler(i.verdict_handler) inside
EnsureVerdictHandlerRegistered, check the returned error and emit a warning that
includes the error and context (e.g., another component may have already
registered a handler). Use the module's logger (e.g., i.logger.Warnf or similar)
or fallback to the package logger to record the warning while keeping the atomic
verdictHandlerRegistered logic intact.

In `@service/interop/verdict_handler.go`:
- Around line 7-15: Rename the method verdict_handler to camelCase
(verdictHandler) on the Interoperability receiver and update all call sites
(e.g., the call referenced in service/interop/module.go at the previous line
127) to use verdictHandler; ensure the method signature (func (i
*Interoperability) verdictHandler(conn *network.Connection) (network.Verdict,
string, bool)) and any variable shadowing inside the loop (verdict, reason,
skipTunnel) remain correct and compile after renaming.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 449a426d-6c14-4cf4-888d-3f2c9201716e

📥 Commits

Reviewing files that changed from the base of the PR and between d216fc2 and 73460f5.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • .vscode/launch.json
  • go.mod
  • service/firewall/interception/module.go
  • service/firewall/packet_handler.go
  • service/instance.go
  • service/interop/api.go
  • service/interop/ivpn/ivpn.go
  • service/interop/module.go
  • service/interop/verdict_handler.go

Comment on lines 19 to 25
"linux": {
"console": "integratedTerminal", // required to debug as root
"asRoot": true, // required to debug as root

"args": ["--bin-dir=/usr/lib/portmaster", "--log-stdout", "--log", "trace"]

},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the launch.json file to understand the context
cat -n .vscode/launch.json

Repository: safing/portmaster

Length of output: 2028


🌐 Web query:

VS Code debug configuration asRoot property

💡 Result:

asRoot is not a built-in VS Code launch.json property. It’s a Go extension (golang.vscode-go) launch attribute used to debug Go programs/tests as root.

  • When "asRoot": true, the Go extension runs Delve (dlv) via sudo, so the debug session must run in a terminal (set "console" to "integratedTerminal" or "externalTerminal").
  • Supported only on Linux and macOS (per the Go extension docs).
  • If you use it with build modes (auto/debug/test), the build runs as root too, which can cause env/cache permission issues; using "mode": "exec" + a preLaunchTask to build as your user is recommended. [1]

Example (Go extension):

{
  "name": "Launch as Root",
  "type": "go",
  "request": "launch",
  "mode": "exec",
  "asRoot": true,
  "program": "${workspaceFolder}/myprogram",
  "console": "integratedTerminal"
}

Sources: Go extension debugging docs (asRoot, console, sudo behavior, platform support, and caveats). [1]


Use "mode": "exec" with a preLaunchTask instead of "mode": "auto" when debugging as root.

The "asRoot": true property is valid and documented in the Go extension. However, using it with "mode": "auto" causes the build to run as root, which can lead to environment and cache permission issues. The recommended approach is to switch to "mode": "exec" and add a preLaunchTask that builds the binary as your regular user before launching the debug session with root privileges.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vscode/launch.json around lines 19 - 25, The linux debug configuration
currently uses "asRoot": true with the Go debugger's default "mode": "auto",
which causes the build to run as root; change the configuration to set "mode":
"exec" and add a "preLaunchTask" that runs the build (e.g., go build
--output=<bin> ...) as your normal user before launch, keep "asRoot": true only
for the launched executable, and keep existing properties like "console" and
"args" in the "linux" configuration so the debug session executes the
already-built binary with root privileges without performing the build as root.

Comment on lines +15 to +28
func (i *Interoperability) registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: APIEndpointPing,
Read: api.PermitAnyone,
Write: api.PermitAnyone,
ActionFunc: i.handlePing,
Name: "Interoperability: Ping Portmaster",
Description: "Ping the Portmaster Core Service for interoperability checks.",
}); err != nil {
return err
}

return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "api\.Permit" --type go -A1 -B1 | head -100

Repository: safing/portmaster

Length of output: 5158


🏁 Script executed:

# Read the interop/api.go file to see handlePing and understand what it does
cat -n service/interop/api.go

Repository: safing/portmaster

Length of output: 1350


🏁 Script executed:

# Search for PingHandler implementation
rg -n "PingHandler" --type go -A10 -B2 | head -80

Repository: safing/portmaster

Length of output: 1882


🏁 Script executed:

# Find connHandler definition and Go method
rg -n "connHandler.*Go\(\)" --type go -B5 -A5

Repository: safing/portmaster

Length of output: 1411


🏁 Script executed:

# Also check how the API service is set up and if it's local-only
rg -n "RegisterEndpoint\|api\.Endpoint" service/interop/api.go -B3 -A8

Repository: safing/portmaster

Length of output: 43


🏁 Script executed:

# Check if there's API authentication setup or local-only binding
fd "api" service/ --type f -name "*.go" | head -5

Repository: safing/portmaster

Length of output: 291


🏁 Script executed:

# Search for API server configuration and binding
rg -n "ListenAddr\|localhost\|127.0.0.1" service/ --type go -B2 -A2 | head -60

Repository: safing/portmaster

Length of output: 43


🏁 Script executed:

# Check for rate limiting or auth middleware
rg -n "WrapInAuthHandler\|rate.*limit\|RateLimit" service/ --type go | head -30

Repository: safing/portmaster

Length of output: 43


🏁 Script executed:

# Find base/api package files
find . -path "./base/api*" -type f -name "*.go" | head -20

Repository: safing/portmaster

Length of output: 595


🏁 Script executed:

# Check what PermitAnyone actually means
rg -n "PermitAnyone\|PermitUser\|PermitAdmin" base/api --type go -B2 -A2 | head -80

Repository: safing/portmaster

Length of output: 43


🏁 Script executed:

# Search for Permission type definition and constants
rg -n "type Permission\|const.*Permit" base/api/ --type go -B1 -A3 | head -100

Repository: safing/portmaster

Length of output: 43


🏁 Script executed:

# Check authentication logic
cat -n base/api/authentication.go | head -80

Repository: safing/portmaster

Length of output: 3024


🏁 Script executed:

# Search for API server initialization and listening setup
rg -n "Listen\|Server\|http\.Server" base/api/main.go --type go -B3 -A3 | head -100

Repository: safing/portmaster

Length of output: 43


🏁 Script executed:

# Check if there's documentation about API security model
cat base/api/doc.go

Repository: safing/portmaster

Length of output: 983


🏁 Script executed:

# Check if there are any other PermitAnyone endpoints that trigger operations
rg -n "PermitAnyone" service/ --type go -B3 -A3

Repository: safing/portmaster

Length of output: 3608


Endpoint permission inconsistent with security pattern

The /interop/ping endpoint uses api.PermitAnyone on both Read and Write, allowing unauthenticated callers to trigger PingHandler(), which queues connection attempts via connHandler.Go(). This is inconsistent with other operational endpoints (e.g., netquery, process modules) which use PermitUser. Repeated unauthenticated calls could be abused to trigger resource-intensive connection attempts.

Align with the established pattern and restrict to authenticated users:

Suggested fix
 	if err := api.RegisterEndpoint(api.Endpoint{
 		Path:        APIEndpointPing,
-		Read:        api.PermitAnyone,
-		Write:       api.PermitAnyone,
+		Read:        api.PermitUser,
+		Write:       api.PermitUser,
 		ActionFunc:  i.handlePing,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/api.go` around lines 15 - 28, The /interop/ping endpoint
currently registers APIEndpointPing with api.PermitAnyone for Read and Write,
allowing unauthenticated callers to trigger handlePing/PingHandler (which calls
connHandler.Go()); change the permissions in registerAPIEndpoints to use
api.PermitUser for both Read and Write so only authenticated users can invoke
this endpoint, and verify handlePing/PingHandler enforce/expect authenticated
context (adjust any auth checks or documentation accordingly).

Comment on lines +77 to +79
func stop() error {
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

stop() does not clean up interop modules.

The start() function calls Start() on each interop module, but stop() is empty. This could lead to resource leaks or improper cleanup when the module is stopped (e.g., IVPN client connections not being closed gracefully).

💡 Suggested implementation
 func stop() error {
+	// Stop all interop modules in reverse order
+	for j := len(module.interopModules) - 1; j >= 0; j-- {
+		// Consider adding Stop() to the interopModule interface
+		// and calling it here
+	}
 	return nil
 }

Consider adding a Stop() error method to the interopModule interface and implementing it in the IVPN module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/interop/module.go` around lines 77 - 79, The stop() function
currently does nothing; add a Stop() error method to the interopModule interface
and implement it on the IVPN module (the IVPN type that currently provides
Start()), then update stop() to iterate the interopModules slice and call each
module's Stop(), aggregating or logging errors and returning a combined error if
any; ensure the IVPN implementation closes network connections and other
resources gracefully and mirrors Start()/Stop() semantics.

@stenya stenya changed the title IVPN client interoperability feat(interop): IVPN client interoperability Mar 6, 2026
@stenya stenya added this to the v2.1.9 milestone Mar 6, 2026
…isplay

Add a `Visibility` field to the `Action` struct allowing actions to be
hidden in the compact notification view and only shown when the user
expands the full notification (value: "detailed").

- base/notifications: add `ActionVisibility` type and `ActionVisibilityDetailed`
  constant to `Action` struct
- notifications.types.ts: expose `Visibility` field on the frontend `BaseAction`
  interface
- notification-list.component.html: filter out `detailed` actions in the
  compact list view
- generic-setting.ts: set default `Visibility: ''` on the inline UI action
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ stenya
❌ Alexandr Stelnykovych


Alexandr Stelnykovych seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

stenya added 5 commits March 9, 2026 22:16
…uppress option

When Portmaster connects to the IVPN Client, display an info notification
informing the user that IVPN connections are allowed and that DNS will be
handled by Portmaster's local resolver when configured.

The notification includes a "Do not notify me anymore" action that
permanently suppresses future notifications by writing a marker record to
the core database. The check runs before showing the notification on each
subsequent connection.

The "Reset Notification States" API endpoint (and matching UI menu item)
now also clears the IVPN suppression record alongside the broadcast states,
so all suppressed notifications can be restored at once.

- service/interop/ivpn: add notification.go with initAndShowNotification,
  isNotificationSuppressed, and suppressNotification
- service/interop/ivpn/ivpn.go: show notification on connect if not suppressed
- service/broadcasts/api.go: extend reset-state handler to also delete the
  IVPN suppression record; update endpoint name and description
- desktop: rename "Reset Broadcast State" menu item and toast messages to
  "Reset Notifications State"
Actions with visibility "in-app-only" are shown in the UI but skipped
when displaying system-level notifications (Tauri/OS). Updates Go,
TypeScript/Angular and Rust projects accordingly.
Add "Don't show again" action to the stale cache notification.
Suppression state is stored in the database and checked on startup.
System notification is shown only on first occurrence.
Reset handler in broadcasts now also clears the suppression record.

#2061
@stenya stenya marked this pull request as ready for review March 10, 2026 13:07
@stenya stenya merged commit 195c4ba into development Mar 10, 2026
3 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants