julianknodt c6b92ddda8 Copy goupnp client into our repo
goupnp is an existing upnp client for go, which provides all the functionality we need, licensed
under BSD-2-Clause, so we can copy it over and modify parts of it for our case.
Specifically, we add contexts to all the methods so we can better handle timeouts, remove the
dependency on large charsets, and (eventually) trim out extra components we don't need.

Signed-off-by: julianknodt <julianknodt@gmail.com>
2021-06-14 13:40:02 -07:00

131 lines
3.4 KiB
Go

// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portmapper
import (
"context"
"time"
"golang.org/x/sync/errgroup"
"inet.af/netaddr"
"tailscale.com/net/upnp/dcps/internetgateway2"
)
type upnpMapping struct {
gw netaddr.IP
external netaddr.IPPort
internal netaddr.IPPort
useUntil time.Time
client upnpClient
}
func (u *upnpMapping) isCurrent() bool { return u.useUntil.After(time.Now()) }
func (u *upnpMapping) validUntil() time.Time { return u.useUntil }
func (u *upnpMapping) externalIPPort() netaddr.IPPort { return u.external }
func (u *upnpMapping) release() {
u.client.DeletePortMapping(context.Background(), "", u.external.Port(), "udp")
}
type upnpClient interface {
// http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
// Implicitly assume that the calls for all these are uniform, which might be a dangerous
// assumption.
AddPortMapping(
ctx context.Context,
newRemoteHost string,
newExternalPort uint16,
newProtocol string,
newInternalPort uint16,
newInternalClient string,
newEnabled bool,
newPortMappingDescription string,
newLeaseDuration uint32,
) (err error)
DeletePortMapping(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) error
GetStatusInfo(ctx context.Context) (status string, lastErr string, uptime uint32, err error)
RequestTermination(ctx context.Context) error
RequestConnection(ctx context.Context) error
}
func AddAnyPortMapping(
ctx context.Context,
upnp upnpClient,
newRemoteHost string,
newExternalPort uint16,
newProtocol string,
newInternalPort uint16,
newInternalClient string,
newEnabled bool,
newPortMappingDescription string,
newLeaseDuration uint32,
) (newPort uint16, err error) {
if upnp, ok := upnp.(*internetgateway2.WANIPConnection2); ok {
return upnp.AddAnyPortMapping(
ctx,
newRemoteHost,
newExternalPort,
newProtocol,
newInternalPort,
newInternalClient,
newEnabled,
newPortMappingDescription,
newLeaseDuration,
)
}
err = upnp.AddPortMapping(
ctx,
newRemoteHost,
newExternalPort,
newProtocol,
newInternalPort,
newInternalClient,
newEnabled,
newPortMappingDescription,
newLeaseDuration,
)
return newInternalPort, err
}
// getUPnPClients gets a client for interfacing with UPnP, ignoring the underlying protocol for
// now.
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
func getUPnPClient(ctx context.Context) (upnpClient, error) {
tasks, _ := errgroup.WithContext(ctx)
var ip1Clients []*internetgateway2.WANIPConnection1
tasks.Go(func() error {
var err error
ip1Clients, _, err = internetgateway2.NewWANIPConnection1Clients()
return err
})
var ip2Clients []*internetgateway2.WANIPConnection2
tasks.Go(func() error {
var err error
ip2Clients, _, err = internetgateway2.NewWANIPConnection2Clients()
return err
})
var ppp1Clients []*internetgateway2.WANPPPConnection1
tasks.Go(func() error {
var err error
ppp1Clients, _, err = internetgateway2.NewWANPPPConnection1Clients()
return err
})
err := tasks.Wait()
switch {
case len(ip2Clients) > 0:
return ip2Clients[0], nil
case len(ip1Clients) > 0:
return ip1Clients[0], nil
case len(ppp1Clients) > 0:
return ppp1Clients[0], nil
default:
// Didn't get any outputs, report if there was an error or nil if
// just no clients.
return nil, err
}
}