From 818cd7dc9e422a32af023fdec45236521731ccad Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Mon, 19 Oct 2026 12:42:36 +0200 Subject: [PATCH 1/2] perf: reduce routine event traffic --- CHANGELOG.md | 1 + members/nullnet-grpc-lib/src/lib.rs | 49 ++++++- members/nullnet-proxy/src/main.rs | 23 +-- members/nullnet-server/src/events.rs | 47 ------- .../nullnet-server/src/nullnet_grpc_impl.rs | 40 ++++-- .../nullnet-server/ui/src/lib/eventKinds.ts | 131 ++++++++++++++++++ .../nullnet-server/ui/src/pages/Events.tsx | 117 ++-------------- members/nullnet-server/ui/src/types.ts | 2 - .../ui/tests/eventKinds.test.mjs | 18 +++ 9 files changed, 235 insertions(+), 193 deletions(-) create mode 100644 members/nullnet-server/ui/src/lib/eventKinds.ts create mode 100644 members/nullnet-server/ui/tests/eventKinds.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 052637e..a9be39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All Nullnet releases with the relative changes are documented in this file. - Per-service egress/ingress traffic filters: arbitrary AND/OR/group combinations of Country, Organization, Src IP (ingress), and Dst IP (egress) conditions, evaluated via `rpn-predicate-interpreter` — replaces the country-only egress/ingress policy ([#171](https://github.com/NullNet-ai/nullnet/pull/171) — fixes [#143](https://github.com/NullNet-ai/nullnet/issues/143)) - Persist ingress and egress sessions to SQLite and show the full history on the Sessions page, filterable by status, service, direction, and policy verdict, with its own retention window ([#170](https://github.com/NullNet-ai/nullnet/pull/170) — fixes [#156](https://github.com/NullNet-ai/nullnet/issues/156)) ### Changed +- Keep routine successful proxy requests out of Events, isolate diagnostic event RPC admission from routing, and group event filters by scope. - Reduce fresh-edge cleanup work, batch conntrack scans, and share TC classifier infrastructure while preserving dedicated encrypted endpoints ([#201](https://github.com/NullNet-ai/nullnet/pull/201)). - Keep active backend chains visible to conntrack so liveness tracking does not release endpoints that still carry traffic ([#201](https://github.com/NullNet-ai/nullnet/pull/201)). - Prevent Docker Swarm VNI collisions and keep source-scoped egress routing valid for the full network-ID range ([#201](https://github.com/NullNet-ai/nullnet/pull/201)). diff --git a/members/nullnet-grpc-lib/src/lib.rs b/members/nullnet-grpc-lib/src/lib.rs index df71e71..11956e8 100644 --- a/members/nullnet-grpc-lib/src/lib.rs +++ b/members/nullnet-grpc-lib/src/lib.rs @@ -23,6 +23,8 @@ use tonic::transport::{Channel, ClientTlsConfig}; const MAX_IN_FLIGHT_UNARY: usize = 32; const MAX_IN_FLIGHT_LIFECYCLE: usize = 8; const MAX_IN_FLIGHT_POLICY: usize = 32; +// Diagnostic bursts must not consume routing admission. +const MAX_IN_FLIGHT_EVENTS: usize = 8; /// Why a `proxy` lookup failed. #[derive(Debug)] @@ -40,6 +42,7 @@ pub struct NullnetGrpcInterface { unary_slots: Arc, lifecycle_slots: Arc, policy_slots: Arc, + event_slots: Arc, } impl NullnetGrpcInterface { @@ -72,6 +75,7 @@ impl NullnetGrpcInterface { unary_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_UNARY)), lifecycle_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_LIFECYCLE)), policy_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_POLICY)), + event_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_EVENTS)), }); } @@ -328,10 +332,13 @@ impl NullnetGrpcInterface { #[allow(clippy::missing_errors_doc)] pub async fn report_event(&self, event: AgentEvent) -> Result<(), String> { - self.unary(self.client.clone().report_event(Request::new(event))) - .await - .map(|_| ()) - .map_err(|e| e.to_string()) + Self::admitted( + &self.event_slots, + self.client.clone().report_event(Request::new(event)), + ) + .await + .map(|_| ()) + .map_err(|e| e.to_string()) } /// Subscribe to certificate changes: the returned stream yields the full @@ -389,6 +396,7 @@ mod admission_tests { unary_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_UNARY)), lifecycle_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_LIFECYCLE)), policy_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_POLICY)), + event_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_EVENTS)), }; let _busy = interface .unary_slots @@ -410,6 +418,37 @@ mod admission_tests { ); } + #[tokio::test] + async fn event_reporting_progresses_when_setup_admission_is_full() { + let interface = NullnetGrpcInterface { + client: NullnetGrpcClient::new( + Channel::from_static("http://127.0.0.1:1").connect_lazy(), + ), + unary_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_UNARY)), + lifecycle_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_LIFECYCLE)), + policy_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_POLICY)), + event_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_EVENTS)), + }; + let _busy = interface + .unary_slots + .acquire_many(MAX_IN_FLIGHT_UNARY as u32) + .await + .unwrap(); + let result = tokio::time::timeout( + std::time::Duration::from_millis(200), + interface.report_event(AgentEvent { event: None }), + ) + .await; + assert!( + result.is_ok(), + "event reporting was queued behind setup admission" + ); + assert!( + result.unwrap().is_err(), + "the intentionally absent server should reject the call" + ); + } + #[tokio::test] async fn clones_share_admission_and_cancellation_releases_capacity() { let interface = NullnetGrpcInterface { @@ -419,6 +458,7 @@ mod admission_tests { unary_slots: Arc::new(Semaphore::new(2)), lifecycle_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_LIFECYCLE)), policy_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_POLICY)), + event_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_EVENTS)), }; let (entered, mut received) = mpsc::unbounded_channel(); let mut calls = tokio::task::JoinSet::new(); @@ -456,6 +496,7 @@ mod admission_tests { unary_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_UNARY)), lifecycle_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_LIFECYCLE)), policy_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_POLICY)), + event_slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT_EVENTS)), }; let _busy = interface .unary_slots diff --git a/members/nullnet-proxy/src/main.rs b/members/nullnet-proxy/src/main.rs index 1913b30..4bf69a9 100644 --- a/members/nullnet-proxy/src/main.rs +++ b/members/nullnet-proxy/src/main.rs @@ -14,8 +14,8 @@ use arc_swap::ArcSwap; use async_trait::async_trait; use nullnet_grpc_lib::NullnetGrpcInterface; use nullnet_grpc_lib::nullnet_grpc::{ - AgentEvent, AgentProxyClientNotInet, AgentProxyRequestRouted, AgentTlsCertificateInvalid, - AgentUpstreamLookupFailed, ProxyRequest, agent_event::Event as AgentEventKind, + AgentEvent, AgentProxyClientNotInet, AgentTlsCertificateInvalid, AgentUpstreamLookupFailed, + ProxyRequest, agent_event::Event as AgentEventKind, }; use nullnet_liberror::{ErrorHandler, Location, location}; use pingora_core::listeners::tls::TlsSettings; @@ -379,25 +379,6 @@ impl ProxyHttp for NullnetProxy { println!("upstream: {upstream}\n"); let latency_ms = init_t.elapsed().as_millis() as u64; - let server = self.server.clone(); - let svc = service_name.clone(); - let cip = client_ip.clone(); - let uip = upstream.ip().to_string(); - tokio::spawn(async move { - let _ = server - .report_event(AgentEvent { - event: Some(AgentEventKind::ProxyRequestRouted( - AgentProxyRequestRouted { - service_name: svc, - client_ip: cip, - upstream_ip: uip, - latency_ms, - }, - )), - }) - .await; - }); - println!("TOTAL VLANS SETUP TIME: {} ms\n", latency_ms); Ok(Box::new(HttpPeer::new(upstream, false, String::new()))) diff --git a/members/nullnet-server/src/events.rs b/members/nullnet-server/src/events.rs index c87100a..f9c8220 100644 --- a/members/nullnet-server/src/events.rs +++ b/members/nullnet-server/src/events.rs @@ -220,12 +220,6 @@ pub(crate) enum Event { client_ip: String, timestamp: u64, }, - StickySessionReused { - service: String, - client_ip: String, - proxy_ip: String, - timestamp: u64, - }, StaleSessionEvicted { service: String, client_ip: String, @@ -495,15 +489,6 @@ pub(crate) enum Event { timestamp: u64, }, - // --- Proxy info events --- - ProxyRequestRouted { - service_name: String, - client_ip: String, - upstream_ip: String, - latency_ms: u64, - timestamp: u64, - }, - // --- Certificate events --- UiTlsCertificateActive { domain: String, @@ -576,7 +561,6 @@ impl Event { Self::AllReplicasRemoved { .. } => "all_replicas_removed", Self::ServiceReachabilityToggled { .. } => "service_reachability_toggled", Self::ProxyClientTimedOut { .. } => "proxy_client_timed_out", - Self::StickySessionReused { .. } => "sticky_session_reused", Self::StaleSessionEvicted { .. } => "stale_session_evicted", Self::MaxNetworksLimitEnforced { .. } => "max_networks_limit_enforced", Self::NetIdPoolExhausted { .. } => "net_id_pool_exhausted", @@ -627,7 +611,6 @@ impl Event { Self::UdpListenerBindFailed { .. } => "udp_listener_bind_failed", Self::TcpUpstreamConnectFailed { .. } => "tcp_upstream_connect_failed", Self::UdpUpstreamConnectFailed { .. } => "udp_upstream_connect_failed", - Self::ProxyRequestRouted { .. } => "proxy_request_routed", Self::UiTlsCertificateActive { .. } => "ui_tls_certificate_active", Self::UiTlsCertificateUnavailable { .. } => "ui_tls_certificate_unavailable", Self::CertificateInstalled { .. } => "certificate_installed", @@ -669,12 +652,10 @@ impl Event { | Self::ConfigStackRemoved { .. } | Self::ServiceUnregistered { .. } | Self::ServiceReachabilityToggled { .. } - | Self::StickySessionReused { .. } | Self::VxlanSetupCompleted { .. } | Self::VlanSetupCompleted { .. } | Self::ControlChannelEstablished { .. } | Self::ServicesListUpdated { .. } - | Self::ProxyRequestRouted { .. } | Self::ProxyConnected { .. } | Self::UiTlsCertificateActive { .. } | Self::CertificateInstalled { .. } @@ -948,19 +929,6 @@ impl Event { } } - pub(crate) fn sticky_session_reused( - service: String, - client_ip: String, - proxy_ip: String, - ) -> Self { - Self::StickySessionReused { - service, - client_ip, - proxy_ip, - timestamp: now_secs(), - } - } - pub(crate) fn stale_session_evicted( service: String, client_ip: String, @@ -1297,21 +1265,6 @@ impl Event { } } - pub(crate) fn proxy_request_routed( - service_name: String, - client_ip: String, - upstream_ip: String, - latency_ms: u64, - ) -> Self { - Self::ProxyRequestRouted { - service_name, - client_ip, - upstream_ip, - latency_ms, - timestamp: now_secs(), - } - } - pub(crate) fn ui_tls_certificate_active(domain: String) -> Self { Self::UiTlsCertificateActive { domain, diff --git a/members/nullnet-server/src/nullnet_grpc_impl.rs b/members/nullnet-server/src/nullnet_grpc_impl.rs index 339c476..927195a 100644 --- a/members/nullnet-server/src/nullnet_grpc_impl.rs +++ b/members/nullnet-server/src/nullnet_grpc_impl.rs @@ -578,15 +578,6 @@ impl NullnetGrpcImpl { if intact { println!("'{client_ip}' ---> '{service_name}' is already set up"); - self.orchestrator - .events - .emit(Event::sticky_session_reused( - service_name.to_string(), - client_ip.to_string(), - proxy_ip.to_string(), - )) - .await; - // update the latest timestamp for this client since it's being used again let mut services_mut = self.services.write().await; if let Some(stack_map) = services_mut.get_mut(&stack) @@ -2868,12 +2859,8 @@ impl NullnetGrpc for NullnetGrpcImpl { AgentEventKind::UdpUpstreamConnectFailed(e) => { Event::udp_upstream_connect_failed(e.service_name, e.client_ip, e.error_message) } - AgentEventKind::ProxyRequestRouted(e) => Event::proxy_request_routed( - e.service_name, - e.client_ip, - e.upstream_ip, - e.latency_ms, - ), + // Older proxies may still report this routine success event. + AgentEventKind::ProxyRequestRouted(_) => return Ok(Response::new(Empty {})), }; self.orchestrator.events.emit(event).await; Ok(Response::new(Empty {})) @@ -2887,6 +2874,29 @@ mod proxy_presence_tests { use tokio_stream::StreamExt; use tonic::transport::server::TcpConnectInfo; + #[tokio::test] + async fn legacy_proxy_request_routed_event_is_ignored() { + let server = NullnetGrpcImpl::new_for_test(StackMap::new()); + let mut events = server.orchestrator.events.subscribe(); + server + .report_event(Request::new(AgentEvent { + event: Some(AgentEventKind::ProxyRequestRouted( + nullnet_grpc_lib::nullnet_grpc::AgentProxyRequestRouted { + service_name: "service".into(), + client_ip: "198.18.0.1".into(), + upstream_ip: "10.0.0.1".into(), + latency_ms: 1, + }, + )), + })) + .await + .unwrap(); + assert!(matches!( + events.try_recv(), + Err(tokio::sync::broadcast::error::TryRecvError::Empty) + )); + } + #[tokio::test] async fn idle_proxy_presence_survives_overlapping_streams() { let mut server = NullnetGrpcImpl::new_for_test(StackMap::new()); diff --git a/members/nullnet-server/ui/src/lib/eventKinds.ts b/members/nullnet-server/ui/src/lib/eventKinds.ts new file mode 100644 index 0000000..8d4206d --- /dev/null +++ b/members/nullnet-server/ui/src/lib/eventKinds.ts @@ -0,0 +1,131 @@ +import type { EventJson } from '../types'; + +export type EventKind = EventJson['type']; + +export interface EventKindGroup { + label: string; + kinds: readonly EventKind[]; +} + +type ExhaustiveEventKindGroups = + Exclude extends never ? T : never; + +function defineEventKindGroups(groups: ExhaustiveEventKindGroups): T { + return groups; +} + +export const EVENT_KIND_GROUPS = defineEventKindGroups([ + { + label: 'Persistence', + kinds: [ + 'event_persistence_failed', + 'event_persistence_overflow', + 'event_persistence_recovered', + 'session_persistence_failed', + 'session_persistence_overflow', + 'session_persistence_recovered', + ], + }, + { + label: 'Server', + kinds: [ + 'all_replicas_removed', + 'backend_trigger_setup_bailed', + 'chain_owner_lost', + 'config_reloaded', + 'config_stack_removed', + 'edge_promotion_lost', + 'egress_reservation_reclaimed', + 'file_watch_failed', + 'max_networks_limit_enforced', + 'net_id_pool_exhausted', + 'net_teardown_unconfirmed', + 'node_connected', + 'node_disconnected', + 'observation_changed', + 'port_mapping_conflict', + 'proxy_chain_setup_failed', + 'proxy_client_timed_out', + 'route_conflict', + 'service_declaration_skipped', + 'service_name_conflict', + 'service_reachability_toggled', + 'service_registered', + 'service_unregistered', + 'session_created', + 'session_torn_down', + 'setup_ack', + 'setup_started', + 'setup_timeout', + 'stale_session_evicted', + ], + }, + { + label: 'Client', + kinds: [ + 'backend_trigger_send_failed', + 'backend_trigger_setup_timed_out', + 'conntrack_flush_failed', + 'conntrack_subscribe_failed', + 'container_resume_failed', + 'container_suspend_failed', + 'control_channel_ack_failed', + 'control_channel_closed', + 'control_channel_established', + 'device_event_bypass_changed', + 'dnat_install_failed', + 'dnat_removal_failed', + 'egress_policy_check_failed', + 'egress_steer_install_failed', + 'egress_steer_setup_timed_out', + 'egress_trigger_send_failed', + 'firewall_rules_load_failed', + 'gateway_forward_install_failed', + 'host_mapping_failed', + 'mss_clamp_install_failed', + 'nfqueue_bind_failed', + 'services_list_update_failed', + 'services_list_updated', + 'vlan_setup_completed', + 'vlan_setup_failed', + 'vlan_teardown_failed', + 'vxlan_environment_failed', + 'vxlan_setup_completed', + 'vxlan_setup_failed', + 'vxlan_teardown_failed', + ], + }, + { + label: 'Proxy', + kinds: [ + 'proxy_client_not_inet', + 'proxy_connected', + 'proxy_disconnected', + 'tcp_listener_bind_failed', + 'tcp_upstream_connect_failed', + 'tls_certificate_invalid', + 'udp_listener_bind_failed', + 'udp_upstream_connect_failed', + 'upstream_ip_parse_failed', + 'upstream_lookup_failed', + ], + }, + { + label: 'Certificates', + kinds: [ + 'certificate_credentials_store_failed', + 'certificate_installed', + 'certificate_removed', + 'certificate_renewal_failed', + 'certificate_renewed', + 'ui_tls_certificate_active', + 'ui_tls_certificate_unavailable', + ], + }, +]); + +const EVENT_KINDS: ReadonlySet = new Set(EVENT_KIND_GROUPS.flatMap(group => group.kinds)); + +export function isSupportedEventKind(kind: string): kind is EventKind { + return EVENT_KINDS.has(kind); +} diff --git a/members/nullnet-server/ui/src/pages/Events.tsx b/members/nullnet-server/ui/src/pages/Events.tsx index 644e72c..561208c 100644 --- a/members/nullnet-server/ui/src/pages/Events.tsx +++ b/members/nullnet-server/ui/src/pages/Events.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; import Layout from '../components/Layout'; import { apiFetch } from '../lib/apiFetch'; +import { EVENT_KIND_GROUPS, isSupportedEventKind } from '../lib/eventKinds'; import { formatTimestamp, formatTimestampFull } from '../lib/time'; import type { EventJson, EventsPage, Severity } from '../types'; @@ -12,101 +13,6 @@ const SEVERITY_COLOR: Record = { error: 'var(--red, #f87171)', }; -const KIND_LABELS: Record = { - session_persistence_overflow: 'session_persistence_overflow', - session_persistence_failed: 'session_persistence_failed', - session_persistence_recovered: 'session_persistence_recovered', - event_persistence_overflow: 'event_persistence_overflow', - event_persistence_failed: 'event_persistence_failed', - event_persistence_recovered: 'event_persistence_recovered', - // Server events - node_connected: 'node_connected', - node_disconnected: 'node_disconnected', - service_registered: 'service_registered', - service_unregistered: 'service_unregistered', - service_declaration_skipped: 'service_declaration_skipped', - setup_started: 'setup_started', - setup_ack: 'setup_ack', - setup_timeout: 'setup_timeout', - egress_reservation_reclaimed: 'egress_reservation_reclaimed', - edge_promotion_lost: 'edge_promotion_lost', - chain_owner_lost: 'chain_owner_lost', - session_created: 'session_created', - session_torn_down: 'session_torn_down', - net_teardown_unconfirmed: 'net_teardown_unconfirmed', - observation_changed: 'observation_changed', - config_reloaded: 'config_reloaded', - config_stack_removed: 'config_stack_removed', - route_conflict: 'route_conflict', - service_name_conflict: 'service_name_conflict', - all_replicas_removed: 'all_replicas_removed', - service_reachability_toggled: 'service_reachability_toggled', - proxy_client_timed_out: 'proxy_client_timed_out', - sticky_session_reused: 'sticky_session_reused', - stale_session_evicted: 'stale_session_evicted', - max_networks_limit_enforced: 'max_networks_limit_enforced', - net_id_pool_exhausted: 'net_id_pool_exhausted', - proxy_chain_setup_failed: 'proxy_chain_setup_failed', - backend_trigger_setup_bailed: 'backend_trigger_setup_bailed', - file_watch_failed: 'file_watch_failed', - port_mapping_conflict: 'port_mapping_conflict', - // Client error - vxlan_setup_failed: 'vxlan_setup_failed', - vlan_setup_failed: 'vlan_setup_failed', - vxlan_teardown_failed: 'vxlan_teardown_failed', - vlan_teardown_failed: 'vlan_teardown_failed', - dnat_install_failed: 'dnat_install_failed', - dnat_removal_failed: 'dnat_removal_failed', - host_mapping_failed: 'host_mapping_failed', - control_channel_closed: 'control_channel_closed', - control_channel_ack_failed: 'control_channel_ack_failed', - services_list_update_failed: 'services_list_update_failed', - backend_trigger_send_failed: 'backend_trigger_send_failed', - egress_trigger_send_failed: 'egress_trigger_send_failed', - gateway_forward_install_failed: 'gateway_forward_install_failed', - device_event_bypass_changed: 'device_event_bypass_changed', - vxlan_environment_failed: 'vxlan_environment_failed', - firewall_rules_load_failed: 'firewall_rules_load_failed', - container_suspend_failed: 'container_suspend_failed', - container_resume_failed: 'container_resume_failed', - backend_trigger_setup_timed_out: 'backend_trigger_setup_timed_out', - egress_steer_setup_timed_out: 'egress_steer_setup_timed_out', - egress_steer_install_failed: 'egress_steer_install_failed', - nfqueue_bind_failed: 'nfqueue_bind_failed', - conntrack_subscribe_failed: 'conntrack_subscribe_failed', - mss_clamp_install_failed: 'mss_clamp_install_failed', - egress_policy_check_failed: 'egress_policy_check_failed', - conntrack_flush_failed: 'conntrack_flush_failed', - // Client info - vxlan_setup_completed: 'vxlan_setup_completed', - vlan_setup_completed: 'vlan_setup_completed', - control_channel_established: 'control_channel_established', - services_list_updated: 'services_list_updated', - // Proxy error - upstream_lookup_failed: 'upstream_lookup_failed', - upstream_ip_parse_failed: 'upstream_ip_parse_failed', - proxy_client_not_inet: 'proxy_client_not_inet', - tls_certificate_invalid: 'tls_certificate_invalid', - tcp_listener_bind_failed: 'tcp_listener_bind_failed', - udp_listener_bind_failed: 'udp_listener_bind_failed', - tcp_upstream_connect_failed: 'tcp_upstream_connect_failed', - udp_upstream_connect_failed: 'udp_upstream_connect_failed', - // Proxy info - proxy_request_routed: 'proxy_request_routed', - proxy_connected: 'proxy_connected', - proxy_disconnected: 'proxy_disconnected', - // Certificate - ui_tls_certificate_active: 'ui_tls_certificate_active', - ui_tls_certificate_unavailable: 'ui_tls_certificate_unavailable', - certificate_installed: 'certificate_installed', - certificate_renewed: 'certificate_renewed', - certificate_removed: 'certificate_removed', - certificate_renewal_failed: 'certificate_renewal_failed', - certificate_credentials_store_failed: 'certificate_credentials_store_failed', -}; - -const ALL_KINDS = Object.keys(KIND_LABELS); - function eventDetail(e: EventJson): string { switch (e.type) { case 'session_persistence_overflow': @@ -162,7 +68,6 @@ function eventDetail(e: EventJson): string { return `${e.service} · ${e.stack} · ${e.reachable ? 'reachable' : 'unreachable'}`; case 'proxy_client_timed_out': return `${e.service} · ${e.client_ip}`; - case 'sticky_session_reused': case 'stale_session_evicted': return `${e.service} · ${e.client_ip} via ${e.proxy_ip}`; case 'max_networks_limit_enforced': @@ -262,8 +167,6 @@ function eventDetail(e: EventJson): string { case 'certificate_credentials_store_failed': return `${e.domain} · ${e.error_message}`; // Proxy info - case 'proxy_request_routed': - return `${e.service_name} · ${e.client_ip} → ${e.upstream_ip} · ${e.latency_ms}ms`; case 'proxy_connected': case 'proxy_disconnected': return e.ip; @@ -287,12 +190,14 @@ async function fetchEventsPage( params.set('limit', String(PAGE_SIZE)); const res = await apiFetch(`/api/events?${params.toString()}`); if (!res.ok) throw new Error(`GET /api/events failed: ${res.status}`); - return res.json(); + const page = await res.json() as EventsPage; + return { ...page, events: page.events.filter(event => isSupportedEventKind(event.type)) }; } export default function Events() { const [searchParams, setSearchParams] = useSearchParams(); - const kindFilter = searchParams.get('kind') ?? ''; + const requestedKind = searchParams.get('kind') ?? ''; + const kindFilter = isSupportedEventKind(requestedKind) ? requestedKind : ''; const severityFilter = (searchParams.get('severity') ?? '') as Severity | ''; function setKindFilter(kind: string) { @@ -377,8 +282,8 @@ export default function Events() { es.onmessage = (ev) => { try { - const event: EventJson = JSON.parse(ev.data); - if (!pausedRef.current) { + const event = JSON.parse(ev.data) as EventJson; + if (isSupportedEventKind(event.type) && !pausedRef.current) { setEvents(prev => { const next = [...prev, event]; return next.length > MAX_EVENTS ? next.slice(next.length - MAX_EVENTS) : next; @@ -465,8 +370,12 @@ export default function Events() { }} > - {ALL_KINDS.map(k => ( - + {EVENT_KIND_GROUPS.map(group => ( + + {group.kinds.map(kind => ( + + ))} + ))} diff --git a/members/nullnet-server/ui/src/types.ts b/members/nullnet-server/ui/src/types.ts index 13f23aa..73edcb2 100644 --- a/members/nullnet-server/ui/src/types.ts +++ b/members/nullnet-server/ui/src/types.ts @@ -168,7 +168,6 @@ export type EventJson = | WithSeverity & { type: 'all_replicas_removed'; service: string; stack: string; ip: string } | WithSeverity & { type: 'service_reachability_toggled'; service: string; stack: string; reachable: boolean } | WithSeverity & { type: 'proxy_client_timed_out'; service: string; client_ip: string } - | WithSeverity & { type: 'sticky_session_reused'; service: string; client_ip: string; proxy_ip: string } | WithSeverity & { type: 'stale_session_evicted'; service: string; client_ip: string; proxy_ip: string } | WithSeverity & { type: 'max_networks_limit_enforced'; service: string; proxy_ip: string; net_id: number; limit: number } | WithSeverity & { type: 'net_id_pool_exhausted'; service: string; client_ip: string } @@ -218,7 +217,6 @@ export type EventJson = | WithSeverity & { type: 'tcp_upstream_connect_failed'; service_name: string; client_ip: string; error_message: string } | WithSeverity & { type: 'udp_upstream_connect_failed'; service_name: string; client_ip: string; error_message: string } // Proxy info events - | WithSeverity & { type: 'proxy_request_routed'; service_name: string; client_ip: string; upstream_ip: string; latency_ms: number } | WithSeverity & { type: 'proxy_connected'; ip: string } | WithSeverity & { type: 'proxy_disconnected'; ip: string } // Certificate events diff --git a/members/nullnet-server/ui/tests/eventKinds.test.mjs b/members/nullnet-server/ui/tests/eventKinds.test.mjs new file mode 100644 index 0000000..a579f8b --- /dev/null +++ b/members/nullnet-server/ui/tests/eventKinds.test.mjs @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import { Buffer } from 'node:buffer'; +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; +import ts from 'typescript'; + +const source = await readFile(new URL('../src/lib/eventKinds.ts', import.meta.url), 'utf8'); +const { outputText } = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ESNext } }); +const { EVENT_KIND_GROUPS } = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString('base64')}`); + +test('event kinds are unique and alphabetical within each scope', () => { + const kinds = EVENT_KIND_GROUPS.flatMap(group => group.kinds); + assert.equal(new Set(kinds).size, kinds.length); + + for (const group of EVENT_KIND_GROUPS) { + assert.deepEqual(group.kinds, group.kinds.toSorted()); + } +}); From d64e73f0d1020640115721f3533bb41e769aa980 Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Mon, 19 Oct 2026 13:07:24 +0200 Subject: [PATCH 2/2] update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9be39f..5db305b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ All Nullnet releases with the relative changes are documented in this file. - Per-service egress/ingress traffic filters: arbitrary AND/OR/group combinations of Country, Organization, Src IP (ingress), and Dst IP (egress) conditions, evaluated via `rpn-predicate-interpreter` — replaces the country-only egress/ingress policy ([#171](https://github.com/NullNet-ai/nullnet/pull/171) — fixes [#143](https://github.com/NullNet-ai/nullnet/issues/143)) - Persist ingress and egress sessions to SQLite and show the full history on the Sessions page, filterable by status, service, direction, and policy verdict, with its own retention window ([#170](https://github.com/NullNet-ai/nullnet/pull/170) — fixes [#156](https://github.com/NullNet-ai/nullnet/issues/156)) ### Changed -- Keep routine successful proxy requests out of Events, isolate diagnostic event RPC admission from routing, and group event filters by scope. +- Keep routine successful proxy requests out of Events, isolate diagnostic event RPC admission from routing, and group event filters by scope ([#204](https://github.com/NullNet-ai/nullnet/pull/204)). - Reduce fresh-edge cleanup work, batch conntrack scans, and share TC classifier infrastructure while preserving dedicated encrypted endpoints ([#201](https://github.com/NullNet-ai/nullnet/pull/201)). - Keep active backend chains visible to conntrack so liveness tracking does not release endpoints that still carry traffic ([#201](https://github.com/NullNet-ai/nullnet/pull/201)). - Prevent Docker Swarm VNI collisions and keep source-scoped egress routing valid for the full network-ID range ([#201](https://github.com/NullNet-ai/nullnet/pull/201)).