From 9a3dfaf332f63fbdc88ee9b389f7dcb075e0d84a Mon Sep 17 00:00:00 2001 From: shtirlic Date: Thu, 16 Nov 2017 19:15:09 +0200 Subject: [PATCH 01/13] Merge changes ontop of master --- lnd.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lnd.go b/lnd.go index edae743a1..88a6da798 100644 --- a/lnd.go +++ b/lnd.go @@ -26,6 +26,8 @@ import ( "sync" "time" + "github.com/NebulousLabs/go-upnp" + "gopkg.in/macaroon-bakery.v2/bakery" "golang.org/x/net/context" @@ -310,6 +312,29 @@ func lndMain() error { "is proxying over Tor as well", cfg.Tor.StreamIsolation) } + // Connect to router + d, err := upnp.Discover() + if err != nil { + fmt.Printf("Unable to discover router %v\n", err) + return err + } + + // Get external IP + ip, err := d.ExternalIP() + if err != nil { + fmt.Printf("Unable to get external ip %v\n", err) + return err + } + + ltndLog.Infof("Your external IP is: %s", ip) + + // Forward peer port + err = d.Forward(uint16(cfg.PeerPort), "lnd pear port") + if err != nil { + fmt.Printf("Unable to forward pear port ip %v\n", err) + return err + } + // Set up the core server which will listen for incoming peer // connections. server, err := newServer( From b59077109ca02191b46e8196625144c173e77bd0 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sun, 24 Dec 2017 23:15:40 +0000 Subject: [PATCH 02/13] Added Upnp support behind a configuration option, moved the logic to the server code --- config.go | 3 +++ lnd.go | 25 ------------------------- server.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/config.go b/config.go index 091a7f4d1..55960acf9 100644 --- a/config.go +++ b/config.go @@ -47,6 +47,7 @@ const ( defaultRPCHost = "localhost" defaultMaxPendingChannels = 1 defaultNoEncryptWallet = false + defaultUpnpSupport = false defaultTrickleDelay = 30 * 1000 defaultMaxLogFiles = 3 defaultMaxLogFileSize = 10 @@ -195,6 +196,7 @@ type config struct { UnsafeDisconnect bool `long:"unsafe-disconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels. USED FOR TESTING ONLY."` UnsafeReplay bool `long:"unsafe-replay" description:"Causes a link to replay the adds on its commitment txn after starting up, this enables testing of the sphinx replay logic."` MaxPendingChannels int `long:"maxpendingchannels" description:"The maximum number of incoming pending channels permitted per peer."` + UpnpSupport bool `long:"upnpsupport" description:"Toggle Upnp support for auto network discovery"` Bitcoin *chainConfig `group:"Bitcoin" namespace:"bitcoin"` BtcdMode *btcdConfig `group:"btcd" namespace:"btcd"` @@ -248,6 +250,7 @@ func loadConfig() (*config, error) { LogDir: defaultLogDir, MaxLogFiles: defaultMaxLogFiles, MaxLogFileSize: defaultMaxLogFileSize, + UpnpSupport: defaultUpnpSupport, Bitcoin: &chainConfig{ MinHTLC: defaultBitcoinMinHTLCMSat, BaseFee: defaultBitcoinBaseFeeMSat, diff --git a/lnd.go b/lnd.go index 88a6da798..edae743a1 100644 --- a/lnd.go +++ b/lnd.go @@ -26,8 +26,6 @@ import ( "sync" "time" - "github.com/NebulousLabs/go-upnp" - "gopkg.in/macaroon-bakery.v2/bakery" "golang.org/x/net/context" @@ -312,29 +310,6 @@ func lndMain() error { "is proxying over Tor as well", cfg.Tor.StreamIsolation) } - // Connect to router - d, err := upnp.Discover() - if err != nil { - fmt.Printf("Unable to discover router %v\n", err) - return err - } - - // Get external IP - ip, err := d.ExternalIP() - if err != nil { - fmt.Printf("Unable to get external ip %v\n", err) - return err - } - - ltndLog.Infof("Your external IP is: %s", ip) - - // Forward peer port - err = d.Forward(uint16(cfg.PeerPort), "lnd pear port") - if err != nil { - fmt.Printf("Unable to forward pear port ip %v\n", err) - return err - } - // Set up the core server which will listen for incoming peer // connections. server, err := newServer( diff --git a/server.go b/server.go index 5f0becd3f..817d591bf 100644 --- a/server.go +++ b/server.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -15,6 +16,7 @@ import ( "sync/atomic" "time" + upnp "github.com/NebulousLabs/go-upnp" "github.com/coreos/bbolt" "github.com/go-errors/errors" "github.com/lightningnetwork/lightning-onion" @@ -300,6 +302,38 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, return nil, err } + // Gather external IPs from config + externalIPs := cfg.ExternalIPs + + if cfg.UpnpSupport { + + // Connect to router + d, err := upnp.DiscoverCtx(context.Background()) + if err != nil { + fmt.Printf("Upnp: Unable to discover router %v\n", err) + return nil, err + } + + // Get external IP + ip, err := d.ExternalIP() + if err != nil { + fmt.Printf("Upnp: Unable to get external ip %v\n", err) + return nil, err + } + + ltndLog.Infof("Your external IP is: %s", ip) + + // Forward peer port + err = d.Forward(uint16(cfg.PeerPort), "lnd peer port") + if err != nil { + fmt.Printf("Upnp: Unable to forward pear port ip %v\n", err) + return nil, err + } + + externalIPs = append(externalIPs, ip) + + } + // If external IP addresses have been specified, add those to the list // of this server's addresses. selfAddrs := make([]net.Addr, 0, len(cfg.ExternalIPs)) From 0d0078baac6a7e8326dd53300a821d7e9b5c3654 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sat, 20 Jan 2018 17:59:43 +0000 Subject: [PATCH 03/13] Continue with server startup, even if UPNP has failed --- server.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/server.go b/server.go index 817d591bf..1ac44279a 100644 --- a/server.go +++ b/server.go @@ -310,28 +310,24 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, // Connect to router d, err := upnp.DiscoverCtx(context.Background()) if err != nil { - fmt.Printf("Upnp: Unable to discover router %v\n", err) - return nil, err + srvrLog.Errorf("Upnp: Unable to discover router %v\n", err) } // Get external IP ip, err := d.ExternalIP() if err != nil { - fmt.Printf("Upnp: Unable to get external ip %v\n", err) - return nil, err + srvrLog.Errorf("Upnp: Unable to get external ip %v\n", err) } - ltndLog.Infof("Your external IP is: %s", ip) - // Forward peer port err = d.Forward(uint16(cfg.PeerPort), "lnd peer port") if err != nil { - fmt.Printf("Upnp: Unable to forward pear port ip %v\n", err) - return nil, err + srvrLog.Errorf("Upnp: Unable to forward pear port ip %v\n", err) + } else { + srvrLog.Infof("Your external IP is: %s", ip) + externalIPs = append(externalIPs, ip) } - externalIPs = append(externalIPs, ip) - } // If external IP addresses have been specified, add those to the list From 1c3df21d189fd12fb2f0f95506050fe53157c173 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sat, 20 Jan 2018 18:58:06 +0000 Subject: [PATCH 04/13] Added NatPmp option --- config.go | 3 ++- server.go | 23 +++++++---------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/config.go b/config.go index 55960acf9..05ebdf9be 100644 --- a/config.go +++ b/config.go @@ -196,7 +196,8 @@ type config struct { UnsafeDisconnect bool `long:"unsafe-disconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels. USED FOR TESTING ONLY."` UnsafeReplay bool `long:"unsafe-replay" description:"Causes a link to replay the adds on its commitment txn after starting up, this enables testing of the sphinx replay logic."` MaxPendingChannels int `long:"maxpendingchannels" description:"The maximum number of incoming pending channels permitted per peer."` - UpnpSupport bool `long:"upnpsupport" description:"Toggle Upnp support for auto network discovery"` + UpnpSupport bool `long:"upnp" description:"Toggle Upnp support for auto network discovery"` + NatPmp bool `long:"natpmp" description:"Toggle Nat Pmp support for auto network discovery"` Bitcoin *chainConfig `group:"Bitcoin" namespace:"bitcoin"` BtcdMode *btcdConfig `group:"btcd" namespace:"btcd"` diff --git a/server.go b/server.go index 1ac44279a..2db829092 100644 --- a/server.go +++ b/server.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "context" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -16,7 +15,6 @@ import ( "sync/atomic" "time" - upnp "github.com/NebulousLabs/go-upnp" "github.com/coreos/bbolt" "github.com/go-errors/errors" "github.com/lightningnetwork/lightning-onion" @@ -307,25 +305,18 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, if cfg.UpnpSupport { - // Connect to router - d, err := upnp.DiscoverCtx(context.Background()) + externalIP, err := configureUpnp() if err != nil { - srvrLog.Errorf("Upnp: Unable to discover router %v\n", err) + externalIPs = append(externalIPs, externalIP) } - // Get external IP - ip, err := d.ExternalIP() - if err != nil { - srvrLog.Errorf("Upnp: Unable to get external ip %v\n", err) - } + } - // Forward peer port - err = d.Forward(uint16(cfg.PeerPort), "lnd peer port") + if cfg.NatPmp { + + externalIP, err := configureNatPmp() if err != nil { - srvrLog.Errorf("Upnp: Unable to forward pear port ip %v\n", err) - } else { - srvrLog.Infof("Your external IP is: %s", ip) - externalIPs = append(externalIPs, ip) + externalIPs = append(externalIPs, externalIP) } } From ba0db4ce91ddcbd8069f2361ececc5343a404fec Mon Sep 17 00:00:00 2001 From: John Griffith Date: Sun, 21 Jan 2018 17:22:13 +0000 Subject: [PATCH 05/13] Added some additional comments --- config.go | 6 ++++++ server.go | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 05ebdf9be..24fdc4097 100644 --- a/config.go +++ b/config.go @@ -725,6 +725,12 @@ func loadConfig() (*config, error) { ) } + if cfg.UpnpSupport && cfg.NatPmp { + str := "%s: Currently both Upnp and NAT-PMP cannot be " + + "enabled together" + return nil, fmt.Errorf(str, funcName) + } + // Append the network type to the log directory so it is "namespaced" // per network in the same fashion as the data directory. cfg.LogDir = filepath.Join(cfg.LogDir, diff --git a/server.go b/server.go index 2db829092..d8376ce96 100644 --- a/server.go +++ b/server.go @@ -303,10 +303,12 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, // Gather external IPs from config externalIPs := cfg.ExternalIPs + // If enabled, use either UPnP or NAT-PMP to automatically configure + // port forwarding for users behind a NAT if cfg.UpnpSupport { externalIP, err := configureUpnp() - if err != nil { + if err == nil { externalIPs = append(externalIPs, externalIP) } @@ -315,7 +317,7 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, if cfg.NatPmp { externalIP, err := configureNatPmp() - if err != nil { + if err == nil { externalIPs = append(externalIPs, externalIP) } From d17a561d8be7525dff7cb448c84b8f63aba8b99f Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 16 Apr 2018 18:17:15 -0400 Subject: [PATCH 06/13] build: include NAT traversal related dependencies --- Gopkg.lock | 47 ++++++++++++++++++++++++++++++++++++++++++++++- Gopkg.toml | 8 ++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Gopkg.lock b/Gopkg.lock index d91217764..883bf22d2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -12,6 +12,25 @@ ] revision = "e06297f34865a50b8e473105e52cb64ad1b55da8" +[[projects]] + branch = "master" + name = "github.com/NebulousLabs/fastrand" + packages = ["."] + revision = "3cf7173006a0b7d2371fa1a220da7f9d48c7827c" + +[[projects]] + name = "github.com/NebulousLabs/go-upnp" + packages = [ + ".", + "goupnp", + "goupnp/dcps/internetgateway1", + "goupnp/httpu", + "goupnp/scpd", + "goupnp/soap", + "goupnp/ssdp" + ] + revision = "29b680b06c82d044ebea91bf3069038eb562df2a" + [[projects]] name = "github.com/Yawning/aez" packages = ["."] @@ -115,6 +134,17 @@ ] revision = "f2862b476edcef83412c7af8687c9cd8e4097c0f" +[[projects]] + name = "github.com/jackpal/gateway" + packages = ["."] + revision = "3e333950771011fed13be63e62b9f473c5e0d9bf" + version = "v1.0.4" + +[[projects]] + name = "github.com/jackpal/go-nat-pmp" + packages = ["."] + revision = "28a68d0c24adce1da43f8df6a57340909ecd7fdd" + [[projects]] name = "github.com/jessevdk/go-flags" packages = ["."] @@ -267,6 +297,9 @@ name = "golang.org/x/net" packages = [ "context", + "html", + "html/atom", + "html/charset", "http2", "http2/hpack", "idna", @@ -291,12 +324,24 @@ packages = [ "collate", "collate/build", + "encoding", + "encoding/charmap", + "encoding/htmlindex", + "encoding/internal", + "encoding/internal/identifier", + "encoding/japanese", + "encoding/korean", + "encoding/simplifiedchinese", + "encoding/traditionalchinese", + "encoding/unicode", "internal/colltab", "internal/gen", "internal/tag", "internal/triegen", "internal/ucd", + "internal/utf8internal", "language", + "runes", "secure/bidirule", "transform", "unicode/bidi", @@ -359,6 +404,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "2133b0035a81c856475302a127bc26d30217a30d1e41708d3604f2de82e1ab31" + inputs-digest = "dc40dd185a90b723e4b3df4a077b4ad4f99648260661cac9d58121e8bd3474ef" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index b5a2f6ddf..2d56993a2 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -26,6 +26,10 @@ name = "github.com/grpc-ecosystem/grpc-gateway" revision = "f2862b476edcef83412c7af8687c9cd8e4097c0f" +[[constraint]] + name = "github.com/jackpal/go-nat-pmp" + revision = "28a68d0c24adce1da43f8df6a57340909ecd7fdd" + [[constraint]] name = "github.com/jessevdk/go-flags" revision = "f88afde2fa19a30cf50ba4b05b3d13bc6bae3079" @@ -54,6 +58,10 @@ name = "github.com/miekg/dns" revision = "79bfde677fa81ff8d27c4330c35bda075d360641" +[[constraint]] + name = "github.com/NebulousLabs/go-upnp" + revision = "29b680b06c82d044ebea91bf3069038eb562df2a" + [[constraint]] name = "github.com/roasbeef/btcutil" revision = "dfb640c57141f1c2113b92b4b16d2a89c30dd258" From 5732f5a1c44cc32ab8c4aced2afb5e0623d6b88b Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 23 Apr 2018 12:42:17 -0400 Subject: [PATCH 07/13] config: normalize external IPs when parsing config --- config.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/config.go b/config.go index 24fdc4097..0f1c2566e 100644 --- a/config.go +++ b/config.go @@ -790,18 +790,27 @@ func loadConfig() (*config, error) { // Add default port to all RPC listener addresses if needed and remove // duplicate addresses. - cfg.RPCListeners = normalizeAddresses(cfg.RPCListeners, - strconv.Itoa(defaultRPCPort)) + cfg.RPCListeners = normalizeAddresses( + cfg.RPCListeners, strconv.Itoa(defaultRPCPort), + ) // Add default port to all REST listener addresses if needed and remove // duplicate addresses. - cfg.RESTListeners = normalizeAddresses(cfg.RESTListeners, - strconv.Itoa(defaultRESTPort)) + cfg.RESTListeners = normalizeAddresses( + cfg.RESTListeners, strconv.Itoa(defaultRESTPort), + ) // Add default port to all listener addresses if needed and remove // duplicate addresses. - cfg.Listeners = normalizeAddresses(cfg.Listeners, - strconv.Itoa(defaultPeerPort)) + cfg.Listeners = normalizeAddresses( + cfg.Listeners, strconv.Itoa(defaultPeerPort), + ) + + // Add default port to all external IP addresses if needed and remove + // duplicate addresses. + cfg.ExternalIPs = normalizeAddresses( + cfg.ExternalIPs, strconv.Itoa(defaultPeerPort), + ) // Finally, ensure that we are only listening on localhost if Tor // inbound support is enabled. From 8651b4a422dc77ec953d29f81b8b42232480437e Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 18 Apr 2018 01:37:34 -0400 Subject: [PATCH 08/13] config: clarify documentation for NAT traversal flag --- config.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/config.go b/config.go index 0f1c2566e..281186610 100644 --- a/config.go +++ b/config.go @@ -47,7 +47,6 @@ const ( defaultRPCHost = "localhost" defaultMaxPendingChannels = 1 defaultNoEncryptWallet = false - defaultUpnpSupport = false defaultTrickleDelay = 30 * 1000 defaultMaxLogFiles = 3 defaultMaxLogFileSize = 10 @@ -185,6 +184,7 @@ type config struct { Listeners []string `long:"listen" description:"Add an interface/port to listen for peer connections"` DisableListen bool `long:"nolisten" description:"Disable listening for incoming peer connections"` ExternalIPs []string `long:"externalip" description:"Add an ip:port to the list of local addresses we claim to listen on to peers. If a port is not specified, the default (9735) will be used regardless of other parameters"` + NAT bool `long:"nat" description:"Toggle NAT traversal support (using either UPnP or NAT-PMP) to automatically advertise your external IP address to the network -- NOTE this does not support devices behind multiple NATs"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` @@ -196,8 +196,6 @@ type config struct { UnsafeDisconnect bool `long:"unsafe-disconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels. USED FOR TESTING ONLY."` UnsafeReplay bool `long:"unsafe-replay" description:"Causes a link to replay the adds on its commitment txn after starting up, this enables testing of the sphinx replay logic."` MaxPendingChannels int `long:"maxpendingchannels" description:"The maximum number of incoming pending channels permitted per peer."` - UpnpSupport bool `long:"upnp" description:"Toggle Upnp support for auto network discovery"` - NatPmp bool `long:"natpmp" description:"Toggle Nat Pmp support for auto network discovery"` Bitcoin *chainConfig `group:"Bitcoin" namespace:"bitcoin"` BtcdMode *btcdConfig `group:"btcd" namespace:"btcd"` @@ -251,7 +249,6 @@ func loadConfig() (*config, error) { LogDir: defaultLogDir, MaxLogFiles: defaultMaxLogFiles, MaxLogFileSize: defaultMaxLogFileSize, - UpnpSupport: defaultUpnpSupport, Bitcoin: &chainConfig{ MinHTLC: defaultBitcoinMinHTLCMSat, BaseFee: defaultBitcoinBaseFeeMSat, @@ -459,6 +456,11 @@ func loadConfig() (*config, error) { } } + if cfg.DisableListen && cfg.NAT { + return nil, errors.New("NAT traversal cannot be used when " + + "listening is disabled") + } + // Determine the active chain configuration and its parameters. switch { // At this moment, multiple active chains are not supported. @@ -725,12 +727,6 @@ func loadConfig() (*config, error) { ) } - if cfg.UpnpSupport && cfg.NatPmp { - str := "%s: Currently both Upnp and NAT-PMP cannot be " + - "enabled together" - return nil, fmt.Errorf(str, funcName) - } - // Append the network type to the log directory so it is "namespaced" // per network in the same fashion as the data directory. cfg.LogDir = filepath.Join(cfg.LogDir, @@ -782,10 +778,11 @@ func loadConfig() (*config, error) { return nil, err } - // Remove all Listeners if listening is disabled. + // Remove the listening addresses specified if listening is disabled. if cfg.DisableListen { ltndLog.Infof("Listening on the p2p interface is disabled!") cfg.Listeners = nil + cfg.ExternalIPs = nil } // Add default port to all RPC listener addresses if needed and remove From 377e770db43344fc67ca9b23935bc53cf5f0255d Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Thu, 19 Apr 2018 23:34:47 -0400 Subject: [PATCH 09/13] nat: introduce new NAT traversal package with UPnP/NAT-PMP implementations In this commit, we introduce a new package `nat`. This package is responsible for handling the different techniques for NAT traversals. Specifically, we have implemented UPnP and NAT-PMP support. This will allow users to easily advertise their nodes to the network as long as their devices are behind a single NAT. Devices behind multiple NATs are not supported. --- nat/pmp.go | 117 +++++++++++++++++++++++++++++++++++++++++++++++ nat/traversal.go | 58 +++++++++++++++++++++++ nat/upnp.go | 112 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 nat/pmp.go create mode 100644 nat/traversal.go create mode 100644 nat/upnp.go diff --git a/nat/pmp.go b/nat/pmp.go new file mode 100644 index 000000000..f35ce5c01 --- /dev/null +++ b/nat/pmp.go @@ -0,0 +1,117 @@ +package nat + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/jackpal/gateway" + natpmp "github.com/jackpal/go-nat-pmp" +) + +// Compile-time check to ensure PMP implements the Traversal interface. +var _ Traversal = (*PMP)(nil) + +// PMP is a concrete implementation of the Traversal interface that uses the +// NAT-PMP technique. +type PMP struct { + client *natpmp.Client + + forwardedPortsMtx sync.Mutex + forwardedPorts map[uint16]struct{} +} + +// DiscoverPMP attempts to scan the local network for a NAT-PMP enabled device +// within the given timeout. +func DiscoverPMP(timeout time.Duration) (*PMP, error) { + // Retrieve the gateway IP address of the local network. + gatewayIP, err := gateway.DiscoverGateway() + if err != nil { + return nil, err + } + + pmp := &PMP{ + client: natpmp.NewClientWithTimeout(gatewayIP, timeout), + forwardedPorts: make(map[uint16]struct{}), + } + + // We'll then attempt to retrieve the external IP address of this + // device to ensure it is not behind multiple NATs. + if _, err := pmp.ExternalIP(); err != nil { + return nil, err + } + + return pmp, nil +} + +// ExternalIP returns the external IP address of the NAT-PMP enabled device. +func (p *PMP) ExternalIP() (net.IP, error) { + res, err := p.client.GetExternalAddress() + if err != nil { + return nil, err + } + + ip := net.IP(res.ExternalIPAddress[:]) + if isPrivateIP(ip) { + return nil, ErrMultipleNAT + } + + return ip, nil +} + +// AddPortMapping enables port forwarding for the given port. +func (p *PMP) AddPortMapping(port uint16) error { + p.forwardedPortsMtx.Lock() + defer p.forwardedPortsMtx.Unlock() + + if _, exists := p.forwardedPorts[port]; exists { + return nil + } + + _, err := p.client.AddPortMapping("tcp", int(port), int(port), 0) + if err != nil { + return err + } + + p.forwardedPorts[port] = struct{}{} + + return nil +} + +// DeletePortMapping disables port forwarding for the given port. +func (p *PMP) DeletePortMapping(port uint16) error { + p.forwardedPortsMtx.Lock() + defer p.forwardedPortsMtx.Unlock() + + if _, exists := p.forwardedPorts[port]; !exists { + return fmt.Errorf("port %d is not being forwarded", port) + } + + _, err := p.client.AddPortMapping("tcp", int(port), 0, 0) + if err != nil { + return err + } + + delete(p.forwardedPorts, port) + + return nil +} + +// ForwardedPorts returns a list of ports currently being forwarded. +func (p *PMP) ForwardedPorts() []uint16 { + p.forwardedPortsMtx.Lock() + defer p.forwardedPortsMtx.Unlock() + + ports := make([]uint16, 0, len(p.forwardedPorts)) + for port := range p.forwardedPorts { + ports = append(ports, port) + } + + return ports +} + +// Name returns the name of the specific NAT traversal technique used. +func (p *PMP) Name() string { + return "NAT-PMP" +} diff --git a/nat/traversal.go b/nat/traversal.go new file mode 100644 index 000000000..852a28cb6 --- /dev/null +++ b/nat/traversal.go @@ -0,0 +1,58 @@ +package nat + +import ( + "errors" + "net" +) + +var ( + // private24BitBlock contains the set of private IPv4 addresses within + // the 10.0.0.0/8 adddress space. + private24BitBlock *net.IPNet + + // private20BitBlock contains the set of private IPv4 addresses within + // the 172.16.0.0/12 address space. + private20BitBlock *net.IPNet + + // private16BitBlock contains the set of private IPv4 addresses within + // the 192.168.0.0/16 address space. + private16BitBlock *net.IPNet + + // ErrMultipleNAT is an error returned when multiple NATs have been + // detected. + ErrMultipleNAT = errors.New("multiple NATs detected") +) + +func init() { + _, private24BitBlock, _ = net.ParseCIDR("10.0.0.0/8") + _, private20BitBlock, _ = net.ParseCIDR("172.16.0.0/12") + _, private16BitBlock, _ = net.ParseCIDR("192.168.0.0/16") +} + +// Traversal is an interface that brings together the different NAT traversal +// techniques. +type Traversal interface { + // ExternalIP returns the external IP address. + ExternalIP() (net.IP, error) + + // AddPortMapping adds a port mapping for the given port between the + // private and public addresses. + AddPortMapping(port uint16) error + + // DeletePortMapping deletes a port mapping for the given port between + // the private and public addresses. + DeletePortMapping(port uint16) error + + // ForwardedPorts returns the ports currently being forwarded using NAT + // traversal. + ForwardedPorts() []uint16 + + // Name returns the name of the specific NAT traversal technique used. + Name() string +} + +// isPrivateIP determines if the IP is private. +func isPrivateIP(ip net.IP) bool { + return private24BitBlock.Contains(ip) || + private20BitBlock.Contains(ip) || private16BitBlock.Contains(ip) +} diff --git a/nat/upnp.go b/nat/upnp.go new file mode 100644 index 000000000..4022456a2 --- /dev/null +++ b/nat/upnp.go @@ -0,0 +1,112 @@ +package nat + +import ( + "context" + "fmt" + "net" + "sync" + + upnp "github.com/NebulousLabs/go-upnp" +) + +// Compile-time check to ensure UPnP implements the Traversal interface. +var _ Traversal = (*UPnP)(nil) + +// UPnP is a concrete implementation of the Traversal interface that uses the +// UPnP technique. +type UPnP struct { + device *upnp.IGD + + forwardedPortsMtx sync.Mutex + forwardedPorts map[uint16]struct{} +} + +// DiscoverUPnP scans the local network for a UPnP enabled device. +func DiscoverUPnP(ctx context.Context) (*UPnP, error) { + // Scan the local network for a UPnP-enabled device. + device, err := upnp.DiscoverCtx(ctx) + if err != nil { + return nil, err + } + + u := &UPnP{ + device: device, + forwardedPorts: make(map[uint16]struct{}), + } + + // We'll then attempt to retrieve the external IP address of this + // device to ensure it is not behind multiple NATs. + if _, err := u.ExternalIP(); err != nil { + return nil, err + } + + return u, nil +} + +// ExternalIP returns the external IP address of the UPnP enabled device. +func (u *UPnP) ExternalIP() (net.IP, error) { + ip, err := u.device.ExternalIP() + if err != nil { + return nil, err + } + + if isPrivateIP(net.ParseIP(ip)) { + return nil, ErrMultipleNAT + } + + return net.ParseIP(ip), nil +} + +// AddPortMapping enables port forwarding for the given port. +func (u *UPnP) AddPortMapping(port uint16) error { + u.forwardedPortsMtx.Lock() + defer u.forwardedPortsMtx.Unlock() + + if _, exists := u.forwardedPorts[port]; exists { + return nil + } + + if err := u.device.Forward(port, ""); err != nil { + return err + } + + u.forwardedPorts[port] = struct{}{} + + return nil +} + +// DeletePortMapping disables port forwarding for the given port. +func (u *UPnP) DeletePortMapping(port uint16) error { + u.forwardedPortsMtx.Lock() + defer u.forwardedPortsMtx.Unlock() + + if _, exists := u.forwardedPorts[port]; !exists { + return fmt.Errorf("port %d is not being forwarded", port) + } + + if err := u.device.Clear(port); err != nil { + return err + } + + delete(u.forwardedPorts, port) + + return nil +} + +// ForwardedPorts returns a list of ports currently being forwarded. +func (u *UPnP) ForwardedPorts() []uint16 { + u.forwardedPortsMtx.Lock() + defer u.forwardedPortsMtx.Unlock() + + ports := make([]uint16, 0, len(u.forwardedPorts)) + for port := range u.forwardedPorts { + ports = append(ports, port) + } + + return ports +} + +// Name returns the name of the specific NAT traversal technique used. +func (u *UPnP) Name() string { + return "UPnP" +} From d63599427279a449f866363419a6a660eb898dd3 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 23 Apr 2018 12:44:14 -0400 Subject: [PATCH 10/13] lnwire: add functional option that updates a node announcement's addresses --- lnwire/node_announcement.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lnwire/node_announcement.go b/lnwire/node_announcement.go index 753fca588..3a0b7ff9b 100644 --- a/lnwire/node_announcement.go +++ b/lnwire/node_announcement.go @@ -85,6 +85,14 @@ type NodeAnnouncement struct { Addresses []net.Addr } +// UpdateNodeAnnAddrs is a functional option that allows updating the addresses +// of the given node announcement. +func UpdateNodeAnnAddrs(addrs []net.Addr) func(*NodeAnnouncement) { + return func(nodeAnn *NodeAnnouncement) { + nodeAnn.Addresses = addrs + } +} + // A compile time check to ensure NodeAnnouncement implements the // lnwire.Message interface. var _ Message = (*NodeAnnouncement)(nil) From 2ad98239456f190593c5b79684e991ed4dbed0d4 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 23 Apr 2018 12:43:50 -0400 Subject: [PATCH 11/13] server: include optional updates when generating a new node announcement --- server.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server.go b/server.go index d8376ce96..8b04d332f 100644 --- a/server.go +++ b/server.go @@ -987,8 +987,8 @@ func (s *server) initTorController() error { // genNodeAnnouncement generates and returns the current fully signed node // announcement. If refresh is true, then the time stamp of the announcement // will be updated in order to ensure it propagates through the network. -func (s *server) genNodeAnnouncement( - refresh bool) (lnwire.NodeAnnouncement, error) { +func (s *server) genNodeAnnouncement(refresh bool, + updates ...func(*lnwire.NodeAnnouncement)) (lnwire.NodeAnnouncement, error) { s.mu.Lock() defer s.mu.Unlock() @@ -997,7 +997,9 @@ func (s *server) genNodeAnnouncement( return *s.currentNodeAnn, nil } - var err error + for _, update := range updates { + update(s.currentNodeAnn) + } newStamp := uint32(time.Now().Unix()) if newStamp <= s.currentNodeAnn.Timestamp { From 6a6b490b50e318b097d8e6ec16b78dc0100aa696 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Fri, 20 Apr 2018 01:00:49 -0400 Subject: [PATCH 12/13] server: add support to automatically advertise external IPs using NAT traversal In this commit, we allow our node to automatically advertise its connection's external IPs on the ports it is currently listening on in order to accept inbound connections. This is only done when specifying a NAT traversal technique when starting the daemon. We also include a handy method that watches for dynamic IP changes in the background. If a new IP is detected, we'll craft a new node announcement using the new IP and broadcast it to the network. --- server.go | 254 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 241 insertions(+), 13 deletions(-) diff --git a/server.go b/server.go index 8b04d332f..a324b4455 100644 --- a/server.go +++ b/server.go @@ -28,6 +28,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/nat" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/tor" "github.com/roasbeef/btcd/btcec" @@ -35,6 +36,7 @@ import ( "github.com/roasbeef/btcd/connmgr" "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" + "golang.org/x/net/context" ) var ( @@ -85,6 +87,16 @@ type server struct { // creating and setting up onion services, etc. torController *tor.Controller + // natTraversal is the specific NAT traversal technique used to + // automatically set up port forwarding rules in order to advertise to + // the network that the node is accepting inbound connections. + natTraversal nat.Traversal + + // lastDetectedIP is the last IP detected by the NAT traversal technique + // above. This IP will be watched periodically in a goroutine in order + // to handle dynamic IP changes. + lastDetectedIP net.IP + mu sync.RWMutex peersByPub map[string]*peer @@ -300,32 +312,77 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, return nil, err } - // Gather external IPs from config - externalIPs := cfg.ExternalIPs - // If enabled, use either UPnP or NAT-PMP to automatically configure - // port forwarding for users behind a NAT - if cfg.UpnpSupport { + // port forwarding for users behind a NAT. + if cfg.NAT { + srvrLog.Info("Scanning local network for a UPnP enabled device") - externalIP, err := configureUpnp() + discoveryTimeout := time.Duration(10 * time.Second) + + ctx, cancel := context.WithTimeout( + context.Background(), discoveryTimeout, + ) + defer cancel() + upnp, err := nat.DiscoverUPnP(ctx) if err == nil { - externalIPs = append(externalIPs, externalIP) - } + s.natTraversal = upnp + } else { + // If we were not able to discover a UPnP enabled device + // on the local network, we'll fall back to attempting + // to discover a NAT-PMP enabled device. + srvrLog.Errorf("Unable to discover a UPnP enabled "+ + "device on the local network: %v", err) + srvrLog.Info("Scanning local network for a NAT-PMP " + + "enabled device") + + pmp, err := nat.DiscoverPMP(discoveryTimeout) + if err != nil { + err := fmt.Errorf("Unable to discover a "+ + "NAT-PMP enabled device on the local "+ + "network: %v", err) + srvrLog.Error(err) + return nil, err + } + + s.natTraversal = pmp + } } - if cfg.NatPmp { + // If we were requested to automatically configure port forwarding, + // we'll use the ports that the server will be listening on. + externalIPs := cfg.ExternalIPs + if s.natTraversal != nil { + listenPorts := make([]uint16, 0, len(listenAddrs)) + for _, listenAddr := range listenAddrs { + // At this point, the listen addresses should have + // already been normalized, so it's safe to ignore the + // errors. + _, portStr, _ := net.SplitHostPort(listenAddr) + port, _ := strconv.Atoi(portStr) - externalIP, err := configureNatPmp() - if err == nil { - externalIPs = append(externalIPs, externalIP) + listenPorts = append(listenPorts, uint16(port)) } + ips, err := s.configurePortForwarding(listenPorts...) + if err != nil { + srvrLog.Errorf("Unable to automatically set up port "+ + "forwarding using %s: %v", + s.natTraversal.Name(), err) + } else { + srvrLog.Infof("Automatically set up port forwarding "+ + "using %s to advertise external IP", + s.natTraversal.Name()) + externalIPs = append(externalIPs, ips...) + } } // If external IP addresses have been specified, add those to the list // of this server's addresses. - selfAddrs := make([]net.Addr, 0, len(cfg.ExternalIPs)) + externalIPs = normalizeAddresses( + externalIPs, strconv.Itoa(defaultPeerPort), + ) + selfAddrs := make([]net.Addr, 0, len(externalIPs)) for _, ip := range cfg.ExternalIPs { addr, err := parseAddr(ip) if err != nil { @@ -643,6 +700,11 @@ func (s *server) Start() error { } } + if s.natTraversal != nil { + s.wg.Add(1) + go s.watchExternalIP() + } + // Start the notification server. This is used so channel management // goroutines can be notified when a funding transaction reaches a // sufficient number of confirmations, or when the input for the @@ -750,6 +812,172 @@ func (s *server) Stopped() bool { return atomic.LoadInt32(&s.shutdown) != 0 } +// configurePortForwarding attempts to set up port forwarding for the diffrent +// ports that the server will be listening on. +// +// NOTE: This should only be used when using some kind of NAT traversal to +// automatically set up forwarding rules. +func (s *server) configurePortForwarding(ports ...uint16) ([]string, error) { + ip, err := s.natTraversal.ExternalIP() + if err != nil { + return nil, err + } + s.lastDetectedIP = ip + + externalIPs := make([]string, 0, len(ports)) + for _, port := range ports { + if err := s.natTraversal.AddPortMapping(port); err != nil { + srvrLog.Debugf("Unable to forward port %d: %v", port, err) + continue + } + + hostIP := fmt.Sprintf("%v:%d", ip, port) + externalIPs = append(externalIPs, hostIP) + } + + return externalIPs, nil +} + +// removePortForwarding attempts to clear the forwarding rules for the different +// ports the server is currently listening on. +// +// NOTE: This should only be used when using some kind of NAT traversal to +// automatically set up forwarding rules. +func (s *server) removePortForwarding() { + forwardedPorts := s.natTraversal.ForwardedPorts() + for _, port := range forwardedPorts { + if err := s.natTraversal.DeletePortMapping(port); err != nil { + srvrLog.Errorf("Unable to remove forwarding rules for "+ + "port %d: %v", port, err) + } + } +} + +// watchExternalIP continously checks for an updated external IP address every +// 15 minutes. Once a new IP address has been detected, it will automatically +// handle port forwarding rules and send updated node announcements to the +// currently connected peers. +// +// NOTE: This MUST be run as a goroutine. +func (s *server) watchExternalIP() { + defer s.wg.Done() + + // Before exiting, we'll make sure to remove the forwarding rules set + // up by the server. + defer s.removePortForwarding() + + // Keep track of the external IPs set by the user to avoid replacing + // them when detecting a new IP. + ipsSetByUser := make(map[string]struct{}) + for _, ip := range cfg.ExternalIPs { + ipsSetByUser[ip] = struct{}{} + } + + forwardedPorts := s.natTraversal.ForwardedPorts() + + ticker := time.NewTicker(15 * time.Minute) + defer ticker.Stop() +out: + for { + select { + case <-ticker.C: + // We'll start off by making sure a new IP address has + // been detected. + ip, err := s.natTraversal.ExternalIP() + if err != nil { + srvrLog.Debugf("Unable to retrieve the "+ + "external IP address: %v", err) + continue + } + + if ip.Equal(s.lastDetectedIP) { + continue + } + + srvrLog.Infof("Detected new external IP address %s", ip) + + // Next, we'll craft the new addresses that will be + // included in the new node announcement and advertised + // to the network. Each address will consist of the new + // IP detected and one of the currently advertised + // ports. + var newAddrs []net.Addr + for _, port := range forwardedPorts { + hostIP := fmt.Sprintf("%v:%d", ip, port) + addr, err := net.ResolveTCPAddr("tcp", hostIP) + if err != nil { + srvrLog.Debugf("Unable to resolve "+ + "host %v: %v", addr, err) + continue + } + + newAddrs = append(newAddrs, addr) + } + + // Skip the update if we weren't able to resolve any of + // the new addresses. + if len(newAddrs) == 0 { + srvrLog.Debug("Skipping node announcement " + + "update due to not being able to " + + "resolve any new addresses") + continue + } + + // Now, we'll need to update the addresses in our node's + // announcement in order to propogate the update + // throughout the network. We'll only include addresses + // that have a different IP from the previous one, as + // the previous IP is no longer valid. + currentNodeAnn, err := s.genNodeAnnouncement(false) + if err != nil { + srvrLog.Debugf("Unable to retrieve current "+ + "node announcement: %v", err) + continue + } + for _, addr := range currentNodeAnn.Addresses { + host, _, err := net.SplitHostPort(addr.String()) + if err != nil { + srvrLog.Debugf("Unable to determine "+ + "host from address %v: %v", + addr, err) + continue + } + + // We'll also make sure to include external IPs + // set manually by the user. + _, setByUser := ipsSetByUser[addr.String()] + if setByUser || host != s.lastDetectedIP.String() { + newAddrs = append(newAddrs, addr) + } + } + + // Then, we'll generate a new timestamped node + // announcement with the updated addresses and broadcast + // it to our peers. + newNodeAnn, err := s.genNodeAnnouncement( + true, lnwire.UpdateNodeAnnAddrs(newAddrs), + ) + if err != nil { + srvrLog.Debugf("Unable to generate new node "+ + "announcement: %v", err) + continue + } + + err = s.BroadcastMessage(nil, &newNodeAnn) + if err != nil { + srvrLog.Debugf("Unable to broadcast new node "+ + "announcement to peers: %v", err) + continue + } + + // Finally, update the last IP seen to the current one. + s.lastDetectedIP = ip + case <-s.quit: + break out + } + } +} + // initNetworkBootstrappers initializes a set of network peer bootstrappers // based on the server, and currently active bootstrap mechanisms as defined // within the current configuration. From c1886d2c1ced9b0e2f5937da051c9928621d11f9 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 12 Jun 2018 18:31:37 -0700 Subject: [PATCH 13/13] docs: add NAT traversal related documentation --- docs/nat_traversal.md | 23 +++++++++++++++++++++++ sample-lnd.conf | 12 ++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 docs/nat_traversal.md diff --git a/docs/nat_traversal.md b/docs/nat_traversal.md new file mode 100644 index 000000000..dd48fcfc7 --- /dev/null +++ b/docs/nat_traversal.md @@ -0,0 +1,23 @@ +# NAT Traversal + +`lnd` has support for NAT traversal using a number of different techniques. At +the time of writing this documentation, UPnP and NAT-PMP are supported. NAT +traversal can be enabled through `lnd`'s `--nat` flag. + +```shell +$ lnd ... --nat +``` + +On startup, `lnd` will try the different techniques until one is found that's +supported by your hardware. The underlying dependencies used for these +techniques rely on using system-specific binaries in order to detect your +gateway device's address. This is needed because we need to be able to reach the +gateway device to determine if it supports the specific NAT traversal technique +currently being tried. Because of this, due to uncommon setups, it is possible +that these binaries are not found in your system. If this is case, `lnd` will +exit stating such error. + +As a bonus, `lnd` spawns a background thread that automatically detects IP +address changes and propagates the new address update to the rest of the +network. This is especially beneficial for users who were provided dynamic IP +addresses from their internet service provider. diff --git a/sample-lnd.conf b/sample-lnd.conf index 21122fe6c..8d3e9262b 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -77,6 +77,18 @@ ; (with host:port notation), the default port (9735) will be added to the ; address. ; externalip= +; +; Instead of explicitly stating your external IP address, you can also enable +; UPnP or NAT-PMP support on the daemon. Both techniques will be tried and +; require proper hardware support. In order to detect this hardware support, +; `lnd` uses a dependency that retrieves the router's gateway address by using +; different built-in binaries in each platform. Therefore, it is possible that +; we are unable to detect the hardware and `lnd` will exit with an error +; indicating this. This option will automatically retrieve your external IP +; address, even after it has changed in the case of dynamic IPs, and advertise +; it to the network using the ports the daemon is listening on. This does not +; support devices behind multiple NATs. +; nat=true ; Debug logging level.