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
36 changes: 35 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -642,10 +642,13 @@ jobs:
# interfaces are never handed to it. `--require` makes a runner that cannot run
# the check a failure, not a quiet SKIP. Not in the branch ruleset: a red one says
# Linux capture broke, and whether that blocks a merge is the owner's call.
#
# Its last step is the window's way to root on Linux (linuxenv.py), for the same
# reason: only a real pkexec and polkitd show what pkexec does to the program.
linux-capture:
name: Linux capture (experimental, real kernel)
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -659,6 +662,37 @@ jobs:
run: uname -r
- name: Real capture inside a network namespace of its own
run: sudo "$(command -v python)" tools/ci_linux_capture_smoke.py --require
# The window asking for root through polkit, with this image's own pkexec and
# polkitd (Ubuntu 24.04: polkit 124, which names the person by PKEXEC_UID
# only). The runner is thrown away after the job, so a polkit rule that lets
# its user run pkexec without a password stands in for the person typing it.
# The smoke first asks without the rule and must see pkexec refuse.
- name: The window asks for root through pkexec (real polkit, Xvfb)
run: |
missing=""
for package in pkexec polkitd python3-tk xvfb; do
dpkg -s "$package" >/dev/null 2>&1 || missing="$missing $package"
done
if [ -n "$missing" ]; then
echo "installing:$missing"
# The mirror and the two-step install: see "GUI render check".
for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list \
/etc/apt/sources.list.d/*.sources \
/etc/apt/sources.list.d/*.list; do
if [ -f "$source" ]; then
sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' "$source"
fi
done
if ! sudo timeout 180 apt-get install -y --no-install-recommends $missing; then
sudo timeout 240 apt-get update \
-o Acquire::Retries=2 \
-o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15
sudo timeout 240 apt-get install -y --no-install-recommends $missing
fi
fi
pkexec --version
timeout 120 xvfb-run -a --server-args="-screen 0 1280x800x24" \
python -u tools/ci_linux_pkexec_smoke.py --require

build:
name: build the executable (Windows)
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol
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.

- **Linux, experimental: the window asks for the administrator password.** Started without
sudo, it shows the desktop's own password dialog, as Windows shows UAC, and opens as root.
**Cancel** opens it without root, as before. Where there is no such dialog, as in WSL or over
ssh, it opens without root at once. `BEAN_NO_ELEVATE=1` turns the question off. The files it
saves stay yours, as under sudo.

- **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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ checkout, which you could then not delete without `sudo`. Plain `sudo` keeps the
the window needs to reach the desktop, `DISPLAY` and `XAUTHORITY`, on Ubuntu 24.04 and 26.04,
also under Wayland. Ubuntu 26.04's `sudo` refuses `-E`, and nothing here needs it.

Started without `sudo` (`.venv/bin/python bean_network_tester.py`), the window asks for an
administrator's password in the desktop's own dialog, through polkit's `pkexec`, as Windows
asks through UAC, and opens as root. **Cancel** opens it without root, as before. Where no such
dialog exists, such as in WSL or over ssh, it opens without root at once, and
`BEAN_NO_ELEVATE=1` turns the question off.

`--doctor` lists what the machine still lacks, and while something stops a capture it ends with
exit code 1, as on Windows without administrator rights. Where Linux differs from Windows:

Expand All @@ -204,8 +210,8 @@ exit code 1, as on Windows without administrator rights. Where Linux differs fro
- If the tool is killed, the traffic it was holding stays held for about 3 seconds, then flows
again. There is nothing for `--cleanup-driver` to do: the kernel cleans up by itself.
- 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.
- In the window, the files it saves (window state, profiles, CSV) stay yours under `sudo` and
`pkexec`, and GitHub and Donate print their address instead of starting a browser as root.
- 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
Expand Down Expand Up @@ -1625,6 +1631,7 @@ beantester/ the implementation package
exitcodes.py EXIT CODES - the CLI/CI-CD contract (single source of truth)
clilog.py CLI output: log on stderr ([bean]), data on stdout (text/NDJSON)
winenv.py Windows: admin, elevation (UAC), console detach, DPI
linuxenv.py Linux: the window asks for root through polkit (pkexec)
driver.py WinDivert driver lifecycle + --doctor / --cleanup-driver
fields.py FIELD REGISTRY - single source of truth: type, label, unit,
range, form section, profile scope, CLI flag
Expand Down
18 changes: 16 additions & 2 deletions beantester/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import threading
import time

from . import appinfo, capture_backend, clilog, driver, exitcodes, winenv
from . import appinfo, capture_backend, clilog, driver, exitcodes, linuxenv, winenv
from .appinfo import command_name, program_name, version_line
from .clilog import CliLog
from . import crashlog
Expand Down Expand Up @@ -1323,6 +1323,14 @@ def _run_gui(argv, relaunched=False):
# elevate_self never asks again, admin or not (see winenv.UAC_RELAUNCH).
if winenv.elevate_self(argv):
return exitcodes.OK # the elevated copy took over
elif linuxenv.wants_root():
# The same question through polkit, and the copy's exit code back; None =
# it did not run (dismissed, no agent), so the window opens here without
# root (see linuxenv.py).
code = linuxenv.ask_for_root(argv)
if code is not None:
return code
linuxenv.take_desktop() # in the copy pkexec started: the person's screen
if is_frozen():
winenv.detach_console() # no black window behind the GUI
# Arm hard-crash capture for the GUI too, not only for a real capture session
Expand All @@ -1338,6 +1346,7 @@ def _run_gui(argv, relaunched=False):
# the import would also make the guard for this line untestable anywhere Tk is
# missing, i.e. green here and vacuous on the Linux runner.
crashlog.arm_native()
root = None
try:
import tkinter as tk

Expand All @@ -1346,7 +1355,12 @@ def _run_gui(argv, relaunched=False):
root = tk.Tk()
App(root)
except Exception as exc:
return _window_did_not_open(exc)
code = _window_did_not_open(exc)
# A copy pkexec started whose Tk cannot reach the person's display answers
# pkexec's own "did not run": the person's process then opens the window.
if root is None and type(exc).__name__ == "TclError" and linuxenv.started_by_pkexec():
return linuxenv.NO_WINDOW
return code
root.mainloop()
return exitcodes.OK

Expand Down
Loading
Loading