Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions diam/datatype/diamident.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,42 @@ func (s DiameterIdentity) Type() TypeID {
func (s DiameterIdentity) String() string {
return fmt.Sprintf("DiameterIdentity{%s},Padding:%d", string(s), s.Padding())
}

// Valid reports whether s has the ASCII label syntax of a DiameterIdentity
// (RFC 6733 §4.3.1 and Appendix D): one or more labels of ASCII letters,
// digits and interior hyphens (RFC 1035 §2.3.1, as relaxed by RFC 1123
// §2.1), separated by dots, with an optional final root dot. Each label is
// 1–63 octets and the name without the final dot at most 253 octets
// (RFC 1035 §§2.3.4, 3.1). Valid checks neither that s names a host or realm
// nor IDNA validity: a label that looks like an A-label is checked as plain
// ASCII (RFC 5891 §5.3).
func (s DiameterIdentity) Valid() bool {
name := string(s)
if len(name) > 0 && name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
if len(name) == 0 || len(name) > 253 {
return false
}
label := 0
for i := 0; i < len(name); i++ {
c := name[i]
if c == '.' {
if label == 0 || name[i-1] == '-' {
return false
}
label = 0
continue
}
letter := c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
digit := c >= '0' && c <= '9'
if !letter && !digit && (c != '-' || label == 0) {
return false
}
label++
if label > 63 {
return false
}
}
return label > 0 && name[len(name)-1] != '-'
}
103 changes: 103 additions & 0 deletions diam/datatype/diamident_syntax_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package datatype

import (
"strings"
"testing"
)

func TestDiameterIdentityValid(t *testing.T) {
maxName := strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "." + strings.Repeat("d", 61)
tests := []struct {
name, value string
valid bool
}{
{"fqdn", "peer.example.net", true},
{"realm", "example.net", true},
{"root_dot", "peer.example.net.", true},
{"empty", "", false},
{"root_only", ".", false},
{"one_label", "localhost", true},
{"one_letter", "a", true},
{"one_digit", "1", true},
{"label_63", strings.Repeat("a", 63), true},
{"label_64", strings.Repeat("a", 64), false},
{"inner_label_63", "a." + strings.Repeat("b", 63) + ".net", true},
{"inner_label_64", "a." + strings.Repeat("b", 64) + ".net", false},
{"name_253", maxName, true},
{"name_254", maxName + "e", false},
{"name_255", maxName + "ef", false},
{"name_253_root_dot", maxName + ".", true},
{"name_254_root_dot", maxName + "e.", false},
{"name_255_root_dot", maxName + "ef.", false},
{"uppercase", "PEER.Example.NET", true},
{"numeric_label", "123.example.net", true},
{"numeric_final_label", "peer.123", true},
{"all_numeric", "123.456", true},
{"leading_hyphen", "-peer.example", false},
{"inner_leading_hyphen", "peer.-host.example", false},
{"trailing_hyphen", "example.peer-", false},
{"inner_trailing_hyphen", "peer-.example", false},
{"only_hyphen", "-", false},
{"interior_hyphen", "peer-1.example", true},
{"reserved_hyphens", "ab--cd.example", true},
{"hyphen_position_3", "ab-cd.example", true},
{"hyphen_position_4", "abc-d.example", true},
{"a_label", "xn--bcher-kva.example", true},
{"uppercase_a_label", "XN--BCHER-KVA.example", true},
{"fake_a_label_control", "xn--a.example", true},
{"fake_a_label_ascii", "xn--abc-.example", false},
{"empty_ace_suffix", "xn--.example", false},
{"u_label", "bücher.example", false},
{"non_ascii", "peer\xff.example", false},
{"continuation_byte", "peer\x80.example", false},
{"underscore", "peer_name.example", false},
{"service_owner", "_diameter._tcp.example", false},
{"space", "peer name.example", false},
{"nul", "peer\x00.example", false},
{"newline", "peer\n.example", false},
{"tab", "peer\t.example", false},
{"delete", "peer\x7f.example", false},
{"leading_dot", ".example", false},
{"empty_inner_label", "peer..example", false},
{"two_root_dots", "peer.example..", false},
{"ipv4_literal", "192.0.2.1", true},
{"ipv6_literal", "2001:db8::1", false},
{"bracketed_ipv6", "[2001:db8::1]", false},
{"escaped_dot", "peer\\.example", false},
{"port", "peer.example:3868", false},
{"slash", "peer/example", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
identity := DiameterIdentity(tt.value)
if got := identity.Valid(); got != tt.valid {
t.Fatalf("Valid(%q) = %t, want %t", tt.value, got, tt.valid)
}
})
}
}

func FuzzDiameterIdentityValid(f *testing.F) {
for _, value := range []string{"", "peer.example", "Peer-1.Example.", "xn--bcher-kva.example", "_diameter._tcp.example", "peer\xff.example", "peer..example", "K.example", strings.Repeat("a", 64)} {
f.Add(value)
}
f.Fuzz(func(t *testing.T, value string) {
valid := DiameterIdentity(value).Valid()
// ASCII case does not affect the accepted label syntax.
lower := []byte(value)
for i, c := range lower {
if c >= 'A' && c <= 'Z' {
lower[i] = c + ('a' - 'A')
}
}
if valid != DiameterIdentity(lower).Valid() {
t.Fatalf("ASCII case changed validity: %q, %q", value, lower)
}
if valid {
unrooted := strings.TrimSuffix(value, ".")
if !DiameterIdentity(unrooted).Valid() || !DiameterIdentity(unrooted+".").Valid() {
t.Fatalf("optional root dot changed validity: %q", value)
}
}
})
}
39 changes: 7 additions & 32 deletions diam/internal/base/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ func validateTypes(items []*diam.AVP, dictionary *dict.Snapshot) *diam.MessageEr
}
}
case datatype.DiameterIdentityType:
invalid = invalid || !validIdentity(a.Data.Serialize())
if !invalid {
id, ok := a.Data.(datatype.DiameterIdentity)
if !ok {
id = datatype.DiameterIdentity(a.Data.Serialize())
}
invalid = !id.Valid()
}
case datatype.UTF8StringType:
invalid = invalid || !utf8.Valid(a.Data.Serialize())
}
Expand Down Expand Up @@ -138,37 +144,6 @@ func validateTypes(items []*diam.AVP, dictionary *dict.Snapshot) *diam.MessageEr
return nil
}

// RFC 6733 §4.3.1 and Appendix D require ASCII FQDN/realm form. Accept
// case, numeric labels, A-labels and the optional final DNS root dot.
func validIdentity(name []byte) bool {
if len(name) > 0 && name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
if len(name) == 0 || len(name) > 253 {
return false
}
label := 0
for i, c := range name {
if c == '.' {
if label == 0 || name[i-1] == '-' {
return false
}
label = 0
continue
}
letter := c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
digit := c >= '0' && c <= '9'
if !letter && !digit && (c != '-' || label == 0) {
return false
}
label++
if label > 63 {
return false
}
}
return label > 0 && name[len(name)-1] != '-'
}

// RequestError is a shared rejection decision; the adapters retain ownership of
// reply delivery and connection closure.
type RequestError struct {
Expand Down
Loading