2016-04-13 18:50:40 +00:00
|
|
|
// 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,
|
2017-02-09 06:52:18 +00:00
|
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
2016-04-13 18:50:40 +00:00
|
|
|
|
|
|
|
package pmp
|
|
|
|
|
|
|
|
import (
|
2019-11-26 07:39:51 +00:00
|
|
|
"context"
|
2022-03-26 10:05:57 +00:00
|
|
|
"errors"
|
2016-04-13 18:50:40 +00:00
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2016-06-02 20:40:30 +00:00
|
|
|
"github.com/jackpal/gateway"
|
2022-03-26 10:05:57 +00:00
|
|
|
natpmp "github.com/jackpal/go-nat-pmp"
|
2020-02-24 20:57:15 +00:00
|
|
|
|
2016-04-13 18:50:40 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/nat"
|
2022-09-14 06:44:46 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/osutil"
|
2023-08-21 17:44:33 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/svcutil"
|
2016-04-13 18:50:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
nat.Register(Discover)
|
|
|
|
}
|
|
|
|
|
2019-11-26 07:39:51 +00:00
|
|
|
func Discover(ctx context.Context, renewal, timeout time.Duration) []nat.Device {
|
2020-02-24 20:57:15 +00:00
|
|
|
var ip net.IP
|
2023-08-21 17:44:33 +00:00
|
|
|
err := svcutil.CallWithContext(ctx, func() error {
|
2020-02-24 20:57:15 +00:00
|
|
|
var err error
|
|
|
|
ip, err = gateway.DiscoverGateway()
|
|
|
|
return err
|
|
|
|
})
|
2016-04-13 18:50:40 +00:00
|
|
|
if err != nil {
|
|
|
|
l.Debugln("Failed to discover gateway", err)
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-16 16:48:07 +00:00
|
|
|
if ip == nil || ip.IsUnspecified() {
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-13 18:50:40 +00:00
|
|
|
|
|
|
|
l.Debugln("Discovered gateway at", ip)
|
|
|
|
|
2020-01-03 11:39:59 +00:00
|
|
|
c := natpmp.NewClientWithTimeout(ip, timeout)
|
2016-04-13 18:50:40 +00:00
|
|
|
// Try contacting the gateway, if it does not respond, assume it does not
|
|
|
|
// speak NAT-PMP.
|
2023-08-21 17:44:33 +00:00
|
|
|
err = svcutil.CallWithContext(ctx, func() error {
|
2020-06-22 08:01:57 +00:00
|
|
|
_, ierr := c.GetExternalAddress()
|
|
|
|
return ierr
|
|
|
|
})
|
|
|
|
if err != nil {
|
2022-03-26 10:05:57 +00:00
|
|
|
if errors.Is(err, context.Canceled) {
|
2020-06-22 08:01:57 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if strings.Contains(err.Error(), "Timed out") {
|
|
|
|
l.Debugln("Timeout trying to get external address, assume no NAT-PMP available")
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-13 18:50:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var localIP net.IP
|
|
|
|
// Port comes from the natpmp package
|
2019-11-26 07:39:51 +00:00
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
|
|
defer cancel()
|
|
|
|
conn, err := (&net.Dialer{}).DialContext(timeoutCtx, "udp", net.JoinHostPort(ip.String(), "5351"))
|
2016-04-13 18:50:40 +00:00
|
|
|
if err == nil {
|
|
|
|
conn.Close()
|
2022-09-14 06:44:46 +00:00
|
|
|
localIP, err = osutil.IPFromAddr(conn.LocalAddr())
|
|
|
|
if localIP == nil {
|
2016-04-13 18:50:40 +00:00
|
|
|
l.Debugln("Failed to lookup local IP", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return []nat.Device{&wrapper{
|
|
|
|
renewal: renewal,
|
|
|
|
localIP: localIP,
|
|
|
|
gatewayIP: ip,
|
|
|
|
client: c,
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
|
|
|
type wrapper struct {
|
|
|
|
renewal time.Duration
|
|
|
|
localIP net.IP
|
|
|
|
gatewayIP net.IP
|
|
|
|
client *natpmp.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *wrapper) ID() string {
|
|
|
|
return fmt.Sprintf("NAT-PMP@%s", w.gatewayIP.String())
|
|
|
|
}
|
|
|
|
|
2023-12-11 06:36:18 +00:00
|
|
|
func (w *wrapper) GetLocalIPv4Address() net.IP {
|
2016-04-13 18:50:40 +00:00
|
|
|
return w.localIP
|
|
|
|
}
|
|
|
|
|
2022-07-28 15:17:29 +00:00
|
|
|
func (w *wrapper) AddPortMapping(ctx context.Context, protocol nat.Protocol, internalPort, externalPort int, _ string, duration time.Duration) (int, error) {
|
2016-04-13 18:50:40 +00:00
|
|
|
// NAT-PMP says that if duration is 0, the mapping is actually removed
|
|
|
|
// Swap the zero with the renewal value, which should make the lease for the
|
|
|
|
// exact amount of time between the calls.
|
|
|
|
if duration == 0 {
|
|
|
|
duration = w.renewal
|
|
|
|
}
|
2020-02-24 20:57:15 +00:00
|
|
|
var result *natpmp.AddPortMappingResult
|
2023-08-21 17:44:33 +00:00
|
|
|
err := svcutil.CallWithContext(ctx, func() error {
|
2020-02-24 20:57:15 +00:00
|
|
|
var err error
|
|
|
|
result, err = w.client.AddPortMapping(strings.ToLower(string(protocol)), internalPort, externalPort, int(duration/time.Second))
|
|
|
|
return err
|
|
|
|
})
|
2016-04-13 18:50:40 +00:00
|
|
|
port := 0
|
|
|
|
if result != nil {
|
|
|
|
port = int(result.MappedExternalPort)
|
|
|
|
}
|
|
|
|
return port, err
|
|
|
|
}
|
|
|
|
|
2023-12-11 06:36:18 +00:00
|
|
|
func (*wrapper) AddPinhole(_ context.Context, _ nat.Protocol, _ nat.Address, _ time.Duration) ([]net.IP, error) {
|
|
|
|
// NAT-PMP doesn't support pinholes.
|
|
|
|
return nil, errors.New("adding IPv6 pinholes is unsupported on NAT-PMP")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*wrapper) SupportsIPVersion(version nat.IPVersion) bool {
|
|
|
|
// NAT-PMP gateways should always try to create port mappings and not pinholes
|
|
|
|
// since NAT-PMP doesn't support IPv6.
|
|
|
|
return version == nat.IPvAny || version == nat.IPv4Only
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *wrapper) GetExternalIPv4Address(ctx context.Context) (net.IP, error) {
|
2020-02-24 20:57:15 +00:00
|
|
|
var result *natpmp.GetExternalAddressResult
|
2023-08-21 17:44:33 +00:00
|
|
|
err := svcutil.CallWithContext(ctx, func() error {
|
2020-02-24 20:57:15 +00:00
|
|
|
var err error
|
|
|
|
result, err = w.client.GetExternalAddress()
|
|
|
|
return err
|
|
|
|
})
|
2016-04-13 18:50:40 +00:00
|
|
|
ip := net.IPv4zero
|
|
|
|
if result != nil {
|
|
|
|
ip = net.IPv4(
|
|
|
|
result.ExternalIPAddress[0],
|
|
|
|
result.ExternalIPAddress[1],
|
|
|
|
result.ExternalIPAddress[2],
|
|
|
|
result.ExternalIPAddress[3],
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return ip, err
|
|
|
|
}
|