Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,36 @@ jobs:
run: cargo build --all-features --verbose

- name: Run tests
run: cargo test --all-features --verbose -- --nocapture
run: cargo test --all-features --verbose -- --nocapture

Test-Asymmetric-Routing:
needs: [ci]
runs-on: ${{ matrix.os }}

strategy:
matrix:
os: [ubuntu-24.04]

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Build
run: cargo build --all-features --verbose

- name: Install tcpdump and net-tools
run: |
sudo apt-get update -qq
sudo apt-get install -y tcpdump net-tools

- name: Test asymmetric routing (--oiface)
run: sudo ci/test-oiface.sh
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
fping-rust 0.2.0 (new)
======================
- New option --oiface for outgoing interface and source ip flag (#18, @gsnw-sebast)

fping-rust 0.1.1 (2026-03-28)
======================
- fix: validate ICMP ID per socket, fix dual-stack ID mismatch (#15, @gsnw-sebast)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Options:
-6, --ipv6 Use IPv6 only
--report-all-rtts Show all individual RTTs
-x, --reachable <N> Minimum number of reachable hosts to be considered success
--oiface <IFACE> Bind outgoing packets to this network interface (e.g. eth0)
-h, --help Print help
-V, --version Print version
```
Expand Down
79 changes: 79 additions & 0 deletions ci/test-oiface.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
set -ex

NS_TESTER="tester_ns"
NS_TARGET="target_ns"
LOG_FILE="/tmp/asym_trace.log"

cleanup() {
echo "--- FINALER TRACE-LOG ---"
[ -f "$LOG_FILE" ] && cat $LOG_FILE
sudo ip netns del $NS_TESTER 2>/dev/null || true
sudo ip netns del $NS_TARGET 2>/dev/null || true
}
trap cleanup EXIT

echo "--- Network-Setup ---"
sudo ip netns add $NS_TESTER
sudo ip netns add $NS_TARGET

sudo ip link add veth1_tst type veth peer name veth1_trg
sudo ip link add veth2_tst type veth peer name veth2_trg

sudo ip link set veth1_tst netns $NS_TESTER
sudo ip link set veth2_tst netns $NS_TESTER
sudo ip link set veth1_trg netns $NS_TARGET
sudo ip link set veth2_trg netns $NS_TARGET

sudo ip netns exec $NS_TESTER ip addr add 10.0.1.1/24 dev veth1_tst
sudo ip netns exec $NS_TESTER ip addr add 10.0.2.1/24 dev veth2_tst
sudo ip netns exec $NS_TARGET ip addr add 10.0.1.2/24 dev veth1_trg
sudo ip netns exec $NS_TARGET ip addr add 10.0.2.2/24 dev veth2_trg

sudo ip netns exec $NS_TESTER ip link set veth1_tst up
sudo ip netns exec $NS_TESTER ip link set veth2_tst up
sudo ip netns exec $NS_TESTER ip link set lo up
sudo ip netns exec $NS_TARGET ip link set veth1_trg up
sudo ip netns exec $NS_TARGET ip link set veth2_trg up
sudo ip netns exec $NS_TARGET ip link set lo up

echo "--- Configuration for asymmetric routing ---"
for ns in $NS_TESTER $NS_TARGET; do
sudo ip netns exec $ns sysctl -w net.ipv4.conf.all.rp_filter=0
sudo ip netns exec $ns sysctl -w net.ipv4.conf.default.rp_filter=0
sudo ip netns exec $ns sysctl -w net.ipv4.conf.all.accept_local=1
sudo ip netns exec $ns sysctl -w net.ipv4.ip_forward=1

for dev in $(sudo ip netns exec $ns ls /sys/class/net/); do
sudo ip netns exec $ns sysctl -w net.ipv4.conf.$dev.rp_filter=0 2>/dev/null || true
done
done

T_MAC2=$(sudo ip netns exec $NS_TARGET cat /sys/class/net/veth2_trg/address)
sudo ip netns exec $NS_TESTER arp -s 10.0.2.2 $T_MAC2 -i veth2_tst

echo "--- Tests ---"
sudo ip netns exec $NS_TESTER tcpdump -i any icmp -n -l > $LOG_FILE 2>&1 &
TCP_PID=$!
sleep 2

echo "Send fping (asymmetry check)..."
sudo ip netns exec $NS_TESTER ./target/debug/fping -c 1 -t 1000 --oiface veth2_tst -S 10.0.1.1 10.0.2.2 || FPING_STATUS=$?

sleep 1
sudo kill $TCP_PID 2>/dev/null || true
sleep 1

echo "--- Analysis ---"

REQ_OK=$(grep "veth2_tst Out IP 10.0.1.1 > 10.0.2.2" $LOG_FILE | wc -l)
REP_OK=$(grep "veth1_tst In IP 10.0.2.2 > 10.0.1.1" $LOG_FILE | wc -l)

if [ "$REQ_OK" -gt 0 ] && [ "$REP_OK" -gt 0 ]; then
echo "RESULT: TEST SUCCESSFUL (True asymmetry detected)"
exit 0
else
echo "RESULT: TEST FAILED"
[ "$REQ_OK" -eq 0 ] && echo "- The request was not sent correctly with source 10.0.1.1 via veth2_tst."
[ "$REP_OK" -eq 0 ] && echo "- The reply was not received asymmetrically via veth1_tst."
exit 1
fi
8 changes: 8 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ pub struct Args {
/// Minimum number of reachable hosts to be considered success
#[arg(short = 'x', long = "reachable", value_name = "N")]
pub reachable: Option<u32>,

/// Source address for outgoing pings
#[arg(short = 'S', long = "source")]
pub source: Option<String>,

/// Bind outgoing packets to this network interface (e.g. eth0)
#[arg(long = "oiface", value_name = "IFACE")]
pub oiface: Option<String>,
}

impl Args {
Expand Down
52 changes: 49 additions & 3 deletions src/pinger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::output::{
print_global_stats, print_per_host_stats, print_recv, print_timeout,
max_host_len, GlobalStatsSummary, RecvLineOpts, TimeoutLineOpts,
};
use crate::socket::{build_icmp_packet, open_raw_socket, recv_ping, send_ping_v4, send_ping_v6, SocketKind};
use crate::socket::{bind_source_v4, bind_source_v6, build_icmp_packet, open_raw_socket, recv_ping, send_ping_v4, send_ping_v6, set_outgoing_iface_v4, set_outgoing_iface_v6, SocketKind};
use crate::types::{HostEntry, PendingPing};

pub fn run(args: Args, hosts_in: Vec<(String, IpAddr)>) {
Expand Down Expand Up @@ -56,6 +56,52 @@ pub fn run(args: Args, hosts_in: Vec<(String, IpAddr)>) {
let fd4 = owned_fd4.as_ref().map(|o| o.as_raw_fd());
let fd6 = owned_fd6.as_ref().map(|o| o.as_raw_fd());

if let Some(ref src) = args.source {
match src.parse::<IpAddr>() {
Ok(IpAddr::V4(_)) => {
if let Some(fd) = fd4 {
if let Err(e) = bind_source_v4(fd, src) {
eprintln!("fping: {}", e);
std::process::exit(1);
}
}
}
Ok(IpAddr::V6(_)) => {
if let Some(fd) = fd6 {
if let Err(e) = bind_source_v6(fd, src) {
eprintln!("fping: {}", e);
std::process::exit(1);
}
}
}
Err(_) => {
eprintln!("fping: -S/--source '{}' is not a valid IP address", src);
std::process::exit(1);
}
}
}

let oiface_idx4: Option<u32> = if let Some(ref iface) = args.oiface {
if let Some(fd) = fd4 {
match set_outgoing_iface_v4(fd, iface) {
Ok(idx) => Some(idx),
Err(e) => { eprintln!("fping: {}", e); std::process::exit(1); }
}
} else { None }
} else { None };

let oiface_idx6: Option<u32> = if let Some(ref iface) = args.oiface {
if let Some(fd) = fd6 {
match set_outgoing_iface_v6(fd, iface) {
Ok(idx) => Some(idx),
Err(e) => { eprintln!("fping: {}", e); std::process::exit(1); }
}
} else { None }
} else { None };

let src_v4: Option<std::net::Ipv4Addr> = args.source.as_ref().and_then(|s| s.parse::<IpAddr>().ok()).and_then(|a| if let IpAddr::V4(v4) = a { Some(v4) } else { None });
let src_v6: Option<std::net::Ipv6Addr> = args.source.as_ref().and_then(|s| s.parse::<IpAddr>().ok()).and_then(|a| if let IpAddr::V6(v6) = a { Some(v6) } else { None });

let pid_id = (std::process::id() & 0xFFFF) as u16;
let my_id4 = dgram_id4.unwrap_or(pid_id);
let my_id6 = dgram_id6.unwrap_or(pid_id);
Expand Down Expand Up @@ -108,8 +154,8 @@ pub fn run(args: Args, hosts_in: Vec<(String, IpAddr)>) {
let pkt = build_icmp_packet(pkt_id, seq, args.size, is_ipv6, kind);

let sent = match hosts[idx].addr {
IpAddr::V4(ref a) => fd4.map(|fd| send_ping_v4(fd, a, &pkt)).unwrap_or(false),
IpAddr::V6(ref a) => fd6.map(|fd| send_ping_v6(fd, a, &pkt)).unwrap_or(false),
IpAddr::V4(ref a) => fd4.map(|fd| send_ping_v4(fd, a, &pkt, oiface_idx4, src_v4)).unwrap_or(false),
IpAddr::V6(ref a) => fd6.map(|fd| send_ping_v6(fd, a, &pkt, oiface_idx6, src_v6)).unwrap_or(false),
};

if sent && !seqmap.contains_key(&seq) {
Expand Down
Loading
Loading