Использование прокси в Swift: руководство для URLSession на iOS и macOS

Практическое руководство по настройке HTTP/HTTPS и SOCKS5 прокси в Swift через URLSession, аутентификации, гео-таргетингу и production-паттернам с residential-прокси ProxyHat.

Using Proxies in Swift: A Code-First Guide to URLSession, SOCKS5 & Residential IPs
В этой статье

Использование прокси в Swift — задача, с которой сталкиваются iOS- и macOS-разработчики, когда нужно собирать публичные данные, тестировать региональные эндпоинты или обходить блокировки дата-центр IP. URLSession не предоставляет высокоуровневого API для прокси, но через URLSessionConfiguration.connectionProxyDictionary можно маршрутизировать трафик через HTTP/HTTPS и SOCKS5-шлюзы. В этом руководстве мы разберём настройку residential-прокси ProxyHat в Swift, аутентификацию, гео-таргетинг, concurrency через async/await и production-паттерны.

Использование прокси в Swift: технический контекст

CFNetwork, лежащий в основе URLSession, поддерживает прокси через словарь connectionProxyDictionary. Этот словарь использует ключи kCFNetworkProxies* из CFNetwork/CFNetworkProxies.h. Проблема в том, что URLSession не всегда корректно обрабатывает kCFProxyUsernameKey/kCFProxyPasswordKey — на практике надёжнее передавать учётные данные через заголовок Proxy-Authorization или реализовать метод делегата urlSession(_:didReceive:completionHandler:) для вызова 407.

Согласно документации Apple, connectionProxyDictionary описывает конфигурацию прокси аналогично системным настройкам. Однако поведение ключей аутентификации нестабильно на разных версиях iOS/macOS, поэтому в production лучше использовать явный заголовок.

Настройка HTTP/HTTPS прокси в URLSession

Базовая конфигурация HTTP-прокси ProxyHat на gate.proxyhat.com:8080 выглядит так:

import Foundation

func makeProxySession(user: String, pass: String) -> URLSession {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable as String: 1,
        kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPPort as String: 8080,
        kCFNetworkProxiesHTTPSEnable as String: 1,
        kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPSPort as String: 8080
    ]
    // Аутентификация через заголовок
    let token = \(user):\(pass)
    let base64 = Data(token.utf8).base64EncodedString()
    config.httpAdditionalHeaders = [
        "Proxy-Authorization": "Basic \(base64)"
    ]
    config.timeoutIntervalForRequest = 30
    config.timeoutIntervalForResource = 120
    return URLSession(configuration: config)
}

Здесь мы включаем и HTTP, и HTTPS прокси, указывая один и тот же шлюз. Порт 8080 — стандартный для HTTP-прокси ProxyHat. Заголовок Proxy-Authorization кодирует USERNAME:PASSWORD в Base64.

Гео-таргетинг и sticky-сессии в имени пользователя

ProxyHat кодирует страну, город и идентификатор сессии прямо в username. Например, user-country-US-city-newyork-session-abc123 закрепляет IP за сессией abc123 в Нью-Йорке.

func makeGeoProxySession(baseUser: String, pass: String,
                          country: String, city: String,
                          session: String) -> URLSession {
    let username = "\(baseUser)-country-\(country)-city-\(city)-session-\(session)"
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable as String: 1,
        kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPPort as String: 8080,
        kCFNetworkProxiesHTTPSEnable as String: 1,
        kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPSPort as String: 8080
    ]
    let credentials = "\(username):\(pass)"
    let base64 = Data(credentials.utf8).base64EncodedString()
    config.httpAdditionalHeaders = ["Proxy-Authorization": "Basic \(base64)"]
    return URLSession(configuration: config)
}

Аутентификация через URLSessionDelegate (вызов 407)

Альтернатива заголовку — реализовать делегат для обработки вызова прокси-аутентификации:

final class ProxyAuthDelegate: NSObject, URLSessionDelegate,
        URLSessionTaskDelegate {
    let username: String
    let password: String

    init(username: String, password: String) {
        self.username = username
        self.password = password
    }

    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition,
                                                URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod
                == NSURLAuthenticationMethodHTTPProxy
                || challenge.protectionSpace.authenticationMethod
                == NSURLAuthenticationMethodHTTPSProxy else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        let cred = URLCredential(user: username,
                                  password: password,
                                  persistence: .forSession)
        completionHandler(.useCredential, cred)
    }
}

// Использование
let delegate = ProxyAuthDelegate(
    username: "user-country-DE-city-berlin-session-s7",
    password: "pass"
)
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
    kCFNetworkProxiesHTTPEnable as String: 1,
    kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
    kCFNetworkProxiesHTTPPort as String: 8080,
    kCFNetworkProxiesHTTPSEnable as String: 1,
    kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
    kCFNetworkProxiesHTTPSPort as String: 8080
]
let session = URLSession(configuration: config, delegate: delegate,
                          delegateQueue: nil)

Делегат перехватывает вызов 407 Proxy Authentication Required и возвращает URLCredential с тем же составным username, что и при ручном заголовке. Этот подход надёжнее на iOS 16+, где httpAdditionalHeaders иногда не пробрасывается в CONNECT-туннель для HTTPS.

SOCKS5-прокси в Swift через kCFStreamPropertySOCKSProxy

Для SOCKS5 ProxyHat использует порт 1080. URLSession поддерживает SOCKS через ключи kCFStreamPropertySOCKSProxy*:

func makeSocks5Session(user: String, pass: String) -> URLSession {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesSOCKSEnable as String: 1,
        kCFNetworkProxiesSOCKSProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesSOCKSPort as String: 1080,
        kCFNetworkProxiesSOCKSVersion as String: kCFStreamSocketSOCKSVersion5 as Any,
        kCFNetworkProxiesSOCKSUser as String: user,
        kCFNetworkProxiesSOCKSPassword as String: pass
    ]
    config.timeoutIntervalForRequest = 30
    return URLSession(configuration: config)
}

SOCKS5 полезен, когда целевой протокол не является HTTP (например, WebSocket через нестандартный порт или кастомный TCP-протокол). Для большинства веб-скрейпинг-задач HTTP-прокси на 8080 достаточно и проще в отладке.

Residential-прокси: зачем они нужны в Swift

Многие API и сайты блокируют дата-центр IP-диапазоны AWS, GCP, Azure и OVH. Residential-прокси используют IP реальных интернет-провайдеров, поэтому запрос выглядит как трафик обычного пользователя. Это критично для:

  • Region-locked контент — стриминговые сервисы, локализованные витрины магазинов, SERP с гео-зависимыми результатами.
  • Сбор публичных данных — цены, отзывы, метаданные приложений, где дата-центр IP сразу попадает в чёрный список.
  • QA и тестирование — проверка региональных A/B-тестов и CDN-маршрутизации.

Согласно RFC 7231, заголовок Accept-Language влияет на контент, возвращаемый сервером. Комбинируя residential-прокси в нужной стране с корректным Accept-Language, вы получаете данные, идентичные тому, что видит локальный пользователь.

Сравнение типов прокси

ТипСкоростьБлок-рискЦенаUse case
DatacenterВысокая (<50ms)ВысокийНизкаяВнутренний QA, API без анти-бота
ResidentialСредняя (100-500ms)НизкийСредняяВеб-скрейпинг, SERP, цены
MobileНизкая (300-1000ms)Очень низкийВысокаяМобильные эндпоинты, соцсети

async/await с URLSession.shared.data(for:) и Codable

Современный Swift использует async/await для сетевых вызовов. Вот пример скрейпинга JSON-API через residential-прокси с декодированием в Codable-модель:

import Foundation

struct Product: Codable {
    let id: Int
    let title: String
    let price: Double
    let currency: String
}

struct Scraper {
    let session: URLSession

    func fetchProducts(url: URL) async throws -> [Product] {
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
        request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
                         forHTTPHeaderField: "User-Agent")
        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse,
              200..<300 ~= http.statusCode else {
            throw ScraperError.badStatus((response as? HTTPURLResponse)?.statusCode ?? -1)
        }
        return try JSONDecoder().decode([Product].self, from: data)
    }
}

enum ScraperError: Error {
    case badStatus(Int)
}

Параллельные запросы через TaskGroup

Для скрейпинга нескольких страниц или регионов используйте ThrowingTaskGroup с ограничением параллелизма:

func scrapeAllRegions(countries: [String], baseURL: URL,
                       baseUser: String, pass: String) async throws -> [Product] {
    try await withThrowingTaskGroup(of: [Product].self) { group in
        for (index, country) in countries.enumerated() {
            let session = makeGeoProxySession(
                baseUser: baseUser, pass: pass,
                country: country, city: "", session: "s-\(index)"
            )
            let scraper = Scraper(session: session)
            let url = baseURL.appendingPathComponent("?region=\(country)")
            group.addTask {
                try await scraper.fetchProducts(url: url)
            }
        }
        var all: [Product] = []
        for try await batch in group {
            all.append(contentsOf: batch)
        }
        return all
    }
}

Каждая задача получает свой URLSession со своей sticky-сессией, что позволяет одновременно собирать данные из США, Германии и Японии без переключения IP внутри одного запроса. Рекомендуемое ограничение — не более 50 одновременных сессий на устройство, чтобы избежать исчерпания портов и задержек TLS-handshake.

curl для быстрой проверки прокси

Перед написанием Swift-кода проверьте прокси через curl:

curl -x http://user-country-US-city-newyork-session-abc123:pass@gate.proxyhat.com:8080 \
  -s https://api.ipify.org?format=json

Если ответ содержит американский IP, конфигурация верна. Для SOCKS5:

curl --socks5 user-country-US:pass@gate.proxyhat.com:1080 \
  -s https://api.ipify.org?format=json

Production-паттерны: TLS, ретраи, ATS и приватность

URLSessionDelegate для TLS и валидации сертификатов

При работе через прокси иногда нужно явно обработать TLS-вызов, особенно с self-signed сертификатами в тестовой среде:

final class TLSPinningDelegate: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition,
                                                URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod
                == NSURLAuthenticationMethodServerTrust,
              let trust = challenge.protectionSpace.serverTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        // В production: проверка pinned-сертификатов
        let certCount = SecTrustGetCertificateCount(trust)
        guard certCount > 0,
              let serverCert = SecTrustGetCertificateAtIndex(trust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        // Сравнение с встроенным публичным ключом
        completionHandler(.useCredential, URLCredential(trust: trust))
    }
}

Ретраи с экспоненциальной задержкой

func fetchWithRetry(scraper: Scraper, url: URL,
                    maxAttempts: Int = 5) async throws -> [Product] {
    var attempt = 0
    var delay: UInt64 = 1_000_000_000 // 1 секунда в наносекундах
    while attempt < maxAttempts {
        do {
            return try await scraper.fetchProducts(url: url)
        } catch ScraperError.badStatus(429) {
            attempt += 1
            try await Task.sleep(nanoseconds: delay)
            delay = min(delay * 2, 30_000_000_000) // cap 30s
        } catch ScraperError.badStatus(403) {
            // Сменить сессию IP
            attempt += 1
            try await Task.sleep(nanoseconds: delay)
            delay = min(delay * 2, 30_000_000_000)
        } catch {
            throw error
        }
    }
    throw ScraperError.badStatus(-1)
}

Экспоненциальная задержка с上限 30 секунд и максимум 5 попыток — стандартный паттерн для скрейпинга. При 429 или 403 стоит менять идентификатор сессии, чтобы получить новый IP.

App Transport Security (ATS)

По умолчанию ATS требует TLS 1.2+ и валидных сертификатов. Прокси-шлюз ProxyHat поддерживает TLS, поэтому изменений в Info.plist обычно не требуется. Если целевой сайт использует устаревший TLS, добавьте исключение:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <key>NSMinimumTLSVersion</key>
            <string>TLSv1.0</string>
        </dict>
    </dict>
</dict>

On-device приватность

Не хардкодьте пароли прокси в бинарл приложения. Используйте Keychain или серверную конфигурацию через ваш backend. Согласно руководству Apple по безопасности, учётные данные должны храниться в Keychain с классом доступности kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, чтобы они не попадали в iCloud-бэкапы.

ProxyHat: настройка и SDK

ProxyHat предоставляет residential, mobile и datacenter-прокси через единый шлюз gate.proxyhat.com. Параметры гео-таргетинга и сессий кодируются в username, как показано выше. Полная документация доступна на docs.proxyhat.com.

Для серверных компонентов (Python/Node.js) у ProxyHat есть SDK, зеркалирующий тот же шлюз. Это удобно, если ваш iOS-клиент отправляет задачи на backend, который выполняет тяжёлый скрейпинг. См. тарифы на странице цен и доступные локации на странице локаций.

Use cases, где residential-прокси через ProxyHat особенно полезны: веб-скрейпинг и SERP-трекинг.

Этика и правовые аспекты

Прокси — инструмент, и его легальность зависит от того, что вы делаете. Уважайте robots.txt и условия обслуживания сайтов. В США закон CFAA (Computer Fraud and Abuse Act) может применяться к несанкционированному доступу. В ЕС GDPR регулирует обработку персональных данных, включая IP-адреса.

Правила App Store Review Guidelines (раздел 5.2.2) требуют, чтобы приложения не собирали данные без согласия и не нарушали ToS третьих сторон. Предпочитайте официальные API, когда они доступны. Если официального API нет, ограничивайте частоту запросов и не собирайте персональные данные без законного основания.

Ключевые выводы

  • URLSession + connectionProxyDictionary — основной способ настройки прокси в Swift; ключи kCFNetworkProxiesHTTP* и kCFNetworkProxiesHTTPS* указывают на gate.proxyhat.com:8080.
  • Аутентификация через Proxy-Authorization: Basic или urlSession(_:didReceive:) надёжнее, чем kCFProxyUsernameKey.
  • Гео-таргетинг и sticky-сессии кодируются в username: user-country-US-city-newyork-session-abc123.
  • SOCKS5 через kCFNetworkProxiesSOCKS* на порту 1080 — для не-HTTP протоколов.
  • Residential-прокси необходимы для сайтов, блокирующих дата-центр IP и для region-locked контента.
  • async/await + TaskGroup — идиоматичный способ параллельного скрейпинга с per-сессионными IP.
  • Production: ретраи с экспоненциальной задержкой, TLS-pinning, Keychain для учётных данных, ATS-совместимость.
  • Этика: уважайте robots.txt, ToS, CFAA, GDPR и App Store Guidelines; предпочитайте официальные API.

FAQ

Что такое использование прокси в Swift?

Использование прокси в Swift — это настройка URLSession для маршрутизации HTTP/HTTPS или SOCKS5-трафика через промежуточный сервер. На iOS и macOS это делается через URLSessionConfiguration.connectionProxyDictionary с ключами kCFNetworkProxiesHTTPEnable, kCFNetworkProxiesHTTPProxy и их HTTPS-эквивалентами, а аутентификация — через заголовок Proxy-Authorization или делегат urlSession(_:didReceive:).

Почему использование прокси в Swift важно для пользователей прокси?

Прокси в Swift важны, потому что многие API и веб-сайты блокируют дата-центр IP и ограничивают доступ по региону. Residential-прокси позволяют iOS/macOS-приложениям обращаться к регион-зависимым эндпоинтам, собирать публичные данные и тестировать приложения из разных географических точек без блокировок и CAPTCHA.

Какой тип прокси лучше всего подходит для использования в Swift?

Residential-прокси лучше всего подходят для большинства сценариев в Swift, так как они используют IP реальных провайдеров и реже блокируются анти-бот-системами. Datacenter-прокси быстрее и дешевле, но часто попадают в чёрные списки. Mobile-прокси идеальны для мобильных эндпоинтов. SOCKS5 полезен, когда HTTP-прокси не подходит для протокола.

Как избежать блокировок при реализации прокси в Swift?

Используйте ротацию IP через sticky-сессии, ограничивайте частоту запросов, добавляйте ретраи с экспоненциальной задержкой, уважайте robots.txt и условия обслуживания. Комбинируйте гео-таргетинг с реалистичными заголовками User-Agent и Accept-Language. Предпочитайте официальные API, когда они доступны, и консультируйтесь с юристом по CFAA и GDPR.

Часто задаваемые вопросы

Что такое использование прокси в Swift?

Использование прокси в Swift — это настройка URLSession для маршрутизации HTTP/HTTPS или SOCKS5-трафика через промежуточный сервер. На iOS и macOS это делается через URLSessionConfiguration.connectionProxyDictionary с ключами kCFNetworkProxiesHTTPEnable, kCFNetworkProxiesHTTPProxy и их HTTPS-эквивалентами, а аутентификация — через заголовок Proxy-Authorization или делегат urlSession(_:didReceive:).

Почему использование прокси в Swift важно для пользователей прокси?

Прокси в Swift важны, потому что многие API и веб-сайты блокируют дата-центр IP и ограничивают доступ по региону. Residential-прокси позволяют iOS/macOS-приложениям обращаться к регион-зависимым эндпоинтам, собирать публичные данные и тестировать приложения из разных географических точек без блокировок и CAPTCHA.

Какой тип прокси лучше всего подходит для использования в Swift?

Residential-прокси лучше всего подходят для большинства сценариев в Swift, так как они используют IP реальных провайдеров и реже блокируются анти-бот-системами. Datacenter-прокси быстрее и дешевле, но часто попадают в чёрные списки. Mobile-прокси идеальны для мобильных эндпоинтов. SOCKS5 полезен, когда HTTP-прокси не подходит для протокола.

Как избежать блокировок при реализации прокси в Swift?

Используйте ротацию IP через sticky-сессии, ограничивайте частоту запросов, добавляйте ретраи с экспоненциальной задержкой, уважайте robots.txt и условия обслуживания. Комбинируйте гео-таргетинг с реалистичными заголовками User-Agent и Accept-Language. Предпочитайте официальные API, когда они доступны, и консультируйтесь с юристом по CFAA и GDPR.

Готовы начать?

Резидентные, ISP и мобильные прокси в 148+ странах. Создайте бесплатный аккаунт.

Создать бесплатный аккаунт
← Вернуться в Блог