Terminal explorer for remote hosts over SMB, SSH/SFTP and FTP, with ranger-style navigation.
Project description
smbex
A terminal (TUI) explorer for remote hosts over SMB, SSH/SFTP, and FTP/FTPS, built for slow links: ranger-style navigation, in-session listing cache, background downloads that yield to browsing, folder preloading, and offline filename translation.
Status: foundation in progress. See CLAUDE.md for the full architecture, roadmap, and hand-off notes.
Install
pipx (PyPI)
Install the smbex command into its own isolated environment (needs Python 3.10+):
pipx install smbex
# ...with offline filename translation (CTranslate2 + SentencePiece, ~65 MB):
pipx install "smbex[translate]"
smbex --help
Installed this way, run it as smbex … (the examples below use python3 -m smbex …;
the two are interchangeable). Two things are not Python packages and so aren't
pulled in automatically: the --mux feature needs the system OpenSSH ssh client,
and translation needs a one-time language-model download (smbex --install-lang <lang>; see below).
Kali Linux (apt only — no pip)
Every runtime/test dependency except the translation engine is packaged for Kali:
sudo apt update
sudo apt install python3-impacket python3-paramiko python3-textual \
python3-pytest python3-pytest-asyncio
# optional: the reference impacket CLI tools (impacket-smbclient, ...)
# sudo apt install impacket-scripts
Then run straight from the repo (no pip, no venv needed):
cd /path/to/smbex
python3 -m smbex --version
Filename translation is optional and needs two small wheels (not in apt) —
CTranslate2 + SentencePiece (~65 MB, no torch/CUDA). Add them in a venv that
inherits the apt packages, then fetch a language model (one .argosmodel file):
python3 -m venv --system-site-packages ~/.venvs/smbex
~/.venvs/smbex/bin/pip install ctranslate2 sentencepiece
~/.venvs/smbex/bin/python -m smbex --install-lang ja # one-time, online: ja->en model (~130 MB)
Translation runs entirely on this machine (no filename leaves the box); the only
network use is that one-time model download. See CLAUDE.md → Install for details
(offline --model-file, reusing existing Argos models, etc.).
Development (any distro, venv)
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q
Run
# SMB, impacket-smbclient-style target (password is prompted if omitted):
python3 -m smbex 'DOMAIN/user:password@host'
python3 -m smbex -H :NThash --no-pass 'DOMAIN/user@host' # pass-the-hash
python3 -m smbex -k --dc-ip 10.0.0.1 'DOMAIN/user@host.domain' # Kerberos (ccache)
# SSH/SFTP (host keys auto-accepted by default; --strict-host-keys to verify):
python3 -m smbex 'ssh://user@host'
python3 -m smbex -i ~/.ssh/id_ed25519 'ssh://user@host:2222/var/log'
# SSH via an existing OpenSSH ControlMaster socket — no re-login (needs the ssh client):
python3 -m smbex --mux # pick from sockets found in ~/.ssh etc.
python3 -m smbex --mux /path/to/master.sock # ride a specific socket directly
# FTP / FTPS (no user -> anonymous; ftps:// for TLS):
python3 -m smbex 'ftp://user@host'
python3 -m smbex 'ftps://user:pass@host:21/pub'
Navigate like ranger: h/j/k/l (or arrows), g/G for top/bottom, q to quit.
Listings show a compact age (3d, 2w) and file sizes; o cycles the sort
(name → newest → oldest). A status gutter marks folders whose listing is cached
(·), and files/folders queued (↓) / downloaded (✓).
Download in the background: d (selected file, or a folder recursively), a (all
files in the current folder), w (show/hide the task panel). Downloads mirror the
remote tree under DIR/<host> (--download-dir, default ./downloads), resume
partial files, skip complete ones, and yield to browsing so navigation stays snappy.
Optional extras: --preload prefetches surrounding folders (toggle with p);
--translate <lang> shows English filename translations beside the originals
(toggle with t), computed on-box — see Install for the one-time model setup.
Press ? for the key reference. If a link drops it's reported and you press r
to reconnect (a deliberate new login); --auto-reconnect heals silently instead.
--theme <name> (dark/light/nord/gruvbox) sets the colour theme; T switches it.
[ / ] hide the parent / preview column to save space. A downloaded file's
content shows in the preview pane (text, or an xxd-style hex dump for binaries).
Press l/Enter on a downloaded file to open a full content viewer: text scrolls
with j/k (loaded lazily, so large files open instantly) and, with translation on,
shows the original and English side by side; a binary opens as a scrollable
xxd-style hex view. h/Esc goes back.
Defaults live in a config file (~/.config/smbex/config.ini; flags override it):
python3 -m smbex --write-config # drop a commented sample, then edit it
smbex --help ends with a quickstart covering connecting, the config file, and
the one-time translation-model setup (with the model index URL).
Build a release to transfer
scripts/make_release.sh writes transferable artifacts to dist/:
bash scripts/make_release.sh
-
dist/smbex.pyz— a single-file zipapp. Copy it to the target and run it; it needs onlypython3plus the runtime deps (on Kali:sudo apt install python3-impacket python3-paramiko python3-textual):python3 smbex.pyz 'demo:demo@127.0.0.1' --port 4455 # or: ./smbex.pyz
-
dist/smbex-<ver>.tar.gz— the source tree (run from it withpython3 -m smbex). -
dist/QUICKSTART.txt— the same quickstart shown at the end of--help.
Try it out (local demo, no root)
Terminal 1 — start a throwaway SMB server with sample files on a high port:
python3 scripts/demo_server.py
Terminal 2 — connect the client (any username/password is accepted):
python3 -m smbex 'demo:demo@127.0.0.1' --port 4455
# with Japanese filename translation (needs: --install-lang ja), then press 't':
python3 -m smbex --translate ja 'demo:demo@127.0.0.1' --port 4455
The demo share has a 日本語/ folder of Japanese-named files and folders (写真/,
仕事/, 地図.png, …) so you can watch them render as 写真 → Photos with t.
Test
.venv/bin/python -m pytest -q # unit + UI tests
.venv/bin/python -m pytest -m integration # local SMB/SSH server tests (Phase 1+)
Protocol fidelity — how smbex differs from the standard tools
smbex builds on the same libraries the reference tools use (impacket for SMB,
paramiko for SSH/SFTP, stdlib ftplib for FTP), so the packets themselves are
well-formed the same way — smbex does not hand-roll protocol framing. The
differences below are all in usage patterns (command sequences, frequency, trust
decisions), not in the bytes on the wire. They are documented here so you can weigh
the risk against your own hosts, especially fragile or tightly-audited ones.
SMB — per-file tree-connect churn
smbex issues one TREE_CONNECT/TREE_DISCONNECT per file, where
impacket-smbclient and the Windows redirector connect a share once and reuse
that tree for the whole session. Each open_file does connectTree → openFile
and its close tears both down (smbex/backend/impacket_backend.py). Downloading 500
files is 500 tree cycles here vs. 1 there.
- Why it's written this way: statelessness. A cached tree id (
tid) goes stale after a dropped link (see the reconnect design), so by never holding one across operations we can't reuse a deadtid. This is a simplicity choice, not a performance one — there is no throughput advantage. - Cost: on a host with SMB auditing this multiplies tree-connect events; it is slightly more churn than a normal client. It is not malformed and cannot desync the session.
- If it matters: cache
{share: tid}on the backend, connect-on-first-use, and invalidate it inreconnect()— a small, self-contained change that restores one-tree-per-share parity.
SMB — stat() uses a directory FIND, not a file QUERY_INFO
stat() reads a single file's metadata via listPath (an SMB2 QUERY_DIRECTORY
against the parent, filename as the search pattern) rather than opening the file and
issuing QUERY_INFO.
- Why: SMB2 has no "getattr by path" — a real
QUERY_INFOrequires first opening the file (CREATE), which emits a file-access audit event and participates in sharing-mode/oplock negotiation (you can collide with a process holding the file open). The directory FIND reads the metadata out of the parent's enumeration without ever opening the file. On a sensitive host this is the lighter, less-intrusive touch, and it matches impacket-smbclient. - Cost: it is a directory-query op rather than a handle getinfo, and (today) it carries a tree-connect cycle with it (see above).
SSH — default host-key policy is trust-on-first-use, not persisted
The default auto policy (smbex/backend/ssh_backend.py) silently accepts an
unknown host key and keeps it only in memory. OpenSSH ssh/scp instead prompt
on an unknown key (and refuse in batch mode), writing accepted keys to
~/.ssh/known_hosts.
- A changed key is still rejected (paramiko raises regardless of policy), so the
exposure is limited to unknown/first-contact hosts (MITM-susceptible on first
connect). Because nothing is written to
known_hosts, every run is "first contact". - If it matters: pass
--strict-host-keys(policystrict) to require a pre-known key, matching default OpenSSH behavior.
SSH — SFTP subsystem, not scp
smbex speaks the SFTP subsystem (sftp-server), not the scp protocol. SFTP was
chosen because it supports listing, stat, and seekable/resumable reads — the things
the browser, resumable downloads, and the lazy preview/viewer need; plain scp is a
transfer-only subset. On a host where the sftp subsystem is disabled
(Subsystem sftp removed, a ForceCommand/restricted shell permitting only scp, or
old dropbear builds without sftp-server), smbex will fail where scp works. See
"scp support" below for the evaluation of adding an scp backend.
SSH — listings probe symlink targets
When listing a directory, smbex issues an extra STAT on each symlink target to
decide whether a link is a directory (_to_entry, smbex/backend/ssh_backend.py).
ls/sftp use the lstat attrs already returned by readdir and do not follow
the link.
- Why: so directory-symlinks are browsable (you can
linto them). - Cost: extra round trips, and it touches paths you did not navigate to.
statdoes not open the target, so the risk is low, but on a sensitive host a link could point at a device node, an automount trigger, or a monitored path.
FTP — early-stopped reads drain the rest of the file instead of ABOR
A normal FTP client that stops a RETR early sends ABOR. smbex instead reads the
data connection to its natural EOF and then consumes the single clean 226
(_drain_and_finish, smbex/backend/ftp_backend.py).
- Why FTP forces the choice: FTP splits control (commands/responses) from
data (file bytes), and the session invariant is that every control response
must be read by the command that provoked it — one response left unread and the
control channel is off-by-one and wedged.
ABORis genuinely awkward: it is preceded by an out-of-band Telnet interrupt (IAC+IP, then Synch) and the server then emits two control responses in a server-dependent order (typically426and226; some do226+225). Real clients (lftp, BSDftp) implement all of that;ftplib'sabort()does not and is known to leave the control channel desynced against many servers. Draining keeps the control channel provably in sync. - The trade-off: correctness/robustness over bandwidth. For a fragile server the drain is the conservative choice — it never leaves the control channel in a weird state (the thing that wedges FTP sessions). The cost is bandwidth: previewing or seeking within a large file can pull the rest of that file. It only triggers when you stop early — a normal download-to-EOF closes cleanly with no drain.
- If the bandwidth ever bites: the alternative is not ftplib's flaky
abort()— it is to drop and reopen the whole control connection to abandon the transfer, which is clean but makes a fresh login/audit event each time.
FTP — passive mode + MLSD
ftplib defaults to passive mode (PASV/EPSV) and smbex prefers MLSD for
listings (falling back to LIST). The classic ftp command defaults to active mode
and uses LIST/NLST. Both smbex choices match modern clients (lftp); if your
baseline is the traditional ftp client, expect a different data-connection direction
(firewall-visible) and MLSD where you would see LIST. TYPE I is also re-sent
before every RETR (extra but valid — MLSD/LIST leave the session in ASCII mode).
Reconnect produces additional login/auth events
By design, a dropped link plus r (or --auto-reconnect) performs a fresh login,
so one smbex "session" can show multiple logins from the same source — unlike a single
long-lived client session. --auto-reconnect makes these re-logins silent. Relevant
for audit correlation.
scp support (evaluated, not implemented)
Adding scp as a peer protocol is technically possible (paramiko exec_command running
scp -f) but fits smbex's model poorly, so it is intentionally not built:
- No listing or stat. The scp protocol only transfers files/trees; browsing would
require shelling out to remote
ls/findover an exec channel — i.e. running commands on the host (a larger footprint than SFTP's read-only file ops, and shell/PATH-dependent). The ranger UI is built onlist()/stat(), which scp lacks. - No seek/resume. scp streams a whole file start-to-finish, so resumable downloads, the lazy preview, and the windowed viewer (all of which seek) cannot work.
- Shrinking need. OpenSSH 9.0 (2022) deprecated the scp protocol and made the
scpcommand use SFTP under the hood, so on modern hosts "scp" already is sftp.
Where it would earn its place: appliances/embedded gear (e.g. dropbear without
sftp-server) or hardened boxes that permit scp but not the sftp subsystem. If such
targets exist in your fleet, the honest shape is not a browsing peer backend but a
restricted download-only mode (transfer via scp, browse via remote ls), with no
resume/preview. Recommendation: don't build it speculatively — revisit only if you can
enumerate real sftp-disabled hosts.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file smbex-0.1.0.tar.gz.
File metadata
- Download URL: smbex-0.1.0.tar.gz
- Upload date:
- Size: 89.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6600ab7146718f252d3befce3d35b92cfeea733855fa35063b57300125054e95
|
|
| MD5 |
e732f1db68e227a6c49fdd1f7cf4bcfb
|
|
| BLAKE2b-256 |
e96ef2f33ddaf32722b6146782c34ed2002c8e7e94b9fd407298a3fcf57526ef
|
Provenance
The following attestation bundles were made for smbex-0.1.0.tar.gz:
Publisher:
release.yml on BearlyStable/smbex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
smbex-0.1.0.tar.gz -
Subject digest:
6600ab7146718f252d3befce3d35b92cfeea733855fa35063b57300125054e95 - Sigstore transparency entry: 2256415552
- Sigstore integration time:
-
Permalink:
BearlyStable/smbex@3b8b66d034663b1ec49072be5e81cafd3da9085d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/BearlyStable
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3b8b66d034663b1ec49072be5e81cafd3da9085d -
Trigger Event:
push
-
Statement type:
File details
Details for the file smbex-0.1.0-py3-none-any.whl.
File metadata
- Download URL: smbex-0.1.0-py3-none-any.whl
- Upload date:
- Size: 67.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
52d07bad3bba1449d6d793c0e507b7964b864f23d6b489a226b2ac1842b3310b
|
|
| MD5 |
116205e79c87d048f65e078460164bc4
|
|
| BLAKE2b-256 |
3a1c7c94c1aabfc15f5c9cc536fdd9130f170ef140ba1a29d8840e2309584be4
|
Provenance
The following attestation bundles were made for smbex-0.1.0-py3-none-any.whl:
Publisher:
release.yml on BearlyStable/smbex
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
smbex-0.1.0-py3-none-any.whl -
Subject digest:
52d07bad3bba1449d6d793c0e507b7964b864f23d6b489a226b2ac1842b3310b - Sigstore transparency entry: 2256415562
- Sigstore integration time:
-
Permalink:
BearlyStable/smbex@3b8b66d034663b1ec49072be5e81cafd3da9085d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/BearlyStable
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3b8b66d034663b1ec49072be5e81cafd3da9085d -
Trigger Event:
push
-
Statement type: