mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	Starting at a minimal binary and adding one feature back...
    tailscaled tailscale combined (linux/amd64)
     30073135  17451704  31543692 omitting everything
    +  480302 +   10258 +  493896 .. add debugportmapper
    +  475317 +  151943 +  467660 .. add portmapper
    +  500086 +  162873 +  510511 .. add portmapper+debugportmapper
Fixes #17148
Change-Id: I90bd0e9d1bd8cbe64fa2e885e9afef8fb5ee74b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
	
			
		
			
				
	
	
		
			1221 lines
		
	
	
		
			49 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1221 lines
		
	
	
		
			49 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/net/portmapper/portmappertype"
 | |
| 	"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>
 | |
| `
 | |
| 
 | |
| 	// Huawei, https://github.com/tailscale/tailscale/issues/10911
 | |
| 	huaweiRootDescXML = `<?xml version="1.0"?>
 | |
| <root xmlns="urn:schemas-upnp-org:device-1-0">
 | |
|   <specVersion>
 | |
|     <major>1</major>
 | |
|     <minor>0</minor>
 | |
|   </specVersion>
 | |
|   <device>
 | |
|     <deviceType>urn:dslforum-org:device:InternetGatewayDevice:1</deviceType>
 | |
|     <friendlyName>HG531 V1</friendlyName>
 | |
|     <manufacturer>Huawei Technologies Co., Ltd.</manufacturer>
 | |
|     <manufacturerURL>http://www.huawei.com</manufacturerURL>
 | |
|     <modelDescription>Huawei Home Gateway</modelDescription>
 | |
|     <modelName>HG531 V1</modelName>
 | |
|     <modelNumber>Huawei Model</modelNumber>
 | |
|     <modelURL>http://www.huawei.com</modelURL>
 | |
|     <serialNumber>G6J8W15326003974</serialNumber>
 | |
|     <UDN>uuid:00e0fc37-2626-2828-2600-587f668bdd9a</UDN>
 | |
|     <UPC>000000000001</UPC>
 | |
|     <serviceList>
 | |
|       <service>
 | |
|         <serviceType>urn:www-huawei-com:service:DeviceConfig:1</serviceType>
 | |
|         <serviceId>urn:www-huawei-com:serviceId:DeviceConfig1</serviceId>
 | |
|         <SCPDURL>/desc/DevCfg.xml</SCPDURL>
 | |
|         <controlURL>/ctrlt/DeviceConfig_1</controlURL>
 | |
|         <eventSubURL>/evt/DeviceConfig_1</eventSubURL>
 | |
|       </service>
 | |
|       <service>
 | |
|         <serviceType>urn:dslforum-org:service:LANConfigSecurity:1</serviceType>
 | |
|         <serviceId>urn:dslforum-org:serviceId:LANConfigSecurity1</serviceId>
 | |
|         <SCPDURL>/desc/LANSec.xml</SCPDURL>
 | |
|         <controlURL>/ctrlt/LANConfigSecurity_1</controlURL>
 | |
|         <eventSubURL>/evt/LANConfigSecurity_1</eventSubURL>
 | |
|       </service>
 | |
|       <service>
 | |
|         <serviceType>urn:dslforum-org:service:Layer3Forwarding:1</serviceType>
 | |
|         <serviceId>urn:dslforum-org:serviceId:Layer3Forwarding1</serviceId>
 | |
|         <SCPDURL>/desc/L3Fwd.xml</SCPDURL>
 | |
|         <controlURL>/ctrlt/Layer3Forwarding_1</controlURL>
 | |
|         <eventSubURL>/evt/Layer3Forwarding_1</eventSubURL>
 | |
|       </service>
 | |
|     </serviceList>
 | |
|     <deviceList>
 | |
|       <device>
 | |
|         <deviceType>urn:dslforum-org:device:WANDevice:1</deviceType>
 | |
|         <friendlyName>WANDevice</friendlyName>
 | |
|         <manufacturer>Huawei Technologies Co., Ltd.</manufacturer>
 | |
|         <manufacturerURL>http://www.huawei.com</manufacturerURL>
 | |
|         <modelDescription>Huawei Home Gateway</modelDescription>
 | |
|         <modelName>HG531 V1</modelName>
 | |
|         <modelNumber>Huawei Model</modelNumber>
 | |
|         <modelURL>http://www.huawei.com</modelURL>
 | |
|         <serialNumber>G6J8W15326003974</serialNumber>
 | |
|         <UDN>uuid:00e0fc37-2626-2828-2601-587f668bdd9a</UDN>
 | |
|         <UPC>000000000001</UPC>
 | |
|         <serviceList>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:WANDSLInterfaceConfig:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:WANDSLInterfaceConfig1</serviceId>
 | |
|             <SCPDURL>/desc/WanDslIfCfg.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/WANDSLInterfaceConfig_1</controlURL>
 | |
|             <eventSubURL>/evt/WANDSLInterfaceConfig_1</eventSubURL>
 | |
|           </service>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:WANCommonInterfaceConfig:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:WANCommonInterfaceConfig1</serviceId>
 | |
|             <SCPDURL>/desc/WanCommonIfc1.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/WANCommonInterfaceConfig_1</controlURL>
 | |
|             <eventSubURL>/evt/WANCommonInterfaceConfig_1</eventSubURL>
 | |
|           </service>
 | |
|         </serviceList>
 | |
|         <deviceList>
 | |
|           <device>
 | |
|             <deviceType>urn:dslforum-org:device:WANConnectionDevice:1</deviceType>
 | |
|             <friendlyName>WANConnectionDevice</friendlyName>
 | |
|             <manufacturer>Huawei Technologies Co., Ltd.</manufacturer>
 | |
|             <manufacturerURL>http://www.huawei.com</manufacturerURL>
 | |
|             <modelDescription>Huawei Home Gateway</modelDescription>
 | |
|             <modelName>HG531 V1</modelName>
 | |
|             <modelNumber>Huawei Model</modelNumber>
 | |
|             <modelURL>http://www.huawei.com</modelURL>
 | |
|             <serialNumber>G6J8W15326003974</serialNumber>
 | |
|             <UDN>uuid:00e0fc37-2626-2828-2603-587f668bdd9a</UDN>
 | |
|             <UPC>000000000001</UPC>
 | |
|             <serviceList>
 | |
|               <service>
 | |
|                 <serviceType>urn:dslforum-org:service:WANPPPConnection:1</serviceType>
 | |
|                 <serviceId>urn:dslforum-org:serviceId:WANPPPConnection1</serviceId>
 | |
|                 <SCPDURL>/desc/WanPppConn.xml</SCPDURL>
 | |
|                 <controlURL>/ctrlt/WANPPPConnection_1</controlURL>
 | |
|                 <eventSubURL>/evt/WANPPPConnection_1</eventSubURL>
 | |
|               </service>
 | |
|               <service>
 | |
|                 <serviceType>urn:dslforum-org:service:WANEthernetConnectionManagement:1</serviceType>
 | |
|                 <serviceId>urn:dslforum-org:serviceId:WANEthernetConnectionManagement1</serviceId>
 | |
|                 <SCPDURL>/desc/WanEthConnMgt.xml</SCPDURL>
 | |
|                 <controlURL>/ctrlt/WANEthernetConnectionManagement_1</controlURL>
 | |
|                 <eventSubURL>/evt/WANEthernetConnectionManagement_1</eventSubURL>
 | |
|               </service>
 | |
|               <service>
 | |
|                 <serviceType>urn:dslforum-org:service:WANDSLLinkConfig:1</serviceType>
 | |
|                 <serviceId>urn:dslforum-org:serviceId:WANDSLLinkConfig1</serviceId>
 | |
|                 <SCPDURL>/desc/WanDslLink.xml</SCPDURL>
 | |
|                 <controlURL>/ctrlt/WANDSLLinkConfig_1</controlURL>
 | |
|                 <eventSubURL>/evt/WANDSLLinkConfig_1</eventSubURL>
 | |
|               </service>
 | |
|             </serviceList>
 | |
|           </device>
 | |
|         </deviceList>
 | |
|       </device>
 | |
|       <device>
 | |
|         <deviceType>urn:dslforum-org:device:LANDevice:1</deviceType>
 | |
|         <friendlyName>LANDevice</friendlyName>
 | |
|         <manufacturer>Huawei Technologies Co., Ltd.</manufacturer>
 | |
|         <manufacturerURL>http://www.huawei.com</manufacturerURL>
 | |
|         <modelDescription>Huawei Home Gateway</modelDescription>
 | |
|         <modelName>HG531 V1</modelName>
 | |
|         <modelNumber>Huawei Model</modelNumber>
 | |
|         <modelURL>http://www.huawei.com</modelURL>
 | |
|         <serialNumber>G6J8W15326003974</serialNumber>
 | |
|         <UDN>uuid:00e0fc37-2626-2828-2602-587f668bdd9a</UDN>
 | |
|         <UPC>000000000001</UPC>
 | |
|         <serviceList>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:WLANConfiguration:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:WLANConfiguration4</serviceId>
 | |
|             <SCPDURL>/desc/WLANCfg.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/WLANConfiguration_4</controlURL>
 | |
|             <eventSubURL>/evt/WLANConfiguration_4</eventSubURL>
 | |
|           </service>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:WLANConfiguration:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:WLANConfiguration3</serviceId>
 | |
|             <SCPDURL>/desc/WLANCfg.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/WLANConfiguration_3</controlURL>
 | |
|             <eventSubURL>/evt/WLANConfiguration_3</eventSubURL>
 | |
|           </service>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:WLANConfiguration:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:WLANConfiguration2</serviceId>
 | |
|             <SCPDURL>/desc/WLANCfg.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/WLANConfiguration_2</controlURL>
 | |
|             <eventSubURL>/evt/WLANConfiguration_2</eventSubURL>
 | |
|           </service>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:WLANConfiguration:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:WLANConfiguration1</serviceId>
 | |
|             <SCPDURL>/desc/WLANCfg.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/WLANConfiguration_1</controlURL>
 | |
|             <eventSubURL>/evt/WLANConfiguration_1</eventSubURL>
 | |
|           </service>
 | |
|           <service>
 | |
|             <serviceType>urn:dslforum-org:service:LANHostConfigManagement:1</serviceType>
 | |
|             <serviceId>urn:dslforum-org:serviceId:LANHostConfigManagement1</serviceId>
 | |
|             <SCPDURL>/desc/LanHostCfgMgmt.xml</SCPDURL>
 | |
|             <controlURL>/ctrlt/LANHostConfigManagement_1</controlURL>
 | |
|             <eventSubURL>/evt/LANHostConfigManagement_1</eventSubURL>
 | |
|           </service>
 | |
|         </serviceList>
 | |
|       </device>
 | |
|     </deviceList>
 | |
|     <presentationURL>http://127.0.0.1</presentationURL>
 | |
|   </device>
 | |
| </root>
 | |
| `
 | |
| 
 | |
| 	noSupportedServicesRootDesc = `<?xml version="1.0"?>
 | |
| <root xmlns="urn:schemas-upnp-org:device-1-0">
 | |
|   <specVersion>
 | |
|     <major>1</major>
 | |
|     <minor>0</minor>
 | |
|   </specVersion>
 | |
|   <device>
 | |
|     <deviceType>urn:dslforum-org:device:InternetGatewayDevice:1</deviceType>
 | |
|     <friendlyName>Fake Router</friendlyName>
 | |
|     <manufacturer>Tailscale, Inc</manufacturer>
 | |
|     <manufacturerURL>http://www.tailscale.com</manufacturerURL>
 | |
|     <modelDescription>Fake Router</modelDescription>
 | |
|     <modelName>Test Model</modelName>
 | |
|     <modelNumber>v1</modelNumber>
 | |
|     <modelURL>http://www.tailscale.com</modelURL>
 | |
|     <serialNumber>123456789</serialNumber>
 | |
|     <UDN>uuid:11111111-2222-3333-4444-555555555555</UDN>
 | |
|     <UPC>000000000001</UPC>
 | |
|     <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/aaaaaaaaaa/osinfo</controlURL>
 | |
|         <eventSubURL>/upnp/event/aaaaaaaaaa/osinfo</eventSubURL>
 | |
|       </service>
 | |
|     </serviceList>
 | |
|     <deviceList>
 | |
|       <device>
 | |
| 	<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
 | |
|         <friendlyName>WANDevice</friendlyName>
 | |
|         <manufacturer>Tailscale, Inc</manufacturer>
 | |
| 	<manufacturerURL>http://www.tailscale.com</manufacturerURL>
 | |
| 	<modelDescription>Tailscale Test Router</modelDescription>
 | |
| 	<modelName>Test Model</modelName>
 | |
| 	<modelNumber>v1</modelNumber>
 | |
| 	<modelURL>http://www.tailscale.com</modelURL>
 | |
| 	<serialNumber>123456789</serialNumber>
 | |
| 	<UDN>uuid:11111111-2222-3333-4444-555555555555</UDN>
 | |
|         <UPC>000000000001</UPC>
 | |
|         <serviceList>
 | |
|           <service>
 | |
|             <serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
 | |
|             <serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
 | |
|             <controlURL>/ctl/bbbbbbbb</controlURL>
 | |
|             <eventSubURL>/evt/bbbbbbbb</eventSubURL>
 | |
|             <SCPDURL>/WANCfg.xml</SCPDURL>
 | |
|           </service>
 | |
|         </serviceList>
 | |
|         <deviceList>
 | |
|           <device>
 | |
| 	    <deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
 | |
|             <friendlyName>WANConnectionDevice</friendlyName>
 | |
| 	    <manufacturer>Tailscale, Inc</manufacturer>
 | |
| 	    <manufacturerURL>http://www.tailscale.com</manufacturerURL>
 | |
| 	    <modelDescription>Tailscale Test Router</modelDescription>
 | |
| 	    <modelName>Test Model</modelName>
 | |
| 	    <modelNumber>v1</modelNumber>
 | |
| 	    <modelURL>http://www.tailscale.com</modelURL>
 | |
| 	    <serialNumber>123456789</serialNumber>
 | |
| 	    <UDN>uuid:11111111-2222-3333-4444-555555555555</UDN>
 | |
|             <UPC>000000000001</UPC>
 | |
|             <serviceList>
 | |
|               <service>
 | |
| 		<serviceType>urn:tailscale:service:SomethingElse:1</serviceType>
 | |
| 		<serviceId>urn:upnp-org:serviceId:TailscaleSomethingElse</serviceId>
 | |
|                 <SCPDURL>/desc/SomethingElse.xml</SCPDURL>
 | |
|                 <controlURL>/ctrlt/SomethingElse_1</controlURL>
 | |
|                 <eventSubURL>/evt/SomethingElse_1</eventSubURL>
 | |
|               </service>
 | |
|             </serviceList>
 | |
|           </device>
 | |
|         </deviceList>
 | |
|       </device>
 | |
|     </deviceList>
 | |
|     <presentationURL>http://127.0.0.1</presentationURL>
 | |
|   </device>
 | |
| </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",
 | |
| 		},
 | |
| 		{
 | |
| 			"huawei",
 | |
| 			huaweiRootDescXML,
 | |
| 			"*portmapper.legacyWANPPPConnection1",
 | |
| 			"saw UPnP type *portmapper.legacyWANPPPConnection1 at http://127.0.0.1:NNN/rootDesc.xml; HG531 V1 (Huawei Technologies Co., Ltd.), method=single\n",
 | |
| 		},
 | |
| 		{
 | |
| 			"not_supported",
 | |
| 			noSupportedServicesRootDesc,
 | |
| 			"<nil>",
 | |
| 			"",
 | |
| 		},
 | |
| 
 | |
| 		// 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, 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, nil)
 | |
| 		t.Logf("Listening on upnp=%v", c.testUPnPPort)
 | |
| 
 | |
| 		c.debug.VerboseLogs = true
 | |
| 
 | |
| 		// Try twice to test the "cache previous mapping" logic.
 | |
| 		var (
 | |
| 			firstResponse netip.AddrPort
 | |
| 			prevPort      uint16
 | |
| 		)
 | |
| 		for i := range 2 {
 | |
| 			sawRequestWithLease.Store(false)
 | |
| 			mustProbeUPnP(t, ctx, c)
 | |
| 
 | |
| 			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 TestGetUPnPPortMapping_LeaseDuration(t *testing.T) {
 | |
| 	testCases := []struct {
 | |
| 		name string
 | |
| 		resp string
 | |
| 	}{
 | |
| 		{"only_permanent_leases", testAddPortMappingPermanentLease},
 | |
| 		{"invalid_args", testAddPortMappingPermanentLease_InvalidArgs},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 
 | |
| 			// 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, tc.resp
 | |
| 					}
 | |
| 
 | |
| 					return http.StatusOK, testAddPortMappingResponse
 | |
| 				},
 | |
| 				"GetExternalIPAddress": testGetExternalIPAddressResponse,
 | |
| 				"GetStatusInfo":        testGetStatusInfoResponse,
 | |
| 				"DeletePortMapping":    "", // Do nothing for test
 | |
| 			}
 | |
| 
 | |
| 			igd, err := NewTestIGD(t, TestIGDOptions{UPnP: true})
 | |
| 			if err != nil {
 | |
| 				t.Fatal(err)
 | |
| 			}
 | |
| 			defer igd.Close()
 | |
| 
 | |
| 			igd.SetUPnPHandler(&upnpServer{
 | |
| 				t:    t,
 | |
| 				Desc: testRootDesc,
 | |
| 				Control: map[string]map[string]any{
 | |
| 					"/ctl/IPConn":                          handlers,
 | |
| 					"/upnp/control/yomkmsnooi/wanipconn-1": handlers,
 | |
| 				},
 | |
| 			})
 | |
| 
 | |
| 			ctx := context.Background()
 | |
| 			c := newTestClient(t, igd, nil)
 | |
| 			c.debug.VerboseLogs = true
 | |
| 			t.Logf("Listening on upnp=%v", c.testUPnPPort)
 | |
| 
 | |
| 			// Actually test the UPnP port mapping.
 | |
| 			mustProbeUPnP(t, ctx, c)
 | |
| 
 | |
| 			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), 0)
 | |
| 			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")
 | |
| 			}
 | |
| 			t.Logf("external IP: %v", ext)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // TestGetUPnPPortMapping_NoValidServices tests that getUPnPPortMapping doesn't
 | |
| // crash when a valid UPnP response with no supported services is discovered
 | |
| // and parsed.
 | |
| //
 | |
| // See https://github.com/tailscale/tailscale/issues/10911
 | |
| func TestGetUPnPPortMapping_NoValidServices(t *testing.T) {
 | |
| 	igd, err := NewTestIGD(t, TestIGDOptions{UPnP: true})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer igd.Close()
 | |
| 
 | |
| 	igd.SetUPnPHandler(&upnpServer{
 | |
| 		t:    t,
 | |
| 		Desc: noSupportedServicesRootDesc,
 | |
| 	})
 | |
| 
 | |
| 	c := newTestClient(t, igd, nil)
 | |
| 	c.debug.VerboseLogs = true
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	mustProbeUPnP(t, ctx, c)
 | |
| 
 | |
| 	gw, myIP, ok := c.gatewayAndSelfIP()
 | |
| 	if !ok {
 | |
| 		t.Fatalf("could not get gateway and self IP")
 | |
| 	}
 | |
| 
 | |
| 	// This shouldn't panic
 | |
| 	_, ok = c.getUPnPPortMapping(ctx, gw, netip.AddrPortFrom(myIP, 12345), 0)
 | |
| 	if ok {
 | |
| 		t.Fatal("did not expect to get UPnP port mapping")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Tests the legacy behaviour with the pre-UPnP standard portmapping service.
 | |
| func TestGetUPnPPortMapping_Legacy(t *testing.T) {
 | |
| 	igd, err := NewTestIGD(t, TestIGDOptions{UPnP: true})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer igd.Close()
 | |
| 
 | |
| 	// This is a very basic fake UPnP server handler.
 | |
| 	handlers := map[string]any{
 | |
| 		"AddPortMapping":       testLegacyAddPortMappingResponse,
 | |
| 		"GetExternalIPAddress": testLegacyGetExternalIPAddressResponse,
 | |
| 		"GetStatusInfo":        testLegacyGetStatusInfoResponse,
 | |
| 		"DeletePortMapping":    "", // Do nothing for test
 | |
| 	}
 | |
| 
 | |
| 	igd.SetUPnPHandler(&upnpServer{
 | |
| 		t:    t,
 | |
| 		Desc: huaweiRootDescXML,
 | |
| 		Control: map[string]map[string]any{
 | |
| 			"/ctrlt/WANPPPConnection_1": handlers,
 | |
| 		},
 | |
| 	})
 | |
| 
 | |
| 	c := newTestClient(t, igd, nil)
 | |
| 	c.debug.VerboseLogs = true
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	mustProbeUPnP(t, ctx, c)
 | |
| 
 | |
| 	gw, myIP, ok := c.gatewayAndSelfIP()
 | |
| 	if !ok {
 | |
| 		t.Fatalf("could not get gateway and self IP")
 | |
| 	}
 | |
| 
 | |
| 	ext, ok := c.getUPnPPortMapping(ctx, gw, netip.AddrPortFrom(myIP, 12345), 0)
 | |
| 	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)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestGetUPnPPortMappingNoResponses(t *testing.T) {
 | |
| 	igd, err := NewTestIGD(t, TestIGDOptions{UPnP: true})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer igd.Close()
 | |
| 
 | |
| 	c := newTestClient(t, igd, nil)
 | |
| 	t.Logf("Listening on upnp=%v", c.testUPnPPort)
 | |
| 
 | |
| 	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)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // See: https://github.com/tailscale/corp/issues/23538
 | |
| func TestGetUPnPPortMapping_Invalid(t *testing.T) {
 | |
| 	for _, responseAddr := range []string{
 | |
| 		"0.0.0.0",
 | |
| 		"127.0.0.1",
 | |
| 	} {
 | |
| 		t.Run(responseAddr, func(t *testing.T) {
 | |
| 			igd, err := NewTestIGD(t, TestIGDOptions{UPnP: true})
 | |
| 			if err != nil {
 | |
| 				t.Fatal(err)
 | |
| 			}
 | |
| 			defer igd.Close()
 | |
| 
 | |
| 			// This is a very basic fake UPnP server handler.
 | |
| 			handlers := map[string]any{
 | |
| 				"AddPortMapping":       testAddPortMappingResponse,
 | |
| 				"GetExternalIPAddress": makeGetExternalIPAddressResponse(responseAddr),
 | |
| 				"GetStatusInfo":        testGetStatusInfoResponse,
 | |
| 				"DeletePortMapping":    "", // Do nothing for test
 | |
| 			}
 | |
| 
 | |
| 			igd.SetUPnPHandler(&upnpServer{
 | |
| 				t:    t,
 | |
| 				Desc: huaweiRootDescXML,
 | |
| 				Control: map[string]map[string]any{
 | |
| 					"/ctrlt/WANPPPConnection_1": handlers,
 | |
| 				},
 | |
| 			})
 | |
| 
 | |
| 			c := newTestClient(t, igd, nil)
 | |
| 			c.debug.VerboseLogs = true
 | |
| 
 | |
| 			ctx := context.Background()
 | |
| 			mustProbeUPnP(t, ctx, c)
 | |
| 
 | |
| 			gw, myIP, ok := c.gatewayAndSelfIP()
 | |
| 			if !ok {
 | |
| 				t.Fatalf("could not get gateway and self IP")
 | |
| 			}
 | |
| 
 | |
| 			ext, ok := c.getUPnPPortMapping(ctx, gw, netip.AddrPortFrom(myIP, 12345), 0)
 | |
| 			if ok {
 | |
| 				t.Fatal("did not expect to get UPnP port mapping")
 | |
| 			}
 | |
| 			if ext.IsValid() {
 | |
| 				t.Fatalf("expected no external address; got %v", ext)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func mustProbeUPnP(tb testing.TB, ctx context.Context, c *Client) portmappertype.ProbeResult {
 | |
| 	tb.Helper()
 | |
| 	res, err := c.Probe(ctx)
 | |
| 	if err != nil {
 | |
| 		tb.Fatalf("Probe: %v", err)
 | |
| 	}
 | |
| 	if !res.UPnP {
 | |
| 		tb.Fatalf("didn't detect UPnP")
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| 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 testAddPortMappingPermanentLease_InvalidArgs = `<?xml version="1.0"?>
 | |
| <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 | |
|  <SOAP:Body>
 | |
|    <SOAP:Fault>
 | |
|     <faultcode>SOAP:Client</faultcode>
 | |
|     <faultstring>UPnPError</faultstring>
 | |
|     <detail>
 | |
|      <UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
 | |
|       <errorCode>402</errorCode>
 | |
|       <errorDescription>Invalid Args</errorDescription>
 | |
|      </UPnPError>
 | |
|     </detail>
 | |
|    </SOAP:Fault>
 | |
|  </SOAP:Body>
 | |
| </SOAP: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>
 | |
| `
 | |
| 
 | |
| const testLegacyAddPortMappingResponse = `<?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:dslforum-org:service:WANPPPConnection:1"/>
 | |
|   </s:Body>
 | |
| </s:Envelope>
 | |
| `
 | |
| 
 | |
| const testLegacyGetExternalIPAddressResponse = `<?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:dslforum-org:service:WANPPPConnection:1">
 | |
|       <NewExternalIPAddress>123.123.123.123</NewExternalIPAddress>
 | |
|     </u:GetExternalIPAddressResponse>
 | |
|   </s:Body>
 | |
| </s:Envelope>
 | |
| `
 | |
| 
 | |
| const testLegacyGetStatusInfoResponse = `<?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:dslforum-org:service:WANPPPConnection:1">
 | |
|       <NewConnectionStatus>Connected</NewConnectionStatus>
 | |
|       <NewLastConnectionError>ERROR_NONE</NewLastConnectionError>
 | |
|       <NewUpTime>9999</NewUpTime>
 | |
|     </u:GetStatusInfoResponse>
 | |
|   </s:Body>
 | |
| </s:Envelope>
 | |
| `
 | |
| 
 | |
| func makeGetExternalIPAddressResponse(ip string) string {
 | |
| 	return fmt.Sprintf(`<?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>%s</NewExternalIPAddress>
 | |
|     </u:GetExternalIPAddressResponse>
 | |
|   </s:Body>
 | |
| </s:Envelope>
 | |
| `, ip)
 | |
| }
 |