forked from ethereum/go-ethereum
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
1 changed file
with
223 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
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() | ||
} |