Проблема: реальное устройство не видит localhost

При разработке мобильного приложения API-сервер обычно работает на localhost:8080. Эмулятор iOS или Android может обращаться к нему напрямую, но настоящий телефон — нет. localhost указывает на само устройство, а не на ваш компьютер. Это значит, что для тестирования на реальном iPhone или Android вам нужен публичный URL, ведущий на ваш локальный сервер. Именно эту задачу решает туннелирование.

Типичный сценарий: вы разрабатываете iOS-приложение с бэкендом на Go, Python или Node.js. В симуляторе всё работает, но на настоящем iPhone приложение получает ошибку Could not connect to the server. Причина проста — телефон не знает, что такое localhost:8080 вашего Mac. Он обращается к своему собственному loopback-адресу и не находит там ничего.

Классический обходной путь — использовать IP-адрес компьютера в локальной сети (192.168.1.42:8080). Но у этого подхода три проблемы:

  • Нет HTTPS — iOS App Transport Security блокирует HTTP-запросы, придётся добавлять исключения в Info.plist.
  • Привязка к сети — работает только если телефон и компьютер в одной Wi-Fi сети. В кафе, дома или через мобильный интернет — не работает.
  • Ручная настройка — IP-адрес меняется при смене сети, приходится обновлять конфигурацию вручную.

Почему эмулятора недостаточно

Эмулятор — удобный инструмент для повседневной разработки, но он не заменяет тестирование на реальном устройстве. Критические различия проявляются в производительности, работе с оборудованием и поведении ОС — всё это невозможно воспроизвести в симуляторе.

  • Производительность — эмулятор работает на мощном компьютере и не отражает реальные задержки, FPS и расход памяти на бюджетном Android-телефоне.
  • Жесты и тач — мультитач, свайпы, 3D Touch / Haptic Touch, жесты навигации ведут себя иначе на реальном экране.
  • Камера и сенсоры — сканирование QR-кодов, ARKit/ARCore, акселерометр, гироскоп, Face ID / Touch ID работают только на устройстве.
  • Push-уведомления — APNs (Apple Push Notification service) не работает в симуляторе iOS. Для тестирования пушей нужен реальный iPhone.
  • Сеть — реальное устройство работает через Wi-Fi или мобильный интернет с задержками и потерей пакетов. Эмулятор использует сеть компьютера напрямую.

Для полноценного тестирования нужны три вещи: локальный API, реальное устройство и способ связать их. Третий пункт – как раз задача туннеля.

Решение: fxTunnel даёт вашему API публичный HTTPS-адрес

fxTunnel создаёт публичный URL для вашего localhost одной командой. Адрес работает по HTTPS с валидным сертификатом — это важно для iOS, где App Transport Security блокирует незащищённые соединения. Телефон обращается к https://my-api.fxtun.dev, запрос проходит через сервер туннеля и попадает на ваш localhost:8080.

Быстрый старт

# Установка fxTunnel
curl -fsSL https://fxtun.dev/install.sh | bash

# Запуск API-сервера (пример на Go)
go run main.go
# → API listening on :8080

# Создание туннеля
fxtunnel http 8080

Вы получите публичный URL:

fxTunnel v1.x — tunnel is active
Public URL:  https://my-api.fxtun.dev
Forwarding:  https://my-api.fxtun.dev → http://localhost:8080

Теперь замените http://localhost:8080 на https://my-api.fxtun.dev в конфигурации мобильного приложения — и реальное устройство получит доступ к вашему API.

Настройка для iOS (Xcode + локальный API + туннель)

При разработке iOS-приложения в Xcode базовый URL API обычно хранится в конфигурации или файле Info.plist. Для переключения между localhost и туннелем достаточно вынести URL в переменную среды или конфигурационный файл.

Конфигурация API-клиента в Swift

// APIConfig.swift
import Foundation

enum APIConfig {
    // Переключение между localhost (симулятор) и туннелем (реальное устройство)
    #if targetEnvironment(simulator)
    static let baseURL = "http://localhost:8080"
    #else
    static let baseURL = "https://my-api.fxtun.dev"
    #endif
}

// Использование
class APIClient {
    func fetchUsers() async throws -> [User] {
        let url = URL(string: "\(APIConfig.baseURL)/api/users")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([User].self, from: data)
    }
}

Почему HTTPS важен для iOS

App Transport Security (ATS) в iOS по умолчанию блокирует все HTTP-запросы без TLS. Разработчики часто добавляют исключения в Info.plist, чтобы обойти это ограничение при тестировании — но это плохая практика, которая может случайно попасть в продакшен-сборку.

fxTunnel автоматически выдаёт HTTPS-адрес с валидным TLS-сертификатом. Никаких исключений ATS добавлять не нужно — приложение работает с туннелем так же, как с продакшен-сервером. Тема HTTPS на localhost подробно раскрыта в «HTTPS на localhost для разработки».

Настройка для Android (Android Studio + локальный API + туннель)

В Android-разработке ситуация аналогичная: эмулятор может обращаться к 10.0.2.2 вместо localhost, но реальное устройство требует настоящий сетевой адрес.

Конфигурация API-клиента в Kotlin

// ApiConfig.kt
object ApiConfig {
    // Для эмулятора: 10.0.2.2 — это хост-машина
    // Для реального устройства: URL туннеля
    val BASE_URL: String
        get() = if (BuildConfig.DEBUG && isEmulator()) {
            "http://10.0.2.2:8080"
        } else {
            "https://my-api.fxtun.dev"
        }

    private fun isEmulator(): Boolean {
        return (android.os.Build.FINGERPRINT.contains("generic")
                || android.os.Build.FINGERPRINT.contains("emulator"))
    }
}

// Retrofit setup
val retrofit = Retrofit.Builder()
    .baseUrl(ApiConfig.BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Настройка Network Security Config

Начиная с Android 9 (API 28), незашифрованный HTTP-трафик заблокирован по умолчанию. При использовании fxTunnel этой проблемы нет — туннель предоставляет HTTPS. Но если вы используете локальный IP, потребуется network_security_config.xml:

<!-- res/xml/network_security_config.xml -->
<!-- НЕ нужно при использовании fxTunnel (HTTPS из коробки) -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
    </domain-config>
</network-security-config>

С fxTunnel этот файл не нужен — HTTPS работает автоматически.

Настройка для React Native и Flutter

Кроссплатформенные фреймворки упрощают работу с API-адресом: достаточно изменить одну строку конфигурации. Но проблема доступа к localhost с реального устройства остаётся.

React Native

// config/api.js
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';

const getBaseUrl = () => {
  if (__DEV__) {
    // Эмулятор: используем localhost / 10.0.2.2
    if (DeviceInfo.isEmulatorSync()) {
      return Platform.OS === 'android'
        ? 'http://10.0.2.2:8080'
        : 'http://localhost:8080';
    }
    // Реальное устройство: используем туннель
    return 'https://my-api.fxtun.dev';
  }
  // Продакшен
  return 'https://api.myapp.com';
};

export const API_BASE_URL = getBaseUrl();

// Использование с fetch
const response = await fetch(`${API_BASE_URL}/api/users`);

Flutter (Dart)

// lib/config/api_config.dart
import 'dart:io';

class ApiConfig {
  static String get baseUrl {
    // В debug-режиме проверяем, запущено ли приложение на эмуляторе
    // Для реального устройства используем туннель
    const bool isRelease = bool.fromEnvironment('dart.vm.product');
    if (isRelease) {
      return 'https://api.myapp.com';
    }

    // Для реального устройства — URL туннеля
    // Для эмулятора — localhost
    // Управляется через --dart-define=API_URL=...
    return const String.fromEnvironment(
      'API_URL',
      defaultValue: 'https://my-api.fxtun.dev',
    );
  }
}

// Запуск с разными URL:
// flutter run --dart-define=API_URL=http://localhost:8080      (эмулятор)
// flutter run --dart-define=API_URL=https://my-api.fxtun.dev   (устройство)
# Запуск Flutter-приложения на реальном устройстве с URL туннеля
flutter run --dart-define=API_URL=https://my-api.fxtun.dev

Тестирование push-уведомлений через туннель

Push-уведомления — один из главных аргументов в пользу реального устройства. APNs (Apple Push Notification service) и FCM (Firebase Cloud Messaging) не работают в симуляторах. Для полного цикла тестирования — отправка запроса на ваш API, формирование пуша на сервере, доставка на устройство — API должен быть доступен извне.

Типичный flow

Мобильное приложение → POST /api/action → ваш API (через туннель)
       ↓
Ваш API формирует push-уведомление
       ↓
APNs / FCM → push на реальное устройство

Без туннеля для этого пришлось бы деплоить API на сервер после каждого изменения. С fxTunnel вы тестируете весь цикл локально:

# API работает на localhost:8080
# Туннель открыт
fxtunnel http 8080

# Тестируем отправку push-уведомления через API
curl -X POST https://my-api.fxtun.dev/api/send-push \
  -H "Content-Type: application/json" \
  -d '{"user_id": "123", "title": "Тест", "body": "Push через туннель"}'

Ваш локальный сервер получает запрос, формирует push через APNs/FCM, и через секунды уведомление приходит на реальный телефон — весь цикл без деплоя.

Отладка: Inspector показывает запросы от мобильного приложения

Бывало такое: на экране телефона ошибка, а что именно приложение отправило – непонятно? Charles Proxy и mitmproxy помогут, но потребуют настройки прокси на телефоне и установки корневых сертификатов. Встроенный Inspector в fxTunnel (от $5/мес) подходит к задаче иначе: он показывает каждый запрос через туннель в веб-интерфейсе, обновляясь в реальном времени.

Что видно в Inspector:

  • URL и методPOST /api/users, GET /api/feed
  • Заголовки запросаAuthorization, Content-Type, User-Agent
  • Тело запроса — JSON-payload, который отправляет приложение
  • Статус ответа200 OK, 401 Unauthorized, 500 Internal Server Error
  • Тело ответа — JSON, который вернул ваш API
  • Время ответа — сколько миллисекунд занял запрос

Replay для повторной отправки

Нашли баг? Не нужно воспроизводить сценарий на телефоне заново. Нажмите Replay в Inspector — запрос будет повторно отправлен на ваш API. Поставьте брейкпоинт в IDE и пошагово отладьте обработку. Это особенно полезно для сложных сценариев, где нужно пройти многошаговый flow в приложении.

Replay так же полезен при работе с вебхуками – подробнее об этом в «Тестирование вебхуков через туннель».

Советы: certificate pinning и управление URL

Certificate Pinning

Если ваше приложение использует SSL certificate pinning (закрепление сертификата), запросы через туннель будут заблокированы — сертификат fxTunnel не совпадёт с закреплённым. Решение:

// iOS: отключаем pinning для debug-сборок
#if DEBUG
// Используем стандартный URLSession без кастомного delegate
let session = URLSession.shared
#else
// Продакшен: включаем pinning
let session = URLSession(configuration: .default, delegate: PinningDelegate(), delegateQueue: nil)
#endif
// Android: отключаем pinning для debug-сборок
val client = if (BuildConfig.DEBUG) {
    OkHttpClient.Builder().build()  // без CertificatePinner
} else {
    OkHttpClient.Builder()
        .certificatePinner(
            CertificatePinner.Builder()
                .add("api.myapp.com", "sha256/AAAA...")
                .build()
        )
        .build()
}

Управление API URL через конфигурацию

Не хардкодьте URL в приложении. Вынесите его в конфигурацию, чтобы переключаться между окружениями одной строкой:

# .env.development
API_URL=https://my-api.fxtun.dev

# .env.production
API_URL=https://api.myapp.com

API-сервер работает в Docker? Это тоже поддерживается – настройка описана в Docker + туннель.

Полный workflow: от кода до тестирования на телефоне

Вот пошаговый процесс разработки мобильного приложения с fxTunnel:

  1. Запустите API-сервер на localhost.
  2. Откройте туннельfxtunnel http 8080.
  3. Укажите URL туннеля в конфигурации мобильного приложения.
  4. Запустите приложение на реальном устройстве через Xcode / Android Studio.
  5. Тестируйте — все запросы идут через туннель на ваш localhost.
  6. Отлаживайте — используйте Inspector для просмотра запросов и Replay для повторной отправки.
  7. Меняйте код — изменения в API применяются мгновенно, без передеплоя.
# Терминал 1: API-сервер
cd ~/projects/my-api && go run main.go

# Терминал 2: туннель
fxtunnel http 8080

# Терминал 3: запуск на устройстве (Flutter)
cd ~/projects/my-app && flutter run --dart-define=API_URL=https://my-api.fxtun.dev

Почему fxTunnel удобен для мобильной разработки

fxTunnel закрывает основные болевые точки мобильного тестирования: HTTPS из коробки (для iOS ATS и Android Network Security), постоянный URL (не надо обновлять конфигурацию) и Inspector для отладки запросов без прокси на телефоне.

ЗадачаБез туннеляС fxTunnel
Доступ к API с телефонаТолько в одной Wi-Fi сетиИз любой сети
HTTPS для iOS ATSИсключения в Info.plistАвтоматически
Просмотр запросовCharles Proxy + сертификатыInspector в браузере
Изменение кода APIПередеплой на серверМгновенно, без деплоя
Push-уведомленияДеплой для каждого тестаПолный цикл локально
Смена IP при смене сетиОбновление конфигурацииПостоянный URL

FAQ

Почему мобильное приложение не может подключиться к localhost?

Адрес 127.0.0.1 (localhost) ведёт только на ту машину, на которой запущен. Когда телефон обращается к 127.0.0.1, он попадает в свой собственный loopback, а не на ваш компьютер. Чтобы перебросить трафик, нужен публичный URL, ведущий на вашу машину, – именно это и делает туннель.

Можно ли тестировать на реальном устройстве без туннеля?

Если телефон и компьютер в одной Wi-Fi – да, можно обращаться по IP компьютера (192.168.1.42:8080). Минусы: за пределами этой сети не работает, IP меняется при смене Wi-Fi, HTTPS нет. Туннель даёт стабильный HTTPS-адрес, доступный откуда угодно.

Блокирует ли iOS HTTP-соединения без HTTPS?

Да, App Transport Security (ATS) по умолчанию отклоняет незашифрованные запросы. Поскольку fxTunnel выдаёт HTTPS-адрес с валидным сертификатом, ATS пропускает запросы без вопросов – исключения в Info.plist добавлять не нужно.

Как посмотреть запросы, которые мобильное приложение отправляет через туннель?

Откройте веб-интерфейс Inspector (доступен от $5/мес). Там видны все HTTP-запросы в реальном времени с заголовками, телом и статусом ответа. В отличие от Charles Proxy или mitmproxy, на телефоне ничего настраивать и устанавливать не нужно.

Нужно ли менять URL в приложении при каждом перезапуске туннеля?

Нет. В fxTunnel SaaS URL сохраняется между перезапусками, даже на бесплатном плане. С кастомным доменом (от $5/мес) адрес полностью ваш. Вынесите URL в конфиг – и переключение между localhost и туннелем будет в одну строку.