diff --git a/net/portmapper/portmapper.go b/net/portmapper/portmapper.go index e22e8932e..a865ce225 100644 --- a/net/portmapper/portmapper.go +++ b/net/portmapper/portmapper.go @@ -63,6 +63,13 @@ type Client struct { pmpMapping *pmpMapping // non-nil if we have a PMP mapping } +// HaveMapping reports whether we have a current valid mapping. +func (c *Client) HaveMapping() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.pmpMapping != nil && c.pmpMapping.useUntil.After(time.Now()) +} + // pmpMapping is an already-created PMP mapping. // // All fields are immutable once created. diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index bd61403be..9528b6cf9 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -431,6 +431,10 @@ type NetInfo struct { // WorkingUDP is whether UDP works. WorkingUDP opt.Bool + // HavePortMap is whether we have an existing portmap open + // (UPnP, PMP, or PCP). + HavePortMap bool `json:",omitempty"` + // UPnP is whether UPnP appears present on the LAN. // Empty means not checked. UPnP opt.Bool @@ -479,10 +483,14 @@ func (ni *NetInfo) String() string { } func (ni *NetInfo) portMapSummary() string { - if ni.UPnP == "" && ni.PMP == "" && ni.PCP == "" { + if !ni.HavePortMap && ni.UPnP == "" && ni.PMP == "" && ni.PCP == "" { return "?" } - return conciseOptBool(ni.UPnP, "U") + conciseOptBool(ni.PMP, "M") + conciseOptBool(ni.PCP, "C") + var prefix string + if ni.HavePortMap { + prefix = "active-" + } + return prefix + conciseOptBool(ni.UPnP, "U") + conciseOptBool(ni.PMP, "M") + conciseOptBool(ni.PCP, "C") } func conciseOptBool(b opt.Bool, trueVal string) string { @@ -512,6 +520,7 @@ func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool { ni.HairPinning == ni2.HairPinning && ni.WorkingIPv6 == ni2.WorkingIPv6 && ni.WorkingUDP == ni2.WorkingUDP && + ni.HavePortMap == ni2.HavePortMap && ni.UPnP == ni2.UPnP && ni.PMP == ni2.PMP && ni.PCP == ni2.PCP && diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index ee57f8113..89633c5c8 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -143,6 +143,7 @@ var _NetInfoNeedsRegeneration = NetInfo(struct { HairPinning opt.Bool WorkingIPv6 opt.Bool WorkingUDP opt.Bool + HavePortMap bool UPnP opt.Bool PMP opt.Bool PCP opt.Bool diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 5fa579b0d..068a6a149 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -375,6 +375,7 @@ func TestNetInfoFields(t *testing.T) { "HairPinning", "WorkingIPv6", "WorkingUDP", + "HavePortMap", "UPnP", "PMP", "PCP", diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 81ad54e49..70ddb99f5 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -628,6 +628,23 @@ func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (chan return true } +// setNetInfoHavePortMap updates NetInfo.HavePortMap to true. +func (c *Conn) setNetInfoHavePortMap() { + c.mu.Lock() + defer c.mu.Unlock() + if c.netInfoLast == nil { + // No NetInfo yet. Nothing to update. + return + } + if c.netInfoLast.HavePortMap { + // No change. + return + } + ni := c.netInfoLast.Clone() + ni.HavePortMap = true + c.callNetInfoCallbackLocked(ni) +} + func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) { c.mu.Lock() dm := c.derpMap @@ -658,6 +675,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) { UPnP: report.UPnP, PMP: report.PMP, PCP: report.PCP, + HavePortMap: c.portMapper.HaveMapping(), } for rid, d := range report.RegionV4Latency { ni.DERPLatency[fmt.Sprintf("%d-v4", rid)] = d.Seconds() @@ -752,6 +770,10 @@ func (c *Conn) callNetInfoCallback(ni *tailcfg.NetInfo) { if ni.BasicallyEqual(c.netInfoLast) { return } + c.callNetInfoCallbackLocked(ni) +} + +func (c *Conn) callNetInfoCallbackLocked(ni *tailcfg.NetInfo) { c.netInfoLast = ni if c.netInfoFunc != nil { c.logf("[v1] magicsock: netInfo update: %+v", ni) @@ -1016,6 +1038,7 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason if ext, err := c.portMapper.CreateOrGetMapping(ctx); err == nil { addAddr(ext.String(), "portmap") + c.setNetInfoHavePortMap() } else if !portmapper.IsNoMappingError(err) { c.logf("portmapper: %v", err) }