qrtunnel: One Command. One QR Scan. No Accounts, No Firewall Rules, No Contact Sharing.
I hated the sheer friction of moving a single file between my laptop and my phone.
Think about how ridiculous this workflow normally is. If I have an iPhone and a MacBook, AirDrop is fine. But the second I am on Linux or Windows with an Android phone (or an iPhone), moving a video, a PDF, or a photo album turns into a chore. Bluetooth on desktop Linux transfers at a miserable 200 KB/s when pairing actually works, which is almost never. Sending a file to myself on Telegram, Slack, or Google Drive feels absurd when my phone is sitting five inches away on my desk. And if I spin up Python’s built-in python -m http.server, I have to open a new terminal tab, run ip addr or ifconfig to find my local IP, type out something like http://192.168.1.44:8000/my-super-long-document-name.pdf on my tiny phone keyboard, and pray that the campus or coffee shop Wi-Fi doesn’t have client isolation enabled.
I wanted something dead simple: I type one command in my terminal, flash an ANSI QR code on my screen, point my phone camera at it, and immediately download or upload files. No accounts, no sign-ups, no cloud storage intermediaries, and no typing URLs on mobile.
Here is the story of how I built qrtunnel, from a fragile script that crashed on large files to a multi-threaded, zero-copy CLI with Smart LAN auto-switching and custom keyboard handling.
flowchart TB
subgraph CLI["Laptop Terminal (Sender / Host)"]
direction TB
CMD["$ qrtunnel send file.mp4
Single-Command CLI"]
QR["Terminal ANSI QR Code
Encodes WAN & LAN Tokens"]
ENGINE["Zero-Copy Streaming Core
os.sendfile() & 64 KB Buffers"]
CMD --> ENGINE
CMD --> QR
end
subgraph PHONE["Mobile Device (Receiver / Uploader)"]
direction TB
SCAN["Camera QR Scan
Opens Web UI in Browser"]
PROBE["Smart LAN Probe
Pings Local IP: 192.168.x.x"]
UI["Minimalist Web Client
Stream Upload / Zip Download"]
SCAN --> UI --> PROBE
end
subgraph ROUTING["Smart Auto-Routing Decision"]
direction TB
LAN_OK{"Same Wi-Fi
Reachable?"}
DIRECT["Direct Local Socket
Bypasses Cloud (80+ MB/s)"]
FALLBACK["Reverse Tunnel Fallback
Encrypted SSH / Ngrok (4 MB/s)"]
LAN_OK -- "Yes (Ping < 15ms)" --> DIRECT
LAN_OK -- "No (AP Isolation / 5G)" --> FALLBACK
end
%% End-to-End Pipeline Connections
QR -. "Point Camera" .-> SCAN
PROBE --> LAN_OK
DIRECT ==> ENGINE
FALLBACK ==> ENGINE
ENGINE -- "Kernel Zero-Copy Stream" --> UI
%% Excalidraw Color Palette
style CLI fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E3A8A
style PHONE fill:#FFFBEB,stroke:#F59E0B,stroke-width:2px,color:#78350F
style ROUTING fill:#F5F3FF,stroke:#8B5CF6,stroke-width:2px,color:#4C1D95
style CMD fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
style QR fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
style ENGINE fill:#FCE7F3,stroke:#EC4899,stroke-width:2px,color:#831843
style SCAN fill:#FEF3C7,stroke:#D97706,color:#92400E
style PROBE fill:#FEF3C7,stroke:#D97706,color:#92400E
style UI fill:#FEF3C7,stroke:#D97706,color:#92400E
style LAN_OK fill:#DCFCE7,stroke:#10B981,stroke-width:2px,color:#064E3B
style DIRECT fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46
style FALLBACK fill:#EDE9FE,stroke:#7C3AED,stroke-width:2px,color:#5B21B6The Core Systems Engine: Moving Bytes Without Melting RAM
When I started out, my mental model was basic: wrap Python’s http.server.BaseHTTPRequestHandler, stick an SSH tunnel on the front, and call it a day.
That naive assumption fell apart the minute I tried sharing a 2 GB screen recording.
From Naive Memory Slurping to Chunked Streams
In my earliest prototype, uploads looked something like this:
# The naive trap: buffering entire payloads in memory
def handle_upload(self):
content_length = int(self.headers["Content-Length"])
raw_data = self.rfile.read(content_length)
parser.data_received(raw_data)
If I uploaded a 1.5 GB video from my phone, Python happily allocated 1.5 GB of RAM to store raw_data, another chunk inside the multipart parser, and completely hammered the garbage collector. On low-memory machines, the kernel OOM killer stepped in and assassinated the process mid-transfer.
I replaced that entire pipeline with streaming chunk consumption. For uploads, I wrapped self.rfile with a bounded stream and fed streaming-form-data in fixed 64 KB buffers:
chunk_size = 65536 # 64 KB
received = 0
while True:
chunk = stream.read(chunk_size)
if not chunk:
break
received += len(chunk)
parser.data_received(chunk)
RAM usage instantly plummeted from gigabytes to a flat 25 MB baseline, regardless of whether the file was 5 MB or 50 GB.
Kernel Zero-Copy with os.sendfile on Linux
For downloads (sending files from the laptop to the phone), Python’s default behavior is to read blocks from disk into user-space application memory, copy that buffer into socket buffers, and hand it to the network stack:
flowchart LR
D["Disk Storage"] --> K["Kernel Page Cache"]
K -- "Context Switch" --> U["User-Space Buffer"]
U -- "Memory Copy" --> S["Socket Buffer"]
S --> N["Network Card (NIC)"]
K == "os.sendfile (Zero-Copy)" ==> S
style D fill:#EFF6FF,stroke:#3B82F6,color:#1E3A8A
style K fill:#FEF3C7,stroke:#D97706,color:#78350F
style U fill:#FEE2E2,stroke:#EF4444,color:#7F1D1D
style S fill:#DCFCE7,stroke:#16A34A,color:#14532D
style N fill:#EDE9FE,stroke:#8B5CF6,color:#4C1D95That involves two context switches and two memory copies per chunk. On Linux, the kernel provides os.sendfile(), which pipes data directly between two file descriptors completely within kernel address space:
if platform.system() == "Linux" and hasattr(os, "sendfile"):
try:
sent = 0
while sent < content_length:
n = os.sendfile(
self.connection.fileno(),
f.fileno(),
start + sent,
content_length - sent,
)
if n == 0:
break
sent += n
except BrokenPipeError:
print(f"{ERR} Client disconnected during transfer")
On a 5 GHz local Wi-Fi connection, this dropped CPU utilization to near zero while maxing out the local network bandwidth. For non-Linux platforms (macOS, Windows), I kept a clean fallback using 1 MB buffered reads (f.read(1024 * 1024)).
Surviving Client Disconnects Gracefully
Mobile browsers are notorious for dropping TCP sockets without an orderly FIN/ACK handshake. If my phone screen timed out, or if I swiped the browser tab away while a 4 GB download was halfway through, Python dumped an ugly 40-line BrokenPipeError traceback into the terminal.
Even worse, on older iterations of my handler, the except Exception block caught the broken pipe and attempted to call self.send_error(500)—writing an HTTP error page down a socket that was already dead, triggering another unhandled exception!
I resolved this by explicitly checking for broken pipes and silencing the noise:
except Exception as e:
if not isinstance(e, BrokenPipeError) and "Broken pipe" not in str(e):
print(f"{ERR} Error serving file '{filename}': {e}")
if not self.wfile.closed:
try:
self.send_error(500, "Internal Server Error")
except Exception:
pass
The Public Tunnel: Zero-Account Access via OpenSSH
To make qrtunnel work when my devices are on different networks (or when client isolation is blocking local traffic), I needed a reverse tunnel.
While I built in support for ngrok (-ngrok) and Cloudflare quick tunnels as alternative backends, I didn’t want either of them to be a hard requirement for everyday transfers. Ngrok requires registering an account and configuring an authtoken, while Cloudflare requires installing a separate daemon (cloudflared). I wanted someone to run pip install qrtunnel and have public tunneling work instantly on Linux and macOS without signing up for anything or downloading external binaries.
Hooking into localhost.run
On Linux and macOS, almost every developer machine already has OpenSSH installed. I leveraged localhost.run, an SSH service that lets you map local ports to a public URL without creating an account:
ssh -R 80:localhost:PORT nokey@localhost.run
When you connect, localhost.run assigns you a dynamic subdomain under *.lhr.life and routes external HTTPS traffic straight back to your local port.
The SSH Output Pollution Problem
Connecting to localhost.run created an immediate usability issue: OpenSSH prints server banners, host key warnings, and connection logs directly to standard output.
When my script spawned the SSH subprocess, localhost.run dumped an entire ASCII art logo and connection disclaimer onto the screen, completely garbling the ANSI QR code that my script was trying to print.
I solved this by decoupling the process output from standard output entirely. I launched ssh with a customized null device and spawned a dedicated daemon thread to parse the stdout stream silently:
null_device = "NUL" if platform.system() == "Windows" else "/dev/null"
cmd = [
"ssh",
"-o", "StrictHostKeyChecking=no",
"-o", f"UserKnownHostsFile={null_device}",
"-o", "ServerAliveInterval=60",
"-o", "ConnectTimeout=15",
"-o", "LogLevel=ERROR",
"-o", "AddressFamily=inet",
"-T",
"-R", f"80:localhost:{self.local_port}",
"nokey@localhost.run",
]
Inside the reader thread, I used a regex to extract the public URL and signal an event flag:
def _read_output(self):
url_pattern = re.compile(r"https://[a-zA-Z0-9.-]+\.lhr\.life")
try:
while self.process and self.process.poll() is None:
line = self.process.stdout.readline()
if not line:
time.sleep(0.1)
continue
line = line.strip()
match = url_pattern.search(line)
if match and not self.public_url:
self.public_url = match.group(0)
self.url_found.set()
except Exception:
pass
The terminal stays clean, no SSH text leaks out, and the moment the URL is extracted, the QR code renders cleanly.
The Mobile Web Interface: Zero-Dependency Embedded UI
For qrtunnel to feel frictionless, the receiving experience on mobile had to be instantaneous. I didn’t want the receiver to install a mobile app or wait for heavy JavaScript bundles to download. The goal was a clean, self-contained web portal that renders immediately the second a phone camera scans the terminal QR code.
I designed a responsive, zero-dependency mobile interface embedded directly inside src/qrtunnel/server.py as pure HTML, CSS, and vanilla JavaScript.
flowchart TB
subgraph UI_PORTAL["qrtunnel Mobile Web Portal"]
direction TB
READY["Files Ready for Download"]
subgraph FILES["Available Payload"]
direction TB
F1["presentation_slides.pdf"]
F2["database_backup.sql"]
F3["raw_footage.mp4"]
F1 --> F2 --> F3
end
ZIP["Download All as ZIP
Streams In-Flight Archive"]
subgraph LAN_BANNER["Smart Wi-Fi Detection"]
direction TB
LAN_MSG["Same Wi-Fi Detected
LAN Ping < 15ms"]
SWITCH_BTN["Switch to High Speed
Direct 80+ MB/s Socket"]
LAN_MSG --> SWITCH_BTN
end
READY --> FILES --> ZIP --> LAN_BANNER
end
style UI_PORTAL fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E3A8A
style READY fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
style FILES fill:#FFFBEB,stroke:#F59E0B,stroke-width:2px,color:#78350F
style F1 fill:#FEF3C7,stroke:#D97706,color:#92400E
style F2 fill:#FEF3C7,stroke:#D97706,color:#92400E
style F3 fill:#FEF3C7,stroke:#D97706,color:#92400E
style ZIP fill:#EDE9FE,stroke:#7C3AED,color:#5B21B6
style LAN_BANNER fill:#ECFDF5,stroke:#10B981,stroke-width:2px,color:#064E3B
style LAN_MSG fill:#D1FAE5,stroke:#059669,color:#065F46
style SWITCH_BTN fill:#A7F3D0,stroke:#047857,color:#064E3BThe web UI is embedded directly into src/qrtunnel/server.py as Python multi-line strings. I kept the aesthetic dark and clean (#1a1a2e background, #16213e cards, and #4361ee accent buttons) so scanning the QR code in a dark room doesn’t blind you.
For file uploads, the page has an interactive file picker that updates the DOM with the selected filename and activates the upload button.
Fixing the Mobile Upload Hang
One of the most annoying quirks I ran into with mobile Safari and Chrome was that after a file upload finished, the browser kept spinning indefinitely. The upload was 100% completed on the server, but the phone never transitioned to the “Upload Success” page.
It turned out that HTTP/1.1 persistent connections were keeping the socket open, waiting to see if the browser wanted to push more data. By explicitly specifying Content-Length on the success HTML response and setting self.close_connection = True, the server immediately terminates the connection, forcing the mobile browser to render the success state instantly.
Smart Mode: The Dual-Stack LAN Discovery Trick
This is my favorite piece of engineering in the entire project.
When I am at home sitting on my couch, my phone and laptop are on the same 5 GHz Wi-Fi. Routing 4 GB of data out through an external SSH server in Europe or North America and back to my phone is terribly inefficient. Through the public tunnel, transfers crawl at 2 to 4 MB/s. Over local LAN, the same transfer clocks in at 60 to 80 MB/s.
But if I generate a QR code pointing only to my LAN IP (http://192.168.1.44:34567), it will fail the moment I am at a university, on an airport network with client isolation, or on cellular data. (By default, qrtunnel assigns a random ephemeral port between 20000 and 60000 to avoid local port conflicts, though you can specify one with -p <port> or the shorthand -<port> like -9000).
I wanted the best of both worlds: a single QR code that works anywhere, but automatically switches to LAN speeds when available.
The 43-Byte GIF Probe
Here is how Smart Mode works:
- The QR code always encodes the public tunnel URL. This guarantees that the phone can connect regardless of network topology.
- When the phone loads the page over the public tunnel, an inline JavaScript snippet attempts to load a lightweight 43-byte transparent GIF from the laptop’s LAN IP:
(function() {
var lanIp = "192.168.1.44";
var lanPort = 34567;
var lanUrl = "http://" + lanIp + ":" + lanPort;
if (lanIp && lanIp !== "None" && window.location.hostname !== lanIp) {
var img = new Image();
img.onload = function() {
// LAN reached! Silently switch to local link
setTimeout(function() {
window.location.href = lanUrl + window.location.pathname + window.location.search;
}, 100);
};
img.src = lanUrl + "/ping?t=" + Date.now();
}
})();
- On the laptop, the Python server responds to
/pingwith a cached transparent GIF:
def send_ping_gif(self):
gif = b"GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"
self.send_response(200)
self.send_header("Content-type", "image/gif")
self.send_header("Content-Length", len(gif))
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Private-Network", "true")
self.end_headers()
self.wfile.write(gif)
Notice that header: Access-Control-Allow-Private-Network: true. Modern Chromium browsers enforce Private Network Access (PNA) restrictions—if a public website tries to fetch resources from a private RFC 1918 IP address, the browser blocks it unless the server explicitly grants permission via PNA headers.
If the phone is on the same Wi-Fi, the /ping GIF loads in under 30 milliseconds, img.onload triggers, and the phone redirects to the LAN server. If the devices are on different networks, the image request quietly times out, and the user continues downloading over the public tunnel without interruption.
Taming the Terminal Interface Without Curses
I wanted an interactive TUI for selecting files with arrow keys, toggling selections with spacebar, and filtering filenames with /.
Standard Python TUI libraries like curses are notoriously painful to distribute. On Windows, curses is not included in standard Python, requiring users to install third-party wheels like windows-curses. Heavy libraries like textual or blessed introduce significant dependency trees.
I chose to build a lightweight, cross-platform keyboard reader from scratch in src/qrtunnel/keyboard.py.
flowchart TB
subgraph WIN["Windows Host Architecture"]
direction TB
W1["msvcrt.getch()"] --> W2["Detect 0x00 / 0xe0 Prefix"] --> W3["Map Virtual Arrow Codes"]
end
subgraph POSIX["Linux & macOS POSIX Architecture"]
direction TB
U1["termios.tcgetattr(fd)"] --> U2["tty.setcbreak(fd)
Preserves Terminal CR/LF"] --> U3["select.select([fd], 0.15s)
Non-Blocking Polling"]
U3 --> U4{"Read Byte Stream"}
U4 -- "ESC (0x1b)" --> U5["Parse CSI / SS3 Arrow Codes"]
U4 -- "Char" --> U6["Input Buffer
ENTER / SPACE / Chars"]
end
style WIN fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E3A8A
style POSIX fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#78350F
style W1 fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
style W2 fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
style W3 fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
style U1 fill:#FFFBEB,stroke:#F59E0B,color:#78350F
style U2 fill:#FFFBEB,stroke:#F59E0B,color:#78350F
style U3 fill:#FFFBEB,stroke:#F59E0B,color:#78350F
style U4 fill:#FEF3C7,stroke:#D97706,color:#78350F
style U5 fill:#FCE7F3,stroke:#DB2777,color:#831843
style U6 fill:#DCFCE7,stroke:#16A34A,color:#14532DOn Windows, msvcrt.getch() handles non-blocking keyboard reads cleanly. On Unix, I read directly from the standard input file descriptor using termios and select.
The Forensic Debugging Journey (The 5 Nightmare Bugs)
Every real software project has subtle, frustrating bugs that derail development. Here are the five hardest bugs I ran into while building qrtunnel, along with how I solved them.
+-----------------------------------------------------------------------------+
| THE NIGHTMARE BUGS |
+-----+-------------------------------+---------------------------------------+
| # | BUG SUMMARY | ROOT CAUSE |
+-----+-------------------------------+---------------------------------------+
| 1 | The Terminal Staircase Bug | Raw mode stripping CR/LF translation |
| 2 | The Docker Interface Hijack | gethostbyname picking docker0/tun0 |
| 3 | Werkzeug 3.0 Breakage | LimitedStream removed from upstream |
| 4 | The Multipart Filename Crash | Attribute mismatch in parser target |
| 5 | Windows Null Device Failure | /dev/null hardcoded on Windows SSH |
+-----+-------------------------------+---------------------------------------+
Bug 1: The Terminal “Staircase” Output Bug
- Symptom: Whenever a background thread printed a status message or log while the interactive TUI was running, the text printed in an awkward diagonal staircase across the terminal:
[*] HTTP server started [*] Connected via localhost.run [*] Ready for transfers - Root Cause: In my initial keyboard implementation on Unix, I put the terminal into raw mode using
tty.setraw(fd). Raw mode disables all terminal line discipline processing. Normally, the terminal driver translates a newline (\n, character 10) into carriage return + line feed (\r\n). In raw mode,\nsimply moves the cursor down one row without resetting the column to zero, causing text to step diagonally. - The Fix: I switched from
tty.setraw()totty.setcbreak(fd, termios.TCSADRAIN):In cbreak mode, keystrokes are delivered immediately without waiting for an Enter keypress, but standard output translation remains intact.# cbreak gives me unbuffered raw input without breaking output formatting old = termios.tcgetattr(fd) tty.setcbreak(fd, termios.TCSADRAIN)
Bug 2: The Docker Interface Hijack
- Symptom: Smart Mode repeatedly failed on my development laptop. When scanning the QR code, my phone tried to connect to
http://172.17.0.1:34567instead of my Wi-Fi IP, timing out every time. - Root Cause: My initial IP discovery logic called
socket.gethostbyname_ex(socket.gethostname()). On developer machines running Docker, the bridge interfacedocker0has an RFC 1918 private address (172.17.0.1). Because it was private and non-loopback, my code enthusiastically selected it as the primary LAN IP, ignoring my actual physical Wi-Fi adapter. - The Fix: I wrote a dedicated Linux interface parser in
src/qrtunnel/utils.pythat readsip -4 -o addr showand explicitly filters out virtual interfaces:If a physical interface likedef _get_linux_interface_ips(): # Exclude virtual bridges, docker, and tunnels ignored_prefixes = ("lo", "docker", "br-", "veth", "tun", "tap") # Prefer physical wireless (wl*) and ethernet (en*, eth*) interfaceswlan0oreth0is present, it is always preferred over virtual adapters.
Bug 3: The Silent Werkzeug 3.0 Upstream Breakage
- Symptom: After reinstalling dependencies on a new machine, file uploads failed with an immediate crash:
ImportError: cannot import name 'LimitedStream' from 'werkzeug.wsgi' - Root Cause: Werkzeug 3.0.0 completely removed
LimitedStreamfrom its public API. Thestreaming-form-dataparser relied on wrapped streams to prevent reads from exceedingContent-Length. Pinning an ancient Werkzeug version was not an acceptable long-term solution. - The Fix: I wrote a minimal, self-contained 25-line drop-in implementation of
LimitedStreamdirectly insidesrc/qrtunnel/streams.py:This removed the broken upstream dependency while keeping stream-capped uploads reliable.class LimitedStream: def __init__(self, stream, limit): self._stream = stream self._limit = limit self._pos = 0 def read(self, size=-1): if self._pos >= self._limit: return b"" if size < 0 or size > self._limit - self._pos: size = self._limit - self._pos data = self._stream.read(size) self._pos += len(data) return data
Bug 4: The Multipart Filename Attribute Crash
- Symptom: Uploads from certain mobile browsers resulted in a
400 Bad Request: File not found in form dataerror. - Root Cause: In earlier versions of
streaming-form-data, inspectinghasattr(file_target, "filename")was common, but in newer versions and edge cases with multipart parsing, the extracted filename is stored undermultipart_filename. If accessed via the wrong attribute name, it evaluated toNone. - The Fix: I updated the attribute extraction to safely look for
multipart_filename:if not getattr(file_target, "multipart_filename", None): self.send_error(400, "File not found in form data") return sanitized_filename = os.path.basename(unquote(file_target.multipart_filename))
Bug 5: The Windows OpenSSH /dev/null Crash
- Symptom: Running SSH mode on Windows threw an immediate error:
Can't open user-known-hosts file /dev/null: No such file or directory - Root Cause: The SSH command line had hardcoded
-o UserKnownHostsFile=/dev/null. While/dev/nullexists on Unix systems, Windows does not have a virtual filesystem path by that name; it uses theNULdevice. - The Fix: I added a simple platform check:
null_device = "NUL" if platform.system() == "Windows" else "/dev/null" cmd = [ "ssh", "-o", f"UserKnownHostsFile={null_device}", ... ]
Progress Throttling, ZIP Streaming & Checksum Integrity
Once the core file sharing was stable, I focused on usability and throughput.
Throttling Terminal I/O
I added real-time progress bars to downloads and uploads using carriage return overwriting (\r). But I quickly ran into a performance trap: updating the progress bar on every single chunk saturated standard output and created significant GIL contention in Python.
If you transfer 100,000 chunks over a high-speed LAN, calling print("\r...", end="") 100,000 times actually slows down the network transfer.
I introduced a 250-millisecond throttle:
def print_progress(self, label, transferred, total, force=False, last_update=0):
now = time.monotonic()
if not force and now - last_update < 0.25:
return last_update
print(f"\r[*] {label}: {format_progress(transferred, total)}", end="", flush=True)
return now
This keeps the terminal responsive and visual without degrading network throughput.
On-the-Fly ZIP Bundling
If I want to share 20 photos or an entire folder of documents, clicking 20 separate download links on a phone is tedious. In version 3.7.0, I implemented an in-memory dynamic ZIP archive generator:
def build_zip_archive(self):
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
for fp in self.file_paths:
path = Path(fp)
if path.is_dir():
for child in path.rglob("*"):
if child.is_file():
archive.write(child, path.name / child.relative_to(path))
elif path.is_file():
archive.write(path, path.name)
return buffer.getvalue()
If multiple files or folders are shared, the mobile page displays a “Download all as ZIP” button. Tapping it bundles the files into a single compressed stream.
Transfer Verification via SHA256
After every successful transfer, qrtunnel calculates and displays the SHA256 checksum on both ends:
def sha256_file(path, chunk_size=1024 * 1024):
digest = hashlib.sha256()
with open(path, "rb") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
digest.update(chunk)
return digest.hexdigest()
This guarantees byte-for-byte integrity and confirms that no data was truncated or corrupted during transmission.
Honest Benchmarks, Limitations & Pragmatic Trade-offs
I want to be completely realistic about where qrtunnel excels and where its boundaries are.
Transfer Speed Benchmarks
| Connection Mode | Transfer Method | Typical Throughput | 1.2 GB Video Transfer Time |
|---|---|---|---|
| Public Tunnel | SSH (localhost.run) | 1.8 – 3.5 MB/s | ~6.5 minutes |
| Public Tunnel | ngrok free tier | 2.5 – 5.0 MB/s | ~4.5 minutes |
| Local LAN | Python standard loop (f.read) | 32 – 45 MB/s | ~32 seconds |
| Local LAN (Linux) | Kernel zero-copy (os.sendfile) | 65 – 88 MB/s | ~16 seconds |
The difference between public tunneling and zero-copy LAN is massive. This is why the Smart Mode ping probe makes such a tangible difference in daily use.
Security Model Realities
- Bearer Links: The public URL generated by
localhost.runorngrokis a bearer token. Anyone with the active link can access the download or upload page while the session is running. It is meant for ad-hoc, short-lived convenience transfers, not for transmitting sensitive or regulated data. - Static LAN Passcode: For LAN transfers, I added a 6-digit OTP to prevent other devices on the same Wi-Fi from browsing shared files. However, that OTP is static for the duration of the process session. It does not invalidate after the first download (tracked in the repository issues).
- Tunnel Session Limits: Free tunnel services like
localhost.runcan experience occasional disconnects or session limits on long transfers. If a connection drops, restartingqrtunnelallocates a new port and URL.
Engineering Takeaways & What’s Next
Building qrtunnel taught me several practical systems lessons:
- Kernel zero-copy is remarkably accessible: Python often gets criticized for being slow, but features like
os.sendfilelet you tap directly into kernel performance with just a few lines of code. - Terminal I/O has real overhead: Writing unthrottled progress updates to stdout can silently bottleneck high-speed network sockets. Always rate-limit terminal refreshes.
- Smart client-side probing avoids hard decisions: Rather than forcing the user to guess whether their devices are on the same Wi-Fi subnet, a 43-byte image ping lets the browser make the routing decision automatically.
- Clean up subprocesses aggressively: If your application manages long-lived SSH or tunnel processes, implement explicit
atexithandlers and SIGINT traps. Otherwise, orphaned child processes will keep holding system ports hostage.
For future updates, I want to investigate UPnP and NAT-PMP port forwarding to allow direct public connections without third-party tunnel intermediaries, and add upload progress indicators to the mobile interface.
You can check out the complete source code on GitHub: AniruthKarthik/qrtunnel.