1- //! UDP listener filter for DNS-based routing with virtual IP allocation.
2- //!
3- //! This filter:
4- //! 1. Listens on port 5353 for DNS queries
5- //! 2. Parses DNS A record queries
6- //! 3. Matches domains against configured policies
7- //! 4. Allocates virtual IPs via VirtualIpCache
8- //! 5. Returns DNS A responses
9-
101use envoy_proxy_dynamic_modules_rust_sdk:: * ;
11- use std:: net:: Ipv4Addr ;
122use hickory_proto:: op:: { Message , MessageType , ResponseCode } ;
133use hickory_proto:: rr:: { Name , RData , Record , RecordType } ;
144use hickory_proto:: serialize:: binary:: { BinDecodable , BinDecoder } ;
5+ use std:: net:: Ipv4Addr ;
156
167use super :: virtual_ip_cache:: { get_cache, init_cache, EgressPolicy } ;
178
18- /// DNS Gateway filter configuration.
199pub struct DnsGatewayFilterConfig {
2010 policies : Vec < PolicyMatcher > ,
2111}
@@ -35,13 +25,11 @@ impl PolicyMatcher {
3525 let suffix = & self . domain_pattern [ 2 ..] ;
3626 domain. ends_with ( suffix)
3727 } else {
38- // Exact match
39- domain == self . domain_pattern
28+ domain == self . domain_pattern // Exact match
4029 }
4130 }
4231}
4332
44- /// Creates a new DNS gateway filter configuration.
4533pub fn new_udp_filter_config < EC : EnvoyUdpListenerFilterConfig , ELF : EnvoyUdpListenerFilter > (
4634 _envoy_filter_config : & mut EC ,
4735 _name : & str ,
@@ -64,15 +52,13 @@ pub fn new_udp_filter_config<EC: EnvoyUdpListenerFilterConfig, ELF: EnvoyUdpList
6452 } ;
6553
6654 // Parse base_ip from config (default: 10.10.0.0)
67- let base_ip_str = config_json[ "base_ip" ]
68- . as_str ( )
69- . unwrap_or ( "10.10.0.0" ) ;
55+ let base_ip_str = config_json[ "base_ip" ] . as_str ( ) . unwrap_or ( "10.10.0.0" ) ;
7056
7157 let base_ip: Ipv4Addr = base_ip_str. parse ( ) . ok ( ) ?;
7258 let base_ip_u32 = u32:: from ( base_ip) ;
7359
7460 // Initialize the cache (first call wins, subsequent calls are ignored)
75- let _ = init_cache ( base_ip_u32) ;
61+ init_cache ( base_ip_u32) ;
7662
7763 // Parse policies
7864 let policies_array = config_json[ "policies" ] . as_array ( ) ?;
@@ -115,7 +101,6 @@ impl<ELF: EnvoyUdpListenerFilter> UdpListenerFilterConfig<ELF> for DnsGatewayFil
115101 }
116102}
117103
118- /// DNS Gateway filter instance.
119104struct DnsGatewayFilter {
120105 policies : Vec < PolicyMatcher > ,
121106}
@@ -125,20 +110,21 @@ impl<ELF: EnvoyUdpListenerFilter> UdpListenerFilter<ELF> for DnsGatewayFilter {
125110 & mut self ,
126111 envoy_filter : & mut ELF ,
127112 ) -> abi:: envoy_dynamic_module_type_on_udp_listener_filter_status {
128- // Get datagram data
129113 let ( chunks, total_length) = envoy_filter. get_datagram_data ( ) ;
130- envoy_log_info ! ( "dns_gateway: received UDP datagram, {} bytes, {} chunks" , total_length, chunks. len( ) ) ;
114+ envoy_log_info ! (
115+ "dns_gateway: received UDP datagram, {} bytes, {} chunks" ,
116+ total_length,
117+ chunks. len( )
118+ ) ;
131119 let mut data = Vec :: new ( ) ;
132120 for chunk in & chunks {
133121 data. extend_from_slice ( chunk. as_slice ( ) ) ;
134122 }
135123
136- // Get peer address for sending the response back
137124 let peer = envoy_filter. get_peer_address ( ) ;
138125 envoy_log_info ! ( "dns_gateway: peer address: {:?}" , peer) ;
139126
140- // Parse DNS query using hickory-dns
141- let mut decoder = BinDecoder :: new ( & data) ;
127+ let mut decoder: BinDecoder < ' _ > = BinDecoder :: new ( & data) ;
142128 let query_message = match Message :: read ( & mut decoder) {
143129 Ok ( msg) => msg,
144130 Err ( e) => {
@@ -147,10 +133,13 @@ impl<ELF: EnvoyUdpListenerFilter> UdpListenerFilter<ELF> for DnsGatewayFilter {
147133 }
148134 } ;
149135
150- envoy_log_info ! ( "dns_gateway: parsed DNS message id={}, type={:?}, queries={}" ,
151- query_message. id( ) , query_message. message_type( ) , query_message. queries( ) . len( ) ) ;
136+ envoy_log_info ! (
137+ "dns_gateway: parsed DNS message id={}, type={:?}, queries={}" ,
138+ query_message. id( ) ,
139+ query_message. message_type( ) ,
140+ query_message. queries( ) . len( )
141+ ) ;
152142
153- // Validate it's a query and has at least one question
154143 if query_message. message_type ( ) != MessageType :: Query {
155144 envoy_log_warn ! ( "dns_gateway: received non-query DNS message" ) ;
156145 return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue ;
@@ -164,123 +153,181 @@ impl<ELF: EnvoyUdpListenerFilter> UdpListenerFilter<ELF> for DnsGatewayFilter {
164153 }
165154 } ;
166155
167- // Only handle A record queries
168- if question. query_type ( ) != RecordType :: A {
169- envoy_log_info ! ( "dns_gateway: ignoring non-A record query: {:?}" , question. query_type( ) ) ;
170- return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue ;
171- }
172-
173156 let domain_raw = question. name ( ) . to_utf8 ( ) ;
174157 // DNS names are fully qualified with a trailing dot (e.g. "api.aws.com.").
175158 // Strip it so our wildcard patterns like "*.aws.com" match correctly.
176- let domain = domain_raw. strip_suffix ( '.' ) . unwrap_or ( & domain_raw) . to_string ( ) ;
177- envoy_log_info ! ( "dns_gateway: A record query for domain: {} (raw: {})" , domain, domain_raw) ;
159+ let domain = domain_raw
160+ . strip_suffix ( '.' )
161+ . unwrap_or ( & domain_raw)
162+ . to_string ( ) ;
163+
164+ // Handle A and AAAA record queries
165+ match question. query_type ( ) {
166+ RecordType :: A => {
167+ envoy_log_info ! (
168+ "dns_gateway: A record query for domain: {} (raw: {})" ,
169+ domain,
170+ domain_raw
171+ ) ;
172+ }
173+ RecordType :: AAAA => {
174+ envoy_log_info ! (
175+ "dns_gateway: AAAA record query for domain: {} (raw: {})" ,
176+ domain,
177+ domain_raw
178+ ) ;
179+ }
180+ _ => {
181+ envoy_log_info ! (
182+ "dns_gateway: ignoring non-A/AAAA record query: {:?}" ,
183+ question. query_type( )
184+ ) ;
185+ return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue ;
186+ }
187+ }
178188
179- // Match against policies
180189 let matched_policy = self . policies . iter ( ) . find ( |p| p. matches ( & domain) ) ;
181190
182191 if let Some ( policy_matcher) = matched_policy {
183- envoy_log_info ! ( "dns_gateway: matched policy pattern '{}' for domain '{}'" ,
184- policy_matcher. domain_pattern, domain) ;
185-
186- // Create EgressPolicy from matcher
187- let policy = EgressPolicy {
188- domain : domain. clone ( ) ,
189- metadata : policy_matcher. metadata . clone ( ) ,
190- } ;
191-
192- // Allocate virtual IP via cache
193- let cache = get_cache ( ) ;
194- let virtual_ip = cache. allocate ( policy) ;
195-
196192 envoy_log_info ! (
197- "dns_gateway: allocated virtual IP {} for domain {} " ,
198- virtual_ip ,
193+ "dns_gateway: matched policy pattern '{}' for domain '{}' " ,
194+ policy_matcher . domain_pattern ,
199195 domain
200196 ) ;
201197
202- // Craft DNS A response using hickory-dns
203- let response_bytes = match craft_dns_response ( & query_message, question. name ( ) , virtual_ip) {
204- Ok ( bytes) => {
205- envoy_log_info ! ( "dns_gateway: crafted DNS response, {} bytes" , bytes. len( ) ) ;
206- bytes
198+ // Craft the appropriate response based on query type
199+ let response_bytes = match question. query_type ( ) {
200+ RecordType :: A => {
201+ // Allocate virtual IP for A record queries
202+ let policy = EgressPolicy {
203+ domain : domain. clone ( ) ,
204+ metadata : policy_matcher. metadata . clone ( ) ,
205+ } ;
206+
207+ let cache = get_cache ( ) ;
208+ let virtual_ip = cache. allocate ( policy) ;
209+
210+ envoy_log_info ! (
211+ "dns_gateway: allocated virtual IP {} for domain {}" ,
212+ virtual_ip,
213+ domain
214+ ) ;
215+
216+ // Craft DNS A response with virtual IP
217+ match build_dns_response ( & query_message, question. name ( ) , virtual_ip) {
218+ Ok ( bytes) => {
219+ envoy_log_info ! (
220+ "dns_gateway: crafted DNS A response, {} bytes" ,
221+ bytes. len( )
222+ ) ;
223+ bytes
224+ }
225+ Err ( e) => {
226+ envoy_log_error ! ( "dns_gateway: failed to craft DNS A response: {}" , e) ;
227+ return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue ;
228+ }
229+ }
207230 }
208- Err ( e) => {
209- envoy_log_error ! ( "dns_gateway: failed to craft DNS response: {}" , e) ;
231+ RecordType :: AAAA => {
232+ // Return NODATA response for AAAA queries (no IPv6 available)
233+ envoy_log_info ! (
234+ "dns_gateway: returning NODATA for AAAA query (no IPv6 available)"
235+ ) ;
236+
237+ match build_nodata_response ( & query_message) {
238+ Ok ( bytes) => {
239+ envoy_log_info ! (
240+ "dns_gateway: crafted NODATA response, {} bytes" ,
241+ bytes. len( )
242+ ) ;
243+ bytes
244+ }
245+ Err ( e) => {
246+ envoy_log_error ! ( "dns_gateway: failed to craft NODATA response: {}" , e) ;
247+ return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue ;
248+ }
249+ }
250+ }
251+ _ => {
252+ // Should never reach here due to earlier match
210253 return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue ;
211254 }
212255 } ;
213256
214- // Send the DNS response back to the peer directly.
215- // Note: set_datagram_data() only modifies the buffer in-place but doesn't
216- // send it back. We must use send_datagram() to actually reply to the client.
217257 if let Some ( ( peer_addr, peer_port) ) = peer {
218- envoy_log_info ! ( "dns_gateway: sending {} byte response to {}:{}" , response_bytes. len( ) , peer_addr, peer_port) ;
258+ envoy_log_info ! (
259+ "dns_gateway: sending {} byte response to {}:{}" ,
260+ response_bytes. len( ) ,
261+ peer_addr,
262+ peer_port
263+ ) ;
219264 if !envoy_filter. send_datagram ( & response_bytes, & peer_addr, peer_port) {
220- envoy_log_error ! ( "dns_gateway: failed to send datagram to {}:{}" , peer_addr, peer_port) ;
265+ envoy_log_error ! (
266+ "dns_gateway: failed to send datagram to {}:{}" ,
267+ peer_addr,
268+ peer_port
269+ ) ;
221270 }
222271 } else {
223272 envoy_log_error ! ( "dns_gateway: no peer address available, cannot send response" ) ;
224273 }
225274
226- // StopIteration prevents the udp_proxy filter from also forwarding this packet
227275 return abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: StopIteration ;
228276 } else {
229277 envoy_log_info ! ( "dns_gateway: no policy matched for domain: {}" , domain) ;
230278 }
231279
232- // No match — let the packet continue to the next filter (udp_proxy)
233280 abi:: envoy_dynamic_module_type_on_udp_listener_filter_status:: Continue
234281 }
235282}
236283
237- /// Crafts a DNS A response with the given virtual IP using hickory-dns.
238- ///
239- /// This function creates a proper DNS response message with:
240- /// - The original query's transaction ID
241- /// - Standard query response flags
242- /// - The original question section
243- /// - An answer section with the allocated virtual IP
244- ///
245- /// # Arguments
246- /// * `query_message` - The original DNS query message
247- /// * `name` - The domain name being queried
248- /// * `ip` - The virtual IP address to return
249- ///
250- /// # Returns
251- /// The serialized DNS response as bytes, or an error if serialization fails
252- fn craft_dns_response (
284+ fn build_dns_response (
253285 query_message : & Message ,
254286 name : & Name ,
255287 ip : Ipv4Addr ,
256288) -> Result < Vec < u8 > , Box < dyn std:: error:: Error > > {
257289 let mut response = Message :: new ( ) ;
258290
259- // Copy transaction ID from query
260291 response. set_id ( query_message. id ( ) ) ;
261292
262- // Set response flags
263293 response. set_message_type ( MessageType :: Response ) ;
264294 response. set_response_code ( ResponseCode :: NoError ) ;
265295 response. set_recursion_desired ( query_message. recursion_desired ( ) ) ;
266296 response. set_recursion_available ( true ) ;
267297
268- // Add the original question
269298 if let Some ( question) = query_message. queries ( ) . first ( ) {
270299 response. add_query ( question. clone ( ) ) ;
271300 }
272301
273- // Create answer record
274302 let mut record = Record :: new ( ) ;
275303 record. set_name ( name. clone ( ) ) ;
276304 record. set_record_type ( RecordType :: A ) ;
277- record. set_ttl ( 60 ) ; // 60 seconds TTL
305+ record. set_ttl ( 600 ) ; // 10 min TTL
278306 record. set_data ( Some ( RData :: A ( ip. into ( ) ) ) ) ;
279307
280- // Add answer to response
281308 response. add_answer ( record) ;
282309
283- // Serialize to bytes
310+ let bytes = response. to_vec ( ) ?;
311+ Ok ( bytes)
312+ }
313+
314+ // For now, no IPv6 support
315+ fn build_nodata_response ( query_message : & Message ) -> Result < Vec < u8 > , Box < dyn std:: error:: Error > > {
316+ let mut response = Message :: new ( ) ;
317+
318+ response. set_id ( query_message. id ( ) ) ;
319+
320+ response. set_message_type ( MessageType :: Response ) ;
321+ response. set_response_code ( ResponseCode :: NoError ) ;
322+ response. set_recursion_desired ( query_message. recursion_desired ( ) ) ;
323+ response. set_recursion_available ( true ) ;
324+
325+ if let Some ( question) = query_message. queries ( ) . first ( ) {
326+ response. add_query ( question. clone ( ) ) ;
327+ }
328+
329+ // No answer records - this is a NODATA response
330+
284331 let bytes = response. to_vec ( ) ?;
285332 Ok ( bytes)
286333}
0 commit comments