Skip to content

Commit 539225c

Browse files
author
Gal Ovadia
committed
work
Signed-off-by: Gal Ovadia <govadia@palantir.com>
1 parent fffbac2 commit 539225c

9 files changed

Lines changed: 297 additions & 76 deletions

File tree

envoy-1.37.0

86.2 MB
Binary file not shown.

envoy-egress-test-tcp-only.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
admin:
2+
address:
3+
socket_address:
4+
address: 127.0.0.1
5+
port_value: 9901
6+
7+
static_resources:
8+
listeners:
9+
# TCP Listener only - for testing
10+
- name: tcp_listener
11+
address:
12+
socket_address:
13+
address: 0.0.0.0
14+
port_value: 17100
15+
filter_chains:
16+
- filters:
17+
- name: envoy.filters.network.dynamic_modules
18+
typed_config:
19+
"@type": type.googleapis.com/envoy.extensions.filters.network.dynamic_modules.v3.DynamicModuleNetworkFilter
20+
dynamic_module_config:
21+
name: rust_module
22+
do_not_close: true
23+
filter_name: hostname_lookup
24+
25+
- name: envoy.filters.network.tcp_proxy
26+
typed_config:
27+
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
28+
stat_prefix: egress_tcp
29+
cluster: default_cluster
30+
31+
clusters:
32+
- name: default_cluster
33+
connect_timeout: 5s
34+
type: STATIC
35+
load_assignment:
36+
cluster_name: default_cluster
37+
endpoints:
38+
- lb_endpoints:
39+
- endpoint:
40+
address:
41+
socket_address:
42+
address: 127.0.0.1
43+
port_value: 8080

envoy-egress-test.yaml

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,54 @@ admin:
66

77
static_resources:
88
listeners:
9-
# DNS Gateway - Listens for DNS queries
109
- name: dns_listener
1110
address:
1211
socket_address:
1312
address: 0.0.0.0
1413
port_value: 5353
1514
protocol: UDP
16-
udp_listener_config:
17-
listener_filters:
18-
- name: envoy.filters.udp_listener.dynamic_modules
19-
typed_config:
20-
"@type": type.googleapis.com/envoy.extensions.dynamic_modules.v3.DynamicModuleConfig
21-
name: dns_gateway
15+
listener_filters:
16+
- name: envoy.filters.udp_listener.dynamic_modules
17+
typed_config:
18+
"@type": type.googleapis.com/envoy.extensions.filters.udp.dynamic_modules.v3.DynamicModuleUdpListenerFilter
19+
dynamic_module_config:
20+
name: rust_module
2221
do_not_close: true
23-
config:
22+
filter_name: dns_gateway
23+
filter_config:
24+
"@type": type.googleapis.com/google.protobuf.Struct
25+
value:
2426
base_ip: "10.10.0.0"
2527
policies:
26-
- domain: "*.aws.com"
27-
metadata:
28-
upstream_cluster: "aws_cluster"
29-
tunneling_hostname: "tunnel.aws.com"
30-
- domain: "*.example.com"
31-
metadata:
32-
upstream_cluster: "example_cluster"
33-
tunneling_hostname: "tunnel.example.com"
28+
- domain: "*.aws.com"
29+
metadata:
30+
my_arbitrary_key: some_value
3431

35-
# TCP Listener - Receives connections to virtual IPs
3632
- name: tcp_listener
3733
address:
3834
socket_address:
3935
address: 0.0.0.0
4036
port_value: 17100
4137
filter_chains:
4238
- filters:
43-
# Hostname lookup filter - looks up policy from virtual IP
4439
- name: envoy.filters.network.dynamic_modules
4540
typed_config:
46-
"@type": type.googleapis.com/envoy.extensions.dynamic_modules.v3.DynamicModuleConfig
47-
name: hostname_lookup
48-
do_not_close: true
49-
config: {}
50-
51-
# TCP proxy - forwards traffic
41+
"@type": type.googleapis.com/envoy.extensions.filters.network.dynamic_modules.v3.DynamicModuleNetworkFilter
42+
dynamic_module_config:
43+
name: rust_module
44+
do_not_close: true
45+
filter_name: hostname_lookup
5246
- name: envoy.filters.network.tcp_proxy
5347
typed_config:
5448
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
5549
stat_prefix: egress_tcp
5650
cluster: default_cluster
51+
access_log:
52+
- name: envoy.access_loggers.stdout
53+
typed_config:
54+
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
55+
log_format:
56+
text_format: "[TCP_PROXY_LOG] my_arbitrary_key=%FILTER_STATE(envoy.wildcard.metadata.my_arbitrary_key:PLAIN)%\n"
5757

5858
clusters:
5959
- name: default_cluster
@@ -69,6 +69,20 @@ static_resources:
6969
address: 127.0.0.1
7070
port_value: 8080
7171

72+
# Upstream DNS server for unmatched DNS queries (udp_proxy fallback)
73+
- name: upstream_dns
74+
connect_timeout: 5s
75+
type: STATIC
76+
load_assignment:
77+
cluster_name: upstream_dns
78+
endpoints:
79+
- lb_endpoints:
80+
- endpoint:
81+
address:
82+
socket_address:
83+
address: 8.8.8.8
84+
port_value: 53
85+
7286
- name: aws_cluster
7387
connect_timeout: 5s
7488
type: STATIC

envoy-simple-test.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ static_resources:
1717
# Hostname lookup filter - manually initialize cache for testing
1818
- name: envoy.filters.network.dynamic_modules
1919
typed_config:
20-
"@type": type.googleapis.com/envoy.extensions.dynamic_modules.v3.DynamicModuleConfig
21-
name: hostname_lookup
22-
do_not_close: true
23-
config: {}
20+
"@type": type.googleapis.com/envoy.extensions.filters.network.dynamic_modules.v3.DynamicModuleNetworkFilter
21+
dynamic_module_config:
22+
name: rust_module
23+
do_not_close: true
24+
filter_name: hostname_lookup
2425

2526
# TCP proxy - forwards traffic
2627
- name: envoy.filters.network.tcp_proxy

run-envoy.sh

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Egress Policies Dynamic Module - Envoy Startup Script
44

55
# Set the module search path
6-
export ENVOY_DYNAMIC_MODULES_SEARCH_PATH=/Users/govadia/Desktop/dynamic-modules-examples/rust/target/release
6+
export ENVOY_DYNAMIC_MODULES_SEARCH_PATH=/home/coder/git/dynamic-modules-examples/rust/target/release
77

88
echo "=== Starting Envoy with Egress Policies Dynamic Module ==="
99
echo ""
@@ -18,12 +18,6 @@ echo ""
1818
echo "Test with: ./test-egress.sh"
1919
echo ""
2020

21-
# Check if envoy is installed
22-
if ! command -v envoy &> /dev/null; then
23-
echo "ERROR: envoy command not found"
24-
echo "Install with: brew install envoyproxy/envoy/envoy"
25-
exit 1
26-
fi
2721

2822
# Check if module exists
2923
if [ ! -f "$ENVOY_DYNAMIC_MODULES_SEARCH_PATH/librust_module.dylib" ] && [ ! -f "$ENVOY_DYNAMIC_MODULES_SEARCH_PATH/librust_module.so" ]; then
@@ -33,5 +27,5 @@ if [ ! -f "$ENVOY_DYNAMIC_MODULES_SEARCH_PATH/librust_module.dylib" ] && [ ! -f
3327
fi
3428

3529
# Start Envoy
36-
cd /Users/govadia/Desktop/dynamic-modules-examples
37-
envoy -c envoy-egress-test.yaml --log-level info
30+
cd /home/coder/git/dynamic-modules-examples
31+
./envoy-1.37.0 -c envoy-egress-test.yaml --log-level info

rust/src/egress_policies/dns_gateway.rs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,21 @@ pub fn new_udp_filter_config<EC: EnvoyUdpListenerFilterConfig, ELF: EnvoyUdpList
4747
_name: &str,
4848
config: &[u8],
4949
) -> Option<Box<dyn UdpListenerFilterConfig<ELF>>> {
50-
// Parse config as JSON
50+
// Parse config as JSON. The config arrives as a JSON-serialized google.protobuf.Any.
51+
// Supported wrappers:
52+
// - StringValue: {"@type":"...StringValue", "value":"<json string>"}
53+
// - Struct: {"@type":"...Struct", "value":{"base_ip":"...", ...}}
5154
let config_str = std::str::from_utf8(config).ok()?;
52-
let config_json: serde_json::Value = serde_json::from_str(config_str).ok()?;
55+
let outer_json: serde_json::Value = serde_json::from_str(config_str).ok()?;
56+
57+
let config_json: serde_json::Value = match &outer_json["value"] {
58+
// StringValue: "value" is a JSON string that we parse again.
59+
serde_json::Value::String(s) => serde_json::from_str(s).ok()?,
60+
// Struct: "value" is already an object with our config fields.
61+
serde_json::Value::Object(_) => outer_json["value"].clone(),
62+
// Fallback: use the outer object directly.
63+
_ => outer_json,
64+
};
5365

5466
// Parse base_ip from config (default: 10.10.0.0)
5567
let base_ip_str = config_json["base_ip"]
@@ -114,49 +126,63 @@ impl<ELF: EnvoyUdpListenerFilter> UdpListenerFilter<ELF> for DnsGatewayFilter {
114126
envoy_filter: &mut ELF,
115127
) -> abi::envoy_dynamic_module_type_on_udp_listener_filter_status {
116128
// Get datagram data
117-
let (chunks, _total_length) = envoy_filter.get_datagram_data();
129+
let (chunks, total_length) = envoy_filter.get_datagram_data();
130+
envoy_log_info!("dns_gateway: received UDP datagram, {} bytes, {} chunks", total_length, chunks.len());
118131
let mut data = Vec::new();
119132
for chunk in &chunks {
120133
data.extend_from_slice(chunk.as_slice());
121134
}
122135

136+
// Get peer address for sending the response back
137+
let peer = envoy_filter.get_peer_address();
138+
envoy_log_info!("dns_gateway: peer address: {:?}", peer);
139+
123140
// Parse DNS query using hickory-dns
124141
let mut decoder = BinDecoder::new(&data);
125142
let query_message = match Message::read(&mut decoder) {
126143
Ok(msg) => msg,
127144
Err(e) => {
128-
envoy_log_warn!("Failed to parse DNS query: {}", e);
145+
envoy_log_warn!("dns_gateway: failed to parse DNS query: {}", e);
129146
return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue;
130147
}
131148
};
132149

150+
envoy_log_info!("dns_gateway: parsed DNS message id={}, type={:?}, queries={}",
151+
query_message.id(), query_message.message_type(), query_message.queries().len());
152+
133153
// Validate it's a query and has at least one question
134154
if query_message.message_type() != MessageType::Query {
135-
envoy_log_warn!("Received non-query DNS message");
155+
envoy_log_warn!("dns_gateway: received non-query DNS message");
136156
return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue;
137157
}
138158

139159
let question = match query_message.queries().first() {
140160
Some(q) => q,
141161
None => {
142-
envoy_log_warn!("DNS query has no questions");
162+
envoy_log_warn!("dns_gateway: DNS query has no questions");
143163
return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue;
144164
}
145165
};
146166

147167
// Only handle A record queries
148168
if question.query_type() != RecordType::A {
149-
envoy_log_debug!("Ignoring non-A record query: {:?}", question.query_type());
169+
envoy_log_info!("dns_gateway: ignoring non-A record query: {:?}", question.query_type());
150170
return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue;
151171
}
152172

153-
let domain = question.name().to_utf8();
154-
envoy_log_debug!("DNS query for domain: {}", domain);
173+
let domain_raw = question.name().to_utf8();
174+
// DNS names are fully qualified with a trailing dot (e.g. "api.aws.com.").
175+
// 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);
155178

156179
// Match against policies
157180
let matched_policy = self.policies.iter().find(|p| p.matches(&domain));
158181

159182
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+
160186
// Create EgressPolicy from matcher
161187
let policy = EgressPolicy {
162188
domain: domain.clone(),
@@ -168,28 +194,42 @@ impl<ELF: EnvoyUdpListenerFilter> UdpListenerFilter<ELF> for DnsGatewayFilter {
168194
let virtual_ip = cache.allocate(policy);
169195

170196
envoy_log_info!(
171-
"Allocated virtual IP {} for domain {}",
197+
"dns_gateway: allocated virtual IP {} for domain {}",
172198
virtual_ip,
173199
domain
174200
);
175201

176202
// Craft DNS A response using hickory-dns
177203
let response_bytes = match craft_dns_response(&query_message, question.name(), virtual_ip) {
178-
Ok(bytes) => bytes,
204+
Ok(bytes) => {
205+
envoy_log_info!("dns_gateway: crafted DNS response, {} bytes", bytes.len());
206+
bytes
207+
}
179208
Err(e) => {
180-
envoy_log_error!("Failed to craft DNS response: {}", e);
209+
envoy_log_error!("dns_gateway: failed to craft DNS response: {}", e);
181210
return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue;
182211
}
183212
};
184213

185-
// Set datagram data to response
186-
if !envoy_filter.set_datagram_data(&response_bytes) {
187-
envoy_log_error!("Failed to set datagram data");
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.
217+
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);
219+
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);
221+
}
222+
} else {
223+
envoy_log_error!("dns_gateway: no peer address available, cannot send response");
188224
}
225+
226+
// StopIteration prevents the udp_proxy filter from also forwarding this packet
227+
return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::StopIteration;
189228
} else {
190-
envoy_log_debug!("No policy matched for domain: {}", domain);
229+
envoy_log_info!("dns_gateway: no policy matched for domain: {}", domain);
191230
}
192231

232+
// No match — let the packet continue to the next filter (udp_proxy)
193233
abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue
194234
}
195235
}

rust/src/egress_policies/hostname_lookup.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,41 +42,41 @@ impl<ENF: EnvoyNetworkFilter> NetworkFilter<ENF> for HostnameLookupFilter {
4242
&mut self,
4343
envoy_filter: &mut ENF,
4444
) -> abi::envoy_dynamic_module_type_on_network_filter_data_status {
45-
let (ip_str, _port) = envoy_filter.get_local_address();
45+
let (ip_str, port) = envoy_filter.get_local_address();
46+
envoy_log_info!("hostname_lookup: new connection, local_address={}:{}", ip_str, port);
4647

4748
let ip: Ipv4Addr = match ip_str.parse() {
4849
Ok(ip) => ip,
4950
Err(_) => {
50-
envoy_log_warn!("Failed to parse destination IP: {}", ip_str);
51+
envoy_log_warn!("hostname_lookup: failed to parse destination IP: {}", ip_str);
5152
return abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue;
5253
}
5354
};
5455

55-
envoy_log_debug!("Looking up policy for virtual IP: {}", ip);
56-
5756
let cache = get_cache();
5857
let policy = match cache.lookup(ip) {
5958
Some(p) => p,
6059
None => {
61-
envoy_log_warn!("No policy found for virtual IP: {}", ip);
60+
envoy_log_warn!("hostname_lookup: no policy found for virtual IP: {} (cache miss)", ip);
6261
return abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue;
6362
}
6463
};
6564

6665
envoy_log_info!(
67-
"Found policy for virtual IP {}: domain={}",
66+
"hostname_lookup: cache hit for virtual IP {}: domain={}, metadata keys=[{}]",
6867
ip,
69-
policy.domain
68+
policy.domain,
69+
policy.metadata.keys().cloned().collect::<Vec<_>>().join(", ")
7070
);
7171

7272
let hostname_key = "envoy.wildcard.hostname";
7373
if !envoy_filter.set_filter_state_bytes(
7474
hostname_key.as_bytes(),
7575
policy.domain.as_bytes(),
7676
) {
77-
envoy_log_error!("Failed to set filter state for hostname");
77+
envoy_log_error!("hostname_lookup: failed to set filter state for hostname");
7878
} else {
79-
envoy_log_debug!("Set filter state: {} = {}", hostname_key, policy.domain);
79+
envoy_log_info!("hostname_lookup: set filter state: {} = {}", hostname_key, policy.domain);
8080
}
8181

8282
for (key, value) in &policy.metadata {
@@ -86,9 +86,9 @@ impl<ENF: EnvoyNetworkFilter> NetworkFilter<ENF> for HostnameLookupFilter {
8686
filter_state_key.as_bytes(),
8787
value.as_bytes(),
8888
) {
89-
envoy_log_error!("Failed to set filter state for key: {}", filter_state_key);
89+
envoy_log_error!("hostname_lookup: failed to set filter state for key: {}", filter_state_key);
9090
} else {
91-
envoy_log_debug!("Set filter state: {} = {}", filter_state_key, value);
91+
envoy_log_info!("hostname_lookup: set filter state: {} = {}", filter_state_key, value);
9292
}
9393
}
9494

0 commit comments

Comments
 (0)