Skip to content

Commit c252c0f

Browse files
olicesxkix
andauthored
test(cache): guard response-jump snapshot threading across reloads (#46)
The existing snapshot regression test only exercises the static short-circuit path, so reverting the phases.rs snapshot threading for response actions/jumps back to mid-request state loads would still pass it. Add a Forward + response-jump test that runs a request from a pre-reload snapshot through a loopback upstream and asserts the jump result is produced by, and cached under, the snapshot generation only. Verified the guard by temporarily reintroducing the mid-request state load at the jump site: this test fails while request_snapshot_cannot_write_into_the_active_generation_after_reload still passes. Co-authored-by: kix <olices@9up.in>
1 parent 041d0c0 commit c252c0f

1 file changed

Lines changed: 125 additions & 1 deletion

File tree

src/engine/execution.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,7 @@ mod tests {
11671167
use crate::engine::response::*;
11681168
use crate::engine::rules::*;
11691169
use crate::matcher::RuntimeResponseMatcherWithOp;
1170-
use hickory_proto::op::{Message, MessageType, OpCode, Query};
1170+
use hickory_proto::op::{Message, MessageType, OpCode, Query, UpdateMessage};
11711171
use hickory_proto::rr::RecordType;
11721172
use hickory_proto::rr::{RData, Record};
11731173
use std::net::IpAddr;
@@ -1321,6 +1321,130 @@ mod tests {
13211321
);
13221322
}
13231323

1324+
/// Minimal UDP upstream that echoes each query back as a NOERROR response
1325+
/// with a single A record, so the forward path completes without network
1326+
/// access. / 最小 UDP 上游:把查询回显为带一条 A 记录的 NOERROR 响应。
1327+
async fn spawn_echo_upstream() -> String {
1328+
let sock = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
1329+
let addr = sock.local_addr().unwrap().to_string();
1330+
tokio::spawn(async move {
1331+
let mut buf = [0u8; 1500];
1332+
while let Ok((n, peer)) = sock.recv_from(&mut buf).await {
1333+
let query = match Message::from_bytes(&buf[..n]) {
1334+
Ok(q) => q,
1335+
Err(_) => continue,
1336+
};
1337+
let Some(question) = query.queries.first().cloned() else {
1338+
continue;
1339+
};
1340+
let mut resp = Message::new(query.id(), MessageType::Response, OpCode::Query);
1341+
resp.add_query(question.clone());
1342+
resp.add_answer(Record::from_rdata(
1343+
question.name().clone(),
1344+
300,
1345+
RData::A(hickory_proto::rr::rdata::A(std::net::Ipv4Addr::LOCALHOST)),
1346+
));
1347+
let _ = sock.send_to(&resp.to_vec().unwrap(), peer).await;
1348+
}
1349+
});
1350+
addr
1351+
}
1352+
1353+
fn jump_config(upstream: &str, target_ip: &str) -> RuntimePipelineConfig {
1354+
let raw = serde_json::json!({
1355+
"settings": { "default_upstream": "1.1.1.1:53", "min_ttl": 60 },
1356+
"pipelines": [
1357+
{
1358+
"id": "main",
1359+
"rules": [{
1360+
"name": "forward-and-jump",
1361+
"matchers": [{ "type": "any" }],
1362+
"actions": [{ "type": "forward", "upstream": upstream }],
1363+
"response_matchers": [{ "type": "upstream_equals", "value": upstream }],
1364+
"response_actions_on_match": [
1365+
{ "type": "jump_to_pipeline", "pipeline": "target" }
1366+
]
1367+
}]
1368+
},
1369+
{
1370+
"id": "target",
1371+
"rules": [{
1372+
"name": "static",
1373+
"matchers": [{ "type": "any" }],
1374+
"actions": [{ "type": "static_ip_response", "ip": target_ip }]
1375+
}]
1376+
}
1377+
]
1378+
});
1379+
let config: crate::config::PipelineConfig =
1380+
serde_json::from_value(raw).expect("parse config");
1381+
RuntimePipelineConfig::from_config(config).expect("build runtime config")
1382+
}
1383+
1384+
#[tokio::test]
1385+
async fn response_jump_writes_only_into_its_own_generation() {
1386+
let upstream = spawn_echo_upstream().await;
1387+
let engine =
1388+
Engine::new(jump_config(&upstream, "192.0.2.1"), "test".to_string()).expect("engine");
1389+
let old_state = engine.state.load_full();
1390+
let old_target_key = Engine::calculate_cache_hash_for_dedupe(
1391+
old_state.cache_namespace("target"),
1392+
"target",
1393+
b"example.com",
1394+
RecordType::A,
1395+
DNSClass::IN,
1396+
None,
1397+
);
1398+
1399+
// Reload changes only the jump target, so "target" rotates its
1400+
// namespace while the entry pipeline stays warm.
1401+
engine.reload(jump_config(&upstream, "192.0.2.2"));
1402+
let new_state = engine.state.load_full();
1403+
let new_target_key = Engine::calculate_cache_hash_for_dedupe(
1404+
new_state.cache_namespace("target"),
1405+
"target",
1406+
b"example.com",
1407+
RecordType::A,
1408+
DNSClass::IN,
1409+
None,
1410+
);
1411+
1412+
let mut request = Message::new(0xBEEF, MessageType::Query, OpCode::Query);
1413+
request.add_query(Query::query(
1414+
Name::from_str("example.com").unwrap(),
1415+
RecordType::A,
1416+
));
1417+
// No explicit cache hash: the jump-path key must be recomputed from
1418+
// the request snapshot, exercising the phases.rs threading.
1419+
let response = engine
1420+
.handle_packet_internal(
1421+
&request.to_vec().unwrap(),
1422+
"127.0.0.1:53000".parse().unwrap(),
1423+
false,
1424+
None,
1425+
None,
1426+
Some(old_state),
1427+
)
1428+
.await
1429+
.expect("complete forwarded request with response jump");
1430+
let response = Message::from_bytes(&response).expect("parse response");
1431+
1432+
// The jump must execute the target pipeline of the request snapshot
1433+
// (192.0.2.1), not the reloaded one (192.0.2.2).
1434+
assert!(matches!(
1435+
response.answers.first().map(|record| &record.data),
1436+
Some(RData::A(address)) if address.0 == "192.0.2.1".parse::<std::net::Ipv4Addr>().unwrap()
1437+
));
1438+
assert!(
1439+
engine.cache_get(&old_target_key).is_some(),
1440+
"the response jump must cache under the snapshot generation's key"
1441+
);
1442+
assert!(
1443+
engine.cache_get(&new_target_key).is_none(),
1444+
"the response jump must not populate the active generation"
1445+
);
1446+
}
1447+
13241448
// ========================================================================
13251449
// Engine Helper Functions Unit Tests / 引擎辅助函数单元测试
13261450
// ========================================================================

0 commit comments

Comments
 (0)