diff --git a/lnwire/dns_addr.go b/lnwire/dns_addr.go index 92cbc073a..94f4bd800 100644 --- a/lnwire/dns_addr.go +++ b/lnwire/dns_addr.go @@ -1,10 +1,18 @@ package lnwire import ( + "errors" + "fmt" "net" "strconv" ) +// ErrEmptyDNSHostname is returned when a DNS hostname is empty. +var ErrEmptyDNSHostname = errors.New("hostname cannot be empty") + +// ErrZeroPort is returned when a DNS port is zero. +var ErrZeroPort = errors.New("port cannot be zero") + // DNSAddress is used to represent a DNS address of a node. type DNSAddress struct { // Hostname is the DNS hostname of the address. This MUST only contain @@ -29,3 +37,40 @@ func (d *DNSAddress) Network() string { func (d *DNSAddress) String() string { return net.JoinHostPort(d.Hostname, strconv.Itoa(int(d.Port))) } + +// ValidateDNSAddr validates that the DNS hostname is not empty and contains +// only ASCII characters and of max length 255 characters and port is non zero +// according to BOLT #7. +func ValidateDNSAddr(hostname string, port uint16) error { + if hostname == "" { + return ErrEmptyDNSHostname + } + + // Per BOLT 7, ports must not be zero for type 5 address (DNS address). + if port == 0 { + return ErrZeroPort + } + + if len(hostname) > 255 { + return fmt.Errorf("DNS hostname length %d, exceeds limit of "+ + "255 bytes", len(hostname)) + } + + // Check if hostname contains only ASCII characters. + for i, r := range hostname { + // Check for valid hostname characters, excluding ASCII control + // characters (0-31), spaces, underscores, delete character + // (127), and the special characters (like /, \, @, #, $, etc.). + if !((r >= 'a' && r <= 'z') || + (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || + r == '-' || + r == '.') { + + return fmt.Errorf("hostname '%s' contains invalid "+ + "character '%c' at position %d", hostname, r, i) + } + } + + return nil +} diff --git a/netann/node_announcement.go b/netann/node_announcement.go index 712502170..893e9fc4a 100644 --- a/netann/node_announcement.go +++ b/netann/node_announcement.go @@ -2,6 +2,7 @@ package netann import ( "bytes" + "errors" "fmt" "image/color" "net" @@ -81,10 +82,45 @@ func SignNodeAnnouncement(signer lnwallet.MessageSigner, return err } -// ValidateNodeAnn validates the node announcement by ensuring that the +// ValidateNodeAnn validates the fields and signature of a node announcement. +func ValidateNodeAnn(a *lnwire.NodeAnnouncement) error { + err := ValidateNodeAnnFields(a) + if err != nil { + return fmt.Errorf("invalid node announcement fields: %w", err) + } + + return ValidateNodeAnnSignature(a) +} + +// ValidateNodeAnnFields validates the fields of a node announcement. +func ValidateNodeAnnFields(a *lnwire.NodeAnnouncement) error { + // Check that it only has at most one DNS address. + hasDNSAddr := false + for _, addr := range a.Addresses { + dnsAddr, ok := addr.(*lnwire.DNSAddress) + if !ok { + continue + } + if hasDNSAddr { + return errors.New("node announcement contains " + + "multiple DNS addresses. Only one is allowed") + } + + hasDNSAddr = true + + err := lnwire.ValidateDNSAddr(dnsAddr.Hostname, dnsAddr.Port) + if err != nil { + return err + } + } + + return nil +} + +// ValidateNodeAnnSignature validates the node announcement by ensuring that the // attached signature is needed a signature of the node announcement under the // specified node public key. -func ValidateNodeAnn(a *lnwire.NodeAnnouncement) error { +func ValidateNodeAnnSignature(a *lnwire.NodeAnnouncement) error { // Reconstruct the data of announcement which should be covered by the // signature so we can verify the signature shortly below data, err := a.DataToSign()