2023-01-27 21:37:20 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2021-08-03 05:09:50 +00:00
|
|
|
|
|
|
|
package portmapper
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-09-11 16:15:02 +00:00
|
|
|
"encoding/xml"
|
2021-08-03 05:09:50 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2022-08-02 20:38:11 +00:00
|
|
|
"net/netip"
|
2021-08-03 05:09:50 +00:00
|
|
|
"reflect"
|
|
|
|
"regexp"
|
2023-12-16 04:28:32 +00:00
|
|
|
"slices"
|
2023-09-11 16:15:02 +00:00
|
|
|
"sync/atomic"
|
2021-08-03 05:09:50 +00:00
|
|
|
"testing"
|
|
|
|
|
2021-09-08 02:27:19 +00:00
|
|
|
"tailscale.com/tstest"
|
2021-08-03 05:09:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Google Wifi
|
|
|
|
const (
|
|
|
|
googleWifiUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:2\r\nUSN: uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece::urn:schemas-upnp-org:device:InternetGatewayDevice:2\r\nEXT:\r\nSERVER: Linux/5.4.0-1034-gcp UPnP/1.1 MiniUPnPd/1.9\r\nLOCATION: http://192.168.86.1:5000/rootDesc.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1\r\nBOOTID.UPNP.ORG: 1\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n"
|
|
|
|
|
|
|
|
googleWifiRootDescXML = `<?xml version="1.0"?>
|
|
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0"><specVersion><major>1</major><minor>0</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:2</deviceType><friendlyName>OnHub</friendlyName><manufacturer>Google</manufacturer><manufacturerURL>http://google.com/</manufacturerURL><modelDescription>Wireless Router</modelDescription><modelName>OnHub</modelName><modelNumber>1</modelNumber><modelURL>https://on.google.com/hub/</modelURL><serialNumber>00000000</serialNumber><UDN>uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece</UDN><serviceList><service><serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType><serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId><controlURL>/ctl/L3F</controlURL><eventSubURL>/evt/L3F</eventSubURL><SCPDURL>/L3F.xml</SCPDURL></service><service><serviceType>urn:schemas-upnp-org:service:DeviceProtection:1</serviceType><serviceId>urn:upnp-org:serviceId:DeviceProtection1</serviceId><controlURL>/ctl/DP</controlURL><eventSubURL>/evt/DP</eventSubURL><SCPDURL>/DP.xml</SCPDURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANDevice:2</deviceType><friendlyName>WANDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>WAN Device</modelDescription><modelName>WAN Device</modelName><modelNumber>20210414</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>00000000</serialNumber><UDN>uuid:a9708184-a6c0-413a-bbac-11bcf7e30ecf</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType><serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId><controlURL>/ctl/CmnIfCfg</controlURL><eventSubURL>/evt/CmnIfCfg</eventSubURL><SCPDURL>/WANCfg.xml</SCPDURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:2</deviceType><friendlyName>WANConnectionDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>MiniUPnP daemon</modelDescription><modelName>MiniUPnPd</modelName><modelNumber>20210414</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>00000000</serialNumber><UDN>uuid:a9708184-a6c0-413a-bbac-11bcf7e30ec0</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANIPConnection:2</serviceType><serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId><controlURL>/ctl/IPConn</controlURL><eventSubURL>/evt/IPConn</eventSubURL><SCPDURL>/WANIPCn.xml</SCPDURL></service></serviceList></device></deviceList></device></deviceList><presentationURL>http://testwifi.here/</presentationURL></device></root>`
|
|
|
|
|
2022-11-25 15:11:17 +00:00
|
|
|
// pfSense 2.5.0-RELEASE / FreeBSD 12.2-STABLE
|
2021-08-03 05:09:50 +00:00
|
|
|
pfSenseUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=120\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nEXT:\r\nSERVER: FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1\r\nLOCATION: http://192.168.1.1:2189/rootDesc.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: 1627958564\r\nBOOTID.UPNP.ORG: 1627958564\r\nCONFIGID.UPNP.ORG: 1337\r\n\r\n"
|
|
|
|
|
|
|
|
pfSenseRootDescXML = `<?xml version="1.0"?>
|
|
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="1337"><specVersion><major>1</major><minor>1</minor></specVersion><device><deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType><friendlyName>FreeBSD router</friendlyName><manufacturer>FreeBSD</manufacturer><manufacturerURL>http://www.freebsd.org/</manufacturerURL><modelDescription>FreeBSD router</modelDescription><modelName>FreeBSD router</modelName><modelNumber>2.5.0-RELEASE</modelNumber><modelURL>http://www.freebsd.org/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac11</UDN><serviceList><service><serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType><serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId><SCPDURL>/L3F.xml</SCPDURL><controlURL>/ctl/L3F</controlURL><eventSubURL>/evt/L3F</eventSubURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType><friendlyName>WANDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>WAN Device</modelDescription><modelName>WAN Device</modelName><modelNumber>20210205</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac12</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType><serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId><SCPDURL>/WANCfg.xml</SCPDURL><controlURL>/ctl/CmnIfCfg</controlURL><eventSubURL>/evt/CmnIfCfg</eventSubURL></service></serviceList><deviceList><device><deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType><friendlyName>WANConnectionDevice</friendlyName><manufacturer>MiniUPnP</manufacturer><manufacturerURL>http://miniupnp.free.fr/</manufacturerURL><modelDescription>MiniUPnP daemon</modelDescription><modelName>MiniUPnPd</modelName><modelNumber>20210205</modelNumber><modelURL>http://miniupnp.free.fr/</modelURL><serialNumber>BEE7052B</serialNumber><UDN>uuid:bee7052b-49e8-3597-b545-55a1e38ac13</UDN><UPC>000000000000</UPC><serviceList><service><serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType><serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId><SCPDURL>/WANIPCn.xml</SCPDURL><controlURL>/ctl/IPConn</controlURL><eventSubURL>/evt/IPConn</eventSubURL></service></serviceList></device></deviceList></device></deviceList><presentationURL>https://192.168.1.1/</presentationURL></device></root>`
|
|
|
|
|
2022-11-25 15:11:17 +00:00
|
|
|
// Sagemcom FAST3890V3, https://github.com/tailscale/tailscale/issues/3557
|
2022-06-11 19:01:04 +00:00
|
|
|
sagemcomUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=1800\r\nDATE: Tue, 14 Dec 2021 07:51:29 GMT\r\nEXT:\r\nLOCATION: http://192.168.0.1:49153/69692b70/gatedesc0b.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: cabd6488-1dd1-11b2-9e52-a7461e1f098e\r\nSERVER: \r\nUser-Agent: redsonic\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:75802409-bccb-40e7-8e6c-fa095ecce13e::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n"
|
2022-11-25 15:11:17 +00:00
|
|
|
|
|
|
|
// Huawei, https://github.com/tailscale/tailscale/issues/6320
|
|
|
|
huaweiUPnPDisco = "HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=1800\r\nDATE: Fri, 25 Nov 2022 07:04:37 GMT\r\nEXT:\r\nLOCATION: http://192.168.1.1:49652/49652gatedesc.xml\r\nOPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n01-NLS: ce8dd8b0-732d-11be-a4a1-a2b26c8915fb\r\nSERVER: Linux/4.4.240, UPnP/1.0, Portable SDK for UPnP devices/1.12.1\r\nX-User-Agent: UPnP/1.0 DLNADOC/1.50\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\nUSN: uuid:00e0fc37-2525-2828-2500-0C31DCD93368::urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n"
|
2023-12-03 16:20:58 +00:00
|
|
|
|
|
|
|
// Mikrotik CHR v7.10, https://github.com/tailscale/tailscale/issues/8364
|
|
|
|
mikrotikRootDescXML = `<?xml version="1.0"?>
|
|
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
|
|
|
<specVersion>
|
|
|
|
<major>1</major>
|
|
|
|
<minor>0</minor>
|
|
|
|
</specVersion>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
|
|
|
|
<friendlyName>MikroTik Router</friendlyName>
|
|
|
|
<manufacturer>MikroTik</manufacturer>
|
|
|
|
<manufacturerURL>https://www.mikrotik.com/</manufacturerURL>
|
|
|
|
<modelName>Router OS</modelName>
|
|
|
|
<UDN>uuid:UUID-MIKROTIK-INTERNET-GATEWAY-DEVICE-</UDN>
|
|
|
|
<iconList>
|
|
|
|
<icon>
|
|
|
|
<mimetype>image/gif</mimetype>
|
|
|
|
<width>16</width>
|
|
|
|
<height>16</height>
|
|
|
|
<depth>8</depth>
|
|
|
|
<url>/logo16.gif</url>
|
|
|
|
</icon>
|
|
|
|
<icon>
|
|
|
|
<mimetype>image/gif</mimetype>
|
|
|
|
<width>32</width>
|
|
|
|
<height>32</height>
|
|
|
|
<depth>8</depth>
|
|
|
|
<url>/logo32.gif</url>
|
|
|
|
</icon>
|
|
|
|
<icon>
|
|
|
|
<mimetype>image/gif</mimetype>
|
|
|
|
<width>48</width>
|
|
|
|
<height>48</height>
|
|
|
|
<depth>8</depth>
|
|
|
|
<url>/logo48.gif</url>
|
|
|
|
</icon>
|
|
|
|
</iconList>
|
|
|
|
<serviceList>
|
|
|
|
<service>
|
|
|
|
<serviceType>urn:schemas-microsoft-com:service:OSInfo:1</serviceType>
|
|
|
|
<serviceId>urn:microsoft-com:serviceId:OSInfo1</serviceId>
|
|
|
|
<SCPDURL>/osinfo.xml</SCPDURL>
|
|
|
|
<controlURL>/upnp/control/oqjsxqshhz/osinfo</controlURL>
|
|
|
|
<eventSubURL>/upnp/event/cwzcyndrjf/osinfo</eventSubURL>
|
|
|
|
</service>
|
|
|
|
</serviceList>
|
|
|
|
<deviceList>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
|
|
|
<friendlyName>WAN Device</friendlyName>
|
|
|
|
<manufacturer>MikroTik</manufacturer>
|
|
|
|
<manufacturerURL>https://www.mikrotik.com/</manufacturerURL>
|
|
|
|
<modelName>Router OS</modelName>
|
|
|
|
<UDN>uuid:UUID-MIKROTIK-WAN-DEVICE--1</UDN>
|
|
|
|
<serviceList>
|
|
|
|
<service>
|
|
|
|
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
|
|
|
|
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
|
|
|
|
<SCPDURL>/wancommonifc-1.xml</SCPDURL>
|
|
|
|
<controlURL>/upnp/control/ivvmxhunyq/wancommonifc-1</controlURL>
|
|
|
|
<eventSubURL>/upnp/event/mkjzdqvryf/wancommonifc-1</eventSubURL>
|
|
|
|
</service>
|
|
|
|
</serviceList>
|
|
|
|
<deviceList>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
|
|
|
<friendlyName>WAN Connection Device</friendlyName>
|
|
|
|
<manufacturer>MikroTik</manufacturer>
|
|
|
|
<manufacturerURL>https://www.mikrotik.com/</manufacturerURL>
|
|
|
|
<modelName>Router OS</modelName>
|
|
|
|
<UDN>uuid:UUID-MIKROTIK-WAN-CONNECTION-DEVICE--1</UDN>
|
|
|
|
<serviceList>
|
|
|
|
<service>
|
|
|
|
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
|
|
|
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
|
|
|
<SCPDURL>/wanipconn-1.xml</SCPDURL>
|
|
|
|
<controlURL>/upnp/control/yomkmsnooi/wanipconn-1</controlURL>
|
|
|
|
<eventSubURL>/upnp/event/veeabhzzva/wanipconn-1</eventSubURL>
|
|
|
|
</service>
|
|
|
|
</serviceList>
|
|
|
|
</device>
|
|
|
|
</deviceList>
|
|
|
|
</device>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
|
|
|
<friendlyName>WAN Device</friendlyName>
|
|
|
|
<manufacturer>MikroTik</manufacturer>
|
|
|
|
<manufacturerURL>https://www.mikrotik.com/</manufacturerURL>
|
|
|
|
<modelName>Router OS</modelName>
|
|
|
|
<UDN>uuid:UUID-MIKROTIK-WAN-DEVICE--7</UDN>
|
|
|
|
<serviceList>
|
|
|
|
<service>
|
|
|
|
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
|
|
|
|
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
|
|
|
|
<SCPDURL>/wancommonifc-7.xml</SCPDURL>
|
|
|
|
<controlURL>/upnp/control/vzcyyzzttz/wancommonifc-7</controlURL>
|
|
|
|
<eventSubURL>/upnp/event/womwbqtbkq/wancommonifc-7</eventSubURL>
|
|
|
|
</service>
|
|
|
|
</serviceList>
|
|
|
|
<deviceList>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
|
|
|
<friendlyName>WAN Connection Device</friendlyName>
|
|
|
|
<manufacturer>MikroTik</manufacturer>
|
|
|
|
<manufacturerURL>https://www.mikrotik.com/</manufacturerURL>
|
|
|
|
<modelName>Router OS</modelName>
|
|
|
|
<UDN>uuid:UUID-MIKROTIK-WAN-CONNECTION-DEVICE--7</UDN>
|
|
|
|
<serviceList>
|
|
|
|
<service>
|
|
|
|
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
|
|
|
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
|
|
|
<SCPDURL>/wanipconn-7.xml</SCPDURL>
|
|
|
|
<controlURL>/upnp/control/xstnsgeuyh/wanipconn-7</controlURL>
|
|
|
|
<eventSubURL>/upnp/event/rscixkusbs/wanipconn-7</eventSubURL>
|
|
|
|
</service>
|
|
|
|
</serviceList>
|
|
|
|
</device>
|
|
|
|
</deviceList>
|
|
|
|
</device>
|
|
|
|
</deviceList>
|
|
|
|
<disabledForTestPresentationURL>http://10.0.0.1/</disabledForTestPresentationURL>
|
|
|
|
<presentationURL>http://127.0.0.1/</presentationURL>
|
|
|
|
</device>
|
|
|
|
<disabledForTestURLBase>http://10.0.0.1:2828</disabledForTestURLBase>
|
|
|
|
</root>
|
|
|
|
`
|
2022-06-11 19:01:04 +00:00
|
|
|
)
|
|
|
|
|
2021-08-03 05:09:50 +00:00
|
|
|
func TestParseUPnPDiscoResponse(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
headers string
|
|
|
|
want uPnPDiscoResponse
|
|
|
|
}{
|
|
|
|
{"google", googleWifiUPnPDisco, uPnPDiscoResponse{
|
|
|
|
Location: "http://192.168.86.1:5000/rootDesc.xml",
|
2021-08-10 21:50:33 +00:00
|
|
|
Server: "Linux/5.4.0-1034-gcp UPnP/1.1 MiniUPnPd/1.9",
|
|
|
|
USN: "uuid:a9708184-a6c0-413a-bbac-11bcf7e30ece::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
2021-08-03 05:09:50 +00:00
|
|
|
}},
|
|
|
|
{"pfsense", pfSenseUPnPDisco, uPnPDiscoResponse{
|
|
|
|
Location: "http://192.168.1.1:2189/rootDesc.xml",
|
2021-08-10 21:50:33 +00:00
|
|
|
Server: "FreeBSD/12.2-STABLE UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
2021-08-03 05:09:50 +00:00
|
|
|
}},
|
2022-06-11 19:01:04 +00:00
|
|
|
{"sagemcom", sagemcomUPnPDisco, uPnPDiscoResponse{
|
|
|
|
Location: "http://192.168.0.1:49153/69692b70/gatedesc0b.xml",
|
|
|
|
Server: "",
|
|
|
|
USN: "uuid:75802409-bccb-40e7-8e6c-fa095ecce13e::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
}},
|
2022-11-25 15:11:17 +00:00
|
|
|
{"huawei", huaweiUPnPDisco, uPnPDiscoResponse{
|
|
|
|
Location: "http://192.168.1.1:49652/49652gatedesc.xml",
|
|
|
|
Server: "Linux/4.4.240, UPnP/1.0, Portable SDK for UPnP devices/1.12.1",
|
|
|
|
USN: "uuid:00e0fc37-2525-2828-2500-0C31DCD93368::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
}},
|
2021-08-03 05:09:50 +00:00
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
got, err := parseUPnPDiscoResponse([]byte(tt.headers))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
|
|
t.Errorf("unexpected result:\n got: %+v\nwant: %+v\n", got, tt.want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGetUPnPClient(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
xmlBody string
|
|
|
|
want string
|
|
|
|
wantLog string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
"google",
|
|
|
|
googleWifiRootDescXML,
|
|
|
|
"*internetgateway2.WANIPConnection2",
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
"saw UPnP type WANIPConnection2 at http://127.0.0.1:NNN/rootDesc.xml; OnHub (Google), method=single\n",
|
2021-08-03 05:09:50 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
"pfsense",
|
|
|
|
pfSenseRootDescXML,
|
|
|
|
"*internetgateway2.WANIPConnection1",
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
"saw UPnP type WANIPConnection1 at http://127.0.0.1:NNN/rootDesc.xml; FreeBSD router (FreeBSD), method=single\n",
|
2021-08-03 05:09:50 +00:00
|
|
|
},
|
2023-12-03 16:20:58 +00:00
|
|
|
{
|
|
|
|
"mikrotik",
|
|
|
|
mikrotikRootDescXML,
|
|
|
|
"*internetgateway2.WANIPConnection1",
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
"saw UPnP type WANIPConnection1 at http://127.0.0.1:NNN/rootDesc.xml; MikroTik Router (MikroTik), method=none\n",
|
2023-12-03 16:20:58 +00:00
|
|
|
},
|
2021-08-03 05:09:50 +00:00
|
|
|
// TODO(bradfitz): find a PPP one in the wild
|
|
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.RequestURI == "/rootDesc.xml" {
|
|
|
|
io.WriteString(w, tt.xmlBody)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
http.NotFound(w, r)
|
|
|
|
}))
|
|
|
|
defer ts.Close()
|
2022-08-02 20:38:11 +00:00
|
|
|
gw, _ := netip.AddrFromSlice(ts.Listener.Addr().(*net.TCPAddr).IP)
|
|
|
|
gw = gw.Unmap()
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
2021-09-08 02:27:19 +00:00
|
|
|
var logBuf tstest.MemLogger
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
dev, loc, err := getUPnPRootDevice(ctx, logBuf.Logf, DebugKnobs{}, gw, uPnPDiscoResponse{
|
2021-08-03 05:09:50 +00:00
|
|
|
Location: ts.URL + "/rootDesc.xml",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
c, err := selectBestService(ctx, logBuf.Logf, dev, loc)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2021-08-03 05:09:50 +00:00
|
|
|
got := fmt.Sprintf("%T", c)
|
|
|
|
if got != tt.want {
|
|
|
|
t.Errorf("got %v; want %v", got, tt.want)
|
|
|
|
}
|
|
|
|
gotLog := regexp.MustCompile(`127\.0\.0\.1:\d+`).ReplaceAllString(logBuf.String(), "127.0.0.1:NNN")
|
|
|
|
if gotLog != tt.wantLog {
|
|
|
|
t.Errorf("logged %q; want %q", gotLog, tt.wantLog)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-09-11 16:15:02 +00:00
|
|
|
|
|
|
|
func TestGetUPnPPortMapping(t *testing.T) {
|
|
|
|
igd, err := NewTestIGD(t.Logf, TestIGDOptions{UPnP: true})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer igd.Close()
|
|
|
|
|
|
|
|
// This is a very basic fake UPnP server handler.
|
|
|
|
var sawRequestWithLease atomic.Bool
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
handlers := map[string]any{
|
|
|
|
"AddPortMapping": func(body []byte) (int, string) {
|
|
|
|
// Decode a minimal body to determine whether we skip the request or not.
|
|
|
|
var req struct {
|
|
|
|
Protocol string `xml:"NewProtocol"`
|
|
|
|
InternalPort string `xml:"NewInternalPort"`
|
|
|
|
ExternalPort string `xml:"NewExternalPort"`
|
|
|
|
InternalClient string `xml:"NewInternalClient"`
|
|
|
|
LeaseDuration string `xml:"NewLeaseDuration"`
|
2023-09-11 16:15:02 +00:00
|
|
|
}
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
if err := xml.Unmarshal(body, &req); err != nil {
|
2023-09-11 16:15:02 +00:00
|
|
|
t.Errorf("bad request: %v", err)
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
return http.StatusBadRequest, "bad request"
|
2023-09-11 16:15:02 +00:00
|
|
|
}
|
|
|
|
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
if req.Protocol != "UDP" {
|
|
|
|
t.Errorf(`got Protocol=%q, want "UDP"`, req.Protocol)
|
2023-09-11 16:15:02 +00:00
|
|
|
}
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
if req.LeaseDuration != "0" {
|
|
|
|
// Return a fake error to ensure that we fall back to a permanent lease.
|
|
|
|
sawRequestWithLease.Store(true)
|
|
|
|
return http.StatusOK, testAddPortMappingPermanentLease
|
|
|
|
}
|
|
|
|
|
|
|
|
// Success!
|
|
|
|
return http.StatusOK, testAddPortMappingResponse
|
|
|
|
},
|
|
|
|
"GetExternalIPAddress": testGetExternalIPAddressResponse,
|
|
|
|
"GetStatusInfo": testGetStatusInfoResponse,
|
|
|
|
"DeletePortMapping": "", // Do nothing for test
|
|
|
|
}
|
2023-09-11 16:15:02 +00:00
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
2023-12-03 16:20:58 +00:00
|
|
|
rootDescsToTest := []string{testRootDesc, mikrotikRootDescXML}
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
for _, rootDesc := range rootDescsToTest {
|
|
|
|
igd.SetUPnPHandler(&upnpServer{
|
|
|
|
t: t,
|
|
|
|
Desc: rootDesc,
|
|
|
|
Control: map[string]map[string]any{
|
|
|
|
"/ctl/IPConn": handlers,
|
|
|
|
"/upnp/control/yomkmsnooi/wanipconn-1": handlers,
|
|
|
|
},
|
|
|
|
})
|
2023-09-11 16:15:02 +00:00
|
|
|
|
2023-12-03 16:20:58 +00:00
|
|
|
c := newTestClient(t, igd)
|
|
|
|
t.Logf("Listening on upnp=%v", c.testUPnPPort)
|
|
|
|
defer c.Close()
|
|
|
|
|
|
|
|
c.debug.VerboseLogs = true
|
|
|
|
|
2023-12-16 04:28:32 +00:00
|
|
|
// Try twice to test the "cache previous mapping" logic.
|
|
|
|
var (
|
|
|
|
firstResponse netip.AddrPort
|
|
|
|
prevPort uint16
|
|
|
|
)
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
sawRequestWithLease.Store(false)
|
|
|
|
res, err := c.Probe(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Probe: %v", err)
|
|
|
|
}
|
|
|
|
if !res.UPnP {
|
|
|
|
t.Errorf("didn't detect UPnP")
|
|
|
|
}
|
2023-12-03 16:20:58 +00:00
|
|
|
|
2023-12-16 04:28:32 +00:00
|
|
|
gw, myIP, ok := c.gatewayAndSelfIP()
|
|
|
|
if !ok {
|
|
|
|
t.Fatalf("could not get gateway and self IP")
|
|
|
|
}
|
|
|
|
t.Logf("gw=%v myIP=%v", gw, myIP)
|
2023-12-03 16:20:58 +00:00
|
|
|
|
2023-12-16 04:28:32 +00:00
|
|
|
ext, ok := c.getUPnPPortMapping(ctx, gw, netip.AddrPortFrom(myIP, 12345), prevPort)
|
|
|
|
if !ok {
|
|
|
|
t.Fatal("could not get UPnP port mapping")
|
|
|
|
}
|
|
|
|
if got, want := ext.Addr(), netip.MustParseAddr("123.123.123.123"); got != want {
|
|
|
|
t.Errorf("bad external address; got %v want %v", got, want)
|
|
|
|
}
|
|
|
|
if !sawRequestWithLease.Load() {
|
|
|
|
t.Errorf("wanted request with lease, but didn't see one")
|
|
|
|
}
|
|
|
|
if i == 0 {
|
|
|
|
firstResponse = ext
|
|
|
|
prevPort = ext.Port()
|
|
|
|
} else if firstResponse != ext {
|
|
|
|
t.Errorf("got different response on second attempt: (got) %v != %v (want)", ext, firstResponse)
|
|
|
|
}
|
|
|
|
t.Logf("external IP: %v", ext)
|
2023-12-03 16:20:58 +00:00
|
|
|
}
|
2023-12-16 04:28:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGetUPnPPortMappingNoResponses(t *testing.T) {
|
|
|
|
igd, err := NewTestIGD(t.Logf, TestIGDOptions{UPnP: true})
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer igd.Close()
|
|
|
|
|
|
|
|
c := newTestClient(t, igd)
|
|
|
|
t.Logf("Listening on upnp=%v", c.testUPnPPort)
|
|
|
|
defer c.Close()
|
|
|
|
|
|
|
|
c.debug.VerboseLogs = true
|
|
|
|
|
|
|
|
// Do this before setting uPnPMetas since it invalidates those mappings
|
|
|
|
// if gw/myIP change.
|
|
|
|
gw, myIP, _ := c.gatewayAndSelfIP()
|
|
|
|
|
|
|
|
t.Run("ErrorContactingUPnP", func(t *testing.T) {
|
|
|
|
c.mu.Lock()
|
|
|
|
c.uPnPMetas = []uPnPDiscoResponse{{
|
|
|
|
Location: "http://127.0.0.1:1/does-not-exist.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
|
|
}}
|
|
|
|
c.mu.Unlock()
|
|
|
|
|
|
|
|
_, ok := c.getUPnPPortMapping(context.Background(), gw, netip.AddrPortFrom(myIP, 12345), 0)
|
|
|
|
if ok {
|
|
|
|
t.Errorf("expected no mapping when there are no responses")
|
2023-12-03 16:20:58 +00:00
|
|
|
}
|
2023-12-16 04:28:32 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestProcessUPnPResponses(t *testing.T) {
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
responses []uPnPDiscoResponse
|
|
|
|
want []uPnPDiscoResponse
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "single",
|
|
|
|
responses: []uPnPDiscoResponse{{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
}},
|
|
|
|
want: []uPnPDiscoResponse{{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "multiple_with_same_location",
|
|
|
|
responses: []uPnPDiscoResponse{
|
|
|
|
{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
want: []uPnPDiscoResponse{{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "multiple_with_different_location",
|
|
|
|
responses: []uPnPDiscoResponse{
|
|
|
|
{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Location: "http://192.168.100.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
want: []uPnPDiscoResponse{
|
|
|
|
// note: this sorts first because we prefer "InternetGatewayDevice:2"
|
|
|
|
{
|
|
|
|
Location: "http://192.168.100.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:2",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Location: "http://192.168.1.1:2828/control.xml",
|
|
|
|
Server: "Tailscale-Test/1.0 UPnP/1.1 MiniUPnPd/2.2.1",
|
|
|
|
USN: "uuid:bee7052b-49e8-3597-b545-55a1e38ac11::urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
got := processUPnPResponses(slices.Clone(tt.responses))
|
|
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
|
|
t.Errorf("unexpected result:\n got: %+v\nwant: %+v\n", got, tt.want)
|
|
|
|
}
|
|
|
|
})
|
2023-09-11 16:15:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
type upnpServer struct {
|
|
|
|
t *testing.T
|
|
|
|
Desc string // root device XML
|
|
|
|
Control map[string]map[string]any // map["/url"]map["UPnPService"]response
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *upnpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
u.t.Logf("got UPnP request %s %s", r.Method, r.URL.Path)
|
|
|
|
if r.URL.Path == "/rootDesc.xml" {
|
|
|
|
io.WriteString(w, u.Desc)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if control, ok := u.Control[r.URL.Path]; ok {
|
|
|
|
u.handleControl(w, r, control)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
u.t.Logf("ignoring request")
|
|
|
|
http.NotFound(w, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *upnpServer) handleControl(w http.ResponseWriter, r *http.Request, handlers map[string]any) {
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
u.t.Errorf("error reading request body: %v", err)
|
|
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Decode the request type.
|
|
|
|
var outerRequest struct {
|
|
|
|
Body struct {
|
|
|
|
Request struct {
|
|
|
|
XMLName xml.Name
|
|
|
|
} `xml:",any"`
|
|
|
|
Inner string `xml:",innerxml"`
|
|
|
|
} `xml:"Body"`
|
|
|
|
}
|
|
|
|
if err := xml.Unmarshal(body, &outerRequest); err != nil {
|
|
|
|
u.t.Errorf("bad request: %v", err)
|
|
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
requestType := outerRequest.Body.Request.XMLName.Local
|
|
|
|
upnpRequest := outerRequest.Body.Inner
|
|
|
|
u.t.Logf("UPnP request: %s", requestType)
|
|
|
|
|
|
|
|
handler, ok := handlers[requestType]
|
|
|
|
if !ok {
|
|
|
|
u.t.Errorf("unhandled UPnP request type %q", requestType)
|
|
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch v := handler.(type) {
|
|
|
|
case string:
|
|
|
|
io.WriteString(w, v)
|
|
|
|
case []byte:
|
|
|
|
w.Write(v)
|
|
|
|
|
|
|
|
// Function handlers
|
|
|
|
case func(string) string:
|
|
|
|
io.WriteString(w, v(upnpRequest))
|
|
|
|
case func([]byte) string:
|
|
|
|
io.WriteString(w, v([]byte(upnpRequest)))
|
|
|
|
|
|
|
|
case func(string) (int, string):
|
|
|
|
code, body := v(upnpRequest)
|
|
|
|
w.WriteHeader(code)
|
|
|
|
io.WriteString(w, body)
|
|
|
|
case func([]byte) (int, string):
|
|
|
|
code, body := v([]byte(upnpRequest))
|
|
|
|
w.WriteHeader(code)
|
|
|
|
io.WriteString(w, body)
|
|
|
|
|
|
|
|
default:
|
|
|
|
u.t.Fatalf("invalid handler type: %T", v)
|
|
|
|
http.Error(w, "invalid handler type", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-11 16:15:02 +00:00
|
|
|
const testRootDesc = `<?xml version="1.0"?>
|
|
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0" configId="1337">
|
|
|
|
<specVersion>
|
|
|
|
<major>1</major>
|
|
|
|
<minor>1</minor>
|
|
|
|
</specVersion>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
|
|
|
|
<friendlyName>Tailscale Test Router</friendlyName>
|
|
|
|
<manufacturer>Tailscale</manufacturer>
|
|
|
|
<manufacturerURL>https://tailscale.com</manufacturerURL>
|
|
|
|
<modelDescription>Tailscale Test Router</modelDescription>
|
|
|
|
<modelName>Tailscale Test Router</modelName>
|
|
|
|
<modelNumber>2.5.0-RELEASE</modelNumber>
|
|
|
|
<modelURL>https://tailscale.com</modelURL>
|
|
|
|
<serialNumber>1234</serialNumber>
|
|
|
|
<UDN>uuid:1974e83b-6dc7-4635-92b3-6a85a4037294</UDN>
|
|
|
|
<deviceList>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
|
|
|
<friendlyName>WANDevice</friendlyName>
|
|
|
|
<manufacturer>MiniUPnP</manufacturer>
|
|
|
|
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
|
|
|
|
<modelDescription>WAN Device</modelDescription>
|
|
|
|
<modelName>WAN Device</modelName>
|
|
|
|
<modelNumber>20990102</modelNumber>
|
|
|
|
<modelURL>http://miniupnp.free.fr/</modelURL>
|
|
|
|
<serialNumber>1234</serialNumber>
|
|
|
|
<UDN>uuid:1974e83b-6dc7-4635-92b3-6a85a4037294</UDN>
|
|
|
|
<UPC>000000000000</UPC>
|
|
|
|
<deviceList>
|
|
|
|
<device>
|
|
|
|
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
|
|
|
<friendlyName>WANConnectionDevice</friendlyName>
|
|
|
|
<manufacturer>MiniUPnP</manufacturer>
|
|
|
|
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
|
|
|
|
<modelDescription>MiniUPnP daemon</modelDescription>
|
|
|
|
<modelName>MiniUPnPd</modelName>
|
|
|
|
<modelNumber>20210205</modelNumber>
|
|
|
|
<modelURL>http://miniupnp.free.fr/</modelURL>
|
|
|
|
<serialNumber>1234</serialNumber>
|
|
|
|
<UDN>uuid:1974e83b-6dc7-4635-92b3-6a85a4037294</UDN>
|
|
|
|
<UPC>000000000000</UPC>
|
|
|
|
<serviceList>
|
|
|
|
<service>
|
|
|
|
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
|
|
|
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
|
|
|
<SCPDURL>/WANIPCn.xml</SCPDURL>
|
|
|
|
<controlURL>/ctl/IPConn</controlURL>
|
|
|
|
<eventSubURL>/evt/IPConn</eventSubURL>
|
|
|
|
</service>
|
|
|
|
</serviceList>
|
|
|
|
</device>
|
|
|
|
</deviceList>
|
|
|
|
</device>
|
|
|
|
</deviceList>
|
|
|
|
<presentationURL>https://127.0.0.1/</presentationURL>
|
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
`
|
|
|
|
|
|
|
|
const testAddPortMappingPermanentLease = `<?xml version="1.0"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<s:Fault>
|
|
|
|
<faultCode>s:Client</faultCode>
|
|
|
|
<faultString>UPnPError</faultString>
|
|
|
|
<detail>
|
|
|
|
<UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
|
|
|
|
<errorCode>725</errorCode>
|
|
|
|
<errorDescription>OnlyPermanentLeasesSupported</errorDescription>
|
|
|
|
</UPnPError>
|
|
|
|
</detail>
|
|
|
|
</s:Fault>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>
|
|
|
|
`
|
|
|
|
|
|
|
|
const testAddPortMappingResponse = `<?xml version="1.0"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:AddPortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"/>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>
|
|
|
|
`
|
|
|
|
|
|
|
|
const testGetExternalIPAddressResponse = `<?xml version="1.0"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:GetExternalIPAddressResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
|
|
|
<NewExternalIPAddress>123.123.123.123</NewExternalIPAddress>
|
|
|
|
</u:GetExternalIPAddressResponse>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>
|
|
|
|
`
|
net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.
Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.
Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.
RELNOTE=Improve UPnP portmapping when multiple UPnP services exist
Updates #8364
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-06 16:55:49 +00:00
|
|
|
|
|
|
|
const testGetStatusInfoResponse = `<?xml version="1.0"?>
|
|
|
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
|
|
<s:Body>
|
|
|
|
<u:GetStatusInfoResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
|
|
|
<NewConnectionStatus>Connected</NewConnectionStatus>
|
|
|
|
<NewLastConnectionError>ERROR_NONE</NewLastConnectionError>
|
|
|
|
<NewUptime>9999</NewUptime>
|
|
|
|
</u:GetStatusInfoResponse>
|
|
|
|
</s:Body>
|
|
|
|
</s:Envelope>
|
|
|
|
`
|