arblant
firefox-vpn-client / app / README.md
README.md198 lines · 10.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
# Firefox VPN app

The Flutter app: the shared UI for Windows, Linux, macOS, and Android. It holds no
VPN logic; it is a client of the core's dashboard API on loopback
(`core/doc/DASHBOARD.md`), authorized by the `X-Local-Token` header from
`local-api.token` in the data folder.

## Desktop model (Windows)

- The app starts the core as a child process, `core\firefox-vpn-core.exe -app -data-dir
  <data>` (in a `core` folder, so the app is the only program beside it), and stops it on quit (`POST /api/app/exit`, then closing its
  stdin). The core also exits when its stdin closes, so it never outlives
  the app, even after a crash. When a core already answers for the data
  folder, the app uses it and leaves it running.
- The close button asks whether to minimize to the tray or quit, with
  "Remember my choice"; Settings changes the choice later.
- One copy per data folder: launching the app again brings the running
  window up, also from the tray (a named mutex and a broadcast message in
  `windows/runner/main.cpp`).
- Start with Windows (Settings > General) is on by default: the app writes
  the current user's Run key to start itself with `--autostart`, which
  keeps the window in the tray. Each version that runs points the entry at
  itself. Copies started with `--data-dir` (development, tests) leave the
  Run key alone.
- The window opens centered, 820×680, without a maximize button.
- The tray icon and the close button are native code in
  `windows/runner/desktop_bridge.cpp` (method channel `app/desktop`), so the
  app needs no plugins.
- App preferences (language, theme, close button) are in
  `<data>/app-settings.json`; VPN settings belong to the core.

## Desktop model (Linux)

The same as on Windows, with the Linux names: the app is `firefox-vpn`
with `core/firefox-vpn-core`, and `linux/runner/desktop_bridge.cc` is the
native side of the same channel.

- The tray icon is an AppIndicator, loaded at run time
  (`libayatana-appindicator3` or `libappindicator3`); without it the app
  still runs, and minimizing to the tray minimizes the window instead. An
  AppIndicator has only a menu: its first entry (Open) also answers a middle
  click.
- Notifications go to the desktop's notification service over D-Bus; a
  click opens the window.
- Start at sign-in writes `~/.config/autostart/firefox-vpn-client.desktop`.
- One copy per user (GtkApplication `com.arman.firefoxvpn`); copies started
  with `--data-dir` run beside it.
- The core sets GNOME's proxy setting (gsettings `org.gnome.system.proxy`),
  which GNOME-based desktops and most browsers read.

## Desktop model (macOS)

The same again: `Firefox VPN.app` holds `firefox-vpn-core` beside its
executable in `Contents/MacOS`, and `DesktopBridge` in
`macos/Runner/MainFlutterWindow.swift` is the native side of the channel.

- The tray icon is a menu bar icon; a click opens its menu. While the window
  is hidden the app has no Dock icon. Opening the app again (Finder, Dock,
  Spotlight) brings the window up.
- Start at sign-in writes the LaunchAgent
  `~/Library/LaunchAgents/com.arman.firefoxvpn.plist`.
- No App Sandbox: the app starts its core and changes the proxy setting.
  It is signed ad hoc, not with an Apple Developer ID.
- The core sets the web, secure web, and SOCKS proxies of every enabled
  network service with `networksetup`, keeping each service's previous
  setting to restore.

The data folder (accounts, settings, logs) on Windows is `user-data` next
to the app (not `data`, which holds Flutter's own files). The installer puts
the app by default in `%LOCALAPPDATA%\Programs\Firefox VPN Client`, a folder
of the user's own; the first install lets the user choose another folder they
can write to. A new version replaces the app where it is, keeping
`user-data`; the portable zip holds a `Firefox VPN` folder to unpack over the
old one. On macOS the
data is in `~/Library/Application Support/Firefox VPN Client`, on Linux in
`~/.local/share/firefox-vpn-client`; a file named `portable` next to the app keeps
it in `user-data` beside it there too.

Arguments: `--data-dir=<path>` (overrides the above) and `--core=<path>`
(default: `firefox-vpn-core.exe` next to the app).

- Ports: the core's defaults are 10810 (SOCKS5 and HTTP proxy) and 10820
  (its API). When the API port is taken (the core exits with
  `app-error: dashboard_port_in_use <address>` on stderr), Windows keeps it
  for itself, or another program answers there, the app moves the API by
  itself to a free port above it (it edits `dashboard.listen` in
  config.json, since the core is not running); only this app uses that
  port. After three moves in a run it shows the problem and offers another
  port. The proxy port never moves on its own, since the user may have set
  it up in other programs: when it is taken, the status carries
  `code: proxy_port_in_use`, and when it cannot be used otherwise (such as
  a port Windows reserves) `code: start_failed` with its `address`; the home
  page says so and offers another port through the API. Settings changes
  either port at any time.

- The core starts with the proxy off; the round button on the home page
  connects and disconnects (`POST /api/power`), and Connect on startup
  (Settings) connects when the app starts. Whether Windows uses the proxy
  is a separate choice beside it, in v2rayN's words: Set system proxy
  ("System proxy") or Clear system proxy ("Proxy"). The tray menu is
  short: Open, Proxy service only, Set system proxy, Quit. Notifications
  show while the window is in the tray.
- Developer mode (Settings > General) adds the technical details: the
  connection test's stages and total, server addresses, and the Logs tab.
- About shows the versions and checks GitHub for a newer release
  (`githubRepo` in `lib/app_info.dart`, empty until the repository exists).
- Country flags are the core's SVG flags (flag-icons, MIT), drawn with
  flutter_svg.

## Android

The core runs in the app's process: `core/mobile` built by gomobile into
`android/app/libs/core.aar` (not in Git; see `core/doc/DEVELOPMENT.md`),
started by a foreground service (`CoreService.kt`) with a lasting
notification whose Stop action ends the service and the app. Its data is in
the app's private storage (`files/core`). The UI talks to it through the
same API on loopback (`lib/core/android_core.dart`, channel `app/android`).
Android is a VPN: the core runs with the proxy off while the UI is open
(settings and accounts work), and the home button, or the Quick Settings
tile (`QuickTile.kt`), turns it on. `CoreService.kt` is then a VpnService:
it asks for the VPN permission once, leaves the app itself out of the VPN
(so the core's own connections go straight out), and hands the TUN device
to the core (`mobile.StartTun`). The core's userspace stack
(`core/internal/tun`, gVisor) sends every TCP connection through its own
SOCKS5 proxy and answers DNS (UDP and TCP port 53) with fake addresses from
198.18.0.0/15 that stand for names, so the proxy gets the name and routes
by domain. Other UDP is refused (Mozilla's proxy carries TCP only), so
apps fall back to TCP; so is TCP to a port Mozilla's proxy refuses (it
accepts 80, 443, 8080, and 8443) unless the routing sends the destination
directly: refused at once, never tried and never sent out directly.
Settings, Routing, "Apps and the VPN" chooses which apps use it: all but
the checked ones, or only the checked ones. In the first mode the apps
on the routing region's list (hiddify/Android-GFW-Apps, GPL-3.0, so the
core downloads it with the routing lists rather than the app shipping it)
are left out too unless unchecked. The service follows Android's default
network (ConnectivityManager) and tells the core (`mobile.NetworkChanged`),
which reconnects to Mozilla at once on a new network. When
the proxy is shared with the LAN the home page shows the address other
devices use. The core trusts Android's updatable root store and two
bundled GlobalSign roots (`core/mobile/roots.go`), which Android 12's
system store lacks. Every app on a phone can reach loopback, so the core's
API answers only this app (the local token) and a browser the app signs in
(About, Web dashboard). Android's backup keeps the accounts and the settings
(`files/core`, so a new phone needs no new sign-in); the logs, the routing
lists, the caches, and the per-run token are left out
(`res/xml/backup_rules.xml`, `data_extraction_rules.xml`). Copying a data
folder into the external files dir (`import/`) works in debug builds only.
The phone's home page has its own dark design
(`_PhoneHome`), and phones get a bottom navigation bar.

Build: `flutter build apk --release --split-per-abi`. From Iran, Gradle
needs a proxy for Google's Maven repository (Google answers Java clients
there with 404), for example `JAVA_TOOL_OPTIONS="-Dhttps.proxyHost=127.0.0.1
-Dhttps.proxyPort=<port> -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=<port>"`.
`ndkVersion` names the NDK installed here.

## Languages and first run

English, Persian (right to left, Vazirmatn font under the SIL Open Font
License, `assets/fonts/OFL.txt`, dates in the Solar Hijri calendar),
Chinese, and Russian. Strings are in
`lib/l10n/app_*.arb`; `flutter gen-l10n` (or any build) generates
`lib/l10n/app_localizations*.dart`.

The first run shows a welcome page: language and country. The country is
the core's routing region (`routing.region`): Persian suggests Iran,
Chinese China, Russian Russia, and other languages the system's country.
Changing the language later offers the matching region.

## Icons

Every icon of the program comes from `tool/app_icon.png` (the fox shield,
1024 px, transparent): `python tool/app_icons.py` (Pillow) writes the
Android launcher icon (adaptive, over a blue gradient, and the older PNG),
the Windows `app_icon.ico`, the web dashboard's `icon.png`, and the core's
`appicon.ico` (its executable). The same script draws
the tray icons, which show the connection state and are not made from the
art (16 px cannot show it): a disk in the state's color with the logo's
shield in white, for the app's tray (`assets/tray`) and the core's
(`core/internal/desktop/tray`).

## Development

- Build a release folder with the core: `scripts\build-windows.ps1` from
  the repository root (output in `dist\Firefox-VPN-<version>\`); on Linux,
  `scripts/build-linux.sh` (output in `dist/Firefox-VPN-<version>-Linux-<amd64|arm64>.tar.gz`);
  on macOS, `scripts/build-macos.sh` (output in `dist/Firefox-VPN-<version>-macOS.dmg`).
- Work on the UI without a real core or accounts:

  ```
  dart run tool/fake_core.dart <data-dir> [port]
  flutter run -d windows --dart-entrypoint-args=--data-dir=<data-dir>
  ```

- Run a development core against a separate data folder whose
  `config.json` uses other ports than a core already running on this
  machine, and pass it with `--core=<path to the built core>`.
- Checks: `flutter analyze` and `flutter test`.