net/portmapper: use the upstream goupnp library instead of our fork

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Iabf9fc4e158f45e4fc385f2b4c2e55ba01351c5c
This commit is contained in:
Andrew Dunham
2023-08-22 10:38:41 -04:00
parent 57129205e6
commit 5ab17bd3dd
7 changed files with 61 additions and 71 deletions

View File

@@ -23,9 +23,9 @@ import (
"sync/atomic"
"time"
"github.com/tailscale/goupnp"
"github.com/tailscale/goupnp/dcps/internetgateway2"
"github.com/tailscale/goupnp/soap"
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway2"
"github.com/huin/goupnp/soap"
"tailscale.com/envknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
@@ -62,48 +62,27 @@ func (u *upnpMapping) GoodUntil() time.Time { return u.goodUntil }
func (u *upnpMapping) RenewAfter() time.Time { return u.renewAfter }
func (u *upnpMapping) External() netip.AddrPort { return u.external }
func (u *upnpMapping) Release(ctx context.Context) {
u.client.DeletePortMapping(ctx, "", u.external.Port(), upnpProtocolUDP)
u.client.DeletePortMappingCtx(ctx, "", u.external.Port(), upnpProtocolUDP)
}
// upnpClient is an interface over the multiple different clients exported by goupnp,
// exposing the functions we need for portmapping. Those clients are auto-generated from XML-specs,
// which is why they're not very idiomatic.
type upnpClient interface {
AddPortMapping(
AddPortMappingCtx(
ctx context.Context,
// remoteHost is the remote device sending packets to this device, in the format of x.x.x.x.
// The empty string, "", means any host out on the internet can send packets in.
remoteHost string,
// externalPort is the exposed port of this port mapping. Visible during NAT operations.
// 0 will let the router select the port, but there is an additional call,
// `AddAnyPortMapping`, which is available on 1 of the 3 possible protocols,
// which should be used if available. See `addAnyPortMapping` below, which calls this if
// `AddAnyPortMapping` is not supported.
externalPort uint16,
// protocol is whether this is over TCP or UDP. Either "TCP" or "UDP".
protocol string,
// internalPort is the port that the gateway device forwards the traffic to.
internalPort uint16,
// internalClient is the IP address that packets will be forwarded to for this mapping.
// Internal client is of the form "x.x.x.x".
internalClient string,
// enabled is whether this portmapping should be enabled or disabled.
enabled bool,
// portMappingDescription is a user-readable description of this portmapping.
portMappingDescription string,
// leaseDurationSec is the duration of this portmapping. The value of this argument must be
// greater than 0. From the spec, it appears if it is set to 0, it will switch to using
// 604800 seconds, but not sure why this is desired. The recommended time is 3600 seconds.
leaseDurationSec uint32,
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) error
DeletePortMapping(ctx context.Context, remoteHost string, externalPort uint16, protocol string) error
GetExternalIPAddress(ctx context.Context) (externalIPAddress string, err error)
DeletePortMappingCtx(ctx context.Context, remoteHost string, externalPort uint16, protocol string) error
GetExternalIPAddressCtx(ctx context.Context) (externalIPAddress string, err error)
}
// tsPortMappingDesc gets sent to UPnP clients as a human-readable label for the portmapping.
@@ -153,7 +132,7 @@ func addAnyPortMapping(
// First off, try using AddAnyPortMapping; if there's a conflict, the
// router will pick another port and return it.
if upnp, ok := upnp.(*internetgateway2.WANIPConnection2); ok {
return upnp.AddAnyPortMapping(
return upnp.AddAnyPortMappingCtx(
ctx,
"",
externalPort,
@@ -168,7 +147,7 @@ func addAnyPortMapping(
// Fall back to using AddPortMapping, which requests a mapping to/from
// a specific external port.
err = upnp.AddPortMapping(
err = upnp.AddPortMappingCtx(
ctx,
"",
externalPort,
@@ -182,8 +161,8 @@ func addAnyPortMapping(
return externalPort, err
}
// getUPnPClient gets a client for interfacing with UPnP, ignoring the underlying protocol for
// now.
// getUPnPClient gets a client for interfacing with UPnP, ignoring the
// underlying protocol for now.
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
//
// The gw is the detected gateway.
@@ -191,9 +170,11 @@ func addAnyPortMapping(
// The meta is the most recently parsed UDP discovery packet response
// from the Internet Gateway Device.
//
// The provided ctx is not retained in the returned upnpClient, but
// its associated HTTP client is (if set via goupnp.WithHTTPClient).
func getUPnPClient(ctx context.Context, logf logger.Logf, debug DebugKnobs, gw netip.Addr, meta uPnPDiscoResponse) (client upnpClient, err error) {
// If set, the provided http.Client is copied and used as the HTTP client for
// SOAP requests.
//
// The provided ctx is not retained in the returned upnpClient.
func getUPnPClient(ctx context.Context, logf logger.Logf, httpc *http.Client, debug DebugKnobs, gw netip.Addr, meta uPnPDiscoResponse) (client upnpClient, err error) {
if debug.DisableUPnP {
return nil, nil
}
@@ -229,12 +210,16 @@ func getUPnPClient(ctx context.Context, logf logger.Logf, debug DebugKnobs, gw n
defer cancel()
// This part does a network fetch.
root, err := goupnp.DeviceByURL(ctx, u)
root, err := goupnp.DeviceByURLCtx(ctx, u)
if err != nil {
return nil, err
}
var soapClient *soap.SOAPClient
defer func() {
if soapClient != nil && httpc != nil {
soapClient.HTTPClient = *httpc
}
if client == nil {
return
}
@@ -245,13 +230,16 @@ func getUPnPClient(ctx context.Context, logf logger.Logf, debug DebugKnobs, gw n
// These parts don't do a network fetch.
// Pick the best service type available.
if cc, _ := internetgateway2.NewWANIPConnection2ClientsFromRootDevice(ctx, root, u); len(cc) > 0 {
if cc, _ := internetgateway2.NewWANIPConnection2ClientsFromRootDevice(root, u); len(cc) > 0 {
soapClient = cc[0].ServiceClient.SOAPClient
return cc[0], nil
}
if cc, _ := internetgateway2.NewWANIPConnection1ClientsFromRootDevice(ctx, root, u); len(cc) > 0 {
if cc, _ := internetgateway2.NewWANIPConnection1ClientsFromRootDevice(root, u); len(cc) > 0 {
soapClient = cc[0].ServiceClient.SOAPClient
return cc[0], nil
}
if cc, _ := internetgateway2.NewWANPPPConnection1ClientsFromRootDevice(ctx, root, u); len(cc) > 0 {
if cc, _ := internetgateway2.NewWANPPPConnection1ClientsFromRootDevice(root, u); len(cc) > 0 {
soapClient = cc[0].ServiceClient.SOAPClient
return cc[0], nil
}
return nil, nil
@@ -305,8 +293,7 @@ func (c *Client) getUPnPPortMapping(
if ok && oldMapping != nil {
client = oldMapping.client
} else {
ctx := goupnp.WithHTTPClient(ctx, httpClient)
client, err = getUPnPClient(ctx, c.logf, c.debug, gw, meta)
client, err = getUPnPClient(ctx, c.logf, httpClient, c.debug, gw, meta)
if c.debug.VerboseLogs {
c.logf("getUPnPClient: %T, %v", client, err)
}
@@ -363,7 +350,7 @@ func (c *Client) getUPnPPortMapping(
}
// TODO cache this ip somewhere?
extIP, err := client.GetExternalIPAddress(ctx)
extIP, err := client.GetExternalIPAddressCtx(ctx)
if c.debug.VerboseLogs {
c.logf("client.GetExternalIPAddress: %v, %v", extIP, err)
}

View File

@@ -114,7 +114,7 @@ func TestGetUPnPClient(t *testing.T) {
gw, _ := netip.AddrFromSlice(ts.Listener.Addr().(*net.TCPAddr).IP)
gw = gw.Unmap()
var logBuf tstest.MemLogger
c, err := getUPnPClient(context.Background(), logBuf.Logf, DebugKnobs{}, gw, uPnPDiscoResponse{
c, err := getUPnPClient(context.Background(), logBuf.Logf, nil, DebugKnobs{}, gw, uPnPDiscoResponse{
Location: ts.URL + "/rootDesc.xml",
})
if err != nil {