lib/nat: Add a nat package and service to track mappings on multiple IGDs

This commit is contained in:
Audrius Butkevicius
2016-04-10 19:36:38 +00:00
committed by Jakob Borg
parent f3ac421266
commit 19b4f3bfb4
20 changed files with 661 additions and 236 deletions

View File

@@ -13,6 +13,9 @@ import (
"net"
"net/url"
"strings"
"time"
"github.com/syncthing/syncthing/lib/nat"
)
// An IGD is a UPnP InternetGatewayDevice.
@@ -24,7 +27,7 @@ type IGD struct {
localIPAddress net.IP
}
func (n *IGD) UUID() string {
func (n *IGD) ID() string {
return n.uuid
}
@@ -47,14 +50,14 @@ func (n *IGD) URL() *url.URL {
// if action is fails for _any_ of the relevant services. For this reason, it
// is generally better to configure port mapping for each individual service
// instead.
func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
func (n *IGD) AddPortMapping(protocol nat.Protocol, externalPort, internalPort int, description string, duration time.Duration) (int, error) {
for _, service := range n.services {
err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout)
err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, duration)
if err != nil {
return err
return externalPort, err
}
}
return nil
return externalPort, nil
}
// DeletePortMapping deletes a port mapping from all relevant services on the
@@ -62,7 +65,7 @@ func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int,
// if action is fails for _any_ of the relevant services. For this reason, it
// is generally better to configure port mapping for each individual service
// instead.
func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error {
func (n *IGD) DeletePortMapping(protocol nat.Protocol, externalPort int) error {
for _, service := range n.services {
err := service.DeletePortMapping(protocol, externalPort)
if err != nil {

View File

@@ -13,6 +13,9 @@ import (
"encoding/xml"
"fmt"
"net"
"time"
"github.com/syncthing/syncthing/lib/nat"
)
// An IGDService is a specific service provided by an IGD.
@@ -23,7 +26,7 @@ type IGDService struct {
}
// AddPortMapping adds a port mapping to the specified IGD service.
func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol nat.Protocol, externalPort, internalPort int, description string, duration time.Duration) error {
tpl := `<u:AddPortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
@@ -34,10 +37,10 @@ func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, ex
<NewPortMappingDescription>%s</NewPortMappingDescription>
<NewLeaseDuration>%d</NewLeaseDuration>
</u:AddPortMapping>`
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, duration/time.Second)
response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
if err != nil && timeout > 0 {
if err != nil && duration > 0 {
// Try to repair error code 725 - OnlyPermanentLeasesSupported
envelope := &soapErrorResponse{}
if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
@@ -52,7 +55,7 @@ func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, ex
}
// DeletePortMapping deletes a port mapping from the specified IGD service.
func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) error {
func (s *IGDService) DeletePortMapping(protocol nat.Protocol, externalPort int) error {
tpl := `<u:DeletePortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>

View File

@@ -1,132 +0,0 @@
// Copyright (C) 2015 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 upnp
import (
"fmt"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
)
// Service runs a loop for discovery of IGDs (Internet Gateway Devices) and
// setup/renewal of a port mapping.
type Service struct {
cfg *config.Wrapper
localPort int
extPort int
extPortMut sync.Mutex
stop chan struct{}
}
func NewUPnPService(cfg *config.Wrapper, localPort int) *Service {
return &Service{
cfg: cfg,
localPort: localPort,
extPortMut: sync.NewMutex(),
}
}
func (s *Service) Serve() {
foundIGD := true
s.stop = make(chan struct{})
for {
igds := Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
if len(igds) > 0 {
foundIGD = true
s.extPortMut.Lock()
oldExtPort := s.extPort
s.extPortMut.Unlock()
newExtPort := s.tryIGDs(igds, oldExtPort)
s.extPortMut.Lock()
s.extPort = newExtPort
s.extPortMut.Unlock()
} else if foundIGD {
// Only print a notice if we've previously found an IGD or this is
// the first time around.
foundIGD = false
l.Infof("No UPnP device detected")
}
d := time.Duration(s.cfg.Options().UPnPRenewalM) * time.Minute
if d == 0 {
// We always want to do renewal so lets just pick a nice sane number.
d = 30 * time.Minute
}
select {
case <-s.stop:
return
case <-time.After(d):
}
}
}
func (s *Service) Stop() {
close(s.stop)
}
func (s *Service) ExternalPort() int {
s.extPortMut.Lock()
port := s.extPort
s.extPortMut.Unlock()
return port
}
func (s *Service) tryIGDs(igds []IGD, prevExtPort int) int {
// Lets try all the IGDs we found and use the first one that works.
// TODO: Use all of them, and sort out the resulting mess to the
// discovery announcement code...
for _, igd := range igds {
extPort, err := s.tryIGD(igd, prevExtPort)
if err != nil {
l.Warnf("Failed to set UPnP port mapping: external port %d on device %s.", extPort, igd.FriendlyIdentifier())
continue
}
if extPort != prevExtPort {
l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
events.Default.Log(events.ExternalPortMappingChanged, map[string]int{"port": extPort})
}
l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
return extPort
}
return 0
}
func (s *Service) tryIGD(igd IGD, suggestedPort int) (int, error) {
var err error
leaseTime := s.cfg.Options().UPnPLeaseM * 60
if suggestedPort != 0 {
// First try renewing our existing mapping.
name := fmt.Sprintf("syncthing-%d", suggestedPort)
err = igd.AddPortMapping(TCP, suggestedPort, s.localPort, name, leaseTime)
if err == nil {
return suggestedPort, nil
}
}
for i := 0; i < 10; i++ {
// Then try up to ten random ports.
extPort := 1024 + util.PredictableRandom.Intn(65535-1024)
name := fmt.Sprintf("syncthing-%d", extPort)
err = igd.AddPortMapping(TCP, extPort, s.localPort, name, leaseTime)
if err == nil {
return extPort, nil
}
}
return 0, err
}

View File

@@ -26,15 +26,13 @@ import (
"time"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/sync"
)
type Protocol string
const (
TCP Protocol = "TCP"
UDP = "UDP"
)
func init() {
nat.Register(Discover)
}
type upnpService struct {
ID string `xml:"serviceId"`
@@ -55,8 +53,8 @@ type upnpRoot struct {
// Discover discovers UPnP InternetGatewayDevices.
// The order in which the devices appear in the results list is not deterministic.
func Discover(timeout time.Duration) []IGD {
var results []IGD
func Discover(renewal, timeout time.Duration) []nat.Device {
var results []nat.Device
interfaces, err := net.Interfaces()
if err != nil {
@@ -91,7 +89,7 @@ func Discover(timeout time.Duration) []IGD {
nextResult:
for result := range resultChan {
for _, existingResult := range results {
if existingResult.uuid == result.uuid {
if existingResult.ID() == result.ID() {
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
for _, service := range result.services {
l.Debugf("* [%s] %s", service.ID, service.URL)
@@ -100,7 +98,7 @@ nextResult:
}
}
results = append(results, result)
results = append(results, &result)
l.Debugf("UPnP discovery result %s with services:", result.uuid)
for _, service := range result.services {
l.Debugf("* [%s] %s", service.ID, service.URL)