Skip to content

Commit

Permalink
test: add tun based simulation test (IPv4/ICMP only) (#908)
Browse files Browse the repository at this point in the history
  • Loading branch information
fujiapple852 committed Jan 8, 2024
1 parent 4f9b7e4 commit 23a9476
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 3 deletions.
48 changes: 47 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,60 @@ jobs:
os: windows-latest
target: x86_64-pc-windows-msvc
rust: stable
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- run: cargo test --target ${{ matrix.target }}

sim-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
build:
- linux-stable
- macos-stable
- windows-stable
include:
- build: linux-stable
os: ubuntu-20.04
target: x86_64-unknown-linux-gnu
rust: stable
- build: macos-stable
os: macos-latest
target: x86_64-apple-darwin
rust: stable
- build: windows-stable
os: windows-latest
target: x86_64-pc-windows-msvc
rust: stable
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- run: cargo test --workspace --all-features --target ${{ matrix.target }}
- name: Copy wintun.dll to current dir
if: startsWith(matrix.build, 'windows')
shell: bash
run: |
cp "tests/resources/wintun.dll" "."
- name: Allow ICMPv4 and ICMPv6 in Windows defender firewall
if: startsWith(matrix.build, 'windows')
shell: pwsh
run: |
New-NetFirewallRule -DisplayName "ICMPv4 Trippy Allow" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow
New-NetFirewallRule -DisplayName "ICMPv6 Trippy Allow" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow
- name: Run simulation test on ${{ matrix.build }}
if: ${{ ! startsWith(matrix.build, 'windows') }}
run: sudo -E env "PATH=$PATH" cargo test --target ${{ matrix.target }} --features sim-tests --test sim -- --exact --nocapture
- name: Run simulation test on ${{ matrix.build }}
run: cargo test --target --target ${{ matrix.target }} --features sim-tests --test sim -- --exact --nocapture
if: startsWith(matrix.build, 'windows')

fmt:
runs-on: ubuntu-22.04
steps:
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ test-case = "3.3.1"
serde_yaml = "0.9.30"
tun = { git = "https://github.com/ssrlive/rust-tun", features = [ "async" ] }

[features]
# Enable simulation integration tests
sim-tests = []

# cargo-generate-rpm dependencies
[package.metadata.generate-rpm]
assets = [
Expand Down
Empty file removed tests/integration_tests.rs
Empty file.
4 changes: 2 additions & 2 deletions tests/resources/simulation/ipv4_icmp_simple.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Simple example
name: Simple IPV4/ICMP example with 9 hops, 2 of which do not respond
target: 10.0.0.109
icmp_identifier: 314
hops:
Expand Down Expand Up @@ -29,7 +29,7 @@ hops:
addr: 10.0.0.107
rtt_ms: 20
- ttl: 8
resp: !NoResponse
resp: NoResponse
- ttl: 9
resp: !SingleHost
addr: 10.0.0.109
Expand Down
Binary file added tests/resources/wintun.dll
Binary file not shown.
6 changes: 6 additions & 0 deletions tests/sim/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#![cfg(feature = "sim-tests")]
mod network;
mod simulation;
mod tests;
mod tracer;
mod tun_device;
99 changes: 99 additions & 0 deletions tests/sim/network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::simulation::{Response, Simulation, SingleHost};
use crate::tun_device::TunDevice;
use parking_lot::Mutex;
use std::io::{ErrorKind, Read, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use trippy::tracing::packet::checksum::{icmp_ipv4_checksum, ipv4_header_checksum};
use trippy::tracing::packet::icmpv4::echo_request::EchoRequestPacket;
use trippy::tracing::packet::icmpv4::time_exceeded::TimeExceededPacket;
use trippy::tracing::packet::icmpv4::{IcmpCode, IcmpType};
use trippy::tracing::packet::ipv4::Ipv4Packet;
use trippy::tracing::packet::IpProtocol;

pub fn run(tun: &Arc<Mutex<TunDevice>>, sim: Arc<Simulation>) -> anyhow::Result<()> {
let mut tun = tun.lock();
loop {
let mut buf = [0_u8; 4096];
let bytes_read = match tun.read(&mut buf) {
Ok(bytes) => Ok(bytes),
Err(err) if err.kind() == ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
continue;
}
Err(err) => Err(err),
}?;
let ipv4 = Ipv4Packet::new_view(&buf[..bytes_read])?;
if ipv4.get_version() != 4 {
continue;
}
let echo_request = EchoRequestPacket::new_view(ipv4.payload())?;
if echo_request.get_identifier() != sim.icmp_identifier {
continue;
}
// if the ttl is greater than the largest ttl in our sim we will reply as the last node in the sim
let index = std::cmp::min(usize::from(ipv4.get_ttl()) - 1, sim.hops.len() - 1);
let reply_addr = match sim.hops[index].resp {
Response::NoResponse => {
continue;
}
Response::SingleHost(SingleHost {
addr: IpAddr::V4(addr),
..
}) => addr,
_ => unimplemented!(),
};
println!("sending ttl {} reply from {}", ipv4.get_ttl(), reply_addr);
let orig_datagram_length = usize::from(ipv4.get_header_length() * 4) + 8;
let te_length = TimeExceededPacket::minimum_packet_size() + orig_datagram_length;
let mut te_buf = vec![0_u8; te_length];
let te_packet = make_time_exceeded_v4(&mut te_buf, &ipv4.packet()[..orig_datagram_length])?;
let ipv4_length = Ipv4Packet::minimum_packet_size() + te_packet.packet().len();
let mut ipv4_buf = vec![0_u8; ipv4_length];
let ipv4_packet = make_ip_v4(
&mut ipv4_buf,
reply_addr,
ipv4.get_source(),
te_packet.packet(),
)?;
tun.write_all(ipv4_packet.packet())?;
}
}

// assumes buf is exactly the right size for the full TimeExceededPacket
fn make_time_exceeded_v4<'a>(
buf: &'a mut [u8],
payload: &[u8],
) -> anyhow::Result<TimeExceededPacket<'a>> {
let mut packet = TimeExceededPacket::new(buf)?;
packet.set_icmp_type(IcmpType::TimeExceeded);
packet.set_icmp_code(IcmpCode(0));
packet.set_payload(payload);
packet.set_checksum(icmp_ipv4_checksum(packet.packet()));
Ok(packet)
}

// assumes buf is exactly the right size for the full Ipv4Packet
fn make_ip_v4<'a>(
buf: &'a mut [u8],
source: Ipv4Addr,
destination: Ipv4Addr,
payload: &[u8],
) -> anyhow::Result<Ipv4Packet<'a>> {
let ipv4_total_length = buf.len();
let mut packet = Ipv4Packet::new(buf)?;
packet.set_version(4);
packet.set_header_length(5);
packet.set_protocol(IpProtocol::Icmp);
packet.set_ttl(64);
packet.set_source(source);
packet.set_destination(destination);
packet.set_total_length(u16::try_from(ipv4_total_length)?);
packet.set_checksum(ipv4_header_checksum(
&packet.packet()[..Ipv4Packet::minimum_packet_size()],
));
packet.set_payload(payload);
Ok(packet)
}
45 changes: 45 additions & 0 deletions tests/sim/simulation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use serde::{Deserialize, Serialize};
use std::net::IpAddr;

/// A simulated trace.
#[derive(Debug, Serialize, Deserialize)]
pub struct Simulation {
pub name: String,
pub target: IpAddr,
pub icmp_identifier: u16,
pub hops: Vec<Hop>,
}

impl Simulation {
pub fn latest_ttl(&self) -> u8 {
// TODO will fail if no hops
self.hops[self.hops.len() - 1].ttl
}
}

/// A simulated hop.
#[derive(Debug, Serialize, Deserialize)]
pub struct Hop {
/// The simulated time-to-live (TTL).
pub ttl: u8,
/// The simulated probe response.
pub resp: Response,
}

/// A simulated probe response.
#[derive(Debug, Serialize, Deserialize)]
pub enum Response {
/// Simulate a hop which does not response to probes.
NoResponse,
/// Simulate a hop which responds to probes from a single host.
SingleHost(SingleHost),
}

/// A simulated probe response with a single addr and fixed ttl.
#[derive(Debug, Serialize, Deserialize)]
pub struct SingleHost {
/// The simulated host responding to the probe.
pub addr: IpAddr,
/// The simulated round trim time (RTT) in ms.
pub rtt_ms: u16,
}
25 changes: 25 additions & 0 deletions tests/sim/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use crate::simulation::Simulation;
use crate::tun_device::{tun, tun_lock};
use crate::{network, tracer};
use std::sync::Arc;
use std::thread;

#[test]
fn test_simulation() -> anyhow::Result<()> {
let sim = serde_yaml::from_str(include_str!(
"../resources/simulation/ipv4_icmp_simple.yaml"
))?;
run_test(sim)
}

fn run_test(simulation: Simulation) -> anyhow::Result<()> {
let _lock = tun_lock().lock();
let tun = tun();
let sim = Arc::new(simulation);
let _handle = {
let sim = sim.clone();
thread::spawn(move || network::run(tun, sim).unwrap())
};
tracer::Tracer::new(sim).trace()?;
Ok(())
}
62 changes: 62 additions & 0 deletions tests/sim/tracer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::simulation::{Response, Simulation, SingleHost};
use std::num::NonZeroUsize;
use std::sync::Arc;
use trippy::tracing::{
Builder, CompletionReason, MaxRounds, ProbeStatus, TimeToLive, TraceId, TracerRound,
};

pub struct Tracer {
sim: Arc<Simulation>,
}

impl Tracer {
pub fn new(sim: Arc<Simulation>) -> Self {
Self { sim }
}

pub fn trace(&self) -> anyhow::Result<()> {
Builder::new(self.sim.target, |round| self.validate_round(round))
.trace_identifier(TraceId(self.sim.icmp_identifier))
.max_rounds(MaxRounds(NonZeroUsize::MIN))
.start()?;
Ok(())
}

fn validate_round(&self, round: &TracerRound<'_>) {
self.show(round);
assert_eq!(CompletionReason::TargetFound, round.reason);
assert_eq!(TimeToLive(self.sim.latest_ttl()), round.largest_ttl);
let largest_ttl = usize::from(round.largest_ttl.0);
for (i, hop) in round.probes[..largest_ttl].iter().enumerate() {
let (expected_status, expected_host) = match self.sim.hops[i].resp {
Response::NoResponse => (ProbeStatus::Awaited, None),
Response::SingleHost(SingleHost { addr, .. }) => {
(ProbeStatus::Complete, Some(addr))
}
};
let expected_ttl = TimeToLive(self.sim.hops[i].ttl);
assert_eq!(expected_status, hop.status);
assert_eq!(expected_host, hop.host);
assert_eq!(expected_ttl, hop.ttl);
}
}

fn show(&self, round: &TracerRound<'_>) {
for hop in &round.probes[..round.largest_ttl.0 as usize] {
match hop.status {
ProbeStatus::Complete => {
println!(
"{} {} {}",
hop.round.0,
hop.ttl.0,
hop.host.as_ref().map(ToString::to_string).unwrap(),
);
}
ProbeStatus::Awaited => {
println!("{} {} * * *", hop.round.0, hop.ttl.0);
}
_ => {}
}
}
}
}
Loading

0 comments on commit 23a9476

Please sign in to comment.