Skip to content

Commit 1d25039

Browse files
authored
p2p/nat: limit UPNP request concurrency (#21390)
This adds a lock around requests because some routers can't handle concurrent requests. Requests are also rate-limited. The Map function request a new mapping exactly when the map timeout occurs instead of 5 minutes earlier. This should prevent duplicate mappings.
1 parent 82a9e11 commit 1d25039

File tree

2 files changed

+64
-25
lines changed

2 files changed

+64
-25
lines changed

p2p/nat/nat.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,14 @@ func Parse(spec string) (Interface, error) {
9191
}
9292

9393
const (
94-
mapTimeout = 20 * time.Minute
95-
mapUpdateInterval = 15 * time.Minute
94+
mapTimeout = 10 * time.Minute
9695
)
9796

9897
// Map adds a port mapping on m and keeps it alive until c is closed.
9998
// This function is typically invoked in its own goroutine.
100-
func Map(m Interface, c chan struct{}, protocol string, extport, intport int, name string) {
99+
func Map(m Interface, c <-chan struct{}, protocol string, extport, intport int, name string) {
101100
log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m)
102-
refresh := time.NewTimer(mapUpdateInterval)
101+
refresh := time.NewTimer(mapTimeout)
103102
defer func() {
104103
refresh.Stop()
105104
log.Debug("Deleting port mapping")
@@ -121,7 +120,7 @@ func Map(m Interface, c chan struct{}, protocol string, extport, intport int, na
121120
if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil {
122121
log.Debug("Couldn't add port mapping", "err", err)
123122
}
124-
refresh.Reset(mapUpdateInterval)
123+
refresh.Reset(mapTimeout)
125124
}
126125
}
127126
}

p2p/nat/natupnp.go

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,25 @@ import (
2121
"fmt"
2222
"net"
2323
"strings"
24+
"sync"
2425
"time"
2526

2627
"github.com/huin/goupnp"
2728
"github.com/huin/goupnp/dcps/internetgateway1"
2829
"github.com/huin/goupnp/dcps/internetgateway2"
2930
)
3031

31-
const soapRequestTimeout = 3 * time.Second
32+
const (
33+
soapRequestTimeout = 3 * time.Second
34+
rateLimit = 200 * time.Millisecond
35+
)
3236

3337
type upnp struct {
34-
dev *goupnp.RootDevice
35-
service string
36-
client upnpClient
38+
dev *goupnp.RootDevice
39+
service string
40+
client upnpClient
41+
mu sync.Mutex
42+
lastReqTime time.Time
3743
}
3844

3945
type upnpClient interface {
@@ -43,8 +49,23 @@ type upnpClient interface {
4349
GetNATRSIPStatus() (sip bool, nat bool, err error)
4450
}
4551

52+
func (n *upnp) natEnabled() bool {
53+
var ok bool
54+
var err error
55+
n.withRateLimit(func() error {
56+
_, ok, err = n.client.GetNATRSIPStatus()
57+
return err
58+
})
59+
return err == nil && ok
60+
}
61+
4662
func (n *upnp) ExternalIP() (addr net.IP, err error) {
47-
ipString, err := n.client.GetExternalIPAddress()
63+
var ipString string
64+
n.withRateLimit(func() error {
65+
ipString, err = n.client.GetExternalIPAddress()
66+
return err
67+
})
68+
4869
if err != nil {
4970
return nil, err
5071
}
@@ -63,7 +84,10 @@ func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, li
6384
protocol = strings.ToUpper(protocol)
6485
lifetimeS := uint32(lifetime / time.Second)
6586
n.DeleteMapping(protocol, extport, intport)
66-
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
87+
88+
return n.withRateLimit(func() error {
89+
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
90+
})
6791
}
6892

6993
func (n *upnp) internalAddress() (net.IP, error) {
@@ -90,36 +114,51 @@ func (n *upnp) internalAddress() (net.IP, error) {
90114
}
91115

92116
func (n *upnp) DeleteMapping(protocol string, extport, intport int) error {
93-
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
117+
return n.withRateLimit(func() error {
118+
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
119+
})
94120
}
95121

96122
func (n *upnp) String() string {
97123
return "UPNP " + n.service
98124
}
99125

126+
func (n *upnp) withRateLimit(fn func() error) error {
127+
n.mu.Lock()
128+
defer n.mu.Unlock()
129+
130+
lastreq := time.Since(n.lastReqTime)
131+
if lastreq < rateLimit {
132+
time.Sleep(rateLimit - lastreq)
133+
}
134+
err := fn()
135+
n.lastReqTime = time.Now()
136+
return err
137+
}
138+
100139
// discoverUPnP searches for Internet Gateway Devices
101140
// and returns the first one it can find on the local network.
102141
func discoverUPnP() Interface {
103142
found := make(chan *upnp, 2)
104143
// IGDv1
105-
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
144+
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(sc goupnp.ServiceClient) *upnp {
106145
switch sc.Service.ServiceType {
107146
case internetgateway1.URN_WANIPConnection_1:
108-
return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{ServiceClient: sc}}
147+
return &upnp{service: "IGDv1-IP1", client: &internetgateway1.WANIPConnection1{ServiceClient: sc}}
109148
case internetgateway1.URN_WANPPPConnection_1:
110-
return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
149+
return &upnp{service: "IGDv1-PPP1", client: &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
111150
}
112151
return nil
113152
})
114153
// IGDv2
115-
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
154+
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(sc goupnp.ServiceClient) *upnp {
116155
switch sc.Service.ServiceType {
117156
case internetgateway2.URN_WANIPConnection_1:
118-
return &upnp{dev, "IGDv2-IP1", &internetgateway2.WANIPConnection1{ServiceClient: sc}}
157+
return &upnp{service: "IGDv2-IP1", client: &internetgateway2.WANIPConnection1{ServiceClient: sc}}
119158
case internetgateway2.URN_WANIPConnection_2:
120-
return &upnp{dev, "IGDv2-IP2", &internetgateway2.WANIPConnection2{ServiceClient: sc}}
159+
return &upnp{service: "IGDv2-IP2", client: &internetgateway2.WANIPConnection2{ServiceClient: sc}}
121160
case internetgateway2.URN_WANPPPConnection_1:
122-
return &upnp{dev, "IGDv2-PPP1", &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
161+
return &upnp{service: "IGDv2-PPP1", client: &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
123162
}
124163
return nil
125164
})
@@ -134,7 +173,7 @@ func discoverUPnP() Interface {
134173
// finds devices matching the given target and calls matcher for all
135174
// advertised services of each device. The first non-nil service found
136175
// is sent into out. If no service matched, nil is sent.
137-
func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) {
176+
func discover(out chan<- *upnp, target string, matcher func(goupnp.ServiceClient) *upnp) {
138177
devs, err := goupnp.DiscoverDevices(target)
139178
if err != nil {
140179
out <- nil
@@ -157,16 +196,17 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice,
157196
Service: service,
158197
}
159198
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
160-
upnp := matcher(devs[i].Root, sc)
199+
upnp := matcher(sc)
161200
if upnp == nil {
162201
return
163202
}
203+
upnp.dev = devs[i].Root
204+
164205
// check whether port mapping is enabled
165-
if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat {
166-
return
206+
if upnp.natEnabled() {
207+
out <- upnp
208+
found = true
167209
}
168-
out <- upnp
169-
found = true
170210
})
171211
}
172212
if !found {

0 commit comments

Comments
 (0)