Documentation
¶
Overview ¶
Package portforward opens TCP and UDP ports on the local network gateway so a node's inbounds are reachable from the public internet without manual router configuration. It tries PCP, then NAT-PMP, then UPnP IGDv2, then IGDv1.
Index ¶
- Constants
- Variables
- func LocalIP() (string, error)
- func ParseManualPort(s string) (uint16, error)
- func ProbeUPnP(ctx context.Context) bool
- type Forwarder
- func (f *Forwarder) ExternalIP(ctx context.Context) (string, error)
- func (f *Forwarder) MapPort(ctx context.Context, protocol string, internalPort uint16, description string) (*Mapping, error)
- func (f *Forwarder) StartRenewal(ctx context.Context)
- func (f *Forwarder) UnmapPort(ctx context.Context) error
- type ManualForwarder
- func (m *ManualForwarder) ExternalIP(_ context.Context) (string, error)
- func (m *ManualForwarder) MapPort(_ context.Context, protocol string, _ uint16, _ string) (*Mapping, error)
- func (m *ManualForwarder) Port() uint16
- func (m *ManualForwarder) StartRenewal(_ context.Context)
- func (m *ManualForwarder) UnmapPort(_ context.Context) error
- type Mapping
Constants ¶
const ( TCP = "TCP" UDP = "UDP" )
Protocols a mapping can carry, as UPnP IGD names them.
Variables ¶
var ErrNoPortForwarding = errors.New("no port forwarding available")
ErrNoPortForwarding is returned when no gateway is reachable, the gateway refuses to map a port, or the discovery scan times out. Callers should treat this as "this network can't host a peer proxy" and surface it to the user rather than retry indefinitely.
Functions ¶
func ParseManualPort ¶
ParseManualPort parses a string into a port number. Values outside 1..65535 return an error so callers can log and fall through to UPnP discovery rather than register a non-listening port with the server.
func ProbeUPnP ¶
ProbeUPnP reports whether discovery on the local network turns up a gateway that could host a port mapping, by PCP, NAT-PMP or UPnP IGD. No port is actually mapped — this is the "would Share My Connection's residential-proxy mode work on this network?" pre-flight check that UI flows show before committing to the long-lived SmC path.
True means discovery succeeded; false means either no gateway was found (ErrNoPortForwarding) or ctx expired before discovery completed. Callers generally treat both as "not available" — distinguishing them requires returning the underlying error, which most UI surfaces don't have a productive use for.
Pick a 5-10s timeout on ctx: M-SEARCH multicast waits for replies and a fast bail leaves slower gateways unmatched. The discovered forwarder is discarded after the probe; it holds no goroutines or sockets that need explicit cleanup, so a subsequent NewForwarder call can re-discover without coordination.
Types ¶
type Forwarder ¶
type Forwarder struct {
// contains filtered or unexported fields
}
Forwarder manages port mappings on the local gateway, at most one per protocol. Construct one per session.
func NewForwarder ¶
NewForwarder discovers the local gateway and returns a Forwarder bound to it, using the first of PCP, NAT-PMP, UPnP IGDv2 and IGDv1 the gateway speaks. Callers should pick a 5-10s timeout on ctx — UPnP discovery is M-SEARCH multicast and waits for replies. PCP and NAT-PMP take at most pmpProbeTimeout of it.
Returns ErrNoPortForwarding only when discovery completes without finding a usable gateway. If ctx was canceled or its deadline expired during discovery, the ctx error is returned verbatim so callers can distinguish "this network can't host a peer" from "we ran out of time, retry later".
func (*Forwarder) ExternalIP ¶
ExternalIP queries the gateway for its WAN-side IP address. Cheaper than dialing a public-IP service when we already have a UPnP client open.
func (*Forwarder) MapPort ¶
func (f *Forwarder) MapPort(ctx context.Context, protocol string, internalPort uint16, description string) (*Mapping, error)
MapPort asks the gateway to forward externalPort → (LocalIP():internalPort) for protocol, TCP or UDP. A Forwarder holds one mapping per protocol. Lease duration is requested as 1 hour but some routers ignore the request and assign their own (or none — "permanent"). description is shown in the router's UI so users can identify and remove the mapping manually if needed.
func (*Forwarder) StartRenewal ¶
func (*Forwarder) UnmapPort ¶
UnmapPort removes every active mapping. No-op if none is active. Always called as part of teardown — even if the gateway has already let the lease expire, DeletePortMapping is the polite signal to the router.
A mapping is forgotten only on a successful delete. A failed delete leaves it in place so the caller can retry; otherwise we'd "forget" about a router rule that's actually still live and the user would have to wait for the UPnP lease to expire.
type ManualForwarder ¶
type ManualForwarder struct {
// contains filtered or unexported fields
}
ManualForwarder exposes the same Map/Unmap/StartRenewal/ExternalIP surface as Forwarder but does no UPnP work. The user is expected to have configured a port forward on their router by hand and supplied the port number out-of-band. This implementation reports the same value for both the external and internal port — callers needing distinct external/internal ports should use the UPnP-based Forwarder (which can negotiate them) or build their own portForwarder.
Use case: networks where UPnP is disabled or unavailable (router has UPnP off for security, ISP-provided gateways without IGD, networks behind double-NAT). The UPnP-based Forwarder fails in those environments; this type lets callers bypass discovery entirely.
func NewManualForwarder ¶
func NewManualForwarder(port uint16) *ManualForwarder
NewManualForwarder builds a ManualForwarder for a pre-configured router port forward. port must be in 1..65535; the caller is responsible for validating its input before calling.
func (*ManualForwarder) ExternalIP ¶
func (m *ManualForwarder) ExternalIP(_ context.Context) (string, error)
ExternalIP returns the empty string deliberately. With a manual port forward we have no UPnP gateway to ask for the WAN address, and probing a public IP service from the client adds a network roundtrip for information lantern-cloud already has — the server observes the peer's source address on the register call and uses that as the canonical external IP when this field is empty.
func (*ManualForwarder) MapPort ¶
func (m *ManualForwarder) MapPort(_ context.Context, protocol string, _ uint16, _ string) (*Mapping, error)
MapPort reports the configured port as both external and internal, for protocol. The router-side rule is already in place; nothing to do at the protocol layer. Whether the user forwarded that protocol is unknown here, so callers verify reachability for each protocol they use. Returns an error if the forwarder was constructed with port==0 (not a valid listen/registration port) so callers can fall through to UPnP rather than register a port no peer will listen on.
func (*ManualForwarder) Port ¶
func (m *ManualForwarder) Port() uint16
Port returns the router port this forwarder was configured with.
func (*ManualForwarder) StartRenewal ¶
func (m *ManualForwarder) StartRenewal(_ context.Context)
StartRenewal is a no-op: manually-configured rules don't carry a UPnP lease and don't need refreshing.