proxy_test.go
1 // Copyright 2026 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package dnsproxy 16 17 import ( 18 "net" 19 "testing" 20 "time" 21 22 "github.com/miekg/dns" 23 "github.com/stretchr/testify/require" 24 25 "github.com/alibaba/opensandbox/egress/pkg/nftables" 26 "github.com/alibaba/opensandbox/egress/pkg/policy" 27 ) 28 29 func TestProxyUpdatePolicy(t *testing.T) { 30 proxy, err := New(nil, "127.0.0.1:15353", nil, nil) 31 require.NoError(t, err, "init proxy") 32 33 require.NotNil(t, proxy.CurrentPolicy(), "expected default deny policy (non-nil)") 34 require.Equal(t, policy.ActionDeny, proxy.CurrentPolicy().Evaluate("example.com."), "expected default deny") 35 36 pol, err := policy.ParsePolicy(`{"defaultAction":"deny","egress":[{"action":"allow","target":"example.com"}]}`) 37 require.NoError(t, err, "parse policy") 38 39 proxy.UpdatePolicy(pol) 40 require.NotNil(t, proxy.CurrentPolicy(), "expected policy after update") 41 require.Equal(t, policy.ActionAllow, proxy.CurrentPolicy().Evaluate("example.com."), "policy evaluation mismatch") 42 43 proxy.UpdatePolicy(nil) 44 require.NotNil(t, proxy.CurrentPolicy(), "expected default deny policy after clearing") 45 require.Equal(t, policy.ActionDeny, proxy.CurrentPolicy().Evaluate("example.com."), "expected default deny after clearing") 46 } 47 48 func TestProxyAlwaysOverlayPrecedence(t *testing.T) { 49 deny, err := policy.ParseValidatedEgressRule(policy.ActionDeny, "nope.test") 50 require.NoError(t, err) 51 pol, err := policy.ParsePolicy(`{"defaultAction":"deny","egress":[{"action":"allow","target":"nope.test"}]}`) 52 require.NoError(t, err) 53 proxy, err := New(pol, "127.0.0.1:15353", []policy.EgressRule{deny}, nil) 54 require.NoError(t, err) 55 require.Equal(t, policy.ActionAllow, proxy.CurrentPolicy().Evaluate("nope.test."), "user policy without overlay") 56 require.Equal(t, policy.ActionDeny, proxy.effectivePolicy.Evaluate("nope.test."), "effective policy includes always deny") 57 } 58 59 func TestExtractResolvedIPs(t *testing.T) { 60 msg := new(dns.Msg) 61 msg.Answer = []dns.RR{ 62 &dns.A{Hdr: dns.RR_Header{Name: "example.com.", Ttl: 120}, A: net.ParseIP("1.2.3.4")}, 63 &dns.AAAA{Hdr: dns.RR_Header{Name: "example.com.", Ttl: 60}, AAAA: net.ParseIP("2001:db8::1")}, 64 &dns.A{Hdr: dns.RR_Header{Name: "example.com.", Ttl: 90}, A: net.ParseIP("5.6.7.8")}, 65 } 66 ips := extractResolvedIPs(msg) 67 require.Len(t, ips, 3, "expected 3 IPs") 68 // Order follows Answer; check first A and AAAA 69 require.Equal(t, "1.2.3.4", ips[0].Addr.String(), "first IP mismatch") 70 require.Equal(t, 120*time.Second, ips[0].TTL, "first IP TTL mismatch") 71 require.Equal(t, "2001:db8::1", ips[1].Addr.String(), "second IP mismatch") 72 require.Equal(t, 60*time.Second, ips[1].TTL, "second IP TTL mismatch") 73 require.Equal(t, "5.6.7.8", ips[2].Addr.String(), "third IP mismatch") 74 require.Equal(t, 90*time.Second, ips[2].TTL, "third IP TTL mismatch") 75 } 76 77 func TestExtractResolvedIPs_EmptyOrNil(t *testing.T) { 78 require.Nil(t, extractResolvedIPs(nil), "nil msg: expected nil") 79 msg := new(dns.Msg) 80 require.Nil(t, extractResolvedIPs(msg), "empty answer: expected nil") 81 msg.Answer = []dns.RR{&dns.CNAME{Hdr: dns.RR_Header{Name: "x."}, Target: "y."}} 82 require.Nil(t, extractResolvedIPs(msg), "CNAME only: expected nil") 83 } 84 85 func TestSetOnResolved(t *testing.T) { 86 proxy, err := New(policy.DefaultDenyPolicy(), "", nil, nil) 87 require.NoError(t, err) 88 var called bool 89 var capturedDomain string 90 var capturedIPs []nftables.ResolvedIP 91 proxy.SetOnResolved(func(domain string, ips []nftables.ResolvedIP) { 92 called = true 93 capturedDomain = domain 94 capturedIPs = ips 95 }) 96 require.NotNil(t, proxy.onResolved, "SetOnResolved did not set callback") 97 proxy.SetOnResolved(nil) 98 require.Nil(t, proxy.onResolved, "SetOnResolved(nil) did not clear callback") 99 _ = called 100 _ = capturedDomain 101 _ = capturedIPs 102 } 103 104 func TestMaybeNotifyResolved_CallsCallbackWhenAOrAAAA(t *testing.T) { 105 proxy, err := New(policy.DefaultDenyPolicy(), "", nil, nil) 106 require.NoError(t, err) 107 ch := make(chan struct { 108 domain string 109 ips []nftables.ResolvedIP 110 }, 1) 111 proxy.SetOnResolved(func(domain string, ips []nftables.ResolvedIP) { 112 ch <- struct { 113 domain string 114 ips []nftables.ResolvedIP 115 }{domain, ips} 116 }) 117 118 msg := new(dns.Msg) 119 msg.Answer = []dns.RR{ 120 &dns.A{Hdr: dns.RR_Header{Name: "example.com.", Ttl: 120}, A: net.ParseIP("1.2.3.4")}, 121 } 122 proxy.maybeNotifyResolved("example.com.", msg) 123 124 select { 125 case got := <-ch: 126 require.Equal(t, "example.com.", got.domain, "domain mismatch") 127 require.Len(t, got.ips, 1, "expected one resolved IP") 128 require.Equal(t, "1.2.3.4", got.ips[0].Addr.String(), "resolved IP mismatch") 129 case <-time.After(2 * time.Second): 130 require.FailNow(t, "callback was not invoked") 131 } 132 } 133 134 func TestMaybeNotifyResolved_NoCallWhenOnResolvedNil(t *testing.T) { 135 proxy, err := New(policy.DefaultDenyPolicy(), "", nil, nil) 136 require.NoError(t, err) 137 msg := new(dns.Msg) 138 msg.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: "x.", Ttl: 60}, A: net.ParseIP("10.0.0.1")}} 139 proxy.maybeNotifyResolved("x.", msg) 140 // No callback set; should not panic. No assertion needed. 141 } 142 143 func TestMaybeNotifyResolved_NoCallWhenNoAOrAAAA(t *testing.T) { 144 proxy, err := New(policy.DefaultDenyPolicy(), "", nil, nil) 145 require.NoError(t, err) 146 ch := make(chan struct { 147 domain string 148 ips []nftables.ResolvedIP 149 }, 1) 150 proxy.SetOnResolved(func(domain string, ips []nftables.ResolvedIP) { 151 ch <- struct { 152 domain string 153 ips []nftables.ResolvedIP 154 }{domain, ips} 155 }) 156 157 msg := new(dns.Msg) 158 msg.Answer = []dns.RR{&dns.CNAME{Hdr: dns.RR_Header{Name: "x."}, Target: "y."}} 159 proxy.maybeNotifyResolved("x.", msg) 160 161 select { 162 case <-ch: 163 require.FailNow(t, "callback should not be invoked when resp has no A/AAAA") 164 case <-time.After(200 * time.Millisecond): 165 // Expected: no callback 166 } 167 }