implement option to filter for state

This commit is contained in:
Marco Cadetg
2025-09-10 11:30:14 +02:00
parent 45056d41c8
commit 8982d24abd
2 changed files with 151 additions and 0 deletions

View File

@@ -143,11 +143,35 @@ Press `/` to enter filter mode. Type to filter connections in real-time, navigat
- `dst:github.com` - Destinations containing "github.com"
- `process:ssh` - Process names containing "ssh"
- `sni:api` - SNI hostnames containing "api"
- `state:established` - Filter connections by protocol state
**State filtering:**
Filter connections by their current protocol state (case-insensitive):
⚠️ **Note:** State tracking accuracy varies by protocol. TCP states are most reliable, while UDP, QUIC, and other protocol states are derived from packet inspection and internal lifecycle management, which may not always reflect the true connection state.
- `state:syn_recv` - Show half-open connections (useful for detecting SYN floods)
- `state:established` - Show only established connections
- `state:fin_wait` - Show connections in closing states
- `state:quic_handshake` - Show QUIC connections during handshake
- `state:dns_query` - Show DNS query connections
- `state:udp_active` - Show active UDP connections
**Available states:**
- **TCP**: `SYN_SENT`, `SYN_RECV`, `ESTABLISHED`, `FIN_WAIT1`, `FIN_WAIT2`, `TIME_WAIT`, `CLOSE_WAIT`, `LAST_ACK`, `CLOSING`, `CLOSED`
- **QUIC**: `QUIC_INITIAL`, `QUIC_HANDSHAKE`, `QUIC_CONNECTED`, `QUIC_DRAINING`, `QUIC_CLOSED` ⚠️ *Note: QUIC state tracking may be incomplete due to encrypted handshake packets and reassembly challenges*
- **UDP**: `UDP_ACTIVE`, `UDP_IDLE`, `UDP_STALE`
- **DNS**: `DNS_QUERY`, `DNS_RESPONSE`
- **Other**: `ECHO_REQUEST`, `ECHO_REPLY`, `ARP_REQUEST`, `ARP_REPLY`
**Examples:**
- `sport:80 process:nginx` - Nginx connections from port 80
- `dport:443 sni:google.com` - HTTPS connections to Google
- `sport:443 state:syn_recv` - Half-open connections to port 443 (SYN flood detection)
- `proto:tcp state:established` - All established TCP connections
- `process:firefox state:quic_connected` - Active QUIC connections from Firefox
Press `Esc` to clear filter.

View File

@@ -24,6 +24,8 @@ pub enum FilterCriteria {
Sni(String),
/// Match DPI application protocol
Application(String),
/// Match connection state (e.g., ESTABLISHED, SYN_RECV)
State(String),
}
pub struct ConnectionFilter {
@@ -79,6 +81,9 @@ impl ConnectionFilter {
"app" | "application" => {
criteria.push(FilterCriteria::Application(value));
}
"state" => {
criteria.push(FilterCriteria::State(value));
}
_ => {
// Unknown keyword, treat as general search
criteria.push(FilterCriteria::General(part.to_lowercase()));
@@ -151,6 +156,9 @@ impl ConnectionFilter {
}
FilterCriteria::Sni(sni_text) => self.matches_sni(connection, sni_text),
FilterCriteria::Application(app_text) => self.matches_application(connection, app_text),
FilterCriteria::State(state_text) => {
connection.state().to_lowercase().contains(state_text)
}
})
}
@@ -372,4 +380,123 @@ mod tests {
_ => panic!("Expected DestinationPort filter"),
}
}
#[test]
fn test_parse_state_filter() {
let filter = ConnectionFilter::parse("state:established");
assert_eq!(filter.criteria.len(), 1);
match &filter.criteria[0] {
FilterCriteria::State(text) => assert_eq!(text, "established"),
_ => panic!("Expected State filter"),
}
}
#[test]
fn test_state_filter_tcp_states() {
use crate::network::types::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
// Create a test connection in ESTABLISHED state
let mut conn = Connection::new(
Protocol::TCP,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 80),
ProtocolState::Tcp(TcpState::Established),
);
// Test matching established state
let established_filter = ConnectionFilter::parse("state:established");
assert!(established_filter.matches(&conn));
// Test partial matching
let est_filter = ConnectionFilter::parse("state:est");
assert!(est_filter.matches(&conn));
// Test case insensitive matching
let upper_filter = ConnectionFilter::parse("state:ESTABLISHED");
assert!(upper_filter.matches(&conn));
// Test non-matching state
let syn_filter = ConnectionFilter::parse("state:syn_recv");
assert!(!syn_filter.matches(&conn));
// Change connection to SYN_RECV state
conn.protocol_state = ProtocolState::Tcp(TcpState::SynReceived);
assert!(syn_filter.matches(&conn));
assert!(!established_filter.matches(&conn));
}
#[test]
fn test_state_filter_udp_states() {
use crate::network::types::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
// Create a fresh UDP connection (should show as UDP_ACTIVE)
let conn = Connection::new(
Protocol::UDP,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 53),
ProtocolState::Udp,
);
let active_filter = ConnectionFilter::parse("state:udp_active");
assert!(active_filter.matches(&conn));
let udp_filter = ConnectionFilter::parse("state:udp");
assert!(udp_filter.matches(&conn));
}
#[test]
fn test_combined_state_and_port_filter() {
use crate::network::types::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let conn = Connection::new(
Protocol::TCP,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 443),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 54321),
ProtocolState::Tcp(TcpState::SynReceived),
);
// Test combined filter: source port 443 AND SYN_RECV state
let combined_filter = ConnectionFilter::parse("sport:443 state:syn_recv");
assert!(combined_filter.matches(&conn));
// Test that both conditions must match
let wrong_port_filter = ConnectionFilter::parse("sport:80 state:syn_recv");
assert!(!wrong_port_filter.matches(&conn));
let wrong_state_filter = ConnectionFilter::parse("sport:443 state:established");
assert!(!wrong_state_filter.matches(&conn));
}
#[test]
fn test_state_filter_case_insensitive() {
use crate::network::types::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let conn = Connection::new(
Protocol::TCP,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 80),
ProtocolState::Tcp(TcpState::Established),
);
// Test various case combinations
let filters = vec![
"state:established",
"state:ESTABLISHED",
"state:Established",
"state:EstAbLiShEd",
];
for filter_str in filters {
let filter = ConnectionFilter::parse(filter_str);
assert!(
filter.matches(&conn),
"Filter '{}' should match ESTABLISHED state",
filter_str
);
}
}
}