Skip to content

Commit d081f81

Browse files
Dhruv-Jdj014275
and
dj014275
authored
Add antctl get fqdncache (#6868)
This PR adds functionality for a new antctl command: `antctl get fqdncache` This command fetches the DNS mapping entries for FQDN policies by reading the cache for DNS entries and outputting FQDN name, associated IP, and expiration time. It can also be used with a --domain (-d) flag that can be specified to filter the result for only a certain FQDN. Signed-off-by: dj014275 <[email protected]> Co-authored-by: dj014275 <[email protected]>
1 parent 1158e90 commit d081f81

File tree

11 files changed

+385
-2
lines changed

11 files changed

+385
-2
lines changed

pkg/agent/apis/types.go

+23
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package apis
1717
import (
1818
"strconv"
1919
"strings"
20+
"time"
2021

2122
corev1 "k8s.io/api/core/v1"
2223

@@ -72,6 +73,28 @@ func (r AntreaAgentInfoResponse) SortRows() bool {
7273
return true
7374
}
7475

76+
type FQDNCacheResponse struct {
77+
FQDNName string `json:"fqdnName,omitempty"`
78+
IPAddress string `json:"ipAddress,omitempty"`
79+
ExpirationTime time.Time `json:"expirationTime,omitempty"`
80+
}
81+
82+
func (r FQDNCacheResponse) GetTableHeader() []string {
83+
return []string{"FQDN", "ADDRESS", "EXPIRATION TIME"}
84+
}
85+
86+
func (r FQDNCacheResponse) GetTableRow(maxColumn int) []string {
87+
return []string{
88+
r.FQDNName,
89+
r.IPAddress,
90+
r.ExpirationTime.String(),
91+
}
92+
}
93+
94+
func (r FQDNCacheResponse) SortRows() bool {
95+
return true
96+
}
97+
7598
type FeatureGateResponse struct {
7699
Component string `json:"component,omitempty"`
77100
Name string `json:"name,omitempty"`

pkg/agent/apiserver/apiserver.go

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"antrea.io/antrea/pkg/agent/apiserver/handlers/bgppolicy"
4141
"antrea.io/antrea/pkg/agent/apiserver/handlers/bgproute"
4242
"antrea.io/antrea/pkg/agent/apiserver/handlers/featuregates"
43+
"antrea.io/antrea/pkg/agent/apiserver/handlers/fqdncache"
4344
"antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist"
4445
"antrea.io/antrea/pkg/agent/apiserver/handlers/multicast"
4546
"antrea.io/antrea/pkg/agent/apiserver/handlers/networkpolicy"
@@ -104,6 +105,7 @@ func installHandlers(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolic
104105
s.Handler.NonGoRestfulMux.HandleFunc("/bgppolicy", bgppolicy.HandleFunc(bgpq))
105106
s.Handler.NonGoRestfulMux.HandleFunc("/bgppeers", bgppeer.HandleFunc(bgpq))
106107
s.Handler.NonGoRestfulMux.HandleFunc("/bgproutes", bgproute.HandleFunc(bgpq))
108+
s.Handler.NonGoRestfulMux.HandleFunc("/fqdncache", fqdncache.HandleFunc(npq))
107109
}
108110

109111
func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) error {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 Antrea Authors
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 fqdncache
16+
17+
import (
18+
"encoding/json"
19+
"net/http"
20+
"net/url"
21+
"regexp"
22+
"strings"
23+
24+
"k8s.io/klog/v2"
25+
26+
agentapi "antrea.io/antrea/pkg/agent/apis"
27+
"antrea.io/antrea/pkg/querier"
28+
)
29+
30+
func HandleFunc(npq querier.AgentNetworkPolicyInfoQuerier) http.HandlerFunc {
31+
return func(w http.ResponseWriter, r *http.Request) {
32+
fqdnFilter, err := newFilterFromURLQuery(r.URL.Query())
33+
if err != nil {
34+
http.Error(w, "Invalid regex: "+err.Error(), http.StatusBadRequest)
35+
klog.ErrorS(err, "Invalid regex")
36+
return
37+
}
38+
dnsEntryCache := npq.GetFQDNCache(fqdnFilter)
39+
resp := make([]agentapi.FQDNCacheResponse, 0, len(dnsEntryCache))
40+
for _, entry := range dnsEntryCache {
41+
resp = append(resp, agentapi.FQDNCacheResponse{
42+
FQDNName: entry.FQDNName,
43+
IPAddress: entry.IPAddress.String(),
44+
ExpirationTime: entry.ExpirationTime,
45+
})
46+
}
47+
if err := json.NewEncoder(w).Encode(resp); err != nil {
48+
http.Error(w, "Failed to encode response: "+err.Error(), http.StatusBadRequest)
49+
klog.ErrorS(err, "Failed to encode response")
50+
return
51+
}
52+
}
53+
}
54+
55+
func newFilterFromURLQuery(query url.Values) (*querier.FQDNCacheFilter, error) {
56+
domain := query.Get("domain")
57+
if domain == "" {
58+
return nil, nil
59+
}
60+
pattern := strings.TrimSpace(domain)
61+
// Replace "." as a regex literal, since it's recogized as a separator in FQDN.
62+
pattern = strings.Replace(pattern, ".", "[.]", -1)
63+
// Replace "*" with ".*".
64+
pattern = strings.Replace(pattern, "*", ".*", -1)
65+
// Anchor the regex match expression.
66+
pattern = "^" + pattern + "$"
67+
68+
regex, err := regexp.Compile(pattern)
69+
if err != nil {
70+
return nil, err
71+
}
72+
return &querier.FQDNCacheFilter{DomainRegex: regex}, nil
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2025 Antrea Authors
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 fqdncache
16+
17+
import (
18+
"encoding/json"
19+
"net"
20+
"net/http"
21+
"net/http/httptest"
22+
"net/url"
23+
"regexp"
24+
"testing"
25+
"time"
26+
27+
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/require"
29+
"go.uber.org/mock/gomock"
30+
31+
"antrea.io/antrea/pkg/agent/apis"
32+
"antrea.io/antrea/pkg/agent/types"
33+
"antrea.io/antrea/pkg/querier"
34+
queriertest "antrea.io/antrea/pkg/querier/testing"
35+
)
36+
37+
func TestFqdnCacheQuery(t *testing.T) {
38+
expirationTime := time.Now().Add(1 * time.Hour).UTC()
39+
tests := []struct {
40+
name string
41+
filteredCacheEntries []types.DnsCacheEntry
42+
expectedResponse []apis.FQDNCacheResponse
43+
}{
44+
{
45+
name: "FQDN cache exists - multiple addresses multiple domains",
46+
filteredCacheEntries: []types.DnsCacheEntry{
47+
{
48+
FQDNName: "example.com",
49+
IPAddress: net.ParseIP("10.0.0.1"),
50+
ExpirationTime: expirationTime,
51+
},
52+
{
53+
FQDNName: "foo.com",
54+
IPAddress: net.ParseIP("10.0.0.4"),
55+
ExpirationTime: expirationTime,
56+
},
57+
{
58+
FQDNName: "bar.com",
59+
IPAddress: net.ParseIP("10.0.0.5"),
60+
ExpirationTime: expirationTime,
61+
},
62+
},
63+
expectedResponse: []apis.FQDNCacheResponse{
64+
{
65+
FQDNName: "example.com",
66+
IPAddress: "10.0.0.1",
67+
ExpirationTime: expirationTime,
68+
},
69+
{
70+
FQDNName: "foo.com",
71+
IPAddress: "10.0.0.4",
72+
ExpirationTime: expirationTime,
73+
},
74+
{
75+
FQDNName: "bar.com",
76+
IPAddress: "10.0.0.5",
77+
ExpirationTime: expirationTime,
78+
},
79+
},
80+
},
81+
{
82+
name: "FQDN cache does not exist",
83+
filteredCacheEntries: []types.DnsCacheEntry{},
84+
expectedResponse: []apis.FQDNCacheResponse{},
85+
},
86+
}
87+
for _, tt := range tests {
88+
t.Run(tt.name, func(t *testing.T) {
89+
ctrl := gomock.NewController(t)
90+
q := queriertest.NewMockAgentNetworkPolicyInfoQuerier(ctrl)
91+
q.EXPECT().GetFQDNCache(nil).Return(tt.filteredCacheEntries)
92+
handler := HandleFunc(q)
93+
req, err := http.NewRequest(http.MethodGet, "", nil)
94+
require.NoError(t, err)
95+
recorder := httptest.NewRecorder()
96+
handler.ServeHTTP(recorder, req)
97+
var receivedResponse []apis.FQDNCacheResponse
98+
err = json.Unmarshal(recorder.Body.Bytes(), &receivedResponse)
99+
require.NoError(t, err)
100+
assert.Equal(t, tt.expectedResponse, receivedResponse)
101+
})
102+
}
103+
}
104+
105+
func TestNewFilterFromURLQuery(t *testing.T) {
106+
tests := []struct {
107+
name string
108+
queryParams url.Values
109+
expectedFilter *querier.FQDNCacheFilter
110+
expectedError string
111+
}{
112+
{
113+
name: "Empty query",
114+
queryParams: url.Values{},
115+
expectedFilter: nil,
116+
},
117+
{
118+
name: "Valid regex domain",
119+
queryParams: url.Values{
120+
"domain": {"example.com"},
121+
},
122+
expectedFilter: &querier.FQDNCacheFilter{DomainRegex: regexp.MustCompile("^example[.]com$")},
123+
},
124+
{
125+
name: "Valid regex domain",
126+
queryParams: url.Values{
127+
"domain": {"*.example.com"},
128+
},
129+
expectedFilter: &querier.FQDNCacheFilter{DomainRegex: regexp.MustCompile("^.*[.]example[.]com$")},
130+
},
131+
{
132+
name: "Invalid regex domain",
133+
queryParams: url.Values{
134+
"domain": {"^example(abc$"},
135+
},
136+
expectedFilter: nil,
137+
expectedError: "missing closing )",
138+
},
139+
}
140+
141+
for _, tt := range tests {
142+
t.Run(tt.name, func(t *testing.T) {
143+
result, err := newFilterFromURLQuery(tt.queryParams)
144+
if tt.expectedError != "" {
145+
assert.ErrorContains(t, err, tt.expectedError)
146+
} else {
147+
require.NoError(t, err)
148+
assert.Equal(t, tt.expectedFilter, result)
149+
}
150+
})
151+
}
152+
}

pkg/agent/controller/networkpolicy/networkpolicy_controller.go

+13
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,19 @@ func NewNetworkPolicyController(antreaClientGetter client.AntreaClientProvider,
538538
return c, nil
539539
}
540540

541+
func (c *Controller) GetFQDNCache(fqdnFilter *querier.FQDNCacheFilter) []types.DnsCacheEntry {
542+
cacheEntryList := []types.DnsCacheEntry{}
543+
for fqdn, dnsMeta := range c.fqdnController.dnsEntryCache {
544+
for _, ipWithExpiration := range dnsMeta.responseIPs {
545+
if fqdnFilter == nil || fqdnFilter.DomainRegex.MatchString(fqdn) {
546+
entry := types.DnsCacheEntry{FQDNName: fqdn, IPAddress: ipWithExpiration.ip, ExpirationTime: ipWithExpiration.expirationTime}
547+
cacheEntryList = append(cacheEntryList, entry)
548+
}
549+
}
550+
}
551+
return cacheEntryList
552+
}
553+
541554
func (c *Controller) GetNetworkPolicyNum() int {
542555
return c.ruleCache.GetNetworkPolicyNum()
543556
}

pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"net"
2121
"os"
22+
"regexp"
2223
"strings"
2324
"sync"
2425
"testing"
@@ -903,3 +904,65 @@ func TestValidate(t *testing.T) {
903904
t.Fatalf("groupAddress %s expect %v, but got %v", groupAddress2, v1beta1.RuleActionDrop, item.RuleAction)
904905
}
905906
}
907+
908+
func TestGetFqdnCache(t *testing.T) {
909+
controller, _, _ := newTestController()
910+
expectedEntryList := []agenttypes.DnsCacheEntry{}
911+
assert.Equal(t, expectedEntryList, controller.GetFQDNCache(nil))
912+
expirationTime := time.Now().Add(1 * time.Hour).UTC()
913+
914+
controller.fqdnController.dnsEntryCache = map[string]dnsMeta{
915+
"example.com": {
916+
responseIPs: map[string]ipWithExpiration{
917+
"10.0.0.1": {
918+
ip: net.ParseIP("10.0.0.1"),
919+
expirationTime: expirationTime,
920+
},
921+
"10.0.0.2": {
922+
ip: net.ParseIP("10.0.0.2"),
923+
expirationTime: expirationTime,
924+
},
925+
"10.0.0.3": {
926+
ip: net.ParseIP("10.0.0.3"),
927+
expirationTime: expirationTime,
928+
},
929+
},
930+
},
931+
"antrea.io": {
932+
responseIPs: map[string]ipWithExpiration{
933+
"10.0.0.4": {
934+
ip: net.ParseIP("10.0.0.4"),
935+
expirationTime: expirationTime,
936+
},
937+
},
938+
},
939+
}
940+
941+
expectedEntryList = []agenttypes.DnsCacheEntry{
942+
{
943+
FQDNName: "example.com",
944+
IPAddress: net.ParseIP("10.0.0.1"),
945+
ExpirationTime: expirationTime,
946+
},
947+
{
948+
FQDNName: "example.com",
949+
IPAddress: net.ParseIP("10.0.0.2"),
950+
ExpirationTime: expirationTime,
951+
},
952+
{
953+
FQDNName: "example.com",
954+
IPAddress: net.ParseIP("10.0.0.3"),
955+
ExpirationTime: expirationTime,
956+
},
957+
{
958+
FQDNName: "antrea.io",
959+
IPAddress: net.ParseIP("10.0.0.4"),
960+
ExpirationTime: expirationTime,
961+
},
962+
}
963+
returnedList := controller.GetFQDNCache(nil)
964+
assert.ElementsMatch(t, expectedEntryList, returnedList)
965+
pattern := regexp.MustCompile("^.*[.]io$")
966+
returnedList = controller.GetFQDNCache(&querier.FQDNCacheFilter{DomainRegex: pattern})
967+
assert.ElementsMatch(t, []agenttypes.DnsCacheEntry{expectedEntryList[3]}, returnedList)
968+
}

pkg/agent/types/networkpolicy.go

+9
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,22 @@
1515
package types
1616

1717
import (
18+
"net"
19+
"time"
20+
1821
"k8s.io/apimachinery/pkg/util/sets"
1922

2023
"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
2124
secv1beta1 "antrea.io/antrea/pkg/apis/crd/v1beta1"
2225
binding "antrea.io/antrea/pkg/ovs/openflow"
2326
)
2427

28+
type DnsCacheEntry struct {
29+
FQDNName string
30+
IPAddress net.IP
31+
ExpirationTime time.Time
32+
}
33+
2534
type MatchKey struct {
2635
ofProtocol binding.Protocol
2736
valueCategory AddressCategory

0 commit comments

Comments
 (0)