Mobile Client
Mobile Client
Section titled “Mobile Client”The Bifrost Mobile Client is a cross-platform application built with React Native and Expo, providing VPN management on iOS and Android devices.
Important — the mobile app is a remote control, not an on-device tunnel. The Expo app manages a Bifrost client running elsewhere on your network over the REST API (see Connecting to Client). It does not establish an encrypted tunnel on the phone itself. The native
PacketTunnelProvider(iOS) andBifrostVpnService(Android) files in the repository are non-functional placeholders: they are unbuildable skeletons and the current on-device path would forward raw packets without a WireGuard/Noise handshake (i.e. cleartext, not secure). For that reason the native VPN path is gated off and must not be relied on for confidentiality. A real on-device WireGuard tunnel is planned future work.
Features
Section titled “Features”- Home Screen: VPN connection status and quick toggle
- Servers Screen: Server selection with latency indicators
- Stats Screen: Real-time traffic statistics and connection details
- Settings Screen: Configuration management and preferences
Installation
Section titled “Installation”App Stores
Section titled “App Stores”- iOS: App Store (coming soon)
- Android: Google Play (coming soon)
Building from Source
Section titled “Building from Source”Prerequisites
Section titled “Prerequisites”- Node.js 18+
- npm or yarn
- Expo CLI
- For iOS: Xcode 14+ (macOS only)
- For Android: Android Studio with SDK
# Clone repositorygit clone https://github.com/rennerdo30/bifrost-proxy.gitcd bifrost-proxy/mobile
# Install dependenciesnpm install
# Start development servernpx expo startRunning on Device/Simulator
Section titled “Running on Device/Simulator”# iOS Simulator (macOS only)npx expo run:ios
# Android Emulatornpx expo run:android
# On physical devicenpx expo start --tunnel# Then scan QR code with Expo Go appBuilding for Production
Section titled “Building for Production”Using EAS Build (recommended):
# Install EAS CLInpm install -g eas-cli
# Login to Expo accounteas login
# Link this project to your Expo account (writes extra.eas.projectId).# The repository ships unlinked, so this is required before the first build.eas init
# Build for iOSeas build --platform ios
# Build for Androideas build --platform android
# Build for botheas build --platform allScreens
Section titled “Screens”Home Screen
Section titled “Home Screen”The main dashboard showing connection status:
┌─────────────────────────────────────┐│ Bifrost VPN │├─────────────────────────────────────┤│ ││ ┌───────────────┐ ││ │ │ ││ │ 🔒 │ ││ │ Connected │ ││ │ │ ││ └───────────────┘ ││ ││ [ Disconnect ] ││ ││ ┌─────────────┐ ┌─────────────┐ ││ │ ↑ Sent │ │ ↓ Received │ ││ │ 1.2 GB │ │ 3.4 GB │ ││ └─────────────┘ └─────────────┘ ││ ││ Server: us-west.example.com ││ Connected: 2h 34m ││ │├─────────────────────────────────────┤│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │└─────────────────────────────────────┘Servers Screen
Section titled “Servers Screen”Server selection with status indicators:
┌─────────────────────────────────────┐│ Available Servers │├─────────────────────────────────────┤│ ││ ┌─────────────────────────────────┐││ │ 🟢 US West ✓ 45ms │││ │ us-west.example.com │││ │ HTTP │││ └─────────────────────────────────┘││ ││ ┌─────────────────────────────────┐││ │ 🟢 Europe 120ms │││ │ eu.example.com │││ │ HTTP │││ └─────────────────────────────────┘││ ││ ┌─────────────────────────────────┐││ │ 🟡 Asia Pacific 200ms │││ │ asia.example.com │││ │ SOCKS5 │││ └─────────────────────────────────┘││ ││ ┌─────────────────────────────────┐││ │ 🔴 Development offline │││ │ dev.example.com │││ │ HTTP │││ └─────────────────────────────────┘││ │├─────────────────────────────────────┤│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │└─────────────────────────────────────┘Status indicators:
- 🟢 Online - Server available
- 🟡 Busy - High load
- 🔴 Offline - Server unavailable
Stats Screen
Section titled “Stats Screen”Detailed connection statistics:
┌─────────────────────────────────────┐│ Connection Stats │├─────────────────────────────────────┤│ ││ ┌─────────────┐ ┌─────────────┐ ││ │ ↑ Sent │ │ ↓ Received │ ││ │ 1.2 GB │ │ 3.4 GB │ ││ └─────────────┘ └─────────────┘ ││ ││ Current Session ││ ───────────────────────────────── ││ Duration 2h 34m ││ Total Data 4.6 GB ││ VPN Status connected ││ ││ Traffic ││ ───────────────────────────────── ││ Packets Sent 1,204,551 ││ Packets Received 3,410,882 ││ Active Conns 42 ││ ││ Split Tunneling ││ ───────────────────────────────── ││ Tunneled 1,204 ││ Bypassed 318 ││ ││ DNS ││ ───────────────────────────────── ││ Queries 8,120 ││ Cache Hits 6,904 ││ │├─────────────────────────────────────┤│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │└─────────────────────────────────────┘Note: Every row above describes the remote Bifrost client the app controls, and each one maps to a field of
vpn.VPNStatsas returned byGET /api/v1/vpn/status. None of it indicates an encrypted tunnel running on the phone itself — see the warning at the top of this page.The API exposes no tunnel protocol, encryption suite, MTU, local IP, gateway, DNS server list or interface name, so the Stats screen does not claim to show them. Session duration comes from
uptime(a Gotime.Duration, i.e. nanoseconds), not from a connection timestamp.
Settings Screen
Section titled “Settings Screen”Configuration and preferences:
┌─────────────────────────────────────┐│ Settings │├─────────────────────────────────────┤│ ││ Connection ││ ───────────────────────────────── ││ Client auto-connect [ 🔘 ] ││ Connect when the client starts ││ ││ VPN Mode [🔘 ] ││ Route remote-client traffic ││ ││ Split Tunneling ││ Manage app/domain/IP rules ││ [Configure →] ││ ││ Bifrost Client ││ ───────────────────────────────── ││ ┌─────────────────────────────────┐││ │ 192.168.1.20:7383 [Save] │││ └─────────────────────────────────┘││ Plain host:port uses HTTP. ││ Prefix https:// for TLS. ││ ┌─────────────────────────────────┐││ │ •••••••••••••••••••••• [Save] │││ └─────────────────────────────────┘││ A token is stored on this device. ││ [Clear] ││ API Endpoint http://…:7383/api/v1 ││ ││ Notifications ││ ───────────────────────────────── ││ Client Notifications [🔘 ] ││ Desktop alerts on the client ││ ││ Data & Privacy ││ ───────────────────────────────── ││ [ Clear Cached Data ] ││ ││ About ││ ───────────────────────────────── ││ Version 1.0.0 ││ Server Status Connected ││ VPN Status connected ││ │├─────────────────────────────────────┤│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │└─────────────────────────────────────┘The Client auto-connect and Client Notifications switches write the
remote client’s tray configuration (tray.auto_connect,
tray.show_notifications). They are labeled as remote settings because they
have no effect on the phone: the app has no launch-time auto-connect and no push
notifications. There is no Kill Switch — that would require a tunnel on the
device, which this app deliberately does not implement.
Split tunneling
Section titled “Split tunneling”The Split Tunneling screen edits the remote client’s policy, and the remote client is the source of truth:
- On open (and on pull-to-refresh) the app reads
GET /api/v1/vpn/split/rulesand reconciles it with what the phone remembers. Rules the client currently enforces show as enabled; rules added from another client are imported rather than ignored. - A rule you remember locally but the client no longer enforces stays visible and disabled, so parking a rule for later does not lose it.
- Toggling a rule off removes it from the client but keeps it on the phone; the trash action removes it from both.
- If the client cannot be reached, the screen says so and falls back to the locally remembered rules instead of showing an empty policy.
Before the VPN is enabled, the app replaces the client’s active rules with the enabled portion of the local policy: stale remote rules are removed, missing ones added, and changed app paths re-created. This is fail-closed — if any single operation fails, the VPN is not enabled and the error names the rule and operation that failed. The previous behaviour only ever added rules, so a rule disabled on the phone stayed in force on the client.
Languages
Section titled “Languages”The interface ships English and German. The language follows the device locale
(a locale starting with de selects German; everything else falls back to
English). Counts and durations use locale-aware formatting, so a German device
reads 1.234 where an English one reads 1,234. Strings live in
mobile/src/i18n.ts; the catalogue is typed, so a key missing a German
translation fails the build rather than leaking English text.
Configuration
Section titled “Configuration”The mobile client connects to a Bifrost client running on your network. Configure its address, and an API token if the client requires one, in Settings.
Connecting to Client
Section titled “Connecting to Client”- Start the Bifrost client on your computer/server
- Note the API address (default:
192.168.x.x:7383) - Enter this address in the mobile app Settings
A bare host:port is reached over plain HTTP. Prefix the address with
https:// when the client sits behind TLS; the implicit port 443 may then be
omitted. IPv6 literals must be bracketed, e.g. [fd00::1]:7383.
Authentication
Section titled “Authentication”When the client config sets api.token, every request must carry it as a bearer
credential or the client answers 401 Unauthorized. Enter the token in the
Bifrost Client section of Settings; it is stored on the device with
AsyncStorage, sent as Authorization: Bearer <token>, never displayed again
after saving, and never written to a log. Use Clear to remove it.
CSRF protection
Section titled “CSRF protection”The client API rejects every POST, PUT, DELETE and PATCH that does not
carry X-Requested-With: XMLHttpRequest with 403. The app sends this header on
every request, matching both web dashboards.
API Configuration
Section titled “API Configuration”The mobile app uses these API endpoints:
| Endpoint | Description |
|---|---|
GET /api/v1/health |
Reachability probe used by the Save buttons |
GET /api/v1/status |
Client status |
GET /api/v1/vpn/status |
VPN connection status (vpn.VPNStats) |
POST /api/v1/vpn/enable |
Enable VPN |
POST /api/v1/vpn/disable |
Disable VPN |
GET /api/v1/servers |
The client’s servers: list (credentials excluded) |
POST /api/v1/server/select |
Select server, body {"server": "<name>"}; switches the client’s live upstream connection and persists the choice |
GET /api/v1/config |
Get configuration |
PUT /api/v1/config |
Update configuration |
GET /api/v1/vpn/split/rules |
Split tunnel rules |
PUT /api/v1/vpn/split/mode |
Set split tunnel mode |
POST/DELETE /api/v1/vpn/split/{apps,domains,ips} |
Split tunnel rule edits |
DELETE /api/v1/debug/entries |
Clear cached debug entries |
Servers are identified by name: ServerInfo carries
name, address, protocol, is_default, latency_ms and status, and has
no id field.
Development
Section titled “Development”Project Structure
Section titled “Project Structure”mobile/├── src/│ ├── components/ # Reusable UI components│ ├── screens/ # Screen components│ │ ├── HomeScreen.tsx│ │ ├── ServersScreen.tsx│ │ ├── StatsScreen.tsx│ │ └── SettingsScreen.tsx│ ├── services/ # API and utilities│ │ └── api.ts│ └── App.tsx # Main app component├── app.json # Expo configuration├── package.json└── tsconfig.jsonTechnology Stack
Section titled “Technology Stack”- React Native: Cross-platform UI framework
- Expo: Development and build tooling
- React Query: Data fetching and caching
- React Navigation: Screen navigation
Adding a New Screen
Section titled “Adding a New Screen”- Create screen component in
src/screens/ - Add to navigation in
App.tsx - Add tab bar icon if needed
API Integration
Section titled “API Integration”The app uses React Query for data fetching:
import { useQuery, useMutation } from '@tanstack/react-query'import { api } from '../services/api'
function MyScreen() { const { data, isLoading, error } = useQuery({ queryKey: ['vpn-status'], queryFn: api.getVPNStatus, refetchInterval: 3000, })
const mutation = useMutation({ mutationFn: api.enableVPN, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['vpn-status'] }) }, })
// ...}Accessibility
Section titled “Accessibility”The Bifrost Mobile Client is designed with accessibility in mind, supporting both VoiceOver (iOS) and TalkBack (Android) screen readers.
Supported Accessibility Features
Section titled “Supported Accessibility Features”| Feature | Support |
|---|---|
| Screen Reader | Supported across all five screens |
| Accessibility Labels | Interactive elements are labelled, including the split-tunnel rule switches; toasts announce as alerts |
| Accessibility Roles | Semantic roles for buttons, tabs, switches |
| Accessibility States | Disabled, selected, checked states |
| Accessibility Hints | Context-aware action hints |
VoiceOver Testing (iOS)
Section titled “VoiceOver Testing (iOS)”How to Test
Section titled “How to Test”- Enable VoiceOver: Settings > Accessibility > VoiceOver
- Navigate using swipe gestures
- Double-tap to activate
- Use two-finger scroll for lists
Expected Behavior
Section titled “Expected Behavior”Home Screen:
- Connect/Disconnect button announces: “Connect to VPN, button” or “Disconnect from VPN, button”
- Status text announces: “Connection status: Protected” or similar
- Error alerts are announced immediately with role “alert”
- Upload/Download cards announce: “Upload: 1.2 GB” and “Download: 3.4 GB”
Servers Screen:
- Server items announce full context: “Server Name, address, protocol, status, selected/not selected”
- Offline servers announce disabled state
- Pull-to-refresh announces “Pull to refresh server list”
Stats Screen:
- All stat rows announce label and value pairs: “Duration: 2h 34m”
- StatusCard components announce: “Total Sent: 1.2 GB”, “Total Received: 3.4 GB”
Settings Screen:
- Switch controls announce their label and current state
- Configure button announces: “Configure split tunneling, button”
- Text input announces: “Server address, Enter the server address in host:port format”
Tab Navigation:
- Tab icons announce: “Home tab, selected” or “Settings tab”
- Uses proper tab role with selected state
TalkBack Testing (Android)
Section titled “TalkBack Testing (Android)”How to Test
Section titled “How to Test”- Enable TalkBack: Settings > Accessibility > TalkBack
- Navigate by swiping left/right
- Double-tap to activate
- Use two-finger scroll for content
Expected Behavior
Section titled “Expected Behavior”All behaviors match VoiceOver testing above with Android-specific variations:
importantForAccessibility="no"hides decorative elementsimportantForAccessibility="no-hide-descendants"groups child elements- Swipe gestures map to Android TalkBack conventions
- Focus indicators appear on currently selected element
Accessibility Implementation Details
Section titled “Accessibility Implementation Details”The following accessibility props are used throughout the app:
// Button example<TouchableOpacity accessibilityRole="button" accessibilityLabel="Connect to VPN" accessibilityHint="Double tap to establish VPN connection" accessibilityState={{ disabled: isLoading }}>
// Switch example<Switch accessibilityLabel="Client auto-connect toggle" accessibilityHint="When enabled, the Bifrost client connects automatically on startup" accessibilityState={{ checked: isEnabled }}/>
// Grouped content example<View accessible={true} accessibilityRole="summary" accessibilityLabel={`${title}: ${value}`}> <Text importantForAccessibility="no">{title}</Text> <Text importantForAccessibility="no">{value}</Text></View>Known Accessibility Considerations
Section titled “Known Accessibility Considerations”- Emoji Icons: Tab bar uses emoji icons which are hidden from screen readers; proper labels are provided instead
- Pull-to-Refresh: Custom accessibility labels describe the refresh action
- Dynamic Content: Status updates may not be immediately announced; users should navigate to the status area to hear updates
- Error States: Error messages use the
alertrole for immediate screen reader announcement
Troubleshooting
Section titled “Troubleshooting”Cannot Connect to Client
Section titled “Cannot Connect to Client”- Verify client is running and API is enabled
- Check network connectivity (same network)
- Ensure correct IP address and port
- Check firewall settings on client machine
App Crashes on Start
Section titled “App Crashes on Start”- Clear app data and reinstall
- Check for conflicting VPN apps
- Update to latest version
High Battery Usage
Section titled “High Battery Usage”- Increase refetch intervals in settings
- Disable notifications if not needed
- Disconnect when not in use
Stats Not Updating
Section titled “Stats Not Updating”- Check network connection
- Verify client is running
- Pull down to refresh manually