Skip to content

feat: local mobile access over a trusted home network (LAN mode) - #52

Merged
quangshuynh merged 5 commits into
mainfrom
feat/local-mobile-access
Sep 27, 2026
Merged

quangshuynh merged 5 commits into
mainfrom
feat/local-mobile-access

Conversation

@quangshuynh

Copy link
Copy Markdown
Owner

Summary

Use Flipper from a phone on the same trusted Wi-Fi while it keeps running on your own computer with its existing SQLite database and attachments. Nothing is deployed and nothing is exposed to the Internet.

python main.py web lan            # 0.0.0.0:8000, sign-in required
python main.py web lan --port 8123 --bind 192.168.1.23

Audit findings (before implementation)

  • There was no web CLI command. uvicorn web.app:app --reload binds 127.0.0.1:8000. Only the container binds 0.0.0.0, and it forces hosted mode.
  • Local mode without a password already failed closed: non-loopback peers get 403 and non-loopback hosts get 400.
  • Local mode with a password could reach a LAN via FLIPPER_ALLOWED_HOSTS, but it accepted any peer address, including public ones. It also needed manual uvicorn flags and broke when DHCP changed the IP.
  • CSRF derives the expected origin from the validated Host outside hosted mode, so LAN works without weakening it. LAN over plain HTTP requires non-Secure cookies. HTTPS isn't required on a trusted LAN, though traffic is plaintext on that network.
  • The CSP default-src 'none' would block a manifest.
  • Seller OAuth has no web callback, so the serving address can't affect it.

Changes

  • lan security mode (web/security.py):
    • Reuses the existing hash, sessions, CSRF checks, throttle and headers.
    • Requires both the hash and a session secret, and rejects FLIPPER_PUBLIC_ORIGIN.
    • Serves only loopback, RFC 1918, link-local and IPv6 unique-local peers; everything else gets 403, including 100.64/10.
    • Also accepts any private IP literal as Host. Literals can't be DNS-rebound, and the URL survives DHCP changes.
    • Local and hosted behavior are unchanged.
  • web/lan.py + main.py web lan:
    • Requires FLIPPER_PASSWORD_HASH.
    • Generates an in-memory per-process session secret when none is configured; it is never printed or written.
    • Refuses hosted configuration and resolves storage exactly as the web app does (no --database).
    • Pre-checks the port. On Windows the check uses SO_EXCLUSIVEADDRUSE, so it also detects a loopback-only listener.
    • Runs one Uvicorn process with proxy_headers=False and no Server header.
    • Prints the local and phone URLs (default-route private address, or fallback addresses, or ipconfig guidance) plus the DB path and safety warnings.
    • Address discovery sends no packets; there's no scanning and no firewall or router changes.
  • Home screen: minimal same-origin manifest.webmanifest (public, since browsers fetch it without cookies), apple-touch-icon, and CSP manifest-src 'self'. No service worker; pages stay no-store.
  • Mobile fix: long unbroken deal titles widened /deals and deal detail to ~496px at 375px. This was pre-existing and found during validation.
  • Docs: new docs/guides/phone-access.md, cross-linked from README, web security, deployment, CLI, configuration, troubleshooting, architecture and getting started.

No schema change (v14). No eBay behavior change.

Validation

  • pytest: 891 passed, 4 skipped (baseline on main: 802 passed, 4 skipped). New tests/test_web_lan.py has 89 tests.
  • ruff check . and ruff format --check . clean. mkdocs build --strict clean. git diff --check clean.
  • Manual validation with synthetic data only:
    • LAN server started; LAN-IP checks for redirect, host rejection, CSRF and 401 all behaved correctly.
    • Port conflict and invalid-config failures produced clear messages.
    • At 375/390/430 widths, 10 pages had no page-level overflow and inputs were at least 16px.
    • Login → mutation → restart persistence → logout all worked.
    • The ordinary dev server stays on loopback.
    • Backup create/verify/fingerprint still work.
  • No real phone was tested. The real database was untouched (checksum verified), no live eBay calls were made, and nothing was deployed.

FLIPPER_WEB_SECURITY_MODE=lan reuses the single-user boundary (owner password hash, signed
sessions, same-origin checks, throttling, headers) and adds two rules: the TCP peer must be on a
loopback/private/link-local network, and the Host may be any private IP literal so the phone URL
survives DHCP changes without admitting DNS-rebinding names. The mode refuses to start without
both FLIPPER_PASSWORD_HASH and FLIPPER_SESSION_SECRET and rejects FLIPPER_PUBLIC_ORIGIN.
Local and hosted behavior is unchanged.
LAN exposure is an explicit command; `uvicorn web.app:app` and the container are unchanged. The
command requires FLIPPER_PASSWORD_HASH, generates an in-memory session secret when none is
configured (never printed or written), refuses hosted configuration, resolves storage exactly as
the web app does, checks the port before serving, and runs one Uvicorn process on 0.0.0.0:8000
(or --port/--bind) without trusting forwarded headers. The startup summary shows the local and
phone URLs (default-route private address, else a short fallback list or ipconfig guidance), the
database path, and the private-network-only warnings; it never shows secrets. Discovery sends no
packets and changes no firewall, router, or interface settings.
The login and app pages link a same-origin manifest (name, standalone display, existing Flipper
logo as icon) and an apple-touch-icon. The manifest is a public login asset because browsers fetch
it without cookies; it contains no data. CSP gains only manifest-src 'self'. No service worker is
registered, so no authenticated or accounting page is cached on the phone.
Deals cards and the deal-detail hero sit in single-column grid tracks on phones, where
overflow-wrap:break-word does not reduce min-content width, so a long unbroken token (serial
number, model string) widened the page to ~500px at a 375px viewport. Add both headings to the
existing overflow-wrap:anywhere list. Found while validating LAN phone access.
New docs/guides/phone-access.md covers setup, the startup summary, finding the private IP,
home-screen use, the LAN security model, storage resolution, eBay behavior, troubleshooting,
future Tailscale compatibility, and the pre-implementation network audit. Web security,
configuration, CLI, deployment, troubleshooting, architecture, README, getting started, and
.env.example now describe the lan mode and link the guide.
@gitguardian

gitguardian Bot commented Sep 27, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
37670708 Triggered Generic High Entropy Secret 6889ffd tests/test_web_lan.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@quangshuynh
quangshuynh merged commit 9c29b63 into main Sep 27, 2026
3 checks passed
@quangshuynh
quangshuynh deleted the feat/local-mobile-access branch September 27, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant