1
0
Эх сурвалжийг харах

lib/pmp: Add NAT-PMP support (ref #698)

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2968
Audrius Butkevicius 9 жил өмнө
parent
commit
c49453c519

+ 4 - 1
cmd/syncthing/main.go

@@ -45,9 +45,12 @@ import (
 	"github.com/syncthing/syncthing/lib/symlinks"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/upgrade"
-	_ "github.com/syncthing/syncthing/lib/upnp"
 	"github.com/syncthing/syncthing/lib/util"
 
+	// Registers NAT service providers
+	_ "github.com/syncthing/syncthing/lib/pmp"
+	_ "github.com/syncthing/syncthing/lib/upnp"
+
 	"github.com/thejerf/suture"
 )
 

+ 28 - 5
lib/nat/registry.go

@@ -8,6 +8,8 @@ package nat
 
 import (
 	"time"
+
+	"github.com/syncthing/syncthing/lib/sync"
 )
 
 type DiscoverFunc func(renewal, timeout time.Duration) []Device
@@ -19,12 +21,33 @@ func Register(provider DiscoverFunc) {
 }
 
 func discoverAll(renewal, timeout time.Duration) map[string]Device {
-	nats := make(map[string]Device)
+	wg := sync.NewWaitGroup()
+	wg.Add(len(providers))
+
+	c := make(chan Device)
+	done := make(chan struct{})
+
 	for _, discoverFunc := range providers {
-		discoveredNATs := discoverFunc(renewal, timeout)
-		for _, discoveredNAT := range discoveredNATs {
-			nats[discoveredNAT.ID()] = discoveredNAT
-		}
+		go func(f DiscoverFunc) {
+			for _, dev := range f(renewal, timeout) {
+				c <- dev
+			}
+			wg.Done()
+		}(discoverFunc)
 	}
+
+	nats := make(map[string]Device)
+
+	go func() {
+		for dev := range c {
+			nats[dev.ID()] = dev
+		}
+		close(done)
+	}()
+
+	wg.Wait()
+	close(c)
+	<-done
+
 	return nats
 }

+ 22 - 0
lib/pmp/debug.go

@@ -0,0 +1,22 @@
+// 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 pmp
+
+import (
+	"os"
+	"strings"
+
+	"github.com/syncthing/syncthing/lib/logger"
+)
+
+var (
+	l = logger.DefaultLogger.NewFacility("pmp", "NAT-PMP discovery and port mapping")
+)
+
+func init() {
+	l.SetDebug("pmp", strings.Contains(os.Getenv("STTRACE"), "pmp") || os.Getenv("STTRACE") == "all")
+}

+ 105 - 0
lib/pmp/pmp.go

@@ -0,0 +1,105 @@
+// 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 pmp
+
+import (
+	"fmt"
+	"net"
+	"strings"
+	"time"
+
+	"github.com/AudriusButkevicius/go-nat-pmp"
+	"github.com/jackpal/gateway"
+	"github.com/syncthing/syncthing/lib/nat"
+)
+
+func init() {
+	nat.Register(Discover)
+}
+
+func Discover(renewal, timeout time.Duration) []nat.Device {
+	ip, err := gateway.DiscoverGateway()
+	if err != nil {
+		l.Debugln("Failed to discover gateway", err)
+		return nil
+	}
+
+	l.Debugln("Discovered gateway at", ip)
+
+	c := natpmp.NewClient(ip, timeout)
+	// Try contacting the gateway, if it does not respond, assume it does not
+	// speak NAT-PMP.
+	_, err = c.GetExternalAddress()
+	if err != nil && strings.Contains(err.Error(), "Timed out") {
+		l.Debugln("Timeout trying to get external address, assume no NAT-PMP available")
+		return nil
+	}
+
+	var localIP net.IP
+	// Port comes from the natpmp package
+	conn, err := net.DialTimeout("udp", net.JoinHostPort(ip.String(), "5351"), timeout)
+	if err == nil {
+		conn.Close()
+		localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String())
+		if err == nil {
+			localIP = net.ParseIP(localIPAddress)
+		} else {
+			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())
+}
+
+func (w *wrapper) GetLocalIPAddress() net.IP {
+	return w.localIP
+}
+
+func (w *wrapper) AddPortMapping(protocol nat.Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error) {
+	// 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
+	}
+	result, err := w.client.AddPortMapping(strings.ToLower(string(protocol)), internalPort, externalPort, int(duration/time.Second))
+	port := 0
+	if result != nil {
+		port = int(result.MappedExternalPort)
+	}
+	return port, err
+}
+
+func (w *wrapper) GetExternalIPAddress() (net.IP, error) {
+	result, err := w.client.GetExternalAddress()
+	ip := net.IPv4zero
+	if result != nil {
+		ip = net.IPv4(
+			result.ExternalIPAddress[0],
+			result.ExternalIPAddress[1],
+			result.ExternalIPAddress[2],
+			result.ExternalIPAddress[3],
+		)
+	}
+	return ip, err
+}

+ 13 - 0
vendor/github.com/AudriusButkevicius/go-nat-pmp/LICENSE

@@ -0,0 +1,13 @@
+   Copyright 2013 John Howard Palevich
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 48 - 0
vendor/github.com/AudriusButkevicius/go-nat-pmp/README.md

@@ -0,0 +1,48 @@
+go-nat-pmp
+==========
+
+A Go language client for the NAT-PMP internet protocol for port mapping and discovering the external
+IP address of a firewall.
+
+NAT-PMP is supported by Apple brand routers and open source routers like Tomato and DD-WRT.
+
+See http://tools.ietf.org/html/draft-cheshire-nat-pmp-03
+
+Get the package
+---------------
+
+    go get -u github.com/jackpal/go-nat-pmp
+
+Usage
+-----
+
+    import natpmp "github.com/jackpal/go-nat-pmp"
+
+    client := natpmp.NewClient(gatewayIP)
+    response, err := client.GetExternalAddress()
+    if err != nil {
+        return
+    }
+    print("External IP address:", response.ExternalIPAddress)
+
+Notes
+-----
+
+There doesn't seem to be an easy way to programmatically determine the address of the default gateway.
+(Linux and OSX have a "routes" kernel API that can be examined to get this information, but there is
+no Go package for getting this information.)
+
+Clients
+-------
+
+This library is used in the Taipei Torrent BitTorrent client http://github.com/jackpal/Taipei-Torrent
+
+Complete documentation
+----------------------
+
+    http://godoc.org/github.com/jackpal/go-nat-pmp
+
+License
+-------
+
+This project is licensed under the Apache License 2.0.

+ 189 - 0
vendor/github.com/AudriusButkevicius/go-nat-pmp/natpmp.go

@@ -0,0 +1,189 @@
+package natpmp
+
+import (
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/jackpal/gateway"
+)
+
+// Implement the NAT-PMP protocol, typically supported by Apple routers and open source
+// routers such as DD-WRT and Tomato.
+//
+// See http://tools.ietf.org/html/draft-cheshire-nat-pmp-03
+//
+// Usage:
+//
+//    client := natpmp.NewClient(gatewayIP)
+//    response, err := client.GetExternalAddress()
+
+const nAT_PMP_PORT = 5351
+
+// The recommended mapping lifetime for AddPortMapping
+const RECOMMENDED_MAPPING_LIFETIME_SECONDS = 3600
+
+// Client is a NAT-PMP protocol client.
+type Client struct {
+	gateway net.IP
+	timeout time.Duration
+}
+
+// Create a NAT-PMP client for the NAT-PMP server at the gateway.
+func NewClient(gateway net.IP, timeout time.Duration) (nat *Client) {
+	return &Client{gateway, timeout}
+}
+
+// Create a NAT-PMP client for the NAT-PMP server at the default gateway.
+func NewClientForDefaultGateway(timeout time.Duration) (nat *Client, err error) {
+	var g net.IP
+	g, err = gateway.DiscoverGateway()
+	if err != nil {
+		return
+	}
+	nat = NewClient(g, timeout)
+	return
+}
+
+// Results of the NAT-PMP GetExternalAddress operation
+type GetExternalAddressResult struct {
+	SecondsSinceStartOfEpoc uint32
+	ExternalIPAddress       [4]byte
+}
+
+// Get the external address of the router.
+func (n *Client) GetExternalAddress() (result *GetExternalAddressResult, err error) {
+	msg := make([]byte, 2)
+	msg[0] = 0 // Version 0
+	msg[1] = 0 // OP Code 0
+	response, err := n.rpc(msg, 12)
+	if err != nil {
+		return
+	}
+	result = &GetExternalAddressResult{}
+	result.SecondsSinceStartOfEpoc = readNetworkOrderUint32(response[4:8])
+	copy(result.ExternalIPAddress[:], response[8:12])
+	return
+}
+
+// Results of the NAT-PMP AddPortMapping operation
+type AddPortMappingResult struct {
+	SecondsSinceStartOfEpoc      uint32
+	InternalPort                 uint16
+	MappedExternalPort           uint16
+	PortMappingLifetimeInSeconds uint32
+}
+
+// Add (or delete) a port mapping. To delete a mapping, set the requestedExternalPort and lifetime to 0
+func (n *Client) AddPortMapping(protocol string, internalPort, requestedExternalPort int, lifetime int) (result *AddPortMappingResult, err error) {
+	var opcode byte
+	if protocol == "udp" {
+		opcode = 1
+	} else if protocol == "tcp" {
+		opcode = 2
+	} else {
+		err = fmt.Errorf("unknown protocol %v", protocol)
+		return
+	}
+	msg := make([]byte, 12)
+	msg[0] = 0 // Version 0
+	msg[1] = opcode
+	writeNetworkOrderUint16(msg[4:6], uint16(internalPort))
+	writeNetworkOrderUint16(msg[6:8], uint16(requestedExternalPort))
+	writeNetworkOrderUint32(msg[8:12], uint32(lifetime))
+	response, err := n.rpc(msg, 16)
+	if err != nil {
+		return
+	}
+	result = &AddPortMappingResult{}
+	result.SecondsSinceStartOfEpoc = readNetworkOrderUint32(response[4:8])
+	result.InternalPort = readNetworkOrderUint16(response[8:10])
+	result.MappedExternalPort = readNetworkOrderUint16(response[10:12])
+	result.PortMappingLifetimeInSeconds = readNetworkOrderUint32(response[12:16])
+	return
+}
+
+func (n *Client) rpc(msg []byte, resultSize int) (result []byte, err error) {
+	var server net.UDPAddr
+	server.IP = n.gateway
+	server.Port = nAT_PMP_PORT
+	conn, err := net.DialUDP("udp", nil, &server)
+	if err != nil {
+		return
+	}
+	defer conn.Close()
+
+	result = make([]byte, resultSize)
+
+	timeout := time.Now().Add(n.timeout)
+
+	err = conn.SetDeadline(timeout)
+	if err != nil {
+		return
+	}
+
+	var bytesRead int
+	var remoteAddr *net.UDPAddr
+	for time.Now().Before(timeout) {
+		_, err = conn.Write(msg)
+		if err != nil {
+			return
+		}
+
+		bytesRead, remoteAddr, err = conn.ReadFromUDP(result)
+		if err != nil {
+			if err.(net.Error).Timeout() {
+				continue
+			}
+			return
+		}
+
+		if !remoteAddr.IP.Equal(n.gateway) {
+			// Ignore this packet.
+			continue
+		}
+		if bytesRead != resultSize {
+			err = fmt.Errorf("unexpected result size %d, expected %d", bytesRead, resultSize)
+			return
+		}
+		if result[0] != 0 {
+			err = fmt.Errorf("unknown protocol version %d", result[0])
+			return
+		}
+		expectedOp := msg[1] | 0x80
+		if result[1] != expectedOp {
+			err = fmt.Errorf("Unexpected opcode %d. Expected %d", result[1], expectedOp)
+			return
+		}
+		resultCode := readNetworkOrderUint16(result[2:4])
+		if resultCode != 0 {
+			err = fmt.Errorf("Non-zero result code %d", resultCode)
+			return
+		}
+		// If we got here the RPC is good.
+		return
+	}
+
+	err = fmt.Errorf("Timed out trying to contact gateway")
+	return
+}
+
+func writeNetworkOrderUint16(buf []byte, d uint16) {
+	buf[0] = byte(d >> 8)
+	buf[1] = byte(d)
+}
+
+func writeNetworkOrderUint32(buf []byte, d uint32) {
+	buf[0] = byte(d >> 24)
+	buf[1] = byte(d >> 16)
+	buf[2] = byte(d >> 8)
+	buf[3] = byte(d)
+}
+
+func readNetworkOrderUint16(buf []byte) uint16 {
+	return (uint16(buf[0]) << 8) | uint16(buf[1])
+}
+
+func readNetworkOrderUint32(buf []byte) uint32 {
+	return (uint32(buf[0]) << 24) | (uint32(buf[1]) << 16) | (uint32(buf[2]) << 8) | uint32(buf[3])
+}

+ 13 - 0
vendor/github.com/AudriusButkevicius/go-nat-pmp/natpmp_test.go

@@ -0,0 +1,13 @@
+package natpmp
+
+import (
+	"testing"
+)
+
+func TestNatPMP(t *testing.T) {
+	client, err := NewClientForDefaultGateway()
+	if err != nil {
+		t.Errorf("NewClientForDefaultGateway() = %v,%v", client, err)
+		return
+	}
+}

+ 27 - 0
vendor/github.com/jackpal/gateway/LICENSE

@@ -0,0 +1,27 @@
+// Copyright (c) 2010 Jack Palevich. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 7 - 0
vendor/github.com/jackpal/gateway/README.md

@@ -0,0 +1,7 @@
+# gateway
+
+A very simple library for discovering the IP address of the local LAN gateway.
+
+Provides implementations for Linux, OS X (Darwin) and Windows.
+
+Pull requests for other OSs happily considered!

+ 40 - 0
vendor/github.com/jackpal/gateway/gateway_darwin.go

@@ -0,0 +1,40 @@
+package gateway
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net"
+	"os/exec"
+)
+
+func DiscoverGateway() (ip net.IP, err error) {
+	routeCmd := exec.Command("/sbin/route", "-n", "get", "0.0.0.0")
+	stdOut, err := routeCmd.StdoutPipe()
+	if err != nil {
+		return
+	}
+	if err = routeCmd.Start(); err != nil {
+		return
+	}
+	output, err := ioutil.ReadAll(stdOut)
+	if err != nil {
+		return
+	}
+
+	// Darwin route out format is always like this:
+	//    route to: default
+	// destination: default
+	//        mask: default
+	//     gateway: 192.168.1.1
+	outputLines := bytes.Split(output, []byte("\n"))
+	for _, line := range outputLines {
+		if bytes.Contains(line, []byte("gateway:")) {
+			gatewayFields := bytes.Fields(line)
+			ip = net.ParseIP(string(gatewayFields[1]))
+			break
+		}
+	}
+
+	err = routeCmd.Wait()
+	return
+}

+ 75 - 0
vendor/github.com/jackpal/gateway/gateway_linux.go

@@ -0,0 +1,75 @@
+package gateway
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net"
+	"os/exec"
+)
+
+func discoverGatewayUsingIp() (ip net.IP, err error) {
+	routeCmd := exec.Command("ip", "route", "show")
+	stdOut, err := routeCmd.StdoutPipe()
+	if err != nil {
+		return
+	}
+	if err = routeCmd.Start(); err != nil {
+		return
+	}
+	output, err := ioutil.ReadAll(stdOut)
+	if err != nil {
+		return
+	}
+
+	// Linux 'ip route show' format looks like this:
+	// default via 192.168.178.1 dev wlp3s0  metric 303
+	// 192.168.178.0/24 dev wlp3s0  proto kernel  scope link  src 192.168.178.76  metric 303
+	outputLines := bytes.Split(output, []byte("\n"))
+	for _, line := range outputLines {
+		if bytes.Contains(line, []byte("default")) {
+			ipFields := bytes.Fields(line)
+			ip = net.ParseIP(string(ipFields[2]))
+			break
+		}
+	}
+	err = routeCmd.Wait()
+	return
+}
+
+func discoverGatewayUsingRoute() (ip net.IP, err error) {
+	routeCmd := exec.Command("route", "-n")
+	stdOut, err := routeCmd.StdoutPipe()
+	if err != nil {
+		return
+	}
+	if err = routeCmd.Start(); err != nil {
+		return
+	}
+	output, err := ioutil.ReadAll(stdOut)
+	if err != nil {
+		return
+	}
+
+	// Linux route out format is always like this:
+	// Kernel IP routing table
+	// Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
+	// 0.0.0.0         192.168.1.1     0.0.0.0         UG    0      0        0 eth0
+	outputLines := bytes.Split(output, []byte("\n"))
+	for _, line := range outputLines {
+		if bytes.Contains(line, []byte("0.0.0.0")) {
+			ipFields := bytes.Fields(line)
+			ip = net.ParseIP(string(ipFields[1]))
+			break
+		}
+	}
+	err = routeCmd.Wait()
+	return
+}
+
+func DiscoverGateway() (ip net.IP, err error) {
+	ip, err = discoverGatewayUsingRoute()
+	if err != nil {
+		ip, err = discoverGatewayUsingIp()
+	}
+	return
+}

+ 10 - 0
vendor/github.com/jackpal/gateway/gateway_test.go

@@ -0,0 +1,10 @@
+package gateway
+
+import "testing"
+
+func TestGateway(t *testing.T) {
+	ip, err := DiscoverGateway()
+	if err != nil {
+		t.Errorf("DiscoverGateway() = %v,%v", ip, err)
+	}
+}

+ 14 - 0
vendor/github.com/jackpal/gateway/gateway_unimplemented.go

@@ -0,0 +1,14 @@
+// +build !darwin,!linux,!windows
+
+package gateway
+
+import (
+	"fmt"
+	"net"
+	"runtime"
+)
+
+func DiscoverGateway() (ip net.IP, err error) {
+	err = fmt.Errorf("DiscoverGateway not implemented for OS %s", runtime.GOOS)
+	return
+}

+ 43 - 0
vendor/github.com/jackpal/gateway/gateway_windows.go

@@ -0,0 +1,43 @@
+package gateway
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net"
+	"os/exec"
+)
+
+func DiscoverGateway() (ip net.IP, err error) {
+	routeCmd := exec.Command("route", "print", "0.0.0.0")
+	stdOut, err := routeCmd.StdoutPipe()
+	if err != nil {
+		return
+	}
+	if err = routeCmd.Start(); err != nil {
+		return
+	}
+	output, err := ioutil.ReadAll(stdOut)
+	if err != nil {
+		return
+	}
+
+	// Windows route output format is always like this:
+	// ===========================================================================
+	// Active Routes:
+	// Network Destination        Netmask          Gateway       Interface  Metric
+	//           0.0.0.0          0.0.0.0      192.168.1.1    192.168.1.100     20
+	// ===========================================================================
+	// I'm trying to pick the active route,
+	// then jump 2 lines and pick the third IP
+	// Not using regex because output is quite standard from Windows XP to 8 (NEEDS TESTING)
+	outputLines := bytes.Split(output, []byte("\n"))
+	for idx, line := range outputLines {
+		if bytes.Contains(line, []byte("Active Routes:")) {
+			ipFields := bytes.Fields(outputLines[idx+2])
+			ip = net.ParseIP(string(ipFields[2]))
+			break
+		}
+	}
+	err = routeCmd.Wait()
+	return
+}

+ 12 - 0
vendor/manifest

@@ -1,6 +1,12 @@
 {
 	"version": 0,
 	"dependencies": [
+		{
+			"importpath": "github.com/AudriusButkevicius/go-nat-pmp",
+			"repository": "https://github.com/AudriusButkevicius/go-nat-pmp",
+			"revision": "88a8019a0eff7e9db55f458230b867f0d7e5d48f",
+			"branch": "master"
+		},
 		{
 			"importpath": "github.com/bkaradzic/go-lz4",
 			"repository": "https://github.com/bkaradzic/go-lz4",
@@ -43,6 +49,12 @@
 			"revision": "5f1c01d9f64b941dd9582c638279d046eda6ca31",
 			"branch": "master"
 		},
+		{
+			"importpath": "github.com/jackpal/gateway",
+			"repository": "https://github.com/jackpal/gateway",
+			"revision": "32194371ec3f370166ee10a5ee079206532fdd74",
+			"branch": "master"
+		},
 		{
 			"importpath": "github.com/juju/ratelimit",
 			"repository": "https://github.com/juju/ratelimit",