Files
netbird/client/internal/portforward/state.go
T
jnfrati c0cb24b6ce [client] Add unicast UPnP discovery for port forwarding.
Add a UPnP IGD discovery fallback that sends unicast SSDP M-SEARCH requests directly to the default gateway when PCP and multicast discovery do not find a gateway.
2026-07-02 15:27:02 +02:00

89 lines
2.3 KiB
Go

//go:build !js
package portforward
import (
"context"
"fmt"
"github.com/libp2p/go-nat"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/portforward/pcp"
"github.com/netbirdio/netbird/client/internal/portforward/upnp"
)
// discoverGateway is the function used for NAT gateway discovery.
// It can be replaced in tests to avoid real network operations.
// Tries PCP first, then falls back to NAT-PMP/UPnP.
var discoverGateway = defaultDiscoverGateway
func defaultDiscoverGateway(ctx context.Context) (nat.NAT, error) {
pcpGateway, err := pcp.DiscoverPCP(ctx)
if err == nil {
return pcpGateway, nil
}
log.Debugf("PCP discovery failed: %v, trying NAT-PMP/UPnP", err)
// Multicast SSDP is not delivered on all networks (hypervisor bridges,
// IGMP-snooping switches), while MiniUPnPd-based gateways also answer
// unicast M-SEARCH sent directly to them. Run a unicast UPnP search
// against the default gateway alongside the multicast discovery.
unicastResult := make(chan nat.NAT, 1)
go func() {
gateway, err := upnp.Discover(ctx)
if err != nil {
log.Debugf("unicast UPnP discovery failed: %v", err)
unicastResult <- nil
return
}
unicastResult <- gateway
}()
gateway, err := nat.DiscoverGateway(ctx)
if err == nil {
return gateway, nil
}
if gateway := <-unicastResult; gateway != nil {
log.Debugf("gateway found via unicast UPnP discovery")
return gateway, nil
}
return nil, err
}
// State is persisted only for crash recovery cleanup
type State struct {
InternalPort uint16 `json:"internal_port,omitempty"`
Protocol string `json:"protocol,omitempty"`
}
func (s *State) Name() string {
return "port_forward_state"
}
// Cleanup implements statemanager.CleanableState for crash recovery
func (s *State) Cleanup() error {
if s.InternalPort == 0 {
return nil
}
log.Infof("cleaning up stale port mapping for port %d", s.InternalPort)
ctx, cancel := context.WithTimeout(context.Background(), discoveryTimeout)
defer cancel()
gateway, err := discoverGateway(ctx)
if err != nil {
// Discovery failure is not an error - gateway may not exist
log.Debugf("cleanup: no gateway found: %v", err)
return nil
}
if err := gateway.DeletePortMapping(ctx, s.Protocol, int(s.InternalPort)); err != nil {
return fmt.Errorf("delete port mapping: %w", err)
}
return nil
}