From 2386cd87f32acdef7720f3e1e3d34ac866f5677b Mon Sep 17 00:00:00 2001 From: Isaac Behrens Date: Thu, 25 Jun 2026 12:01:26 -0500 Subject: [PATCH 1/2] Connections: resolve LAN-side / private endpoints too (v0.2.1) Both source and destination were already passed through the resolver, but private LAN addresses have no public PTR record, so the LAN tab showed only raw IPs and WAN sources stayed numeric. - Resolver gains a static_names map (IP -> name), checked before PTR, like /etc/hosts; seeds the cache so local hosts display a name - Demo seeds friendly names for its synthetic LAN hosts and router so both endpoints resolve on both tabs - App passes the demo static names when building the resolver (both at startup and on the runtime 'r' toggle) - Bump version to 0.2.1; regenerate connections screenshot --- images/connections.svg | 258 +++++++++++++++++++------------------- pyproject.toml | 2 +- src/fwtop/app.py | 11 +- src/fwtop/resolve.py | 10 +- src/fwtop/sources/demo.py | 13 ++ uv.lock | 2 +- 6 files changed, 161 insertions(+), 135 deletions(-) diff --git a/images/connections.svg b/images/connections.svg index fca9cbf..de9a357 100644 --- a/images/connections.svg +++ b/images/connections.svg @@ -19,257 +19,257 @@ font-weight: 700; } - .terminal-1821002665-matrix { + .terminal-3744543391-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1821002665-title { + .terminal-3744543391-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1821002665-r1 { fill: #c5c8c6 } -.terminal-1821002665-r2 { fill: #50a0ff;font-weight: bold } -.terminal-1821002665-r3 { fill: #3a6aab;font-weight: bold } -.terminal-1821002665-r4 { fill: #0a0a1a } -.terminal-1821002665-r5 { fill: #606080 } -.terminal-1821002665-r6 { fill: #c0c0d0 } -.terminal-1821002665-r7 { fill: #555563 } -.terminal-1821002665-r8 { fill: #303050 } -.terminal-1821002665-r9 { fill: #50a0ff } -.terminal-1821002665-r10 { fill: #2d2d3f } -.terminal-1821002665-r11 { fill: #8080a0;font-weight: bold } -.terminal-1821002665-r12 { fill: #0e0e20 } -.terminal-1821002665-r13 { fill: #ffffff } -.terminal-1821002665-r14 { fill: #c080ff } -.terminal-1821002665-r15 { fill: #ffa050 } -.terminal-1821002665-r16 { fill: #50ffa0 } -.terminal-1821002665-r17 { fill: #404060 } -.terminal-1821002665-r18 { fill: #8080a0 } -.terminal-1821002665-r19 { fill: #000000 } -.terminal-1821002665-r20 { fill: #003054 } -.terminal-1821002665-r21 { fill: #ffa62b;font-weight: bold } -.terminal-1821002665-r22 { fill: #e0e0e0 } -.terminal-1821002665-r23 { fill: #414151 } + .terminal-3744543391-r1 { fill: #c5c8c6 } +.terminal-3744543391-r2 { fill: #50a0ff;font-weight: bold } +.terminal-3744543391-r3 { fill: #3a6aab;font-weight: bold } +.terminal-3744543391-r4 { fill: #0a0a1a } +.terminal-3744543391-r5 { fill: #606080 } +.terminal-3744543391-r6 { fill: #c0c0d0 } +.terminal-3744543391-r7 { fill: #555563 } +.terminal-3744543391-r8 { fill: #303050 } +.terminal-3744543391-r9 { fill: #50a0ff } +.terminal-3744543391-r10 { fill: #2d2d3f } +.terminal-3744543391-r11 { fill: #8080a0;font-weight: bold } +.terminal-3744543391-r12 { fill: #0e0e20 } +.terminal-3744543391-r13 { fill: #ffffff } +.terminal-3744543391-r14 { fill: #ffa050 } +.terminal-3744543391-r15 { fill: #c080ff } +.terminal-3744543391-r16 { fill: #404060 } +.terminal-3744543391-r17 { fill: #8080a0 } +.terminal-3744543391-r18 { fill: #50ffa0 } +.terminal-3744543391-r19 { fill: #000000 } +.terminal-3744543391-r20 { fill: #003054 } +.terminal-3744543391-r21 { fill: #ffa62b;font-weight: bold } +.terminal-3744543391-r22 { fill: #e0e0e0 } +.terminal-3744543391-r23 { fill: #414151 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - fwtop + fwtop - - - - fwtop v0.2.0 — 0.1s [DEMO DNS] -OverviewConnectionsFirewallDrops -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Connections ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -WAN (68)LAN (21) -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Proto   Source                                Destination                           NAT →                         State         Packets      Bytes      - TCP     192.168.1.42:1136                     198.51.100.23:22                      203.0.113.2                   FIN_WAIT      1,366        4.6 MB     -TCP192.168.1.42:4117 198.51.100.23:123                    203.0.113.2FIN_WAIT 837          4.6 MB     -UDP192.168.1.77:62255 198.51.100.23:22                     203.0.113.2 3,002        4.5 MB     -TCP192.168.1.42:12745 198.51.100.23:80                     203.0.113.2FIN_WAIT 739          4.5 MB     -TCP192.168.1.10:12194 dns.google:3478                      203.0.113.2ESTABLISHED 3,185        4.5 MB     -UDP192.168.1.10:48766 dns.google:51820                     203.0.113.2 4,665        4.5 MB     -TCP10.0.10.5:64244 lb-140-82-121-4-fra.github.com:123   203.0.113.2ESTABLISHED 1,013        4.4 MB     -ICMP192.168.1.77:19036 198.51.100.23:51820                  203.0.113.2 2,812        4.4 MB     -UDP10.0.10.5:11900 lb-140-82-121-4-fra.github.com:22    203.0.113.2 2,661        4.2 MB     -UDP192.168.1.10:6862 203.0.113.9:51820                    203.0.113.2 1,553        4.1 MB     -UDP192.168.1.77:7165 lb-140-82-121-4-fra.github.com:3478  203.0.113.2 688          4.0 MB     -TCP10.0.10.5:64270 dns.google:53                        203.0.113.2TIME_WAIT 767          4.0 MB     -TCP192.168.1.10:18912 dns.google:53                        203.0.113.2TIME_WAIT 4,308        3.9 MB     -UDP10.0.10.5:61956 one.one.one.one:80                   203.0.113.2 3,725        3.7 MB     -TCP192.168.1.77:41159 dns.google:51820                     203.0.113.2ESTABLISHED 363          3.7 MB     -TCP10.0.10.5:59987 198.51.100.23:443                    203.0.113.2ESTABLISHED 4,595        3.7 MB     -TCP10.0.10.5:27562 lb-140-82-121-4-fra.github.com:443   203.0.113.2ESTABLISHED 4,330        3.6 MB     -TCP192.168.1.10:56478 one.one.one.one:22                   203.0.113.2ESTABLISHED 144          3.5 MB     -ICMP10.0.10.5:14185 203.0.113.9:3478                     203.0.113.2 3,354        3.5 MB     -TCP192.168.1.10:32343 one.one.one.one:3478                 203.0.113.2TIME_WAIT 4,220        3.5 MB     -TCP192.168.1.42:56072 203.0.113.9:51820                    203.0.113.2ESTABLISHED 2,672        3.4 MB     -TCP192.168.1.77:24425 dns.google:443                       203.0.113.2SYN_SENT 4,016        3.4 MB     -TCP10.0.10.5:46586 198.51.100.23:443                    203.0.113.2ESTABLISHED 1,282        3.2 MB     -TCP192.168.1.42:44853 dns.google:53                        203.0.113.2TIME_WAIT 2,670        3.1 MB    ▅▅ -TCP192.168.1.10:6866 one.one.one.one:123                  203.0.113.2ESTABLISHED 3,097        3.1 MB     -UDP10.0.10.5:3616 198.51.100.23:443                    203.0.113.2 2,265        3.1 MB     -UDP192.168.1.10:19703 198.51.100.23:80                     203.0.113.2 2,056        3.0 MB     -UDP10.0.10.5:47140 dns.google:123                       203.0.113.2 3,287        2.9 MB     -TCP192.168.1.77:29911 203.0.113.9:443                      203.0.113.2ESTABLISHED 601          2.8 MB     -TCP192.168.1.77:53967 lb-140-82-121-4-fra.github.com:3478  203.0.113.2ESTABLISHED 4,586        2.7 MB     -UDP10.0.10.5:20854 lb-140-82-121-4-fra.github.com:3478  203.0.113.2 2,842        2.5 MB     -TCP192.168.1.10:43208 one.one.one.one:51820                203.0.113.2ESTABLISHED 42           2.5 MB     -ICMP192.168.1.42:38153 203.0.113.9:123                      203.0.113.2 1,820        2.5 MB     -UDP10.0.10.5:27822 198.51.100.23:3478                   203.0.113.2 3,844        2.5 MB     -TCP192.168.1.10:42871 lb-140-82-121-4-fra.github.com:123   203.0.113.2SYN_SENT 4,852        2.4 MB     -UDP192.168.1.10:3219 198.51.100.23:3478                   203.0.113.2 3,526        2.4 MB     -ICMP192.168.1.42:63955 203.0.113.9:80                       203.0.113.2 1,059        2.3 MB     -TCP10.0.10.5:27154 lb-140-82-121-4-fra.github.com:51820 203.0.113.2ESTABLISHED 4,333        2.2 MB     -TCP192.168.1.10:32453 203.0.113.9:51820                    203.0.113.2CLOSE_WAIT 1,137        2.1 MB     -UDP192.168.1.42:33726 one.one.one.one:3478                 203.0.113.2 3,850        2.1 MB     - -────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Quit  p Pause  r Resolve  n Interval  z Zones  1 Overview  2 Connections  3 Firewall  4 Drops  w WAN conns  l LAN conns ^p palette▇▇ + + + + fwtop v0.2.1 — 0.2s [DEMO DNS] +OverviewConnectionsFirewallDrops +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Connections ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +WAN (58)LAN (31) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Proto   Source                                Destination                           NAT →                         State         Packets      Bytes      + TCP     desktop.lan:63454                     vpn-peer.example.net:51820            router.wan                    CLOSE_WAIT    3,897        4.6 MB     +ICMPlaptop.lan:3330 vpn-peer.example.net:123             router.wan 4,851        4.6 MB     +TCPnas.lan:3692 one.one.one.one:53                   router.wanSYN_SENT 35           4.6 MB     +UDPiot-hub.lan:34581 one.one.one.one:22                   router.wan 647          4.5 MB     +TCPdesktop.lan:44844 dns.google:123                       router.wanESTABLISHED 1,951        4.5 MB     +TCPiot-hub.lan:49947 one.one.one.one:80                   router.wanESTABLISHED 4,431        4.4 MB     +TCPlaptop.lan:7573 dns.google:80                        router.wanESTABLISHED 2,145        4.0 MB     +TCPnas.lan:52565 vpn-peer.example.net:123             router.wanFIN_WAIT 2,071        3.9 MB     +UDPlaptop.lan:38360 one.one.one.one:80                   router.wan 3,886        3.9 MB     +TCPdesktop.lan:46694 mail.example.org:53                  router.wanSYN_SENT 3,431        3.8 MB     +TCPnas.lan:49164 dns.google:53                        router.wanTIME_WAIT 3,493        3.8 MB     +TCPlaptop.lan:63398 vpn-peer.example.net:51820           router.wanESTABLISHED 31           3.6 MB     +UDPnas.lan:56643 lb-140-82-121-4-fra.github.com:53    router.wan 2,399        3.5 MB     +TCPnas.lan:58343 lb-140-82-121-4-fra.github.com:123   router.wanSYN_SENT 3,320        3.5 MB     +TCPlaptop.lan:22308 vpn-peer.example.net:53              router.wanESTABLISHED 4,718        3.4 MB     +TCPnas.lan:58144 mail.example.org:51820               router.wanTIME_WAIT 1,299        3.4 MB     +TCPnas.lan:48301 mail.example.org:51820               router.wanESTABLISHED 4,942        3.4 MB     +ICMPnas.lan:34332 one.one.one.one:53                   router.wan 76           3.4 MB     +UDPlaptop.lan:57778 mail.example.org:51820               router.wan 1,468        3.3 MB     +TCPlaptop.lan:21527 mail.example.org:443                 router.wanESTABLISHED 455          2.9 MB     +UDPdesktop.lan:26790 lb-140-82-121-4-fra.github.com:123   router.wan 1,695        2.9 MB     +TCPnas.lan:36588 mail.example.org:3478                router.wanCLOSE_WAIT 4,536        2.9 MB     +UDPlaptop.lan:24864 mail.example.org:80                  router.wan 3,524        2.8 MB     +TCPnas.lan:21464 vpn-peer.example.net:123             router.wanESTABLISHED 2,000        2.7 MB     +UDPnas.lan:28898 one.one.one.one:22                   router.wan 3,217        2.6 MB     +TCPnas.lan:47479 lb-140-82-121-4-fra.github.com:80    router.wanFIN_WAIT 2,982        2.5 MB     +UDPdesktop.lan:62620 lb-140-82-121-4-fra.github.com:53    router.wan 2,593        2.5 MB     +TCPlaptop.lan:7096 one.one.one.one:123                  router.wanESTABLISHED 3,828        2.2 MB    ▄▄ +TCPnas.lan:24998 mail.example.org:123                 router.wanFIN_WAIT 3,145        2.1 MB     +UDPdesktop.lan:35575 lb-140-82-121-4-fra.github.com:51820 router.wan 2,844        2.1 MB     +UDPiot-hub.lan:52023 lb-140-82-121-4-fra.github.com:443   router.wan 2,694        2.1 MB     +TCPiot-hub.lan:5859 mail.example.org:3478                router.wanFIN_WAIT 2,594        2.1 MB     +TCPiot-hub.lan:56455 one.one.one.one:123                  router.wanFIN_WAIT 2,384        2.0 MB     +UDPnas.lan:21841 one.one.one.one:123                  router.wan 3,788        2.0 MB     +UDPnas.lan:19566 dns.google:51820                     router.wan 694          1.9 MB     +TCPdesktop.lan:58048 dns.google:22                        router.wanFIN_WAIT 2,775        1.9 MB     +UDPlaptop.lan:2196 mail.example.org:443                 router.wan 1,000        1.9 MB     +UDPnas.lan:49594 vpn-peer.example.net:53              router.wan 1,937        1.9 MB     +TCPlaptop.lan:8206 vpn-peer.example.net:123             router.wanESTABLISHED 3,270        1.9 MB     +TCPdesktop.lan:60663 mail.example.org:80                  router.wanESTABLISHED 78           1.8 MB     + +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Quit  p Pause  r Resolve  n Interval  z Zones  1 Overview  2 Connections  3 Firewall  4 Drops  w WAN conns  l LAN conns ^p palette▇▇ diff --git a/pyproject.toml b/pyproject.toml index ae7f1b3..6be66a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fwtop" -version = "0.2.0" +version = "0.2.1" description = "Real-time router/firewall traffic visualizer TUI for Linux (interfaces, conntrack, firewall drops)" readme = "README.md" authors = [ diff --git a/src/fwtop/app.py b/src/fwtop/app.py index 4178462..1b72ae3 100644 --- a/src/fwtop/app.py +++ b/src/fwtop/app.py @@ -12,6 +12,7 @@ from fwtop.collector import Collector from fwtop.config import ZONE_CYCLE, Config from fwtop.resolve import Resolver +from fwtop.sources.demo import DEMO_STATIC_NAMES from fwtop.widgets import ( ConnectionsTable, ConntrackPanel, @@ -197,9 +198,15 @@ def compose(self) -> ComposeResult: yield Label("", id="status-label", markup=False) yield Footer() + def _make_resolver(self) -> Resolver: + # In demo mode seed the resolver with the synthetic hosts' names, since + # the private LAN addresses have no real PTR records to look up. + static = DEMO_STATIC_NAMES if self.collector.demo else None + return Resolver(static_names=static) + def on_mount(self) -> None: if self.resolve_enabled: - self.resolver = Resolver() + self.resolver = self._make_resolver() self._start_time = monotonic() self._update_subtitle() @@ -306,7 +313,7 @@ def action_toggle_pause(self) -> None: def action_toggle_resolve(self) -> None: self.resolve_enabled = not self.resolve_enabled if self.resolve_enabled and self.resolver is None: - self.resolver = Resolver() + self.resolver = self._make_resolver() self._update_subtitle() def action_change_interval(self) -> None: diff --git a/src/fwtop/resolve.py b/src/fwtop/resolve.py index 47be60a..92bab15 100644 --- a/src/fwtop/resolve.py +++ b/src/fwtop/resolve.py @@ -12,10 +12,16 @@ class Resolver: otherwise the IP itself while scheduling a lookup in the background so a later call can show the name. Lookups never block the caller — the UI keeps showing the raw IP until the PTR record (if any) comes back. + + A ``static_names`` map (IP -> name) is consulted before any PTR lookup, + like ``/etc/hosts``. This lets local hosts that have no reverse-DNS record + (typical of LAN addresses) still display a name, so both source and + destination resolve on the WAN and LAN views. """ - def __init__(self, max_workers: int = 4) -> None: - self._cache: dict[str, str] = {} # ip -> hostname (or ip if no PTR) + def __init__(self, max_workers: int = 4, static_names: dict[str, str] | None = None) -> None: + # Static names count as already-resolved, so seed the cache with them. + self._cache: dict[str, str] = dict(static_names or {}) # ip -> hostname self._pending: set[str] = set() self._lock = threading.Lock() self._queue: queue.Queue[str] = queue.Queue() diff --git a/src/fwtop/sources/demo.py b/src/fwtop/sources/demo.py index 52e3087..f75aa63 100644 --- a/src/fwtop/sources/demo.py +++ b/src/fwtop/sources/demo.py @@ -20,6 +20,19 @@ _WAN_HOSTS = ("203.0.113.9", "198.51.100.23", "8.8.8.8", "1.1.1.1", "140.82.121.4") _WAN_IP = "203.0.113.2" # the router's public address (NAT egress) +# Friendly names for the synthetic hosts, mimicking a router's local DNS / +# DHCP lease table. Private LAN addresses have no public PTR record, so +# without these the demo could never show source/LAN-side resolution. +DEMO_STATIC_NAMES = { + "192.168.1.10": "desktop.lan", + "192.168.1.42": "laptop.lan", + "192.168.1.77": "nas.lan", + "10.0.10.5": "iot-hub.lan", + "203.0.113.2": "router.wan", + "203.0.113.9": "vpn-peer.example.net", + "198.51.100.23": "mail.example.org", +} + class DemoSource: """Synthetic data source producing plausible, time-varying router metrics. diff --git a/uv.lock b/uv.lock index bd35982..f41091a 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "fwtop" -version = "0.2.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "psutil" }, From 5c0a3855bdfab72b9faad99b3f73b72611417c0e Mon Sep 17 00:00:00 2001 From: Isaac Behrens Date: Thu, 25 Jun 2026 12:06:21 -0500 Subject: [PATCH 2/2] Resolve all IPs via DNS + local cache; drop static names map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the static_names seeding (wrong model) with a pure reverse-DNS resolver: every address, local or public, is looked up through the system resolver and the result cached in the program-local map. On a router whose local DNS serves PTR for DHCP leases, private addresses resolve through DNS like any other. - Resolver takes an injectable 'lookup' callable (defaults to the real system PTR lookup); no static table - Demo provides demo_ptr_lookup() as a fake DNS backend for its synthetic addresses, exercising the same cache/queue path - App injects the demo backend only in --demo mode The static map never touched /etc/hosts (it was in-memory), but seeding names is the wrong approach regardless — DNS is the source of truth. --- images/connections.svg | 258 +++++++++++++++++++------------------- src/fwtop/app.py | 12 +- src/fwtop/resolve.py | 46 ++++--- src/fwtop/sources/demo.py | 17 ++- 4 files changed, 181 insertions(+), 152 deletions(-) diff --git a/images/connections.svg b/images/connections.svg index de9a357..b24f23e 100644 --- a/images/connections.svg +++ b/images/connections.svg @@ -19,257 +19,257 @@ font-weight: 700; } - .terminal-3744543391-matrix { + .terminal-3562616864-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3744543391-title { + .terminal-3562616864-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3744543391-r1 { fill: #c5c8c6 } -.terminal-3744543391-r2 { fill: #50a0ff;font-weight: bold } -.terminal-3744543391-r3 { fill: #3a6aab;font-weight: bold } -.terminal-3744543391-r4 { fill: #0a0a1a } -.terminal-3744543391-r5 { fill: #606080 } -.terminal-3744543391-r6 { fill: #c0c0d0 } -.terminal-3744543391-r7 { fill: #555563 } -.terminal-3744543391-r8 { fill: #303050 } -.terminal-3744543391-r9 { fill: #50a0ff } -.terminal-3744543391-r10 { fill: #2d2d3f } -.terminal-3744543391-r11 { fill: #8080a0;font-weight: bold } -.terminal-3744543391-r12 { fill: #0e0e20 } -.terminal-3744543391-r13 { fill: #ffffff } -.terminal-3744543391-r14 { fill: #ffa050 } -.terminal-3744543391-r15 { fill: #c080ff } -.terminal-3744543391-r16 { fill: #404060 } -.terminal-3744543391-r17 { fill: #8080a0 } -.terminal-3744543391-r18 { fill: #50ffa0 } -.terminal-3744543391-r19 { fill: #000000 } -.terminal-3744543391-r20 { fill: #003054 } -.terminal-3744543391-r21 { fill: #ffa62b;font-weight: bold } -.terminal-3744543391-r22 { fill: #e0e0e0 } -.terminal-3744543391-r23 { fill: #414151 } + .terminal-3562616864-r1 { fill: #c5c8c6 } +.terminal-3562616864-r2 { fill: #50a0ff;font-weight: bold } +.terminal-3562616864-r3 { fill: #3a6aab;font-weight: bold } +.terminal-3562616864-r4 { fill: #0a0a1a } +.terminal-3562616864-r5 { fill: #606080 } +.terminal-3562616864-r6 { fill: #c0c0d0 } +.terminal-3562616864-r7 { fill: #555563 } +.terminal-3562616864-r8 { fill: #303050 } +.terminal-3562616864-r9 { fill: #50a0ff } +.terminal-3562616864-r10 { fill: #2d2d3f } +.terminal-3562616864-r11 { fill: #8080a0;font-weight: bold } +.terminal-3562616864-r12 { fill: #0e0e20 } +.terminal-3562616864-r13 { fill: #ffffff } +.terminal-3562616864-r14 { fill: #c080ff } +.terminal-3562616864-r15 { fill: #ffa050 } +.terminal-3562616864-r16 { fill: #50ffa0 } +.terminal-3562616864-r17 { fill: #404060 } +.terminal-3562616864-r18 { fill: #8080a0 } +.terminal-3562616864-r19 { fill: #000000 } +.terminal-3562616864-r20 { fill: #003054 } +.terminal-3562616864-r21 { fill: #ffa62b;font-weight: bold } +.terminal-3562616864-r22 { fill: #e0e0e0 } +.terminal-3562616864-r23 { fill: #414151 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - fwtop + fwtop - - - - fwtop v0.2.1 — 0.2s [DEMO DNS] -OverviewConnectionsFirewallDrops -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Connections ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -WAN (58)LAN (31) -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Proto   Source                                Destination                           NAT →                         State         Packets      Bytes      - TCP     desktop.lan:63454                     vpn-peer.example.net:51820            router.wan                    CLOSE_WAIT    3,897        4.6 MB     -ICMPlaptop.lan:3330 vpn-peer.example.net:123             router.wan 4,851        4.6 MB     -TCPnas.lan:3692 one.one.one.one:53                   router.wanSYN_SENT 35           4.6 MB     -UDPiot-hub.lan:34581 one.one.one.one:22                   router.wan 647          4.5 MB     -TCPdesktop.lan:44844 dns.google:123                       router.wanESTABLISHED 1,951        4.5 MB     -TCPiot-hub.lan:49947 one.one.one.one:80                   router.wanESTABLISHED 4,431        4.4 MB     -TCPlaptop.lan:7573 dns.google:80                        router.wanESTABLISHED 2,145        4.0 MB     -TCPnas.lan:52565 vpn-peer.example.net:123             router.wanFIN_WAIT 2,071        3.9 MB     -UDPlaptop.lan:38360 one.one.one.one:80                   router.wan 3,886        3.9 MB     -TCPdesktop.lan:46694 mail.example.org:53                  router.wanSYN_SENT 3,431        3.8 MB     -TCPnas.lan:49164 dns.google:53                        router.wanTIME_WAIT 3,493        3.8 MB     -TCPlaptop.lan:63398 vpn-peer.example.net:51820           router.wanESTABLISHED 31           3.6 MB     -UDPnas.lan:56643 lb-140-82-121-4-fra.github.com:53    router.wan 2,399        3.5 MB     -TCPnas.lan:58343 lb-140-82-121-4-fra.github.com:123   router.wanSYN_SENT 3,320        3.5 MB     -TCPlaptop.lan:22308 vpn-peer.example.net:53              router.wanESTABLISHED 4,718        3.4 MB     -TCPnas.lan:58144 mail.example.org:51820               router.wanTIME_WAIT 1,299        3.4 MB     -TCPnas.lan:48301 mail.example.org:51820               router.wanESTABLISHED 4,942        3.4 MB     -ICMPnas.lan:34332 one.one.one.one:53                   router.wan 76           3.4 MB     -UDPlaptop.lan:57778 mail.example.org:51820               router.wan 1,468        3.3 MB     -TCPlaptop.lan:21527 mail.example.org:443                 router.wanESTABLISHED 455          2.9 MB     -UDPdesktop.lan:26790 lb-140-82-121-4-fra.github.com:123   router.wan 1,695        2.9 MB     -TCPnas.lan:36588 mail.example.org:3478                router.wanCLOSE_WAIT 4,536        2.9 MB     -UDPlaptop.lan:24864 mail.example.org:80                  router.wan 3,524        2.8 MB     -TCPnas.lan:21464 vpn-peer.example.net:123             router.wanESTABLISHED 2,000        2.7 MB     -UDPnas.lan:28898 one.one.one.one:22                   router.wan 3,217        2.6 MB     -TCPnas.lan:47479 lb-140-82-121-4-fra.github.com:80    router.wanFIN_WAIT 2,982        2.5 MB     -UDPdesktop.lan:62620 lb-140-82-121-4-fra.github.com:53    router.wan 2,593        2.5 MB     -TCPlaptop.lan:7096 one.one.one.one:123                  router.wanESTABLISHED 3,828        2.2 MB    ▄▄ -TCPnas.lan:24998 mail.example.org:123                 router.wanFIN_WAIT 3,145        2.1 MB     -UDPdesktop.lan:35575 lb-140-82-121-4-fra.github.com:51820 router.wan 2,844        2.1 MB     -UDPiot-hub.lan:52023 lb-140-82-121-4-fra.github.com:443   router.wan 2,694        2.1 MB     -TCPiot-hub.lan:5859 mail.example.org:3478                router.wanFIN_WAIT 2,594        2.1 MB     -TCPiot-hub.lan:56455 one.one.one.one:123                  router.wanFIN_WAIT 2,384        2.0 MB     -UDPnas.lan:21841 one.one.one.one:123                  router.wan 3,788        2.0 MB     -UDPnas.lan:19566 dns.google:51820                     router.wan 694          1.9 MB     -TCPdesktop.lan:58048 dns.google:22                        router.wanFIN_WAIT 2,775        1.9 MB     -UDPlaptop.lan:2196 mail.example.org:443                 router.wan 1,000        1.9 MB     -UDPnas.lan:49594 vpn-peer.example.net:53              router.wan 1,937        1.9 MB     -TCPlaptop.lan:8206 vpn-peer.example.net:123             router.wanESTABLISHED 3,270        1.9 MB     -TCPdesktop.lan:60663 mail.example.org:80                  router.wanESTABLISHED 78           1.8 MB     - -────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Quit  p Pause  r Resolve  n Interval  z Zones  1 Overview  2 Connections  3 Firewall  4 Drops  w WAN conns  l LAN conns ^p palette▇▇ + + + + fwtop v0.2.1 — 0.2s [DEMO DNS] +OverviewConnectionsFirewallDrops +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Connections ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +WAN (63)LAN (23) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Proto   Source                                Destination                           NAT →                         State         Packets      Bytes      + TCP     desktop.lan:45487                     dns.google:443                        router.wan                    ESTABLISHED   537          4.7 MB     +TCPnas.lan:55367 one.one.one.one:80                   router.wanFIN_WAIT 4,122        4.3 MB     +UDPdesktop.lan:9066 mail.example.org:3478                router.wan 4,611        4.1 MB     +TCPdesktop.lan:17644 vpn-peer.example.net:53              router.wanESTABLISHED 802          4.0 MB     +TCPdesktop.lan:32305 vpn-peer.example.net:51820           router.wanFIN_WAIT 706          4.0 MB     +ICMPlaptop.lan:63369 dns.google:51820                     router.wan 566          3.9 MB     +UDPdesktop.lan:43240 dns.google:3478                      router.wan 3,592        3.9 MB     +UDPlaptop.lan:52865 mail.example.org:22                  router.wan 543          3.9 MB     +TCPiot-hub.lan:1094 lb-140-82-121-4-fra.github.com:53    router.wanFIN_WAIT 1,812        3.9 MB     +ICMPdesktop.lan:32282 dns.google:80                        router.wan 863          3.8 MB     +TCPlaptop.lan:6831 vpn-peer.example.net:443             router.wanESTABLISHED 4,262        3.7 MB     +UDPiot-hub.lan:64474 one.one.one.one:80                   router.wan 1,765        3.7 MB     +UDPlaptop.lan:46100 vpn-peer.example.net:22              router.wan 2,354        3.6 MB     +TCPlaptop.lan:49347 mail.example.org:443                 router.wanTIME_WAIT 2,254        3.6 MB     +UDPdesktop.lan:26144 one.one.one.one:80                   router.wan 4,315        3.6 MB     +TCPlaptop.lan:21568 lb-140-82-121-4-fra.github.com:53    router.wanESTABLISHED 4,637        3.5 MB     +UDPlaptop.lan:28056 vpn-peer.example.net:51820           router.wan 3,973        3.4 MB     +TCPnas.lan:23963 vpn-peer.example.net:443             router.wanESTABLISHED 1,595        3.4 MB     +UDPnas.lan:27177 vpn-peer.example.net:53              router.wan 1,291        3.3 MB     +UDPiot-hub.lan:58488 one.one.one.one:3478                 router.wan 1,119        3.3 MB     +UDPlaptop.lan:45016 lb-140-82-121-4-fra.github.com:53    router.wan 3,542        3.3 MB     +TCPdesktop.lan:58538 vpn-peer.example.net:123             router.wanESTABLISHED 187          3.2 MB     +UDPdesktop.lan:31406 vpn-peer.example.net:123             router.wan 4,600        3.2 MB     +TCPnas.lan:7560 mail.example.org:3478                router.wanSYN_SENT 2,556        3.0 MB     +UDPlaptop.lan:50807 one.one.one.one:443                  router.wan 1,405        3.0 MB     +TCPnas.lan:3769 mail.example.org:51820               router.wanESTABLISHED 41           2.7 MB    ▅▅ +UDPlaptop.lan:39692 vpn-peer.example.net:3478            router.wan 1,119        2.4 MB     +TCPdesktop.lan:57156 lb-140-82-121-4-fra.github.com:22    router.wanTIME_WAIT 4,667        2.3 MB     +UDPnas.lan:12499 dns.google:53                        router.wan 4,848        2.1 MB     +TCPnas.lan:57695 vpn-peer.example.net:22              router.wanESTABLISHED 3,967        2.1 MB     +UDPiot-hub.lan:42190 one.one.one.one:443                  router.wan 2,687        2.0 MB     +TCPlaptop.lan:12965 vpn-peer.example.net:51820           router.wanSYN_SENT 340          1.9 MB     +TCPnas.lan:23106 mail.example.org:53                  router.wanESTABLISHED 555          1.9 MB     +TCPlaptop.lan:22924 dns.google:53                        router.wanESTABLISHED 4,963        1.8 MB     +TCPlaptop.lan:53086 vpn-peer.example.net:80              router.wanFIN_WAIT 1,045        1.7 MB     +ICMPdesktop.lan:57626 lb-140-82-121-4-fra.github.com:3478  router.wan 366          1.6 MB     +ICMPdesktop.lan:38607 vpn-peer.example.net:22              router.wan 1,848        1.6 MB     +TCPdesktop.lan:38437 one.one.one.one:123                  router.wanESTABLISHED 3,412        1.6 MB     +ICMPdesktop.lan:11593 mail.example.org:80                  router.wan 1,750        1.5 MB     +TCPnas.lan:29268 lb-140-82-121-4-fra.github.com:51820 router.wanESTABLISHED 2,956        1.4 MB     + +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Quit  p Pause  r Resolve  n Interval  z Zones  1 Overview  2 Connections  3 Firewall  4 Drops  w WAN conns  l LAN conns ^p palette▇▇ diff --git a/src/fwtop/app.py b/src/fwtop/app.py index 1b72ae3..9a0d6b7 100644 --- a/src/fwtop/app.py +++ b/src/fwtop/app.py @@ -12,7 +12,7 @@ from fwtop.collector import Collector from fwtop.config import ZONE_CYCLE, Config from fwtop.resolve import Resolver -from fwtop.sources.demo import DEMO_STATIC_NAMES +from fwtop.sources.demo import demo_ptr_lookup from fwtop.widgets import ( ConnectionsTable, ConntrackPanel, @@ -199,10 +199,12 @@ def compose(self) -> ComposeResult: yield Footer() def _make_resolver(self) -> Resolver: - # In demo mode seed the resolver with the synthetic hosts' names, since - # the private LAN addresses have no real PTR records to look up. - static = DEMO_STATIC_NAMES if self.collector.demo else None - return Resolver(static_names=static) + # Production resolves every address (local or public) through the + # system DNS resolver. In demo mode the synthetic addresses don't + # exist, so swap in a fake DNS backend that returns their PTR records; + # the cache/queue path is identical either way. + lookup = demo_ptr_lookup if self.collector.demo else None + return Resolver(lookup=lookup) def on_mount(self) -> None: if self.resolve_enabled: diff --git a/src/fwtop/resolve.py b/src/fwtop/resolve.py index 92bab15..8e3cc40 100644 --- a/src/fwtop/resolve.py +++ b/src/fwtop/resolve.py @@ -3,26 +3,47 @@ import queue import socket import threading +from typing import Callable, Optional + + +def _ptr_lookup(ip: str) -> str: + """Resolve ``ip`` to a hostname via the system resolver (reverse DNS). + + Returns the IP unchanged when there is no PTR record or the lookup fails. + This goes through the OS resolver, so on a router whose local DNS serves + PTR records for its DHCP leases, private addresses resolve here too — no + hosts file or static map required. + """ + try: + return socket.gethostbyaddr(ip)[0] + except Exception: + return ip class Resolver: - """Background reverse-DNS resolver with a cache. + """Background reverse-DNS resolver with a program-local cache. - ``display(ip)`` returns the resolved hostname if it is already known, + ``display(ip)`` returns the resolved hostname if it is already cached, otherwise the IP itself while scheduling a lookup in the background so a later call can show the name. Lookups never block the caller — the UI keeps - showing the raw IP until the PTR record (if any) comes back. + showing the raw IP until the PTR record (if any) comes back. Every result + (including "no PTR -> the IP itself") is cached in an in-memory map, so each + address is looked up at most once. - A ``static_names`` map (IP -> name) is consulted before any PTR lookup, - like ``/etc/hosts``. This lets local hosts that have no reverse-DNS record - (typical of LAN addresses) still display a name, so both source and - destination resolve on the WAN and LAN views. + All resolution goes through DNS via ``lookup`` (the system resolver by + default). The hook exists only so tests and the synthetic ``--demo`` mode + can stand in a fake DNS backend for addresses that don't exist for real; it + is not a static names table. """ - def __init__(self, max_workers: int = 4, static_names: dict[str, str] | None = None) -> None: - # Static names count as already-resolved, so seed the cache with them. - self._cache: dict[str, str] = dict(static_names or {}) # ip -> hostname + def __init__( + self, + max_workers: int = 4, + lookup: Optional[Callable[[str], str]] = None, + ) -> None: + self._cache: dict[str, str] = {} # ip -> hostname (or ip if no PTR) self._pending: set[str] = set() + self._lookup = lookup or _ptr_lookup self._lock = threading.Lock() self._queue: queue.Queue[str] = queue.Queue() self._stop = threading.Event() @@ -51,10 +72,7 @@ def _worker(self) -> None: ip = self._queue.get(timeout=0.5) except queue.Empty: continue - try: - host = socket.gethostbyaddr(ip)[0] - except Exception: - host = ip # no PTR record or lookup failed -> fall back to IP + host = self._lookup(ip) with self._lock: self._cache[ip] = host self._pending.discard(ip) diff --git a/src/fwtop/sources/demo.py b/src/fwtop/sources/demo.py index f75aa63..f74dbb1 100644 --- a/src/fwtop/sources/demo.py +++ b/src/fwtop/sources/demo.py @@ -20,10 +20,11 @@ _WAN_HOSTS = ("203.0.113.9", "198.51.100.23", "8.8.8.8", "1.1.1.1", "140.82.121.4") _WAN_IP = "203.0.113.2" # the router's public address (NAT egress) -# Friendly names for the synthetic hosts, mimicking a router's local DNS / -# DHCP lease table. Private LAN addresses have no public PTR record, so -# without these the demo could never show source/LAN-side resolution. -DEMO_STATIC_NAMES = { +# PTR answers a router's local DNS would return for the synthetic hosts. The +# demo's addresses are fictional (TEST-NET / RFC1918), so a real reverse-DNS +# lookup can't resolve them; demo_ptr_lookup() stands in as that DNS backend +# so the resolver still exercises its real cache/lookup path. +_DEMO_PTR = { "192.168.1.10": "desktop.lan", "192.168.1.42": "laptop.lan", "192.168.1.77": "nas.lan", @@ -31,9 +32,17 @@ "203.0.113.2": "router.wan", "203.0.113.9": "vpn-peer.example.net", "198.51.100.23": "mail.example.org", + "8.8.8.8": "dns.google", + "1.1.1.1": "one.one.one.one", + "140.82.121.4": "lb-140-82-121-4-fra.github.com", } +def demo_ptr_lookup(ip: str) -> str: + """Stand-in DNS backend for the demo: return the simulated PTR or the IP.""" + return _DEMO_PTR.get(ip, ip) + + class DemoSource: """Synthetic data source producing plausible, time-varying router metrics.