Questions about Starting a Proxy Server in iOS Using SwiftNIO

I want to start a proxy server in iOS, specifically for iOS 17 and above, to return some requests from the WebView with local resources. I've searched online but couldn't find any information on how to start a proxy HTTPS server in iOS. Eventually, I came across SwiftNIO, which indicates that I'm not really familiar with it.

I have several questions. Can SwiftNIO support these features?

  1. How can I start a proxy server?
  2. How to adapt to the foreground - background switching?
  3. When intercepting HTTP/HTTPS requests, for HTTPS, since the server's certificate can't be placed on the client side, how can I support self - signed certificates to handle data?
  4. For resources that are not available locally, how can I convert the requests to directly access the original server?
  5. Is it necessary to use Network Extension?

I'm really looking forward to getting some help. Good luck to you! :four_leaf_clover:

There are definitely folks who have used NIO to power proxy servers on iOS before. I believe Proxyman does this.

SwiftNIO has a number of HTTP/HTTPS server examples floating around. Any of these would do. The connect-proxy is a common example, though it doesn't do exactly what you want in the HTTPS case.

If you're providing this via network extension, I don't believe you have to.

If you're just wanting this to run while your app is foregrounded, that's a different story. In that case, the best thing to do is to close the server while you're being backgrounded, and restart it when you get foregrounded again.

The iOS behaviour of invalidating open file descriptors produces errors that will make SwiftNIO very unhappy, and they can be hard to debug in your app. So it's easiest to just avoid them.

This one is somewhat tricky, but you basically have to arrange for your user to install a root certificate that is produced by your app. You can then mint self-signed certificates on the fly to support each website the user is trying to reach.

This is going to be far and away the most complex part of the process if you're not familiar with TLS.

If you can arrange to control just a specific URLSession configuration, you can instead override the trust challenge to trust your self-signed root. That makes life a little easier.

The main thing you'd do is rewrite the request, and then use a HTTP client to issue the new one. That mostly involves forwarding through headers and other things.

I suspect it isn't, if you just want to proxy for your specific app.

Dear lukasa,

Thank you very much for your response. The following content might not be directly related to SwiftNIO. Some of it is just a record of my experimental attempts over the past couple of days.

As we all know, these experimental attempts have failed. Could you take a look at them? This might be of interest to many people.

  1. Understood. I'll experiment with how to start a proxy server through NIO.

  2. This product will only be an ordinary APP. There are no plans to apply for Network Extension permissions. We only need to ensure that the proxy can be used normally within the APP.

  3. Over the past two days, I've tried to create an HTTP server on iOS using Telegraph (a simple Swift - based HTTP server framework).

  • 3.1 According to its tutorial documentation, I created a DER and a P12 file, and used them to create a certificate.
  • 3.2 I created an HTTPS server, set it on localhost, with a DEF file and started the server.
  • 3.3 I used URLSession:didReceive challenge:completionHandler to handle certificate verification.
  • 3.4 The request was successfully returned.
  • 3.5 Then I started trying to use it as a proxy server.
  • 3.6 While keeping the other code of URLSession, I added proxy configuration in the SessionConfig where the request was initiated.
let proxyEndpoint = NWEndpoint.hostPort(host:.name("localhost", nil), port: 9000)
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
    sec_protocol_verify_complete(true)
}, DispatchQueue.global())
  • 3.7 I received an error message:
Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x2818962e0>, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, kCFStreamErrorDomainKey=3, kCFStreamErrorCodeKey=-9802......
  • 3.8 I confirmed that the proxy method was called before the error log was output, and I made a callback:
completionHandler(.useCredential, init(trust: challenge.protectionSpace.serverTrust!))
  1. If I configure allowFailover, in SwiftNIO, do I not need to initiate the request to the remote server myself?
proxyConfig.allowFailover = true, 
  1. Just as the second point in your reply, we are only doing proxy work within the app.

Ah, yeah, don't set this up as a proxy for URLSession. When URLSession tries to proxy HTTPS through your server, it will actually use the HTTP CONNECT method, which establishes a TCP tunnel. I suspect your server isn't doing that.

Instead, rewrite the HTTP queries to entirely point at localhost.

Yes, when compared to directly making a request to localhost, when acting as a proxy server, it received a CONNECT request before receiving a GET request. I debugged the internal part of the framework and found that it maintains this connection internally before serving as a proxy server.

When functioning as a proxy server, I received the CONNECT request and immediately returned a Response 200 for it. Then, I trusted the certificate in the callback of the URLSession's Delegate method. However, an error still occurred in the end. (The details of the error are in the previous post.)

The process of using URLSession and configuring the proxy was just a test to expose some issues and verify the feasibility of being a proxy. In iOS, we can indeed directly hook URLSession and modify the Request and Response.

In the scenario of intercepting WkWebView requests, we have several options:

  • If we hook the XMLRequest and replace the domain name with localhost, it will likely encounter a series of cross-origin security issues.
  • If we use the unconventional URLSchemeHandler of WkWebView to intercept requests, this solution will also have many compatibility issues.
  • The ideal solution would be to have a mature HTTP proxy server, something like a simplified version of Charles/ProxyMan, running within the APP.

Due to time constraints, in the project, I could only choose to use URLScheme to intercept requests. I'm not even sure whether there were errors in my process or if the solution is simply not feasible.

Compared with Telegraph, SwiftNIO is more flexible. I will continue to verify its functionality in my spare time. If we can succeed in this direction, it will significantly enhance the user experience for many APPs.

I would be grateful if someone could provide a definite conclusion or a feasible guidance plan.

If you really want it configured as a proxy server, you need to intercept the subsequent HTTPS request that follows the CONNECT request. You can do that, by adding a SSL handler after the CONNECT and using that to terminate the TLS directly.