Skip to main content

toole_core/
firewall.rs

1// analyse des pare-feux système pour l'aide au diagnostic : je détecte
2// si un pare-feu est actif et si les ports UDP de Toolé (58199 découverte,
3// 58200 réception) sont autorisés. Je ne fais que lire la configuration,
4// jamais de modification : l'ouverture des ports se fait à l'installation
5// (install.sh / NSIS) et les commandes à exécuter sont affichées à l'UI
6
7use std::net::Ipv4Addr;
8
9pub const DISCOVERY_PORT: u16 = 58199;
10pub const RECEIVER_PORT: u16 = 58200;
11
12// état consolidé renvoyé à l'interface
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
14pub struct FirewallStatus {
15    pub os: String,
16    pub active: bool,
17    pub ports_open: bool,
18    pub commands: Vec<String>,
19}
20
21// je déduis l'adresse du groupe multicast... non, rien ici : ce module est
22// uniquement de la lecture de configuration. Je garde la structure minimale.
23
24// je parse la sortie de `ufw status` : je vérifie que le pare-feu est actif
25// et que les règles 58199/udp et 58200/udp sont présentes. La sortie a ce
26// format (locale anglaise par défaut d'ufw) :
27//   Status: active
28//   To                         Action      From
29//   --                         ------      ----
30//   58199/udp                  ALLOW       Anywhere
31//   58200/udp                  ALLOW       Anywhere (v6)
32pub fn ufw_ports_open(status: &str) -> (bool, bool) {
33    let active = status.lines().any(|l| l.trim().starts_with("Status: active"));
34    let mut udp = false;
35    let mut tcp = false;
36    for l in status.lines() {
37        let t = l.trim();
38        if let Some(rest) = t.strip_prefix("58199/udp") {
39            udp = rest.trim_start().starts_with("ALLOW");
40        } else if let Some(rest) = t.strip_prefix("58200/udp") {
41            tcp = rest.trim_start().starts_with("ALLOW");
42        }
43    }
44    (active, udp && tcp)
45}
46
47// je parse la sortie de `firewall-cmd --list-ports` (format "58199/udp 58200/udp") :
48// je renvoie true si les deux ports UDP sont présents
49pub fn firewalld_ports_open(ports: &str) -> bool {
50    let mut discovery = false;
51    let mut receiver = false;
52    for p in ports.split_whitespace() {
53        if p == format!("{DISCOVERY_PORT}/udp") {
54            discovery = true;
55        } else if p == format!("{RECEIVER_PORT}/udp") {
56            receiver = true;
57        }
58    }
59    discovery && receiver
60}
61
62// commandes à proposer à l'utilisateur selon le pare-feu actif (ou l'OS) :
63// les ports UDP de Toolé sont 58199 (découverte) et 58200 (réception)
64pub fn commands_for(fw: &str) -> Vec<String> {
65    match fw {
66        "ufw" => vec![
67            format!("sudo ufw allow {DISCOVERY_PORT}/udp"),
68            format!("sudo ufw allow {RECEIVER_PORT}/udp"),
69        ],
70        "firewalld" => vec![
71            format!("sudo firewall-cmd --permanent --add-port={DISCOVERY_PORT}/udp"),
72            format!("sudo firewall-cmd --permanent --add-port={RECEIVER_PORT}/udp"),
73            "sudo firewall-cmd --reload".to_string(),
74        ],
75        "windows" => vec![
76            format!(
77                "netsh advfirewall firewall add rule name=\"Toolé UDP\" dir=in action=allow protocol=UDP localport={DISCOVERY_PORT},{RECEIVER_PORT} profile=private,domain"
78            ),
79        ],
80        _ => vec![],
81    }
82}
83
84// je construis le statut consolidé pour Linux : je combine ufw et firewalld.
85// Les deux peuvent coexister mais en pratique un seul est actif ; si aucun
86// n'est présent, je considère le pare-feu inactif (pas d'iptables/nftables
87// natif suivi ici). Je ne propose que les commandes du pare-feu réellement
88// actif : montrer du ufw sur une machine firewalld serait trompeur.
89pub fn linux_status(ufw_active: bool, ufw_open: bool, fw_active: bool, fw_open: bool) -> FirewallStatus {
90    let active = ufw_active || fw_active;
91    // sans pare-feu actif, les ports sont ouverts par défaut ; sinon je me
92    // fie au pare-feu effectivement actif
93    let ports_open = if !active {
94        true
95    } else if ufw_active {
96        ufw_open
97    } else {
98        fw_open
99    };
100    let commands = if ufw_active {
101        commands_for("ufw")
102    } else if fw_active {
103        commands_for("firewalld")
104    } else {
105        vec![]
106    };
107    FirewallStatus {
108        os: "linux".to_string(),
109        active,
110        ports_open,
111        commands,
112    }
113}
114
115// je valide qu'une adresse IPv4 est utilisable pour un pair manuel : je ne
116// garde que les adresses unicast routables du LAN privé (RFC 1918) et je
117// refuse loopback, multicast et adresses non spécifiées
118pub fn is_manual_ipv4_allowed(ip: Ipv4Addr) -> bool {
119    if ip.is_loopback() || ip.is_unspecified() || ip.is_multicast() {
120        return false;
121    }
122    let octets = ip.octets();
123    matches!(
124        octets,
125        [10, _, _, _] | [172, 16..=31, _, _] | [192, 168, _, _] | [169, 254, _, _]
126    )
127}