Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol
capture, and a refused START says what to do. The Statistics page and its CSV count
packets that went around the tool and those the driver dropped, with a red line while
either grows. Its files stay yours, and GitHub and Donate print the address instead of
starting a browser as root. On the Tools tab, TCP health says it is Windows-only for now,
and the row menus grey out Show TCP health.
starting a browser as root.

- **Linux, experimental: Diagnostics speaks of Linux.** Its help explains what stops a
session there - capture rights, the kernel, BTF and pydivert - and what to do, and
Expand Down Expand Up @@ -48,6 +47,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol
and the `environment.d` folders, also under sudo as yours. **Add network adapters** and
**Add proxy settings** in Diagnostics work too.

- **Linux, experimental: TCP health works, without sudo.** It shows what the kernel keeps for a
connection, as `ss -ti` does, also for other users' programs, and **Show TCP health** works
in the row menus. Linux keeps no newest or highest round trip and counts no fast resends or
duplicate ACKs, so those rows are not there. It does count the segments sent again, in a
row of its own. The lowest round trip is the lowest of the last 5 minutes, and timeouts are
counted from Linux 6.7 on.

- **Ping on the Tools tab, to see the session's latency and loss arrive.** Type an address or a
name and press **Start**: each ping is a row with its round trip and what came back, the last
60 are charted, and the totals say what was lost. The ping goes on while another tab is in
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,13 @@ exit code 1, as on Windows without administrator rights. Where Linux differs fro
- IP fragments always pass through the tool, even those its filter does not match.
- In the window, the files it saves (window state, profiles, CSV) stay yours under `sudo`, and
GitHub and Donate print their address instead of starting a browser as root.
- On the Tools tab, Ping, Sockets, Port check, Filter tester and Diagnostics work. Ping works
without `sudo` where the system lets your group send pings (the setting
- On the Tools tab, Ping, Sockets, Port check, Filter tester, TCP health and Diagnostics work.
Ping works without `sudo` where the system lets your group send pings (the setting
`net.ipv4.ping_group_range`: Ubuntu 26.04 allows it, 24.04 does not). With `sudo` it sends
through a raw socket, which needs the capability CAP_NET_RAW: root has it unless the system
takes it away, as some containers do. A ping Linux refuses shows its own word for it:
`EPERM` usually means a firewall rule on this computer.
A zone of an IPv6 address can be the interface's name (`fe80::1%eth0`). TCP health says that
it works on Windows only for now, and so do the row menu entries that open it. The
A zone of an IPv6 address can be the interface's name (`fe80::1%eth0`). The
Diagnostics help explains the Linux checks: capture rights, the kernel, BTF and pydivert.
- **"Reset this connection"** on the Connections page works on Linux for TCP over IPv4 and IPv6,
as root or with the capability CAP_NET_ADMIN. The program using the connection gets
Expand All @@ -236,6 +235,15 @@ exit code 1, as on Windows without administrator rights. Where Linux differs fro
not read. The desktop's settings are read with `gsettings`, as you, also under `sudo`: this
is the one program the tool starts, and it sends nothing. There is no proxy for services as
on Windows (WinHTTP).
- **TCP health** on the Tools tab works on Linux, without `sudo`, also for connections of other
users' programs: the kernel keeps these numbers for every TCP connection from the moment it
opens, as `ss -ti` shows them. Linux keeps no newest single round trip and no highest one,
and does not count fast resends or duplicate ACKs, so those rows are not shown. It does
count the segments sent again, which is a row of its own there. The lowest round trip is
the lowest of the last 5 minutes. Timeouts are counted from Linux 6.7 on and count every
time the resend timer ran out. Linux kept measuring when the round trip of an open
connection jumped by 200 ms, so there is no "not measured" note on Linux. The bytes received
do not count data received twice. A connection whose program has closed it reads as Ended.

## The window

Expand Down Expand Up @@ -328,7 +336,8 @@ monitor is gone it returns to the centre of the current screen.
right-click a row to target its program, leave it alone, limit to or block its
remote address, watch a TCP connection in **TCP health** or ping its far end in
**Ping**. Program names are shown without administrator rights.
- **TCP health** - what Windows measures for a TCP connection: the round-trip time
- **TCP health** - what Windows measures for a TCP connection (on Linux, what the kernel
keeps: see the Linux section): the round-trip time
(smoothed, newest, lowest and highest, charted for the last minute), the bytes sent
again, timeouts, duplicate ACKs, the segment size and the bytes sent and received (data
sent again included). Windows measures a round trip only from data it sent once: when
Expand Down Expand Up @@ -1622,14 +1631,16 @@ beantester/ the implementation package
proxy.py the proxy this account and this machine use (winhttp/ctypes and the
registry; on Linux gsettings and the environment files), read only,
for the Tools tab
tcpestats.py what Windows' TCP stack measures for one connection (EStats,
iphlpapi/ctypes), for the Tools tab's TCP health
tcpestats.py what the TCP stack measures for one connection (EStats,
iphlpapi/ctypes; on Linux the kernel's tcp_info), for the Tools
tab's TCP health
tcpreset.py reset one TCP connection (iphlpapi/ctypes; on Linux the kernel's
SOCK_DESTROY), for the Connections page's row menu
tcprow.py one TCP connection the way Windows names it: the row layouts and
finding it in the socket table, without calling Windows
sockdiag.py the kernel's own list of TCP sockets on Linux (sock_diag over
netlink), read only: where a reset finds the socket it destroys
netlink), read only: where a reset finds the socket it destroys,
and one socket's tcp_info for TCP health
rtnetlink.py the kernel's tables of adapters, addresses and routes on Linux, and a
route lookup (what ip route get asks), read only, for the adapters
netlink.py the message socket to the Linux kernel under sockdiag and rtnetlink:
Expand Down
4 changes: 3 additions & 1 deletion beantester/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
(``why_not()``, asked when it is there): Ping, the first one, needs the ping socket
for the user's group or sudo (``icmp.why_not``). Reset on Linux needs a kernel built
with ``CONFIG_INET_DIAG_DESTROY`` (WSL's is not); it has no words of its own, because
its only reader is a row menu, which greys the entry out - the README says why.
its only reader is a row menu, which greys the entry out - the README says why. TCP
health on Linux needs a kernel that lists TCP sockets through sock_diag (every one
measured does); without it the shared words are said.
"""
from types import ModuleType
from typing import NamedTuple, Optional
Expand Down
55 changes: 44 additions & 11 deletions beantester/gui/toolbox/tcphealth.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Tools tab: TCP health - what Windows' TCP stack measures for a watched connection.
"""Tools tab: TCP health - what the system's TCP stack measures for a watched connection.
The numbers are ``tcpestats``'s; the list, the differences and the words are
``nettools/tcphealth.py``'s.

"Does the stack see my 200 ms as 200 ms of round trip, my 5% loss as resent
segments?" A connection is put on the list from a row menu - the socket table
beside this tool, or the Connections page of a session (the owner's choice,
2026-10-19: no second socket table here). Turning counting on for it needs
administrator rights; without them the tool says so in a line of its own, like
"Clean up the driver" does.
2026-10-19: no second socket table here). On Windows turning counting on for it
needs administrator rights; without them the tool says so in a line of its own,
like "Clean up the driver" does. Linux needs none, and has its own "?" sheet, tips
and chart legend (its stack keeps no newest single round trip).

Layout (the owner's choice of the rendered two, 2026-10-19): the list above, the
chosen connection's details below with its round trips charted BESIDE them - stacked
Expand All @@ -20,6 +21,7 @@
after the last one answered: a run waiting behind another is REPLACED by a newer one
(``ToolJob``), so asking twice could have lost the first request.
"""
import sys
import time
import tkinter as tk
import weakref
Expand All @@ -44,6 +46,17 @@
"remote": "tools.tcphealth.col.remote", "state": "tools.tcphealth.col.state",
"rtt": "tools.tcphealth.col.rtt", "resent": "tools.tcphealth.col.resent"}
TIPS = {column: f"tips.tools_tcphealth_col_{column}" for column in COLUMNS}
# What Linux says in its own words: the whole "?" sheet (it is about what Windows
# measures and when), the two column tips and the Stop tip that name Windows.
HELP, HELP_LINUX = "tools.tcphealth.help_body", "tools.tcphealth.help_linux"
TIPS_LINUX = {**TIPS, "state": "tips.tools_tcphealth_col_state_linux",
"resent": "tips.tools_tcphealth_col_resent_linux"}
STOP_TIP, STOP_TIP_LINUX = "tips.tools_tcphealth_stop", "tips.tools_tcphealth_stop_linux"
# The chart's legend: (swatch style, key) of each line, the newest single round trip
# only where the stack keeps one.
LEGEND = (("Swatch.Down.TLabel", "tools.tcphealth.legend_smoothed"),
("Swatch.Up.TLabel", "tools.tcphealth.legend_sample"))
LEGEND_LINUX = LEGEND[:1]
MIN_CHARS = {"process": CHARS["process"], "local": CHARS["address"],
"remote": CHARS["address"], "state": CHARS["tcp_state"], "rtt": CHARS["rtt"],
"resent": CHARS["resent"]}
Expand All @@ -70,6 +83,29 @@ def watchlist(app):
return _LISTS.setdefault(app, tcphealth.Watchlist())


def _is_linux(platform=None):
return (platform or sys.platform).startswith("linux")


def help_key(platform=None):
"""The "?" sheet of this system."""
return HELP_LINUX if _is_linux(platform) else HELP


def tips(platform=None):
"""The list's column tips on this system."""
return TIPS_LINUX if _is_linux(platform) else TIPS


def stop_tip(platform=None):
return STOP_TIP_LINUX if _is_linux(platform) else STOP_TIP


def legend_lines(platform=None):
"""The chart legend's lines on this system."""
return LEGEND_LINUX if _is_linux(platform) else LEGEND


def render(watch):
return tuple(say(cell) for cell in tcphealth.cells(watch))

Expand Down Expand Up @@ -104,17 +140,15 @@ def __init__(self, app, parent):

bar = ttk.Frame(self.frame)
bar.pack(fill="x", padx=space("page"), pady=(space("row"), 0))
self.stop_btn = self._button(bar, "tools.tcphealth.stop", "tips.tools_tcphealth_stop",
self.stop)
self.stop_btn = self._button(bar, "tools.tcphealth.stop", stop_tip(), self.stop)
self.copy_btn = self._button(bar, "tools.tcphealth.copy_report",
"tips.tools_tcphealth_copy", self.copy_report)
self.background = tk.BooleanVar(value=self.memory.get("background") == "1")
box = ttk.Checkbutton(bar, text=T("tools.tcphealth.background"),
variable=self.background, command=self._background_ticked)
box.pack(side="left", padx=(0, space("inline")))
add_tooltip(box, "tips.tools_tcphealth_background")
dialogs.help_button(bar, app.root, "tools.tcphealth.help_title",
"tools.tcphealth.help_body",
dialogs.help_button(bar, app.root, "tools.tcphealth.help_title", help_key(),
"tips.tools_tcphealth_help").pack(side="right")

# Asked once: a running process does not gain or lose administrator rights.
Expand All @@ -132,7 +166,7 @@ def __init__(self, app, parent):
top.pack(fill="x", padx=space("page"), pady=(space("tight"), 0))
self.table = SortableTree(
top, COLUMNS, sort={"col": "", "reverse": False}, on_sort=lambda _s: self._show(),
height=ROWS["watched"], min_chars=MIN_CHARS, tips=TIPS, tags=HEALTH_COLORS,
height=ROWS["watched"], min_chars=MIN_CHARS, tips=tips(), tags=HEALTH_COLORS,
selectmode="browse", empty_text="tools.tcphealth.empty")
self.table.bind_select(self._on_select)

Expand All @@ -146,8 +180,7 @@ def __init__(self, app, parent):
# not its legend (rendered 2026-10-19 without the note's room: the legend went).
legend = ttk.Frame(box)
legend.pack(side="bottom", fill="x", padx=space("tight"), pady=(0, space("tight")))
for style, key in (("Swatch.Down.TLabel", "tools.tcphealth.legend_smoothed"),
("Swatch.Up.TLabel", "tools.tcphealth.legend_sample")):
for style, key in legend_lines():
ttk.Label(legend, text=" ", style=style).pack(side="left")
ttk.Label(legend, text=T(key)).pack(side="left", padx=(space("tight"), space("row")))
self.canvas = chart_canvas(box, size("side_chart"), size("chart"))
Expand Down
Loading
Loading