fix(relay): handle relay-relay candidate pairs in eBPF (#10286)

Currently, the eBPF module can translate from channel data messages to
UDP packets and vice versa. It can even do that across IP stacks, i.e.
translate from an IPv6 UDP packet to an IPv4 channel data messages.

What it cannot do is handle packets to itself. This can happen if both -
Client and Gateway - pick the same relay to make an allocation. When
exchanging candidates, ICE will then form pairs between both relay
candidates, essentially requiring the relay to loop packets back to
itself.

In eBPF, we cannot do that. When sending a packet back out with
`XDP_TX`, it will actually go out on the wire without an additional
check whether they are for our own IP.

Properly handling this in eBPF (by comparing the destination IP to our
public IP) adds more cases we need to handle. The current module
structure where everything is one file makes this quite hard to
understand, which is why I opted to create four sub-modules:

- `from_ipv4_channel`
- `from_ipv4_udp`
- `from_ipv6_channel`
- `from_ipv6_udp`

For traffic arriving via a data-channel, it is possible that we also
need to send it back out via a data-channel if the peer address we are
sending to is the relay itself. Therefore, the `from_ipX_channel`
modules have four sub-modules:

- `to_ipv4_channel`
- `to_ipv4_udp`
- `to_ipv6_channel`
- `to_ipv6_udp`

For the traffic arriving on an allocation port (`from_ipX_udp`), we
always map to a data-channel and therefore can never get into a routing
loop, resulting in only two modules:

- `to_ipv4_channel`
- `to_ipv6_channel`

The actual implementation of the new code paths is rather simple and
mostly copied from the existing ones. For half of them, we don't need to
make any adjustments to the buffer size (i.e. IPv4 channel to IPv4
channel). For the other half, we need to adjust for the difference in
the IP header size.

To test these changes, we add a new integration test that makes use of
the new docker-compose setup added in #10301 and configures masquerading
for both Client and Gateway. To make this more useful, we also remove
the `direct-` prefix from all tests as the test script itself no longer
makes any decisions as to whether it is operating over a direct or
relayed connection.

Resolves: #7518
This commit is contained in:
Thomas Eizinger
2025-09-11 07:19:23 +00:00
committed by GitHub
parent 9cd25d70d8
commit 0b89959354
44 changed files with 2297 additions and 1444 deletions

View File

@@ -73,7 +73,7 @@ env:
jobs:
integration-tests:
name: ${{ matrix.test.name }}
name: ${{ matrix.test.name || matrix.test.script }}
runs-on: ubuntu-24.04
permissions:
contents: read
@@ -100,20 +100,27 @@ jobs:
fail-fast: false
matrix:
test:
- name: direct-curl-api-down
- name: direct-curl-api-restart
- name: direct-curl-gateway-restart
- name: direct-curl-ecn
- name: direct-download-packet-loss
- name: direct-dns-api-down
- name: direct-dns-two-resources
- name: direct-dns
- name: direct-download-roaming-network
- script: curl-api-down
- script: curl-api-restart
- script: curl-ecn
- script: dns
- script: dns-api-down
- script: dns-nm
- script: dns-two-resources
- script: systemd/dns-systemd-resolved
- script: tcp-dns
# Setting both client and gateway to random masquerade will force relay-relay candidate pair
- name: download-double-symmetric-nat
script: download
client_masquerade: random
gateway_masquerade: random
rust_log: debug
stop_containers: relay-2 # Force single relay
- script: download-packet-loss
rust_log: debug
- script: download-roaming-network
# Too noisy can cause flaky tests due to the amount of data
rust_log: debug
- name: dns-nm
- name: tcp-dns
- name: systemd/dns-systemd-resolved
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/ghcr-docker-login
@@ -131,6 +138,14 @@ jobs:
export RUST_LOG="${{ matrix.test.rust_log }}"
fi
if [[ -n "${{ matrix.test.client_masquerade }}" ]]; then
export CLIENT_MASQUERADE="${{ matrix.test.client_masquerade }}"
fi
if [[ -n "${{ matrix.test.gateway_masquerade }}" ]]; then
export GATEWAY_MASQUERADE="${{ matrix.test.gateway_masquerade }}"
fi
docker compose build client-router gateway-router relay-1-router relay-2-router api-router
# Start one-by-one to avoid variability in service startup order
@@ -145,6 +160,10 @@ jobs:
docker compose up -d client --no-build
docker compose up veth-config
if [[ -n "${{ matrix.test.stop_containers }}" ]]; then
docker compose stop ${{ matrix.test.stop_containers }}
fi
# Wait a few seconds for the services to fully start. GH runners are
# slow, so this gives the Client enough time to initialize its tun interface,
# for example.
@@ -162,7 +181,7 @@ jobs:
docker compose exec -T gateway sh -c 'apk add --update --no-cache iproute2-tc'
docker compose exec -T gateway sh -c 'tc qdisc add dev eth0 root netem delay 10ms'
- run: ./scripts/tests/${{ matrix.test.name }}.sh
- run: ./scripts/tests/${{ matrix.test.script }}.sh
- name: Ensure Client emitted no warnings
if: "!cancelled()"