Skip to content

Commit f500f88

Browse files
fjlgzliudan
authored andcommitted
feat(p2p/nat): limit UPNP request concurrency ethereum#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 012b939 commit f500f88

2 files changed

Lines changed: 64 additions & 25 deletions

File tree

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 nat bool
54+
err := n.withRateLimit(func() error {
55+
var innerErr error
56+
_, nat, innerErr = n.client.GetNATRSIPStatus()
57+
return innerErr
58+
})
59+
return err == nil && nat
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) {
@@ -93,36 +117,51 @@ func (n *upnp) internalAddress() (net.IP, error) {
93117
}
94118

95119
func (n *upnp) DeleteMapping(protocol string, extport, intport int) error {
96-
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
120+
return n.withRateLimit(func() error {
121+
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
122+
})
97123
}
98124

99125
func (n *upnp) String() string {
100126
return "UPNP " + n.service
101127
}
102128

129+
func (n *upnp) withRateLimit(fn func() error) error {
130+
n.mu.Lock()
131+
defer n.mu.Unlock()
132+
133+
lastreq := time.Since(n.lastReqTime)
134+
if lastreq < rateLimit {
135+
time.Sleep(rateLimit - lastreq)
136+
}
137+
err := fn()
138+
n.lastReqTime = time.Now()
139+
return err
140+
}
141+
103142
// discoverUPnP searches for Internet Gateway Devices
104143
// and returns the first one it can find on the local network.
105144
func discoverUPnP() Interface {
106145
found := make(chan *upnp, 2)
107146
// IGDv1
108-
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
147+
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(sc goupnp.ServiceClient) *upnp {
109148
switch sc.Service.ServiceType {
110149
case internetgateway1.URN_WANIPConnection_1:
111-
return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{ServiceClient: sc}}
150+
return &upnp{service: "IGDv1-IP1", client: &internetgateway1.WANIPConnection1{ServiceClient: sc}}
112151
case internetgateway1.URN_WANPPPConnection_1:
113-
return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
152+
return &upnp{service: "IGDv1-PPP1", client: &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
114153
}
115154
return nil
116155
})
117156
// IGDv2
118-
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
157+
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(sc goupnp.ServiceClient) *upnp {
119158
switch sc.Service.ServiceType {
120159
case internetgateway2.URN_WANIPConnection_1:
121-
return &upnp{dev, "IGDv2-IP1", &internetgateway2.WANIPConnection1{ServiceClient: sc}}
160+
return &upnp{service: "IGDv2-IP1", client: &internetgateway2.WANIPConnection1{ServiceClient: sc}}
122161
case internetgateway2.URN_WANIPConnection_2:
123-
return &upnp{dev, "IGDv2-IP2", &internetgateway2.WANIPConnection2{ServiceClient: sc}}
162+
return &upnp{service: "IGDv2-IP2", client: &internetgateway2.WANIPConnection2{ServiceClient: sc}}
124163
case internetgateway2.URN_WANPPPConnection_1:
125-
return &upnp{dev, "IGDv2-PPP1", &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
164+
return &upnp{service: "IGDv2-PPP1", client: &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
126165
}
127166
return nil
128167
})
@@ -137,7 +176,7 @@ func discoverUPnP() Interface {
137176
// finds devices matching the given target and calls matcher for all
138177
// advertised services of each device. The first non-nil service found
139178
// is sent into out. If no service matched, nil is sent.
140-
func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) {
179+
func discover(out chan<- *upnp, target string, matcher func(goupnp.ServiceClient) *upnp) {
141180
devs, err := goupnp.DiscoverDevices(target)
142181
if err != nil {
143182
out <- nil
@@ -160,16 +199,17 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice,
160199
Service: service,
161200
}
162201
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
163-
upnp := matcher(devs[i].Root, sc)
202+
upnp := matcher(sc)
164203
if upnp == nil {
165204
return
166205
}
206+
upnp.dev = devs[i].Root
207+
167208
// check whether port mapping is enabled
168-
if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat {
169-
return
209+
if upnp.natEnabled() {
210+
out <- upnp
211+
found = true
170212
}
171-
out <- upnp
172-
found = true
173213
})
174214
}
175215
if !found {

0 commit comments

Comments
 (0)