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

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2968
This commit is contained in:
Audrius Butkevicius 2016-04-13 18:50:40 +00:00 committed by Jakob Borg
parent 52c7804f32
commit c49453c519
16 changed files with 650 additions and 6 deletions

View File

@ -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"
)

View File

@ -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
lib/pmp/debug.go Normal file
View File

@ -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
lib/pmp/pmp.go Normal file
View File

@ -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
}

View File

@ -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.

View File

@ -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.

View File

@ -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])
}

View File

@ -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
vendor/github.com/jackpal/gateway/LICENSE generated vendored Normal file
View File

@ -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
vendor/github.com/jackpal/gateway/README.md generated vendored Normal file
View File

@ -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
vendor/github.com/jackpal/gateway/gateway_darwin.go generated vendored Normal file
View File

@ -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
vendor/github.com/jackpal/gateway/gateway_linux.go generated vendored Normal file
View File

@ -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
vendor/github.com/jackpal/gateway/gateway_test.go generated vendored Normal file
View File

@ -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)
}
}

View File

@ -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
vendor/github.com/jackpal/gateway/gateway_windows.go generated vendored Normal file
View File

@ -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
vendor/manifest vendored
View File

@ -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",