netcore/service.rs
1//! Listening services and active flows.
2//!
3//! `Service` describes a listening socket plus its effective exposure to the
4//! outside world, computed from the bind scope and the firewall posture.
5//! `Flow` is an established connection.
6
7use std::net::{IpAddr, SocketAddr};
8
9use serde::{Deserialize, Serialize};
10
11use crate::diag::FirewallVerdict;
12use crate::link::{L4Proto, TcpState};
13use crate::process::ProcessInfo;
14
15/// Something listening for inbound connections.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Service {
18 /// Local port the socket is listening on.
19 pub port: u16,
20 pub proto: L4Proto,
21 pub bind: BindScope,
22 pub process: ProcessInfo,
23 /// Effective reachability, computed by joining [`BindScope`] with the
24 /// firewall's inbound verdict for (port, proto).
25 pub exposure: Exposure,
26}
27
28/// Where a listening socket is bound.
29///
30/// A Linux `[::]:22` listener accepts IPv4 connections as well unless the
31/// `net.ipv6.bindv6only` sysctl is set — [`BindScope::AnyAddress`] is the
32/// right variant in that dual-stack case.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(tag = "type", content = "value", rename_all = "snake_case")]
35pub enum BindScope {
36 AnyAddress,
37 Loopback,
38 /// `%iface` suffix on the bind address (common with link-local IPv6).
39 SpecificInterface(String),
40 /// A specific non-loopback address (e.g. `127.0.0.53` for the systemd
41 /// stub, or a LAN IP when the process bound to one).
42 SpecificAddress(IpAddr),
43}
44
45/// How widely a listening service is reachable.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum Exposure {
49 /// Bound to loopback — only reachable from this host.
50 LocalOnly,
51 /// Firewall blocks WAN but LAN peers can reach the service.
52 LanOnly,
53 /// Reachable from any peer that can reach this host.
54 Exposed,
55 /// Firewall state unknown (no [`Firewall`](crate::Firewall) backend).
56 Unknown,
57}
58
59impl Exposure {
60 /// Combine a [`BindScope`] with a [`FirewallVerdict`] into an [`Exposure`].
61 ///
62 /// Loopback-bound services are always `LocalOnly` regardless of firewall.
63 /// A specific-address bind on a non-loopback address is treated like
64 /// any-address for firewall purposes (the firewall still governs WAN).
65 pub fn from_scope_and_verdict(bind: &BindScope, verdict: FirewallVerdict) -> Self {
66 match bind {
67 BindScope::Loopback => Exposure::LocalOnly,
68 BindScope::SpecificAddress(ip) if ip.is_loopback() => Exposure::LocalOnly,
69 _ => match verdict {
70 FirewallVerdict::Allow => Exposure::Exposed,
71 FirewallVerdict::Drop | FirewallVerdict::Reject => Exposure::LanOnly,
72 FirewallVerdict::NoMatch | FirewallVerdict::Unknown => Exposure::Unknown,
73 },
74 }
75 }
76}
77
78/// An established or in-flight connection to/from this host.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Flow {
81 pub proto: L4Proto,
82 /// Local socket address.
83 pub local: SocketAddr,
84 /// Remote peer socket address.
85 pub remote: SocketAddr,
86 pub state: TcpState,
87 pub process: ProcessInfo,
88 /// Bytes received on this flow.
89 pub bytes_in: u64,
90 /// Bytes sent on this flow.
91 pub bytes_out: u64,
92 /// Smoothed round-trip time in microseconds, when the kernel has one
93 /// (TCP only, and only once the connection has carried data).
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub rtt_us: Option<u32>,
96}