Motivation
Sailfish OS is one of the last truly independent mobile operating systems — a Linux-based platform with a distinctive gesture-driven UI, strong privacy defaults, and a small but dedicated community of developers and enthusiasts. Despite running a full Linux stack underneath, Sailfish has historically lacked one tool that network engineers and security researchers consider essential: a packet capture and analysis tool with a usable interface.
tshark and tcpdump can be installed and run from the terminal, but doing so on a phone is cumbersome. The full Wireshark desktop application is not portable to Sailfish without significant work — it targets Qt Widgets, assumes a large screen, and pulls in dependencies that either do not exist in the Sailfish SDK or conflict with its Qt 5.6.3 base.
SailShark closes that gap: a Sailfish-native network analyzer with a Silica QML interface designed for portrait phone use, backed by the proven dissection engine of Wireshark 3.6.x.
What's in v1.1
tshark -D. One tap to select.tshark -V, cached for instant revisits, collapsible section in the detail page.tshark -z follow,tcp,ascii,N. Accessible from the pulley menu on TCP/TLS/HTTP/SSH packets.~/Documents/SailShark/ as a timestamped .pcapng. Directory is created automatically if missing.Packet Detail — Screenshot
// PacketDetailPage — Protocol Tree + Hex Dump // SailfishEmul
Architecture
SailShark separates the capture engine from the UI entirely, a clean match for the Sailfish application model:
CaptureListPage
PacketDetailPage
TcpStreamPage
InterfacePickerPage
AboutPage · CoverPage
Poll timer (1s)
Hex + tree + stream cache
Qt signals
-r -Y frame.number>N reader
-V proto tree
-z follow,tcp,ascii
cap_net_raw
The capture pipeline uses a two-process poll architecture to avoid tshark's stdout buffering problem:
- Writer process —
tshark -i <iface> -w /tmp/sailshark-capture.pcapngwrites raw pcap to disk. Binary output has no buffering issues. - Poll timer — fires every second, spawning a fresh
tshark -rreader against the growing pcap. - Reader process — uses
-Y "frame.number > N"to only dissect new packets, avoiding the catastrophic re-read cost that accumulates at high packet counts. - Hex fetch —
tshark -r -x -Y frame.number==Non demand. Results cached inQMap<int,QString>. - Proto tree fetch —
tshark -r -V -Y frame.number==Non demand. Accumulated viareadyReadStandardOutputinto a buffer before processing, cached per packet. - TCP stream fetch —
tshark -r -q -z follow,tcp,ascii,Nwhere N is thetcp.streamindex carried in every packet's metadata. - The pcap is kept on disk after stop — hex, tree and stream views remain available after capture ends. Cleaned up at the next
start().
Qt 5.6 QML Signal Quirk
In Qt 5.6's QML engine, signal parameters in a Connections block are not automatically bound by parameter name. Code like onPacketReceived: { pkt.protocol } raises ReferenceError: pkt is not defined. The fix is positional access via the arguments array:
onPacketReceived: {
var pkt = arguments[0] // Qt 5.6: names not in scope
globalPacketModel.append({ ... })
}
This affected every signal with parameters in the project and was the root cause of packets not appearing in the UI despite the C++ engine working correctly.
mapplauncherd / PIE
Sailfish launches apps via mapplauncherd, which dlopen()s the binary into a pre-forked booster process. This requires the binary to be a position-independent executable. Without -fPIE -pie -rdynamic in the cmake build, the app launches fine from terminal but fails with "cannot dynamically load executable" from the app grid. Added to CMakeLists.txt:
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie -rdynamic")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE")
Technical Challenges
| Challenge | Solution |
|---|---|
| Qt version mismatch | Sailfish ships Qt 5.6.3. QOverload<> arrived in Qt 5.7 — all overloaded signal connections rewritten using static_cast<> disambiguation. |
| tshark stdout buffering | tshark buffers text output aggressively. Solved by switching to a pcap writer + poll reader architecture — the writer outputs binary and a fresh reader is spawned every second. |
| Poll cost at high packet counts | The reader was re-dissecting the entire pcap every second and skipping already-seen packets in C++. Fixed by passing -Y "frame.number > N" so tshark only outputs new packets. |
| QML signal parameter scope | Qt 5.6 Connections blocks do not bind signal parameter names. All handlers use arguments[0], arguments[1] positional access. |
| Hex dump timing race | Component.onCompleted fires during the page push animation before Connections is active. Moved fetch trigger to onStatusChanged with PageStatus.Active. |
| Proto tree stdout race | readAllStandardOutput() in the finished slot races against OS pipe flushing. Fixed by accumulating into a QByteArray buffer via readyReadStandardOutput, draining the remainder in finished. |
| Qt 5.6 Label segfault on large text | The Qt 5.6 text layout engine crashes when a Label with wrapMode: Text.NoWrap receives very large strings (proto tree output can exceed 3000+ chars). Fixed by truncating to 8000 chars in C++ and switching to Text.WrapAtWordBoundaryOrAnywhere. |
| mapplauncherd PIE requirement | Sailfish's booster dlopen()s the app binary — requires PIE. Binary compiled as EXEC runs from terminal but fails from the app grid. Fixed with -fPIE -pie -rdynamic CMake flags. |
| Post-install user detection | loginctl list-sessions filtered on $4 == "seat0" identifies the active graphical session user regardless of username or other logged-in accounts. |
| homeDir() wrong under booster | QStandardPaths::HomeLocation resolves via getpwuid() which returned the wrong user under mapplauncherd. Fixed by reading the HOME environment variable first, which the invoker sets correctly. |
| No desktop UI on phones | Set -DBUILD_wireshark=OFF. Built only tshark and dumpcap, then wrote a new Silica/QML frontend from scratch. |
| MOC not running on QObject | CMAKE_AUTOMOC missing from sfos-ui CMakeLists.txt caused linker errors. Fixed with set(CMAKE_AUTOMOC ON). |
| Unicode in C++ source | The OBS GCC rejects multibyte UTF-8 characters (e.g. …) as invalid identifiers in source files. All non-ASCII literals replaced with plain ASCII equivalents. |
Source File Structure
The RPM spec coordinates two independent cmake builds and twelve source files:
build/ ← tshark + libwireshark cmake build
sfos-ui/
main.cpp ← SailfishApp entry point
captureengine.h/.cpp ← QProcess wrapper, caches, all tshark calls
qml/
harbour-wireshark.qml ← ApplicationWindow + globalPacketModel (5k cap)
CaptureListPage.qml ← Packet list + pulley menu + save notification
PacketDetailPage.qml ← Summary + proto tree + hex dump
TcpStreamPage.qml ← Follow TCP stream reassembly view
InterfacePickerPage.qml ← Interface selector
AboutPage.qml ← Developer info + links
CoverPage.qml ← Sailfish home screen cover
CMakeLists.txt ← Generated by spec heredoc
build/ ← sfos-ui cmake build output
Installation
Prerequisites
Sailfish OS 2.0 or later. Built for armv7hl, aarch64, and i486 (emulator). Developer mode and root access are required for packet capture.
Installing
Add my repository at build.sailfishos.org/nieldk for your architecture, and install using pkcon:
devel-su pkcon refresh devel-su pkcon install sailshark
The %post scriptlet sets the required capabilities on dumpcap and creates ~/Documents/SailShark/ for the active session user:
setcap cap_net_raw,cap_net_admin=eip /usr/bin/dumpcap
Enabling Developer Mode
Enable Developer Mode under Settings › System › Developer Tools. This provides SSH access and the devel-su command required for installation.
Using SailShark
Starting a Capture
- Launch SailShark from the app grid.
- Pull down from the top of the screen to open the Pulley Menu.
- Tap Select Interface… — a full list of available interfaces is shown. Tap one to select.
- Optionally enter a BPF capture filter expression in the filter field.
- Tap Start Capture. A status banner shows the active interface and running packet count.
Reading the Packet List
Each row shows a colour-coded protocol stripe on the left edge:
The list is capped at 5000 entries — oldest packets are dropped on overflow to keep the UI responsive. Tap any row to open the detail page.
Packet Detail Page
- Summary — timestamp, protocol, length, source, destination.
- Info — full tshark info string.
- Protocol Tree — collapsed by default. Tap the section header to expand. Shows the full tshark
-Vdissection — frame, Ethernet, IP, transport, and application layers. Fetched on demand, cached for instant revisits. - Hex Dump — monospace, horizontally scrollable. tshark's inline ASCII column is shown alongside the hex bytes. Collapsed/expanded independently.
Follow TCP Stream
On any TCP, TLS, HTTP, or SSH packet, pull down the packet detail page to reveal Follow TCP Stream. This runs tshark -z follow,tcp,ascii,N against the pcap and displays the reassembled conversation in a scrollable monospace view. The stream index is captured automatically from the tcp.stream field during live capture.
Saving a Capture
Pull down on the main list and tap Save Capture. The pcap is saved to:
~/Documents/SailShark/capture-YYYYMMDD-HHMMSS.pcapng
The directory is created automatically if it does not exist. The file can be transferred to a desktop and opened directly in Wireshark.
Stopping a Capture
Pull down and tap Stop Capture, or press the cover action button on the home screen. The packet list, pcap, hex dumps, and protocol trees remain available for browsing after stopping.
Capture Filters
BPF expressions are passed directly to tshark as -f filters:
tcp port 443 # HTTPS only host 192.168.1.1 # Traffic to/from a specific host not port 22 # Exclude SSH udp and port 53 # DNS queries only
Interface Reference
| Interface | Description |
|---|---|
| wlan0 | Wi-Fi — most common for general network analysis |
| rmnet0 / rmnet_data0 | Mobile data (varies by device and modem driver) |
| lo | Loopback — useful for debugging local app traffic |
| any | Capture on all interfaces simultaneously |
| usb0 | USB tethering or RNDIS interface |
| eth0 | Emulator virtual ethernet interface |
The full list of interfaces available on your device is shown in the interface picker at launch, populated by tshark -D.
Known Limitations
- No display filters. BPF capture filters (
-f) are supported; Wireshark display filters (-Yon live traffic) are not yet exposed in the UI. - No load from file. SailShark can save pcaps but cannot yet open an existing file for offline browsing.
- Poll latency. New packets appear with up to 1 second delay due to the polling interval.
- Proto tree truncated at 8000 chars. Very verbose dissections are cut off to prevent a Qt 5.6 text layout crash.
- TCP stream truncated at 8000 chars. Large HTTP bodies or long SSH sessions will be cut off in the stream view.
- No protocol tree collapse per node. The tree is shown as flat indented text — the whole section collapses but individual layers cannot be folded.
Author
Built by Niel Nielsen (NielDK) — Farum, Denmark.
High-signal tech pragmatist focused on logic, efficiency, and impactful open-source contributions.