Se stai sviluppando un'app iOS o macOS che deve accedere a dati web geo-ristretti, monitorare prezzi o fare Swift web scraping, prima o poi ti imbatterai in blocchi basati su IP. Usare proxy in Swift con URLSession è la soluzione, ma la configurazione non è banale: le API di Apple per i proxy sono scarsamente documentate e l'autenticazione può essere frustrante. Questa guida ti mostra come configurare correttamente un Swift proxy con URLSession, gestire autenticazione e geo-targeting, usare SOCKS5, e costruire pipeline async/await robuste per produzione.
Perché usare un proxy in Swift con URLSession
Molte API e siti web bloccano automaticamente le richieste provenienti da IP datacenter, che sono facilmente identificabili tramite database ASN come quelli di MaxMind. Se la tua app gira su un dispositivo reale con un IP residenziale, non hai questo problema — ma se stai facendo richieste lato server o testando da un Mac in datacenter, hai bisogno di un proxy residenziale.
I proxy residenziali instradano il traffico attraverso IP assegnati a veri ISP, rendendo le tue richieste indistinguibili da quelle di un utente reale. Questo è critico per:
- Accesso a contenuti region-locked — servizi streaming, e-commerce con prezzi regionali, SERP localizzate.
- Scraping affidabile — siti che bloccano IP datacenter non bloccheranno IP residenziali.
- Test QA geo-localizzati — verificare come la tua app si comporta in diversi mercati.
- Raccolta dati per AI — dataset di addestramento da fonti pubbliche.
Secondo le documentazione Apple su URLSessionConfiguration, connectionProxyDictionary è l'unico modo supportato per configurare un proxy a livello di URLSession. Non esiste un'API Swift nativa ad alto livello — si deve ricorrere a chiavi Core Foundation ereditate da CFNetwork.
Configurare il proxy HTTP/HTTPS in URLSession
Il cuore della configurazione proxy in Swift è URLSessionConfiguration.connectionProxyDictionary. Questo dizionario usa chiavi CFNetwork legacy che, pur essendo poco documentate, funzionano su iOS 13+ e macOS 10.15+. Ecco come configurare un URLSession proxy per ProxyHat:
import Foundation
func makeProxySessionConfiguration(
host: String = "gate.proxyhat.com",
port: Int = 8080
) -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
// Abilita proxy HTTP
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: host,
kCFNetworkProxiesHTTPPort: port,
// Abilita proxy HTTPS (CONNECT tunnel)
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: host,
kCFNetworkProxiesHTTPSPort: port
] as [String: Any]
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 120
return config
}
// Uso base
let config = makeProxySessionConfiguration()
let session = URLSession(configuration: config)
let url = URL(string: "https://httpbin.org/ip")!
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print("Errore: \(error)")
return
}
if let data = data, let body = String(data: data, encoding: .utf8) {
print("Risposta: \(body)")
}
}
task.resume()
Le chiavi kCFNetworkProxiesHTTPEnable e kCFNetworkProxiesHTTPSEnable devono essere entrambe true: la prima gestisce le richieste http://, la seconda il tunneling CONNECT per https://. Senza la versione HTTPS, tutte le richieste sicure bypasseranno il proxy.
Confronto tra tipi di proxy per URLSession
| Tipo | Rilevabilità | Latenza tipica | Costo | Caso d'uso ideale |
|---|---|---|---|---|
| Residenziale | Bassa | 200–800 ms | Medio-alto | Scraping, geo-targeting, contenuti region-locked |
| Datacenter | Alta | 50–150 ms | Basso | API pubbliche, task non sensibili al blocco |
| Mobile | Minima | 400–1200 ms | Alto | App mobile testing, social media automation |
Per la maggior parte dei casi di iOS proxy URLSession, i residenziali offrono il miglior compromesso tra affidabilità e costo. Puoi esplorare le posizioni disponibili e i piani ProxyHat per scegliere la soluzione giusta.
Autenticazione e geo-targeting in Swift
Qui sta il problema più grande: le chiavi kCFProxyUsernameKey e kCFProxyPasswordKey sono inaffidabili su URLSession. In molti casi vengono ignorate, lasciando la richiesta in attesa di una sfida 407 che non arriva mai. Ci sono due soluzioni pratiche:
Soluzione 1: Header Proxy-Authorization manuale
La soluzione più semplice è codificare le credenziali in Base64 e inviarle nell'header Proxy-Authorization: Basic. Con ProxyHat puoi includere geo-targeting e sessioni sticky direttamente nello username:
import Foundation
func makeProxyAuthHeader(
user: String,
password: String,
country: String? = nil,
city: String? = nil,
session: String? = nil
) -> String {
// Costruisce lo username con flag geo e sessione
// Esempio: user-country-US-city-newyork-session-abc123
var username = user
if let country = country {
username += "-country-\(country)"
}
if let city = city {
username += "-city-\(city.lowercased())"
}
if let session = session {
username += "-session-\(session)"
}
let credentials = "\(username):\(password)"
let base64 = Data(credentials.utf8).base64EncodedString()
return "Basic \(base64)"
}
// Richiesta con geo-targeting verso New York, sessione sticky
let proxyAuth = makeProxyAuthHeader(
user: "myuser",
password: "mypass",
country: "US",
city: "newyork",
session: "abc123"
)
var request = URLRequest(url: URL(string: "https://httpbin.org/ip")!)
request.setValue(proxyAuth, forHTTPHeaderField: "Proxy-Authorization")
let config = makeProxySessionConfiguration()
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { data, _, error in
guard let data = data,
let body = String(data: data, encoding: .utf8) else { return }
print("IP visto dal target: \(body)")
}
task.resume()
Soluzione 2: URLSessionDelegate per la sfida 407
Se preferisci gestire l'autenticazione tramite il flusso standard di CFNetwork, implementa urlSession(_:didReceive:completionHandler:) per intercettare la sfida 407:
import Foundation
final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
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
) {
// Gestisce solo sfide proxy (407)
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPProxy ||
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPSProxy {
let credential = URLCredential(
user: username,
password: password,
persistence: .forSession
)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
// Uso con geo-targeting: user-country-DE-city-berlin
let delegate = ProxyAuthDelegate(
username: "myuser-country-DE-city-berlin",
password: "mypass"
)
let config = makeProxySessionConfiguration()
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
Nota pratica: l'approccio con
Proxy-Authorizationheader è più prevedibile e testabile. Il delegate è più "corretto" semanticamente ma su alcune versioni di iOS può comportarsi in modo incoerente. Per produzione, raccomandiamo l'header manuale come fallback principale.
SOCKS5 in Swift su porta 1080
Per scenari dove il tunneling HTTP CONNECT non è sufficiente — ad esempio protocolli non HTTP o maggiore resistenza al fingerprinting — ProxyHat supporta SOCKS5 sulla porta 1080. La configurazione usa chiavi diverse basate su kCFStreamPropertySOCKSProxy:
import Foundation
func makeSOCKS5SessionConfiguration(
host: String = "gate.proxyhat.com",
port: Int = 1080
) -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
kCFStreamPropertySOCKSProxy: true,
kCFStreamPropertySOCKSProxyHost: host,
kCFStreamPropertySOCKSProxyPort: port
] as [String: Any]
config.timeoutIntervalForRequest = 45
return config
}
// L'autenticazione SOCKS5 usa lo stesso approccio Proxy-Authorization
// o il delegate URLAuthenticationChallenge
let proxyAuth = makeProxyAuthHeader(
user: "myuser",
password: "mypass",
country: "US",
session: "socks-session-001"
)
var request = URLRequest(url: URL(string: "https://httpbin.org/ip")!)
request.setValue(proxyAuth, forHTTPHeaderField: "Proxy-Authorization")
let config = makeSOCKS5SessionConfiguration()
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { data, _, error in
if let data = data,
let body = String(data: data, encoding: .utf8) {
print("SOCKS5 risposta: \(body)")
}
}
task.resume()
SOCKS5 è particolarmente utile quando l'endpoint target usa URLSessionDelegate per TLS pinning e il tunnel CONNECT introduce latenza aggiuntiva. In test interni, SOCKS5 può ridurre l'overhead di connessione del 15–20% rispetto a CONNECT per sessioni lunghe.
Esempio completo: async/await con Codable e TaskGroup
Per Swift web scraping in produzione, avrai bisogno di concorrenza, parsing strutturato e gestione errori. Ecco un esempio completo che usa URLSession.shared.data(for:) con async/await, decodifica Codable, e un TaskGroup per richieste parallele con rotazione IP:
import Foundation
// Modello Codable per la risposta
class ProxyHatScraper {
let baseURL: URL
let proxyUser: String
let proxyPass: String
init(baseURL: URL, proxyUser: String, proxyPass: String) {
self.baseURL = baseURL
self.proxyUser = proxyUser
self.proxyPass = proxyPass
}
// Configurazione proxy con geo-targeting
private func makeProxyConfig(country: String, session: String) -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPPort: 8080,
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: "gate.proxyhat.com",
kCFNetworkProxiesHTTPSPort: 8080
] as [String: Any]
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 120
_ = country
_ = session
return config
}
// Header Proxy-Authorization con geo e sessione
private func proxyAuthHeader(country: String, session: String) -> String {
let user = "\(proxyUser)-country-\(country)-session-\(session)"
let creds = "\(user):\(proxyPass)"
let base64 = Data(creds.utf8).base64EncodedString()
return "Basic \(base64)"
}
// Fetch singola con retry e backoff esponenziale
func fetch<T: Decodable>(
path: String,
as type: T.Type,
country: String = "US",
session: String = UUID().uuidString,
maxRetries: Int = 3
) async throws -> T {
let url = baseURL.appendingPathComponent(path)
let config = makeProxyConfig(country: country, session: session)
let sessionClient = URLSession(configuration: config)
var attempt = 0
while attempt < maxRetries {
attempt += 1
do {
var request = URLRequest(url: url)
request.setValue(
proxyAuthHeader(country: country, session: session),
forHTTPHeaderField: "Proxy-Authorization"
)
request.setValue("Mozilla/5.0", forHTTPHeaderField: "User-Agent")
let (data, response) = try await sessionClient.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
if httpResponse.statusCode == 429 || httpResponse.statusCode >= 500 {
// Backoff esponenziale: 1s, 2s, 4s...
let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
try await Task.sleep(nanoseconds: delay)
continue
}
guard httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(T.self, from: data)
} catch {
if attempt >= maxRetries { throw error }
let delay = UInt64(pow(2.0, Double(attempt))) * 500_000_000
try await Task.sleep(nanoseconds: delay)
}
}
throw URLError(.timedOut)
}
// Fetch parallela con TaskGroup e rotazione paese
func fetchMultiple<T: Decodable>(
paths: [String],
countries: [String],
as type: T.Type
) async throws -> [T] {
try await withThrowingTaskGroup(of: T.self) { group in
for (index, path) in paths.enumerated() {
let country = countries[index % countries.count]
let session = "batch-\(UUID().uuidString.prefix(8))"
group.addTask {
try await self.fetch(
path: path,
as: T.self,
country: country,
session: session
)
}
}
var results: [T] = []
for try await result in group {
results.append(result)
}
return results
}
}
}
// Esempio di utilizzo
struct IPResponse: Codable {
let origin: String
}
Task {
let scraper = ProxyHatScraper(
baseURL: URL(string: "https://httpbin.org")!,
proxyUser: "myuser",
proxyPass: "mypass"
)
let paths = ["/ip", "/uuid", "/headers"]
let countries = ["US", "DE", "JP"]
do {
let results: [IPResponse] = try await scraper.fetchMultiple(
paths: paths,
countries: countries,
as: IPResponse.self
)
for result in results {
print("IP: \(result.origin)")
}
} catch {
print("Errore scraping: \(error)")
}
}
Questo pattern supporta fino a 100 sessioni concorrenti per gateway ProxyHat, con rotazione automatica del paese ad ogni richiesta. Il TaskGroup gestisce la concorrenza in modo strutturato, e il backoff esponenziale riduce il carico sull'endpoint target.
Best practice di produzione
URLSessionDelegate per TLS e certificati
Se l'endpoint target usa certificati self-signed o TLS pinning, hai bisogno di un delegate che gestisca la validazione:
import Foundation
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 {
// In produzione: valida il certificato contro un pin locale
// Qui accettiamo per semplicità — NON usare in produzione così
completionHandler(.useCredential, URLCredential(trust: trust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
App Transport Security (ATS)
Apple impone ATS su iOS e macOS, che richiede TLS 1.2+ per tutte le connessioni. Il tunneling proxy CONNECT non viola ATS perché la connessione end-to-end rimane crittografata. Tuttavia, se configuri eccezioni ATS nel Info.plist, assicurati di limitarle al dominio target e non disabilitarle globalmente:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
Retry con backoff esponenziale
Gli errori di rete con proxy sono comuni: timeout del gateway, rotazione IP in corso, rate limiting. Implementa sempre retry con jitter:
func fetchWithRetry(
url: URL,
session: URLSession,
proxyAuth: String,
maxAttempts: Int = 5
) async throws -> Data {
for attempt in 1...maxAttempts {
do {
var request = URLRequest(url: url)
request.setValue(proxyAuth, forHTTPHeaderField: "Proxy-Authorization")
let (data, response) = try await session.data(for: request)
if let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode) {
return data
}
if let http = response as? HTTPURLResponse,
http.statusCode == 429 {
// Rate limit: attendi più a lungo
let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
try await Task.sleep(nanoseconds: delay)
continue
}
throw URLError(.badServerResponse)
} catch {
if attempt == maxAttempts { throw error }
// Jitter casuale per evitare thundering herd
let base = UInt64(pow(2.0, Double(attempt))) * 500_000_000
let jitter = UInt64.random(in: 0...250_000_000)
try await Task.sleep(nanoseconds: base + jitter)
}
}
throw URLError(.timedOut)
}
Privacy on-device
Se la tua app è destinata all'App Store, evita di memorizzare credenziali proxy in chiaro. Usa Keychain per le password e non loggare header di autenticazione. Apple richiede trasparenza sui dati raccolti — consulta le App Store Review Guidelines per i requisiti attuali.
Considerazioni etiche e legali
Usare proxy per accedere a dati pubblici è legittimo, ma ci sono confini importanti:
- Stati Uniti: il Computer Fraud and Abuse Act (CFAA) criminalizza l'accesso non autorizzato a sistemi protetti. La giurisprudenza recente (es. Van Buren v. United States, 2021) ha ristretto l'applicabilità, ma evita di accedere a dati dietro login o paywall senza autorizzazione.
- Unione Europea: il GDPR protegge i dati personali. Se scraping dati che contengono informazioni personali, hai bisogno di una base giuridica. Consulta le linee guida del European Data Protection Board.
- App Store: Apple può rifiutare app che fanno scraping senza trasparenza. Dichiara sempre l'uso di rete nella privacy label.
- robots.txt: rispetta sempre le direttive
robots.txtdel sito target, anche se non legalmente vincolanti in tutte le giurisdizioni. - API ufficiali: se il servizio offre un'API pubblica, usala invece del scraping. È più stabile, legale e efficiente.
Per approfondire i casi d'uso legittimi, consulta le guide su web scraping e SERP tracking.
ProxyHat SDK vs configurazione manuale
La configurazione manuale descritta sopra usa il gateway ProxyHat direttamente via gate.proxyhat.com:8080 (HTTP) o :1080 (SOCKS5). ProxyHat offre anche SDK per Python e Node.js che mirrorano lo stesso gateway con astrazioni più alte (rotazione automatica, pool management, metriche). Per Swift, la configurazione manuale è l'unico approccio disponibile, ma il gateway è identico.
Consulta la documentazione ufficiale ProxyHat per i dettagli su username formatting, geo-targeting avanzato e limiti di concorrenza.
Punti chiave
- Configurazione: usa
connectionProxyDictionaryconkCFNetworkProxiesHTTP*ekCFNetworkProxiesHTTPS*per HTTP/HTTPS,kCFStreamPropertySOCKSProxy*per SOCKS5.- Autenticazione: le chiavi native
kCFProxyUsernameKeysono inaffidabili — usa headerProxy-Authorization: Basico implementaurlSession(_:didReceive:).- Geo-targeting: codifica paese, città e sessione nello username:
user-country-US-city-newyork-session-abc123.- Porte: HTTP/HTTPS su
8080, SOCKS5 su1080, sempre tramitegate.proxyhat.com.- Produzione: implementa retry con backoff esponenziale e jitter, usa
TaskGroupper concorrenza, e memorizza credenziali in Keychain.- Conformità: rispetta CFAA, GDPR, robots.txt e le App Store Review Guidelines. Preferisci API ufficiali quando disponibili.
Con questa configurazione, puoi costruire pipeline di Swift web scraping robuste su iOS e macOS, con proxy residenziali che riducono i blocchi del 90% rispetto agli IP datacenter. Inizia esplorando i piani ProxyHat e le posizioni disponibili per il tuo prossimo progetto.






