Usare proxy in Swift con URLSession: guida pratica per iOS e macOS

Una guida code-first per configurare proxy residenziali in Swift tramite URLSession: HTTP/HTTPS, SOCKS5, autenticazione, geo-targeting, async/await e best practice di produzione.

Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

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-Authorization header è 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.txt del 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 connectionProxyDictionary con kCFNetworkProxiesHTTP* e kCFNetworkProxiesHTTPS* per HTTP/HTTPS, kCFStreamPropertySOCKSProxy* per SOCKS5.
  • Autenticazione: le chiavi native kCFProxyUsernameKey sono inaffidabili — usa header Proxy-Authorization: Basic o implementa urlSession(_:didReceive:).
  • Geo-targeting: codifica paese, città e sessione nello username: user-country-US-city-newyork-session-abc123.
  • Porte: HTTP/HTTPS su 8080, SOCKS5 su 1080, sempre tramite gate.proxyhat.com.
  • Produzione: implementa retry con backoff esponenziale e jitter, usa TaskGroup per 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.

Pronto per iniziare?

Accedi a oltre 50M di IP residenziali in oltre 148 paesi con filtraggio AI.

Vedi i prezziProxy residenziali
← Torna al Blog