aboutsummaryrefslogtreecommitdiffstats
path: root/p2p
diff options
context:
space:
mode:
Diffstat (limited to 'p2p')
-rw-r--r--p2p/nat/nat.go45
-rw-r--r--p2p/nat/nat_test.go48
-rw-r--r--p2p/nat/natupnp_test.go223
3 files changed, 292 insertions, 24 deletions
diff --git a/p2p/nat/nat.go b/p2p/nat/nat.go
index fe00bdab0..9acb34398 100644
--- a/p2p/nat/nat.go
+++ b/p2p/nat/nat.go
@@ -86,13 +86,13 @@ func Map(m Interface, c chan struct{}, protocol string, extport, intport int, na
refresh := time.NewTimer(mapUpdateInterval)
defer func() {
refresh.Stop()
- glog.V(logger.Debug).Infof("Deleting port mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
+ glog.V(logger.Debug).Infof("deleting port mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
m.DeleteMapping(protocol, extport, intport)
}()
- glog.V(logger.Debug).Infof("add mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
if err := m.AddMapping(protocol, intport, extport, name, mapTimeout); err != nil {
- glog.V(logger.Warn).Infof("network port %d could not be mapped: %v\n", intport, err)
- glog.V(logger.Debug).Infof("mapping with %v returned %v\n", m, err)
+ glog.V(logger.Debug).Infof("network port %s:%d could not be mapped: %v\n", protocol, intport, err)
+ } else {
+ glog.V(logger.Info).Infof("mapped network port %s:%d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
}
for {
select {
@@ -101,10 +101,9 @@ func Map(m Interface, c chan struct{}, protocol string, extport, intport int, na
return
}
case <-refresh.C:
- glog.V(logger.Detail).Infof("refresh mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
+ glog.V(logger.Detail).Infof("refresh port mapping %s:%d -> %d (%s) using %s\n", protocol, extport, intport, name, m)
if err := m.AddMapping(protocol, intport, extport, name, mapTimeout); err != nil {
- glog.V(logger.Warn).Infof("network port %d could not be mapped: %v\n", intport, err)
- glog.V(logger.Debug).Infof("mapping with %v returned %v\n", m, err)
+ glog.V(logger.Debug).Infof("network port %s:%d could not be mapped: %v\n", protocol, intport, err)
}
refresh.Reset(mapUpdateInterval)
}
@@ -172,8 +171,9 @@ func PMP(gateway net.IP) Interface {
// This type is useful because discovery can take a while but we
// want return an Interface value from UPnP, PMP and Auto immediately.
type autodisc struct {
- what string
- done <-chan Interface
+ what string // type of interface being autodiscovered
+ once sync.Once
+ doit func() Interface
mu sync.Mutex
found Interface
@@ -181,9 +181,10 @@ type autodisc struct {
func startautodisc(what string, doit func() Interface) Interface {
// TODO: monitor network configuration and rerun doit when it changes.
- done := make(chan Interface)
- ad := &autodisc{what: what, done: done}
- go func() { done <- doit(); close(done) }()
+ ad := &autodisc{what: what, doit: doit}
+ // Start the auto discovery as early as possible so it is already
+ // in progress when the rest of the stack calls the methods.
+ go ad.wait()
return ad
}
@@ -218,19 +219,15 @@ func (n *autodisc) String() string {
}
}
+// wait blocks until auto-discovery has been performed.
func (n *autodisc) wait() error {
- n.mu.Lock()
- found := n.found
- n.mu.Unlock()
- if found != nil {
- // already discovered
- return nil
- }
- if found = <-n.done; found == nil {
- return errors.New("no UPnP or NAT-PMP router discovered")
+ n.once.Do(func() {
+ n.mu.Lock()
+ n.found = n.doit()
+ n.mu.Unlock()
+ })
+ if n.found == nil {
+ return fmt.Errorf("no %s router discovered", n.what)
}
- n.mu.Lock()
- n.found = found
- n.mu.Unlock()
return nil
}
diff --git a/p2p/nat/nat_test.go b/p2p/nat/nat_test.go
new file mode 100644
index 000000000..95c50522e
--- /dev/null
+++ b/p2p/nat/nat_test.go
@@ -0,0 +1,48 @@
+package nat
+
+import (
+ "bytes"
+ "net"
+ "testing"
+ "time"
+)
+
+// This test checks that autodisc doesn't hang and returns
+// consistent results when multiple goroutines call its methods
+// concurrently.
+func TestAutoDiscRace(t *testing.T) {
+ ad := startautodisc("thing", func() Interface {
+ time.Sleep(500 * time.Millisecond)
+ return extIP{33, 44, 55, 66}
+ })
+
+ // Spawn a few concurrent calls to ad.ExternalIP.
+ type rval struct {
+ ip net.IP
+ err error
+ }
+ results := make(chan rval, 50)
+ for i := 0; i < cap(results); i++ {
+ go func() {
+ ip, err := ad.ExternalIP()
+ results <- rval{ip, err}
+ }()
+ }
+
+ // Check that they all return the correct result within the deadline.
+ deadline := time.After(550 * time.Millisecond)
+ for i := 0; i < cap(results); i++ {
+ select {
+ case <-deadline:
+ t.Fatal("deadline exceeded")
+ case rval := <-results:
+ if rval.err != nil {
+ t.Errorf("result %d: unexpected error: %v", i, rval.err)
+ }
+ wantIP := net.IP{33, 44, 55, 66}
+ if !bytes.Equal(rval.ip, wantIP) {
+ t.Errorf("result %d: got IP %v, want %v", i, rval.ip, wantIP)
+ }
+ }
+ }
+}
diff --git a/p2p/nat/natupnp_test.go b/p2p/nat/natupnp_test.go
new file mode 100644
index 000000000..074e97c81
--- /dev/null
+++ b/p2p/nat/natupnp_test.go
@@ -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()
+}