Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 4, 2026

DuckDuckGo's image proxy returns HTTP 404 for formats it can't decode (SVG, some WebP). This replaces it with an in-app proxy using Cloudflare WARP over WireGuard.

New Rust Crate: letterbox-proxy

  • WARP Provisioning: Per-user Cloudflare identity creation via api.cloudflareclient.com, X25519 keypair generation, credential persistence
  • WireGuard Transport: boringtun-based encryption, UDP socket handling, handshake/keepalive management
  • TCP/IP Stack: smoltcp userspace networking, virtual device bridging to WireGuard
  • HTTP Client: Privacy-preserving fetch with cookie stripping, referrer blocking, content-type validation, all image formats supported
  • FFI API: proxy_init, proxy_fetch_image, proxy_fetch_images_batch, proxy_status, proxy_shutdown, proxy_clear_cache

Kotlin Integration

  • ImageProxyService: Coroutine-based wrapper for Rust FFI
  • UserPreferencesRepository: Added ProxyMode enum (WARP/DUCKDUCKGO/DIRECT), Cloudflare consent preference
  • UniFFI bindings in org.joefang.letterbox.ffi.proxy

Usage

val service = ImageProxyService.getInstance(context)
service.initialize()

when (val result = service.fetchImage("https://example.com/logo.svg")) {
    is ImageFetchResult.Success -> imageView.load(result.data)
    is ImageFetchResult.Error -> Log.e(TAG, result.message)
}

Documentation

  • docs/image-proxy-design.md: Architecture, data flow, component design
  • docs/remote-images.md: Updated to describe both proxy modes

Testing

77 tests (41 proxy + 36 core), covering provisioning API serialization, WireGuard transport, smoltcp device behavior, HTTP content-type validation.

Original prompt

The DuckDuckGo image proxy is broken for many image files like SVG. If the server cannot decode a file, it returns HTTP 404. I want you to revamp the whole "image proxy" design where you run a local WireGuard userspace implementation with Cloudflare Warp account that are created per-user during the first time the user clicks "load image" through proxy and persisted thereafter based on the provided Cloudflare API design. Atop of the WG layer, you would have a userspace TCP/IP implementation and then you would have the typical HTTP library atop. You would expose an image proxy API to the Kotlin layer through the FFI interface that accepts an URL and you would design the API to allow for maximum parallelism since an email can contain many small images and you want to avoid one image blocking the rest of them. Follow best practices in both Kotlin and Rust to write efficient, high performance code. Write idiomatic patterns with comprehensive test (unit test+integration+end-to-end) and documentation coverage and create a document to record the design decisions you made while implementing the new image proxy architecture. ===design docs=== This can be built without running your own proxy infrastructure by embedding a Rust “fetcher” that (1) provisions a per-user Cloudflare WARP WireGuard identity and (2) routes only image HTTP(S) requests through an in-process userspace TCP/IP stack (smoltcp) carried over a WireGuard engine (Mullvad’s GotaTun). The key constraint is that WireGuard libraries implement the tunnel protocol but not the TCP/IP stack, so smoltcp (or equivalent) is what lets you originate TCP connections without a TUN/VPN service.[1][2][3]

Architecture overview

  • Use Mullvad’s GotaTun as the embedded userspace WireGuard implementation (it is a Rust WireGuard implementation and a fork of Cloudflare’s BoringTun).[4][3]
  • Use smoltcp as the embedded TCP/IP stack; it is an event-driven standalone TCP/IP stack designed for constrained/bare-metal-like environments, and it includes example client code such as an HTTP client example.[2][5]
  • The app never exposes a local HTTP/SOCKS proxy port; instead, the Kotlin/Swift UI calls into a Rust FFI that returns image bytes (or a stream) fetched through the tunnel.

Cloudflare WARP provisioning

Cloudflare WARP account/device provisioning is done via HTTPS calls to api.cloudflareclient.com (the same endpoint family used by wgcf), and Cloudflare’s docs note the WARP client uses a standard HTTPS connection outside the tunnel for operations like registration/settings changes.[6][7][8]

Data model (persisted per user)

Persist the minimum you need to recreate the tunnel deterministically:

  • account_id (device id), access_token, private_key, license_key (matches the script’s AccountData).
  • Latest fetched server config: IPv4/IPv6 interface addresses, peer public key, endpoint host/IPs, and warp_enabled (matches the script’s ConfigurationData).
    Store these in platform secure storage (Keychain/Keystore) and keep a small JSON mirror only if needed for debugging/restore.

Provisioning flow (mirrors your script)

  1. Register (create identity)
  • Generate a WireGuard keypair locally (X25519), then POST to /{api_version}/reg with JSON including the public key and a “tos” timestamp (as your script does).
  • The wgcf tool documents that it can “Register new account” and uses https://api.cloudflareclient.com + an API version constant to do so.[8][6]
  • Persist returned id and token plus your locally generated private key (the server never needs your private key).
  1. Fetch configuration
  • GET /{api_version}/reg/{account_id} with Authorization: Bearer {token} and parse:
    • Interface addresses (IPv4/IPv6).
    • First peer’s public_key and endpoint (host + v4/v6).
  • wgcf’s Cloudflare package API indicates this API base/version scheme and is widely used to retrieve the profile material needed to form a WireGuard configuration.[6][8]
  1. Enable WARP
  • If warp_enabled is false, PATCH /{api_version}/reg/{account_id} with {"warp_enabled": true} (same as your script).
  1. (Optional) license key binding / device activation
  • If you support WARP+ license binding, wgcf explicitly supports “Change license key to use existing Warp+ subscription,” so the design can include a PUT to the account endpoint to bind the license key (as your script does).[6]
  • If you need device activation state, your script’s “devices” endpoint logic can be replicated, but consider keeping this out of the hot path (do it only on initial setup or troubleshooting).

Legal/consent UI requirement

  • Show a non-blocking inline note near the “Enable Image Privacy Proxy” toggle: “Images are fetched through Cloudflare infrastructure; by enabling this, you agree to Cloudflare legal terms.”
  • Link directly to Cloudflare WARP client legal resources (which list the Application Terms of Service and related legal links).[9]

Embedded tunnel + smolt...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Revamp image proxy design with WireGuard and Cloudflare API Revamp image proxy to use Cloudflare WARP with WireGuard userspace implementation Jan 4, 2026
Copilot AI requested a review from MinecraftFuns January 4, 2026 02:57
@MinecraftFuns MinecraftFuns marked this pull request as ready for review January 5, 2026 02:27
@MinecraftFuns MinecraftFuns merged commit bcdb83a into main Jan 5, 2026
4 checks passed
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.

2 participants