Skip to content

Commit feda877

Browse files
shane-ns1miekg
andauthored
Properly parse alpn values in SVCB (#1363)
* Modify the SVCBAlpn to properly parse/print * Remove debug * Change SVCB test from reflect to loop * Refactor SVCB code to reduce indentation * When stringifying SVCBAlpn, use strings.Builder for whole process * Update comment in svcb.go Co-authored-by: Miek Gieben <[email protected]> * Describe why we use a specific size for the alpn buffer Co-authored-by: Miek Gieben <[email protected]>
1 parent 0d2c95b commit feda877

File tree

3 files changed

+136
-6
lines changed

3 files changed

+136
-6
lines changed

parse_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1650,8 +1650,8 @@ func TestParseSVCB(t *testing.T) {
16501650
`example.com. SVCB 1 foo.example.com. ipv6hint="2001:db8::1,2001:db8::53:1"`: `example.com. 3600 IN SVCB 1 foo.example.com. ipv6hint="2001:db8::1,2001:db8::53:1"`,
16511651
`example.com. SVCB 1 example.com. ipv6hint="2001:db8::198.51.100.100"`: `example.com. 3600 IN SVCB 1 example.com. ipv6hint="2001:db8::c633:6464"`,
16521652
`example.com. SVCB 16 foo.example.org. alpn=h2,h3-19 mandatory=ipv4hint,alpn ipv4hint=192.0.2.1`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="h2,h3-19" mandatory="ipv4hint,alpn" ipv4hint="192.0.2.1"`,
1653-
`example.com. SVCB 16 foo.example.org. alpn="f\\\\oo\\,bar,h2"`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="f\\\\oo\\,bar,h2"`,
1654-
`example.com. SVCB 16 foo.example.org. alpn=f\\\092oo\092,bar,h2`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="f\\\092oo\092,bar,h2"`,
1653+
`example.com. SVCB 16 foo.example.org. alpn="f\\\\oo\\,bar,h2"`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="f\\\092oo\\\044bar,h2"`,
1654+
`example.com. SVCB 16 foo.example.org. alpn=f\\\092oo\092,bar,h2`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="f\\\092oo\\\044bar,h2"`,
16551655
// From draft-ietf-add-ddr-06
16561656
`_dns.example.net. SVCB 1 example.net. alpn=h2 dohpath=/dns-query{?dns}`: `_dns.example.net. 3600 IN SVCB 1 example.net. alpn="h2" dohpath="/dns-query{?dns}"`,
16571657
}
@@ -1704,6 +1704,10 @@ func TestParseBadSVCB(t *testing.T) {
17041704
`1 . ipv4hint=`, // empty ipv4
17051705
`1 . port=`, // empty port
17061706
`1 . echconfig=YUd`, // bad base64
1707+
`1 . alpn=h\`, // unterminated escape
1708+
`1 . alpn=h2\\.h3`, // comma-separated list with bad character
1709+
`1 . alpn=h2,,h3`, // empty protocol identifier
1710+
`1 . alpn=h3,`, // final protocol identifier empty
17071711
}
17081712
for _, o := range evils {
17091713
_, err := NewRR(header + o)

svcb.go

+88-4
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,57 @@ func (s *SVCBMandatory) copy() SVCBKeyValue {
342342
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
343343
// e := new(dns.SVCBAlpn)
344344
// e.Alpn = []string{"h2", "http/1.1"}
345-
// h.Value = append(o.Value, e)
345+
// h.Value = append(h.Value, e)
346346
type SVCBAlpn struct {
347347
Alpn []string
348348
}
349349

350-
func (*SVCBAlpn) Key() SVCBKey { return SVCB_ALPN }
351-
func (s *SVCBAlpn) String() string { return strings.Join(s.Alpn, ",") }
350+
func (*SVCBAlpn) Key() SVCBKey { return SVCB_ALPN }
351+
352+
func (s *SVCBAlpn) String() string {
353+
// An ALPN value is a comma-separated list of values, each of which can be
354+
// an arbitrary binary value. In order to allow parsing, the comma and
355+
// backslash characters are themselves excaped.
356+
//
357+
// However, this escaping is done in addition to the normal escaping which
358+
// happens in zone files, meaning that these values must be
359+
// double-escaped. This looks terrible, so if you see a never-ending
360+
// sequence of backslash in a zone file this may be why.
361+
//
362+
// https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-08#appendix-A.1
363+
var str strings.Builder
364+
for i, alpn := range s.Alpn {
365+
// 4*len(alpn) is the worst case where we escape every character in the alpn as \123, plus 1 byte for the ',' separating the alpn from others
366+
str.Grow(4*len(alpn) + 1)
367+
if i > 0 {
368+
str.WriteByte(',')
369+
}
370+
for j := 0; j < len(alpn); j++ {
371+
e := alpn[j]
372+
if ' ' > e || e > '~' {
373+
str.WriteString(escapeByte(e))
374+
continue
375+
}
376+
switch e {
377+
// We escape a few characters which may confuse humans or parsers.
378+
case '"', ';', ' ':
379+
str.WriteByte('\\')
380+
str.WriteByte(e)
381+
// The comma and backslash characters themselves must be
382+
// doubly-escaped. We use `\\` for the first backslash and
383+
// the escaped numeric value for the other value. We especially
384+
// don't want a comma in the output.
385+
case ',':
386+
str.WriteString(`\\\044`)
387+
case '\\':
388+
str.WriteString(`\\\092`)
389+
default:
390+
str.WriteByte(e)
391+
}
392+
}
393+
}
394+
return str.String()
395+
}
352396

353397
func (s *SVCBAlpn) pack() ([]byte, error) {
354398
// Liberally estimate the size of an alpn as 10 octets
@@ -383,7 +427,47 @@ func (s *SVCBAlpn) unpack(b []byte) error {
383427
}
384428

385429
func (s *SVCBAlpn) parse(b string) error {
386-
s.Alpn = strings.Split(b, ",")
430+
if len(b) == 0 {
431+
s.Alpn = []string{}
432+
return nil
433+
}
434+
435+
alpn := []string{}
436+
a := []byte{}
437+
for p := 0; p < len(b); {
438+
c, q := nextByte(b, p)
439+
if q == 0 {
440+
return errors.New("dns: svcbalpn: unterminated escape")
441+
}
442+
p += q
443+
// If we find a comma, we have finished reading an alpn.
444+
if c == ',' {
445+
if len(a) == 0 {
446+
return errors.New("dns: svcbalpn: empty protocol identifier")
447+
}
448+
alpn = append(alpn, string(a))
449+
a = []byte{}
450+
continue
451+
}
452+
// If it's a backslash, we need to handle a comma-separated list.
453+
if c == '\\' {
454+
dc, dq := nextByte(b, p)
455+
if dq == 0 {
456+
return errors.New("dns: svcbalpn: unterminated escape decoding comma-separated list")
457+
}
458+
if dc != '\\' && dc != ',' {
459+
return errors.New("dns: svcbalpn: bad escaped character decoding comma-separated list")
460+
}
461+
p += dq
462+
c = dc
463+
}
464+
a = append(a, c)
465+
}
466+
// Add the final alpn.
467+
if len(a) == 0 {
468+
return errors.New("dns: svcbalpn: last protocol identifier empty")
469+
}
470+
s.Alpn = append(alpn, string(a))
387471
return nil
388472
}
389473

svcb_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,48 @@ func TestDecodeBadSVCB(t *testing.T) {
9595
}
9696
}
9797

98+
func TestPresentationSVCBAlpn(t *testing.T) {
99+
tests := map[string]string{
100+
"h2": "h2",
101+
"http": "http",
102+
"\xfa": `\250`,
103+
"some\"other,chars": `some\"other\\\044chars`,
104+
}
105+
for input, want := range tests {
106+
e := new(SVCBAlpn)
107+
e.Alpn = []string{input}
108+
if e.String() != want {
109+
t.Errorf("improper conversion with String(), wanted %v got %v", want, e.String())
110+
}
111+
}
112+
}
113+
114+
func TestSVCBAlpn(t *testing.T) {
115+
tests := map[string][]string{
116+
`. 1 IN SVCB 10 one.test. alpn=h2`: {"h2"},
117+
`. 2 IN SVCB 20 two.test. alpn=h2,h3-19`: {"h2", "h3-19"},
118+
`. 3 IN SVCB 30 three.test. alpn="f\\\\oo\\,bar,h2"`: {`f\oo,bar`, "h2"},
119+
`. 4 IN SVCB 40 four.test. alpn="part1,part2,part3\\,part4\\\\"`: {"part1", "part2", `part3,part4\`},
120+
`. 5 IN SVCB 50 five.test. alpn=part1\,\p\a\r\t2\044part3\092,part4\092\\`: {"part1", "part2", `part3,part4\`},
121+
}
122+
for s, v := range tests {
123+
rr, err := NewRR(s)
124+
if err != nil {
125+
t.Error("failed to parse RR: ", err)
126+
continue
127+
}
128+
alpn := rr.(*SVCB).Value[0].(*SVCBAlpn).Alpn
129+
if len(v) != len(alpn) {
130+
t.Fatalf("parsing alpn failed, wanted %v got %v", v, alpn)
131+
}
132+
for i := range v {
133+
if v[i] != alpn[i] {
134+
t.Fatalf("parsing alpn failed, wanted %v got %v", v, alpn)
135+
}
136+
}
137+
}
138+
}
139+
98140
func TestCompareSVCB(t *testing.T) {
99141
val1 := []SVCBKeyValue{
100142
&SVCBPort{

0 commit comments

Comments
 (0)