fix(interop/ivpn): Handle local service port connections
Some checks are pending
Go / Linter (push) Waiting to run
Go / Test & Build (push) Waiting to run
Release v2.X / Installer windows (push) Blocked by required conditions
Release v2.X / Prep (push) Waiting to run
Release v2.X / Installer linux (push) Blocked by required conditions

The changes add support for tracking and properly routing connections to the IVPN client's local service port,
which is needed when the default firewall action is to block unknown connections.
The verdict handler is now registered earlier in the connection flow to handle connections while the client is connecting.

https://github.com/safing/portmaster-shadow/issues/34
This commit is contained in:
Alexandr Stelnykovych 2026-04-03 10:13:07 +03:00
parent a3f746d2b1
commit d8cc799203
3 changed files with 37 additions and 12 deletions

2
go.mod
View file

@ -30,7 +30,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb
github.com/ivpn/desktop-app/daemon/protocol/ivpnclient v0.0.0-20260326085211-75eceae81a57
github.com/ivpn/desktop-app/daemon/protocol/ivpnclient v0.0.0-20260403070528-13364be7597e
github.com/jackc/puddle/v2 v2.2.2
github.com/jaswdr/faker/v2 v2.9.0
github.com/lmittmann/tint v1.1.2

4
go.sum
View file

@ -225,8 +225,8 @@ github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cO
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/ivpn/desktop-app/daemon/protocol/ivpnclient v0.0.0-20260326085211-75eceae81a57 h1:UbPzCftqy05OKk7spUtASQz5d8jutgbneAwpJPoiWvA=
github.com/ivpn/desktop-app/daemon/protocol/ivpnclient v0.0.0-20260326085211-75eceae81a57/go.mod h1:dvedOGgEXvFCK/gz+BJQhd5BSTQvIygmOr/22xPdgIw=
github.com/ivpn/desktop-app/daemon/protocol/ivpnclient v0.0.0-20260403070528-13364be7597e h1:UzZ/4Ya1jvWuC/NjyhnqfD70rkJ2QklcFpatbCqBjeg=
github.com/ivpn/desktop-app/daemon/protocol/ivpnclient v0.0.0-20260403070528-13364be7597e/go.mod h1:dvedOGgEXvFCK/gz+BJQhd5BSTQvIygmOr/22xPdgIw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaswdr/faker/v2 v2.9.0 h1:Sqqpp+pxduDO+MGOhYE3UHtI9Sowt9j95f8h8nVvips=

View file

@ -13,6 +13,7 @@ import (
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/spn/hub"
)
@ -43,6 +44,7 @@ type vpnConnectionInfo struct {
// and its current VPN connection status, used for providing context in firewall verdicts.
type clientStatus struct {
serviceBinary string
servicePort uint16
vpnConnection vpnConnectionInfo // VPN server endpoint
connectedInfo *ivpnclient.ConnectedResp // info about already established VPN connection, if any
}
@ -133,14 +135,28 @@ func (i *InteropIvpn) connectIvpnClient(wc *mgr.WorkerCtx) error {
notifWarnOldVersion.Store(nil)
}
ci := ivpnclient.ClientInfo{
Type: ivpnclient.ClientPortmaster,
Name: "Portmaster",
Version: info.Version()}
servicePort, _, err := ivpnclient.GetConnectionPortInfo()
if err != nil {
return err
}
// Save ServicePort.
status := *i.getStatus()
status.servicePort = uint16(servicePort)
i.setStatus(&status)
// Now we know the service port, we can register the verdict handler
// to allow accepting connections to the IVPN client service port while the client is connecting.
// This is needed for case when Portmaster default action is to block unknown connections.
i.owner.EnsureVerdictHandlerRegistered()
// Create client.
// Ignoring error here, since it is expected that the client may not be in running state
client, err := ivpnclient.NewClientAsRoot(nil, time.Second*10, ci)
client, err := ivpnclient.NewClientAsRoot(
nil,
time.Second*10,
ivpnclient.ClientInfo{
Type: ivpnclient.ClientPortmaster,
Name: "Portmaster",
Version: info.Version()})
if err != nil {
return nil
}
@ -183,15 +199,12 @@ func (i *InteropIvpn) connectIvpnClient(wc *mgr.WorkerCtx) error {
}
// Save ServiceBinary.
status := *i.getStatus()
status = *i.getStatus()
status.serviceBinary = helloResp.ServiceBinary
i.setStatus(&status)
// The status.vpnConnection must be already initialized (ConnectionStarting message already received).
i.setFirstTryDone()
// Notify owner that we can now provide verdicts for firewall module
i.owner.EnsureVerdictHandlerRegistered()
wc.Debug(fmt.Sprintf("Connected to IVPN client %s", helloResp.Version))
// Show UI notification if not suppressed by user
@ -270,6 +283,7 @@ func (i *InteropIvpn) VerdictHandler(conn *network.Connection) (verdict network.
return network.VerdictUndecided, "", false
}
// Connection to remote VPN server
if status.vpnConnection.dstPort != 0 {
if conn.Entity.Port == status.vpnConnection.dstPort &&
conn.Entity.Protocol == status.vpnConnection.protocol &&
@ -283,6 +297,17 @@ func (i *InteropIvpn) VerdictHandler(conn *network.Connection) (verdict network.
}
}
// connections to IVPN service port
if conn.LocalIPScope == netutils.HostLocal && conn.IPProtocol == packet.TCP {
if conn.Inbound && conn.LocalPort == status.servicePort {
return network.VerdictAccept, "IVPN Local Service connection", true
}
if !conn.Inbound && conn.Entity.Port == status.servicePort {
return network.VerdictAccept, "IVPN Local Service connection", true
}
}
// Connections from/to IVPN service (only when serviceBinary initialized)
if status.serviceBinary != "" && conn.Process().Path == status.serviceBinary {
return network.VerdictAccept, "IVPN Service connection", true
}