Si necesitas usar proxies en Swift para rutas de red en iOS o macOS, probablemente ya descubriste que URLSession no expone una API sencilla para configurar proxies HTTP/SOCKS5 con autenticación. Esta guía cubre todo lo que necesitas: desde connectionProxyDictionary hasta autenticación Proxy-Authorization, geo-targeting, concurrencia con async/await y consejos de producción para scraping y automatización.
Por qué usar proxies en Swift con URLSession
Las apps de iOS y macOS que consumen datos de terceros —sean rastreadores de precios, herramientas de SERP tracking o clientes de APIs regionales— se enfrentan a dos problemas recurrentes: bloqueos por IP de datacenter y restricciones geográficas. Cuando tu app sale por una IP de un cloud provider como AWS o DigitalOcean, muchos endpoints la rechazan con un 403 antes de evaluar el contenido de la petición. Los proxies residenciales resuelven esto enrutaando el tráfico por IPs de hogares reales, que los sistemas anti-bot tratan con mayor confianza.
Apple no ofrece un constructor de proxies de alto nivel en URLSession. Toda la configuración vive en URLSessionConfiguration.connectionProxyDictionary, un diccionario con claves de Core Foundation (kCFNetworkProxies*). Esto significa que debes conocer las claves exactas, manejar la autenticación manualmente y tener cuidado con ATS (App Transport Security). Puedes consultar la documentación oficial de URLSessionConfiguration y la guía de URL Session Programming Guide de Apple.
Configuración básica: connectionProxyDictionary con HTTP/HTTPS
El núcleo de cualquier configuración de proxy en Swift es URLSessionConfiguration.connectionProxyDictionary. Este diccionario usa claves de CFNetwork para definir el host, puerto y tipo de proxy. Para ProxyHat, el gateway HTTP está en gate.proxyhat.com:8080.
import Foundation
func createProxySession(username: String, password: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort: 8080,
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort: 8080
]
let delegate = ProxyAuthDelegate(username: username, password: password)
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
}
Aquí usamos URLSessionConfiguration.ephemeral para evitar que las cookies y la caché se filtren entre sesiones con diferentes IPs de proxy, lo cual es crítico cuando rotas entre regiones.
Autenticación y geo-targeting: el problema de kCFProxyUsername
Core Foundation define kCFProxyUsernameKey y kCFProxyPasswordKey, pero en la práctica estos valores son poco fiables con URLSession en iOS 15+ y macOS 12+. La solución más robusta es enviar manualmente el header Proxy-Authorization: Basic o manejar el desafío 407 mediante URLSessionDelegate.
ProxyHat permite codificar geo-targeting y sesiones en el nombre de usuario con el formato user-country-US-city-newyork-session-abc123. Esto te da control granular sobre la IP de salida sin cambiar la configuración del proxy.
Opción A: header Proxy-Authorization manual
import Foundation
func proxyAuthHeader(username: String, password: String) -> String {
let credentials = "\(username):\(password)"
let data = credentials.data(using: .utf8)!
let base64 = data.base64EncodedString()
return "Basic \(base64)"
}
// Construye una petición con geo-targeting
func makeProxiedRequest(url: URL, country: String, city: String, sessionId: String) -> URLRequest {
var request = URLRequest(url: url, timeoutInterval: 30)
let username = "user-country-\(country)-city-\(city)-session-\(sessionId)"
let password = "TU_PASSWORD"
request.setValue(proxyAuthHeader(username: username, password: password),
forHTTPHeaderField: "Proxy-Authorization")
return request
}
Opción B: URLSessionDelegate para el desafío 407
import Foundation
final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
private let username: String
private 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 == NSURLAuthenticationMethodHTTPBasic,
challenge.previousFailureCount == 0 else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(user: username, password: password, persistence: .none)
completionHandler(.useCredential, credential)
}
}
El enfoque del delegate es más limpio porque no acopla la lógica de autenticación a cada URLRequest. Sin embargo, si tu proxy requiere el header Proxy-Authorization en el túnel CONNECT (para HTTPS vía proxy HTTP), el delegate puede no interceptarlo correctamente en todas las versiones de iOS. Por eso recomendamos probar ambos enfoques y quedarte con el que funcione en tu target mínimo de deployment.
SOCKS5 en Swift: kCFStreamPropertySOCKSProxy
Para tráfico que se beneficia de SOCKS5 —por ejemplo, conexiones TCP crudas o cuando el proxy HTTP interfiere con ciertos certificados— puedes usar el puerto 1080 de ProxyHat con las claves kCFStreamPropertySOCKSProxy*.
import Foundation
func createSOCKS5Session(username: String, password: String) -> URLSession {
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFStreamPropertySOCKSProxyHost: "gate.proxyhat.com",
kCFStreamPropertySOCKSProxyPort: 1080,
kCFStreamPropertySOCKSVersion: kCFStreamSocketSOCKSVersion5,
kCFStreamPropertySOCKSUser: username,
kCFStreamPropertySOCKSPassword: password
]
return URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
SOCKS5 puede ofrecer menor overhead que HTTP CONNECT en escenarios de alto volumen, pero la autenticación con usuario/contraseña vía kCFStreamPropertySOCKSUser puede requerir pruebas en tu versión de OS. Si encuentras problemas, vuelve al enfoque del delegate o del header manual.
Web scraping en Swift: async/await con TaskGroup y Codable
El caso de uso más común para Swift web scraping es recolectar datos de múltiples endpoints en paralelo, decodificando JSON con Codable. Aquí tienes un ejemplo completo que usa URLSession.shared.data(for:) con concurrencia estructurada vía TaskGroup, proxies residenciales y manejo de errores.
import Foundation
// Modelo decodificable
struct ProductPrice: Codable {
let id: Int
let name: String
let price: Double
let currency: String
}
struct ScrapeError: Error {
let message: String
let statusCode: Int?
}
// Cliente con proxy residencial y retry exponencial
final class ProxyScrapeClient {
private let session: URLSession
private let password: String
init(password: String) {
self.password = password
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort: 8080,
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort: 8080
]
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
config.httpMaximumConnectionsPerHost = 10
self.session = URLSession(configuration: config)
}
func fetchPrices(productIds: [Int], country: String) async throws -> [ProductPrice] {
try await withThrowingTaskGroup(of: ProductPrice?.self) { group in
for id in productIds {
group.addTask {
try await self.fetchSinglePrice(id: id, country: country)
}
}
var results: [ProductPrice] = []
for try await result in group {
if let r = result { results.append(r) }
}
return results
}
}
private func fetchSinglePrice(id: Int, country: String) async throws -> ProductPrice? {
let sessionId = "prod-\(id)-\(UUID().uuidString.prefix(8))"
let username = "user-country-\(country)-session-\(sessionId)"
let auth = proxyAuthHeader(username: username, password: password)
let url = URL(string: "https://api.ejemplo.com/products/\(id)/price")!
var request = URLRequest(url: url)
request.setValue(auth, forHTTPHeaderField: "Proxy-Authorization")
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
forHTTPHeaderField: "User-Agent")
var attempt = 0
let maxRetries = 3
while attempt < maxRetries {
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw ScrapeError(message: "Respuesta no HTTP", statusCode: nil)
}
if http.statusCode == 200 {
return try JSONDecoder().decode(ProductPrice.self, from: data)
}
if http.statusCode == 429 || http.statusCode >= 500 {
throw ScrapeError(message: "Rate limited o error de servidor",
statusCode: http.statusCode)
}
throw ScrapeError(message: "HTTP \(http.statusCode)",
statusCode: http.statusCode)
} catch {
attempt += 1
if attempt >= maxRetries { return nil }
let delay = pow(2.0, Double(attempt)) // 2s, 4s, 8s
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
}
return nil
}
}
// Uso
let client = ProxyScrapeClient(password: "TU_PASSWORD")
Task {
let prices = try await client.fetchPrices(productIds: [1, 2, 3, 4, 5], country: "US")
for p in prices { print("\(p.name): \(p.price) \(p.currency)") }
}
Este patrón con TaskGroup permite ejecutar hasta 10 peticiones concurrentes (configurado en httpMaximumConnectionsPerHost), cada una con su propia sesión de proxy y IP residencial. El retry con backoff exponencial de 2s/4s/8s maneja los 429 y errores transitorios sin saturar el endpoint.
Comparación: proxy HTTP vs SOCKS5 vs sin proxy
| Aspecto | Sin proxy | Proxy HTTP (8080) | SOCKS5 (1080) |
|---|---|---|---|
| IP visible | Tu IP real / datacenter | IP residencial ProxyHat | IP residencial ProxyHat |
| Autenticación | N/A | Header o delegate 407 | kCFStreamPropertySOCKSUser |
| Overhead | Mínimo | Medio (CONNECT tunnel) | Bajo |
| Compatibilidad ATS | Total | Requiere verificación | Requiere verificación |
| Geo-targeting | No | Sí, en username | Sí, en username |
| Sticky sessions | No | Sí | Sí |
Errores comunes y casos límite
1. Olvidar kCFNetworkProxiesHTTPSEnable
Si solo configuras kCFNetworkProxiesHTTPEnable, las peticiones HTTPS no pasarán por el proxy. Debes incluir ambos conjuntos de claves —HTTP y HTTPS— apuntando al mismo gateway.
2. ATS bloquea conexiones vía proxy
App Transport Security exige TLS 1.2+ y certificados válidos. Si tu proxy hace inspección TLS (MITM), necesitarás añadir excepciones en Info.plist bajo NSAppTransportSecurity. Para ProxyHat, que opera como túnel transparente, ATS normalmente no interfiere, pero verifica con nscurl --ats-diagnostics.
3. Sesiones pegajosas que caducan
Las sesiones sticky de ProxyHat mantienen la misma IP durante un período. Si tu sesión caduca a mitad de una operación de scraping, obtendrás una IP nueva sin aviso. Usa identificadores de sesión únicos por tarea y reinícialos si detectas un cambio de IP inesperado.
4. Fuga de DNS
Por defecto, URLSession puede resolver DNS localmente antes de enviar la petición por el proxy. Esto expone qué dominios estás consultando. Para mitigarlo, asegúrate de que el proxy maneje la resolución DNS remota, lo cual ProxyHat hace por defecto en modo túnel.
Consejos de producción
URLSessionDelegate para TLS personalizado
Si necesitas validar certificados de origen o implementar certificate pinning a través del proxy, implementa urlSession(_:didReceive:completionHandler:) en tu delegate:
final class TLSPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let trust = challenge.protectionSpace.serverTrust {
// Validar trust chain manualmente si implementas pinning
completionHandler(.useCredential, URLCredential(trust: trust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
completionHandler(.performDefaultHandling, nil)
}
}
Retry con circuit breaker
Para volúmenes altos (por ejemplo, 1,500 peticiones/minuto), implementa un circuit breaker que pausa el tráfico tras N fallos consecutivos. Un patrón simple: un contador compartido con actor que se reinicia tras un período de cooldown.
actor CircuitBreaker {
private var failures = 0
private var lastFailure: Date?
private let threshold = 5
private let cooldown: TimeInterval = 60 // 60s
func recordFailure() {
failures += 1
lastFailure = Date()
}
func recordSuccess() {
failures = 0
}
var isOpen: Bool {
guard let last = lastFailure else { return false }
return failures >= threshold && Date().timeIntervalSince(last) < cooldown
}
}
Privacidad on-device
En iOS, usa URLSessionConfiguration.ephemeral para evitar persistencia de cookies y caché en disco. Además, declara NSPrivacyTracking en tu Privacy Manifest si tu app usa proxies para rastrear datos de usuarios, y sé transparente en la App Store sobre el uso de red. Las App Store Review Guidelines exigen que las apps no recopilen datos sin consentimiento.
Configuración específica de ProxyHat
ProxyHat ofrece un gateway unificado en gate.proxyhat.com con puertos 8080 (HTTP) y 1080 (SOCKS5). El formato de usuario soporta geo-targeting a nivel de país y ciudad, además de sesiones sticky:
user-country-US— IP en Estados Unidosuser-country-DE-city-berlin— IP en Berlín, Alemaniauser-country-GB-session-abc123— Sesión sticky en Reino Unidouser-country-US-city-newyork-session-abc123— Combinación completa
Consulta las documentación de ProxyHat para detalles sobre parámetros avanzados. El SDK de ProxyHat (disponible en Python y Node.js) usa el mismo gateway, por lo que la lógica de geo-targeting es transferible si tu backend también consume datos.
Para explorar opciones de precios de proxies o ver la lista de ubicaciones disponibles, visita las páginas correspondientes. Si tu caso de uso es web scraping o SERP tracking, ProxyHat tiene guías específicas.
Consideraciones éticas y legales
Usar proxies para acceder a datos públicos legítimos es generalmente aceptable, pero hay límites importantes:
- Computer Fraud and Abuse Act (CFAA) — En EE.UU., acceder a sistemas sin autorización o evadir controles de acceso puede violar el CFAA. Extrae solo datos públicamente accesibles.
- GDPR — En la UE, recopilar datos personales de personas requiere base legal. Si tu scraping captura datos personales, necesitas una base legítima bajo el Artículo 6 del GDPR.
- Términos de servicio — Muchos sitios prohíben el scraping en sus ToS. Revisa siempre los términos antes de automatizar peticiones.
- robots.txt — Respeta las directivas de
robots.txtcomo buena práctica, aunque no sean legalmente vinculantes en todas las jurisdicciones. - App Store Guidelines — Apple prohíbe apps que recopilan datos sin consentimiento del usuario. Si tu app hace scraping en segundo plano, decláralo claramente.
Siempre prefiere APIs oficiales cuando existan. Muchos servicios ofrecen APIs con rate limits generosos (por ejemplo, 1,000 req/hora) que son más fiables y legales que el scraping.
Puntos clave
- Usa
connectionProxyDictionarycon claveskCFNetworkProxies*para configurar proxies HTTP/HTTPS enURLSession.- La autenticación de proxy en Swift requiere header
Proxy-Authorizationmanual o unURLSessionDelegatepara el desafío 407.- SOCKS5 usa
kCFStreamPropertySOCKSProxy*en el puerto1080de ProxyHat.- Geo-targeting y sesiones se codifican en el username:
user-country-US-city-newyork-session-abc123.- Usa
TaskGroupconasync/awaitpara concurrencia estructurada y retry con backoff exponencial.- Configura
URLSessionConfiguration.ephemeralpara evitar fugas de cookies y caché entre sesiones de proxy.- Respeta CFAA, GDPR, ToS y las App Store Guidelines al hacer scraping.
Preguntas frecuentes
¿Qué es usar proxies en Swift?
Usar proxies en Swift significa enrutar el tráfico de red de tu app iOS o macOS a través de un servidor intermedio (como ProxyHat) en lugar de conectarse directamente. En URLSession, esto se logra configurando connectionProxyDictionary en URLSessionConfiguration con las claves de Core Foundation correspondientes. El proxy puede ser HTTP (puerto 8080) o SOCKS5 (puerto 1080), y soporta autenticación con geo-targeting codificado en el nombre de usuario.
¿Por qué importa usar proxies en Swift para usuarios de proxy?
Porque muchas APIs y sitios web bloquean IPs de datacenter y restringen contenido por región. Sin un proxy residencial, tu app sale por la IP del dispositivo o del cloud provider, que puede ser bloqueada o geo-restringida. Los proxies residenciales de ProxyHat enrutran el tráfico por IPs de hogares reales, reduciendo bloqueos y permitiendo acceso a contenido regional. Esto es crítico para scraping de precios, SERP tracking y testing regional de apps.
¿Qué tipo de proxy funciona mejor para usar proxies en Swift?
Los proxies residenciales son los más efectivos para la mayoría de casos de uso en Swift, porque las IPs de hogares reales son menos detectadas por sistemas anti-bot. Para volúmenes muy altos y latencia baja, los proxies datacenter pueden ser suficientes si el endpoint no bloquea IPs de cloud. Los proxies móviles ofrecen la mayor confianza pero a mayor costo. Para URLSession, el proxy HTTP en el puerto 8080 es el más compatible; SOCKS5 en el puerto 1080 es útil para tráfico no HTTP.
¿Cómo evitar bloqueos al implementar proxies en Swift?
Usa sesiones sticky con identificadores únicos por tarea para mantener consistencia de IP, rota user-agents realistas, implementa retry con backoff exponencial (2s, 4s, 8s) para manejar 429s, limita la concurrencia con httpMaximumConnectionsPerHost, y usa URLSessionConfiguration.ephemeral para evitar fugas de cookies. Además, respeta robots.txt, mantén tasas de petición razonables (por ejemplo, 1-2 req/seg por dominio) y prefiere APIs oficiales cuando existan.
¿Se puede usar SOCKS5 con URLSession en Swift?
Sí, configurando kCFStreamPropertySOCKSProxyHost, kCFStreamPropertySOCKSProxyPort (1080), kCFStreamPropertySOCKSVersion con kCFStreamSocketSOCKSVersion5, y las credenciales en kCFStreamPropertySOCKSUser y kCFStreamPropertySOCKSPassword dentro de connectionProxyDictionary. La compatibilidad puede variar entre versiones de iOS/macOS, por lo que se recomienda probar en tu target mínimo de deployment.






