mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
Enable UPnP portmapping
This actually adds portmapping in UPnP, which is checked after NAT-PMP. Signed-off-by: julianknodt <julianknodt@gmail.com>
This commit is contained in:
parent
c6b92ddda8
commit
cb2d9c13fe
@ -38,7 +38,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/upnp from tailscale.com/net/upnp/dcps/internetgateway2
|
||||
tailscale.com/net/upnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
tailscale.com/net/upnp/httpu from tailscale.com/net/upnp+
|
||||
tailscale.com/net/upnp/httpu from tailscale.com/net/upnp
|
||||
tailscale.com/net/upnp/scpd from tailscale.com/net/upnp
|
||||
tailscale.com/net/upnp/soap from tailscale.com/net/upnp+
|
||||
tailscale.com/net/upnp/ssdp from tailscale.com/net/upnp
|
||||
|
@ -114,7 +114,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/upnp from tailscale.com/net/upnp/dcps/internetgateway2
|
||||
tailscale.com/net/upnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
tailscale.com/net/upnp/httpu from tailscale.com/net/upnp+
|
||||
tailscale.com/net/upnp/httpu from tailscale.com/net/upnp
|
||||
tailscale.com/net/upnp/scpd from tailscale.com/net/upnp
|
||||
tailscale.com/net/upnp/soap from tailscale.com/net/upnp+
|
||||
tailscale.com/net/upnp/ssdp from tailscale.com/net/upnp
|
||||
|
@ -12,4 +12,4 @@ func networkIsUnreachable(err error) bool { return false }
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. It always returns false on this
|
||||
// platform.
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
|
@ -693,7 +693,8 @@ func (rs *reportState) probePortMapServices() {
|
||||
rs.setOptBool(&rs.report.PMP, false)
|
||||
rs.setOptBool(&rs.report.PCP, false)
|
||||
|
||||
res, err := rs.c.PortMapper.Probe(context.Background())
|
||||
rs.c.PortMapper.NewProber(context.Background())
|
||||
res, err := rs.c.PortMapper.Prober.StatusBlock()
|
||||
if err != nil {
|
||||
rs.c.logf("probePortMapServices: %v", err)
|
||||
return
|
||||
|
@ -56,18 +56,25 @@ type Client struct {
|
||||
pmpPubIPTime time.Time // time pmpPubIP last verified
|
||||
pmpLastEpoch uint32
|
||||
|
||||
pcpSawTime time.Time // time we last saw PCP was available
|
||||
uPnPSawTime time.Time // time we last saw UPnP was available
|
||||
localPort uint16
|
||||
|
||||
localPort uint16
|
||||
pmpMapping *pmpMapping // non-nil if we have a PMP mapping
|
||||
mapping Mapping // non-nil if we have a mapping
|
||||
|
||||
Prober *Prober
|
||||
}
|
||||
|
||||
type Mapping interface {
|
||||
isCurrent() bool
|
||||
release()
|
||||
validUntil() time.Time
|
||||
externalIPPort() netaddr.IPPort
|
||||
}
|
||||
|
||||
// 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())
|
||||
return c.mapping != nil && c.mapping.isCurrent()
|
||||
}
|
||||
|
||||
// pmpMapping is an already-created PMP mapping.
|
||||
@ -86,6 +93,10 @@ func (m *pmpMapping) externalValid() bool {
|
||||
return !m.external.IP().IsZero() && m.external.Port() != 0
|
||||
}
|
||||
|
||||
func (p *pmpMapping) isCurrent() bool { return p.useUntil.After(time.Now()) }
|
||||
func (p *pmpMapping) validUntil() time.Time { return p.useUntil }
|
||||
func (p *pmpMapping) externalIPPort() netaddr.IPPort { return p.external }
|
||||
|
||||
// release does a best effort fire-and-forget release of the PMP mapping m.
|
||||
func (m *pmpMapping) release() {
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
@ -118,8 +129,8 @@ func (c *Client) SetGatewayLookupFunc(f func() (gw, myIP netaddr.IP, ok bool)) {
|
||||
// comes back.
|
||||
func (c *Client) NoteNetworkDown() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.invalidateMappingsLocked(false)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
@ -153,7 +164,6 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
||||
gw = netaddr.IP{}
|
||||
myIP = netaddr.IP{}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -166,16 +176,14 @@ func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
|
||||
}
|
||||
|
||||
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
|
||||
if c.pmpMapping != nil {
|
||||
if c.mapping != nil {
|
||||
if releaseOld {
|
||||
c.pmpMapping.release()
|
||||
c.mapping.release()
|
||||
}
|
||||
c.pmpMapping = nil
|
||||
c.mapping = nil
|
||||
}
|
||||
c.pmpPubIP = netaddr.IP{}
|
||||
c.pmpPubIPTime = time.Time{}
|
||||
c.pcpSawTime = time.Time{}
|
||||
c.uPnPSawTime = time.Time{}
|
||||
}
|
||||
|
||||
func (c *Client) sawPMPRecently() bool {
|
||||
@ -189,15 +197,19 @@ func (c *Client) sawPMPRecentlyLocked() bool {
|
||||
}
|
||||
|
||||
func (c *Client) sawPCPRecently() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||
if c.Prober == nil {
|
||||
return false
|
||||
}
|
||||
present, _ := c.Prober.PCP.PresentCurrent()
|
||||
return present
|
||||
}
|
||||
|
||||
func (c *Client) sawUPnPRecently() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
|
||||
if c.Prober == nil {
|
||||
return false
|
||||
}
|
||||
present, _ := c.Prober.UPnP.PresentCurrent()
|
||||
return present
|
||||
}
|
||||
|
||||
// closeCloserOnContextDone starts a new goroutine to call c.Close
|
||||
@ -264,13 +276,13 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
|
||||
// Do we have an existing mapping that's valid?
|
||||
now := time.Now()
|
||||
if m := c.pmpMapping; m != nil {
|
||||
if now.Before(m.useUntil) {
|
||||
if m := c.mapping; m != nil {
|
||||
if now.Before(m.validUntil()) {
|
||||
defer c.mu.Unlock()
|
||||
return m.external, nil
|
||||
return m.externalIPPort(), nil
|
||||
}
|
||||
// The mapping might still be valid, so just try to renew it.
|
||||
prevPort = m.external.Port()
|
||||
prevPort = m.externalIPPort().Port()
|
||||
}
|
||||
|
||||
// If we just did a Probe (e.g. via netchecker) but didn't
|
||||
@ -280,11 +292,11 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
if haveRecentPMP {
|
||||
m.external = m.external.WithIP(c.pmpPubIP)
|
||||
}
|
||||
|
||||
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
|
||||
c.mu.Unlock()
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
@ -319,7 +331,8 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
if ctx.Err() == context.Canceled {
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
// switch to trying UPnP
|
||||
break
|
||||
}
|
||||
srcu := srci.(*net.UDPAddr)
|
||||
src, ok := netaddr.FromStdAddr(srcu.IP, srcu.Port, srcu.Zone)
|
||||
@ -350,10 +363,58 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
if m.externalValid() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.pmpMapping = m
|
||||
c.mapping = m
|
||||
return m.external, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If did not see UPnP within the past 5 seconds then bail
|
||||
haveRecentUPnP := c.sawUPnPRecently()
|
||||
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentUPnP {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
// Otherwise try a uPnP mapping if PMP did not work
|
||||
mpnp := &upnpMapping{
|
||||
gw: m.gw,
|
||||
internal: m.internal,
|
||||
}
|
||||
|
||||
var client upnpClient
|
||||
c.mu.Lock()
|
||||
oldMapping, ok := c.mapping.(*upnpMapping)
|
||||
c.mu.Unlock()
|
||||
if ok {
|
||||
client = oldMapping.client
|
||||
} else if c.Prober != nil && c.Prober.upnpClient != nil {
|
||||
client = c.Prober.upnpClient
|
||||
} else {
|
||||
client, err = getUPnPClient(ctx)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
}
|
||||
if client == nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
|
||||
var newPort uint16
|
||||
newPort, err = AddAnyPortMapping(
|
||||
ctx, client,
|
||||
"", prevPort, "UDP", localPort, m.internal.IP().String(), true,
|
||||
"tailscale-portfwd", pmpMapLifetimeSec,
|
||||
)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
mpnp.external = netaddr.IPPortFrom(gw, newPort)
|
||||
d := time.Duration(pmpMapLifetimeSec) * time.Second / 2
|
||||
mpnp.useUntil = time.Now().Add(d)
|
||||
mpnp.client = client
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.mapping = mpnp
|
||||
c.localPort = newPort
|
||||
return mpnp.external, nil
|
||||
}
|
||||
|
||||
type pmpResultCode uint16
|
||||
@ -376,12 +437,6 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
pmpCodeUnsupportedOpcode pmpResultCode = 5
|
||||
)
|
||||
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
func buildPMPRequestMappingPacket(localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
|
||||
pkt = make([]byte, 12)
|
||||
|
||||
@ -438,6 +493,12 @@ func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
|
||||
return res, true
|
||||
}
|
||||
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
const (
|
||||
pcpVersion = 2
|
||||
pcpPort = 5351
|
||||
|
@ -12,131 +12,6 @@
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
/*
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
*/
|
||||
|
||||
// Probe returns a summary of which port mapping services are
|
||||
// available on the network.
|
||||
//
|
||||
// If a probe has run recently and there haven't been any network changes since,
|
||||
// the returned result might be served from the Client's cache, without
|
||||
// sending any network traffic.
|
||||
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return res, ErrGatewayNotFound
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
c.mu.Lock()
|
||||
c.lastProbe = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
c.logf("ProbePCP: %v", err)
|
||||
return res, err
|
||||
}
|
||||
defer uc.Close()
|
||||
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
wg := sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
} else {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
// TODO(jknodt) this is expensive, maybe it's worth caching it and just reusing it
|
||||
// more aggressively
|
||||
hasUPnP, _ := probeUPnP(ctx)
|
||||
if hasUPnP {
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
c.uPnPSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else {
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else {
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
pcpHeard := false // true when we get any PCP response
|
||||
for {
|
||||
if pcpHeard && res.PMP {
|
||||
// Nothing more to discover.
|
||||
return res, nil
|
||||
}
|
||||
n, _, err := uc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
err = nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
c.mu.Lock()
|
||||
c.pcpSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
res.PCP = true
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
res.PCP = false
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
c.logf("unexpected PCP probe response: %+v", pres)
|
||||
}
|
||||
if pres, ok := parsePMPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
|
||||
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
res.PMP = true
|
||||
c.mu.Lock()
|
||||
c.pmpPubIP = pres.PublicAddr
|
||||
c.pmpPubIPTime = time.Now()
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Prober struct {
|
||||
// pause signals the probe to either pause temporarily (true), or stop entirely (false)
|
||||
// to restart the probe, send another pause to it.
|
||||
@ -151,18 +26,22 @@ type Prober struct {
|
||||
|
||||
// NewProber creates a new prober for a given client.
|
||||
func (c *Client) NewProber(ctx context.Context) (p *Prober) {
|
||||
stop := make(chan bool)
|
||||
if c.Prober != nil {
|
||||
return c.Prober
|
||||
}
|
||||
pause := make(chan bool)
|
||||
p = &Prober{
|
||||
stop: stop,
|
||||
pause: pause,
|
||||
|
||||
PMP: NewProbeSubResult(),
|
||||
PCP: NewProbeSubResult(),
|
||||
UPnP: NewProbeSubResult(),
|
||||
}
|
||||
c.Prober = p
|
||||
|
||||
go func() {
|
||||
for {
|
||||
pmp_ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
|
||||
pmp_ctx, cancel := context.WithTimeout(ctx, portMapServiceTimeout)
|
||||
hasPCP, hasPMP, err := c.probePMPAndPCP(pmp_ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
@ -209,23 +88,23 @@ func (c *Client) NewProber(ctx context.Context) (p *Prober) {
|
||||
defer func() {
|
||||
// unset client when no longer using it.
|
||||
p.upnpClient = nil
|
||||
upnpClient.RequestTermination()
|
||||
upnpClient.RequestTermination(context.Background())
|
||||
}()
|
||||
// TODO maybe do something fancy/dynamic with more delay (exponential back-off)
|
||||
for {
|
||||
upnp_ctx, cancel := context.WithTimeout(ctx, 6*time.Second)
|
||||
upnp_ctx, cancel := context.WithTimeout(ctx, portMapServiceTimeout*5)
|
||||
retries := 0
|
||||
hasUPnP := false
|
||||
const num_connect_retries = 5
|
||||
for retries < num_connect_retries {
|
||||
status, _, _, statusErr := p.upnpClient.GetStatusInfo()
|
||||
status, _, _, statusErr := p.upnpClient.GetStatusInfo(upnp_ctx)
|
||||
if statusErr != nil {
|
||||
err = statusErr
|
||||
break
|
||||
}
|
||||
hasUPnP = hasUPnP || status == "Connected"
|
||||
if status == "Disconnected" {
|
||||
upnpClient.RequestConnection()
|
||||
upnpClient.RequestConnection(upnp_ctx)
|
||||
}
|
||||
retries += 1
|
||||
}
|
||||
@ -413,9 +292,9 @@ func (c *Client) probePMPAndPCP(ctx context.Context) (pcp bool, pmp bool, err er
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
c.mu.Lock()
|
||||
c.pcpSawTime = time.Now()
|
||||
c.mu.Unlock()
|
||||
//c.mu.Lock()
|
||||
//c.pcpSawTime = time.Now()
|
||||
//c.mu.Unlock()
|
||||
switch pres.ResultCode {
|
||||
case pcpCodeOK:
|
||||
c.logf("Got PCP response: epoch: %v", pres.Epoch)
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/upnp/dcps/internetgateway2"
|
||||
"tailscale.com/tempfork/upnp/dcps/internetgateway2"
|
||||
)
|
||||
|
||||
type upnpMapping struct {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,312 +0,0 @@
|
||||
package ssdp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/upnp/httpu"
|
||||
)
|
||||
|
||||
const (
|
||||
maxExpiryTimeSeconds = 24 * 60 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
maxAgeRx = regexp.MustCompile("max-age= *([0-9]+)")
|
||||
)
|
||||
|
||||
const (
|
||||
EventAlive = EventType(iota)
|
||||
EventUpdate
|
||||
EventByeBye
|
||||
)
|
||||
|
||||
type EventType int8
|
||||
|
||||
func (et EventType) String() string {
|
||||
switch et {
|
||||
case EventAlive:
|
||||
return "EventAlive"
|
||||
case EventUpdate:
|
||||
return "EventUpdate"
|
||||
case EventByeBye:
|
||||
return "EventByeBye"
|
||||
default:
|
||||
return fmt.Sprintf("EventUnknown(%d)", int8(et))
|
||||
}
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
// The USN of the service.
|
||||
USN string
|
||||
// What happened.
|
||||
EventType EventType
|
||||
// The entry, which is nil if the service was not known and
|
||||
// EventType==EventByeBye. The contents of this must not be modified as it is
|
||||
// shared with the registry and other listeners. Once created, the Registry
|
||||
// does not modify the Entry value - any updates are replaced with a new
|
||||
// Entry value.
|
||||
Entry *Entry
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
// The address that the entry data was actually received from.
|
||||
RemoteAddr string
|
||||
// Unique Service Name. Identifies a unique instance of a device or service.
|
||||
USN string
|
||||
// Notfication Type. The type of device or service being announced.
|
||||
NT string
|
||||
// Server's self-identifying string.
|
||||
Server string
|
||||
Host string
|
||||
// Location of the UPnP root device description.
|
||||
Location url.URL
|
||||
|
||||
// Despite BOOTID,CONFIGID being required fields, apparently they are not
|
||||
// always set by devices. Set to -1 if not present.
|
||||
|
||||
BootID int32
|
||||
ConfigID int32
|
||||
|
||||
SearchPort uint16
|
||||
|
||||
// When the last update was received for this entry identified by this USN.
|
||||
LastUpdate time.Time
|
||||
// When the last update's cached values are advised to expire.
|
||||
CacheExpiry time.Time
|
||||
}
|
||||
|
||||
func newEntryFromRequest(r *http.Request) (*Entry, error) {
|
||||
now := time.Now()
|
||||
expiryDuration, err := parseCacheControlMaxAge(r.Header.Get("CACHE-CONTROL"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssdp: error parsing CACHE-CONTROL max age: %v", err)
|
||||
}
|
||||
|
||||
loc, err := url.Parse(r.Header.Get("LOCATION"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssdp: error parsing entry Location URL: %v", err)
|
||||
}
|
||||
|
||||
bootID, err := parseUpnpIntHeader(r.Header, "BOOTID.UPNP.ORG", -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configID, err := parseUpnpIntHeader(r.Header, "CONFIGID.UPNP.ORG", -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
searchPort, err := parseUpnpIntHeader(r.Header, "SEARCHPORT.UPNP.ORG", ssdpSearchPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if searchPort < 1 || searchPort > 65535 {
|
||||
return nil, fmt.Errorf("ssdp: search port %d is out of range", searchPort)
|
||||
}
|
||||
|
||||
return &Entry{
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
USN: r.Header.Get("USN"),
|
||||
NT: r.Header.Get("NT"),
|
||||
Server: r.Header.Get("SERVER"),
|
||||
Host: r.Header.Get("HOST"),
|
||||
Location: *loc,
|
||||
BootID: bootID,
|
||||
ConfigID: configID,
|
||||
SearchPort: uint16(searchPort),
|
||||
LastUpdate: now,
|
||||
CacheExpiry: now.Add(expiryDuration),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseCacheControlMaxAge(cc string) (time.Duration, error) {
|
||||
matches := maxAgeRx.FindStringSubmatch(cc)
|
||||
if len(matches) != 2 {
|
||||
return 0, fmt.Errorf("did not find exactly one max-age in cache control header: %q", cc)
|
||||
}
|
||||
expirySeconds, err := strconv.ParseInt(matches[1], 10, 16)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if expirySeconds < 1 || expirySeconds > maxExpiryTimeSeconds {
|
||||
return 0, fmt.Errorf("rejecting bad expiry time of %d seconds", expirySeconds)
|
||||
}
|
||||
return time.Duration(expirySeconds) * time.Second, nil
|
||||
}
|
||||
|
||||
// parseUpnpIntHeader is intended to parse the
|
||||
// {BOOT,CONFIGID,SEARCHPORT}.UPNP.ORG header fields. It returns the def if
|
||||
// the head is empty or missing.
|
||||
func parseUpnpIntHeader(headers http.Header, headerName string, def int32) (int32, error) {
|
||||
s := headers.Get(headerName)
|
||||
if s == "" {
|
||||
return def, nil
|
||||
}
|
||||
v, err := strconv.ParseInt(s, 10, 32)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ssdp: could not parse header %s: %v", headerName, err)
|
||||
}
|
||||
return int32(v), nil
|
||||
}
|
||||
|
||||
var _ httpu.Handler = new(Registry)
|
||||
|
||||
// Registry maintains knowledge of discovered devices and services.
|
||||
//
|
||||
// NOTE: the interface for this is experimental and may change, or go away
|
||||
// entirely.
|
||||
type Registry struct {
|
||||
lock sync.Mutex
|
||||
byUSN map[string]*Entry
|
||||
|
||||
listenersLock sync.RWMutex
|
||||
listeners map[chan<- Update]struct{}
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
byUSN: make(map[string]*Entry),
|
||||
listeners: make(map[chan<- Update]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// NewServerAndRegistry is a convenience function to create a registry, and an
|
||||
// httpu server to pass it messages. Call ListenAndServe on the server for
|
||||
// messages to be processed.
|
||||
func NewServerAndRegistry() (*httpu.Server, *Registry) {
|
||||
reg := NewRegistry()
|
||||
srv := &httpu.Server{
|
||||
Addr: ssdpUDP4Addr,
|
||||
Multicast: true,
|
||||
Handler: reg,
|
||||
}
|
||||
return srv, reg
|
||||
}
|
||||
|
||||
func (reg *Registry) AddListener(c chan<- Update) {
|
||||
reg.listenersLock.Lock()
|
||||
defer reg.listenersLock.Unlock()
|
||||
reg.listeners[c] = struct{}{}
|
||||
}
|
||||
|
||||
func (reg *Registry) RemoveListener(c chan<- Update) {
|
||||
reg.listenersLock.Lock()
|
||||
defer reg.listenersLock.Unlock()
|
||||
delete(reg.listeners, c)
|
||||
}
|
||||
|
||||
func (reg *Registry) sendUpdate(u Update) {
|
||||
reg.listenersLock.RLock()
|
||||
defer reg.listenersLock.RUnlock()
|
||||
for c := range reg.listeners {
|
||||
c <- u
|
||||
}
|
||||
}
|
||||
|
||||
// GetService returns known service (or device) entries for the given service
|
||||
// URN.
|
||||
func (reg *Registry) GetService(serviceURN string) []*Entry {
|
||||
// Currently assumes that the map is small, so we do a linear search rather
|
||||
// than indexed to avoid maintaining two maps.
|
||||
var results []*Entry
|
||||
reg.lock.Lock()
|
||||
defer reg.lock.Unlock()
|
||||
for _, entry := range reg.byUSN {
|
||||
if entry.NT == serviceURN {
|
||||
results = append(results, entry)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// ServeMessage implements httpu.Handler, and uses SSDP NOTIFY requests to
|
||||
// maintain the registry of devices and services.
|
||||
func (reg *Registry) ServeMessage(r *http.Request) {
|
||||
if r.Method != methodNotify {
|
||||
return
|
||||
}
|
||||
|
||||
nts := r.Header.Get("nts")
|
||||
|
||||
var err error
|
||||
switch nts {
|
||||
case ntsAlive:
|
||||
err = reg.handleNTSAlive(r)
|
||||
case ntsUpdate:
|
||||
err = reg.handleNTSUpdate(r)
|
||||
case ntsByebye:
|
||||
err = reg.handleNTSByebye(r)
|
||||
default:
|
||||
err = fmt.Errorf("unknown NTS value: %q", nts)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("goupnp/ssdp: failed to handle %s message from %s: %v", nts, r.RemoteAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (reg *Registry) handleNTSAlive(r *http.Request) error {
|
||||
entry, err := newEntryFromRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reg.lock.Lock()
|
||||
reg.byUSN[entry.USN] = entry
|
||||
reg.lock.Unlock()
|
||||
|
||||
reg.sendUpdate(Update{
|
||||
USN: entry.USN,
|
||||
EventType: EventAlive,
|
||||
Entry: entry,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (reg *Registry) handleNTSUpdate(r *http.Request) error {
|
||||
entry, err := newEntryFromRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextBootID, err := parseUpnpIntHeader(r.Header, "NEXTBOOTID.UPNP.ORG", -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry.BootID = nextBootID
|
||||
|
||||
reg.lock.Lock()
|
||||
reg.byUSN[entry.USN] = entry
|
||||
reg.lock.Unlock()
|
||||
|
||||
reg.sendUpdate(Update{
|
||||
USN: entry.USN,
|
||||
EventType: EventUpdate,
|
||||
Entry: entry,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (reg *Registry) handleNTSByebye(r *http.Request) error {
|
||||
usn := r.Header.Get("USN")
|
||||
|
||||
reg.lock.Lock()
|
||||
entry := reg.byUSN[usn]
|
||||
delete(reg.byUSN, usn)
|
||||
reg.lock.Unlock()
|
||||
|
||||
reg.sendUpdate(Update{
|
||||
USN: usn,
|
||||
EventType: EventByeBye,
|
||||
Entry: entry,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
@ -21,8 +21,8 @@
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/upnp"
|
||||
"tailscale.com/net/upnp/soap"
|
||||
"tailscale.com/tempfork/upnp"
|
||||
"tailscale.com/tempfork/upnp/soap"
|
||||
)
|
||||
|
||||
// Hack to avoid Go complaining if time isn't used.
|
@ -8,8 +8,8 @@
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/upnp"
|
||||
"tailscale.com/net/upnp/scpd"
|
||||
"tailscale.com/tempfork/upnp"
|
||||
"tailscale.com/tempfork/upnp/scpd"
|
||||
)
|
||||
|
||||
// DCP collects together information about a UPnP Device Control Protocol.
|
@ -11,13 +11,6 @@ type DCPMetadata struct {
|
||||
}
|
||||
|
||||
var dcpMetadata = []DCPMetadata{
|
||||
{
|
||||
Name: "internetgateway1",
|
||||
OfficialName: "Internet Gateway Device v1",
|
||||
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
|
||||
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
|
||||
Hacks: []DCPHackFn{totalBytesHack},
|
||||
},
|
||||
{
|
||||
Name: "internetgateway2",
|
||||
OfficialName: "Internet Gateway Device v2",
|
||||
@ -36,14 +29,35 @@ func(dcp *DCP) error {
|
||||
dcp.ServiceTypes[missingURN] = urnParts
|
||||
return nil
|
||||
}, totalBytesHack,
|
||||
func(dcp *DCP) error {
|
||||
// omit certain device types that we do not need
|
||||
var allowedServices = map[string]bool{
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:1": true,
|
||||
"urn:schemas-upnp-org:service:WANIPConnection:2": true,
|
||||
"urn:schemas-upnp-org:service:WANPPPConnection:1": true,
|
||||
}
|
||||
var allowedParts = map[string]bool{
|
||||
"WANIPConnection": true,
|
||||
"WANPPPConnection": true,
|
||||
}
|
||||
for service := range dcp.ServiceTypes {
|
||||
if _, ok := allowedServices[service]; ok {
|
||||
continue
|
||||
}
|
||||
delete(dcp.ServiceTypes, service)
|
||||
}
|
||||
var permitted []SCPDWithURN
|
||||
for _, v := range dcp.Services {
|
||||
if _, ok := allowedParts[v.URNParts.Name]; ok {
|
||||
permitted = append(permitted, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
dcp.Services = permitted
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "av1",
|
||||
OfficialName: "MediaServer v1 and MediaRenderer v1",
|
||||
DocURL: "http://upnp.org/specs/av/av1/",
|
||||
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
|
||||
},
|
||||
}
|
||||
|
||||
func totalBytesHack(dcp *DCP) error {
|
2351
tempfork/upnp/dcps/internetgateway2/internetgateway2.go
Normal file
2351
tempfork/upnp/dcps/internetgateway2/internetgateway2.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,8 +8,8 @@
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/net/upnp/scpd"
|
||||
"tailscale.com/net/upnp/soap"
|
||||
"tailscale.com/tempfork/upnp/scpd"
|
||||
"tailscale.com/tempfork/upnp/soap"
|
||||
)
|
||||
|
||||
const (
|
@ -21,7 +21,7 @@
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/upnp/ssdp"
|
||||
"tailscale.com/tempfork/upnp/ssdp"
|
||||
)
|
||||
|
||||
// ContextError is an error that wraps an error with some context information.
|
@ -4,7 +4,7 @@
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"tailscale.com/net/upnp/httpu"
|
||||
"tailscale.com/tempfork/upnp/httpu"
|
||||
)
|
||||
|
||||
// httpuClient creates a HTTPU client that multiplexes to all multicast-capable
|
@ -4,7 +4,7 @@
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"tailscale.com/net/upnp/soap"
|
||||
"tailscale.com/tempfork/upnp/soap"
|
||||
)
|
||||
|
||||
// ServiceClient is a SOAP client, root device and the service for the SOAP
|
Loading…
Reference in New Issue
Block a user