@@ -57,7 +57,7 @@ func TestDNSInterceptor_AllowedQuery(t *testing.T) {
5757
5858 policy := NewPolicy ([]HostSpec {{Name : "api.github.com" }})
5959 dr := firewall .NewDynamicRules ()
60- interceptor := NewDNSInterceptor (policy , dr )
60+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
6161
6262 frame := buildDNSQueryFrame (testSrcMAC , testDstMAC , testSrcIP , testDstIP , 12345 , "api.github.com" )
6363 hdr := firewall .ParseHeaders (frame )
@@ -71,7 +71,7 @@ func TestDNSInterceptor_BlockedQuery(t *testing.T) {
7171
7272 policy := NewPolicy ([]HostSpec {{Name : "api.github.com" }})
7373 dr := firewall .NewDynamicRules ()
74- interceptor := NewDNSInterceptor (policy , dr )
74+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
7575
7676 frame := buildDNSQueryFrame (testSrcMAC , testDstMAC , testSrcIP , testDstIP , 12345 , "evil.com" )
7777 hdr := firewall .ParseHeaders (frame )
@@ -95,7 +95,7 @@ func TestDNSInterceptor_WildcardAllowed(t *testing.T) {
9595
9696 policy := NewPolicy ([]HostSpec {{Name : "*.docker.io" }})
9797 dr := firewall .NewDynamicRules ()
98- interceptor := NewDNSInterceptor (policy , dr )
98+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
9999
100100 frame := buildDNSQueryFrame (testSrcMAC , testDstMAC , testSrcIP , testDstIP , 12345 , "registry-1.docker.io" )
101101 hdr := firewall .ParseHeaders (frame )
@@ -109,18 +109,18 @@ func TestDNSInterceptor_ResponseSnooping(t *testing.T) {
109109
110110 policy := NewPolicy ([]HostSpec {{Name : "api.github.com" , Ports : []uint16 {443 }}})
111111 dr := firewall .NewDynamicRules ()
112- interceptor := NewDNSInterceptor (policy , dr )
112+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
113113
114114 ips := []net.IP {net .ParseIP ("140.82.121.5" ), net .ParseIP ("140.82.121.6" )}
115115 frame := buildDNSResponseFrame (testDstMAC , testSrcMAC , testDstIP , testSrcIP , 12345 , "api.github.com" , ips , 300 )
116116 hdr := firewall .ParseHeaders (frame )
117117
118118 interceptor .HandleIngress (frame , hdr )
119119
120- // Should have created dynamic rules for both IPs.
121- assert .Equal (t , 2 , dr .Len ())
120+ // Protocol unset → TCP+UDP per IP. 2 IPs × 2 protocols = 4 rules .
121+ assert .Equal (t , 4 , dr .Len ())
122122
123- // Verify that the rules match egress traffic to the IPs.
123+ // Verify that the rules match TCP egress traffic to the IPs.
124124 hdr1 := & firewall.PacketHeader {
125125 DstIP : [4 ]byte {140 , 82 , 121 , 5 },
126126 Protocol : 6 ,
@@ -130,6 +130,16 @@ func TestDNSInterceptor_ResponseSnooping(t *testing.T) {
130130 require .True (t , ok )
131131 assert .Equal (t , firewall .Allow , action )
132132
133+ // UDP to the same port should also match.
134+ hdr1udp := & firewall.PacketHeader {
135+ DstIP : [4 ]byte {140 , 82 , 121 , 5 },
136+ Protocol : 17 ,
137+ DstPort : 443 ,
138+ }
139+ action , ok = dr .Match (firewall .Egress , hdr1udp )
140+ require .True (t , ok )
141+ assert .Equal (t , firewall .Allow , action )
142+
133143 // Wrong port should not match.
134144 hdr2 := & firewall.PacketHeader {
135145 DstIP : [4 ]byte {140 , 82 , 121 , 5 },
@@ -145,14 +155,17 @@ func TestDNSInterceptor_ResponseSnooping_AllPorts(t *testing.T) {
145155
146156 policy := NewPolicy ([]HostSpec {{Name : "example.com" }})
147157 dr := firewall .NewDynamicRules ()
148- interceptor := NewDNSInterceptor (policy , dr )
158+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
149159
150160 ips := []net.IP {net .ParseIP ("93.184.216.34" )}
151161 frame := buildDNSResponseFrame (testDstMAC , testSrcMAC , testDstIP , testSrcIP , 12345 , "example.com" , ips , 60 )
152162 hdr := firewall .ParseHeaders (frame )
153163
154164 interceptor .HandleIngress (frame , hdr )
155165
166+ // Protocol unset → TCP+UDP. 1 IP × 2 protocols = 2 rules.
167+ assert .Equal (t , 2 , dr .Len ())
168+
156169 // Rule should match any port for TCP.
157170 hdr1 := & firewall.PacketHeader {
158171 DstIP : [4 ]byte {93 , 184 , 216 , 34 },
@@ -162,14 +175,24 @@ func TestDNSInterceptor_ResponseSnooping_AllPorts(t *testing.T) {
162175 action , ok := dr .Match (firewall .Egress , hdr1 )
163176 require .True (t , ok )
164177 assert .Equal (t , firewall .Allow , action )
178+
179+ // Rule should also match UDP.
180+ hdr1udp := & firewall.PacketHeader {
181+ DstIP : [4 ]byte {93 , 184 , 216 , 34 },
182+ Protocol : 17 ,
183+ DstPort : 443 ,
184+ }
185+ action , ok = dr .Match (firewall .Egress , hdr1udp )
186+ require .True (t , ok )
187+ assert .Equal (t , firewall .Allow , action )
165188}
166189
167190func TestDNSInterceptor_ResponseSnooping_NotAllowed (t * testing.T ) {
168191 t .Parallel ()
169192
170193 policy := NewPolicy ([]HostSpec {{Name : "allowed.com" }})
171194 dr := firewall .NewDynamicRules ()
172- interceptor := NewDNSInterceptor (policy , dr )
195+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
173196
174197 // Response for a host not in the policy — should be ignored.
175198 ips := []net.IP {net .ParseIP ("1.2.3.4" )}
@@ -186,7 +209,7 @@ func TestDNSInterceptor_ResponseNXDOMAINFrameFormat(t *testing.T) {
186209
187210 policy := NewPolicy ([]HostSpec {{Name : "allowed.com" }})
188211 dr := firewall .NewDynamicRules ()
189- interceptor := NewDNSInterceptor (policy , dr )
212+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
190213
191214 frame := buildDNSQueryFrame (testSrcMAC , testDstMAC , testSrcIP , testDstIP , 54321 , "blocked.com" )
192215 hdr := firewall .ParseHeaders (frame )
@@ -220,3 +243,94 @@ func TestDNSInterceptor_ResponseNXDOMAINFrameFormat(t *testing.T) {
220243 expectedIPLen := uint16 (ihl + 8 + len (result .ResponseFrame ) - (ipStart + ihl + 8 ))
221244 assert .Equal (t , expectedIPLen , ipTotalLen )
222245}
246+
247+ func TestDNSInterceptor_UnparseableQuery_Dropped (t * testing.T ) {
248+ t .Parallel ()
249+
250+ policy := NewPolicy ([]HostSpec {{Name : "allowed.com" }})
251+ dr := firewall .NewDynamicRules ()
252+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
253+
254+ // Build a frame with valid Ethernet+IP+UDP headers but garbage DNS payload.
255+ frame := buildTestUDPFrame (testSrcMAC , testDstMAC , testSrcIP , testDstIP , 12345 , 53 , []byte {0xDE , 0xAD })
256+ hdr := firewall .ParseHeaders (frame )
257+
258+ result := interceptor .HandleEgress (frame , hdr )
259+ assert .Equal (t , firewall .InterceptDrop , result .Action ,
260+ "unparseable DNS queries must be dropped, not forwarded" )
261+ }
262+
263+ func TestDNSInterceptor_MultipleQuestions_Dropped (t * testing.T ) {
264+ t .Parallel ()
265+
266+ policy := NewPolicy ([]HostSpec {{Name : "allowed.com" }})
267+ dr := firewall .NewDynamicRules ()
268+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
269+
270+ // Build a DNS query with two questions.
271+ msg := & mdns.Msg {
272+ MsgHdr : mdns.MsgHdr {Id : 0x5678 , RecursionDesired : true },
273+ Question : []mdns.Question {
274+ {Name : mdns .Fqdn ("allowed.com" ), Qtype : mdns .TypeA , Qclass : mdns .ClassINET },
275+ {Name : mdns .Fqdn ("evil.com" ), Qtype : mdns .TypeA , Qclass : mdns .ClassINET },
276+ },
277+ }
278+ payload , _ := msg .Pack ()
279+ frame := buildTestUDPFrame (testSrcMAC , testDstMAC , testSrcIP , testDstIP , 12345 , 53 , payload )
280+ hdr := firewall .ParseHeaders (frame )
281+
282+ result := interceptor .HandleEgress (frame , hdr )
283+ assert .Equal (t , firewall .InterceptDrop , result .Action ,
284+ "queries with multiple questions must be dropped" )
285+ }
286+
287+ func TestDNSInterceptor_ResponseFromNonGateway_Ignored (t * testing.T ) {
288+ t .Parallel ()
289+
290+ policy := NewPolicy ([]HostSpec {{Name : "allowed.com" }})
291+ dr := firewall .NewDynamicRules ()
292+ gatewayIP := [4 ]byte {192 , 168 , 127 , 1 }
293+ interceptor := NewDNSInterceptor (policy , dr , gatewayIP )
294+
295+ // Build a response that looks like it comes from a non-gateway source.
296+ nonGatewayIP := [4 ]byte {10 , 0 , 0 , 99 }
297+ ips := []net.IP {net .ParseIP ("1.2.3.4" )}
298+ frame := buildDNSResponseFrame (testDstMAC , testSrcMAC , nonGatewayIP , testSrcIP , 12345 , "allowed.com" , ips , 300 )
299+ hdr := firewall .ParseHeaders (frame )
300+
301+ interceptor .HandleIngress (frame , hdr )
302+
303+ assert .Equal (t , 0 , dr .Len (),
304+ "DNS responses from non-gateway sources must not create dynamic rules" )
305+ }
306+
307+ func TestDNSInterceptor_ExplicitProtocol_SingleRule (t * testing.T ) {
308+ t .Parallel ()
309+
310+ // When Protocol is explicitly TCP, only TCP rules should be created.
311+ policy := NewPolicy ([]HostSpec {{Name : "example.com" , Ports : []uint16 {443 }, Protocol : 6 }})
312+ dr := firewall .NewDynamicRules ()
313+ interceptor := NewDNSInterceptor (policy , dr , testDstIP )
314+
315+ ips := []net.IP {net .ParseIP ("93.184.216.34" )}
316+ frame := buildDNSResponseFrame (testDstMAC , testSrcMAC , testDstIP , testSrcIP , 12345 , "example.com" , ips , 60 )
317+ hdr := firewall .ParseHeaders (frame )
318+
319+ interceptor .HandleIngress (frame , hdr )
320+
321+ // Explicit TCP → 1 rule per IP.
322+ assert .Equal (t , 1 , dr .Len ())
323+
324+ // TCP should match.
325+ action , ok := dr .Match (firewall .Egress , & firewall.PacketHeader {
326+ DstIP : [4 ]byte {93 , 184 , 216 , 34 }, Protocol : 6 , DstPort : 443 ,
327+ })
328+ require .True (t , ok )
329+ assert .Equal (t , firewall .Allow , action )
330+
331+ // UDP should not match.
332+ _ , ok = dr .Match (firewall .Egress , & firewall.PacketHeader {
333+ DstIP : [4 ]byte {93 , 184 , 216 , 34 }, Protocol : 17 , DstPort : 443 ,
334+ })
335+ assert .False (t , ok )
336+ }
0 commit comments