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" diff --git a/config.go b/config.go index 091a7f4d1..281186610 100644 --- a/config.go +++ b/config.go @@ -184,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"` @@ -455,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. @@ -772,26 +778,36 @@ 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 // 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. 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/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) 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" +} 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. diff --git a/server.go b/server.go index 5f0becd3f..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,9 +312,77 @@ func newServer(listenAddrs []string, chanDB *channeldb.DB, cc *chainControl, return nil, err } + // If enabled, use either UPnP or NAT-PMP to automatically configure + // port forwarding for users behind a NAT. + if cfg.NAT { + srvrLog.Info("Scanning local network for a UPnP enabled device") + + discoveryTimeout := time.Duration(10 * time.Second) + + ctx, cancel := context.WithTimeout( + context.Background(), discoveryTimeout, + ) + defer cancel() + upnp, err := nat.DiscoverUPnP(ctx) + if err == nil { + 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 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) + + 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 { @@ -620,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 @@ -727,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. @@ -964,8 +1215,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() @@ -974,7 +1225,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 {