/ components / egress / pkg / dnsproxy / proxy_test.go
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  }