Skip to content

Commit a7f24c2

Browse files
cskiralyfjl
andauthored
p2p/nat: fix UPnP port reset (#31566)
Make UPnP more robust - Once a random port was mapped, we try to stick to it even if a UPnP refresh fails. Previously we were immediately moving back to try the default port, leading to frequent ENR changes. - We were deleting port mappings before refresh as a possible workaround. This created issues in some UPnP servers. The UPnP (and PMP) specification is explicit about the refresh requirements, and delete is clearly not needed (see #30265 (comment)). From now on we only delete when closing. - We were trying to add port mappings only once, and then moved on to random ports. Now we insist a bit more, so that a simple failed request won't lead to ENR changes. Fixes #31418 --------- Signed-off-by: Csaba Kiraly <[email protected]> Co-authored-by: Felix Lange <[email protected]>
1 parent 5cc9137 commit a7f24c2

File tree

2 files changed

+72
-34
lines changed

2 files changed

+72
-34
lines changed

p2p/nat/natupnp.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"sync"
2727
"time"
2828

29+
"github.com/ethereum/go-ethereum/log"
2930
"github.com/huin/goupnp"
3031
"github.com/huin/goupnp/dcps/internetgateway1"
3132
"github.com/huin/goupnp/dcps/internetgateway2"
@@ -34,6 +35,8 @@ import (
3435
const (
3536
soapRequestTimeout = 3 * time.Second
3637
rateLimit = 200 * time.Millisecond
38+
retryCount = 3 // number of retries after a failed AddPortMapping
39+
randomCount = 3 // number of random ports to try
3740
)
3841

3942
type upnp struct {
@@ -89,42 +92,43 @@ func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, li
8992

9093
if extport == 0 {
9194
extport = intport
92-
} else {
93-
// Only delete port mapping if the external port was already used by geth.
94-
n.DeleteMapping(protocol, extport, intport)
9595
}
9696

9797
// Try to add port mapping, preferring the specified external port.
98-
err = n.withRateLimit(func() error {
99-
p, err := n.addAnyPortMapping(protocol, extport, intport, ip, desc, lifetimeS)
100-
if err == nil {
101-
extport = int(p)
102-
}
103-
return err
104-
})
105-
return uint16(extport), err
98+
return n.addAnyPortMapping(protocol, extport, intport, ip, desc, lifetimeS)
10699
}
107100

108101
// addAnyPortMapping tries to add a port mapping with the specified external port.
109102
// If the external port is already in use, it will try to assign another port.
110103
func (n *upnp) addAnyPortMapping(protocol string, extport, intport int, ip net.IP, desc string, lifetimeS uint32) (uint16, error) {
111104
if client, ok := n.client.(*internetgateway2.WANIPConnection2); ok {
112-
return client.AddAnyPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
105+
return n.portWithRateLimit(func() (uint16, error) {
106+
return client.AddAnyPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
107+
})
113108
}
114109
// For IGDv1 and v1 services we should first try to add with extport.
115-
err := n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
116-
if err == nil {
117-
return uint16(extport), nil
110+
for i := 0; i < retryCount+1; i++ {
111+
err := n.withRateLimit(func() error {
112+
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
113+
})
114+
if err == nil {
115+
return uint16(extport), nil
116+
}
117+
log.Debug("Failed to add port mapping", "protocol", protocol, "extport", extport, "intport", intport, "err", err)
118118
}
119119

120120
// If above fails, we retry with a random port.
121121
// We retry several times because of possible port conflicts.
122-
for i := 0; i < 3; i++ {
122+
var err error
123+
for i := 0; i < randomCount; i++ {
123124
extport = n.randomPort()
124-
err := n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
125+
err := n.withRateLimit(func() error {
126+
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
127+
})
125128
if err == nil {
126129
return uint16(extport), nil
127130
}
131+
log.Debug("Failed to add random port mapping", "protocol", protocol, "extport", extport, "intport", intport, "err", err)
128132
}
129133
return 0, err
130134
}
@@ -169,6 +173,17 @@ func (n *upnp) String() string {
169173
return "UPNP " + n.service
170174
}
171175

176+
func (n *upnp) portWithRateLimit(pfn func() (uint16, error)) (uint16, error) {
177+
var port uint16
178+
var err error
179+
fn := func() error {
180+
port, err = pfn()
181+
return err
182+
}
183+
n.withRateLimit(fn)
184+
return port, err
185+
}
186+
172187
func (n *upnp) withRateLimit(fn func() error) error {
173188
n.mu.Lock()
174189
defer n.mu.Unlock()

p2p/server_nat.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ const (
3131
portMapRefreshInterval = 8 * time.Minute
3232
portMapRetryInterval = 5 * time.Minute
3333
extipRetryInterval = 2 * time.Minute
34+
maxRetries = 5 // max number of failed attempts to refresh the mapping
3435
)
3536

3637
type portMapping struct {
3738
protocol string
3839
name string
3940
port int
41+
retries int // number of failed attempts to refresh the mapping
4042

4143
// for use by the portMappingLoop goroutine:
4244
extPort int // the mapped port returned by the NAT interface
@@ -154,28 +156,49 @@ func (srv *Server) portMappingLoop() {
154156
log.Trace("Attempting port mapping")
155157
p, err := srv.NAT.AddMapping(m.protocol, m.extPort, m.port, m.name, portMapDuration)
156158
if err != nil {
157-
log.Debug("Couldn't add port mapping", "err", err)
158-
m.extPort = 0
159+
// Failed to add or refresh port mapping.
160+
if m.extPort == 0 {
161+
log.Debug("Couldn't add port mapping", "err", err)
162+
} else {
163+
// Failed refresh. Since UPnP implementation are often buggy,
164+
// and lifetime is larger than the retry interval, this does not
165+
// mean we lost our existing mapping. We do not reset the external
166+
// port, as it is still our best chance, but we do retry soon.
167+
// We could check the error code, but UPnP implementations are buggy.
168+
log.Debug("Couldn't refresh port mapping", "err", err)
169+
m.retries++
170+
if m.retries > maxRetries {
171+
m.retries = 0
172+
err := srv.NAT.DeleteMapping(m.protocol, m.extPort, m.port)
173+
log.Debug("Couldn't refresh port mapping, trying to delete it:", "err", err)
174+
m.extPort = 0
175+
}
176+
}
159177
m.nextTime = srv.clock.Now().Add(portMapRetryInterval)
178+
// Note ENR is not updated here, i.e. we keep the last port.
160179
continue
161180
}
162-
// It was mapped!
163-
m.extPort = int(p)
164-
m.nextTime = srv.clock.Now().Add(portMapRefreshInterval)
165-
log = newLogger(m.protocol, m.extPort, m.port)
166-
if m.port != m.extPort {
167-
log.Info("NAT mapped alternative port")
168-
} else {
169-
log.Info("NAT mapped port")
170-
}
171181

172-
// Update port in local ENR.
173-
switch m.protocol {
174-
case "TCP":
175-
srv.localnode.Set(enr.TCP(m.extPort))
176-
case "UDP":
177-
srv.localnode.SetFallbackUDP(m.extPort)
182+
// It was mapped!
183+
m.retries = 0
184+
log = newLogger(m.protocol, int(p), m.port)
185+
if int(p) != m.extPort {
186+
m.extPort = int(p)
187+
if m.port != m.extPort {
188+
log.Info("NAT mapped alternative port")
189+
} else {
190+
log.Info("NAT mapped port")
191+
}
192+
193+
// Update port in local ENR.
194+
switch m.protocol {
195+
case "TCP":
196+
srv.localnode.Set(enr.TCP(m.extPort))
197+
case "UDP":
198+
srv.localnode.SetFallbackUDP(m.extPort)
199+
}
178200
}
201+
m.nextTime = srv.clock.Now().Add(portMapRefreshInterval)
179202
}
180203
}
181204
}

0 commit comments

Comments
 (0)