Skip to main content

flowscope/well_known/
mod.rs

1//! `flowscope::well_known` — curated port → protocol label table.
2//!
3//! Every observability example written for the 0.9 cycle ended up
4//! reinventing a small "port → protocol name" table:
5//!
6//! ```ignore
7//! match port {
8//!     80 | 8080 => "http",
9//!     443 => "tls/https",
10//!     53 => "dns",
11//!     // … and so on
12//!     _ => "other",
13//! }
14//! ```
15//!
16//! This module ships that table once. ~80 entries (IANA-aligned
17//! plus widely-deployed cloud-native services), refreshed once
18//! per minor release. Lookup is binary-search-based and zero-cost
19//! when the port is unknown.
20//!
21//! ```
22//! use flowscope::L4Proto;
23//! use flowscope::well_known::protocol_label;
24//!
25//! assert_eq!(protocol_label(L4Proto::Tcp, 33000, 80), Some("http"));
26//! assert_eq!(protocol_label(L4Proto::Tcp, 5432, 65000), Some("postgres"));
27//! assert_eq!(protocol_label(L4Proto::Udp, 53, 33000), Some("dns"));
28//! assert_eq!(protocol_label(L4Proto::Tcp, 33000, 33001), None);
29//! ```
30//!
31//! Disambiguation: the lower-numbered port is always treated as
32//! the well-known side. If both ports are non-zero and both
33//! resolve to known labels, the lower one wins (e.g. an
34//! 80 ↔ 443 flow labels as `"http"`).
35//!
36//! New in 0.10.0 (plan 102 sub-D).
37
38use crate::extractor::L4Proto;
39
40/// One row in the curated table. Public so consumers can build
41/// their own filters over the shipped entries.
42pub type Entry = (L4Proto, u16, &'static str);
43
44/// Canonical short label for the given protocol + port pair.
45///
46/// Returns `None` if neither port is in the curated table.
47///
48/// The two port arguments are accepted as `src_port, dst_port`,
49/// but the lookup is order-insensitive — the lower-numbered port
50/// is treated as the well-known side. Pass `0` for either port
51/// to opt out of that side's lookup (useful for ICMP or other
52/// portless flows).
53pub fn protocol_label(proto: L4Proto, src_port: u16, dst_port: u16) -> Option<&'static str> {
54    let table = table_for(proto)?;
55    let lower = match (src_port, dst_port) {
56        (0, 0) => return None,
57        (0, p) | (p, 0) => p,
58        (a, b) => a.min(b),
59    };
60    if let Some(label) = lookup(table, lower) {
61        return Some(label);
62    }
63    // Fall back to the higher port for the pathological case where
64    // the higher port is well-known and the lower one is in the
65    // ephemeral range but happens to be smaller.
66    let higher = src_port.max(dst_port);
67    if higher != lower {
68        lookup(table, higher)
69    } else {
70        None
71    }
72}
73
74/// Iterate every shipped `(proto, port, label)` row. Useful for
75/// rendering the table in `--help` output, or for constructing
76/// custom filters over the curated set.
77pub fn entries() -> impl Iterator<Item = Entry> {
78    TCP_TABLE
79        .iter()
80        .map(|(p, l)| (L4Proto::Tcp, *p, *l))
81        .chain(UDP_TABLE.iter().map(|(p, l)| (L4Proto::Udp, *p, *l)))
82}
83
84fn table_for(proto: L4Proto) -> Option<&'static [(u16, &'static str)]> {
85    match proto {
86        L4Proto::Tcp => Some(TCP_TABLE),
87        L4Proto::Udp => Some(UDP_TABLE),
88        _ => None,
89    }
90}
91
92fn lookup(table: &[(u16, &'static str)], port: u16) -> Option<&'static str> {
93    table
94        .binary_search_by_key(&port, |(p, _)| *p)
95        .ok()
96        .map(|i| table[i].1)
97}
98
99/// Sorted-ascending TCP entries. Add to / refresh during each
100/// minor-release sweep.
101const TCP_TABLE: &[(u16, &str)] = &[
102    (20, "ftp-data"),
103    (21, "ftp"),
104    (22, "ssh"),
105    (23, "telnet"),
106    (25, "smtp"),
107    (53, "dns"),
108    (80, "http"),
109    (110, "pop3"),
110    (143, "imap"),
111    (443, "tls/https"),
112    (465, "smtps"),
113    (587, "smtp-submission"),
114    (853, "dns-over-tls"),
115    (993, "imaps"),
116    (995, "pop3s"),
117    (1433, "mssql"),
118    (1521, "oracle"),
119    (2049, "nfs"),
120    (3306, "mysql"),
121    (3389, "rdp"),
122    (5432, "postgres"),
123    (5672, "amqp"),
124    (5984, "couchdb"),
125    (6379, "redis"),
126    (6443, "kubernetes-api"),
127    (6667, "irc"),
128    (7000, "cassandra"),
129    (7001, "cassandra"),
130    (8000, "http"),
131    (8080, "http"),
132    (8088, "hbase"),
133    (8443, "tls/https"),
134    (8500, "consul"),
135    (9000, "minio"),
136    (9001, "minio"),
137    (9042, "cassandra-cql"),
138    (9092, "kafka"),
139    (9093, "kafka"),
140    (9200, "elasticsearch"),
141    (9300, "elasticsearch"),
142    (10000, "webmin"),
143    (11211, "memcached"),
144    (15672, "rabbitmq-mgmt"),
145    (27017, "mongodb"),
146    (50070, "hdfs"),
147];
148
149/// Sorted-ascending UDP entries.
150const UDP_TABLE: &[(u16, &str)] = &[
151    (53, "dns"),
152    (67, "dhcp"),
153    (68, "dhcp"),
154    (69, "tftp"),
155    (88, "kerberos"),
156    (123, "ntp"),
157    (137, "netbios"),
158    (138, "netbios"),
159    (139, "netbios"),
160    (161, "snmp"),
161    (162, "snmp"),
162    (389, "ldap"),
163    (443, "quic/http3"),
164    (500, "ipsec"),
165    (514, "syslog"),
166    (636, "ldaps"),
167    (853, "dns-over-quic"),
168    (1812, "radius"),
169    (1813, "radius"),
170    (2049, "nfs"),
171    (2152, "gtp-u"),
172    (3478, "stun"),
173    (4500, "ipsec"),
174    (4789, "vxlan"),
175    (5060, "sip"),
176    (5061, "sip"),
177];
178
179// ── Plan 165 (0.14) — LabelTable extensibility ───────────────
180
181/// Caller-supplied port → label table that layers over (or
182/// replaces) the built-in [`protocol_label`] dispatch.
183///
184/// Use for site-custom services ("our internal gRPC on
185/// 8765", "metrics scrape on 9101"). The built-in table
186/// covers ~80 standard ports; this struct lets you add the
187/// rest without forking the source.
188///
189/// `Clone + Send + Sync`. Labels are `&'static str` — match
190/// the built-in contract. For runtime-loaded labels (e.g.
191/// from a YAML/JSON config), use `Box::leak(string)` to
192/// bridge:
193///
194/// ```rust,ignore
195/// let leaked: &'static str = Box::leak(String::from("gRPC-Internal").into_boxed_str());
196/// table.set(L4Proto::Tcp, 8765, leaked);
197/// ```
198///
199/// Plan 165 (0.14).
200#[derive(Clone, Default, Debug)]
201pub struct LabelTable {
202    overrides: std::collections::HashMap<(L4Proto, u16), &'static str>,
203    /// If `true` (default), unknown ports fall back to the
204    /// built-in [`protocol_label`] table. If `false`, only
205    /// `overrides` are consulted.
206    inherit_builtin: bool,
207}
208
209impl LabelTable {
210    /// Empty table that inherits the built-in entries when no
211    /// override matches.
212    pub fn new() -> Self {
213        Self {
214            overrides: std::collections::HashMap::new(),
215            inherit_builtin: true,
216        }
217    }
218
219    /// Empty table that does NOT inherit the built-in
220    /// entries. Strict whitelist semantics.
221    pub fn standalone() -> Self {
222        Self {
223            overrides: std::collections::HashMap::new(),
224            inherit_builtin: false,
225        }
226    }
227
228    /// Add or override a single `(proto, port) → label` entry.
229    pub fn set(&mut self, proto: L4Proto, port: u16, label: &'static str) -> &mut Self {
230        self.overrides.insert((proto, port), label);
231        self
232    }
233
234    /// Bulk-set from an iterator. Convenient for config-
235    /// driven table population.
236    pub fn extend<I>(&mut self, entries: I) -> &mut Self
237    where
238        I: IntoIterator<Item = (L4Proto, u16, &'static str)>,
239    {
240        for (proto, port, label) in entries {
241            self.overrides.insert((proto, port), label);
242        }
243        self
244    }
245
246    /// Lookup. Same shape as the free function
247    /// [`protocol_label`].
248    ///
249    /// Algorithm:
250    /// - Try the override map on `(proto, src_port)`.
251    /// - Try the override map on `(proto, dst_port)`.
252    /// - If [`inherit_builtin`](Self::inherit_builtin), fall
253    ///   back to the built-in [`protocol_label`].
254    /// - Else return `None`.
255    pub fn lookup(&self, proto: L4Proto, src_port: u16, dst_port: u16) -> Option<&'static str> {
256        if let Some(label) = self.overrides.get(&(proto, src_port)) {
257            return Some(*label);
258        }
259        if let Some(label) = self.overrides.get(&(proto, dst_port)) {
260            return Some(*label);
261        }
262        if self.inherit_builtin {
263            protocol_label(proto, src_port, dst_port)
264        } else {
265            None
266        }
267    }
268
269    /// `true` if this table falls back to the built-in
270    /// [`protocol_label`] dispatch when no override matches.
271    pub fn inherit_builtin(&self) -> bool {
272        self.inherit_builtin
273    }
274
275    /// Remove the override for `(proto, port)`. Returns the
276    /// previously-set label if any. After removal, [`Self::lookup`]
277    /// falls back to the built-in table if
278    /// [`Self::inherit_builtin`] is `true`, otherwise returns
279    /// `None`.
280    ///
281    /// Plan 172 (0.14).
282    pub fn remove(&mut self, proto: L4Proto, port: u16) -> Option<&'static str> {
283        self.overrides.remove(&(proto, port))
284    }
285
286    /// `true` if this table has an override for `(proto, port)`.
287    /// Does **not** consult the built-in fallback — use
288    /// [`Self::lookup`] for that.
289    ///
290    /// Plan 172 (0.14).
291    pub fn contains(&self, proto: L4Proto, port: u16) -> bool {
292        self.overrides.contains_key(&(proto, port))
293    }
294
295    /// Number of overrides currently registered. Independent
296    /// of [`Self::inherit_builtin`].
297    ///
298    /// Plan 172 (0.14) — replaces the removed `override_count`
299    /// method.
300    pub fn len(&self) -> usize {
301        self.overrides.len()
302    }
303
304    /// `true` if no overrides have been registered. Independent
305    /// of [`Self::inherit_builtin`] — a [`Self::new`] table is
306    /// "empty of overrides" but still resolves built-in labels
307    /// via [`Self::lookup`].
308    ///
309    /// Plan 172 (0.14).
310    pub fn is_empty(&self) -> bool {
311        self.overrides.is_empty()
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn tcp_sorted_ascending() {
321        for w in TCP_TABLE.windows(2) {
322            assert!(
323                w[0].0 < w[1].0,
324                "TCP_TABLE not sorted: {} >= {}",
325                w[0].0,
326                w[1].0
327            );
328        }
329    }
330
331    #[test]
332    fn udp_sorted_ascending() {
333        for w in UDP_TABLE.windows(2) {
334            assert!(
335                w[0].0 < w[1].0,
336                "UDP_TABLE not sorted: {} >= {}",
337                w[0].0,
338                w[1].0
339            );
340        }
341    }
342
343    #[test]
344    fn known_labels() {
345        assert_eq!(protocol_label(L4Proto::Tcp, 80, 33000), Some("http"));
346        assert_eq!(protocol_label(L4Proto::Tcp, 33000, 443), Some("tls/https"));
347        assert_eq!(protocol_label(L4Proto::Udp, 33000, 53), Some("dns"));
348        assert_eq!(protocol_label(L4Proto::Tcp, 33000, 6379), Some("redis"));
349    }
350
351    #[test]
352    fn lower_port_disambiguates_two_known() {
353        // 80 is lower than 443 → http wins.
354        assert_eq!(protocol_label(L4Proto::Tcp, 80, 443), Some("http"));
355        assert_eq!(protocol_label(L4Proto::Tcp, 443, 80), Some("http"));
356    }
357
358    #[test]
359    fn higher_port_fallback_when_lower_unknown() {
360        // 33000 (unknown) + 80 → label resolves via the higher port.
361        // We already covered this above; this case is the explicit
362        // "lower unknown" path.
363        assert_eq!(protocol_label(L4Proto::Tcp, 1024, 80), Some("http"));
364    }
365
366    #[test]
367    fn unknown_returns_none() {
368        assert_eq!(protocol_label(L4Proto::Tcp, 33000, 33001), None);
369        assert_eq!(protocol_label(L4Proto::Udp, 33000, 33001), None);
370        // Wrong proto on a known TCP port → None.
371        assert_eq!(protocol_label(L4Proto::Udp, 80, 33000), None);
372    }
373
374    #[test]
375    fn icmp_and_other_protocols_return_none() {
376        assert_eq!(protocol_label(L4Proto::Icmp, 0, 0), None);
377        assert_eq!(protocol_label(L4Proto::IcmpV6, 0, 0), None);
378        assert_eq!(protocol_label(L4Proto::Sctp, 80, 80), None);
379        assert_eq!(protocol_label(L4Proto::Other(99), 80, 80), None);
380    }
381
382    #[test]
383    fn zero_port_opts_out_of_that_side() {
384        // Only the non-zero side looks up.
385        assert_eq!(protocol_label(L4Proto::Tcp, 0, 80), Some("http"));
386        assert_eq!(protocol_label(L4Proto::Tcp, 80, 0), Some("http"));
387        // Both zero → None.
388        assert_eq!(protocol_label(L4Proto::Tcp, 0, 0), None);
389    }
390
391    #[test]
392    fn entries_iterates_full_table() {
393        let count = entries().count();
394        assert_eq!(count, TCP_TABLE.len() + UDP_TABLE.len());
395    }
396
397    #[test]
398    fn entries_contains_known_rows() {
399        let v: Vec<_> = entries().collect();
400        assert!(v.contains(&(L4Proto::Tcp, 80, "http")));
401        assert!(v.contains(&(L4Proto::Udp, 53, "dns")));
402        assert!(v.contains(&(L4Proto::Udp, 4789, "vxlan")));
403    }
404
405    // ── Plan 165 (0.14) — LabelTable tests ───────────────────
406
407    #[test]
408    fn label_table_new_starts_empty_inheriting_builtin() {
409        let t = LabelTable::new();
410        assert!(t.inherit_builtin());
411        assert_eq!(t.len(), 0);
412    }
413
414    #[test]
415    fn label_table_standalone_does_not_inherit() {
416        let t = LabelTable::standalone();
417        assert!(!t.inherit_builtin());
418    }
419
420    #[test]
421    fn label_table_lookup_uses_override_first() {
422        let mut t = LabelTable::new();
423        t.set(L4Proto::Tcp, 80, "internal-proxy");
424        // Override wins over the built-in "http".
425        assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("internal-proxy"));
426    }
427
428    #[test]
429    fn label_table_lookup_falls_back_to_builtin_when_inherit() {
430        let t = LabelTable::new();
431        // No overrides; built-in lookup applies.
432        assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("http"));
433    }
434
435    #[test]
436    fn label_table_standalone_returns_none_when_no_override() {
437        let t = LabelTable::standalone();
438        assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), None);
439    }
440
441    #[test]
442    fn label_table_extend_bulk_sets_entries() {
443        let mut t = LabelTable::new();
444        t.extend([
445            (L4Proto::Tcp, 8765, "grpc-internal"),
446            (L4Proto::Tcp, 9101, "metrics-scrape"),
447        ]);
448        assert_eq!(t.len(), 2);
449        assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), Some("grpc-internal"));
450        assert_eq!(t.lookup(L4Proto::Tcp, 9101, 0), Some("metrics-scrape"));
451    }
452
453    #[test]
454    fn label_table_set_overrides_existing_label() {
455        let mut t = LabelTable::new();
456        t.set(L4Proto::Tcp, 8765, "old");
457        t.set(L4Proto::Tcp, 8765, "new");
458        assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), Some("new"));
459        assert_eq!(t.len(), 1);
460    }
461
462    #[test]
463    fn label_table_lookup_tries_src_port_first_then_dst() {
464        let mut t = LabelTable::new();
465        t.set(L4Proto::Tcp, 8765, "src-side");
466        // src_port = 8765 matches first.
467        assert_eq!(t.lookup(L4Proto::Tcp, 8765, 9100), Some("src-side"));
468        // dst_port = 8765 also matches when src misses.
469        assert_eq!(t.lookup(L4Proto::Tcp, 33000, 8765), Some("src-side"));
470    }
471
472    #[test]
473    fn label_table_is_send_and_sync() {
474        fn assert_send_sync<T: Send + Sync>() {}
475        assert_send_sync::<LabelTable>();
476    }
477
478    // ── Plan 172 (0.14) — completeness sweep tests ───────────
479
480    #[test]
481    fn label_table_remove_returns_previous_label() {
482        let mut t = LabelTable::new();
483        t.set(L4Proto::Tcp, 8765, "grpc-internal");
484        assert_eq!(t.remove(L4Proto::Tcp, 8765), Some("grpc-internal"));
485    }
486
487    #[test]
488    fn label_table_remove_absent_returns_none() {
489        let mut t = LabelTable::new();
490        assert_eq!(t.remove(L4Proto::Tcp, 8765), None);
491    }
492
493    #[test]
494    fn label_table_remove_falls_back_to_builtin_when_inherit() {
495        let mut t = LabelTable::new();
496        t.set(L4Proto::Tcp, 80, "internal-proxy");
497        // Override shadows the built-in "http".
498        assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("internal-proxy"));
499        t.remove(L4Proto::Tcp, 80);
500        // After removal, falls back to built-in "http".
501        assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("http"));
502    }
503
504    #[test]
505    fn label_table_remove_standalone_returns_none_after() {
506        let mut t = LabelTable::standalone();
507        t.set(L4Proto::Tcp, 8765, "grpc-internal");
508        assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), Some("grpc-internal"));
509        t.remove(L4Proto::Tcp, 8765);
510        // Standalone — no builtin fallback.
511        assert_eq!(t.lookup(L4Proto::Tcp, 8765, 0), None);
512    }
513
514    #[test]
515    fn label_table_contains_reflects_set_remove() {
516        let mut t = LabelTable::new();
517        assert!(!t.contains(L4Proto::Tcp, 8765));
518        t.set(L4Proto::Tcp, 8765, "grpc-internal");
519        assert!(t.contains(L4Proto::Tcp, 8765));
520        t.remove(L4Proto::Tcp, 8765);
521        assert!(!t.contains(L4Proto::Tcp, 8765));
522    }
523
524    #[test]
525    fn label_table_contains_does_not_consult_builtin() {
526        // Port 80 is a built-in "http" entry — but the OVERRIDE
527        // table has nothing for it. contains() should reflect
528        // overrides only.
529        let t = LabelTable::new();
530        assert!(!t.contains(L4Proto::Tcp, 80));
531        // Confirm lookup still works:
532        assert_eq!(t.lookup(L4Proto::Tcp, 80, 33000), Some("http"));
533    }
534
535    #[test]
536    fn label_table_is_empty_on_new() {
537        assert!(LabelTable::new().is_empty());
538        assert!(LabelTable::standalone().is_empty());
539    }
540
541    #[test]
542    fn label_table_is_empty_after_set_then_remove() {
543        let mut t = LabelTable::new();
544        t.set(L4Proto::Tcp, 8765, "grpc");
545        assert!(!t.is_empty());
546        t.remove(L4Proto::Tcp, 8765);
547        assert!(t.is_empty());
548    }
549
550    // ── Plan 165 (0.14) — `FiveTupleKey` *_with companions ──
551    //
552    // Direct tests on the LabelTable lookup path are covered
553    // above; these exercise the FiveTupleKey wrappers
554    // (`protocol_label_with` / `app_label_with`) end-to-end.
555
556    fn key(src_port: u16, dst_port: u16) -> crate::extract::FiveTupleKey {
557        use std::net::{IpAddr, Ipv4Addr, SocketAddr};
558        crate::extract::FiveTupleKey {
559            proto: L4Proto::Tcp,
560            a: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port),
561            b: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), dst_port),
562        }
563    }
564
565    #[test]
566    fn protocol_label_with_override_wins_over_builtin() {
567        let mut t = LabelTable::new();
568        t.set(L4Proto::Tcp, 80, "internal-proxy");
569        // Port 80 hits the override before the built-in "http".
570        assert_eq!(
571            key(80, 33000).protocol_label_with(&t),
572            Some("internal-proxy"),
573        );
574    }
575
576    #[test]
577    fn protocol_label_with_falls_back_to_builtin_when_inheriting() {
578        let t = LabelTable::new(); // empty overrides, inherits built-in.
579        assert_eq!(key(80, 33000).protocol_label_with(&t), Some("http"));
580    }
581
582    #[test]
583    fn protocol_label_with_standalone_returns_none_for_unmapped() {
584        let t = LabelTable::standalone(); // strict whitelist.
585        assert_eq!(key(80, 33000).protocol_label_with(&t), None);
586    }
587
588    #[test]
589    fn app_label_with_falls_back_to_canonical_name() {
590        let t = LabelTable::standalone();
591        // Standalone + ephemeral ports → no L7 label → fall
592        // back to L4 canonical_name() = "tcp".
593        assert_eq!(key(33000, 33001).app_label_with(&t), "tcp");
594    }
595
596    #[test]
597    fn app_label_with_override_wins() {
598        let mut t = LabelTable::new();
599        t.set(L4Proto::Tcp, 8765, "grpc-internal");
600        assert_eq!(key(8765, 33000).app_label_with(&t), "grpc-internal");
601    }
602}