mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
679 lines
29 KiB
Go
679 lines
29 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package portmapper
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
// 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>`
|
|
|
|
// pfSense 2.5.0-RELEASE / FreeBSD 12.2-STABLE
|
|
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>`
|
|
|
|
// Sagemcom FAST3890V3, https://github.com/tailscale/tailscale/issues/3557
|
|
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"
|
|
|
|
// 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"
|
|
|
|
// 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>
|
|
`
|
|
)
|
|
|
|
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",
|
|
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",
|
|
}},
|
|
{"pfsense", pfSenseUPnPDisco, uPnPDiscoResponse{
|
|
Location: "http://192.168.1.1:2189/rootDesc.xml",
|
|
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",
|
|
}},
|
|
{"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",
|
|
}},
|
|
{"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",
|
|
}},
|
|
}
|
|
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",
|
|
"saw UPnP type WANIPConnection2 at http://127.0.0.1:NNN/rootDesc.xml; OnHub (Google), method=single\n",
|
|
},
|
|
{
|
|
"pfsense",
|
|
pfSenseRootDescXML,
|
|
"*internetgateway2.WANIPConnection1",
|
|
"saw UPnP type WANIPConnection1 at http://127.0.0.1:NNN/rootDesc.xml; FreeBSD router (FreeBSD), method=single\n",
|
|
},
|
|
{
|
|
"mikrotik",
|
|
mikrotikRootDescXML,
|
|
"*internetgateway2.WANIPConnection1",
|
|
"saw UPnP type WANIPConnection1 at http://127.0.0.1:NNN/rootDesc.xml; MikroTik Router (MikroTik), method=none\n",
|
|
},
|
|
// 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()
|
|
gw, _ := netip.AddrFromSlice(ts.Listener.Addr().(*net.TCPAddr).IP)
|
|
gw = gw.Unmap()
|
|
|
|
ctx := context.Background()
|
|
|
|
var logBuf tstest.MemLogger
|
|
dev, loc, err := getUPnPRootDevice(ctx, logBuf.Logf, DebugKnobs{}, gw, uPnPDiscoResponse{
|
|
Location: ts.URL + "/rootDesc.xml",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := selectBestService(ctx, logBuf.Logf, dev, loc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
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"`
|
|
}
|
|
if err := xml.Unmarshal(body, &req); err != nil {
|
|
t.Errorf("bad request: %v", err)
|
|
return http.StatusBadRequest, "bad request"
|
|
}
|
|
|
|
if req.Protocol != "UDP" {
|
|
t.Errorf(`got Protocol=%q, want "UDP"`, req.Protocol)
|
|
}
|
|
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
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
rootDescsToTest := []string{testRootDesc, mikrotikRootDescXML}
|
|
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,
|
|
},
|
|
})
|
|
|
|
c := newTestClient(t, igd)
|
|
t.Logf("Listening on upnp=%v", c.testUPnPPort)
|
|
defer c.Close()
|
|
|
|
c.debug.VerboseLogs = true
|
|
|
|
// 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")
|
|
}
|
|
|
|
gw, myIP, ok := c.gatewayAndSelfIP()
|
|
if !ok {
|
|
t.Fatalf("could not get gateway and self IP")
|
|
}
|
|
t.Logf("gw=%v myIP=%v", gw, myIP)
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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>
|
|
`
|
|
|
|
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>
|
|
`
|