Использование прокси в 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.






