Skip to content

How it works

Nothing in the apps you use changes. An app connects to a server’s address as usual, and macOS hands the connection to the Periscopes network extension before anything leaves the Mac. The extension opens its own connection to the Riptides proxy, tells it where the app wanted to go, and copies bytes between the two.

THIS MAC INTERNET Client app browser, AI agent… Network extension Periscopes Riptides proxy 127.0.0.1:8089 Origin server 160.79.104.10:443 TCP :443 flow PROXY header + raw bytes its own connection proxy unreachable: direct, not inspected

Every outbound TCP connection on ports 80 and 443 takes this path. The extension announces the real destination with a PROXY v2 header, and the proxy makes its own connection to the server. That second connection passes the extension too, which recognizes the proxy and lets it go straight out. If the proxy can’t be reached, the extension connects directly, so the Mac stays online and the traffic goes uninspected.

  • The Periscopes app sets things up and shows the state in the menu bar. See Using Periscopes.
  • The network extension is where connections are taken over. It’s a system extension built on Apple’s Network Extension framework, as a transparent proxy provider.
  • The Riptides proxy inspects the traffic. Periscopes sends it connections at 127.0.0.1:8089 by default.

What the extension does with each connection

Section titled “What the extension does with each connection”

The extension claims outbound TCP to any address on ports 80 and 443, for both IPv4 and IPv6. For each connection it:

  1. Lets the proxy’s own connections go, so the proxy’s connection to the server doesn’t loop back into it.
  2. Works out the destination. Usually that’s an IP address. If the app connected by hostname, the extension resolves it, because the PROXY header has no room for names.
  3. Finds the local address and port the app connected from, so the proxy knows the real source.
  4. Connects to the proxy, sends a PROXY v2 header as the first bytes, then copies bytes in both directions until either side closes.

If the proxy doesn’t answer, the extension connects to the destination directly and marks the proxy as down. While it’s down, new connections skip the extension entirely. A check every 5 seconds marks the proxy as up again once it answers.

Connections an app opened before routing started stay direct until the app reconnects.

The extension can’t send an HTTP CONNECT request, because it takes over connections the app has already opened. Instead it puts a PROXY protocol v2 header, the format HAProxy introduced, in front of each one. For an IPv4 connection from 192.168.1.23:51820 to 160.79.104.10:443, that’s 28 bytes:

| Bytes | Field | Value | |---|---|---| | 0–11 | Signature | 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A | | 12 | Version and command | 21: version 2, PROXY | | 13 | Family and protocol | 11: IPv4 over TCP | | 14–15 | Address length | 00 0C: 12 bytes | | 16–19 | Source address | C0 A8 01 17: 192.168.1.23 | | 20–23 | Destination address | A0 4F 68 0A: 160.79.104.10 | | 24–25 | Source port | CA 6C: 51820 | | 26–27 | Destination port | 01 BB: 443 |

An IPv6 connection uses 21 as the family byte and a 36-byte address block. When the extension can’t work out the destination, it sends a LOCAL header with no addresses, and the proxy closes the connection.

The extension only claims TCP. UDP isn’t routed, so DNS and QUIC go out as usual. Browsers that use HTTP/3 (which runs over QUIC) to Google, Cloudflare and similar hosts bypass the proxy.