mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 09:01:29 +02:00
131 lines
4.0 KiB
Go
131 lines
4.0 KiB
Go
//go:build !js
|
|
|
|
package portforward
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/netbirdio/go-nat"
|
|
"github.com/netbirdio/go-nat/pcp"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// discoverGateway is the function used for NAT gateway discovery.
|
|
// It can be replaced in tests to avoid real network operations.
|
|
var discoverGateway = defaultDiscoverGateway
|
|
|
|
// pinholeDiscoveryTimeout is the slice of the discovery budget held back for
|
|
// the IPv6 pinhole probe.
|
|
//
|
|
// Sizing it is coarser than it looks: PCP retransmits on a 3s socket timeout
|
|
// and a 3s first backoff, so a second attempt needs about 9s. Anything from
|
|
// roughly 1s to 8s therefore buys exactly one attempt, and this only sets how
|
|
// long that attempt waits. A PCP server sits on the local link and answers in
|
|
// milliseconds, so 3s is margin rather than need, and the rest is left to
|
|
// gateway discovery, whose multicast SSDP search alone takes 5s. A probe lost
|
|
// to a dropped packet is retried by the next discovery round.
|
|
//
|
|
// It is a variable so tests can shorten it.
|
|
var pinholeDiscoveryTimeout = 3 * time.Second
|
|
|
|
// Discovery entry points, as variables so tests can drive the fallback without
|
|
// touching the network.
|
|
var (
|
|
discoverNATGateway = nat.DiscoverGateway
|
|
|
|
discoverPCPPinhole = func(ctx context.Context) (nat.NAT, error) {
|
|
pinhole, err := pcp.DiscoverPCP(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pinhole, nil
|
|
}
|
|
)
|
|
|
|
// defaultDiscoverGateway finds a gateway that can make the WireGuard port
|
|
// reachable. DiscoverGateway prefers PCP for IPv4, races UPnP and NAT-PMP
|
|
// behind it, and attaches an IPv6 pinhole independently of which IPv4 protocol
|
|
// wins.
|
|
//
|
|
// It reports no gateway on a network offering only IPv6, having no IPv4 mapping
|
|
// to attach a pinhole to. Such a network still needs one: there is no
|
|
// translation to traverse, but the router drops inbound IPv6 until something
|
|
// opens it. Fall back to PCP alone, which yields a gateway holding just the
|
|
// pinhole.
|
|
func defaultDiscoverGateway(ctx context.Context) (nat.NAT, error) {
|
|
gatewayCtx, cancel := reserveForPinhole(ctx)
|
|
defer cancel()
|
|
|
|
gateway, err := discoverNATGateway(gatewayCtx)
|
|
if err == nil {
|
|
return gateway, nil
|
|
}
|
|
if !errors.Is(err, nat.ErrNoNATFound) {
|
|
return nil, err
|
|
}
|
|
|
|
pinhole, pinholeErr := discoverPCPPinhole(ctx)
|
|
if pinholeErr != nil {
|
|
log.Debugf("no IPv6 pinhole after %v: %v", err, pinholeErr)
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof("no IPv4 gateway, continuing with an IPv6 pinhole only")
|
|
return pinhole, nil
|
|
}
|
|
|
|
// reserveForPinhole shortens ctx so that a pinhole probe still has time to run
|
|
// afterwards. Finding nothing takes gateway discovery everything it is given,
|
|
// so on the unshortened context the probe would start already expired. A budget
|
|
// too small to divide is left to gateway discovery, which is the likelier win.
|
|
func reserveForPinhole(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
return context.WithCancel(ctx)
|
|
}
|
|
|
|
remaining := time.Until(deadline)
|
|
if remaining <= pinholeDiscoveryTimeout {
|
|
return context.WithCancel(ctx)
|
|
}
|
|
return context.WithTimeout(ctx, remaining-pinholeDiscoveryTimeout)
|
|
}
|
|
|
|
// 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
|
|
}
|