How to inspect websocket traffic with charlesproxy for iOS simulator/devices

UPDATE JUNE 2022: Whilst socket.io-client-swift has an API to enableSOCKSProxy, it seems Starscream v4 has actually removed the built-in support for SOCKS proxying, so this option doesn't actually do anything!

So it looks like we're back to manually patching Starscream to enable SOCKS proxying.

Just to recap: we've gone from Starscream supporting SOCKS proxy but socket.io-client-swift not having an API to enable it, to now socket.io-client-swift having an API to enable SOCKS proxying, which Starscream no longer supports 🤣🙄.


UPDATE JUNE 2019: Apparently socket.io-client-swift v15.1.0 now properly supports SOCKS proxy. I have not yet tried it, but it would mean that these manual edits to Starscream are no longer required.


The accepted answer does not seem to work with Socket.IO on iOS devices.

The latest version of Socket.IO-Client-Swift (15.0.0 at the time of writing) uses Starscream for WebSockets on iOS/OS X.

The good news is that Starscream supports SOCKS proxying however:

  1. Socket.IO does not expose the Starscream websocket or provide any API for enabling the SOCKS proxying behaviour.

  2. The SOCKS proxying built into Starscream uses the OS SOCKS proxy settings which are cumbersome to setup (at least for iOS).

If I get some time I might propose a PR to address this more thoroughly, but given that it requires work to both Starscream and Socket.IO-Client-Swift, this is not entirely straightforward.

The easiest way to hack around this for temporary debugging purposes (which is the use case for Charles!), is to edit the WebSocket.swift file as part of Starscream, and replace this code:

if enableSOCKSProxy {
    let proxyDict = CFNetworkCopySystemProxySettings()
    let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue())
    let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy)
    CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig)
    CFReadStreamSetProperty(inputStream, propertyKey, socksConfig)
}

with this code:

let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, CFNetworkCopySystemProxySettings()!.takeRetainedValue()) as! [String: Any]
let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy)
let ip = socksConfig["HTTPSProxy"]
let proxySocksConfig = ["SOCKSProxy": ip, "SOCKSPort": 8889, "SOCKSEnable": true] as CFDictionary // Where 8889 is the SOCKS proxy port in Charles
CFWriteStreamSetProperty(outputStream, propertyKey, proxySocksConfig)
CFReadStreamSetProperty(inputStream, propertyKey, proxySocksConfig)

This will ensure the SOCKS proxy is enabled by default, and will route all the websockets traffic via Charles.

You then need to ensure that the HTTP proxy settings are configured in iOS (since the same IP will be used for both HTTP and SOCKS), SOCKS proxy is enabled in Charles, and that the port matches the port in the code above (by default 8889).


Thanks for your very very helpful answer Jonathan Ellis! I'm using Pusher and this worked great!

However, I found socksConfig to not always contain valid data and didn't work or would crash the app when I pulled the IP from there. Since the only thing we are getting from there is the localhost IP I just replaced the following in WebSocket.swift

if enableSOCKSProxy {
    let proxyDict = CFNetworkCopySystemProxySettings()
    let socksConfig = CFDictionaryCreateMutableCopy(nil, 0, proxyDict!.takeRetainedValue())
    let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy)
    CFWriteStreamSetProperty(outputStream, propertyKey, socksConfig)
    CFReadStreamSetProperty(inputStream, propertyKey, socksConfig)
}

with this:

let propertyKey = CFStreamPropertyKey(rawValue: kCFStreamPropertySOCKSProxy)
let proxySocksConfig = ["SOCKSProxy": "127.0.0.1", "SOCKSPort": 8889, "SOCKSEnable": true] as CFDictionary // Where 8889 is the SOCKS proxy port in Charles
CFWriteStreamSetProperty(outputStream, propertyKey, proxySocksConfig)
CFReadStreamSetProperty(inputStream, propertyKey, proxySocksConfig)

And then enabled the socks proxy in Charles as you described.

Thanks Again!


I finally found the answer.

Charles 3.11.2 works perfectly with WebSocket.

I use socketIO, so I've already seen http requests sent during the negotiation phase, but I missed websockets traffic.

In the beginning, socketIO try to use polling then switches to use websockets.

The websocket traffic is visible when you go to the request with status: "Sending request body" which is actually wss:// request.

You even have a dedicated tab for this kind of traffic. The rest of messages will appear right there.

enter image description here

PS1. Ensure you're connected to socket properly then it appears in Charles.
PS2. I suggest using socketIO it's a great enhancement for full-duplex traffic like websockets.