feat(connections, nat): add UDP portmapping/pinhole for QUIC (fixes #7403) (#10171)

Fixes #7403.

Tested by enabling UPnP on the router, and checking on the router page
that the external ports of the UDP mappings match what is shown in the
logs and the internal ports matching the QUIC listening port.
This commit is contained in:
Marcus B Spencer
2025-06-20 04:24:45 +00:00
committed by GitHub
parent b4ff96d754
commit 4c64843d60
4 changed files with 60 additions and 27 deletions
+27 -4
View File
@@ -49,9 +49,11 @@ type quicListener struct {
registry *registry.Registry
lanChecker *lanChecker
address *url.URL
laddr net.Addr
mut sync.Mutex
address *url.URL
natService *nat.Service
mapping *nat.Mapping
laddr net.Addr
mut sync.Mutex
}
func (t *quicListener) OnNATTypeChanged(natType stun.NATType) {
@@ -126,7 +128,24 @@ func (t *quicListener) serve(ctx context.Context) error {
l.Infof("QUIC listener (%v) starting", udpConn.LocalAddr())
defer l.Infof("QUIC listener (%v) shutting down", udpConn.LocalAddr())
var ipVersion nat.IPVersion
switch t.uri.Scheme {
case "quic4":
ipVersion = nat.IPv4Only
case "quic6":
ipVersion = nat.IPv6Only
default:
ipVersion = nat.IPvAny
}
mapping := t.natService.NewMapping(nat.UDP, ipVersion, udpAddr.IP, udpAddr.Port)
mapping.OnChanged(func() {
t.notifyAddressesChanged(t)
})
// Should be called after t.mapping is nil'ed out.
defer t.natService.RemoveMapping(mapping)
t.mut.Lock()
t.mapping = mapping
t.laddr = udpConn.LocalAddr()
t.mut.Unlock()
defer func() {
@@ -196,6 +215,9 @@ func (t *quicListener) WANAddresses() []*url.URL {
if t.address != nil {
uris = append(uris, t.address)
}
uris = append(uris, portMappingURIs(t.mapping, *t.uri)...)
t.mut.Unlock()
return uris
}
@@ -232,12 +254,13 @@ func (*quicListenerFactory) Valid(config.Configuration) error {
return nil
}
func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, _ *nat.Service, registry *registry.Registry, lanChecker *lanChecker) genericListener {
func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service, registry *registry.Registry, lanChecker *lanChecker) genericListener {
l := &quicListener{
uri: fixupPort(uri, config.DefaultQUICPort),
cfg: cfg,
tlsCfg: tlsCfg,
conns: conns,
natService: natService,
factory: f,
registry: registry,
lanChecker: lanChecker,
+2 -17
View File
@@ -175,24 +175,9 @@ func (t *tcpListener) WANAddresses() []*url.URL {
uris := []*url.URL{
maybeReplacePort(t.uri, t.laddr),
}
if t.mapping != nil {
addrs := t.mapping.ExternalAddresses()
for _, addr := range addrs {
uri := *t.uri
// Does net.JoinHostPort internally
uri.Host = addr.String()
uris = append(uris, &uri)
// For every address with a specified IP, add one without an IP,
// just in case the specified IP is still internal (router behind DMZ).
if len(addr.IP) != 0 && !addr.IP.IsUnspecified() {
zeroUri := *t.uri
addr.IP = nil
zeroUri.Host = addr.String()
uris = append(uris, &zeroUri)
}
}
}
uris = append(uris, portMappingURIs(t.mapping, *t.uri)...)
t.mut.RUnlock()
// If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
+25
View File
@@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil"
)
@@ -130,3 +131,27 @@ func maybeReplacePort(uri *url.URL, laddr net.Addr) *url.URL {
uriCopy.Host = net.JoinHostPort(host, lportStr)
return &uriCopy
}
func portMappingURIs(mapping *nat.Mapping, listener_uri url.URL) []*url.URL {
var uris []*url.URL
if mapping != nil {
addrs := mapping.ExternalAddresses()
for _, addr := range addrs {
uri := listener_uri
// Does net.JoinHostPort internally
uri.Host = addr.String()
uris = append(uris, &uri)
// For every address with a specified IP, add one without an IP,
// just in case the specified IP is still internal (router behind DMZ).
if len(addr.IP) != 0 && !addr.IP.IsUnspecified() {
zeroUri := listener_uri
addr.IP = nil
zeroUri.Host = addr.String()
uris = append(uris, &zeroUri)
}
}
}
return uris
}