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:
julianknodt 2021-06-14 13:20:04 -07:00
parent c6b92ddda8
commit cb2d9c13fe
34 changed files with 2499 additions and 5754 deletions

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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.

View File

@ -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.

View File

@ -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 {

File diff suppressed because it is too large Load Diff

View File

@ -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 (

View File

@ -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.

View File

@ -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

View File

@ -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