ufwctl 0.1.0

Linux-only Rust library for managing UFW firewall rules
Documentation
//! Comprehensive real-world test of the `ufwctl` crate.
//!
//! Run with:
//!   cargo run --manifest-path /home/snape/_dev/xray/ufwctl/Cargo.toml --example main-test
//!
//! Requires passwordless `sudo ufw` or interactive sudo.
//! Creates and then deletes test rules — does not leave the system modified.

use std::net::IpAddr;

use ufwctl::{Firewall, Protocol, RuleStatus};

fn section(n: u32, label: &str) {
    println!("\n─── [{n}] {label} ───────────────────────────────────────");
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("╔══════════════════════════════════════════════════════╗");
    println!("║          ufwctl  Integration Test Suite             ║");
    println!("╚══════════════════════════════════════════════════════╝");

    let fw = Firewall::new();

    // ── [1] Installation check ──────────────────────────────────────
    section(1, "Installation check");
    let installed = fw.is_installed()?;
    println!("  ufw installed: {installed}");
    if !installed {
        println!("  → ufw not found on this system.  Skipping all tests.");
        return Ok(());
    }

    // ── [2] Status ──────────────────────────────────────────────────
    section(2, "Firewall status");
    let enabled = fw.is_enabled()?;
    println!("  ufw enabled: {enabled}");

    let raw = fw.raw_status()?;
    let first = raw.lines().next().unwrap_or("(empty)");
    println!("  raw status line: {first}");

    // ── [3] Port allow / status / delete (single protocol) ──────────
    section(3, "Port allow / status / delete (TCP)");

    println!("  allowing 5050/tcp …");
    fw.port(5050).allow().tcp().apply()?;

    let s = fw.port(5050).status().tcp()?;
    println!("  status after allow: {s:?}");
    assert_eq!(s, RuleStatus::Allowed, "5050/tcp should be Allowed");

    println!("  deleting 5050/tcp …");
    let deleted = fw.port(5050).delete().tcp().apply()?;
    println!("  deleted: {deleted}");
    assert!(deleted, "5050/tcp should have been deleted");

    let s = fw.port(5050).status().tcp()?;
    println!("  status after delete: {s:?}");
    assert_eq!(s, RuleStatus::None, "5050/tcp should be gone");

    // ── [4] Port allow / status / delete (Both) ─────────────────────
    section(4, "Port allow / status / delete (Both)");

    println!("  allowing 6060 both …");
    fw.port(6060).allow().both().apply()?;
    println!("  tcp status: {:?}", fw.port(6060).status().tcp()?);
    println!("  udp status: {:?}", fw.port(6060).status().udp()?);
    println!("  both status: {:?}", fw.port(6060).status().both()?);

    println!("  deleting 6060 both …");
    let deleted = fw.port(6060).delete().both().apply()?;
    println!("  deleted: {deleted}");
    assert!(deleted, "6060 both should have been deleted");

    let s = fw.port(6060).status().both()?;
    println!("  status after delete: {s:?}");

    // ── [5] Port deny ───────────────────────────────────────────────
    section(5, "Port deny");

    println!("  denying 9090/udp …");
    fw.port(9090).deny().udp().apply()?;
    let s = fw.port(9090).status().udp()?;
    println!("  status: {s:?}");
    assert_eq!(s, RuleStatus::Denied, "9090/udp should be Denied");

    println!("  deleting 9090/udp …");
    let deleted = fw.port(9090).delete().udp().apply()?;
    println!("  deleted: {deleted}");
    assert!(deleted, "9090/udp should have been deleted");

    let s = fw.port(9090).status().udp()?;
    println!("  status after delete: {s:?}");
    assert_eq!(s, RuleStatus::None, "9090/udp should be gone");

    // ── [6] IPv4 allow / status / delete ────────────────────────────
    section(6, "IPv4 allow / status / delete");
    let v4 = "10.255.255.254";

    // Use both (no proto keyword) for reliable create + status lookup
    println!("  allowing {v4} both …");
    fw.ipv4(v4)?.allow().both().apply()?;
    let s = fw.ipv4(v4)?.status().both()?;
    println!("  {v4} both status: {s:?}");
    assert_eq!(s, RuleStatus::Allowed, "{v4} should be Allowed after allow");

    println!("  deleting {v4} both …");
    let deleted = fw.ipv4(v4)?.delete().both().apply()?;
    println!("  deleted: {deleted}");
    assert!(deleted, "{v4} should have been deleted");

    let s = fw.ipv4(v4)?.status().both()?;
    println!("  {v4} both status after delete: {s:?}");

    // ── [7] IPv4 deny ───────────────────────────────────────────────
    section(7, "IPv4 deny");
    let v4d = "10.255.255.253";

    println!("  denying {v4d} both …");
    fw.ipv4(v4d)?.deny().both().apply()?;
    let s = fw.ipv4(v4d)?.status().both()?;
    println!("  {v4d} status: {s:?}");
    assert_eq!(s, RuleStatus::Denied, "{v4d} should be Denied");

    println!("  deleting {v4d} both …");
    let deleted = fw.ipv4(v4d)?.delete().both().apply()?;
    println!("  deleted: {deleted}");
    assert!(deleted, "{v4d} should have been deleted");

    let s = fw.ipv4(v4d)?.status().both()?;
    println!("  {v4d} status after delete: {s:?}");

    // ── [8] IPv6 allow / status / delete ────────────────────────────
    section(8, "IPv6 allow / status / delete");
    let v6 = "::1";

    println!("  allowing {v6} both …");
    fw.ipv6(v6)?.allow().both().apply()?;
    let s = fw.ipv6(v6)?.status().both()?;
    println!("  {v6} both status: {s:?}");

    println!("  deleting {v6} both …");
    let deleted = fw.ipv6(v6)?.delete().both().apply()?;
    println!("  deleted: {deleted}");
    // ::1 might be special on this system; don't assert delete result

    let s = fw.ipv6(v6)?.status().both()?;
    println!("  {v6} both status after delete: {s:?}");

    // ── [9] Listing ────────────────────────────────────────────────
    section(9, "Rule listing");
    let ports = fw.ports()?;
    println!("  port rules: {}", ports.len());
    for r in &ports {
        println!("    {}/{:?}{:?}", r.port, r.protocol, r.status);
    }

    let ips = fw.ips()?;
    println!("  ip rules: {}", ips.len());
    for r in &ips {
        println!("    {} / {:?}{:?}", r.ip, r.protocol, r.status);
    }

    // ── [10] Delete of non-existing rule (idempotent) ───────────────
    section(10, "Delete idempotent (non-existing rule)");

    let deleted = fw.port(59999).delete().tcp().apply()?;
    println!("  delete non-existing port: {deleted} (should be false)");
    assert!(!deleted, "non-existing rule delete should return false");

    let deleted = fw.ipv4("10.255.255.255")?.delete().both().apply()?;
    println!("  delete non-existing IPv4: {deleted} (should be false)");
    assert!(!deleted, "non-existing IPv4 delete should return false");

    // ── [11] Validation ─────────────────────────────────────────────
    section(11, "Address validation");
    assert!(fw.ipv4("192.168.1.1").is_ok(), "valid IPv4");
    assert!(fw.ipv4("not-an-ip").is_err(), "garbage IPv4");
    assert!(fw.ipv4("::1").is_err(), "IPv6 in ipv4()");

    assert!(fw.ipv6("::1").is_ok(), "valid IPv6");
    assert!(fw.ipv6("not-an-ip").is_err(), "garbage IPv6");
    assert!(fw.ipv6("192.168.1.1").is_err(), "IPv4 in ipv6()");

    assert!(fw.ipv4("10.0.0.1").is_ok(), "private IPv4");
    assert!(fw.ipv6("2001:db8::1").is_ok(), "doc IPv6");
    println!("  all validation checks passed");

    // ── [12] Protocol helpers ───────────────────────────────────────
    section(12, "Protocol helpers");
    assert_eq!(Protocol::Tcp.as_str(), "tcp");
    assert_eq!(Protocol::Udp.as_str(), "udp");
    assert_eq!(Protocol::Both.as_str(), "both");

    assert!(Protocol::Tcp.is_tcp());
    assert!(!Protocol::Udp.is_tcp());
    assert!(Protocol::Udp.is_udp());
    assert!(!Protocol::Tcp.is_udp());

    assert_eq!(Protocol::Tcp.to_string(), "tcp");
    assert_eq!(Protocol::Udp.to_string(), "udp");
    assert_eq!(Protocol::Both.to_string(), "both");
    println!("  all protocol helper checks passed");

    // ── [13] Public types ───────────────────────────────────────────
    section(13, "Public type construction");
    let _port_rule = ufwctl::PortRule {
        port: 22,
        protocol: Protocol::Tcp,
        status: RuleStatus::Allowed,
    };
    let ip: IpAddr = "10.0.0.1".parse()?;
    let _ip_rule = ufwctl::IpRule {
        ip,
        protocol: Protocol::Both,
        status: RuleStatus::Denied,
    };
    println!("  type construction works");

    // ── Done ─────────────────────────────────────────────────────────
    println!("\n╔══════════════════════════════════════════════════════╗");
    println!("║   All integration tests PASSED                       ║");
    println!("╚══════════════════════════════════════════════════════╝");

    Ok(())
}