@@ -12,6 +12,31 @@ import (
1212 "github.com/stretchr/testify/require"
1313)
1414
15+ func TestNormalizeDNSName (t * testing.T ) {
16+ t .Parallel ()
17+
18+ tests := []struct {
19+ in , want string
20+ }{
21+ {"example.com" , "example.com" },
22+ {"example.com." , "example.com" },
23+ {"Example.COM." , "example.com" },
24+ {"API.GitHub.com" , "api.github.com" },
25+ {"" , "" },
26+ {"." , "" },
27+ // Only a single trailing dot is stripped; double-trailing is a
28+ // malformed name and should not be coerced.
29+ {"foo.." , "foo." },
30+ }
31+
32+ for _ , tt := range tests {
33+ t .Run (tt .in , func (t * testing.T ) {
34+ t .Parallel ()
35+ assert .Equal (t , tt .want , normalizeDNSName (tt .in ))
36+ })
37+ }
38+ }
39+
1540func TestParseDNSQuery (t * testing.T ) {
1641 t .Parallel ()
1742
@@ -121,13 +146,114 @@ func TestParseDNSResponse(t *testing.T) {
121146
122147 qname , ips , ttl , err := ParseDNSResponse (payload )
123148 require .NoError (t , err )
124- assert .Equal (t , "example.com. " , qname )
149+ assert .Equal (t , "example.com" , qname )
125150 assert .Len (t , ips , 2 )
126151 assert .Equal (t , "93.184.216.34" , ips [0 ].String ())
127152 assert .Equal (t , "93.184.216.35" , ips [1 ].String ())
128153 assert .Equal (t , uint32 (60 ), ttl ) // minimum TTL
129154}
130155
156+ func TestParseDNSResponse_DropsOutOfBailiwickAnswers (t * testing.T ) {
157+ t .Parallel ()
158+
159+ // Question for example.com; attacker-controlled response slips an A
160+ // record with a different owner name (typical out-of-bailiwick injection
161+ // attempt to smuggle an internal IP into dynamic rules).
162+ msg := & dns.Msg {
163+ MsgHdr : dns.MsgHdr {Id : 0x1234 , Response : true },
164+ Question : []dns.Question {
165+ {Name : "example.com." , Qtype : dns .TypeA , Qclass : dns .ClassINET },
166+ },
167+ Answer : []dns.RR {
168+ & dns.A {
169+ Hdr : dns.RR_Header {Name : "example.com." , Rrtype : dns .TypeA , Class : dns .ClassINET , Ttl : 300 },
170+ A : net .ParseIP ("93.184.216.34" ),
171+ },
172+ & dns.A {
173+ Hdr : dns.RR_Header {Name : "internal.local." , Rrtype : dns .TypeA , Class : dns .ClassINET , Ttl : 300 },
174+ A : net .ParseIP ("169.254.169.254" ),
175+ },
176+ },
177+ }
178+ payload , err := msg .Pack ()
179+ require .NoError (t , err )
180+
181+ qname , ips , _ , err := ParseDNSResponse (payload )
182+ require .NoError (t , err )
183+ assert .Equal (t , "example.com" , qname )
184+ require .Len (t , ips , 1 )
185+ assert .Equal (t , "93.184.216.34" , ips [0 ].String ())
186+ }
187+
188+ func TestParseDNSResponse_FollowsCNAMEChain (t * testing.T ) {
189+ t .Parallel ()
190+
191+ // Legitimate CNAME chain: example.com -> cdn.example.net -> 1.2.3.4.
192+ // The A record's owner matches the CNAME target, which is reachable
193+ // from the question via the chain, so the IP is accepted.
194+ msg := & dns.Msg {
195+ MsgHdr : dns.MsgHdr {Id : 0x2345 , Response : true },
196+ Question : []dns.Question {
197+ {Name : "example.com." , Qtype : dns .TypeA , Qclass : dns .ClassINET },
198+ },
199+ Answer : []dns.RR {
200+ & dns.CNAME {
201+ Hdr : dns.RR_Header {Name : "example.com." , Rrtype : dns .TypeCNAME , Class : dns .ClassINET , Ttl : 300 },
202+ Target : "cdn.example.net." ,
203+ },
204+ & dns.A {
205+ Hdr : dns.RR_Header {Name : "cdn.example.net." , Rrtype : dns .TypeA , Class : dns .ClassINET , Ttl : 300 },
206+ A : net .ParseIP ("1.2.3.4" ),
207+ },
208+ },
209+ }
210+ payload , err := msg .Pack ()
211+ require .NoError (t , err )
212+
213+ qname , ips , _ , err := ParseDNSResponse (payload )
214+ require .NoError (t , err )
215+ assert .Equal (t , "example.com" , qname )
216+ require .Len (t , ips , 1 )
217+ assert .Equal (t , "1.2.3.4" , ips [0 ].String ())
218+ }
219+
220+ func TestParseDNSResponse_DropsUnreachableCNAMEA (t * testing.T ) {
221+ t .Parallel ()
222+
223+ // An A record whose owner is NOT reachable via any CNAME chain from
224+ // the question must be dropped even if another A record from the same
225+ // name-chain is valid.
226+ msg := & dns.Msg {
227+ MsgHdr : dns.MsgHdr {Id : 0x3456 , Response : true },
228+ Question : []dns.Question {
229+ {Name : "example.com." , Qtype : dns .TypeA , Qclass : dns .ClassINET },
230+ },
231+ Answer : []dns.RR {
232+ & dns.CNAME {
233+ Hdr : dns.RR_Header {Name : "example.com." , Rrtype : dns .TypeCNAME , Class : dns .ClassINET , Ttl : 300 },
234+ Target : "cdn.example.net." ,
235+ },
236+ // A record for an unrelated name slipped into the Answer section.
237+ & dns.A {
238+ Hdr : dns.RR_Header {Name : "attacker.example.net." , Rrtype : dns .TypeA , Class : dns .ClassINET , Ttl : 300 },
239+ A : net .ParseIP ("10.0.0.1" ),
240+ },
241+ // Legitimate A record at the CNAME target.
242+ & dns.A {
243+ Hdr : dns.RR_Header {Name : "cdn.example.net." , Rrtype : dns .TypeA , Class : dns .ClassINET , Ttl : 300 },
244+ A : net .ParseIP ("1.2.3.4" ),
245+ },
246+ },
247+ }
248+ payload , err := msg .Pack ()
249+ require .NoError (t , err )
250+
251+ _ , ips , _ , err := ParseDNSResponse (payload )
252+ require .NoError (t , err )
253+ require .Len (t , ips , 1 )
254+ assert .Equal (t , "1.2.3.4" , ips [0 ].String ())
255+ }
256+
131257func TestParseDNSResponse_NoARecords (t * testing.T ) {
132258 t .Parallel ()
133259
0 commit comments