Skip to content

Add Simple Proxy Protocol support for trusted UDP proxies - #4508

Open
jakemmarsh wants to merge 1 commit into
ACEmulator:masterfrom
jakemmarsh:simple-proxy-protocol
Open

jakemmarsh wants to merge 1 commit into
ACEmulator:masterfrom
jakemmarsh:simple-proxy-protocol

Conversation

@jakemmarsh

Copy link
Copy Markdown

When players connect through a UDP proxy, ACE sees every one of them at the proxy's address. That covers Cloudflare Spectrum, DDoS protection services, and relays such as a WebSocket-to-UDP bridge for a browser client. As a result, MaximumAllowedSessionsPerIPAddress, AllowUnlimitedSessionsFromIPAddresses, account create/last-login IPs and the admin session displays all see the proxy. A per-IP limit then either caps all proxied players together or can be bypassed by mixing direct and proxied connections.

This adds support for Simple Proxy Protocol (SPP), the format Cloudflare Spectrum uses for UDP. A proxy prepends a 38-byte header with the client's address to every datagram, and the server echoes the header on its replies.

Config

// Network
"TrustedProxyAddresses": []

Empty by default, which leaves behaviour unchanged.

Behaviour when an address is listed

  • Trusted proxies: datagrams from a listed address must start with a valid SPP header or they are dropped. The header is never parsed for other addresses: an ACE packet can legitimately start with the magic bytes (0x56EC as the low half of a sequence number), and a direct client must not be able to claim another address. Datagrams from any other address are processed exactly as before.
  • Session identity: sessions are still keyed by the proxy's endpoint (each proxied client has its own source port). The new Session.ClientAddress is the address from the header, or the endpoint's address when not proxied.
  • What uses the client address: the per-IP limit, the unlimited-sessions allowlist, GetUniqueSessionEndpointCount, account create/last-login IP, and /showsession and /forcelogoff, which also show "via proxy".
  • Mismatches: a packet for a proxied session whose header names a different client address is dropped.
  • Outbound: every packet sent to a proxied session carries the header received on that socket (C2S before the ConnectResponse, S2C after), as the spec requires. That includes the login rejections, so a proxied client gets "server full" and the other rejections like everyone else.
  • Non-proxied path: the same buffer sizes, offset 0 and no extra allocations. ClientPacket.Unpack and ServerPacket.CreateReadyToSendPacket take an optional offset, so the header is handled without copying.

Testing

  • Unit tests (SimpleProxyProtocolTests, 10 tests):
    • header round-trips with IPv4-mapped and IPv6 clients
    • short and bad-magic headers, including the reversed magic
    • the trusted / untrusted accept-or-drop decision
    • max packet size behind a header
    • a server packet written behind a header that is byte-identical and still passes VerifyCRC
  • End to end: ran it in Docker with MaximumAllowedSessionsPerIPAddress: 1 and full clients through an SPP relay.
    • Baseline without SPP: two players at different real addresses were rejected after the first, because both arrived from the relay's address.
    • With the relay listed in TrustedProxyAddresses:
      • Both players logged in, created characters and played (movement, NPC chat) at the same time.
      • A third login from the same real address as the second was rejected, logged as Login Request from 203.0.113.2:0 via proxy … rejected, and the client got the normal server-full message.
      • account.last_Login_I_P / create_I_P held the real addresses.
      • A datagram without a header from the trusted address was dropped.
  • CanParseStarterGearJson fails in my environment (arm64 output path) on master as well; everything else that doesn't need a database passes.

A UDP proxy in front of ACE (Cloudflare Spectrum, a DDoS shield, a relay)
makes every proxied player arrive from the proxy's address, so the per IP
session limit, the unlimited sessions allowlist, account IP addresses and
admin displays all see the proxy.

The new Network.TrustedProxyAddresses setting lists proxies that prepend
a Simple Proxy Protocol header to every datagram
(https://developers.cloudflare.com/spectrum/reference/simple-proxy-protocol-header/).

- Datagrams from a trusted proxy must start with a valid header or they
  are dropped. An ACE packet can legitimately start with the magic bytes,
  so the header is never stripped from other addresses: their datagrams
  are processed exactly as before.
- Sessions stay keyed by the proxy endpoint. Session.ClientAddress is the
  client address from the header (or the endpoint address when not
  proxied) and is used for MaximumAllowedSessionsPerIPAddress,
  AllowUnlimitedSessionsFromIPAddresses, the unique connection count,
  account create/last login IP, /showsession and /forcelogoff.
- Packets for a proxied session whose header names a different client
  address are dropped.
- Packets sent to a proxied session, including login rejections, carry
  the header received on the connection they are sent on.

Empty by default, which leaves behaviour unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant