plugeth/p2p/nat/natupnp_test.go
Felix Lange 663d4e0aff p2p/nat: add test for UPnP auto discovery via SSDP
The test listens for multicast UDP packets on the default interface
because I couldn't get it to work reliably on loopback without massive
changes to goupnp. This means that the test might fail when there is a
UPnP-enabled router attached on that interface. I checked that locally
by looping the test and it passes reliably because the local SSDP server
always responds faster.
2015-05-14 12:13:19 +02:00

224 lines
7.9 KiB
Go

package nat
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"testing"
"github.com/huin/goupnp/httpu"
)
func TestUPNP_DDWRT(t *testing.T) {
dev := &fakeIGD{
t: t,
ssdpResp: "HTTP/1.1 200 OK\r\n" +
"Cache-Control: max-age=300\r\n" +
"Date: Sun, 10 May 2015 10:05:33 GMT\r\n" +
"Ext: \r\n" +
"Location: http://{{listenAddr}}/InternetGatewayDevice.xml\r\n" +
"Server: POSIX UPnP/1.0 DD-WRT Linux/V24\r\n" +
"ST: urn:schemas-upnp-org:device:WANConnectionDevice:1\r\n" +
"USN: uuid:CB2471CC-CF2E-9795-8D9C-E87B34C16800::urn:schemas-upnp-org:device:WANConnectionDevice:1\r\n" +
"\r\n",
httpResps: map[string]string{
"GET /InternetGatewayDevice.xml": `
<?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>
<manufacturer>DD-WRT</manufacturer>
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
<modelDescription>Gateway</modelDescription>
<friendlyName>Asus RT-N16:DD-WRT</friendlyName>
<modelName>Asus RT-N16</modelName>
<modelNumber>V24</modelNumber>
<serialNumber>0000001</serialNumber>
<modelURL>http://www.dd-wrt.com</modelURL>
<UDN>uuid:A13AB4C3-3A14-E386-DE6A-EFEA923A06FE</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId>
<SCPDURL>/x_layer3forwarding.xml</SCPDURL>
<controlURL>/control?Layer3Forwarding</controlURL>
<eventSubURL>/event?Layer3Forwarding</eventSubURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice</friendlyName>
<manufacturer>DD-WRT</manufacturer>
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
<modelDescription>Gateway</modelDescription>
<modelName>router</modelName>
<modelURL>http://www.dd-wrt.com</modelURL>
<UDN>uuid:48FD569B-F9A9-96AE-4EE6-EB403D3DB91A</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<SCPDURL>/x_wancommoninterfaceconfig.xml</SCPDURL>
<controlURL>/control?WANCommonInterfaceConfig</controlURL>
<eventSubURL>/event?WANCommonInterfaceConfig</eventSubURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WAN Connection Device</friendlyName>
<manufacturer>DD-WRT</manufacturer>
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
<modelDescription>Gateway</modelDescription>
<modelName>router</modelName>
<modelURL>http://www.dd-wrt.com</modelURL>
<UDN>uuid:CB2471CC-CF2E-9795-8D9C-E87B34C16800</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<SCPDURL>/x_wanipconnection.xml</SCPDURL>
<controlURL>/control?WANIPConnection</controlURL>
<eventSubURL>/event?WANIPConnection</eventSubURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
<device>
<deviceType>urn:schemas-upnp-org:device:LANDevice:1</deviceType>
<friendlyName>LANDevice</friendlyName>
<manufacturer>DD-WRT</manufacturer>
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
<modelDescription>Gateway</modelDescription>
<modelName>router</modelName>
<modelURL>http://www.dd-wrt.com</modelURL>
<UDN>uuid:04021998-3B35-2BDB-7B3C-99DA4435DA09</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:LANHostConfigManagement:1</serviceType>
<serviceId>urn:upnp-org:serviceId:LANHostCfg1</serviceId>
<SCPDURL>/x_lanhostconfigmanagement.xml</SCPDURL>
<controlURL>/control?LANHostConfigManagement</controlURL>
<eventSubURL>/event?LANHostConfigManagement</eventSubURL>
</service>
</serviceList>
</device>
</deviceList>
<presentationURL>http://{{listenAddr}}</presentationURL>
</device>
</root>
`,
// The response to our GetNATRSIPStatus call. This
// particular implementation has a bug where the elements
// inside u:GetNATRSIPStatusResponse are not properly
// namespaced.
"POST /control?WANIPConnection": `
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetNATRSIPStatusResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewRSIPAvailable>0</NewRSIPAvailable>
<NewNATEnabled>1</NewNATEnabled>
</u:GetNATRSIPStatusResponse>
</s:Body>
</s:Envelope>
`,
},
}
if err := dev.listen(); err != nil {
t.Skipf("cannot listen: %v", err)
}
dev.serve()
defer dev.close()
// Attempt to discover the fake device.
discovered := discoverUPnP()
if discovered == nil {
t.Fatalf("not discovered")
}
upnp, _ := discovered.(*upnp)
if upnp.service != "IGDv1-IP1" {
t.Errorf("upnp.service mismatch: got %q, want %q", upnp.service, "IGDv1-IP1")
}
wantURL := "http://" + dev.listener.Addr().String() + "/InternetGatewayDevice.xml"
if upnp.dev.URLBaseStr != wantURL {
t.Errorf("upnp.dev.URLBaseStr mismatch: got %q, want %q", upnp.dev.URLBaseStr, wantURL)
}
}
// fakeIGD presents itself as a discoverable UPnP device which sends
// canned responses to HTTPU and HTTP requests.
type fakeIGD struct {
t *testing.T // for logging
listener net.Listener
mcastListener *net.UDPConn
// This should be a complete HTTP response (including headers).
// It is sent as the response to any sspd packet. Any occurrence
// of "{{listenAddr}}" is replaced with the actual TCP listen
// address of the HTTP server.
ssdpResp string
// This one should contain XML payloads for all requests
// performed. The keys contain method and path, e.g. "GET /foo/bar".
// As with ssdpResp, "{{listenAddr}}" is replaced with the TCP
// listen address.
httpResps map[string]string
}
// httpu.Handler
func (dev *fakeIGD) ServeMessage(r *http.Request) {
dev.t.Logf(`HTTPU request %s %s`, r.Method, r.RequestURI)
conn, err := net.Dial("udp4", r.RemoteAddr)
if err != nil {
fmt.Printf("reply Dial error: %v", err)
return
}
defer conn.Close()
io.WriteString(conn, dev.replaceListenAddr(dev.ssdpResp))
}
// http.Handler
func (dev *fakeIGD) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if resp, ok := dev.httpResps[r.Method+" "+r.RequestURI]; ok {
dev.t.Logf(`HTTP request "%s %s" --> %d`, r.Method, r.RequestURI, 200)
io.WriteString(w, dev.replaceListenAddr(resp))
} else {
dev.t.Logf(`HTTP request "%s %s" --> %d`, r.Method, r.RequestURI, 404)
w.WriteHeader(http.StatusNotFound)
}
}
func (dev *fakeIGD) replaceListenAddr(resp string) string {
return strings.Replace(resp, "{{listenAddr}}", dev.listener.Addr().String(), -1)
}
func (dev *fakeIGD) listen() (err error) {
if dev.listener, err = net.Listen("tcp", "127.0.0.1:0"); err != nil {
return err
}
laddr := &net.UDPAddr{IP: net.ParseIP("239.255.255.250"), Port: 1900}
if dev.mcastListener, err = net.ListenMulticastUDP("udp", nil, laddr); err != nil {
dev.listener.Close()
return err
}
return nil
}
func (dev *fakeIGD) serve() {
go httpu.Serve(dev.mcastListener, dev)
go http.Serve(dev.listener, dev)
}
func (dev *fakeIGD) close() {
dev.mcastListener.Close()
dev.listener.Close()
}