Files
syncthing/lib/connections/util.go
T
Marcus B SpencerandGitHub 4c64843d60 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.
2025-06-20 04:24:45 +00:00

158 lines
4.0 KiB
Go

// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"net"
"net/url"
"strconv"
"strings"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil"
)
func fixupPort(uri *url.URL, defaultPort int) *url.URL {
copyURI := *uri
host, port, err := net.SplitHostPort(uri.Host)
if e, ok := err.(*net.AddrError); ok && strings.Contains(e.Err, "missing port") {
// addr is of the form "1.2.3.4" or "[fe80::1]"
host = uri.Host
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
// net.JoinHostPort will add the brackets again
host = host[1 : len(host)-1]
}
copyURI.Host = net.JoinHostPort(host, strconv.Itoa(defaultPort))
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:" or "[fe80::1]:"
copyURI.Host = net.JoinHostPort(host, strconv.Itoa(defaultPort))
}
return &copyURI
}
func getURLsForAllAdaptersIfUnspecified(network string, uri *url.URL) []*url.URL {
ip, port, err := resolve(network, uri.Host)
// Failed to resolve
if err != nil || port == 0 {
return nil
}
// Not an unspecified address, so no point of substituting with local
// interface addresses as it's listening on a specific adapter anyway.
if len(ip) != 0 && !ip.IsUnspecified() {
return nil
}
hostPorts := getHostPortsForAllAdapters(port)
addrs := make([]*url.URL, 0, len(hostPorts))
for _, hostPort := range hostPorts {
newUri := *uri
newUri.Host = hostPort
addrs = append(addrs, &newUri)
}
return addrs
}
func getHostPortsForAllAdapters(port int) []string {
nets, err := osutil.GetInterfaceAddrs(true)
if err != nil {
// Ignore failure.
return nil
}
hostPorts := make([]string, 0, len(nets))
portStr := strconv.Itoa(port)
for _, network := range nets {
// Only accept IPv4 link-local unicast and the private ranges defined in RFC 1918 and RFC 4193
// IPv6 link-local addresses require an interface identifier to work correctly
if (network.IP.To4() != nil && network.IP.IsLinkLocalUnicast()) || network.IP.IsPrivate() {
hostPorts = append(hostPorts, net.JoinHostPort(network.IP.String(), portStr))
}
}
return hostPorts
}
func resolve(network, hostPort string) (net.IP, int, error) {
switch network {
case "tcp", "tcp4", "tcp6":
if addr, err := net.ResolveTCPAddr(network, hostPort); err != nil {
return net.IPv4zero, 0, err
} else {
return addr.IP, addr.Port, nil
}
case "udp", "udp4", "udp6":
if addr, err := net.ResolveUDPAddr(network, hostPort); err != nil {
return net.IPv4zero, 0, err
} else {
return addr.IP, addr.Port, nil
}
case "ip", "ip4", "ip6":
if addr, err := net.ResolveIPAddr(network, hostPort); err != nil {
return net.IPv4zero, 0, err
} else {
return addr.IP, 0, nil
}
}
return net.IPv4zero, 0, net.UnknownNetworkError(network)
}
func maybeReplacePort(uri *url.URL, laddr net.Addr) *url.URL {
if laddr == nil {
return uri
}
host, portStr, err := net.SplitHostPort(uri.Host)
if err != nil {
return uri
}
port, err := strconv.Atoi(portStr)
if err != nil {
return uri
}
if port != 0 {
return uri
}
_, lportStr, err := net.SplitHostPort(laddr.String())
if err != nil {
return uri
}
uriCopy := *uri
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
}