From f0a20c9d04d2f8151a174ca98b9a0d470c2318d2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 14 Aug 2025 13:15:40 +0000 Subject: [PATCH] netadd: split ValidatenodeAnn into sig and field checks Check that the node ann doesnt contain more than 1 DNS addr. This will ensure that we now start rejecting new node announcements with multiple DNS addrs since this check is called in the gossiper before persisting a node ann to our local graph. It also validates the DNS fields according to BOLT #7 specs. --- lnwire/dns_addr.go | 57 ++++++++++++++++++++++++ lnwire/dns_addr_test.go | 87 +++++++++++++++++++++++++++++++++++++ netann/node_announcement.go | 40 ++++++++++++++++- 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 lnwire/dns_addr_test.go diff --git a/lnwire/dns_addr.go b/lnwire/dns_addr.go index 92cbc073a..87ddcd8cd 100644 --- a/lnwire/dns_addr.go +++ b/lnwire/dns_addr.go @@ -1,10 +1,29 @@ package lnwire import ( + "errors" + "fmt" "net" "strconv" ) +var ( + // ErrEmptyDNSHostname is returned when a DNS hostname is empty. + ErrEmptyDNSHostname = errors.New("hostname cannot be empty") + + // ErrZeroPort is returned when a DNS port is zero. + ErrZeroPort = errors.New("port cannot be zero") + + // ErrHostnameTooLong is returned when a DNS hostname exceeds 255 bytes. + ErrHostnameTooLong = errors.New("DNS hostname length exceeds limit " + + "of 255 bytes") + + // ErrInvalidHostnameCharacter is returned when a DNS hostname contains + // an invalid character. + ErrInvalidHostnameCharacter = errors.New("hostname contains invalid " + + "character") +) + // 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 +48,41 @@ 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("%w: DNS hostname length %d", + ErrHostnameTooLong, 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("%w: hostname '%s' contains invalid "+ + "character '%c' at position %d", + ErrInvalidHostnameCharacter, hostname, r, i) + } + } + + return nil +} diff --git a/lnwire/dns_addr_test.go b/lnwire/dns_addr_test.go new file mode 100644 index 000000000..8cdf2a858 --- /dev/null +++ b/lnwire/dns_addr_test.go @@ -0,0 +1,87 @@ +package lnwire + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestValidateDNSAddr tests hostname and port validation per BOLT #7. +func TestValidateDNSAddr(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + hostname string + port uint16 + err error + }{ + { + name: "empty hostname", + hostname: "", + port: 9735, + err: ErrEmptyDNSHostname, + }, + { + name: "zero port", + hostname: "example.com", + port: 0, + err: ErrZeroPort, + }, + { + name: "hostname too long", + hostname: strings.Repeat("a", 256), + port: 9735, + err: fmt.Errorf("%w: DNS hostname length 256", + ErrHostnameTooLong), + }, + { + name: "hostname with invalid ASCII space", + hostname: "exa mple.com", + port: 9735, + err: fmt.Errorf("%w: hostname 'exa mple.com' contains "+ + "invalid character ' ' at position 3", + ErrInvalidHostnameCharacter), + }, + { + name: "hostname with invalid ASCII underscore", + hostname: "example_node.com", + port: 9735, + err: fmt.Errorf("%w: hostname 'example_node.com' "+ + "contains invalid character '_' at position 7", + ErrInvalidHostnameCharacter), + }, + { + name: "hostname with non-ASCII character", + hostname: "example❄️.com", + port: 9735, + err: fmt.Errorf("%w: hostname 'example❄️.com' "+ + "contains invalid character '❄' at position 7", + ErrInvalidHostnameCharacter), + }, + { + name: "valid hostname", + hostname: "example.com", + port: 9735, + }, + { + name: "valid hostname with numbers", + hostname: "node101.example.com", + port: 9735, + }, + { + name: "valid hostname with hyphens", + hostname: "my-node.example-domain.com", + port: 9735, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateDNSAddr(tc.hostname, tc.port) + require.Equal(t, err, tc.err) + }) + } +} 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()