Skip to content

Commit 2a3d79c

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 2a3d79c

2 files changed

Lines changed: 65 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: 61 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,24 @@ 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+
err = n.withRateLimit(func() error {
65+
var innerErr error
66+
ipString, innerErr = n.client.GetExternalIPAddress()
67+
return innerErr
68+
})
69+
4870
if err != nil {
4971
return nil, err
5072
}
@@ -63,7 +85,10 @@ func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, li
6385
protocol = strings.ToUpper(protocol)
6486
lifetimeS := uint32(lifetime / time.Second)
6587
n.DeleteMapping(protocol, extport, intport)
66-
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
88+
89+
return n.withRateLimit(func() error {
90+
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
91+
})
6792
}
6893

6994
func (n *upnp) internalAddress() (net.IP, error) {
@@ -93,36 +118,51 @@ func (n *upnp) internalAddress() (net.IP, error) {
93118
}
94119

95120
func (n *upnp) DeleteMapping(protocol string, extport, intport int) error {
96-
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
121+
return n.withRateLimit(func() error {
122+
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
123+
})
97124
}
98125

99126
func (n *upnp) String() string {
100127
return "UPNP " + n.service
101128
}
102129

130+
func (n *upnp) withRateLimit(fn func() error) error {
131+
n.mu.Lock()
132+
defer n.mu.Unlock()
133+
134+
lastreq := time.Since(n.lastReqTime)
135+
if lastreq < rateLimit {
136+
time.Sleep(rateLimit - lastreq)
137+
}
138+
err := fn()
139+
n.lastReqTime = time.Now()
140+
return err
141+
}
142+
103143
// discoverUPnP searches for Internet Gateway Devices
104144
// and returns the first one it can find on the local network.
105145
func discoverUPnP() Interface {
106146
found := make(chan *upnp, 2)
107147
// IGDv1
108-
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
148+
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(sc goupnp.ServiceClient) *upnp {
109149
switch sc.Service.ServiceType {
110150
case internetgateway1.URN_WANIPConnection_1:
111-
return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{ServiceClient: sc}}
151+
return &upnp{service: "IGDv1-IP1", client: &internetgateway1.WANIPConnection1{ServiceClient: sc}}
112152
case internetgateway1.URN_WANPPPConnection_1:
113-
return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
153+
return &upnp{service: "IGDv1-PPP1", client: &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
114154
}
115155
return nil
116156
})
117157
// IGDv2
118-
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
158+
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(sc goupnp.ServiceClient) *upnp {
119159
switch sc.Service.ServiceType {
120160
case internetgateway2.URN_WANIPConnection_1:
121-
return &upnp{dev, "IGDv2-IP1", &internetgateway2.WANIPConnection1{ServiceClient: sc}}
161+
return &upnp{service: "IGDv2-IP1", client: &internetgateway2.WANIPConnection1{ServiceClient: sc}}
122162
case internetgateway2.URN_WANIPConnection_2:
123-
return &upnp{dev, "IGDv2-IP2", &internetgateway2.WANIPConnection2{ServiceClient: sc}}
163+
return &upnp{service: "IGDv2-IP2", client: &internetgateway2.WANIPConnection2{ServiceClient: sc}}
124164
case internetgateway2.URN_WANPPPConnection_1:
125-
return &upnp{dev, "IGDv2-PPP1", &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
165+
return &upnp{service: "IGDv2-PPP1", client: &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
126166
}
127167
return nil
128168
})
@@ -137,7 +177,7 @@ func discoverUPnP() Interface {
137177
// finds devices matching the given target and calls matcher for all
138178
// advertised services of each device. The first non-nil service found
139179
// 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) {
180+
func discover(out chan<- *upnp, target string, matcher func(goupnp.ServiceClient) *upnp) {
141181
devs, err := goupnp.DiscoverDevices(target)
142182
if err != nil {
143183
out <- nil
@@ -160,16 +200,17 @@ func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice,
160200
Service: service,
161201
}
162202
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
163-
upnp := matcher(devs[i].Root, sc)
203+
upnp := matcher(sc)
164204
if upnp == nil {
165205
return
166206
}
207+
upnp.dev = devs[i].Root
208+
167209
// check whether port mapping is enabled
168-
if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat {
169-
return
210+
if upnp.natEnabled() {
211+
out <- upnp
212+
found = true
170213
}
171-
out <- upnp
172-
found = true
173214
})
174215
}
175216
if !found {

0 commit comments

Comments
 (0)