diff --git a/config.go b/config.go index 8c3718e950..098419af4c 100644 --- a/config.go +++ b/config.go @@ -325,26 +325,28 @@ type Config struct { // loadConfig function. We need to expose the 'raw' strings so the // command line library can access them. // Only the parsed net.Addrs should be used! - RawRPCListeners []string `long:"rpclisten" description:"Add an interface/port/socket to listen for RPC connections"` - RawRESTListeners []string `long:"restlisten" description:"Add an interface/port/socket to listen for REST connections"` - RawListeners []string `long:"listen" description:"Add an interface/port to listen for peer connections"` - RawExternalIPs []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"` - ExternalHosts []string `long:"externalhosts" description:"Add a hostname:port that should be periodically resolved to announce IPs for. If a port is not specified, the default (9735) will be used."` - RPCListeners []net.Addr - RESTListeners []net.Addr - RestCORS []string `long:"restcors" description:"Add an ip:port/hostname to allow cross origin access from. To allow all origins, set as \"*\"."` - Listeners []net.Addr - ExternalIPs []net.Addr - DisableListen bool `long:"nolisten" description:"Disable listening for incoming peer connections"` - DisableRest bool `long:"norest" description:"Disable REST API"` - DisableRestTLS bool `long:"no-rest-tls" description:"Disable TLS for REST connections"` - WSPingInterval time.Duration `long:"ws-ping-interval" description:"The ping interval for REST based WebSocket connections, set to 0 to disable sending ping messages from the server side"` - WSPongWait time.Duration `long:"ws-pong-wait" description:"The time we wait for a pong response message on REST based WebSocket connections before the connection is closed as inactive"` - 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"` - AddPeers []string `long:"addpeer" description:"Specify peers to connect to first"` - MinBackoff time.Duration `long:"minbackoff" description:"Shortest backoff when reconnecting to persistent peers. Valid time units are {s, m, h}."` - MaxBackoff time.Duration `long:"maxbackoff" description:"Longest backoff when reconnecting to persistent peers. Valid time units are {s, m, h}."` - ConnectionTimeout time.Duration `long:"connectiontimeout" description:"The timeout value for network connections. Valid time units are {ms, s, m, h}."` + RawRPCListeners []string `long:"rpclisten" description:"Add an interface/port/socket to listen for RPC connections"` + RawRESTListeners []string `long:"restlisten" description:"Add an interface/port/socket to listen for REST connections"` + RawListeners []string `long:"listen" description:"Add an interface/port to listen for peer connections"` + RawExternalIPs []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"` + RawExternalDNSHostnameAddress string `long:"external-dns-hostname" description:"Specify a DNS hostname for the node's external address. If no port is provided, the default (9735) is used."` + ExternalHosts []string `long:"externalhosts" description:"Add a hostname:port that should be periodically resolved to announce IPs for. If a port is not specified, the default (9735) will be used."` + RPCListeners []net.Addr + RESTListeners []net.Addr + RestCORS []string `long:"restcors" description:"Add an ip:port/hostname to allow cross origin access from. To allow all origins, set as \"*\"."` + Listeners []net.Addr + ExternalIPs []net.Addr + ExternalDNSHostnameAddress *lnwire.DNSHostnameAddress + DisableListen bool `long:"nolisten" description:"Disable listening for incoming peer connections"` + DisableRest bool `long:"norest" description:"Disable REST API"` + DisableRestTLS bool `long:"no-rest-tls" description:"Disable TLS for REST connections"` + WSPingInterval time.Duration `long:"ws-ping-interval" description:"The ping interval for REST based WebSocket connections, set to 0 to disable sending ping messages from the server side"` + WSPongWait time.Duration `long:"ws-pong-wait" description:"The time we wait for a pong response message on REST based WebSocket connections before the connection is closed as inactive"` + 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"` + AddPeers []string `long:"addpeer" description:"Specify peers to connect to first"` + MinBackoff time.Duration `long:"minbackoff" description:"Shortest backoff when reconnecting to persistent peers. Valid time units are {s, m, h}."` + MaxBackoff time.Duration `long:"maxbackoff" description:"Longest backoff when reconnecting to persistent peers. Valid time units are {s, m, h}."` + ConnectionTimeout time.Duration `long:"connectiontimeout" description:"The timeout value for network connections. Valid time units are {ms, s, m, h}."` 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"` @@ -1555,6 +1557,7 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, ltndLog.Infof("Listening on the p2p interface is disabled!") cfg.Listeners = nil cfg.ExternalIPs = nil + cfg.ExternalDNSHostnameAddress = nil } else { // Add default port to all listener addresses if needed and remove @@ -1578,6 +1581,23 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, return nil, err } + // Add default port to external dnsh hostname address if needed. + if cfg.RawExternalDNSHostnameAddress != "" { + addr, err := parseAddr( + cfg.RawExternalDNSHostnameAddress, cfg.net, + ) + if err != nil { + return nil, err + } + dnsAddr, ok := addr.(*lnwire.DNSHostnameAddress) + if !ok { + return nil, fmt.Errorf("failed to cast "+ + "address to lnwire.DNSHostnameAddr: "+ + "got %T", addr) + } + cfg.ExternalDNSHostnameAddress = dnsAddr + } + // For the p2p port it makes no sense to listen to an Unix socket. // Also, we would need to refactor the brontide listener to support // that. diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 290e529bd9..a154ec9582 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -1988,6 +1988,7 @@ func (d *AuthenticatedGossiper) addNode(msg *lnwire.NodeAnnouncement, HaveNodeAnnouncement: true, LastUpdate: timestamp, Addresses: msg.Addresses, + DNSHostnameAddress: msg.DNSHostnameAddress, PubKeyBytes: msg.NodeID, Alias: msg.Alias.String(), AuthSigBytes: msg.Signature.ToSignatureBytes(), diff --git a/graph/db/addr.go b/graph/db/addr.go index f994131582..0c5c31d441 100644 --- a/graph/db/addr.go +++ b/graph/db/addr.go @@ -7,6 +7,7 @@ import ( "io" "net" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tor" ) @@ -26,6 +27,9 @@ const ( // v3OnionAddr denotes a version 3 Tor (prop224) onion service address. v3OnionAddr addressType = 3 + + // dnsHostnameAddr denotes a DNS hostname address. + dnsHostnameAddr addressType = 4 ) // encodeTCPAddr serializes a TCP address into its compact raw bytes @@ -121,6 +125,47 @@ func encodeOnionAddr(w io.Writer, addr *tor.OnionAddr) error { return nil } +// encodeDNSHostnameAddr serializes a DNS hostname address into its compact raw +// bytes representation. It writes the address type, hostname length, hostname, +// and port (in big-endian order) to the writer. The function validates that the +// hostname is non-empty and does not exceed 255 characters. Returns an error if +// any part of the serialization fails. +func encodeDNSHostnameAddr(w io.Writer, addr *lnwire.DNSHostnameAddress) error { + // Validate the hostname. + if len(addr.Hostname) == 0 { + return errors.New("hostname cannot be empty") + } + if len(addr.Hostname) > 255 { + return errors.New("hostname exceeds maximum length of 255 " + + "characters") + } + + // Write the address type. + if _, err := w.Write([]byte{byte(dnsHostnameAddr)}); err != nil { + return err + } + + // Write the length of the hostname. + hostnameLen := byte(len(addr.Hostname)) + if _, err := w.Write([]byte{hostnameLen}); err != nil { + return err + } + + // Write the hostname bytes. + if _, err := w.Write([]byte(addr.Hostname)); err != nil { + return err + } + + // Write the port in big-endian order. + var port [2]byte + binary.BigEndian.PutUint16(port[:], uint16(addr.Port)) + if _, err := w.Write(port[:]); err != nil { + return err + } + + return nil +} + // DeserializeAddr reads the serialized raw representation of an address and // deserializes it into the actual address. This allows us to avoid address // resolution within the channeldb package. @@ -200,6 +245,29 @@ func DeserializeAddr(r io.Reader) (net.Addr, error) { OnionService: onionService, Port: port, } + + case dnsHostnameAddr: + var hostnameLen byte + err := binary.Read(r, binary.BigEndian, &hostnameLen) + if err != nil { + return nil, err + } + + hostname := make([]byte, hostnameLen) + if _, err := r.Read(hostname); err != nil { + return nil, err + } + + var port [2]byte + if _, err := r.Read(port[:]); err != nil { + return nil, err + } + + address = &lnwire.DNSHostnameAddress{ + Hostname: string(hostname), + Port: int(binary.BigEndian.Uint16(port[:])), + } + default: return nil, ErrUnknownAddressType } @@ -215,6 +283,8 @@ func SerializeAddr(w io.Writer, address net.Addr) error { return encodeTCPAddr(w, addr) case *tor.OnionAddr: return encodeOnionAddr(w, addr) + case *lnwire.DNSHostnameAddress: + return encodeDNSHostnameAddr(w, addr) default: return ErrUnknownAddressType } diff --git a/graph/db/graph.go b/graph/db/graph.go index d5a876a79a..bb97fee150 100644 --- a/graph/db/graph.go +++ b/graph/db/graph.go @@ -3972,13 +3972,20 @@ func putLightningNode(nodeBucket kvdb.RwBucket, aliasBucket kvdb.RwBucket, // no return err } - numAddresses := uint16(len(node.Addresses)) + var allAddresses []net.Addr + var numAddresses uint16 + if node.DNSHostnameAddress != nil { + allAddresses = append(allAddresses, node.DNSHostnameAddress) + numAddresses += 1 + } + allAddresses = append(allAddresses, node.Addresses...) + numAddresses += uint16(len(node.Addresses)) byteOrder.PutUint16(scratch[:2], numAddresses) if _, err := b.Write(scratch[:2]); err != nil { return err } - for _, address := range node.Addresses { + for _, address := range allAddresses { if err := SerializeAddr(&b, address); err != nil { return err } @@ -4171,14 +4178,20 @@ func deserializeLightningNode(r io.Reader) (models.LightningNode, error) { numAddresses := int(byteOrder.Uint16(scratch[:2])) var addresses []net.Addr + var dnsHostnameAddress *lnwire.DNSHostnameAddress for i := 0; i < numAddresses; i++ { address, err := DeserializeAddr(r) if err != nil { return models.LightningNode{}, err } - addresses = append(addresses, address) + if dnsAddr, ok := address.(*lnwire.DNSHostnameAddress); ok { + dnsHostnameAddress = dnsAddr + } else { + addresses = append(addresses, address) + } } node.Addresses = addresses + node.DNSHostnameAddress = dnsHostnameAddress node.AuthSigBytes, err = wire.ReadVarBytes(r, 0, 80, "sig") if err != nil { diff --git a/graph/db/models/node.go b/graph/db/models/node.go index 9624154339..37dac876fc 100644 --- a/graph/db/models/node.go +++ b/graph/db/models/node.go @@ -32,6 +32,9 @@ type LightningNode struct { // Address is the TCP address this node is reachable over. Addresses []net.Addr + // DNSHostnameAddress is the DNS hostname address for this node. + DNSHostnameAddress *lnwire.DNSHostnameAddress + // Color is the selected color for the node. Color color.RGBA @@ -109,13 +112,14 @@ func (l *LightningNode) NodeAnnouncement(signed bool) (*lnwire.NodeAnnouncement, } nodeAnn := &lnwire.NodeAnnouncement{ - Features: l.Features.RawFeatureVector, - NodeID: l.PubKeyBytes, - RGBColor: l.Color, - Alias: alias, - Addresses: l.Addresses, - Timestamp: uint32(l.LastUpdate.Unix()), - ExtraOpaqueData: l.ExtraOpaqueData, + Features: l.Features.RawFeatureVector, + NodeID: l.PubKeyBytes, + RGBColor: l.Color, + Alias: alias, + Addresses: l.Addresses, + DNSHostnameAddress: l.DNSHostnameAddress, + Timestamp: uint32(l.LastUpdate.Unix()), + ExtraOpaqueData: l.ExtraOpaqueData, } if !signed { diff --git a/lnrpc/peersrpc/peers_server.go b/lnrpc/peersrpc/peers_server.go index 27dfa34703..3ecc493812 100644 --- a/lnrpc/peersrpc/peers_server.go +++ b/lnrpc/peersrpc/peers_server.go @@ -371,6 +371,32 @@ func (s *Server) UpdateNodeAnnouncement(_ context.Context, } } + if req.DnsHostnameAddress != "" { + addr, err := s.cfg.ParseAddr(req.DnsHostnameAddress) + if err != nil { + return nil, fmt.Errorf("invalid dns hostname address "+ + "value: %w", err) + } + dnsAddr, ok := addr.(*lnwire.DNSHostnameAddress) + if !ok { + return nil, fmt.Errorf("failed to cast address to "+ + "lnwire.DNSHostnameAddr: got %T", addr) + } + + if dnsAddr != currentNodeAnn.DNSHostnameAddress { + resp.Ops = append(resp.Ops, &lnrpc.Op{ + Entity: "dns_hostname_address", + Actions: []string{ + fmt.Sprintf("changed to %v", dnsAddr), + }, + }) + nodeModifiers = append( + nodeModifiers, + netann.NodeAnnSetDNSHostnameAddress(dnsAddr), + ) + } + } + if len(req.AddressUpdates) > 0 { newAddrs, ops, err := s.updateAddresses( currentNodeAnn.Addresses, diff --git a/lnwire/dns_hostname_addr.go b/lnwire/dns_hostname_addr.go new file mode 100644 index 0000000000..7d09f0ee97 --- /dev/null +++ b/lnwire/dns_hostname_addr.go @@ -0,0 +1,26 @@ +package lnwire + +import ( + "fmt" + "net" +) + +// DNSHostnameAddress is a custom implementation of the net.Addr interface. +type DNSHostnameAddress struct { + Hostname string + Port int +} + +// A compile-time check to ensure that DNSHostnameAddr implements +// the net.Addr interface. +var _ net.Addr = (*DNSHostnameAddress)(nil) + +// Network returns the network type, e.g., "tcp". +func (d *DNSHostnameAddress) Network() string { + return "tcp" +} + +// String returns the address in the form "hostname:port". +func (d *DNSHostnameAddress) String() string { + return fmt.Sprintf("%s:%d", d.Hostname, d.Port) +} diff --git a/lnwire/lnwire.go b/lnwire/lnwire.go index 4d2b814e5d..40c3788738 100644 --- a/lnwire/lnwire.go +++ b/lnwire/lnwire.go @@ -52,6 +52,9 @@ const ( // v3OnionAddr denotes a version 3 Tor (prop224) onion service address. v3OnionAddr addressType = 4 + + // dnsHostnameAddr denotes a DNS hostname address. + dnsHostnameAddr addressType = 5 ) // AddrLen returns the number of bytes that it takes to encode the target @@ -412,6 +415,35 @@ func WriteElement(w *bytes.Buffer, element interface{}) error { return err } + case *DNSHostnameAddress: + if e == nil { + return fmt.Errorf("cannot write nil DNSHostnameAddr") + } + + // Write the address type. + _, err := w.Write([]byte{byte(dnsHostnameAddr)}) + if err != nil { + return err + } + + // Write the length of the hostname. + hostnameLen := byte(len(e.Hostname)) + if _, err := w.Write([]byte{hostnameLen}); err != nil { + return err + } + + // Write the hostname bytes. + if _, err := w.WriteString(e.Hostname); err != nil { + return err + } + + // Write the port in big-endian order. + var port [2]byte + binary.BigEndian.PutUint16(port[:], uint16(e.Port)) + if _, err := w.Write(port[:]); err != nil { + return err + } + case []net.Addr: // First, we'll encode all the addresses into an intermediate // buffer. We need to do this in order to compute the total @@ -881,6 +913,35 @@ func ReadElement(r io.Reader, element interface{}) error { } addrBytesRead += aType.AddrLen() + case dnsHostnameAddr: + var hostnameLen byte + err := binary.Read( + addrBuf, binary.BigEndian, &hostnameLen, + ) + if err != nil { + return err + } + + hostname := make([]byte, hostnameLen) + _, err = io.ReadFull(addrBuf, hostname) + if err != nil { + return err + } + + var port [2]byte + _, err = io.ReadFull(addrBuf, port[:]) + if err != nil { + return err + } + + portNum := int(binary.BigEndian.Uint16(port[:])) + address = &DNSHostnameAddress{ + Hostname: string(hostname), + Port: portNum, + } + addrLen := uint16(1 + int(hostnameLen) + 2) + addrBytesRead += addrLen + default: // If we don't understand this address type, // we just store it along with the remaining diff --git a/lnwire/node_announcement.go b/lnwire/node_announcement.go index 4f16202812..9ea2f1c2f6 100644 --- a/lnwire/node_announcement.go +++ b/lnwire/node_announcement.go @@ -91,6 +91,9 @@ type NodeAnnouncement struct { // which the node is accepting incoming connections. Addresses []net.Addr + // DNSHostnameAddress is The DNS hostname address of the node. + DNSHostnameAddress *DNSHostnameAddress + // ExtraOpaqueData is the set of data that was appended to this // message, some of which we may not actually know how to iterate or // parse. By holding onto this data, we ensure that we're able to diff --git a/netann/node_announcement.go b/netann/node_announcement.go index 5b6f7a430e..7478b6d80e 100644 --- a/netann/node_announcement.go +++ b/netann/node_announcement.go @@ -22,6 +22,16 @@ func NodeAnnSetAlias(alias lnwire.NodeAlias) func(*lnwire.NodeAnnouncement) { } } +// NodeAnnSetAlias is a functional option that sets the alias of the +// given node announcement. +// +//nolint:ll +func NodeAnnSetDNSHostnameAddress(addr *lnwire.DNSHostnameAddress) func(*lnwire.NodeAnnouncement) { + return func(nodeAnn *lnwire.NodeAnnouncement) { + nodeAnn.DNSHostnameAddress = addr + } +} + // NodeAnnSetAddrs is a functional option that allows updating the addresses of // the given node announcement. func NodeAnnSetAddrs(addrs []net.Addr) func(*lnwire.NodeAnnouncement) { diff --git a/rpcserver.go b/rpcserver.go index 72e2fa4afd..4289a29efb 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6873,18 +6873,27 @@ func marshalNode(node *models.LightningNode) *lnrpc.LightningNode { nodeAddrs[i] = nodeAddr } + var dnsHostNameAddr *lnrpc.NodeAddress + if node.DNSHostnameAddress != nil { + dnsHostNameAddr = &lnrpc.NodeAddress{ + Network: node.DNSHostnameAddress.Network(), + Addr: node.DNSHostnameAddress.String(), + } + } + features := invoicesrpc.CreateRPCFeatures(node.Features) customRecords := marshalExtraOpaqueData(node.ExtraOpaqueData) return &lnrpc.LightningNode{ - LastUpdate: uint32(node.LastUpdate.Unix()), - PubKey: hex.EncodeToString(node.PubKeyBytes[:]), - Addresses: nodeAddrs, - Alias: node.Alias, - Color: graph.EncodeHexColor(node.Color), - Features: features, - CustomRecords: customRecords, + LastUpdate: uint32(node.LastUpdate.Unix()), + PubKey: hex.EncodeToString(node.PubKeyBytes[:]), + Addresses: nodeAddrs, + DnsHostnameAddress: dnsHostNameAddr, + Alias: node.Alias, + Color: graph.EncodeHexColor(node.Color), + Features: features, + CustomRecords: customRecords, } } diff --git a/sample-lnd.conf b/sample-lnd.conf index b447e6b6fe..9b3f88746f 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -214,6 +214,14 @@ ; address. ; externalip= ; + +; Specify a DNS hostname for the node's external address. If no port is +; provided, the default (9735) is used. +; Default: +; external-dns-hostname= +; Example: +; external-dns-hostname=my-node-domain.com + ; 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, diff --git a/server.go b/server.go index d71ebc0326..597bf34b4f 100644 --- a/server.go +++ b/server.go @@ -481,16 +481,36 @@ func parseAddr(address string, netCfg tor.Net) (net.Addr, error) { port = portNum } + // Handle Onion address type. if tor.IsOnionHost(host) { return &tor.OnionAddr{OnionService: host, Port: port}, nil } - // If the host is part of a TCP address, we'll use the network - // specific ResolveTCPAddr function in order to resolve these - // addresses over Tor in order to prevent leaking your real IP - // address. - hostPort := net.JoinHostPort(host, strconv.Itoa(port)) - return netCfg.ResolveTCPAddr("tcp", hostPort) + // Handle loopback and IP address types. Use the ResolveTCPAddr function + // to resolve these addresses over Tor, preventing IP leakage. + if lncfg.IsLoopback(host) || isIP(host) { + hostPort := net.JoinHostPort(host, strconv.Itoa(port)) + return netCfg.ResolveTCPAddr("tcp", hostPort) + } + + // Attempt DNS lookup for hostname. + _, err = netCfg.LookupHost(host) + if err == nil { + return &lnwire.DNSHostnameAddress{ + Hostname: host, + Port: port, + }, nil + } + + // Return error if address is invalid. + return nil, fmt.Errorf("invalid address: %s, got error: %w", host, err) +} + +// isIP checks if the provided host is an IP address (IPv4 or IPv6). +func isIP(host string) bool { + // Try parsing the host as an IP address. + ip := net.ParseIP(host) + return ip != nil } // noiseDial is a factory function which creates a connmgr compliant dialing @@ -900,6 +920,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, HaveNodeAnnouncement: true, LastUpdate: time.Now(), Addresses: selfAddrs, + DNSHostnameAddress: cfg.ExternalDNSHostnameAddress, Alias: nodeAlias.String(), Features: s.featureMgr.Get(feature.SetNodeAnn), Color: color, @@ -3268,6 +3289,7 @@ func (s *server) createNewHiddenService() error { HaveNodeAnnouncement: true, LastUpdate: time.Unix(int64(newNodeAnn.Timestamp), 0), Addresses: newNodeAnn.Addresses, + DNSHostnameAddress: newNodeAnn.DNSHostnameAddress, Alias: newNodeAnn.Alias.String(), Features: lnwire.NewFeatureVector( newNodeAnn.Features, lnwire.Features, @@ -3387,6 +3409,7 @@ func (s *server) updateAndBroadcastSelfNode(features *lnwire.RawFeatureVector, selfNode.Features = s.featureMgr.Get(feature.SetNodeAnn) selfNode.Color = newNodeAnn.RGBColor selfNode.AuthSigBytes = newNodeAnn.Signature.ToSignatureBytes() + selfNode.DNSHostnameAddress = newNodeAnn.DNSHostnameAddress copy(selfNode.PubKeyBytes[:], s.identityECDH.PubKey().SerializeCompressed())