mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 06:07:00 +02:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10e9514e44 | |||
| 913a37cfee | |||
| 6c3972f464 | |||
| 861d3aabd7 | |||
| 86733864fe | |||
| ab736e4c6b | |||
| 5ecdd4eaa9 | |||
| 1b84bd0050 | |||
| 384610f81a | |||
| c1eea118f4 | |||
| 1e66c0d3ee | |||
| e5c0fdad8d | |||
| 7bd0bc285a | |||
| 942ee522e0 | |||
| 32149f3a93 | |||
| 19ad3bb904 | |||
| 647775d8c3 | |||
| abfeb502a8 | |||
| 0a953915bb | |||
| 6aa3363d85 | |||
| 95d98b1f4b | |||
| 6afca0f461 | |||
| 02471b4121 | |||
| 58ab7250f5 | |||
| 184cdc8586 | |||
| 7d3166a19d | |||
| fe1c5682f0 | |||
| e4cc80aaca | |||
| 16b302c11d | |||
| ab539f8a3f | |||
| b7d83b0500 | |||
| ef95b25fa3 | |||
| 36b38396af | |||
| 2e9117da5b | |||
| a690c904ba | |||
| e028e6bf1a | |||
| 3db406b8ac | |||
| eaad4896c1 | |||
| e6032f81aa | |||
| b041f306cb | |||
| 3a95495c63 | |||
| 873f94f465 | |||
| 72bad1603a | |||
| 0c1ad9bb48 | |||
| 074a123a4b | |||
| 04dea41f74 | |||
| 0d23377c65 | |||
| ffd5249cf5 | |||
| 625f58b84a | |||
| 99c5854e5c | |||
| 3c121e7ab1 | |||
| 6c7ebb0875 | |||
| 110ea8f45c |
@@ -25,9 +25,9 @@ inputs:
|
||||
required: false
|
||||
default: "code-signer"
|
||||
key-prefix:
|
||||
description: "S3 key prefix the caller is authorized to write under"
|
||||
description: "S3 key prefix to write under; defaults to code-signing/<owner>/<repo> of the calling repo"
|
||||
required: false
|
||||
default: "code-signing/slackhq/nebula"
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -57,6 +57,9 @@ runs:
|
||||
KEY_PREFIX: ${{ inputs.key-prefix }}
|
||||
run: |
|
||||
set -eu
|
||||
# Default the prefix to this repo so the S3 key attributes the sign correctly.
|
||||
# nebula-nightly runs this same action but writes under its own repo's prefix.
|
||||
KEY_PREFIX="${KEY_PREFIX:-code-signing/$GITHUB_REPOSITORY}"
|
||||
RUN="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
|
||||
find "$SIGN_PATH" -name '*.exe' -print | while read -r path
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
name: gofmt
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/gofmt.yml'
|
||||
- '**.go'
|
||||
jobs:
|
||||
|
||||
gofmt:
|
||||
name: Run gofmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Install goimports
|
||||
run: |
|
||||
go install golang.org/x/tools/cmd/goimports@latest
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
if [ "$(find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -l)" ]
|
||||
then
|
||||
find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -d
|
||||
exit 1
|
||||
fi
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
name: Build Linux/BSD All
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
HAS_SIGNING_CREDS: ${{ secrets.AC_USERNAME != '' }}
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
# be overwritten
|
||||
- name: Checkout code
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Download artifacts
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
@@ -163,14 +163,17 @@ jobs:
|
||||
mkdir -p build/linux-{amd64,arm64}
|
||||
tar -zxvf artifacts/nebula-linux-amd64.tar.gz -C build/linux-amd64/
|
||||
tar -zxvf artifacts/nebula-linux-arm64.tar.gz -C build/linux-arm64/
|
||||
docker buildx build . --push -f docker/Dockerfile --platform linux/amd64,linux/arm64 --tag "${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG}" --tag "${DOCKER_IMAGE_REPO}:${GITHUB_REF#refs/tags/v}"
|
||||
docker buildx build . --push -f docker/Dockerfile --platform linux/amd64,linux/arm64 \
|
||||
--build-arg VERSION="${GITHUB_REF#refs/tags/v}" \
|
||||
--build-arg REVISION="${GITHUB_SHA}" \
|
||||
--tag "${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG}" --tag "${DOCKER_IMAGE_REPO}:${GITHUB_REF#refs/tags/v}"
|
||||
|
||||
release:
|
||||
name: Create and Upload Release
|
||||
needs: [build-linux, build-darwin, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
VAGRANT_DEFAULT_PROVIDER: libvirt
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
VAGRANT_DEFAULT_PROVIDER: virtualbox
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -36,6 +36,14 @@ jobs:
|
||||
working-directory: ./.github/workflows/smoke
|
||||
run: ./smoke.sh
|
||||
|
||||
- name: setup docker image ipv6
|
||||
working-directory: ./.github/workflows/smoke
|
||||
run: SMOKE_OVERLAY_IPV6=1 ./build.sh
|
||||
|
||||
- name: run smoke ipv6
|
||||
working-directory: ./.github/workflows/smoke
|
||||
run: SMOKE_OVERLAY_IPV6=1 ./smoke.sh
|
||||
|
||||
- name: setup relay docker image
|
||||
working-directory: ./.github/workflows/smoke
|
||||
run: ./build-relay.sh
|
||||
|
||||
@@ -5,6 +5,19 @@ set -e -x
|
||||
rm -rf ./build
|
||||
mkdir ./build
|
||||
|
||||
if [ "$SMOKE_OVERLAY_IPV6" ]
|
||||
then
|
||||
LIGHTHOUSE_NIP="fd00:4242:0:0:0:ffff:c0a8:6401"
|
||||
HOST2_NIP="fd00:4242:0:0:0:ffff:c0a8:6402"
|
||||
HOST3_NIP="fd00:4242:0:0:0:ffff:c0a8:6403"
|
||||
HOST4_NIP="fd00:4242:0:0:0:ffff:c0a8:6404"
|
||||
else
|
||||
LIGHTHOUSE_NIP="192.168.100.1"
|
||||
HOST2_NIP="192.168.100.2"
|
||||
HOST3_NIP="192.168.100.3"
|
||||
HOST4_NIP="192.168.100.4"
|
||||
fi
|
||||
|
||||
# Smoke containers run on a dedicated docker network whose subnet is allocated
|
||||
# at smoke time, not known at build time. Configs are written with TEST-NET-3
|
||||
# placeholder IPs (RFC 5737) and smoke.sh / smoke-vagrant.sh / smoke-relay.sh
|
||||
@@ -31,24 +44,24 @@ LIGHTHOUSE_IP="203.0.113.2"
|
||||
../genconfig.sh >lighthouse1.yml
|
||||
|
||||
HOST="host2" \
|
||||
LIGHTHOUSES="192.168.100.1 $LIGHTHOUSE_IP:4242" \
|
||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||
../genconfig.sh >host2.yml
|
||||
|
||||
HOST="host3" \
|
||||
LIGHTHOUSES="192.168.100.1 $LIGHTHOUSE_IP:4242" \
|
||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||
INBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
||||
../genconfig.sh >host3.yml
|
||||
|
||||
HOST="host4" \
|
||||
LIGHTHOUSES="192.168.100.1 $LIGHTHOUSE_IP:4242" \
|
||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||
OUTBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
||||
../genconfig.sh >host4.yml
|
||||
|
||||
../../../../nebula-cert ca -curve "${CURVE:-25519}" -name "Smoke Test"
|
||||
../../../../nebula-cert sign -name "lighthouse1" -groups "lighthouse,lighthouse1" -ip "192.168.100.1/24"
|
||||
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "192.168.100.2/24"
|
||||
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "192.168.100.3/24"
|
||||
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "192.168.100.4/24"
|
||||
../../../../nebula-cert sign -name "lighthouse1" -groups "lighthouse,lighthouse1" -ip "$LIGHTHOUSE_NIP/24"
|
||||
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "$HOST2_NIP/24"
|
||||
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "$HOST3_NIP/24"
|
||||
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "$HOST4_NIP/24"
|
||||
)
|
||||
|
||||
docker build -t "nebula:${NAME:-smoke}" .
|
||||
|
||||
@@ -47,6 +47,19 @@ HOST2_IP="$PREFIX.3"
|
||||
HOST3_IP="$PREFIX.4"
|
||||
HOST4_IP="$PREFIX.5"
|
||||
|
||||
if [ "$SMOKE_OVERLAY_IPV6" ]
|
||||
then
|
||||
LIGHTHOUSE_NIP="fd00:4242:0:0:0:ffff:c0a8:6401"
|
||||
HOST2_NIP="fd00:4242:0:0:0:ffff:c0a8:6402"
|
||||
HOST3_NIP="fd00:4242:0:0:0:ffff:c0a8:6403"
|
||||
HOST4_NIP="fd00:4242:0:0:0:ffff:c0a8:6404"
|
||||
else
|
||||
LIGHTHOUSE_NIP="192.168.100.1"
|
||||
HOST2_NIP="192.168.100.2"
|
||||
HOST3_NIP="192.168.100.3"
|
||||
HOST4_NIP="192.168.100.4"
|
||||
fi
|
||||
|
||||
# Sed the placeholder TEST-NET-3 IPs in the host configs to the real ones.
|
||||
# build/lighthouse1.yml has no IPs to rewrite so it's skipped.
|
||||
for f in build/host2.yml build/host3.yml build/host4.yml; do
|
||||
@@ -80,28 +93,28 @@ docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host
|
||||
docker exec host4 tcpdump -i tun0 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
|
||||
docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
|
||||
|
||||
docker exec host2 ncat -nklv 0.0.0.0 2000 &
|
||||
docker exec host3 ncat -nklv 0.0.0.0 2000 &
|
||||
docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 0.0.0.0 4000 &
|
||||
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
|
||||
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 &
|
||||
docker exec host2 ncat -nklv 2000 &
|
||||
docker exec host3 ncat -nklv 2000 &
|
||||
docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 4000 &
|
||||
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 3000 &
|
||||
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 3000 &
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from lighthouse1"
|
||||
echo
|
||||
set -x
|
||||
docker exec lighthouse1 ping -c1 192.168.100.2
|
||||
docker exec lighthouse1 ping -c1 192.168.100.3
|
||||
docker exec lighthouse1 ping -c1 $HOST2_NIP
|
||||
docker exec lighthouse1 ping -c1 $HOST3_NIP
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from host2"
|
||||
echo
|
||||
set -x
|
||||
docker exec host2 ping -c1 192.168.100.1
|
||||
docker exec host2 ping -c1 $LIGHTHOUSE_NIP
|
||||
# Should fail because not allowed by host3 inbound firewall
|
||||
! docker exec host2 ping -c1 192.168.100.3 -w5 || exit 1
|
||||
! docker exec host2 ping -c1 $HOST3_NIP -w5 || exit 1
|
||||
|
||||
set +x
|
||||
echo
|
||||
@@ -109,34 +122,34 @@ echo " *** Testing ncat from host2"
|
||||
echo
|
||||
set -x
|
||||
# Should fail because not allowed by host3 inbound firewall
|
||||
! docker exec host2 ncat -nzv -w5 192.168.100.3 2000 || exit 1
|
||||
! docker exec host2 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
||||
! docker exec host2 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
|
||||
! docker exec host2 ncat -nzuv -w5 $HOST3_NIP 3000 | grep -q host3 || exit 1
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from host3"
|
||||
echo
|
||||
set -x
|
||||
docker exec host3 ping -c1 192.168.100.1
|
||||
docker exec host3 ping -c1 192.168.100.2
|
||||
docker exec host3 ping -c1 $LIGHTHOUSE_NIP
|
||||
docker exec host3 ping -c1 $HOST2_NIP
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ncat from host3"
|
||||
echo
|
||||
set -x
|
||||
docker exec host3 ncat -nzv -w5 192.168.100.2 2000
|
||||
docker exec host3 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2
|
||||
docker exec host3 ncat -nzv -w5 $HOST2_NIP 2000
|
||||
docker exec host3 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from host4"
|
||||
echo
|
||||
set -x
|
||||
docker exec host4 ping -c1 192.168.100.1
|
||||
docker exec host4 ping -c1 $LIGHTHOUSE_NIP
|
||||
# Should fail because not allowed by host4 outbound firewall
|
||||
! docker exec host4 ping -c1 192.168.100.2 -w5 || exit 1
|
||||
! docker exec host4 ping -c1 192.168.100.3 -w5 || exit 1
|
||||
! docker exec host4 ping -c1 $HOST2_NIP -w5 || exit 1
|
||||
! docker exec host4 ping -c1 $HOST3_NIP -w5 || exit 1
|
||||
|
||||
set +x
|
||||
echo
|
||||
@@ -144,10 +157,10 @@ echo " *** Testing ncat from host4"
|
||||
echo
|
||||
set -x
|
||||
# Should fail because not allowed by host4 outbound firewall
|
||||
! docker exec host4 ncat -nzv -w5 192.168.100.2 2000 || exit 1
|
||||
! docker exec host4 ncat -nzv -w5 192.168.100.3 2000 || exit 1
|
||||
! docker exec host4 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2 || exit 1
|
||||
! docker exec host4 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
||||
! docker exec host4 ncat -nzv -w5 $HOST2_NIP 2000 || exit 1
|
||||
! docker exec host4 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
|
||||
! docker exec host4 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2 || exit 1
|
||||
! docker exec host4 ncat -nzuv -w5 $HOST3_NIP 3000 | grep -q host3 || exit 1
|
||||
|
||||
set +x
|
||||
echo
|
||||
@@ -159,7 +172,7 @@ set -x
|
||||
# cannot initiate UDP to host2. Once host2 initiates a flow to host4:4000,
|
||||
# conntrack must let host4's listener reply on that flow. If it doesn't,
|
||||
# the echo back from host4 never reaches host2.
|
||||
docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv 192.168.100.4 4000" | grep -q helloagainfromhost4
|
||||
docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv $HOST4_NIP 4000" | grep -q helloagainfromhost4
|
||||
|
||||
docker exec host4 sh -c 'kill 1'
|
||||
docker exec host3 sh -c 'kill 1'
|
||||
|
||||
+96
-78
@@ -13,20 +13,28 @@ on:
|
||||
- 'go.sum'
|
||||
jobs:
|
||||
|
||||
test-linux:
|
||||
name: Build all and test on ubuntu-linux
|
||||
static:
|
||||
name: Static checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build
|
||||
run: make all
|
||||
- name: Install goimports
|
||||
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
if [ "$(find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -l)" ]
|
||||
then
|
||||
find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -d
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Vet
|
||||
run: make vet
|
||||
@@ -36,97 +44,107 @@ jobs:
|
||||
with:
|
||||
version: v2.5
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
- name: End 2 end
|
||||
run: make e2evv
|
||||
|
||||
- name: Build test mobile
|
||||
run: make build-test-mobile
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: e2e packet flow linux-latest
|
||||
path: e2e/mermaid/linux-latest
|
||||
if-no-files-found: warn
|
||||
|
||||
test-linux-boringcrypto:
|
||||
name: Build and test on linux with boringcrypto
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build
|
||||
run: make bin-boringcrypto
|
||||
|
||||
- name: Test
|
||||
run: make test-boringcrypto
|
||||
|
||||
- name: End 2 end
|
||||
run: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
||||
|
||||
test-linux-pkcs11:
|
||||
name: Build and test on linux with pkcs11
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build
|
||||
run: make bin-pkcs11
|
||||
|
||||
- name: Test
|
||||
run: make test-pkcs11
|
||||
|
||||
test:
|
||||
name: Build and test on ${{ matrix.os }}
|
||||
name: Test ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest]
|
||||
include:
|
||||
- name: linux
|
||||
os: ubuntu-latest
|
||||
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
||||
test-cmd: make test
|
||||
e2e-cmd: make e2evv
|
||||
- name: linux-boringcrypto
|
||||
os: ubuntu-latest
|
||||
build-cmd: make bin-boringcrypto
|
||||
test-cmd: make test-boringcrypto
|
||||
e2e-cmd: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
||||
- name: linux-pkcs11
|
||||
os: ubuntu-latest
|
||||
build-cmd: make bin-pkcs11
|
||||
test-cmd: make test-pkcs11
|
||||
e2e-cmd: ''
|
||||
- name: macos
|
||||
os: macos-latest
|
||||
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
||||
test-cmd: make test
|
||||
e2e-cmd: make e2evv
|
||||
- name: windows
|
||||
os: windows-latest
|
||||
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
||||
test-cmd: make test
|
||||
e2e-cmd: make e2evv
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build nebula
|
||||
run: go build ./cmd/nebula
|
||||
- name: Build
|
||||
run: ${{ matrix.build-cmd }}
|
||||
|
||||
- name: Build nebula-cert
|
||||
run: go build ./cmd/nebula-cert
|
||||
|
||||
- name: Vet
|
||||
run: make vet
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.5
|
||||
- name: Cross-build darwin-amd64
|
||||
if: matrix.name == 'macos'
|
||||
run: GOARCH=amd64 go build -o /tmp/nebula-amd64 ./cmd/nebula && GOARCH=amd64 go build -o /tmp/nebula-cert-amd64 ./cmd/nebula-cert
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
run: ${{ matrix.test-cmd }}
|
||||
|
||||
- name: End 2 end
|
||||
run: make e2evv
|
||||
if: matrix.e2e-cmd != ''
|
||||
run: ${{ matrix.e2e-cmd }}
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: matrix.e2e-cmd != '' && always()
|
||||
with:
|
||||
name: e2e packet flow ${{ matrix.os }}
|
||||
path: e2e/mermaid/${{ matrix.os }}
|
||||
name: e2e packet flow ${{ matrix.name }}
|
||||
path: e2e/mermaid/
|
||||
if-no-files-found: warn
|
||||
|
||||
cross-build:
|
||||
name: Cross-build ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- {name: linux-arm, make-target: all-cross-linux-arm}
|
||||
- {name: linux-mips, make-target: all-cross-linux-mips}
|
||||
- {name: linux-other, make-target: all-cross-linux-other}
|
||||
- {name: freebsd, make-target: all-freebsd}
|
||||
- {name: openbsd, make-target: all-openbsd}
|
||||
- {name: netbsd, make-target: all-netbsd}
|
||||
- {name: windows, make-target: all-cross-windows}
|
||||
- {name: mobile, make-target: build-test-mobile}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build ${{ matrix.name }}
|
||||
run: make -j"$(nproc)" ${{ matrix.make-target }}
|
||||
|
||||
finish:
|
||||
name: CI status
|
||||
if: always()
|
||||
needs: [static, test, cross-build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Fail if any upstream job failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||
run: |
|
||||
echo "upstream results: ${{ toJSON(needs) }}"
|
||||
exit 1
|
||||
|
||||
- name: All upstream jobs passed
|
||||
run: echo "ok"
|
||||
|
||||
@@ -60,6 +60,18 @@ ALL = $(ALL_LINUX) \
|
||||
windows-amd64 \
|
||||
windows-arm64
|
||||
|
||||
# Cross-build shards used by .github/workflows/test.yml — same as ALL_*
|
||||
# but with the arch that has a native CI runner removed, so the cross-build
|
||||
# job is not duplicating coverage the native test jobs already give.
|
||||
ALL_CROSS_LINUX = $(filter-out linux-amd64,$(ALL_LINUX))
|
||||
|
||||
# ALL_CROSS_LINUX further split into family sub-shards so each can run on
|
||||
# its own CI runner in parallel. Union of the three must equal
|
||||
# ALL_CROSS_LINUX; adding a new linux arch goes into the matching family.
|
||||
ALL_CROSS_LINUX_ARM = linux-arm-5 linux-arm-6 linux-arm-7 linux-arm64
|
||||
ALL_CROSS_LINUX_MIPS = linux-mips linux-mipsle linux-mips64 linux-mips64le linux-mips-softfloat
|
||||
ALL_CROSS_LINUX_OTHER = linux-386 linux-ppc64le linux-riscv64 linux-loong64
|
||||
|
||||
e2e:
|
||||
$(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./e2e
|
||||
|
||||
@@ -82,6 +94,35 @@ DOCKER_BIN = build/linux-amd64/nebula build/linux-amd64/nebula-cert
|
||||
|
||||
all: $(ALL:%=build/%/nebula) $(ALL:%=build/%/nebula-cert)
|
||||
|
||||
all-linux: $(ALL_LINUX:%=build/%/nebula) $(ALL_LINUX:%=build/%/nebula-cert)
|
||||
|
||||
all-freebsd: $(ALL_FREEBSD:%=build/%/nebula) $(ALL_FREEBSD:%=build/%/nebula-cert)
|
||||
|
||||
all-openbsd: $(ALL_OPENBSD:%=build/%/nebula) $(ALL_OPENBSD:%=build/%/nebula-cert)
|
||||
|
||||
all-netbsd: $(ALL_NETBSD:%=build/%/nebula) $(ALL_NETBSD:%=build/%/nebula-cert)
|
||||
|
||||
all-darwin: build/darwin-amd64/nebula build/darwin-amd64/nebula-cert build/darwin-arm64/nebula build/darwin-arm64/nebula-cert
|
||||
|
||||
all-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe build/windows-arm64/nebula.exe build/windows-arm64/nebula-cert.exe
|
||||
|
||||
# CI cross-build shards. darwin-arm64 is covered by the native macos-latest
|
||||
# job; windows-amd64 is covered by the native windows-latest job; both are
|
||||
# omitted here to avoid building them a second time. darwin-amd64 stays in
|
||||
# all-cross-darwin because intel mac is only a labeled/master-time native
|
||||
# job, so PRs still need cross-build coverage for it.
|
||||
all-cross-linux: $(ALL_CROSS_LINUX:%=build/%/nebula) $(ALL_CROSS_LINUX:%=build/%/nebula-cert)
|
||||
|
||||
all-cross-linux-arm: $(ALL_CROSS_LINUX_ARM:%=build/%/nebula) $(ALL_CROSS_LINUX_ARM:%=build/%/nebula-cert)
|
||||
|
||||
all-cross-linux-mips: $(ALL_CROSS_LINUX_MIPS:%=build/%/nebula) $(ALL_CROSS_LINUX_MIPS:%=build/%/nebula-cert)
|
||||
|
||||
all-cross-linux-other: $(ALL_CROSS_LINUX_OTHER:%=build/%/nebula) $(ALL_CROSS_LINUX_OTHER:%=build/%/nebula-cert)
|
||||
|
||||
all-cross-darwin: build/darwin-amd64/nebula build/darwin-amd64/nebula-cert
|
||||
|
||||
all-cross-windows: build/windows-arm64/nebula.exe build/windows-arm64/nebula-cert.exe
|
||||
|
||||
docker: docker/linux-$(shell go env GOARCH)
|
||||
|
||||
release: $(ALL:%=build/nebula-%.tar.gz)
|
||||
@@ -227,6 +268,9 @@ smoke-relay-docker: bin-docker
|
||||
cd .github/workflows/smoke/ && ./build-relay.sh
|
||||
cd .github/workflows/smoke/ && ./smoke-relay.sh
|
||||
|
||||
smoke-docker-ipv6: export SMOKE_OVERLAY_IPV6 = 1
|
||||
smoke-docker-ipv6: smoke-docker
|
||||
|
||||
smoke-docker-race: BUILD_ARGS = -race
|
||||
smoke-docker-race: CGO_ENABLED = 1
|
||||
smoke-docker-race: smoke-docker
|
||||
@@ -236,5 +280,5 @@ smoke-vagrant/%: bin-docker build/%/nebula
|
||||
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
||||
|
||||
.FORCE:
|
||||
.PHONY: bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
|
||||
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
|
||||
.DEFAULT_GOAL := bin
|
||||
|
||||
+10
-4
@@ -13,6 +13,12 @@ import (
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
// testCertNow is the reference "now" used to derive default before/after times
|
||||
// in NewTestCaCert and NewTestCert. Holding it fixed for the lifetime of the
|
||||
// test binary keeps CA and leaf defaults aligned at the same second, so a leaf
|
||||
// signed with default times can never expire after its CA on a rounding race.
|
||||
var testCertNow = time.Now().Round(time.Second)
|
||||
|
||||
// NewTestCaCert will create a new ca certificate
|
||||
func NewTestCaCert(version Version, curve Curve, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (Certificate, []byte, []byte, []byte) {
|
||||
var err error
|
||||
@@ -34,10 +40,10 @@ func NewTestCaCert(version Version, curve Curve, before, after time.Time, networ
|
||||
}
|
||||
|
||||
if before.IsZero() {
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
before = testCertNow.Add(time.Second * -60)
|
||||
}
|
||||
if after.IsZero() {
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
}
|
||||
|
||||
t := &TBSCertificate{
|
||||
@@ -70,11 +76,11 @@ func NewTestCaCert(version Version, curve Curve, before, after time.Time, networ
|
||||
// Expiry times are defaulted if you do not pass them in
|
||||
func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (Certificate, []byte, []byte, []byte) {
|
||||
if before.IsZero() {
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
before = testCertNow.Add(time.Second * -60)
|
||||
}
|
||||
|
||||
if after.IsZero() {
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
}
|
||||
|
||||
if len(networks) == 0 {
|
||||
|
||||
+32
-2
@@ -148,6 +148,9 @@ func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||
// consumed data or an error on failure. Only key-agreement (ECDH) public key banners are accepted.
|
||||
// Use UnmarshalSigningPublicKeyFromPEM for Ed25519/ECDSA banners.
|
||||
func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
k, r := pem.Decode(b)
|
||||
if k == nil {
|
||||
@@ -156,10 +159,10 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
case X25519PublicKeyBanner, Ed25519PublicKeyBanner:
|
||||
case X25519PublicKeyBanner:
|
||||
expectedLen = 32
|
||||
curve = Curve_CURVE25519
|
||||
case P256PublicKeyBanner, ECDSAP256PublicKeyBanner:
|
||||
case P256PublicKeyBanner:
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
@@ -172,6 +175,33 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
|
||||
// UnmarshalSigningPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||
// consumed data or an error on failure. Only Ed25519/ECDSA public key banners are accepted.
|
||||
// Use UnmarshalPublicKeyFromPEM for X25519/P256 (ECDH) banners.
|
||||
func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
k, r := pem.Decode(b)
|
||||
if k == nil {
|
||||
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
case Ed25519PublicKeyBanner:
|
||||
expectedLen = 32
|
||||
curve = Curve_CURVE25519
|
||||
case ECDSAP256PublicKeyBanner:
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
default:
|
||||
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
}
|
||||
if len(k.Bytes) != expectedLen {
|
||||
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
|
||||
func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||
switch curve {
|
||||
case Curve_CURVE25519:
|
||||
|
||||
+88
-68
@@ -255,60 +255,6 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
pubKey := []byte(`# A good key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
shortKey := []byte(`# A short key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
invalidBanner := []byte(`# Invalid banner
|
||||
-----BEGIN NOT A NEBULA PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NOT A NEBULA PUBLIC KEY-----
|
||||
`)
|
||||
invalidPem := []byte(`# Not a valid PEM format
|
||||
-BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA ED25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// Success test case
|
||||
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||
|
||||
// Fail due to short key
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||
|
||||
// Fail due to invalid banner
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
}
|
||||
|
||||
func TestUnmarshalX25519PublicKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
pubKey := []byte(`# A good key
|
||||
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA X25519 PUBLIC KEY-----
|
||||
@@ -319,7 +265,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA P256 PUBLIC KEY-----
|
||||
`)
|
||||
oldPubP256Key := []byte(`# A good key
|
||||
signingKey := []byte(`# A signing key has the wrong scope for this function
|
||||
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
@@ -340,44 +286,118 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA X25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem)
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, signingKey, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// Success test case
|
||||
// X25519 key
|
||||
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, signingKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
|
||||
// Success test case
|
||||
// P256 key
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(oldPubP256Key, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(signingKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
|
||||
// Success test case
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
// Reject a signing public key (Ed25519/ECDSA banner)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
|
||||
// Fail due to short key
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||
|
||||
// Fail due to invalid banner
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
}
|
||||
|
||||
func TestUnmarshalSigningPublicKeyFromPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
pubKey := []byte(`# A good key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
pubP256Key := []byte(`# A good key
|
||||
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
`)
|
||||
ecdhKey := []byte(`# A key-agreement key has the wrong scope for this function
|
||||
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA X25519 PUBLIC KEY-----
|
||||
`)
|
||||
shortKey := []byte(`# A short key
|
||||
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||
`)
|
||||
invalidBanner := []byte(`# Invalid banner
|
||||
-----BEGIN NOT A NEBULA PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NOT A NEBULA PUBLIC KEY-----
|
||||
`)
|
||||
invalidPem := []byte(`# Not a valid PEM format
|
||||
-BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA ED25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, ecdhKey, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// Ed25519 key
|
||||
k, rest, curve, err := UnmarshalSigningPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
|
||||
// ECDSA P256 key
|
||||
k, rest, curve, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
|
||||
// Reject a key-agreement public key (X25519/P256 banner)
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
|
||||
// Fail due to short key
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||
|
||||
// Fail due to invalid banner
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
|
||||
+10
-4
@@ -14,6 +14,12 @@ import (
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
// testCertNow is the reference "now" used to derive default before/after times
|
||||
// in NewTestCaCert and NewTestCert. Holding it fixed for the lifetime of the
|
||||
// test binary keeps CA and leaf defaults aligned at the same second, so a leaf
|
||||
// signed with default times can never expire after its CA on a rounding race.
|
||||
var testCertNow = time.Now().Round(time.Second)
|
||||
|
||||
// NewTestCaCert will create a new ca certificate
|
||||
func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte, []byte, []byte) {
|
||||
var err error
|
||||
@@ -35,10 +41,10 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
|
||||
}
|
||||
|
||||
if before.IsZero() {
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
before = testCertNow.Add(time.Second * -60)
|
||||
}
|
||||
if after.IsZero() {
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
}
|
||||
|
||||
t := &cert.TBSCertificate{
|
||||
@@ -71,11 +77,11 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
|
||||
// Expiry times are defaulted if you do not pass them in
|
||||
func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte, []byte, []byte) {
|
||||
if before.IsZero() {
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
before = testCertNow.Add(time.Second * -60)
|
||||
}
|
||||
|
||||
if after.IsZero() {
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
}
|
||||
|
||||
var pub, priv []byte
|
||||
|
||||
+32
-7
@@ -97,6 +97,19 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// out-key is meaningless under PKCS#11 because the private key never
|
||||
// leaves the HSM; reject it so we never silently accept or claim a
|
||||
// stdout slot for it.
|
||||
outKeySet := false
|
||||
cf.set.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "out-key" {
|
||||
outKeySet = true
|
||||
}
|
||||
})
|
||||
if outKeySet {
|
||||
return newHelpErrorf("cannot set -out-key with -pkcs11")
|
||||
}
|
||||
}
|
||||
if err := mustFlagString("out-crt", cf.outCertPath); err != nil {
|
||||
return err
|
||||
@@ -171,12 +184,21 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
}
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if err := reserveOutputs(&claims,
|
||||
"out-key", *cf.outKeyPath,
|
||||
"out-crt", *cf.outCertPath,
|
||||
"out-qr", *cf.outQRPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var passphrase []byte
|
||||
if !isP11 && *cf.encryption {
|
||||
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
||||
if len(passphrase) == 0 {
|
||||
for i := 0; i < 5; i++ {
|
||||
out.Write([]byte("Enter passphrase: "))
|
||||
errOut.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if err == ErrNoTerminal {
|
||||
@@ -261,14 +283,16 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
Curve: curve,
|
||||
}
|
||||
|
||||
if !isP11 {
|
||||
if !isP11 && !isStdio(*cf.outKeyPath) {
|
||||
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
|
||||
if !isStdio(*cf.outCertPath) {
|
||||
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
var c cert.Certificate
|
||||
@@ -294,7 +318,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*cf.outKeyPath, b, 0600)
|
||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
@@ -305,7 +329,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return fmt.Errorf("error while marshalling certificate: %s", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*cf.outCertPath, b, 0600)
|
||||
err = writeOutput(*cf.outCertPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
||||
}
|
||||
@@ -316,7 +340,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return fmt.Errorf("error while generating qr code: %s", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*cf.outQRPath, b, 0600)
|
||||
err = writeOutput(*cf.outQRPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||
}
|
||||
@@ -332,6 +356,7 @@ func caSummary() string {
|
||||
func caHelp(out io.Writer) {
|
||||
cf := newCaFlags()
|
||||
out.Write([]byte("Usage of " + os.Args[0] + " " + caSummary() + "\n"))
|
||||
out.Write([]byte(stdioHelpText))
|
||||
cf.set.SetOutput(out)
|
||||
cf.set.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func Test_caHelp(t *testing.T) {
|
||||
assert.Equal(
|
||||
t,
|
||||
"Usage of "+os.Args[0]+" ca <flags>: create a self signed certificate authority\n"+
|
||||
" Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
|
||||
" -argon-iterations uint\n"+
|
||||
" \tOptional: Argon2 iterations parameter used for encrypted private key passphrase (default 1)\n"+
|
||||
" -argon-memory uint\n"+
|
||||
@@ -84,7 +85,7 @@ func Test_ca(t *testing.T) {
|
||||
err: nil,
|
||||
}
|
||||
|
||||
pwPromptOb := "Enter passphrase: "
|
||||
pwPromptEB := "Enter passphrase: "
|
||||
|
||||
// required args
|
||||
assertHelpError(t, ca(
|
||||
@@ -168,8 +169,8 @@ func Test_ca(t *testing.T) {
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||
require.NoError(t, ca(args, ob, eb, testpw))
|
||||
assert.Equal(t, pwPromptOb, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, pwPromptEB, eb.String())
|
||||
|
||||
// test encrypted key with passphrase environment variable
|
||||
os.Remove(keyF.Name())
|
||||
@@ -207,8 +208,8 @@ func Test_ca(t *testing.T) {
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||
require.Error(t, ca(args, ob, eb, errpw))
|
||||
assert.Equal(t, pwPromptOb, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, pwPromptEB, eb.String())
|
||||
|
||||
// test when user fails to enter a password
|
||||
os.Remove(keyF.Name())
|
||||
@@ -217,8 +218,8 @@ func Test_ca(t *testing.T) {
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||
require.EqualError(t, ca(args, ob, eb, nopw), "no passphrase specified, remove -encrypt flag to write out-key in plaintext")
|
||||
assert.Equal(t, strings.Repeat(pwPromptOb, 5), ob.String()) // prompts 5 times before giving up
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, strings.Repeat(pwPromptEB, 5), eb.String()) // prompts 5 times before giving up
|
||||
|
||||
// create valid cert/key for overwrite tests
|
||||
os.Remove(keyF.Name())
|
||||
@@ -247,3 +248,67 @@ func Test_ca(t *testing.T) {
|
||||
os.Remove(keyF.Name())
|
||||
|
||||
}
|
||||
|
||||
func Test_ca_stdio(t *testing.T) {
|
||||
nopw := &StubPasswordReader{}
|
||||
|
||||
keyF, err := os.CreateTemp("", "ca.key")
|
||||
require.NoError(t, err)
|
||||
os.Remove(keyF.Name())
|
||||
defer os.Remove(keyF.Name())
|
||||
|
||||
crtF, err := os.CreateTemp("", "ca.crt")
|
||||
require.NoError(t, err)
|
||||
os.Remove(crtF.Name())
|
||||
defer os.Remove(crtF.Name())
|
||||
|
||||
// out-crt on stdout, out-key on disk
|
||||
ob := &bytes.Buffer{}
|
||||
eb := &bytes.Buffer{}
|
||||
require.NoError(t, ca([]string{"-name", "test-ca", "-duration", "1h", "-out-crt", "-", "-out-key", keyF.Name()}, ob, eb, nopw))
|
||||
assert.Empty(t, eb.String())
|
||||
c, _, err := cert.UnmarshalCertificateFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, c.IsCA())
|
||||
assert.Equal(t, "test-ca", c.Name())
|
||||
|
||||
// out-key on stdout, out-crt on disk
|
||||
os.Remove(keyF.Name())
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.NoError(t, ca([]string{"-name", "test-ca", "-duration", "1h", "-out-crt", crtF.Name(), "-out-key", "-"}, ob, eb, nopw))
|
||||
assert.Empty(t, eb.String())
|
||||
_, _, curve, err := cert.UnmarshalSigningPrivateKeyFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||
|
||||
// dual stdout is rejected up front
|
||||
os.Remove(crtF.Name())
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.EqualError(t,
|
||||
ca([]string{"-name", "test-ca", "-duration", "1h", "-out-crt", "-", "-out-key", "-"}, ob, eb, nopw),
|
||||
`-out-key and -out-crt both set to "-", only one output may write to stdout`)
|
||||
assert.Empty(t, ob.String())
|
||||
|
||||
// an output conflict combined with -encrypt must error BEFORE prompting
|
||||
// for a passphrase; pr would record any read attempt
|
||||
tracker := &trackingPasswordReader{}
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.EqualError(t,
|
||||
ca([]string{"-name", "test-ca", "-duration", "1h", "-encrypt", "-out-crt", "-", "-out-key", "-"}, ob, eb, tracker),
|
||||
`-out-key and -out-crt both set to "-", only one output may write to stdout`)
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Zero(t, tracker.calls, "passphrase prompt should not have been called")
|
||||
}
|
||||
|
||||
type trackingPasswordReader struct {
|
||||
calls int
|
||||
}
|
||||
|
||||
func (pr *trackingPasswordReader) ReadPassword() ([]byte, error) {
|
||||
pr.calls++
|
||||
return []byte(""), nil
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if *cf.outKeyPath != "" {
|
||||
return newHelpErrorf("cannot set -out-key with -pkcs11")
|
||||
}
|
||||
if err = mustFlagString("out-pub", cf.outPubPath); err != nil {
|
||||
return err
|
||||
@@ -69,6 +71,14 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if err := reserveOutputs(&claims,
|
||||
"out-key", *cf.outKeyPath,
|
||||
"out-pub", *cf.outPubPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isP11 {
|
||||
p11Client, err := pkclient.FromUrl(*cf.p11url)
|
||||
if err != nil {
|
||||
@@ -82,12 +92,12 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while getting public key: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
|
||||
err = writeOutput(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
}
|
||||
err = os.WriteFile(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600)
|
||||
err = writeOutput(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-pub: %s", err)
|
||||
}
|
||||
@@ -102,6 +112,7 @@ func keygenSummary() string {
|
||||
func keygenHelp(out io.Writer) {
|
||||
cf := newKeygenFlags()
|
||||
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + keygenSummary() + "\n"))
|
||||
_, _ = out.Write([]byte(stdioHelpText))
|
||||
cf.set.SetOutput(out)
|
||||
cf.set.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func Test_keygenHelp(t *testing.T) {
|
||||
assert.Equal(
|
||||
t,
|
||||
"Usage of "+os.Args[0]+" keygen <flags>: create a public/private key pair. the public key can be passed to `nebula-cert sign`\n"+
|
||||
" Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
|
||||
" -curve string\n"+
|
||||
" \tECDH Curve (25519, P256) (default \"25519\")\n"+
|
||||
" -out-key string\n"+
|
||||
@@ -93,3 +94,43 @@ func Test_keygen(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, lPub, 32)
|
||||
}
|
||||
|
||||
func Test_keygen_stdio(t *testing.T) {
|
||||
keyF, err := os.CreateTemp("", "test.key")
|
||||
require.NoError(t, err)
|
||||
os.Remove(keyF.Name())
|
||||
defer os.Remove(keyF.Name())
|
||||
|
||||
pubF, err := os.CreateTemp("", "test.pub")
|
||||
require.NoError(t, err)
|
||||
os.Remove(pubF.Name())
|
||||
defer os.Remove(pubF.Name())
|
||||
|
||||
// out-pub on stdout, out-key on disk
|
||||
ob := &bytes.Buffer{}
|
||||
eb := &bytes.Buffer{}
|
||||
require.NoError(t, keygen([]string{"-out-pub", "-", "-out-key", keyF.Name()}, ob, eb))
|
||||
assert.Empty(t, eb.String())
|
||||
lPub, _, curve, err := cert.UnmarshalPublicKeyFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||
assert.Len(t, lPub, 32)
|
||||
|
||||
// out-key on stdout, out-pub on disk
|
||||
os.Remove(keyF.Name())
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.NoError(t, keygen([]string{"-out-pub", pubF.Name(), "-out-key", "-"}, ob, eb))
|
||||
assert.Empty(t, eb.String())
|
||||
lKey, _, curve, err := cert.UnmarshalPrivateKeyFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||
assert.Len(t, lKey, 32)
|
||||
|
||||
// both on stdout is a conflict caught up front
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.EqualError(t, keygen([]string{"-out-pub", "-", "-out-key", "-"}, ob, eb),
|
||||
`-out-key and -out-pub both set to "-", only one output may write to stdout`)
|
||||
assert.Empty(t, ob.String())
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ func (pr StdinPasswordReader) ReadPassword() ([]byte, error) {
|
||||
}
|
||||
|
||||
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
// Terminal echo is off while reading, so the user's Enter key does not
|
||||
// produce a visible newline. Emit one on stderr to match the prompt.
|
||||
fmt.Fprintln(os.Stderr)
|
||||
|
||||
return password, err
|
||||
}
|
||||
|
||||
@@ -40,11 +40,23 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
rawCert, err := os.ReadFile(*pf.path)
|
||||
var claims ioClaims
|
||||
if err := reserveInputs(&claims, "path", *pf.path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reserveOutputs(&claims, "out-qr", *pf.outQRPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawCert, err := readInput("path", *pf.path, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read cert; %s", err)
|
||||
}
|
||||
|
||||
// When the QR is going to stdout, suppress the human-readable text/json
|
||||
// output so the binary stream is not contaminated.
|
||||
qrToStdout := isStdio(*pf.outQRPath)
|
||||
|
||||
var c cert.Certificate
|
||||
var qrBytes []byte
|
||||
part := 0
|
||||
@@ -57,11 +69,13 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while unmarshaling cert: %s", err)
|
||||
}
|
||||
|
||||
if *pf.json {
|
||||
jsonCerts = append(jsonCerts, c)
|
||||
} else {
|
||||
_, _ = out.Write([]byte(c.String()))
|
||||
_, _ = out.Write([]byte("\n"))
|
||||
if !qrToStdout {
|
||||
if *pf.json {
|
||||
jsonCerts = append(jsonCerts, c)
|
||||
} else {
|
||||
_, _ = out.Write([]byte(c.String()))
|
||||
_, _ = out.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
if *pf.outQRPath != "" {
|
||||
@@ -79,7 +93,7 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
part++
|
||||
}
|
||||
|
||||
if *pf.json {
|
||||
if *pf.json && !qrToStdout {
|
||||
b, _ := json.Marshal(jsonCerts)
|
||||
_, _ = out.Write(b)
|
||||
_, _ = out.Write([]byte("\n"))
|
||||
@@ -91,7 +105,7 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while generating qr code: %s", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*pf.outQRPath, b, 0600)
|
||||
err = writeOutput(*pf.outQRPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||
}
|
||||
@@ -107,6 +121,7 @@ func printSummary() string {
|
||||
func printHelp(out io.Writer) {
|
||||
pf := newPrintFlags()
|
||||
out.Write([]byte("Usage of " + os.Args[0] + " " + printSummary() + "\n"))
|
||||
out.Write([]byte(stdioHelpText))
|
||||
pf.set.SetOutput(out)
|
||||
pf.set.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ func Test_printHelp(t *testing.T) {
|
||||
assert.Equal(
|
||||
t,
|
||||
"Usage of "+os.Args[0]+" print <flags>: prints details about a certificate\n"+
|
||||
" Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
|
||||
" -json\n"+
|
||||
" \tOptional: outputs certificates in json format\n"+
|
||||
" -out-qr string\n"+
|
||||
@@ -178,6 +179,44 @@ func Test_printCert(t *testing.T) {
|
||||
ob.String(),
|
||||
)
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// read cert from stdin
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
withStdin(t, bytes.NewReader(p))
|
||||
err = printCert([]string{"-json", "-path", "-"}, ob, eb)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
`[{"details":{"curve":"CURVE25519","groups":["hi"],"isCa":false,"issuer":"`+c.Issuer()+`","name":"test","networks":["10.0.0.123/8"],"notAfter":"0001-01-01T00:00:00Z","notBefore":"0001-01-01T00:00:00Z","publicKey":"`+pk+`","unsafeNetworks":[]},"fingerprint":"`+fp+`","signature":"`+sig+`","version":1}]
|
||||
`,
|
||||
ob.String(),
|
||||
)
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// -out-qr - sends only the PNG to stdout, suppressing the cert dump
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
withStdin(t, bytes.NewReader(p))
|
||||
err = printCert([]string{"-path", "-", "-out-qr", "-"}, ob, eb)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, eb.String())
|
||||
stdout := ob.Bytes()
|
||||
require.NotEmpty(t, stdout)
|
||||
// PNG magic, no PEM/JSON noise prepended
|
||||
assert.Equal(t, []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, stdout[:8])
|
||||
assert.NotContains(t, string(stdout), "NebulaCertificate")
|
||||
assert.NotContains(t, string(stdout), `"details"`)
|
||||
|
||||
// json + out-qr - still suppresses json
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
withStdin(t, bytes.NewReader(p))
|
||||
err = printCert([]string{"-json", "-path", "-", "-out-qr", "-"}, ob, eb)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Equal(t, []byte{0x89, 'P', 'N', 'G'}, ob.Bytes()[:4])
|
||||
assert.NotContains(t, ob.String(), `"details"`)
|
||||
}
|
||||
|
||||
// NewTestCaCert will generate a CA cert
|
||||
|
||||
+42
-20
@@ -85,6 +85,9 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
if !isP11 && *sf.inPubPath != "" && *sf.outKeyPath != "" {
|
||||
return newHelpErrorf("cannot set both -in-pub and -out-key")
|
||||
}
|
||||
if isP11 && *sf.outKeyPath != "" {
|
||||
return newHelpErrorf("cannot set -out-key with -pkcs11")
|
||||
}
|
||||
|
||||
var v4Networks []netip.Prefix
|
||||
var v6Networks []netip.Prefix
|
||||
@@ -102,13 +105,35 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return newHelpErrorf("-version must be either %v or %v", cert.Version1, cert.Version2)
|
||||
}
|
||||
|
||||
if *sf.outKeyPath == "" {
|
||||
*sf.outKeyPath = *sf.name + ".key"
|
||||
}
|
||||
if *sf.outCertPath == "" {
|
||||
*sf.outCertPath = *sf.name + ".crt"
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if err := reserveInputs(&claims,
|
||||
"ca-key", *sf.caKeyPath,
|
||||
"ca-crt", *sf.caCertPath,
|
||||
"in-pub", *sf.inPubPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reserveOutputs(&claims,
|
||||
"out-key", *sf.outKeyPath,
|
||||
"out-crt", *sf.outCertPath,
|
||||
"out-qr", *sf.outQRPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var curve cert.Curve
|
||||
var caKey []byte
|
||||
|
||||
if !isP11 {
|
||||
var rawCAKey []byte
|
||||
rawCAKey, err := os.ReadFile(*sf.caKeyPath)
|
||||
|
||||
rawCAKey, err = readInput("ca-key", *sf.caKeyPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca-key: %s", err)
|
||||
}
|
||||
@@ -121,7 +146,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
if len(passphrase) == 0 {
|
||||
// ask for a passphrase until we get one
|
||||
for i := 0; i < 5; i++ {
|
||||
out.Write([]byte("Enter passphrase: "))
|
||||
errOut.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if errors.Is(err, ErrNoTerminal) {
|
||||
@@ -147,7 +172,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
}
|
||||
|
||||
rawCACert, err := os.ReadFile(*sf.caCertPath)
|
||||
rawCACert, err := readInput("ca-crt", *sf.caCertPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca-crt: %s", err)
|
||||
}
|
||||
@@ -245,7 +270,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
|
||||
if *sf.inPubPath != "" {
|
||||
var pubCurve cert.Curve
|
||||
rawPub, err := os.ReadFile(*sf.inPubPath)
|
||||
rawPub, err := readInput("in-pub", *sf.inPubPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading in-pub: %s", err)
|
||||
}
|
||||
@@ -266,16 +291,10 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
pub, rawPriv = newKeypair(curve)
|
||||
}
|
||||
|
||||
if *sf.outKeyPath == "" {
|
||||
*sf.outKeyPath = *sf.name + ".key"
|
||||
}
|
||||
|
||||
if *sf.outCertPath == "" {
|
||||
*sf.outCertPath = *sf.name + ".crt"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(*sf.outCertPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
|
||||
if !isStdio(*sf.outCertPath) {
|
||||
if _, err := os.Stat(*sf.outCertPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
var crts []cert.Certificate
|
||||
@@ -360,11 +379,13 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
|
||||
if !isP11 && *sf.inPubPath == "" {
|
||||
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
||||
if !isStdio(*sf.outKeyPath) {
|
||||
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
|
||||
err = writeOutput(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
@@ -379,7 +400,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
b = append(b, sb...)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*sf.outCertPath, b, 0600)
|
||||
err = writeOutput(*sf.outCertPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
||||
}
|
||||
@@ -390,7 +411,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return fmt.Errorf("error while generating qr code: %s", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(*sf.outQRPath, b, 0600)
|
||||
err = writeOutput(*sf.outQRPath, b, 0600, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||
}
|
||||
@@ -440,6 +461,7 @@ func signSummary() string {
|
||||
func signHelp(out io.Writer) {
|
||||
sf := newSignFlags()
|
||||
out.Write([]byte("Usage of " + os.Args[0] + " " + signSummary() + "\n"))
|
||||
out.Write([]byte(stdioHelpText))
|
||||
sf.set.SetOutput(out)
|
||||
sf.set.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func Test_signHelp(t *testing.T) {
|
||||
assert.Equal(
|
||||
t,
|
||||
"Usage of "+os.Args[0]+" sign <flags>: create and sign a certificate\n"+
|
||||
" Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
|
||||
" -ca-crt string\n"+
|
||||
" \tOptional: path to the signing CA cert (default \"ca.crt\")\n"+
|
||||
" -ca-key string\n"+
|
||||
@@ -376,15 +377,18 @@ func Test_signCert(t *testing.T) {
|
||||
// test with the proper password
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||
require.NoError(t, signCert(args, ob, eb, testpw))
|
||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||
|
||||
// test with the proper password in the environment
|
||||
os.Remove(crtF.Name())
|
||||
os.Remove(keyF.Name())
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase))
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.NoError(t, signCert(args, ob, eb, testpw))
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
||||
|
||||
@@ -395,8 +399,8 @@ func Test_signCert(t *testing.T) {
|
||||
testpw.password = []byte("invalid password")
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||
require.Error(t, signCert(args, ob, eb, testpw))
|
||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||
|
||||
// test with the wrong password in environment
|
||||
ob.Reset()
|
||||
@@ -416,8 +420,8 @@ func Test_signCert(t *testing.T) {
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||
require.Error(t, signCert(args, ob, eb, nopw))
|
||||
// normally the user hitting enter on the prompt would add newlines between these
|
||||
assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", eb.String())
|
||||
|
||||
// test an error condition
|
||||
ob.Reset()
|
||||
@@ -425,6 +429,106 @@ func Test_signCert(t *testing.T) {
|
||||
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||
require.Error(t, signCert(args, ob, eb, errpw))
|
||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||
}
|
||||
|
||||
func Test_signCert_stdio(t *testing.T) {
|
||||
nopw := &StubPasswordReader{
|
||||
password: []byte(""),
|
||||
err: nil,
|
||||
}
|
||||
|
||||
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
rawCAKey := cert.MarshalSigningPrivateKeyToPEM(cert.Curve_CURVE25519, caPriv)
|
||||
|
||||
ca, _ := NewTestCaCert("ca", caPub, caPriv, time.Now(), time.Now().Add(time.Minute*200), nil, nil, nil)
|
||||
rawCACrt, _ := ca.MarshalPEM()
|
||||
|
||||
caCrtF, err := os.CreateTemp("", "sign-cert.crt")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(caCrtF.Name())
|
||||
caCrtF.Write(rawCACrt)
|
||||
|
||||
caKeyF, err := os.CreateTemp("", "sign-cert.key")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(caKeyF.Name())
|
||||
caKeyF.Write(rawCAKey)
|
||||
|
||||
keyF, err := os.CreateTemp("", "sign.key")
|
||||
require.NoError(t, err)
|
||||
os.Remove(keyF.Name())
|
||||
defer os.Remove(keyF.Name())
|
||||
|
||||
// ca-key on stdin, cert to stdout
|
||||
withStdin(t, bytes.NewReader(rawCAKey))
|
||||
ob := &bytes.Buffer{}
|
||||
eb := &bytes.Buffer{}
|
||||
args := []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", "-", "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", "-", "-out-key", keyF.Name(), "-duration", "100m"}
|
||||
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
lCrt, _, err := cert.UnmarshalCertificateFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "stdin-test", lCrt.Name())
|
||||
assert.True(t, lCrt.CheckSignature(caPub))
|
||||
|
||||
// two flags reading from stdin should error before any read attempt;
|
||||
// otherwise an interactive shell would hang on io.ReadAll
|
||||
stdinIn := bytes.NewReader(rawCAKey)
|
||||
withStdin(t, stdinIn)
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-ca-crt", "-", "-ca-key", "-", "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||
require.EqualError(t, signCert(args, ob, eb, nopw),
|
||||
`-ca-key and -ca-crt both set to "-", only one input may read from stdin`)
|
||||
assert.Equal(t, len(rawCAKey), stdinIn.Len(), "stdin should be untouched when conflict is caught up front")
|
||||
|
||||
// two flags writing to stdout should error before any output is written
|
||||
// AND before stdin is consumed
|
||||
stdinR := bytes.NewReader(rawCAKey)
|
||||
withStdin(t, stdinR)
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", "-", "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", "-", "-out-key", "-", "-duration", "100m"}
|
||||
require.EqualError(t, signCert(args, ob, eb, nopw),
|
||||
`-out-key and -out-crt both set to "-", only one output may write to stdout`)
|
||||
assert.Empty(t, ob.String())
|
||||
// stdin should be untouched because the conflict was caught up front
|
||||
assert.Equal(t, len(rawCAKey), stdinR.Len())
|
||||
|
||||
// out-key on stdout, cert on disk
|
||||
keyF2, err := os.CreateTemp("", "sign.key")
|
||||
require.NoError(t, err)
|
||||
os.Remove(keyF2.Name())
|
||||
defer os.Remove(keyF2.Name())
|
||||
crtF, err := os.CreateTemp("", "sign.crt")
|
||||
require.NoError(t, err)
|
||||
os.Remove(crtF.Name())
|
||||
defer os.Remove(crtF.Name())
|
||||
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", "-", "-duration", "100m"}
|
||||
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||
assert.Empty(t, eb.String())
|
||||
_, _, curve, err := cert.UnmarshalPrivateKeyFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||
|
||||
// in-pub on stdin (caller already has a keypair, only the cert is generated)
|
||||
inPub, _ := x25519Keypair()
|
||||
rawInPub := cert.MarshalPublicKeyToPEM(cert.Curve_CURVE25519, inPub)
|
||||
|
||||
withStdin(t, bytes.NewReader(rawInPub))
|
||||
os.Remove(crtF.Name())
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "in-pub-test", "-ip", "1.1.1.1/24", "-in-pub", "-", "-out-crt", "-", "-duration", "100m"}
|
||||
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||
assert.Empty(t, eb.String())
|
||||
stdinCrt, _, err := cert.UnmarshalCertificateFromPEM(ob.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "in-pub-test", stdinCrt.Name())
|
||||
assert.Equal(t, inPub, stdinCrt.PublicKey())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// stdioPath is the special path value that selects stdin (for inputs) or
|
||||
// stdout (for outputs) instead of a file on disk.
|
||||
const stdioPath = "-"
|
||||
|
||||
// stdioHelpText is rendered just under the Usage line of each subcommand
|
||||
// help so the - convention is documented once instead of on every flag.
|
||||
const stdioHelpText = " Pass \"-\" to any path flag to read from stdin or write to stdout.\n"
|
||||
|
||||
// stdinReader is the source used when an input flag is set to "-".
|
||||
// It is a package level var so tests can swap in a deterministic reader.
|
||||
// Tests that mutate stdinReader cannot run with t.Parallel().
|
||||
var stdinReader io.Reader = os.Stdin
|
||||
|
||||
// ioClaims tracks which flags have claimed stdin and stdout during a single
|
||||
// command invocation so we can refuse a second flag asking for the same
|
||||
// stream.
|
||||
type ioClaims struct {
|
||||
in string
|
||||
out string
|
||||
}
|
||||
|
||||
func (c *ioClaims) claimIn(flagName string) error {
|
||||
if c.in != "" && c.in != flagName {
|
||||
return fmt.Errorf("-%s and -%s both set to %q, only one input may read from stdin", c.in, flagName, stdioPath)
|
||||
}
|
||||
c.in = flagName
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ioClaims) claimOut(flagName string) error {
|
||||
if c.out != "" && c.out != flagName {
|
||||
return fmt.Errorf("-%s and -%s both set to %q, only one output may write to stdout", c.out, flagName, stdioPath)
|
||||
}
|
||||
c.out = flagName
|
||||
return nil
|
||||
}
|
||||
|
||||
// reserveInputs walks alternating (flagName, path) pairs and claims stdin
|
||||
// for any path equal to stdioPath. It must be called before any input is
|
||||
// read so a conflict can be reported immediately instead of blocking on
|
||||
// io.ReadAll while waiting for input that will never arrive.
|
||||
func reserveInputs(claims *ioClaims, pairs ...string) error {
|
||||
return reserveStdio(claims, "reserveInputs", (*ioClaims).claimIn, pairs)
|
||||
}
|
||||
|
||||
// reserveOutputs walks alternating (flagName, path) pairs and claims stdout
|
||||
// for any path equal to stdioPath. It must be called before any output is
|
||||
// written so a conflict cannot leave one stream half written before the
|
||||
// second flag fails.
|
||||
func reserveOutputs(claims *ioClaims, pairs ...string) error {
|
||||
return reserveStdio(claims, "reserveOutputs", (*ioClaims).claimOut, pairs)
|
||||
}
|
||||
|
||||
func reserveStdio(claims *ioClaims, who string, claim func(*ioClaims, string) error, pairs []string) error {
|
||||
if len(pairs)%2 != 0 {
|
||||
panic(who + " requires alternating name, path pairs")
|
||||
}
|
||||
for i := 0; i < len(pairs); i += 2 {
|
||||
name, path := pairs[i], pairs[i+1]
|
||||
if path != stdioPath {
|
||||
continue
|
||||
}
|
||||
if err := claim(claims, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readInput returns the bytes referenced by path, reading from stdin when
|
||||
// path is stdioPath.
|
||||
func readInput(flagName, path string, claims *ioClaims) ([]byte, error) {
|
||||
if path == stdioPath {
|
||||
if err := claims.claimIn(flagName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(stdinReader)
|
||||
}
|
||||
return os.ReadFile(path)
|
||||
}
|
||||
|
||||
// openInput returns a reader for path. When path is stdioPath the returned
|
||||
// reader wraps stdin and Close is a no-op.
|
||||
func openInput(flagName, path string, claims *ioClaims) (io.ReadCloser, error) {
|
||||
if path == stdioPath {
|
||||
if err := claims.claimIn(flagName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.NopCloser(stdinReader), nil
|
||||
}
|
||||
return os.Open(path)
|
||||
}
|
||||
|
||||
// writeOutput writes data to path, or to stdout when path is stdioPath. perm
|
||||
// is only used for file output. The caller must have already claimed stdout
|
||||
// via reserveOutputs before invoking with stdioPath.
|
||||
func writeOutput(path string, data []byte, perm os.FileMode, stdout io.Writer) error {
|
||||
if path == stdioPath {
|
||||
_, err := stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, perm)
|
||||
}
|
||||
|
||||
// isStdio reports whether path is the stdio sentinel and so should skip
|
||||
// existence checks like "refuse to overwrite".
|
||||
func isStdio(path string) bool {
|
||||
return path == stdioPath
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// withStdin temporarily replaces stdinReader for the duration of t.
|
||||
func withStdin(t *testing.T, r io.Reader) {
|
||||
t.Helper()
|
||||
prev := stdinReader
|
||||
stdinReader = r
|
||||
t.Cleanup(func() { stdinReader = prev })
|
||||
}
|
||||
|
||||
func Test_readInput_stdin(t *testing.T) {
|
||||
withStdin(t, bytes.NewBufferString("hello"))
|
||||
var claims ioClaims
|
||||
|
||||
got, err := readInput("path", "-", &claims)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("hello"), got)
|
||||
assert.Equal(t, "path", claims.in)
|
||||
}
|
||||
|
||||
func Test_readInput_file(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "f")
|
||||
require.NoError(t, os.WriteFile(p, []byte("file"), 0600))
|
||||
var claims ioClaims
|
||||
|
||||
got, err := readInput("path", p, &claims)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("file"), got)
|
||||
assert.Empty(t, claims.in)
|
||||
}
|
||||
|
||||
func Test_readInput_doubleStdinErrors(t *testing.T) {
|
||||
withStdin(t, bytes.NewBufferString("hello"))
|
||||
var claims ioClaims
|
||||
|
||||
_, err := readInput("ca-key", "-", &claims)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = readInput("ca-crt", "-", &claims)
|
||||
require.EqualError(t, err, `-ca-key and -ca-crt both set to "-", only one input may read from stdin`)
|
||||
}
|
||||
|
||||
func Test_openInput_stdin(t *testing.T) {
|
||||
withStdin(t, bytes.NewBufferString("hi"))
|
||||
var claims ioClaims
|
||||
|
||||
r, err := openInput("ca", "-", &claims)
|
||||
require.NoError(t, err)
|
||||
defer r.Close()
|
||||
b, err := io.ReadAll(r)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("hi"), b)
|
||||
}
|
||||
|
||||
func Test_openInput_doubleStdinErrors(t *testing.T) {
|
||||
withStdin(t, bytes.NewBufferString("hi"))
|
||||
var claims ioClaims
|
||||
|
||||
r, err := openInput("ca", "-", &claims)
|
||||
require.NoError(t, err)
|
||||
r.Close()
|
||||
|
||||
_, err = openInput("crt", "-", &claims)
|
||||
require.EqualError(t, err, `-ca and -crt both set to "-", only one input may read from stdin`)
|
||||
}
|
||||
|
||||
func Test_writeOutput_stdout(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
err := writeOutput("-", []byte("payload"), 0600, out)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "payload", out.String())
|
||||
}
|
||||
|
||||
func Test_writeOutput_file(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "f")
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
err := writeOutput(p, []byte("payload"), 0600, out)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out.String())
|
||||
got, err := os.ReadFile(p)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("payload"), got)
|
||||
}
|
||||
|
||||
func Test_reserveOutputs_noConflict(t *testing.T) {
|
||||
var claims ioClaims
|
||||
require.NoError(t, reserveOutputs(&claims,
|
||||
"out-key", "/tmp/key",
|
||||
"out-crt", "-",
|
||||
"out-qr", "",
|
||||
))
|
||||
assert.Equal(t, "out-crt", claims.out)
|
||||
}
|
||||
|
||||
func Test_reserveOutputs_conflict(t *testing.T) {
|
||||
var claims ioClaims
|
||||
err := reserveOutputs(&claims,
|
||||
"out-key", "-",
|
||||
"out-crt", "-",
|
||||
)
|
||||
require.EqualError(t, err, `-out-key and -out-crt both set to "-", only one output may write to stdout`)
|
||||
}
|
||||
|
||||
func Test_reserveOutputs_panicsOnOddPairs(t *testing.T) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
require.NotNil(t, r)
|
||||
}()
|
||||
var claims ioClaims
|
||||
_ = reserveOutputs(&claims, "out-key")
|
||||
}
|
||||
|
||||
func Test_reserveInputs_noConflict(t *testing.T) {
|
||||
var claims ioClaims
|
||||
require.NoError(t, reserveInputs(&claims,
|
||||
"ca-key", "/tmp/ca.key",
|
||||
"ca-crt", "-",
|
||||
"in-pub", "",
|
||||
))
|
||||
assert.Equal(t, "ca-crt", claims.in)
|
||||
}
|
||||
|
||||
func Test_reserveInputs_conflict(t *testing.T) {
|
||||
var claims ioClaims
|
||||
err := reserveInputs(&claims,
|
||||
"ca-key", "-",
|
||||
"ca-crt", "-",
|
||||
)
|
||||
require.EqualError(t, err, `-ca-key and -ca-crt both set to "-", only one input may read from stdin`)
|
||||
}
|
||||
|
||||
func Test_claimIn_idempotent(t *testing.T) {
|
||||
// pre-claim then a lazy re-claim of the same flag should be a no-op
|
||||
var claims ioClaims
|
||||
require.NoError(t, claims.claimIn("ca-key"))
|
||||
require.NoError(t, claims.claimIn("ca-key"))
|
||||
assert.Equal(t, "ca-key", claims.in)
|
||||
}
|
||||
|
||||
func Test_claimOut_idempotent(t *testing.T) {
|
||||
var claims ioClaims
|
||||
require.NoError(t, claims.claimOut("out-crt"))
|
||||
require.NoError(t, claims.claimOut("out-crt"))
|
||||
assert.Equal(t, "out-crt", claims.out)
|
||||
}
|
||||
|
||||
func Test_isStdio(t *testing.T) {
|
||||
assert.True(t, isStdio("-"))
|
||||
assert.False(t, isStdio(""))
|
||||
assert.False(t, isStdio("./-"))
|
||||
assert.False(t, isStdio("foo"))
|
||||
}
|
||||
@@ -39,18 +39,26 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
caFile, err := os.Open(*vf.caPath)
|
||||
var claims ioClaims
|
||||
if err := reserveInputs(&claims,
|
||||
"ca", *vf.caPath,
|
||||
"crt", *vf.certPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caReader, err := openInput("ca", *vf.caPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca: %w", err)
|
||||
}
|
||||
defer caFile.Close()
|
||||
defer caReader.Close()
|
||||
|
||||
caPool, err := cert.NewCAPoolFromPEMReader(caFile)
|
||||
caPool, err := cert.NewCAPoolFromPEMReader(caReader)
|
||||
if err != nil && !errors.Is(err, cert.ErrExpired) {
|
||||
return fmt.Errorf("error while adding ca cert to pool: %w", err)
|
||||
}
|
||||
|
||||
rawCert, err := os.ReadFile(*vf.certPath)
|
||||
rawCert, err := readInput("crt", *vf.certPath, &claims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read crt: %w", err)
|
||||
}
|
||||
@@ -85,6 +93,7 @@ func verifySummary() string {
|
||||
func verifyHelp(out io.Writer) {
|
||||
vf := newVerifyFlags()
|
||||
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + verifySummary() + "\n"))
|
||||
_, _ = out.Write([]byte(stdioHelpText))
|
||||
vf.set.SetOutput(out)
|
||||
vf.set.PrintDefaults()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func Test_verifyHelp(t *testing.T) {
|
||||
assert.Equal(
|
||||
t,
|
||||
"Usage of "+os.Args[0]+" verify <flags>: verifies a certificate isn't expired and was signed by a trusted authority.\n"+
|
||||
" Pass \"-\" to any path flag to read from stdin or write to stdout.\n"+
|
||||
" -ca string\n"+
|
||||
" \tRequired: path to a file containing one or more ca certificates\n"+
|
||||
" -crt string\n"+
|
||||
@@ -122,3 +123,46 @@ func Test_verify(t *testing.T) {
|
||||
assert.Empty(t, eb.String())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_verify_stdio(t *testing.T) {
|
||||
ob := &bytes.Buffer{}
|
||||
eb := &bytes.Buffer{}
|
||||
|
||||
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
ca, _ := NewTestCaCert("test-ca", caPub, caPriv, time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour*2), nil, nil, nil)
|
||||
caPEM, _ := ca.MarshalPEM()
|
||||
|
||||
crt, _ := NewTestCert(ca, caPriv, "test-cert", time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour), nil, nil, nil)
|
||||
crtPEM, _ := crt.MarshalPEM()
|
||||
|
||||
caFile, err := os.CreateTemp("", "verify-ca")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(caFile.Name())
|
||||
caFile.Write(caPEM)
|
||||
|
||||
// crt on stdin, ca on disk
|
||||
withStdin(t, bytes.NewReader(crtPEM))
|
||||
require.NoError(t, verify([]string{"-ca", caFile.Name(), "-crt", "-"}, ob, eb))
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// ca on stdin, crt on disk
|
||||
certFile, err := os.CreateTemp("", "verify-cert")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(certFile.Name())
|
||||
certFile.Write(crtPEM)
|
||||
|
||||
withStdin(t, bytes.NewReader(caPEM))
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.NoError(t, verify([]string{"-ca", "-", "-crt", certFile.Name()}, ob, eb))
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// both flags on stdin should error
|
||||
withStdin(t, bytes.NewReader(caPEM))
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
require.EqualError(t, verify([]string{"-ca", "-", "-crt", "-"}, ob, eb),
|
||||
`-ca and -crt both set to "-", only one input may read from stdin`)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,12 @@ func main() {
|
||||
l := logging.NewLogger(os.Stdout)
|
||||
|
||||
if *serviceFlag != "" {
|
||||
if err := doService(configPath, configTest, Build, serviceFlag); err != nil {
|
||||
if *configTest {
|
||||
fmt.Println("-test is not supported with -service, run the config test without -service")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := doService(configPath, Build, serviceFlag); err != nil {
|
||||
l.Error("Service command failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -61,9 +66,12 @@ func main() {
|
||||
}
|
||||
|
||||
if *configPath == "" {
|
||||
fmt.Println("-config flag must be set")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
p, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
*configPath = p
|
||||
}
|
||||
|
||||
c := config.NewC(l)
|
||||
@@ -90,15 +98,14 @@ func main() {
|
||||
}
|
||||
|
||||
if !*configTest {
|
||||
wait, err := ctrl.Start()
|
||||
if err != nil {
|
||||
if err := ctrl.Start(); err != nil {
|
||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go ctrl.ShutdownBlock()
|
||||
|
||||
if err := wait(); err != nil {
|
||||
if err := ctrl.Wait(); err != nil {
|
||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"github.com/slackhq/nebula"
|
||||
@@ -16,7 +15,6 @@ var logger service.Logger
|
||||
|
||||
type program struct {
|
||||
configPath *string
|
||||
configTest *bool
|
||||
build string
|
||||
control *nebula.Control
|
||||
}
|
||||
@@ -42,39 +40,47 @@ func (p *program) Start(s service.Service) error {
|
||||
}
|
||||
})
|
||||
|
||||
p.control, err = nebula.Main(c, *p.configTest, Build, l, nil)
|
||||
p.control, err = nebula.Main(c, false, Build, l, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.control.Start()
|
||||
if err := p.control.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Nebula can stop itself on a fatal packet reader error, make sure to log it if it happens.
|
||||
go func() {
|
||||
if err := p.control.Wait(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Nebula stopped due to fatal error: %v", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) Stop(s service.Service) error {
|
||||
logger.Info("Nebula service stopping.")
|
||||
if p.control == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.control.Stop()
|
||||
|
||||
// block until nebula has fully drained before reporting stopped.
|
||||
// error logging is handled by Start.
|
||||
_ = p.control.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func doService(configPath *string, configTest *bool, build string, serviceFlag *string) error {
|
||||
func doService(configPath *string, build string, serviceFlag *string) error {
|
||||
if *configPath == "" {
|
||||
ex, err := os.Executable()
|
||||
p, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*configPath = filepath.Dir(ex) + "/config.yaml"
|
||||
if !fileExists(*configPath) {
|
||||
*configPath = filepath.Dir(ex) + "/config.yml"
|
||||
}
|
||||
*configPath = p
|
||||
}
|
||||
|
||||
svcConfig := &service.Config{
|
||||
@@ -86,7 +92,6 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
|
||||
|
||||
prg := &program{
|
||||
configPath: configPath,
|
||||
configTest: configTest,
|
||||
build: build,
|
||||
}
|
||||
|
||||
@@ -118,8 +123,9 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
|
||||
switch *serviceFlag {
|
||||
case "run":
|
||||
if err := s.Run(); err != nil {
|
||||
// Route any errors to the system logger
|
||||
// Route any errors to the system logger and report the failure
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := service.Control(s, *serviceFlag); err != nil {
|
||||
|
||||
+8
-6
@@ -50,9 +50,12 @@ func main() {
|
||||
}
|
||||
|
||||
if *configPath == "" {
|
||||
fmt.Println("-config flag must be set")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
p, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
*configPath = p
|
||||
}
|
||||
|
||||
l := logging.NewLogger(os.Stdout)
|
||||
@@ -81,8 +84,7 @@ func main() {
|
||||
}
|
||||
|
||||
if !*configTest {
|
||||
wait, err := ctrl.Start()
|
||||
if err != nil {
|
||||
if err := ctrl.Start(); err != nil {
|
||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -90,7 +92,7 @@ func main() {
|
||||
go ctrl.ShutdownBlock()
|
||||
notifyReady(l)
|
||||
|
||||
if err := wait(); err != nil {
|
||||
if err := ctrl.Wait(); err != nil {
|
||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DefaultPath returns a path to a config file alongside the running executable, preferring config.yaml over config.yml.
|
||||
// If neither file exists an error is returned that names both paths checked.
|
||||
func DefaultPath() (string, error) {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return defaultPathInDir(filepath.Dir(ex))
|
||||
}
|
||||
|
||||
func defaultPathInDir(dir string) (string, error) {
|
||||
yamlPath := filepath.Join(dir, "config.yaml")
|
||||
if _, err := os.Stat(yamlPath); err == nil {
|
||||
return yamlPath, nil
|
||||
}
|
||||
ymlPath := filepath.Join(dir, "config.yml")
|
||||
if _, err := os.Stat(ymlPath); err == nil {
|
||||
return ymlPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("no default config found at %s or %s", yamlPath, ymlPath)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultPathInDir(t *testing.T) {
|
||||
t.Run("prefers config.yaml when both exist", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
want := filepath.Join(dir, "config.yaml")
|
||||
other := filepath.Join(dir, "config.yml")
|
||||
require.NoError(t, os.WriteFile(want, []byte("a: 1"), 0644))
|
||||
require.NoError(t, os.WriteFile(other, []byte("a: 2"), 0644))
|
||||
|
||||
got, err := defaultPathInDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
|
||||
t.Run("returns config.yaml when only it exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
want := filepath.Join(dir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(want, []byte("a: 1"), 0644))
|
||||
|
||||
got, err := defaultPathInDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
|
||||
t.Run("falls back to config.yml when only it exists", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
want := filepath.Join(dir, "config.yml")
|
||||
require.NoError(t, os.WriteFile(want, []byte("a: 1"), 0644))
|
||||
|
||||
got, err := defaultPathInDir(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, got)
|
||||
})
|
||||
|
||||
t.Run("errors when neither exists and names both paths", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
got, err := defaultPathInDir(dir)
|
||||
assert.Empty(t, got)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), filepath.Join(dir, "config.yaml"))
|
||||
assert.Contains(t, err.Error(), filepath.Join(dir, "config.yml"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultPath(t *testing.T) {
|
||||
got, err := DefaultPath()
|
||||
if err != nil {
|
||||
ex, exErr := os.Executable()
|
||||
require.NoError(t, exErr)
|
||||
assert.Contains(t, err.Error(), filepath.Dir(ex))
|
||||
return
|
||||
}
|
||||
ex, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Dir(ex), filepath.Dir(got))
|
||||
assert.Contains(t, []string{"config.yaml", "config.yml"}, filepath.Base(got))
|
||||
}
|
||||
+2
-10
@@ -136,14 +136,6 @@ func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time)
|
||||
return in, out
|
||||
}
|
||||
|
||||
// AddTrafficWatch must be called for every new HostInfo.
|
||||
// We will continue to monitor the HostInfo until the tunnel is dropped.
|
||||
func (cm *connectionManager) AddTrafficWatch(h *HostInfo) {
|
||||
if h.out.Swap(true) == false {
|
||||
cm.trafficTimer.Add(h.localIndexId, cm.checkInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *connectionManager) Start(ctx context.Context) {
|
||||
clockSource := time.NewTicker(cm.trafficTimer.t.tickDuration)
|
||||
defer clockSource.Stop()
|
||||
@@ -306,8 +298,8 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
||||
} else {
|
||||
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||
cm.l.Info("send CreateRelayRequest",
|
||||
"relayFrom", req.RelayFromAddr,
|
||||
"relayTo", req.RelayToAddr,
|
||||
"relayFrom", relayFrom,
|
||||
"relayTo", relayTo,
|
||||
"initiatorRelayIndex", req.InitiatorRelayIndex,
|
||||
"responderRelayIndex", req.ResponderRelayIndex,
|
||||
"vpnAddrs", newhostinfo.vpnAddrs,
|
||||
|
||||
+51
-25
@@ -69,29 +69,29 @@ type ControlHostInfo struct {
|
||||
}
|
||||
|
||||
// Start actually runs nebula, this is a nonblocking call.
|
||||
// The returned function blocks until nebula has fully stopped and returns the
|
||||
// first fatal reader error (if any). A nil error means nebula shut down
|
||||
// gracefully; a non-nil error means a reader hit an unexpected failure that
|
||||
// triggered the shutdown.
|
||||
func (c *Control) Start() (func() error, error) {
|
||||
// Use Wait to block until nebula has fully stopped and to learn whether a fatal reader error caused the shutdown.
|
||||
func (c *Control) Start() error {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
switch c.state {
|
||||
case StateReady:
|
||||
//yay!
|
||||
case StateStopped, StateStopping:
|
||||
return nil, ErrAlreadyStopped
|
||||
return ErrAlreadyStopped
|
||||
case StateStarted:
|
||||
return nil, ErrAlreadyStarted
|
||||
return ErrAlreadyStarted
|
||||
default:
|
||||
return nil, ErrUnknownState
|
||||
return ErrUnknownState
|
||||
}
|
||||
|
||||
// Activate the interface
|
||||
err := c.f.activate()
|
||||
if err != nil {
|
||||
// Cancel before Close so a caller returning from Wait always observes a dead Context
|
||||
c.cancel()
|
||||
_ = c.f.Close()
|
||||
c.state = StateStopped
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Call all the delayed funcs that waited patiently for the interface to be created.
|
||||
@@ -114,13 +114,9 @@ func (c *Control) Start() (func() error, error) {
|
||||
c.f.triggerShutdown = c.Stop
|
||||
|
||||
// Start reading packets.
|
||||
out, err := c.f.run()
|
||||
if err != nil {
|
||||
c.state = StateStopped
|
||||
return nil, err
|
||||
}
|
||||
c.f.run()
|
||||
c.state = StateStarted
|
||||
return out, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) State() RunState {
|
||||
@@ -133,10 +129,26 @@ func (c *Control) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
// Stop is a non-blocking call that signals nebula to close all tunnels and shut down
|
||||
// Stop tears nebula down, closing all tunnels and releasing everything it holds.
|
||||
// Use Wait to block until the shutdown has completed.
|
||||
// A Control that has been stopped cannot be started again, Start will return ErrAlreadyStopped.
|
||||
func (c *Control) Stop() {
|
||||
c.stateLock.Lock()
|
||||
if c.state != StateStarted {
|
||||
switch c.state {
|
||||
case StateStarted:
|
||||
// Fall through to the full teardown below
|
||||
|
||||
case StateReady:
|
||||
// Never started
|
||||
c.cancel()
|
||||
c.state = StateStopped
|
||||
if err := c.f.Close(); err != nil {
|
||||
c.l.Error("Close interface failed", "error", err)
|
||||
}
|
||||
c.stateLock.Unlock()
|
||||
return
|
||||
|
||||
default:
|
||||
c.stateLock.Unlock()
|
||||
// We are stopping or stopped already
|
||||
return
|
||||
@@ -145,19 +157,26 @@ func (c *Control) Stop() {
|
||||
c.state = StateStopping
|
||||
c.stateLock.Unlock()
|
||||
|
||||
// Stop the handshakeManager (and other services), to prevent new tunnels from
|
||||
// being created while we're shutting them all down.
|
||||
// Closing tunnels can be slow with a large hostmap, don't hold the lock for it
|
||||
c.cancel()
|
||||
|
||||
c.CloseAllTunnels(false)
|
||||
|
||||
c.stateLock.Lock()
|
||||
c.state = StateStopped
|
||||
if err := c.f.Close(); err != nil {
|
||||
c.l.Error("Close interface failed", "error", err)
|
||||
}
|
||||
c.stateLock.Lock()
|
||||
c.state = StateStopped
|
||||
c.stateLock.Unlock()
|
||||
}
|
||||
|
||||
// Wait blocks until nebula has fully stopped, either via Stop or an internal fatal error,
|
||||
// and returns the first fatal packet reader error if there was one.
|
||||
// It is safe to call from multiple goroutines and at any point in the lifecycle,
|
||||
// but a Wait on a Control that is never started and never stopped will block forever.
|
||||
func (c *Control) Wait() error {
|
||||
return c.f.wait()
|
||||
}
|
||||
|
||||
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
||||
func (c *Control) ShutdownBlock() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
@@ -170,8 +189,15 @@ func (c *Control) ShutdownBlock() {
|
||||
c.Stop()
|
||||
}
|
||||
|
||||
// RebindUDPServer asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change
|
||||
// RebindUDPServer asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change.
|
||||
func (c *Control) RebindUDPServer() {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
|
||||
if c.state != StateStarted {
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.f.outside.Rebind()
|
||||
|
||||
// Trigger a lighthouse update, useful for mobile clients that should have an update interval of 0
|
||||
@@ -305,7 +331,7 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
||||
|
||||
c.l.Debug("Sending close tunnel message",
|
||||
"vpnAddrs", h.vpnAddrs,
|
||||
"udpAddr", h.remote,
|
||||
"udpAddr", h.GetRemote(),
|
||||
)
|
||||
closed++
|
||||
}
|
||||
@@ -350,7 +376,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
||||
RemoteAddrs: h.remotes.CopyAddrs(preferredRanges),
|
||||
CurrentRelaysToMe: h.relayState.CopyRelayIps(),
|
||||
CurrentRelaysThroughMe: h.relayState.CopyRelayForIps(),
|
||||
CurrentRemote: h.remote,
|
||||
CurrentRemote: h.GetRemote(),
|
||||
}
|
||||
|
||||
for i, a := range h.vpnAddrs {
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeDevice struct {
|
||||
closeOnce sync.Once
|
||||
closedCh chan struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newFakeDevice() *fakeDevice {
|
||||
return &fakeDevice{closedCh: make(chan struct{})}
|
||||
}
|
||||
|
||||
// Read blocks until Close like a real tun with no traffic, then reports EOF
|
||||
// the same way a closed device does
|
||||
func (d *fakeDevice) Read() ([]tio.Packet, error) {
|
||||
<-d.closedCh
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
func (d *fakeDevice) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
func (d *fakeDevice) Close() error {
|
||||
d.closeOnce.Do(func() {
|
||||
d.closed = true
|
||||
close(d.closedCh)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *fakeDevice) Activate() error { return nil }
|
||||
func (d *fakeDevice) Networks() []netip.Prefix { return nil }
|
||||
func (d *fakeDevice) Name() string { return "fake" }
|
||||
func (d *fakeDevice) RoutesFor(netip.Addr) routing.Gateways { return nil }
|
||||
|
||||
func (d *fakeDevice) Queues(int) ([]tio.Queue, error) { return []tio.Queue{d}, nil }
|
||||
|
||||
// newReadyControl hand-builds the minimum Control that Main would have
|
||||
// produced right before Start, including the construction token NewInterface
|
||||
// takes so waiters block until Close releases the resources
|
||||
func newReadyControl(t *testing.T) (*Control, *fakeDevice, *fakeConn) {
|
||||
l := test.NewLogger()
|
||||
dev := newFakeDevice()
|
||||
conn := &fakeConn{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
myVpnNet := netip.MustParsePrefix("10.128.0.1/16")
|
||||
nt := new(bart.Lite)
|
||||
nt.Insert(myVpnNet)
|
||||
cs := &CertState{
|
||||
myVpnNetworks: []netip.Prefix{myVpnNet},
|
||||
myVpnNetworksTable: nt,
|
||||
}
|
||||
lh, err := NewLightHouseFromConfig(ctx, l, config.NewC(l), cs, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
f := &Interface{
|
||||
ctx: ctx,
|
||||
inside: dev,
|
||||
outside: conn,
|
||||
writers: []udp.Conn{conn},
|
||||
routines: 1,
|
||||
hostMap: newHostMap(l),
|
||||
lightHouse: lh,
|
||||
l: l,
|
||||
}
|
||||
f.wg.Add(1)
|
||||
|
||||
return &Control{
|
||||
state: StateReady,
|
||||
f: f,
|
||||
l: l,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, dev, conn
|
||||
}
|
||||
|
||||
func TestControl_StopBeforeStart(t *testing.T) {
|
||||
c, dev, conn := newReadyControl(t)
|
||||
|
||||
// A Stop on a never started control must release everything Main acquired
|
||||
c.Stop()
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
assert.True(t, dev.closed, "the tun device should have been closed")
|
||||
assert.True(t, conn.closed, "the udp socket should have been closed")
|
||||
require.ErrorIs(t, c.ctx.Err(), context.Canceled, "the service context should have been cancelled")
|
||||
|
||||
// Wait must return promptly now that the resources are released
|
||||
require.NoError(t, c.Wait())
|
||||
|
||||
// A stopped control can never be started
|
||||
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
|
||||
|
||||
// A second Stop is a harmless no-op
|
||||
c.Stop()
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
require.NoError(t, c.Wait())
|
||||
}
|
||||
|
||||
func TestControl_WaitBlocksUntilStop(t *testing.T) {
|
||||
c, _, _ := newReadyControl(t)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- c.Wait() }()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatal("Wait returned before Stop")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
c.Stop()
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Wait did not return after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeConn struct {
|
||||
closed bool
|
||||
rebinds int
|
||||
}
|
||||
|
||||
func (c *fakeConn) Rebind() error { c.rebinds++; return nil }
|
||||
func (c *fakeConn) LocalAddr() (netip.AddrPort, error) { return netip.AddrPort{}, nil }
|
||||
func (c *fakeConn) ListenOut(_ udp.EncReader) error { return nil }
|
||||
func (c *fakeConn) WriteTo(_ []byte, _ netip.AddrPort) error { return nil }
|
||||
func (c *fakeConn) ReloadConfig(_ *config.C) {}
|
||||
func (c *fakeConn) SupportsMultipleReaders() bool { return true }
|
||||
func (c *fakeConn) Close() error { c.closed = true; return nil }
|
||||
|
||||
type multiqueueDevice struct {
|
||||
*fakeDevice
|
||||
}
|
||||
|
||||
// Queues claims multiqueue support but fails to open the second queue,
|
||||
// exercising the activation error path.
|
||||
func (d *multiqueueDevice) Queues(n int) ([]tio.Queue, error) {
|
||||
if n > 1 {
|
||||
return nil, errors.New("second queue failed to open")
|
||||
}
|
||||
return d.fakeDevice.Queues(n)
|
||||
}
|
||||
|
||||
func TestControl_StartMultiqueueFailureReleases(t *testing.T) {
|
||||
dev := &multiqueueDevice{fakeDevice: newFakeDevice()}
|
||||
conn := &fakeConn{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
f := &Interface{
|
||||
ctx: ctx,
|
||||
inside: dev,
|
||||
outside: conn,
|
||||
writers: []udp.Conn{conn},
|
||||
routines: 2,
|
||||
l: test.NewLogger(),
|
||||
}
|
||||
f.wg.Add(1)
|
||||
|
||||
c := &Control{
|
||||
state: StateReady,
|
||||
f: f,
|
||||
l: test.NewLogger(),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
// The second reader fails to open, everything must be released
|
||||
require.Error(t, c.Start())
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
assert.True(t, dev.closed, "the tun device should have been closed")
|
||||
assert.True(t, conn.closed, "the udp socket should have been closed")
|
||||
require.ErrorIs(t, c.ctx.Err(), context.Canceled)
|
||||
|
||||
// And Wait must not hang on the construction token
|
||||
require.NoError(t, c.Wait())
|
||||
}
|
||||
|
||||
func TestInterface_CloseIsIdempotent(t *testing.T) {
|
||||
dev := newFakeDevice()
|
||||
f := &Interface{
|
||||
inside: dev,
|
||||
l: test.NewLogger(),
|
||||
}
|
||||
f.wg.Add(1)
|
||||
|
||||
require.NoError(t, f.Close())
|
||||
assert.True(t, dev.closed)
|
||||
|
||||
// A second Close must not double release the wg token or the device
|
||||
require.NoError(t, f.Close())
|
||||
require.NoError(t, f.wait())
|
||||
}
|
||||
|
||||
func TestControl_FatalErrorReportsThroughWait(t *testing.T) {
|
||||
c, dev, conn := newReadyControl(t)
|
||||
|
||||
// Mirror what Start wires up, without needing real packet readers
|
||||
c.f.triggerShutdown = c.Stop
|
||||
c.state = StateStarted
|
||||
|
||||
boom := errors.New("boom")
|
||||
c.f.onFatal(boom)
|
||||
|
||||
require.ErrorIs(t, c.Wait(), boom)
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
assert.True(t, dev.closed)
|
||||
assert.True(t, conn.closed)
|
||||
|
||||
// A second fatal error must not fire the shutdown again or replace the first
|
||||
c.f.onFatal(errors.New("later"))
|
||||
require.ErrorIs(t, c.Wait(), boom)
|
||||
|
||||
// Wait stays factual, a Stop after the death does not mask the error
|
||||
c.Stop()
|
||||
require.ErrorIs(t, c.Wait(), boom)
|
||||
}
|
||||
|
||||
func TestControl_ConcurrentStopAndStart(t *testing.T) {
|
||||
c, _, _ := newReadyControl(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 2; i++ {
|
||||
wg.Go(func() { c.Stop() })
|
||||
}
|
||||
wg.Go(func() { _ = c.Start() })
|
||||
wg.Go(func() {
|
||||
_ = c.Wait()
|
||||
// A returned Wait must always observe the final state, no matter how
|
||||
// the race resolved
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
})
|
||||
wg.Wait()
|
||||
|
||||
// However the race resolves, the control must end fully stopped with no
|
||||
// panic and Wait must observe the final state
|
||||
require.NoError(t, c.Wait())
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
|
||||
}
|
||||
|
||||
func TestControl_StartStopLifecycle(t *testing.T) {
|
||||
c, dev, conn := newReadyControl(t)
|
||||
|
||||
require.NoError(t, c.Start())
|
||||
assert.Equal(t, StateStarted, c.State())
|
||||
require.ErrorIs(t, c.Start(), ErrAlreadyStarted)
|
||||
|
||||
// Stop must unpark the reader blocked in the device and release everything
|
||||
c.Stop()
|
||||
assert.Equal(t, StateStopped, c.State())
|
||||
assert.True(t, dev.closed, "the tun device should have been closed")
|
||||
assert.True(t, conn.closed, "the udp socket should have been closed")
|
||||
require.ErrorIs(t, c.ctx.Err(), context.Canceled)
|
||||
|
||||
// The reader drained off a closed device, that is not a fatal error
|
||||
require.NoError(t, c.Wait())
|
||||
require.ErrorIs(t, c.Start(), ErrAlreadyStopped)
|
||||
}
|
||||
|
||||
func TestControl_RebindIsGatedByState(t *testing.T) {
|
||||
c, _, conn := newReadyControl(t)
|
||||
|
||||
// A rebind before Start reaches nothing, the interface is not up
|
||||
c.RebindUDPServer()
|
||||
assert.Equal(t, 0, conn.rebinds, "rebind before start must be a no-op")
|
||||
|
||||
require.NoError(t, c.Start())
|
||||
c.RebindUDPServer()
|
||||
assert.Equal(t, 1, conn.rebinds, "rebind while started must reach the conn")
|
||||
|
||||
// A rebind racing a completed stop must not touch the closed conn
|
||||
c.Stop()
|
||||
require.NoError(t, c.Wait())
|
||||
c.RebindUDPServer()
|
||||
assert.Equal(t, 1, conn.rebinds, "rebind after stop must be a no-op")
|
||||
}
|
||||
+161
-6
@@ -1,6 +1,8 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
@@ -42,8 +45,7 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
|
||||
crt := &dummyCert{}
|
||||
hm.unlockedAddHostInfo(&HostInfo{
|
||||
remote: remote1,
|
||||
hi := &HostInfo{
|
||||
remotes: remotes,
|
||||
ConnectionState: &ConnectionState{
|
||||
peerCert: &cert.CachedCertificate{Certificate: crt},
|
||||
@@ -56,13 +58,14 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}, &Interface{})
|
||||
}
|
||||
hi.remote.Store(&remote1)
|
||||
hm.unlockedAddHostInfo(hi, &Interface{})
|
||||
|
||||
vpnIp2, ok := netip.AddrFromSlice(ipNet2.IP)
|
||||
assert.True(t, ok)
|
||||
|
||||
hm.unlockedAddHostInfo(&HostInfo{
|
||||
remote: remote1,
|
||||
hi2 := &HostInfo{
|
||||
remotes: remotes,
|
||||
ConnectionState: &ConnectionState{
|
||||
peerCert: nil,
|
||||
@@ -75,7 +78,9 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}, &Interface{})
|
||||
}
|
||||
hi2.remote.Store(&remote1)
|
||||
hm.unlockedAddHostInfo(hi2, &Interface{})
|
||||
|
||||
c := Control{
|
||||
state: StateReady,
|
||||
@@ -119,3 +124,153 @@ func assertFields(t *testing.T, expected []string, actualStruct any) {
|
||||
|
||||
assert.Equal(t, expected, fields)
|
||||
}
|
||||
|
||||
// alwaysAllowV4/V6 are check funcs that accept every entry (including nil pointers),
|
||||
// letting us inject a nil *V4AddrPort/*V6AddrPort into a RemoteList's reported cache
|
||||
// the same way a malformed proto message off the wire could.
|
||||
func alwaysAllowV4(netip.Addr, *V4AddrPort) bool { return true }
|
||||
func alwaysAllowV6(netip.Addr, *V6AddrPort) bool { return true }
|
||||
|
||||
// TestGetRelays_SkipsNilRelayAddrs proves GetRelays tolerates nil entries in the
|
||||
// RelayVpnAddrs proto slice (which protoAddrToNetAddr would nil-deref on) and still
|
||||
// returns the valid relays, including the legacy OldRelayVpnAddrs.
|
||||
func TestGetRelays_SkipsNilRelayAddrs(t *testing.T) {
|
||||
good := netip.MustParseAddr("10.0.0.9")
|
||||
|
||||
d := &NebulaMetaDetails{
|
||||
OldRelayVpnAddrs: []uint32{0x0a000001}, // 10.0.0.1
|
||||
RelayVpnAddrs: []*Addr{
|
||||
nil,
|
||||
netAddrToProtoAddr(good),
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
var relays []netip.Addr
|
||||
require.NotPanics(t, func() { relays = d.GetRelays() })
|
||||
|
||||
assert.Equal(t, []netip.Addr{
|
||||
netip.MustParseAddr("10.0.0.1"),
|
||||
good,
|
||||
}, relays)
|
||||
}
|
||||
|
||||
// TestGetRelays_AllNil ensures an all-nil RelayVpnAddrs slice yields no relays and no panic.
|
||||
func TestGetRelays_AllNil(t *testing.T) {
|
||||
d := &NebulaMetaDetails{RelayVpnAddrs: []*Addr{nil, nil}}
|
||||
var relays []netip.Addr
|
||||
require.NotPanics(t, func() { relays = d.GetRelays() })
|
||||
assert.Empty(t, relays)
|
||||
}
|
||||
|
||||
// TestRemoteList_CopyCache_SkipsNilReported proves CopyCache skips nil reported
|
||||
// pointers (v4 and v6) instead of nil-dereferencing them in protoV*AddrPortToNetAddrPort.
|
||||
func TestRemoteList_CopyCache_SkipsNilReported(t *testing.T) {
|
||||
owner := netip.MustParseAddr("10.0.0.1")
|
||||
rl := NewRemoteList([]netip.Addr{owner}, nil)
|
||||
|
||||
rl.unlockedSetV4(owner, owner, []*V4AddrPort{
|
||||
nil,
|
||||
newIp4AndPortFromString("1.2.3.4:5"),
|
||||
nil,
|
||||
}, alwaysAllowV4)
|
||||
|
||||
rl.unlockedSetV6(owner, owner, []*V6AddrPort{
|
||||
nil,
|
||||
newIp6AndPortFromString("[1::1]:6"),
|
||||
nil,
|
||||
}, alwaysAllowV6)
|
||||
|
||||
var cm *CacheMap
|
||||
require.NotPanics(t, func() { cm = rl.CopyCache() })
|
||||
|
||||
c := (*cm)[owner.String()]
|
||||
require.NotNil(t, c)
|
||||
assert.ElementsMatch(t, []netip.AddrPort{
|
||||
netip.MustParseAddrPort("1.2.3.4:5"),
|
||||
netip.MustParseAddrPort("[1::1]:6"),
|
||||
}, c.Reported)
|
||||
}
|
||||
|
||||
// TestRemoteList_Rebuild_SkipsNilReported drives unlockedCollect (via Rebuild) with
|
||||
// nil reported entries and confirms only the valid addresses survive, with no panic.
|
||||
func TestRemoteList_Rebuild_SkipsNilReported(t *testing.T) {
|
||||
owner := netip.MustParseAddr("10.0.0.1")
|
||||
rl := NewRemoteList([]netip.Addr{owner}, nil)
|
||||
|
||||
rl.unlockedSetV4(owner, owner, []*V4AddrPort{
|
||||
nil,
|
||||
newIp4AndPortFromString("1.2.3.4:5"),
|
||||
}, alwaysAllowV4)
|
||||
rl.unlockedSetV6(owner, owner, []*V6AddrPort{
|
||||
newIp6AndPortFromString("[1::1]:6"),
|
||||
nil,
|
||||
}, alwaysAllowV6)
|
||||
|
||||
require.NotPanics(t, func() { rl.Rebuild([]netip.Prefix{}) })
|
||||
|
||||
assert.ElementsMatch(t, []netip.AddrPort{
|
||||
netip.MustParseAddrPort("1.2.3.4:5"),
|
||||
netip.MustParseAddrPort("[1::1]:6"),
|
||||
}, rl.addrs)
|
||||
}
|
||||
|
||||
// newRelayControl marshals a NebulaControl the way it arrives on the wire so we can feed
|
||||
// it through HandleControlMsg's unmarshal + validate path.
|
||||
func newRelayControl(t *testing.T, typ NebulaControl_MessageType, from, to *Addr) []byte {
|
||||
t.Helper()
|
||||
msg := &NebulaControl{
|
||||
Type: typ,
|
||||
RelayFromAddr: from,
|
||||
RelayToAddr: to,
|
||||
}
|
||||
b, err := msg.Marshal()
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
||||
// TestRelayManager_HandleControlMsg_NilRelayAddrs verifies the validation block added to
|
||||
// HandleControlMsg: CreateRelay{Request,Response} carrying a nil RelayFromAddr or
|
||||
// RelayToAddr are dropped with a debug log rather than nil-dereferencing downstream.
|
||||
func TestRelayManager_HandleControlMsg_NilRelayAddrs(t *testing.T) {
|
||||
good := netAddrToProtoAddr(netip.MustParseAddr("10.0.0.9"))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
typ NebulaControl_MessageType
|
||||
from *Addr
|
||||
to *Addr
|
||||
wantLog string // debug substring expected, "" == expect no drop log
|
||||
}{
|
||||
{"request nil from", NebulaControl_CreateRelayRequest, nil, good, "nil RelayFromAddr"},
|
||||
{"request nil to", NebulaControl_CreateRelayRequest, good, nil, "nil RelayToAddr"},
|
||||
{"request both nil", NebulaControl_CreateRelayRequest, nil, nil, "nil RelayFromAddr"},
|
||||
{"response nil from", NebulaControl_CreateRelayResponse, nil, good, "nil RelayFromAddr"},
|
||||
{"response nil to", NebulaControl_CreateRelayResponse, good, nil, "nil RelayToAddr"},
|
||||
// A non-relay control type is not subject to the relay-addr validation and must
|
||||
// pass through it untouched (the final switch simply no-ops on it).
|
||||
{"unrelated type nil addrs", NebulaControl_None, nil, nil, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := test.NewLoggerWithOutputAndLevel(&buf, slog.LevelDebug)
|
||||
rm := &relayManager{l: l, hostmap: newHostMap(l)}
|
||||
rm.useRelays.Store(true)
|
||||
|
||||
f := &Interface{l: l}
|
||||
h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, localIndexId: 1}
|
||||
|
||||
d := newRelayControl(t, tc.typ, tc.from, tc.to)
|
||||
|
||||
require.NotPanics(t, func() { rm.HandleControlMsg(h, d, f) })
|
||||
|
||||
if tc.wantLog == "" {
|
||||
assert.NotContains(t, buf.String(), "nil Relay")
|
||||
} else {
|
||||
assert.Contains(t, buf.String(), tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,14 @@ func (c *Control) GetHostmap() *HostMap {
|
||||
return c.f.hostMap
|
||||
}
|
||||
|
||||
// GetHostmapIndexCount returns the number of entries in the main hostmap Indexes table, holding
|
||||
// the hostmap read lock so tests can poll it while connection manager churns tunnels.
|
||||
func (c *Control) GetHostmapIndexCount() int {
|
||||
c.f.hostMap.RLock()
|
||||
defer c.f.hostMap.RUnlock()
|
||||
return len(c.f.hostMap.Indexes)
|
||||
}
|
||||
|
||||
func (c *Control) GetF() *Interface {
|
||||
return c.f
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -62,7 +62,7 @@ function nebula.dissector(tvbuf, pktinfo, root)
|
||||
tree:add(pf_version, tvbuf:range(0,1))
|
||||
local type = tree:add(pf_type, tvbuf:range(0,1))
|
||||
|
||||
local nebula_type = bit32.band(tvbuf:range(0,1):uint(), 0x0F)
|
||||
local nebula_type = bit.band(tvbuf:range(0,1):uint(), 0x0F)
|
||||
if nebula_type == 0 then
|
||||
local stage = tvbuf(8,8):uint64()
|
||||
tree:add(pf_subtype_handshake, tvbuf:range(1,1))
|
||||
|
||||
+92
-26
@@ -11,19 +11,21 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/slackhq/nebula/config"
|
||||
)
|
||||
|
||||
type dnsServer struct {
|
||||
sync.RWMutex
|
||||
l *slog.Logger
|
||||
ctx context.Context
|
||||
dnsMap4 map[string]netip.Addr
|
||||
dnsMap6 map[string]netip.Addr
|
||||
hostMap *HostMap
|
||||
myVpnAddrsTable *bart.Lite
|
||||
l *slog.Logger
|
||||
ctx context.Context
|
||||
dnsMap4 map[string]netip.Addr
|
||||
dnsMap6 map[string]netip.Addr
|
||||
hostMap *HostMap
|
||||
pki *PKI
|
||||
|
||||
// selfHost is the cached FQDN we last seeded for ourselves
|
||||
selfHost string
|
||||
|
||||
mux *dns.ServeMux
|
||||
|
||||
@@ -55,14 +57,14 @@ type dnsServer struct {
|
||||
// they no-op when DNS isn't enabled. Each Start invocation owns a ctx-cancel
|
||||
// watcher that tears the listener down on nebula shutdown. The returned
|
||||
// pointer is always non-nil, even on error.
|
||||
func newDnsServerFromConfig(ctx context.Context, l *slog.Logger, cs *CertState, hostMap *HostMap, c *config.C) (*dnsServer, error) {
|
||||
func newDnsServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI, hostMap *HostMap, c *config.C) (*dnsServer, error) {
|
||||
ds := &dnsServer{
|
||||
l: l,
|
||||
ctx: ctx,
|
||||
dnsMap4: make(map[string]netip.Addr),
|
||||
dnsMap6: make(map[string]netip.Addr),
|
||||
hostMap: hostMap,
|
||||
myVpnAddrsTable: cs.myVpnAddrsTable,
|
||||
l: l,
|
||||
ctx: ctx,
|
||||
dnsMap4: make(map[string]netip.Addr),
|
||||
dnsMap6: make(map[string]netip.Addr),
|
||||
hostMap: hostMap,
|
||||
pki: pki,
|
||||
}
|
||||
ds.mux = dns.NewServeMux()
|
||||
ds.mux.HandleFunc(".", ds.handleDnsRequest)
|
||||
@@ -76,6 +78,7 @@ func newDnsServerFromConfig(ctx context.Context, l *slog.Logger, cs *CertState,
|
||||
if err := ds.reload(c, true); err != nil {
|
||||
return ds, err
|
||||
}
|
||||
ds.seedSelf()
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
@@ -113,7 +116,7 @@ func (d *dnsServer) reload(c *config.C, initial bool) error {
|
||||
d.Stop()
|
||||
}
|
||||
// Drop any records that accumulated while enabled; a later re-enable
|
||||
// will repopulate from fresh handshakes.
|
||||
// will repopulate from fresh handshakes and a fresh seedSelf.
|
||||
d.clearRecords()
|
||||
return nil
|
||||
}
|
||||
@@ -121,17 +124,14 @@ func (d *dnsServer) reload(c *config.C, initial bool) error {
|
||||
if running == nil {
|
||||
// Was disabled (or never started); bring it up now.
|
||||
go d.Start()
|
||||
return nil
|
||||
} else if !sameAddr {
|
||||
d.shutdownServer(running, runningStarted, "reload")
|
||||
// Old Start goroutine has now exited; bring up a fresh listener on the new address.
|
||||
go d.Start()
|
||||
}
|
||||
|
||||
if sameAddr {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.shutdownServer(running, runningStarted, "reload")
|
||||
// Old Start goroutine has now exited; bring up a fresh listener on the
|
||||
// new address.
|
||||
go d.Start()
|
||||
// Refresh the self entry every enabled reload so cert renewals that change our name or VPN addresses are picked up.
|
||||
d.seedSelf()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -249,6 +249,20 @@ func (d *dnsServer) QueryCert(data string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// The hostmap only ever contains peers we have handshaked with, so it never carries an entry for ourselves.
|
||||
// Answer self lookups straight from the local cert state.
|
||||
if cs := d.certState(); cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
|
||||
c := cs.GetDefaultCertificate()
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
b, err := c.MarshalJSON()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
hostinfo := d.hostMap.QueryVpnAddr(ip)
|
||||
if hostinfo == nil {
|
||||
return ""
|
||||
@@ -266,12 +280,60 @@ func (d *dnsServer) QueryCert(data string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// clearRecords drops all DNS records.
|
||||
// clearRecords drops all DNS records, including the self entry.
|
||||
func (d *dnsServer) clearRecords() {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
clear(d.dnsMap4)
|
||||
clear(d.dnsMap6)
|
||||
d.selfHost = ""
|
||||
}
|
||||
|
||||
// seedSelf inserts (or refreshes) a record for our own cert name pointing at our VPN addresses,
|
||||
// so a single-lighthouse network can resolve the lighthouse's own hostname without the two-process workaround.
|
||||
func (d *dnsServer) seedSelf() {
|
||||
if !d.enabled.Load() {
|
||||
return
|
||||
}
|
||||
cs := d.certState()
|
||||
if cs == nil {
|
||||
return
|
||||
}
|
||||
c := cs.GetDefaultCertificate()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
newHost := strings.ToLower(c.Name()) + "."
|
||||
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
if d.selfHost != "" && d.selfHost != newHost {
|
||||
delete(d.dnsMap4, d.selfHost)
|
||||
delete(d.dnsMap6, d.selfHost)
|
||||
}
|
||||
d.selfHost = newHost
|
||||
delete(d.dnsMap4, newHost)
|
||||
delete(d.dnsMap6, newHost)
|
||||
haveV4, haveV6 := false, false
|
||||
for _, addr := range cs.myVpnAddrs {
|
||||
if addr.Is4() && !haveV4 {
|
||||
d.dnsMap4[newHost] = addr
|
||||
haveV4 = true
|
||||
} else if addr.Is6() && !haveV6 {
|
||||
d.dnsMap6[newHost] = addr
|
||||
haveV6 = true
|
||||
}
|
||||
if haveV4 && haveV6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnsServer) certState() *CertState {
|
||||
if d.pki == nil {
|
||||
return nil
|
||||
}
|
||||
return d.pki.getCertState()
|
||||
}
|
||||
|
||||
// Add adds the first IPv4 and IPv6 address that appears in `addresses` as the record for `host`
|
||||
@@ -309,8 +371,12 @@ func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
cs := d.certState()
|
||||
if cs == nil || cs.myVpnAddrsTable == nil {
|
||||
return false
|
||||
}
|
||||
//if we found it in this table, it's good
|
||||
return d.myVpnAddrsTable.Contains(b)
|
||||
return cs.myVpnAddrsTable.Contains(b)
|
||||
}
|
||||
|
||||
func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -276,6 +279,92 @@ func TestDnsServer_Stop_beforeBind_doesNotHang(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// newTestPKI builds a minimal *PKI with a single v1 cert whose name and
|
||||
// VPN addresses are caller-provided, suitable for exercising seedSelf and
|
||||
// QueryCert self handling.
|
||||
func newTestPKI(t *testing.T, name string, addrs []netip.Addr) *PKI {
|
||||
t.Helper()
|
||||
networks := make([]netip.Prefix, 0, len(addrs))
|
||||
for _, a := range addrs {
|
||||
bits := 32
|
||||
if a.Is6() {
|
||||
bits = 128
|
||||
}
|
||||
networks = append(networks, netip.PrefixFrom(a, bits))
|
||||
}
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
|
||||
c, _, _, _ := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, name, time.Time{}, time.Time{}, networks, nil, nil)
|
||||
|
||||
addrsTable := new(bart.Lite)
|
||||
for _, a := range addrs {
|
||||
addrsTable.Insert(netip.PrefixFrom(a, a.BitLen()))
|
||||
}
|
||||
|
||||
cs := &CertState{
|
||||
v2Cert: c,
|
||||
initiatingVersion: cert.Version2,
|
||||
myVpnAddrs: addrs,
|
||||
myVpnAddrsTable: addrsTable,
|
||||
}
|
||||
pki := &PKI{}
|
||||
pki.cs.Store(cs)
|
||||
return pki
|
||||
}
|
||||
|
||||
func TestDnsServer_seedSelf_addsOwnRecord(t *testing.T) {
|
||||
ds, c := newTestDnsServer(t)
|
||||
myV4 := netip.MustParseAddr("10.0.0.1")
|
||||
myV6 := netip.MustParseAddr("fd00::1")
|
||||
ds.pki = newTestPKI(t, "lighthouse", []netip.Addr{myV4, myV6})
|
||||
setDnsConfig(c, "127.0.0.1", "0", true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
ds.seedSelf()
|
||||
got4, exists := ds.Query(dns.TypeA, "lighthouse.")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, myV4, got4)
|
||||
got6, exists := ds.Query(dns.TypeAAAA, "lighthouse.")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, myV6, got6)
|
||||
}
|
||||
|
||||
func TestDnsServer_seedSelf_disabled_noOp(t *testing.T) {
|
||||
ds, c := newTestDnsServer(t)
|
||||
ds.pki = newTestPKI(t, "lighthouse", []netip.Addr{netip.MustParseAddr("10.0.0.1")})
|
||||
setDnsConfig(c, "127.0.0.1", "0", true, false)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
ds.seedSelf()
|
||||
_, exists := ds.Query(dns.TypeA, "lighthouse.")
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestDnsServer_clearRecords_dropsSelfHost(t *testing.T) {
|
||||
ds, c := newTestDnsServer(t)
|
||||
ds.pki = newTestPKI(t, "lighthouse", []netip.Addr{netip.MustParseAddr("10.0.0.1")})
|
||||
setDnsConfig(c, "127.0.0.1", "0", true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
ds.seedSelf()
|
||||
require.NotEmpty(t, ds.selfHost)
|
||||
|
||||
ds.clearRecords()
|
||||
assert.Empty(t, ds.selfHost)
|
||||
_, exists := ds.Query(dns.TypeA, "lighthouse.")
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestDnsServer_QueryCert_returnsOwnCert(t *testing.T) {
|
||||
ds, _ := newTestDnsServer(t)
|
||||
myV4 := netip.MustParseAddr("10.0.0.1")
|
||||
ds.pki = newTestPKI(t, "lighthouse", []netip.Addr{myV4})
|
||||
|
||||
got := ds.QueryCert(myV4.String() + ".")
|
||||
assert.NotEmpty(t, got, "TXT lookup of our own VPN address should return our cert")
|
||||
|
||||
other := netip.MustParseAddr("10.0.0.99")
|
||||
assert.Empty(t, ds.QueryCert(other.String()+"."), "unknown peer IP should return nothing")
|
||||
}
|
||||
|
||||
func TestDnsServer_reload_disable_stopsRunningServer(t *testing.T) {
|
||||
port := freeUDPPort(t)
|
||||
ds, c := newTestDnsServer(t)
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
FROM gcr.io/distroless/static:latest
|
||||
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
ARG VERSION=dev
|
||||
ARG REVISION=unknown
|
||||
LABEL org.opencontainers.image.title="nebula" \
|
||||
org.opencontainers.image.description="A scalable overlay networking tool with a focus on performance, simplicity and security" \
|
||||
org.opencontainers.image.vendor="Nebula OSS" \
|
||||
org.opencontainers.image.source="https://github.com/slackhq/nebula" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${REVISION}"
|
||||
|
||||
COPY build/$TARGETOS-$TARGETARCH/nebula /nebula
|
||||
COPY build/$TARGETOS-$TARGETARCH/nebula-cert /nebula-cert
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/e2e/router"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func assertTestRequestEchoed(t *testing.T, cipher string) {
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
over := m{"cipher": cipher}
|
||||
a, aNet, aUdp, _ := newSimpleServer(cert.Version1, ca, caKey, "a", "10.128.0.1/24", over)
|
||||
b, bNet, bUdp, _ := newSimpleServer(cert.Version1, ca, caKey, "b", "10.128.0.2/24", over)
|
||||
|
||||
a.InjectLightHouseAddr(bNet[0].Addr(), bUdp)
|
||||
b.InjectLightHouseAddr(aNet[0].Addr(), aUdp)
|
||||
a.Start()
|
||||
b.Start()
|
||||
t.Cleanup(func() { a.Stop(); b.Stop() })
|
||||
r := router.NewR(t, a, b)
|
||||
defer r.RenderFlow()
|
||||
|
||||
assertTunnel(t, aNet[0].Addr(), bNet[0].Addr(), a, b, r)
|
||||
drainUDPTx(a)
|
||||
drainUDPTx(b)
|
||||
|
||||
payload := []byte("a test payload well over sixteen bytes long, wow it's so very long long long!")
|
||||
require.Greater(t, len(payload), header.Len)
|
||||
a.GetF().SendMessageToVpnAddr(header.Test, header.TestRequest, bNet[0].Addr(), payload, make([]byte, 12, 12), make([]byte, udp.MTU))
|
||||
|
||||
// Deliver A's request to B; B must echo a reply back
|
||||
b.InjectUDPPacket(a.GetFromUDP(true))
|
||||
reply := nextUDPTxOfType(t, b, header.Test, header.TestReply, 2*time.Second)
|
||||
|
||||
assert.Equal(t, aUdp, reply.To, "the reply must go back to the requester")
|
||||
// header + echoed payload + 16-byte AEAD tag: proves the whole payload
|
||||
// round-tripped rather than being dropped or truncated.
|
||||
assert.Equal(t, header.Len+len(payload)+16, len(reply.Data), "the full payload must be echoed back")
|
||||
}
|
||||
|
||||
func TestTestRequestEchoesLongPayloadAES(t *testing.T) {
|
||||
assertTestRequestEchoed(t, "aes")
|
||||
}
|
||||
|
||||
func TestTestRequestEchoesLongPayloadChaChaPoly(t *testing.T) {
|
||||
assertTestRequestEchoed(t, "chachapoly")
|
||||
}
|
||||
|
||||
// drainUDPTx empties a control's UDP tx queue without blocking.
|
||||
func drainUDPTx(c *nebula.Control) {
|
||||
for c.GetFromUDP(false) != nil {
|
||||
}
|
||||
}
|
||||
|
||||
// nextUDPTxOfType returns the next packet a control transmits whose nebula
|
||||
// header matches (wantType, wantSub), skipping unrelated packets.
|
||||
// It fails the test if none arrives within the timeout.
|
||||
func nextUDPTxOfType(t *testing.T, c *nebula.Control, wantType header.MessageType, wantSub header.MessageSubType, within time.Duration) *udp.Packet {
|
||||
t.Helper()
|
||||
ch := c.GetUDPTxChan()
|
||||
timeout := time.After(within)
|
||||
for {
|
||||
select {
|
||||
case p := <-ch:
|
||||
var h header.H
|
||||
if err := h.Parse(p.Data); err == nil && h.Type == wantType && h.Subtype == wantSub {
|
||||
return p
|
||||
}
|
||||
case <-timeout:
|
||||
t.Fatalf("timed out waiting for a %v/%v packet on the udp tx queue", wantType, wantSub)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
-30
@@ -405,7 +405,7 @@ func TestStage1Race(t *testing.T) {
|
||||
|
||||
r.Log("Spin until connection manager tears down a tunnel")
|
||||
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -453,9 +453,11 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
||||
|
||||
r.Log("Nuke my hostmap")
|
||||
myHostmap := myControl.GetHostmap()
|
||||
myHostmap.Lock()
|
||||
myHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
||||
myHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
||||
myHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{}
|
||||
myHostmap.Unlock()
|
||||
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me again")))
|
||||
p = r.RouteForAllUntilTxTun(theirControl)
|
||||
@@ -465,10 +467,10 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
r.Log("Wait for the dead index to go away")
|
||||
start := len(theirControl.GetHostmap().Indexes)
|
||||
start := theirControl.GetHostmapIndexCount()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
if len(theirControl.GetHostmap().Indexes) < start {
|
||||
if theirControl.GetHostmapIndexCount() < start {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -504,9 +506,11 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
||||
|
||||
r.Log("Nuke my hostmap")
|
||||
theirHostmap := theirControl.GetHostmap()
|
||||
theirHostmap.Lock()
|
||||
theirHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
||||
theirHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
||||
theirHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{}
|
||||
theirHostmap.Unlock()
|
||||
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them again")))
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
@@ -517,10 +521,10 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
r.Log("Wait for the dead index to go away")
|
||||
start := len(myControl.GetHostmap().Indexes)
|
||||
start := myControl.GetHostmapIndexCount()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
if len(myControl.GetHostmap().Indexes) < start {
|
||||
if myControl.GetHostmapIndexCount() < start {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -628,10 +632,10 @@ func TestReestablishRelays(t *testing.T) {
|
||||
r.Log("Close the tunnel")
|
||||
relayControl.CloseTunnel(theirVpnIpNet[0].Addr(), true)
|
||||
|
||||
start := len(myControl.GetHostmap().Indexes)
|
||||
curIndexes := len(myControl.GetHostmap().Indexes)
|
||||
start := myControl.GetHostmapIndexCount()
|
||||
curIndexes := myControl.GetHostmapIndexCount()
|
||||
for curIndexes >= start {
|
||||
curIndexes = len(myControl.GetHostmap().Indexes)
|
||||
curIndexes = myControl.GetHostmapIndexCount()
|
||||
r.Logf("Wait for the dead index to go away:start=%v indexes, current=%v indexes", start, curIndexes)
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me should fail")))
|
||||
|
||||
@@ -819,18 +823,18 @@ func TestStage1RaceRelays2(t *testing.T) {
|
||||
|
||||
t.Log("Wait until we remove extra tunnels")
|
||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
||||
len(myControl.GetHostmap().Indexes),
|
||||
len(theirControl.GetHostmap().Indexes),
|
||||
len(relayControl.GetHostmap().Indexes),
|
||||
myControl.GetHostmapIndexCount(),
|
||||
theirControl.GetHostmapIndexCount(),
|
||||
relayControl.GetHostmapIndexCount(),
|
||||
)
|
||||
hostInfos := len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||
hostInfos := myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
||||
retries := 60
|
||||
for hostInfos > 6 && retries > 0 {
|
||||
hostInfos = len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||
hostInfos = myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
||||
len(myControl.GetHostmap().Indexes),
|
||||
len(theirControl.GetHostmap().Indexes),
|
||||
len(relayControl.GetHostmap().Indexes),
|
||||
myControl.GetHostmapIndexCount(),
|
||||
theirControl.GetHostmapIndexCount(),
|
||||
relayControl.GetHostmapIndexCount(),
|
||||
)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
@@ -924,24 +928,24 @@ func TestRehandshakingRelays(t *testing.T) {
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
||||
// We should have two hostinfos on all sides
|
||||
for len(myControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(myControl.GetHostmap().Indexes))
|
||||
for myControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("myControl hostinfos got cleaned up!")
|
||||
for len(theirControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(theirControl.GetHostmap().Indexes))
|
||||
for theirControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("theirControl hostinfos got cleaned up!")
|
||||
for len(relayControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(relayControl.GetHostmap().Indexes))
|
||||
for relayControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
@@ -1029,24 +1033,24 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
||||
// We should have two hostinfos on all sides
|
||||
for len(myControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(myControl.GetHostmap().Indexes))
|
||||
for myControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("myControl hostinfos got cleaned up!")
|
||||
for len(theirControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(theirControl.GetHostmap().Indexes))
|
||||
for theirControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
t.Logf("theirControl hostinfos got cleaned up!")
|
||||
for len(relayControl.GetHostmap().Indexes) != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", len(relayControl.GetHostmap().Indexes))
|
||||
for relayControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
@@ -1123,7 +1127,7 @@ func TestRehandshaking(t *testing.T) {
|
||||
theirConfig.ReloadConfigString(string(rc))
|
||||
|
||||
r.Log("Spin until there is only 1 tunnel")
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -1223,7 +1227,7 @@ func TestRehandshakingLoser(t *testing.T) {
|
||||
myConfig.ReloadConfigString(string(rc))
|
||||
|
||||
r.Log("Spin until there is only 1 tunnel")
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -1535,3 +1539,78 @@ func TestGoodHandshakeUnsafeDest(t *testing.T) {
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestMultiVpnAddrDeletePrimaryKeepsSecondAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Regression for the hostmap multi-vpnAddr delete bug. A dual-stack (v4+v6) V2-cert peer that
|
||||
// handshakes twice at once ends up with two hostinfos linked in the shared next/prev chain, with the
|
||||
// primary owning both addresses. Deleting that primary (e.g. connection manager dropping it, a
|
||||
// CloseTunnel, a collision) must promote the surviving sibling for EVERY address. The pre-fix code
|
||||
// unlinked the chain once per address, so it promoted the sibling for the first address and orphaned
|
||||
// the second: the peer stayed reachable at its v4 addr but not its v6 addr despite a live tunnel.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me ", "10.128.0.1/24,fd00::1/64", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.2/24,fd00::2/64", nil)
|
||||
|
||||
// This bug only exists for peers carrying more than one vpn address
|
||||
require.Len(t, theirVpnIpNet, 2)
|
||||
theirV4 := theirVpnIpNet[0].Addr()
|
||||
theirV6 := theirVpnIpNet[1].Addr()
|
||||
|
||||
// Put their info in our lighthouse and vice versa
|
||||
myControl.InjectLightHouseAddr(theirV4, theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
// Race a handshake so both of us build a hostinfo for the other, leaving my hostmap with a single
|
||||
// host (them) backed by two linked hostinfos, just like TestStage1Race.
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirV4, 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirV4, 80, []byte("Hi from them")))
|
||||
|
||||
myHsForThem := myControl.GetFromUDP(true)
|
||||
theirHsForMe := theirControl.GetFromUDP(true)
|
||||
|
||||
r.InjectUDPPacket(theirControl, myControl, theirHsForMe)
|
||||
r.InjectUDPPacket(myControl, theirControl, myHsForThem)
|
||||
|
||||
r.RouteForAllUntilTxTun(theirControl)
|
||||
r.RouteForAllUntilTxTun(myControl)
|
||||
|
||||
r.RenderHostmaps("Racing hostmaps", myControl, theirControl)
|
||||
|
||||
// Two hostinfos for them means the shared next/prev chain has a sibling to promote. The Hosts map has
|
||||
// one entry per vpn address (two, for dual stack), so the index count is what tells us there are two
|
||||
// hostinfos.
|
||||
require.Len(t, myControl.ListHostmapIndexes(false), 2)
|
||||
|
||||
// The primary owns both of their addresses
|
||||
primaryV4 := myControl.GetHostInfoByVpnAddr(theirV4, false)
|
||||
primaryV6 := myControl.GetHostInfoByVpnAddr(theirV6, false)
|
||||
require.NotNil(t, primaryV4)
|
||||
require.NotNil(t, primaryV6)
|
||||
require.Equal(t, primaryV4.LocalIndex, primaryV6.LocalIndex, "both addrs should point at the same primary")
|
||||
|
||||
// Delete the primary tunnel. localOnly so we don't perturb their side, we only care about my hostmap.
|
||||
require.True(t, myControl.CloseTunnel(theirV4, true))
|
||||
|
||||
// The surviving sibling must still serve BOTH addresses.
|
||||
survivorV4 := myControl.GetHostInfoByVpnAddr(theirV4, false)
|
||||
survivorV6 := myControl.GetHostInfoByVpnAddr(theirV6, false)
|
||||
require.NotNil(t, survivorV4, "v4 addr should still resolve to the surviving tunnel")
|
||||
// Pre-fix this is nil: the second address was orphaned when the primary was deleted.
|
||||
require.NotNil(t, survivorV6, "v6 addr was orphaned after deleting the primary (multi-vpnAddr delete bug)")
|
||||
assert.Equal(t, survivorV4.LocalIndex, survivorV6.LocalIndex, "both addrs should promote to the same survivor")
|
||||
assert.NotEqual(t, primaryV4.LocalIndex, survivorV4.LocalIndex, "a different hostinfo should now be primary")
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
+101
-6
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -42,8 +43,8 @@ func TestDropInactiveTunnels(t *testing.T) {
|
||||
r.Log("Go inactive and wait for the tunnels to get dropped")
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 && theirIndexes == 0 {
|
||||
break
|
||||
}
|
||||
@@ -373,6 +374,100 @@ func TestCrossStackRelaysWork(t *testing.T) {
|
||||
//relayControl.Stop()
|
||||
}
|
||||
|
||||
// TestRelayReplayProtection asserts that a relay (forwarding-type) node rejects
|
||||
// replayed relay frames. A captured relay frame, re-injected with the same
|
||||
// message counter, must be dropped by the replay window rather than re-forwarded
|
||||
// to the relay target. Before the fix, handleOutsideRelayPacket authenticated the
|
||||
// frame but never advanced the replay window, so every replay was re-forwarded.
|
||||
func TestRelayReplayProtection(t *testing.T) {
|
||||
t.Parallel()
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version2, ca, caKey, "me ", "10.128.0.1/24,fc00::1/64", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "relay ", "10.128.0.128/24,fc00::128/64", m{"relay": m{"am_relay": true}})
|
||||
theirUdp := netip.MustParseAddrPort("10.0.0.2:4242")
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServerWithUdp(cert.Version2, ca, caKey, "them ", "fc00::2/64", theirUdp, m{"relay": m{"use_relays": true}})
|
||||
|
||||
myVpnV6 := myVpnIpNet[1]
|
||||
relayVpnV4 := relayVpnIpNet[0]
|
||||
relayVpnV6 := relayVpnIpNet[1]
|
||||
theirVpnV6 := theirVpnIpNet[0]
|
||||
|
||||
// Teach me how to reach the relay and that them is reachable via the relay
|
||||
myControl.InjectLightHouseAddr(relayVpnV4.Addr(), relayUdpAddr)
|
||||
myControl.InjectLightHouseAddr(relayVpnV6.Addr(), relayUdpAddr)
|
||||
myControl.InjectRelays(theirVpnV6.Addr(), []netip.Addr{relayVpnV6.Addr()})
|
||||
relayControl.InjectLightHouseAddr(theirVpnV6.Addr(), theirUdpAddr)
|
||||
|
||||
r := router.NewR(t, myControl, relayControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
myControl.Start()
|
||||
relayControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
// Establish the relayed tunnel in both directions so all handshakes complete.
|
||||
t.Log("Establish the relayed tunnel")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("Hi from me")))
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnV6.Addr(), theirVpnV6.Addr(), 80, 80)
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnV6.Addr(), 80, theirVpnV6.Addr(), 80, []byte("Hi from them")))
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
assertUdpPacket(t, []byte("Hi from them"), p, theirVpnV6.Addr(), myVpnV6.Addr(), 80, 80)
|
||||
|
||||
// Drain anything still queued on me's UDP tx so the next packet we pull is the
|
||||
// relay frame we are about to generate.
|
||||
for myControl.GetFromUDP(false) != nil {
|
||||
}
|
||||
|
||||
// Capture a single legitimate relay frame that me transmits toward the relay.
|
||||
t.Log("Capture a relay frame from me -> relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("replay me")))
|
||||
relayFrame := myControl.GetFromUDP(true)
|
||||
require.Equal(t, relayUdpAddr, relayFrame.To, "captured frame should be addressed to the relay")
|
||||
var fh header.H
|
||||
require.NoError(t, fh.Parse(relayFrame.Data))
|
||||
require.Equal(t, header.Message, fh.Type)
|
||||
require.Equal(t, header.MessageRelay, fh.Subtype)
|
||||
|
||||
// drainForwards counts relay frames the relay forwards toward them within the
|
||||
// settle window. We match on destination + (Message, MessageRelay) so the
|
||||
// relay's own direct traffic to them can't be miscounted.
|
||||
drainForwards := func(settle time.Duration) int {
|
||||
ch := relayControl.GetUDPTxChan()
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case pkt := <-ch:
|
||||
var ph header.H
|
||||
if pkt.To == theirUdpAddr && ph.Parse(pkt.Data) == nil &&
|
||||
ph.Type == header.Message && ph.Subtype == header.MessageRelay {
|
||||
count++
|
||||
}
|
||||
pkt.Release()
|
||||
case <-time.After(settle):
|
||||
return count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First delivery of the captured frame: the relay should forward it once.
|
||||
t.Log("Deliver the captured frame once; relay forwards it to them")
|
||||
relayControl.InjectUDPPacket(relayFrame)
|
||||
require.Equal(t, 1, drainForwards(200*time.Millisecond), "relay should forward the first, legitimate copy")
|
||||
|
||||
// Replay the exact same frame several times. A correct replay window rejects
|
||||
// these duplicates so the relay forwards none of them.
|
||||
t.Log("Replay the captured frame; relay must drop the duplicates")
|
||||
const replays = 3
|
||||
for i := 0; i < replays; i++ {
|
||||
relayControl.InjectUDPPacket(relayFrame)
|
||||
}
|
||||
forwarded := drainForwards(200 * time.Millisecond)
|
||||
assert.Equal(t, 0, forwarded, "relay re-forwarded %d/%d replayed relay frames; replay protection is ineffective on relay tunnels", forwarded, replays)
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
|
||||
}
|
||||
|
||||
func TestCloseTunnelAuthenticated(t *testing.T) {
|
||||
t.Parallel()
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
@@ -398,8 +493,8 @@ func TestCloseTunnelAuthenticated(t *testing.T) {
|
||||
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 && theirIndexes == 0 {
|
||||
break
|
||||
}
|
||||
@@ -453,8 +548,8 @@ func TestCloseTunnelAuthenticated(t *testing.T) {
|
||||
r.Log("Injected bogus close tunnel. Let's see!")
|
||||
waitStart = time.Now()
|
||||
for {
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 {
|
||||
t.Fatal("myIndexes should not be 0")
|
||||
}
|
||||
|
||||
+15
-1
@@ -254,6 +254,20 @@ tun:
|
||||
# Default MTU for every packet, safe setting is (and the default) 1300 for internet based traffic
|
||||
mtu: 1300
|
||||
|
||||
# Linux only. pin_threads pins each tun reader/encrypt OS thread to a single CPU. This keeps every goroutine's
|
||||
# sends flowing through one XPS-selected NIC TX ring, so packets within a flow stay ordered on the wire
|
||||
# instead of being sprayed across multiple TX rings and reordered. Not reloadable.
|
||||
#pin_threads: true
|
||||
|
||||
# Linux only. cpu_affinity overrides which CPUs the tun reader threads pin to: a list of CPU IDs, one per routine
|
||||
# (see the top-level `routines` setting). Lists shorter than `routines` are modulo-cycled across the queues; extra
|
||||
# entries are ignored. IDs must be within the process's allowed CPU set, so this respects taskset / cgroup cpusets;
|
||||
# a non-integer or not-allowed entry disables the override and falls back to spreading queues across the allowed
|
||||
# CPUs. Only meaningful while pin_threads is true. Not reloadable.
|
||||
#cpu_affinity:
|
||||
# - 2
|
||||
# - 4
|
||||
|
||||
# Route based MTU overrides, you have known vpn ip paths that can support larger MTUs you can increase/decrease them here
|
||||
routes:
|
||||
#- mtu: 8800
|
||||
@@ -397,7 +411,7 @@ firewall:
|
||||
# `drop` (default): silently drop the packet.
|
||||
# `reject`: send a reject reply.
|
||||
# - For TCP, this will be a RST "Connection Reset" packet.
|
||||
# - For other protocols, this will be an ICMP port unreachable packet.
|
||||
# - For other protocols, this will be an ICMP "Destination unreachable: Communication administratively prohibited" packet.
|
||||
outbound_action: drop
|
||||
inbound_action: drop
|
||||
|
||||
|
||||
+56
-39
@@ -44,8 +44,8 @@ type Firewall struct {
|
||||
InRules *FirewallTable
|
||||
OutRules *FirewallTable
|
||||
|
||||
InSendReject bool
|
||||
OutSendReject bool
|
||||
InboundSendReject bool
|
||||
OutboundSendReject bool
|
||||
|
||||
//TODO: we should have many more options for TCP, an option for ICMP, and mimic the kernel a bit better
|
||||
// https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt
|
||||
@@ -58,8 +58,9 @@ type Firewall struct {
|
||||
routableNetworks *bart.Lite
|
||||
|
||||
// assignedNetworks is a list of vpn networks assigned to us in the certificate.
|
||||
assignedNetworks []netip.Prefix
|
||||
hasUnsafeNetworks bool
|
||||
assignedNetworks []netip.Prefix
|
||||
// unsafeNetworks is the list of unsafe networks issued to us in the certificate
|
||||
unsafeNetworks []netip.Prefix
|
||||
|
||||
rules string
|
||||
rulesVersion uint16
|
||||
@@ -158,10 +159,9 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
|
||||
assignedNetworks = append(assignedNetworks, network)
|
||||
}
|
||||
|
||||
hasUnsafeNetworks := false
|
||||
for _, n := range c.UnsafeNetworks() {
|
||||
unsafeNetworks := c.UnsafeNetworks()
|
||||
for _, n := range unsafeNetworks {
|
||||
routableNetworks.Insert(n)
|
||||
hasUnsafeNetworks = true
|
||||
}
|
||||
|
||||
return &Firewall{
|
||||
@@ -169,15 +169,15 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
|
||||
Conns: make(map[firewall.Packet]*conn),
|
||||
TimerWheel: NewTimerWheel[firewall.Packet](tmin, tmax),
|
||||
},
|
||||
InRules: newFirewallTable(),
|
||||
OutRules: newFirewallTable(),
|
||||
TCPTimeout: tcpTimeout,
|
||||
UDPTimeout: UDPTimeout,
|
||||
DefaultTimeout: defaultTimeout,
|
||||
routableNetworks: routableNetworks,
|
||||
assignedNetworks: assignedNetworks,
|
||||
hasUnsafeNetworks: hasUnsafeNetworks,
|
||||
l: l,
|
||||
InRules: newFirewallTable(),
|
||||
OutRules: newFirewallTable(),
|
||||
TCPTimeout: tcpTimeout,
|
||||
UDPTimeout: UDPTimeout,
|
||||
DefaultTimeout: defaultTimeout,
|
||||
routableNetworks: routableNetworks,
|
||||
assignedNetworks: assignedNetworks,
|
||||
unsafeNetworks: unsafeNetworks,
|
||||
l: l,
|
||||
|
||||
incomingMetrics: firewallMetrics{
|
||||
droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_addr", nil),
|
||||
@@ -216,23 +216,23 @@ func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewal
|
||||
inboundAction := c.GetString("firewall.inbound_action", "drop")
|
||||
switch inboundAction {
|
||||
case "reject":
|
||||
fw.InSendReject = true
|
||||
fw.InboundSendReject = true
|
||||
case "drop":
|
||||
fw.InSendReject = false
|
||||
fw.InboundSendReject = false
|
||||
default:
|
||||
l.Warn("invalid firewall.inbound_action, defaulting to `drop`", "action", inboundAction)
|
||||
fw.InSendReject = false
|
||||
fw.InboundSendReject = false
|
||||
}
|
||||
|
||||
outboundAction := c.GetString("firewall.outbound_action", "drop")
|
||||
switch outboundAction {
|
||||
case "reject":
|
||||
fw.OutSendReject = true
|
||||
fw.OutboundSendReject = true
|
||||
case "drop":
|
||||
fw.OutSendReject = false
|
||||
fw.OutboundSendReject = false
|
||||
default:
|
||||
l.Warn("invalid firewall.outbound_action, defaulting to `drop`", "action", outboundAction)
|
||||
fw.OutSendReject = false
|
||||
fw.OutboundSendReject = false
|
||||
}
|
||||
|
||||
err := AddFirewallRulesFromConfig(l, false, c, fw)
|
||||
@@ -423,11 +423,6 @@ var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
|
||||
// Drop returns an error if the packet should be dropped, explaining why. It
|
||||
// returns nil if the packet should not be dropped.
|
||||
func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
|
||||
// Check if we spoke to this tuple, if we did then allow this packet
|
||||
if f.inConns(fp, h, caPool, localCache) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure remote address matches nebula certificate, and determine how to treat it
|
||||
if h.networks == nil {
|
||||
// Simple case: Certificate has one address and no unsafe networks
|
||||
@@ -461,6 +456,11 @@ func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *
|
||||
return ErrInvalidLocalIP
|
||||
}
|
||||
|
||||
// Check if we spoke to this tuple, if we did then allow this packet
|
||||
if f.inConns(fp, h, caPool, localCache) {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := f.OutRules
|
||||
if incoming {
|
||||
table = f.InRules
|
||||
@@ -897,7 +897,7 @@ func (flc *firewallLocalCIDR) addRule(f *Firewall, localCidr string) error {
|
||||
}
|
||||
|
||||
if localCidr == "" {
|
||||
if !f.hasUnsafeNetworks || f.defaultLocalCIDRAny {
|
||||
if len(f.unsafeNetworks) == 0 || f.defaultLocalCIDRAny {
|
||||
flc.Any = true
|
||||
return nil
|
||||
}
|
||||
@@ -1055,7 +1055,6 @@ func (r *rule) sanity() error {
|
||||
}
|
||||
|
||||
func parsePort(s string) (int32, int32, error) {
|
||||
var err error
|
||||
const notAPort int32 = -2
|
||||
if s == "any" {
|
||||
return firewall.PortAny, firewall.PortAny, nil
|
||||
@@ -1064,11 +1063,11 @@ func parsePort(s string) (int32, int32, error) {
|
||||
return firewall.PortFragment, firewall.PortFragment, nil
|
||||
}
|
||||
if !strings.Contains(s, `-`) {
|
||||
rPort, err := strconv.Atoi(s)
|
||||
rPort, err := parsePortValue("", s)
|
||||
if err != nil {
|
||||
return notAPort, notAPort, fmt.Errorf("was not a number; `%s`", s)
|
||||
return notAPort, notAPort, err
|
||||
}
|
||||
return int32(rPort), int32(rPort), nil
|
||||
return rPort, rPort, nil
|
||||
}
|
||||
|
||||
sPorts := strings.SplitN(s, `-`, 2)
|
||||
@@ -1079,22 +1078,40 @@ func parsePort(s string) (int32, int32, error) {
|
||||
return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
|
||||
}
|
||||
|
||||
rStartPort, err := strconv.Atoi(sPorts[0])
|
||||
startPort, err := parsePortValue("beginning range ", sPorts[0])
|
||||
if err != nil {
|
||||
return notAPort, notAPort, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
|
||||
return notAPort, notAPort, err
|
||||
}
|
||||
|
||||
rEndPort, err := strconv.Atoi(sPorts[1])
|
||||
endPort, err := parsePortValue("ending range ", sPorts[1])
|
||||
if err != nil {
|
||||
return notAPort, notAPort, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
|
||||
return notAPort, notAPort, err
|
||||
}
|
||||
|
||||
startPort := int32(rStartPort)
|
||||
endPort := int32(rEndPort)
|
||||
|
||||
if startPort == firewall.PortAny {
|
||||
endPort = firewall.PortAny
|
||||
}
|
||||
|
||||
return startPort, endPort, nil
|
||||
}
|
||||
|
||||
// parsePortValue accepts a base-10 decimal in [0, 65535] and returns it
|
||||
// widened to int32. Using strconv.ParseUint with bitSize 16 rejects
|
||||
// negative input, out-of-range input (>65535), and any non-decimal byte
|
||||
// by construction, so the int32 widening that follows is provably safe
|
||||
// and cannot collide with firewall.PortAny (0) or firewall.PortFragment
|
||||
// (-1) via integer truncation.
|
||||
//
|
||||
// prefix is prepended to both error messages so callers can disambiguate
|
||||
// the single-port path (prefix="") from the range bounds (prefix="beginning
|
||||
// range " / "ending range "), preserving the historical error strings.
|
||||
func parsePortValue(prefix, s string) (int32, error) {
|
||||
n, err := strconv.ParseUint(s, 10, 16)
|
||||
if err == nil {
|
||||
return int32(n), nil
|
||||
}
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return 0, fmt.Errorf("%sout of range [0,65535]; `%s`", prefix, s)
|
||||
}
|
||||
return 0, fmt.Errorf("%swas not a number; `%s`", prefix, s)
|
||||
}
|
||||
|
||||
@@ -916,6 +916,159 @@ func TestFirewall_DropIPSpoofing(t *testing.T) {
|
||||
assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP)
|
||||
}
|
||||
|
||||
func TestFirewall_ConntrackSourceSpoofingAcrossPeers(t *testing.T) {
|
||||
l := test.NewLoggerWithOutput(&bytes.Buffer{})
|
||||
|
||||
myVpnNetworksTable := new(bart.Lite)
|
||||
myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
|
||||
|
||||
owner := &dummyCert{
|
||||
name: "owner",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
|
||||
}
|
||||
|
||||
victim := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "victim",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
|
||||
},
|
||||
}
|
||||
victimHI := HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: victim},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
victimHI.buildNetworks(myVpnNetworksTable, victim.Certificate)
|
||||
|
||||
attacker := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "attacker",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.3/24")},
|
||||
},
|
||||
}
|
||||
attackerHI := HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: attacker},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.3")},
|
||||
}
|
||||
attackerHI.buildNetworks(myVpnNetworksTable, attacker.Certificate)
|
||||
|
||||
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
|
||||
// Allow any inbound traffic that passes the cert / source-IP checks.
|
||||
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
|
||||
cp := cert.NewCAPool()
|
||||
|
||||
flow := firewall.Packet{
|
||||
LocalAddr: netip.MustParseAddr("192.0.2.1"),
|
||||
RemoteAddr: netip.MustParseAddr("192.0.2.2"),
|
||||
LocalPort: 443,
|
||||
RemotePort: 55000,
|
||||
Protocol: firewall.ProtoUDP,
|
||||
}
|
||||
|
||||
require.NoError(t, fw.Drop(flow, true, &victimHI, cp, nil),
|
||||
"victim's own traffic from its own overlay IP must be allowed")
|
||||
|
||||
unseen := flow
|
||||
unseen.RemotePort = 55001
|
||||
assert.Equal(t, ErrInvalidRemoteIP, fw.Drop(unseen, true, &attackerHI, cp, nil),
|
||||
"sanity: attacker forging victim's source IP must be rejected when no conntrack entry exists")
|
||||
|
||||
got := fw.Drop(flow, true, &attackerHI, cp, nil)
|
||||
t.Logf("attacker replaying victim's 4-tuple: Drop returned %v (nil == packet ALLOWED == spoof succeeded)", got)
|
||||
assert.Equal(t, ErrInvalidRemoteIP, got,
|
||||
"SECURITY: attacker spoofed victim's overlay source IP (192.0.2.2) by reusing an existing conntrack 4-tuple; Drop returned %v instead of rejecting", got)
|
||||
}
|
||||
|
||||
// BenchmarkFirewallDropConntrackHit measures Drop on an already-established flow
|
||||
// (a conntrack hit). This is the fast path that the source-IP<->cert binding
|
||||
// reordering adds work to, so it quantifies the cost of moving the address checks
|
||||
// ahead of the conntrack lookup. Cases:
|
||||
// - simple: peer cert has one address, no unsafe networks (h.networks == nil),
|
||||
// so the remote-address check is a single netip.Addr compare.
|
||||
// - complex: peer cert has unsafe networks (h.networks populated), so the
|
||||
// remote-address check is a BART lookup.
|
||||
// - noCache/localCache: whether a per-batch ConntrackCache is supplied, which in
|
||||
// the original code let the fast path skip straight past the address checks.
|
||||
func BenchmarkFirewallDropConntrackHit(b *testing.B) {
|
||||
l := test.NewLoggerWithOutput(&bytes.Buffer{})
|
||||
|
||||
myVpnNetworksTable := new(bart.Lite)
|
||||
myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
|
||||
|
||||
owner := &dummyCert{
|
||||
name: "owner",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/24")},
|
||||
}
|
||||
|
||||
simpleCert := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "simple",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
|
||||
},
|
||||
}
|
||||
simpleHost := &HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: simpleCert},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
simpleHost.buildNetworks(myVpnNetworksTable, simpleCert.Certificate)
|
||||
|
||||
complexCert := &cert.CachedCertificate{
|
||||
Certificate: &dummyCert{
|
||||
name: "complex",
|
||||
networks: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/24")},
|
||||
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")},
|
||||
},
|
||||
}
|
||||
complexHost := &HostInfo{
|
||||
ConnectionState: &ConnectionState{peerCert: complexCert},
|
||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("192.0.2.2")},
|
||||
}
|
||||
complexHost.buildNetworks(myVpnNetworksTable, complexCert.Certificate)
|
||||
|
||||
cp := cert.NewCAPool()
|
||||
|
||||
flow := firewall.Packet{
|
||||
LocalAddr: netip.MustParseAddr("192.0.2.1"),
|
||||
RemoteAddr: netip.MustParseAddr("192.0.2.2"),
|
||||
LocalPort: 443,
|
||||
RemotePort: 55000,
|
||||
Protocol: firewall.ProtoUDP,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
host *HostInfo
|
||||
useCache bool
|
||||
}{
|
||||
{"simple/noCache", simpleHost, false},
|
||||
{"simple/localCache", simpleHost, true},
|
||||
{"complex/noCache", complexHost, false},
|
||||
{"complex/localCache", complexHost, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, owner)
|
||||
require.NoError(b, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
|
||||
|
||||
// Establish the conntrack entry so every benchmarked Drop is a hit.
|
||||
require.NoError(b, fw.Drop(flow, true, tc.host, cp, nil))
|
||||
|
||||
var cache firewall.ConntrackCache
|
||||
if tc.useCache {
|
||||
cache = firewall.ConntrackCache{}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := fw.Drop(flow, true, tc.host, cp, cache); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLookup(b *testing.B) {
|
||||
ml := func(m map[string]struct{}, a [][]string) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
@@ -1029,6 +1182,75 @@ func Test_parsePort(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test_parsePort_invalid covers inputs that must error. The named bug is
|
||||
// that int32(strconv.Atoi("4294967296")) truncates to 0 == firewall.PortAny,
|
||||
// silently turning a typo into a match-all-ports rule; the rest are
|
||||
// representative syntax/range probes.
|
||||
func Test_parsePort_invalid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErrContains string
|
||||
}{
|
||||
// Numeric overflow (the named bug + boundary).
|
||||
{"named bug: 2^32 truncates to PortAny", "4294967296", "out of range"},
|
||||
{"just above max real port", "65536", "out of range"},
|
||||
|
||||
// Negatives route through the range branch and hit the empty-half
|
||||
// guard; included as defense in depth so a future refactor cannot
|
||||
// accidentally reach the int32 cast.
|
||||
{"negative", "-1", "could not be parsed"},
|
||||
|
||||
// Syntax probes.
|
||||
{"NUL between digits", "4\x002", "was not a number"},
|
||||
{"hex notation", "0x10", "was not a number"},
|
||||
{"scientific notation", "1e3", "was not a number"},
|
||||
{"leading whitespace", " 42", "was not a number"},
|
||||
{"fullwidth digits", "42", "was not a number"},
|
||||
|
||||
// Range branch.
|
||||
{"range upper out of range", "1-65536", "ending range out of range"},
|
||||
{"range lower out of range", "65536-65537", "beginning range out of range"},
|
||||
{"range with negative upper", "1--1", "ending range was not a number"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, err := parsePort(tc.input)
|
||||
require.Error(t, err, "input %q must error", tc.input)
|
||||
require.ErrorContains(t, err, tc.wantErrContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_parsePort_valid_boundaries locks in success cases at 0, 1, and 65535
|
||||
// so a future refactor cannot regress the boundaries.
|
||||
func Test_parsePort_valid_boundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantStart int32
|
||||
wantEnd int32
|
||||
}{
|
||||
{"zero is PortAny", "0", 0, 0},
|
||||
{"min real port", "1", 1, 1},
|
||||
{"max real port", "65535", 65535, 65535},
|
||||
{"range zero to max forces end to zero", "0-65535", 0, 0},
|
||||
{"range max to max", "65535-65535", 65535, 65535},
|
||||
{"range one to max", "1-65535", 1, 65535},
|
||||
{"range with whitespace inside", " 1 - 2 ", 1, 2},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, e, err := parsePort(tc.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantStart, s, "start port")
|
||||
assert.Equal(t, tc.wantEnd, e, "end port")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFirewallFromConfig(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
// Test a bad rule definition
|
||||
|
||||
@@ -9,10 +9,10 @@ require (
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432
|
||||
github.com/flynn/noise v1.1.0
|
||||
github.com/gaissmai/bart v0.26.1
|
||||
github.com/gaissmai/bart v0.28.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/kardianos/service v1.2.4
|
||||
github.com/kardianos/service v1.3.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/miekg/pkcs11 v1.1.2
|
||||
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f
|
||||
@@ -24,15 +24,15 @@ require (
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/term v0.42.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/term v0.44.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
)
|
||||
|
||||
@@ -26,8 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/gaissmai/bart v0.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko=
|
||||
github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -66,8 +66,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
|
||||
github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/kardianos/service v1.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI=
|
||||
github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
@@ -162,16 +162,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -182,8 +182,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -191,8 +191,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -208,11 +208,11 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -223,8 +223,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -233,8 +233,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1 h1:XMaKojH1Hs/raMrmnir4n35nTvzvWj7NmSYzHn2F4qU=
|
||||
golang.zx2c4.com/wireguard/windows v0.6.1/go.mod h1:04aqInu5GYuTFvMuDw/rKBAF7mHrltW/3rekpfbbZDM=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
|
||||
@@ -13,6 +13,7 @@ var (
|
||||
ErrUnknownSubtype = errors.New("unknown handshake subtype")
|
||||
ErrMissingContent = errors.New("expected handshake content but message was empty")
|
||||
ErrUnexpectedContent = errors.New("received unexpected handshake content")
|
||||
ErrInvalidRemoteIndex = errors.New("peer sent an invalid index in handshake payload")
|
||||
ErrIndexAllocation = errors.New("failed to allocate local index")
|
||||
ErrNoCredential = errors.New("no handshake credential available for cert version")
|
||||
ErrAsymmetricCipherKeys = errors.New("noise produced only one cipher key")
|
||||
|
||||
+10
-2
@@ -312,11 +312,19 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
|
||||
|
||||
// Process payload
|
||||
if flags.expectsPayload {
|
||||
var remoteIndex uint32
|
||||
if m.result.Initiator {
|
||||
m.result.RemoteIndex = payload.ResponderIndex
|
||||
remoteIndex = payload.ResponderIndex
|
||||
} else {
|
||||
m.result.RemoteIndex = payload.InitiatorIndex
|
||||
remoteIndex = payload.InitiatorIndex
|
||||
}
|
||||
// The payload presence check above can be satisfied by Time alone, so a payload
|
||||
// could still carry a zero index here. We need to reject it.
|
||||
if remoteIndex == 0 {
|
||||
m.failed = true
|
||||
return ErrInvalidRemoteIndex
|
||||
}
|
||||
m.result.RemoteIndex = remoteIndex
|
||||
m.result.HandshakeTime = payload.Time
|
||||
m.payloadSet = true
|
||||
}
|
||||
|
||||
@@ -229,6 +229,24 @@ func TestMachineProcessPayload(t *testing.T) {
|
||||
require.ErrorIs(t, err, ErrUnexpectedContent)
|
||||
assert.True(t, m.Failed())
|
||||
})
|
||||
|
||||
t.Run("zero initiator index on responder is fatal", func(t *testing.T) {
|
||||
m := newTestMachine(t, cs, v, false, 100)
|
||||
bytes := MarshalPayload(nil, Payload{InitiatorIndex: 0, Time: 1})
|
||||
err := m.processPayload(bytes, msgFlags{expectsPayload: true})
|
||||
require.ErrorIs(t, err, ErrInvalidRemoteIndex)
|
||||
assert.True(t, m.Failed())
|
||||
assert.Zero(t, m.result.RemoteIndex)
|
||||
})
|
||||
|
||||
t.Run("zero responder index on initiator is fatal", func(t *testing.T) {
|
||||
m := newTestMachine(t, cs, v, true, 100)
|
||||
bytes := MarshalPayload(nil, Payload{InitiatorIndex: 100, ResponderIndex: 0, Time: 1})
|
||||
err := m.processPayload(bytes, msgFlags{expectsPayload: true})
|
||||
require.ErrorIs(t, err, ErrInvalidRemoteIndex)
|
||||
assert.True(t, m.Failed())
|
||||
assert.Zero(t, m.result.RemoteIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMachineRequireComplete checks the fail-on-incomplete-handshake path
|
||||
|
||||
+4
-11
@@ -83,6 +83,7 @@ type HandshakeHostInfo struct {
|
||||
initiatingVersionOverride cert.Version // Should we use a non-default cert version for this handshake?
|
||||
counter int64 // How many attempts have we made so far
|
||||
lastRemotes []netip.AddrPort // Remotes that we sent to during the previous attempt
|
||||
lastRelays []netip.Addr // Relays we attempted to use during the previous attempt
|
||||
packetStore []*cachedPacket // A set of packets to be transmitted once the handshake completes
|
||||
|
||||
hostinfo *HostInfo
|
||||
@@ -217,7 +218,6 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
|
||||
fields := []any{
|
||||
"udpAddrs", hh.hostinfo.remotes.CopyAddrs(hm.mainHostMap.GetPreferredRanges()),
|
||||
"initiatorIndex", hh.hostinfo.localIndexId,
|
||||
"remoteIndex", hh.hostinfo.remoteIndexId,
|
||||
"durationNs", time.Since(hh.startTime).Nanoseconds(),
|
||||
}
|
||||
// hh.machine can be nil here if buildStage0Packet never succeeded
|
||||
@@ -323,7 +323,7 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
|
||||
)
|
||||
}
|
||||
|
||||
hm.f.relayManager.StartRelays(hm.f, vpnIp, hostinfo, stage0)
|
||||
hm.f.relayManager.StartRelays(hm.f, vpnIp, hh, stage0)
|
||||
|
||||
// If a lighthouse triggered this attempt then we are still in the timer wheel and do not need to re-add
|
||||
if !lighthouseTriggered {
|
||||
@@ -430,14 +430,11 @@ func (hm *HandshakeManager) CheckAndComplete(hostinfo *HostInfo, handshakePacket
|
||||
// Check if we already have a tunnel with this vpn ip
|
||||
existingHostInfo, found := hm.mainHostMap.Hosts[hostinfo.vpnAddrs[0]]
|
||||
if found && existingHostInfo != nil {
|
||||
testHostInfo := existingHostInfo
|
||||
for testHostInfo != nil {
|
||||
// Is it just a delayed handshake packet?
|
||||
// Is it just a delayed handshake packet? Check every hostinfo we hold for this address.
|
||||
for _, testHostInfo := range hm.mainHostMap.unlockedGetHostList(hostinfo.vpnAddrs[0]) {
|
||||
if bytes.Equal(hostinfo.HandshakePacket[handshakePacket], testHostInfo.HandshakePacket[handshakePacket]) {
|
||||
return testHostInfo, ErrAlreadySeen
|
||||
}
|
||||
|
||||
testHostInfo = testHostInfo.next
|
||||
}
|
||||
|
||||
// Is this a newer handshake?
|
||||
@@ -465,7 +462,6 @@ func (hm *HandshakeManager) CheckAndComplete(hostinfo *HostInfo, handshakePacket
|
||||
// We have a collision, but this can happen since we can't control
|
||||
// the remote ID. Just log about the situation as a note.
|
||||
hostinfo.logger(hm.l).Info("New host shadows existing host remoteIndex",
|
||||
"remoteIndex", hostinfo.remoteIndexId,
|
||||
"collision", existingRemoteIndex.vpnAddrs,
|
||||
)
|
||||
}
|
||||
@@ -488,7 +484,6 @@ func (hm *HandshakeManager) Complete(hostinfo *HostInfo, f *Interface) {
|
||||
// We have a collision, but this can happen since we can't control
|
||||
// the remote ID. Just log about the situation as a note.
|
||||
hostinfo.logger(hm.l).Info("New host shadows existing host remoteIndex",
|
||||
"remoteIndex", hostinfo.remoteIndexId,
|
||||
"collision", existingRemoteIndex.vpnAddrs,
|
||||
)
|
||||
}
|
||||
@@ -798,7 +793,6 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
|
||||
}
|
||||
|
||||
hm.sendHandshakeResponse(via, response, hostinfo, false)
|
||||
f.connectionManager.AddTrafficWatch(hostinfo)
|
||||
hostinfo.remotes.RefreshFromHandshake(vpnAddrs)
|
||||
|
||||
// Don't wait for UpdateWorker
|
||||
@@ -965,7 +959,6 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
|
||||
hostinfo.buildNetworks(f.myVpnNetworksTable, remoteCert.Certificate)
|
||||
|
||||
hm.Complete(hostinfo, f)
|
||||
f.connectionManager.AddTrafficWatch(hostinfo)
|
||||
|
||||
if len(hh.packetStore) > 0 {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
|
||||
+171
-118
@@ -56,11 +56,20 @@ type Relay struct {
|
||||
}
|
||||
|
||||
type HostMap struct {
|
||||
sync.RWMutex //Because we concurrently read and write to our maps
|
||||
Indexes map[uint32]*HostInfo
|
||||
Relays map[uint32]*HostInfo // Maps a Relay IDX to a Relay HostInfo object
|
||||
RemoteIndexes map[uint32]*HostInfo
|
||||
sync.RWMutex //Because we concurrently read and write to our maps
|
||||
Indexes map[uint32]*HostInfo
|
||||
Relays map[uint32]*HostInfo // Maps a Relay IDX to a Relay HostInfo object
|
||||
RemoteIndexes map[uint32]*HostInfo
|
||||
// Hosts maps a vpn address to its primary hostinfo, one entry per address we hold a tunnel
|
||||
// for. moreHosts only has an entry while an address is held by 2 or more hostinfos and stores
|
||||
// the full most-recent-first list; moreHosts[a][0] is always the same hostinfo as Hosts[a].
|
||||
// Each address gets its own independent list, so a hostinfo owning multiple addresses can
|
||||
// never corrupt another address's ordering the way the old shared next/prev chain could.
|
||||
// Entries in moreHosts are only ever written by unlockedSetHostsForAddr; Hosts is written
|
||||
// directly only in the single-hostinfo fast paths where moreHosts is known to have no entry,
|
||||
// and unlockedDeleteHostInfo swaps either map for a fresh one when it fully drains.
|
||||
Hosts map[netip.Addr]*HostInfo
|
||||
moreHosts map[netip.Addr][]*HostInfo
|
||||
preferredRanges atomic.Pointer[[]netip.Prefix]
|
||||
l *slog.Logger
|
||||
}
|
||||
@@ -138,9 +147,9 @@ func (rs *RelayState) InsertRelayTo(ip netip.Addr) {
|
||||
}
|
||||
|
||||
func (rs *RelayState) CopyRelayIps() []netip.Addr {
|
||||
ret := make([]netip.Addr, len(rs.relays))
|
||||
rs.RLock()
|
||||
defer rs.RUnlock()
|
||||
ret := make([]netip.Addr, len(rs.relays))
|
||||
copy(ret, rs.relays)
|
||||
return ret
|
||||
}
|
||||
@@ -229,7 +238,7 @@ const (
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
remote netip.AddrPort
|
||||
remote atomic.Pointer[netip.AddrPort]
|
||||
remotes *RemoteList
|
||||
promoteCounter atomic.Uint32
|
||||
ConnectionState *ConnectionState
|
||||
@@ -266,10 +275,6 @@ type HostInfo struct {
|
||||
lastRoam time.Time
|
||||
lastRoamRemote netip.AddrPort
|
||||
|
||||
// Used to track other hostinfos for this vpn ip since only 1 can be primary
|
||||
// Synchronised via hostmap lock and not the hostinfo lock.
|
||||
next, prev *HostInfo
|
||||
|
||||
//TODO: in, out, and others might benefit from being an atomic.Int32. We could collapse connectionManager pendingDeletion, relayUsed, and in/out into this 1 thing
|
||||
in, out, pendingDeletion atomic.Bool
|
||||
|
||||
@@ -334,6 +339,7 @@ func newHostMap(l *slog.Logger) *HostMap {
|
||||
Relays: map[uint32]*HostInfo{},
|
||||
RemoteIndexes: map[uint32]*HostInfo{},
|
||||
Hosts: map[netip.Addr]*HostInfo{},
|
||||
moreHosts: map[netip.Addr][]*HostInfo{},
|
||||
l: l,
|
||||
}
|
||||
}
|
||||
@@ -382,13 +388,55 @@ func (hm *HostMap) EmitStats() {
|
||||
metrics.GetOrRegisterGauge("hostmap.main.relayIndexes", nil).Update(int64(relaysLen))
|
||||
}
|
||||
|
||||
// DeleteHostInfo will fully unlink the hostinfo and return true if it was the final hostinfo for this vpn ip
|
||||
// unlockedSetHostsForAddr stores the per-address hostinfo list (list[0] is the primary). An empty
|
||||
// list removes the address. This is the one place Hosts and moreHosts are written together, keep
|
||||
// it that way. Callers must hold the write lock.
|
||||
func (hm *HostMap) unlockedSetHostsForAddr(addr netip.Addr, list []*HostInfo) {
|
||||
if len(list) == 0 {
|
||||
delete(hm.Hosts, addr)
|
||||
delete(hm.moreHosts, addr)
|
||||
return
|
||||
}
|
||||
hm.Hosts[addr] = list[0]
|
||||
if len(list) > 1 {
|
||||
hm.moreHosts[addr] = list
|
||||
} else {
|
||||
delete(hm.moreHosts, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// unlockedGetHostList returns every hostinfo holding addr, primary first, or nil if we have no
|
||||
// tunnel for addr. The common single-hostinfo case builds a fresh one element list, so keep this
|
||||
// off the packet hot path; the primary is a direct Hosts read. Callers must hold the lock (read
|
||||
// or write).
|
||||
func (hm *HostMap) unlockedGetHostList(addr netip.Addr) []*HostInfo {
|
||||
if list, ok := hm.moreHosts[addr]; ok {
|
||||
return list
|
||||
}
|
||||
if h, ok := hm.Hosts[addr]; ok {
|
||||
return []*HostInfo{h}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeHostInfo returns list with hi removed (order preserved), or list unchanged if hi is
|
||||
// absent. It deletes in place: every mutator holds the hostmap write lock and no reader ever
|
||||
// retains a slice across a mutation (readers iterate under RLock), so there is no snapshot to
|
||||
// invalidate.
|
||||
func removeHostInfo(list []*HostInfo, hi *HostInfo) []*HostInfo {
|
||||
idx := slices.Index(list, hi)
|
||||
if idx < 0 {
|
||||
return list
|
||||
}
|
||||
return slices.Delete(list, idx, idx+1)
|
||||
}
|
||||
|
||||
// DeleteHostInfo will fully unlink the hostinfo and return true if no other hostinfo still holds
|
||||
// any of its vpn addrs, meaning we no longer have a tunnel to the peer
|
||||
func (hm *HostMap) DeleteHostInfo(hostinfo *HostInfo) bool {
|
||||
// Delete the host itself, ensuring it's not modified anymore
|
||||
hm.Lock()
|
||||
// If we have a previous or next hostinfo then we are not the last one for this vpn ip
|
||||
final := (hostinfo.next == nil && hostinfo.prev == nil)
|
||||
hm.unlockedDeleteHostInfo(hostinfo)
|
||||
final := hm.unlockedDeleteHostInfo(hostinfo)
|
||||
hm.Unlock()
|
||||
|
||||
return final
|
||||
@@ -400,85 +448,66 @@ func (hm *HostMap) MakePrimary(hostinfo *HostInfo) {
|
||||
hm.unlockedMakePrimary(hostinfo)
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedMakePrimary(hostinfo *HostInfo) {
|
||||
// Get the current primary, if it exists
|
||||
oldHostinfo := hm.Hosts[hostinfo.vpnAddrs[0]]
|
||||
|
||||
// Every address in the hostinfo gets elevated to primary
|
||||
for _, vpnAddr := range hostinfo.vpnAddrs {
|
||||
//NOTE: It is possible that we leave a dangling hostinfo here but connection manager works on
|
||||
// indexes so it should be fine.
|
||||
hm.Hosts[vpnAddr] = hostinfo
|
||||
// unlockedMakePrimary reports whether hostinfo is (now) the primary for each of its addresses,
|
||||
// false only when it is no longer in the hostmap at all.
|
||||
func (hm *HostMap) unlockedMakePrimary(hostinfo *HostInfo) bool {
|
||||
// A hostinfo that is no longer in the hostmap must not be re-inserted here. Callers can race
|
||||
// tunnel teardown, deciding to promote under the read lock and only taking the write lock
|
||||
// after a delete fully unlinked the hostinfo (connection manager swapPrimary, AddRelay). Every
|
||||
// live hostinfo is registered in Indexes by unlockedAddHostInfo, so this is a membership test.
|
||||
if hm.Indexes[hostinfo.localIndexId] != hostinfo {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we are already primary then we won't bother re-linking
|
||||
if oldHostinfo == hostinfo {
|
||||
return
|
||||
}
|
||||
|
||||
// Unlink this hostinfo
|
||||
if hostinfo.prev != nil {
|
||||
hostinfo.prev.next = hostinfo.next
|
||||
}
|
||||
if hostinfo.next != nil {
|
||||
hostinfo.next.prev = hostinfo.prev
|
||||
}
|
||||
|
||||
// If there wasn't a previous primary then clear out any links
|
||||
if oldHostinfo == nil {
|
||||
hostinfo.next = nil
|
||||
hostinfo.prev = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Relink the hostinfo as primary
|
||||
hostinfo.next = oldHostinfo
|
||||
oldHostinfo.prev = hostinfo
|
||||
hostinfo.prev = nil
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) {
|
||||
// Move hostinfo to the front (primary) of each of its address lists. The lists are
|
||||
// independent per address, so this can never leave a dangling entry the way promoting
|
||||
// against a single shared chain could.
|
||||
for _, addr := range hostinfo.vpnAddrs {
|
||||
h := hm.Hosts[addr]
|
||||
for h != nil {
|
||||
if h == hostinfo {
|
||||
hm.unlockedInnerDeleteHostInfo(h, addr)
|
||||
}
|
||||
h = h.next
|
||||
if hm.Hosts[addr] == hostinfo {
|
||||
// Already primary for this address, the list is already in the right order
|
||||
continue
|
||||
}
|
||||
list := removeHostInfo(hm.unlockedGetHostList(addr), hostinfo)
|
||||
list = append([]*HostInfo{hostinfo}, list...)
|
||||
hm.unlockedSetHostsForAddr(addr, list)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Addr) {
|
||||
primary, ok := hm.Hosts[addr]
|
||||
isLastHostinfo := hostinfo.next == nil && hostinfo.prev == nil
|
||||
if ok && primary == hostinfo {
|
||||
// The vpn addr pointer points to the same hostinfo as the local index id, we can remove it
|
||||
delete(hm.Hosts, addr)
|
||||
if len(hm.Hosts) == 0 {
|
||||
hm.Hosts = map[netip.Addr]*HostInfo{}
|
||||
}
|
||||
|
||||
if hostinfo.next != nil {
|
||||
// We had more than 1 hostinfo at this vpn addr, promote the next in the list to primary
|
||||
hm.Hosts[addr] = hostinfo.next
|
||||
// It is primary, there is no previous hostinfo now
|
||||
hostinfo.next.prev = nil
|
||||
}
|
||||
|
||||
} else {
|
||||
// Relink if we were in the middle of multiple hostinfos for this vpn addr
|
||||
if hostinfo.prev != nil {
|
||||
hostinfo.prev.next = hostinfo.next
|
||||
}
|
||||
|
||||
if hostinfo.next != nil {
|
||||
hostinfo.next.prev = hostinfo.prev
|
||||
// unlockedDeleteHostInfo removes hostinfo from every one of its address lists and from the index
|
||||
// maps. It returns true if this was the last hostinfo for all of its addresses (we no longer have
|
||||
// any tunnel to the peer), which the caller uses to decide whether to clear learned lighthouse
|
||||
// state and disestablish relays.
|
||||
func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) bool {
|
||||
// Remove this hostinfo from each of its address lists. The lists are independent, so a
|
||||
// sibling is never promoted to an address it does not own and no other list is touched.
|
||||
final := true
|
||||
for _, addr := range hostinfo.vpnAddrs {
|
||||
if list, ok := hm.moreHosts[addr]; ok {
|
||||
list = removeHostInfo(list, hostinfo)
|
||||
hm.unlockedSetHostsForAddr(addr, list)
|
||||
if len(list) > 0 {
|
||||
final = false
|
||||
}
|
||||
} else if existing, ok := hm.Hosts[addr]; ok {
|
||||
if existing == hostinfo {
|
||||
// Common case, the only hostinfo for this address. moreHosts has no entry to clean up.
|
||||
delete(hm.Hosts, addr)
|
||||
} else {
|
||||
// We don't hold this address but another hostinfo does, we still have a tunnel to the peer
|
||||
final = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostinfo.next = nil
|
||||
hostinfo.prev = nil
|
||||
// Go maps never shrink their buckets, replace fully drained maps so a node that churned
|
||||
// through a large peer count gives the memory back. Same idiom as the index maps below.
|
||||
if len(hm.Hosts) == 0 {
|
||||
hm.Hosts = map[netip.Addr]*HostInfo{}
|
||||
}
|
||||
if len(hm.moreHosts) == 0 {
|
||||
hm.moreHosts = map[netip.Addr][]*HostInfo{}
|
||||
}
|
||||
|
||||
// The remote index uses index ids outside our control so lets make sure we are only removing
|
||||
// the remote index pointer here if it points to the hostinfo we are deleting
|
||||
@@ -502,7 +531,7 @@ func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Ad
|
||||
)
|
||||
}
|
||||
|
||||
if isLastHostinfo {
|
||||
if final {
|
||||
// I have lost connectivity to my peers. My relay tunnel is likely broken. Mark the next
|
||||
// hops as 'Requested' so that new relay tunnels are created in the future.
|
||||
hm.unlockedDisestablishVpnAddrRelayFor(hostinfo)
|
||||
@@ -511,6 +540,8 @@ func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Ad
|
||||
for _, localRelayIdx := range hostinfo.relayState.CopyRelayForIdxs() {
|
||||
delete(hm.Relays, localRelayIdx)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
func (hm *HostMap) QueryIndex(index uint32) *HostInfo {
|
||||
@@ -554,19 +585,30 @@ func (hm *HostMap) QueryVpnAddrsRelayFor(targetIps []netip.Addr, relayHostIp net
|
||||
hm.RLock()
|
||||
defer hm.RUnlock()
|
||||
|
||||
// This runs per relayed packet, so check the primary with a single map probe and only consult
|
||||
// moreHosts when the primary can't relay for us.
|
||||
h, ok := hm.Hosts[relayHostIp]
|
||||
if !ok {
|
||||
return nil, nil, errors.New("unable to find host")
|
||||
}
|
||||
|
||||
for h != nil {
|
||||
for _, targetIp := range targetIps {
|
||||
r, ok := h.relayState.QueryRelayForByIp(targetIp)
|
||||
if ok && r.State == Established {
|
||||
return h, r, nil
|
||||
for _, targetIp := range targetIps {
|
||||
r, ok := h.relayState.QueryRelayForByIp(targetIp)
|
||||
if ok && r.State == Established {
|
||||
return h, r, nil
|
||||
}
|
||||
}
|
||||
|
||||
if list, ok := hm.moreHosts[relayHostIp]; ok {
|
||||
// list[0] is the primary we already checked
|
||||
for _, h := range list[1:] {
|
||||
for _, targetIp := range targetIps {
|
||||
r, ok := h.relayState.QueryRelayForByIp(targetIp)
|
||||
if ok && r.State == Established {
|
||||
return h, r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
h = h.next
|
||||
}
|
||||
|
||||
return nil, nil, errors.New("unable to find host with relay")
|
||||
@@ -574,20 +616,14 @@ func (hm *HostMap) QueryVpnAddrsRelayFor(targetIps []netip.Addr, relayHostIp net
|
||||
|
||||
func (hm *HostMap) unlockedDisestablishVpnAddrRelayFor(hi *HostInfo) {
|
||||
for _, relayHostIp := range hi.relayState.CopyRelayIps() {
|
||||
if h, ok := hm.Hosts[relayHostIp]; ok {
|
||||
for h != nil {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
h = h.next
|
||||
}
|
||||
for _, h := range hm.unlockedGetHostList(relayHostIp) {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
}
|
||||
}
|
||||
for _, rs := range hi.relayState.CopyAllRelayFor() {
|
||||
if rs.Type == ForwardingType {
|
||||
if h, ok := hm.Hosts[rs.PeerAddr]; ok {
|
||||
for h != nil {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
h = h.next
|
||||
}
|
||||
for _, h := range hm.unlockedGetHostList(rs.PeerAddr) {
|
||||
h.relayState.UpdateRelayForByIpState(hi.vpnAddrs[0], Disestablished)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,6 +659,11 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
|
||||
hm.Indexes[hostinfo.localIndexId] = hostinfo
|
||||
hm.RemoteIndexes[hostinfo.remoteIndexId] = hostinfo
|
||||
|
||||
hostinfo.out.Store(true)
|
||||
if f.connectionManager != nil { // f.connectionManager is only nil in some unit tests
|
||||
f.connectionManager.trafficTimer.Add(hostinfo.localIndexId, f.connectionManager.checkInterval)
|
||||
}
|
||||
|
||||
if hm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hm.l.Debug("Hostmap vpnIp added",
|
||||
"hostMap", m{"vpnAddrs": hostinfo.vpnAddrs, "mapTotalSize": len(hm.Hosts),
|
||||
@@ -632,22 +673,27 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
|
||||
}
|
||||
|
||||
func (hm *HostMap) unlockedInnerAddHostInfo(vpnAddr netip.Addr, hostinfo *HostInfo, f *Interface) {
|
||||
existing := hm.Hosts[vpnAddr]
|
||||
hm.Hosts[vpnAddr] = hostinfo
|
||||
|
||||
if existing != nil && existing != hostinfo {
|
||||
hostinfo.next = existing
|
||||
existing.prev = hostinfo
|
||||
existing, ok := hm.Hosts[vpnAddr]
|
||||
if !ok {
|
||||
// Common case, the first hostinfo for this address. moreHosts stays empty.
|
||||
hm.Hosts[vpnAddr] = hostinfo
|
||||
return
|
||||
}
|
||||
|
||||
i := 1
|
||||
check := hostinfo
|
||||
for check != nil {
|
||||
if i > MaxHostInfosPerVpnIp {
|
||||
hm.unlockedDeleteHostInfo(check)
|
||||
}
|
||||
check = check.next
|
||||
i++
|
||||
// The new hostinfo becomes the primary for this address. Remove any stale copy of it first so
|
||||
// we never hold a duplicate, then prepend.
|
||||
list, ok := hm.moreHosts[vpnAddr]
|
||||
if !ok {
|
||||
list = []*HostInfo{existing}
|
||||
}
|
||||
list = removeHostInfo(list, hostinfo)
|
||||
list = append([]*HostInfo{hostinfo}, list...)
|
||||
hm.unlockedSetHostsForAddr(vpnAddr, list)
|
||||
|
||||
// Enforce the per-address cap by fully retiring the oldest hostinfo once we exceed it.
|
||||
// Deleting it removes it from all of its addresses and the index maps, matching prior behavior.
|
||||
if len(list) > MaxHostInfosPerVpnIp {
|
||||
hm.unlockedDeleteHostInfo(list[len(list)-1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,7 +725,7 @@ func (hm *HostMap) ForEachIndex(f controlEach) {
|
||||
func (i *HostInfo) TryPromoteBest(preferredRanges []netip.Prefix, ifce *Interface) {
|
||||
c := i.promoteCounter.Add(1)
|
||||
if c%ifce.tryPromoteEvery.Load() == 0 {
|
||||
remote := i.remote
|
||||
remote := i.GetRemote()
|
||||
|
||||
// return early if we are already on a preferred remote
|
||||
if remote.IsValid() {
|
||||
@@ -721,11 +767,18 @@ func (i *HostInfo) GetCert() *cert.CachedCertificate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *HostInfo) GetRemote() netip.AddrPort {
|
||||
if p := i.remote.Load(); p != nil {
|
||||
return *p
|
||||
}
|
||||
return netip.AddrPort{}
|
||||
}
|
||||
|
||||
// TODO: Maybe use ViaSender here?
|
||||
func (i *HostInfo) SetRemote(remote netip.AddrPort) {
|
||||
// We copy here because we likely got this remote from a source that reuses the object
|
||||
if i.remote != remote {
|
||||
i.remote = remote
|
||||
if i.GetRemote() != remote {
|
||||
i.remote.Store(&remote)
|
||||
i.remotes.LearnRemote(i.vpnAddrs[0], remote)
|
||||
}
|
||||
}
|
||||
@@ -737,7 +790,7 @@ func (i *HostInfo) SetRemoteIfPreferred(hm *HostMap, via ViaSender) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
currentRemote := i.remote
|
||||
currentRemote := i.GetRemote()
|
||||
if !currentRemote.IsValid() {
|
||||
i.SetRemote(via.UdpAddr)
|
||||
return true
|
||||
|
||||
+294
-137
@@ -2,6 +2,7 @@ package nebula
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/config"
|
||||
@@ -10,78 +11,84 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// chainIds returns the localIndexIds of the hostinfos holding addr, primary (index 0) first. It
|
||||
// also validates the Hosts/moreHosts sync contract on every call so a mutation that broke it
|
||||
// fails fast.
|
||||
func chainIds(t *testing.T, hm *HostMap, addr netip.Addr) []uint32 {
|
||||
t.Helper()
|
||||
assertHostMapInvariants(t, hm)
|
||||
list := hm.unlockedGetHostList(addr)
|
||||
ids := make([]uint32, len(list))
|
||||
for i, h := range list {
|
||||
ids[i] = h.localIndexId
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// assertHostMapInvariants checks the Hosts/moreHosts contract: moreHosts only holds addresses
|
||||
// with 2 or more hostinfos, its first entry is always the primary in Hosts, lists never hold
|
||||
// duplicates, every hostinfo in a list owns the address and is registered in Indexes, and every
|
||||
// indexed hostinfo is reachable through each of its addresses.
|
||||
func assertHostMapInvariants(t *testing.T, hm *HostMap) {
|
||||
t.Helper()
|
||||
for addr, list := range hm.moreHosts {
|
||||
require.GreaterOrEqualf(t, len(list), 2, "moreHosts[%s] must hold at least 2 hostinfos", addr)
|
||||
require.Samef(t, hm.Hosts[addr], list[0], "moreHosts[%s][0] must match the primary in Hosts", addr)
|
||||
seen := map[*HostInfo]bool{}
|
||||
for _, h := range list {
|
||||
require.NotNilf(t, h, "moreHosts[%s] must never hold a nil hostinfo", addr)
|
||||
require.Falsef(t, seen[h], "moreHosts[%s] holds hostinfo %d twice", addr, h.localIndexId)
|
||||
seen[h] = true
|
||||
require.Samef(t, hm.Indexes[h.localIndexId], h, "moreHosts[%s] member %d is not registered in Indexes", addr, h.localIndexId)
|
||||
require.Truef(t, slices.Contains(h.vpnAddrs, addr), "moreHosts[%s] member %d does not own the address", addr, h.localIndexId)
|
||||
}
|
||||
}
|
||||
for addr, h := range hm.Hosts {
|
||||
require.NotNilf(t, h, "Hosts[%s] must never be nil", addr)
|
||||
require.Samef(t, hm.Indexes[h.localIndexId], h, "Hosts[%s] primary %d is not registered in Indexes", addr, h.localIndexId)
|
||||
require.Truef(t, slices.Contains(h.vpnAddrs, addr), "Hosts[%s] primary (index %d) does not own the address", addr, h.localIndexId)
|
||||
}
|
||||
for idx, h := range hm.Indexes {
|
||||
require.Equalf(t, idx, h.localIndexId, "Indexes[%d] holds hostinfo with localIndexId %d", idx, h.localIndexId)
|
||||
for _, va := range h.vpnAddrs {
|
||||
require.Truef(t, slices.Contains(hm.unlockedGetHostList(va), h), "indexed hostinfo %d is missing from the list for %s", idx, va)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostMap_MakePrimary(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 4}
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 4}
|
||||
|
||||
hm.unlockedAddHostInfo(h4, f)
|
||||
hm.unlockedAddHostInfo(h3, f)
|
||||
hm.unlockedAddHostInfo(h2, f)
|
||||
hm.unlockedAddHostInfo(h1, f)
|
||||
|
||||
// Make sure we go h1 -> h2 -> h3 -> h4
|
||||
prim := hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h1.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h4.prev.localIndexId)
|
||||
assert.Nil(t, h4.next)
|
||||
// Most-recently-added is primary: h1, h2, h3, h4
|
||||
assert.Equal(t, []uint32{1, 2, 3, 4}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h1, hm.QueryVpnAddr(a))
|
||||
|
||||
// Swap h3/middle to primary
|
||||
// Swap the middle to primary: h3, h1, h2, h4
|
||||
hm.MakePrimary(h3)
|
||||
assert.Equal(t, []uint32{3, 1, 2, 4}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h3, hm.QueryVpnAddr(a))
|
||||
|
||||
// Make sure we go h3 -> h1 -> h2 -> h4
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h3.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h2.localIndexId, h1.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h1.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h4.prev.localIndexId)
|
||||
assert.Nil(t, h4.next)
|
||||
|
||||
// Swap h4/tail to primary
|
||||
// Swap the tail to primary: h4, h3, h1, h2
|
||||
hm.MakePrimary(h4)
|
||||
assert.Equal(t, []uint32{4, 3, 1, 2}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h4 -> h3 -> h1 -> h2
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h4.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h1.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h1.prev.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Nil(t, h2.next)
|
||||
|
||||
// Swap h4 again should be no-op
|
||||
// Swapping the current primary again is a no-op
|
||||
hm.MakePrimary(h4)
|
||||
|
||||
// Make sure we go h4 -> h3 -> h1 -> h2
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h4.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h1.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h1.prev.localIndexId)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Nil(t, h2.next)
|
||||
assert.Equal(t, []uint32{4, 3, 1, 2}, chainIds(t, hm, a))
|
||||
}
|
||||
|
||||
func TestHostMap_DeleteHostInfo(t *testing.T) {
|
||||
@@ -89,13 +96,14 @@ func TestHostMap_DeleteHostInfo(t *testing.T) {
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 4}
|
||||
h5 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 5}
|
||||
h6 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 6}
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
|
||||
h3 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 3}
|
||||
h4 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 4}
|
||||
h5 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 5}
|
||||
h6 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 6}
|
||||
|
||||
hm.unlockedAddHostInfo(h6, f)
|
||||
hm.unlockedAddHostInfo(h5, f)
|
||||
@@ -104,94 +112,243 @@ func TestHostMap_DeleteHostInfo(t *testing.T) {
|
||||
hm.unlockedAddHostInfo(h2, f)
|
||||
hm.unlockedAddHostInfo(h1, f)
|
||||
|
||||
// h6 should be deleted
|
||||
assert.Nil(t, h6.next)
|
||||
assert.Nil(t, h6.prev)
|
||||
h := hm.QueryIndex(h6.localIndexId)
|
||||
assert.Nil(t, h)
|
||||
// h6 is evicted by the MaxHostInfosPerVpnIp cap; the rest are newest-first.
|
||||
assert.Nil(t, hm.QueryIndex(h6.localIndexId))
|
||||
assert.Equal(t, []uint32{1, 2, 3, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h1 -> h2 -> h3 -> h4 -> h5
|
||||
prim := hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h1.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h1.localIndexId, h2.prev.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h4.prev.localIndexId)
|
||||
assert.Equal(t, h5.localIndexId, h4.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h5.prev.localIndexId)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete primary; not final since siblings remain.
|
||||
assert.False(t, hm.DeleteHostInfo(h1))
|
||||
assert.Equal(t, []uint32{2, 3, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Delete primary
|
||||
hm.DeleteHostInfo(h1)
|
||||
assert.Nil(t, h1.prev)
|
||||
assert.Nil(t, h1.next)
|
||||
// Deleting the same hostinfo again must not report final while siblings remain and must not
|
||||
// disturb the list. The old chain code got this wrong: the first delete nil'd next/prev, so a
|
||||
// second delete looked final and wiped lighthouse state out from under the live sibling.
|
||||
assert.False(t, hm.DeleteHostInfo(h1))
|
||||
assert.Equal(t, []uint32{2, 3, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h2 -> h3 -> h4 -> h5
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h2.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h3.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h3.prev.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h3.next.localIndexId)
|
||||
assert.Equal(t, h3.localIndexId, h4.prev.localIndexId)
|
||||
assert.Equal(t, h5.localIndexId, h4.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h5.prev.localIndexId)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete a middle node.
|
||||
assert.False(t, hm.DeleteHostInfo(h3))
|
||||
assert.Equal(t, []uint32{2, 4, 5}, chainIds(t, hm, a))
|
||||
|
||||
// Delete in the middle
|
||||
hm.DeleteHostInfo(h3)
|
||||
assert.Nil(t, h3.prev)
|
||||
assert.Nil(t, h3.next)
|
||||
// Delete the tail.
|
||||
assert.False(t, hm.DeleteHostInfo(h5))
|
||||
assert.Equal(t, []uint32{2, 4}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we go h2 -> h4 -> h5
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h2.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h4.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h4.prev.localIndexId)
|
||||
assert.Equal(t, h5.localIndexId, h4.next.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, h5.prev.localIndexId)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete the head; h4 remains and becomes primary.
|
||||
assert.False(t, hm.DeleteHostInfo(h2))
|
||||
assert.Equal(t, []uint32{4}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h4, hm.QueryVpnAddr(a))
|
||||
|
||||
// Delete the tail
|
||||
hm.DeleteHostInfo(h5)
|
||||
assert.Nil(t, h5.prev)
|
||||
assert.Nil(t, h5.next)
|
||||
// Delete the only remaining item; final is true and the address is gone.
|
||||
assert.True(t, hm.DeleteHostInfo(h4))
|
||||
assert.Empty(t, chainIds(t, hm, a))
|
||||
assert.Nil(t, hm.QueryVpnAddr(a))
|
||||
|
||||
// Make sure we go h2 -> h4
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h2.localIndexId, prim.localIndexId)
|
||||
assert.Equal(t, h4.localIndexId, prim.next.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Equal(t, h4.localIndexId, h2.next.localIndexId)
|
||||
assert.Equal(t, h2.localIndexId, h4.prev.localIndexId)
|
||||
assert.Nil(t, h4.next)
|
||||
// Deleting an already-gone hostinfo is still final; nothing holds the address anymore.
|
||||
assert.True(t, hm.DeleteHostInfo(h4))
|
||||
assert.Empty(t, chainIds(t, hm, a))
|
||||
}
|
||||
|
||||
// Delete the head
|
||||
hm.DeleteHostInfo(h2)
|
||||
assert.Nil(t, h2.prev)
|
||||
assert.Nil(t, h2.next)
|
||||
// TestHostMap_MakePrimary_DeletedHostInfo covers promoting a hostinfo that lost a race with
|
||||
// tunnel teardown: swapPrimary and AddRelay decide to promote while holding a stale pointer and
|
||||
// only take the write lock after a delete fully unlinked the hostinfo. MakePrimary must be a
|
||||
// no-op, not a resurrection that installs an unmanaged primary.
|
||||
func TestHostMap_MakePrimary_DeletedHostInfo(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
// Make sure we only have h4
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Equal(t, h4.localIndexId, prim.localIndexId)
|
||||
assert.Nil(t, prim.prev)
|
||||
assert.Nil(t, prim.next)
|
||||
assert.Nil(t, h4.next)
|
||||
h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(h1, f)
|
||||
hm.unlockedAddHostInfo(h2, f)
|
||||
|
||||
// Delete the only item
|
||||
hm.DeleteHostInfo(h4)
|
||||
assert.Nil(t, h4.prev)
|
||||
assert.Nil(t, h4.next)
|
||||
// h1 is fully deleted while another goroutine still holds a pointer to it.
|
||||
assert.False(t, hm.DeleteHostInfo(h1))
|
||||
assert.Equal(t, []uint32{2}, chainIds(t, hm, a))
|
||||
|
||||
// Make sure we have nil
|
||||
prim = hm.QueryVpnAddr(netip.MustParseAddr("0.0.0.1"))
|
||||
assert.Nil(t, prim)
|
||||
// The stale promote must not bring it back.
|
||||
hm.MakePrimary(h1)
|
||||
assert.Equal(t, []uint32{2}, chainIds(t, hm, a))
|
||||
assert.Equal(t, h2, hm.QueryVpnAddr(a))
|
||||
assert.Nil(t, hm.QueryIndex(h1.localIndexId))
|
||||
}
|
||||
|
||||
// TestHostMap_QueryVpnAddrsRelayFor_NonPrimary makes sure a relay established on an older
|
||||
// hostinfo is still found after a newer tunnel without relay state takes primary for the same
|
||||
// address. The lookup checks the primary first and falls back to the rest of the list.
|
||||
func TestHostMap_QueryVpnAddrsRelayFor_NonPrimary(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
relayAddr := netip.MustParseAddr("0.0.0.9")
|
||||
target := netip.MustParseAddr("0.0.0.1")
|
||||
|
||||
older := &HostInfo{
|
||||
vpnAddrs: []netip.Addr{relayAddr},
|
||||
localIndexId: 1,
|
||||
relayState: RelayState{
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}
|
||||
older.relayState.InsertRelay(target, 100, &Relay{Type: ForwardingType, State: Established, LocalIndex: 100, PeerAddr: target})
|
||||
hm.unlockedAddHostInfo(older, f)
|
||||
|
||||
// The relay is found on the primary.
|
||||
h, r, err := hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, relayAddr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, older, h)
|
||||
assert.Equal(t, uint32(100), r.LocalIndex)
|
||||
|
||||
// A re-handshake with no relay state takes primary; the established relay on the older
|
||||
// hostinfo must still be found through the fallback.
|
||||
newer := &HostInfo{vpnAddrs: []netip.Addr{relayAddr}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(newer, f)
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, relayAddr))
|
||||
|
||||
h, r, err = hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, relayAddr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, older, h)
|
||||
assert.Equal(t, uint32(100), r.LocalIndex)
|
||||
|
||||
// No hostinfo at all is a plain miss.
|
||||
_, _, err = hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, netip.MustParseAddr("0.0.0.42"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestHostMap_DeleteHostInfo_MultipleVpnAddrs exercises the case where a hostinfo carries more than one
|
||||
// vpnAddr and shares its next/prev chain with a live sibling. Deleting the head must not corrupt the
|
||||
// sibling: every address the sibling owns has to keep pointing at it. The pre-fix code unlinked the shared
|
||||
// chain once per vpnAddr, so on the first address it nil'd next/prev, and on the second address the node
|
||||
// looked already-detached: it dropped the map entry instead of promoting the sibling (and tripped the
|
||||
// isLastHostinfo relay teardown). See unlockedDeleteHostInfo.
|
||||
func TestHostMap_DeleteHostInfo_MultipleVpnAddrs(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
// Two tunnels for the same peer, each reachable at both a and b.
|
||||
other := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 1}
|
||||
head := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 2}
|
||||
|
||||
hm.unlockedAddHostInfo(other, f)
|
||||
hm.unlockedAddHostInfo(head, f)
|
||||
|
||||
// head is primary for both addresses, other is next in each address's list.
|
||||
assert.Equal(t, head, hm.QueryVpnAddr(a))
|
||||
assert.Equal(t, head, hm.QueryVpnAddr(b))
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, b))
|
||||
|
||||
// Delete the head. other is still live, so it must become primary for BOTH addresses.
|
||||
assert.False(t, hm.DeleteHostInfo(head))
|
||||
assert.Equal(t, other, hm.QueryVpnAddr(a))
|
||||
assert.Equal(t, other, hm.QueryVpnAddr(b))
|
||||
assert.Equal(t, []uint32{1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{1}, chainIds(t, hm, b))
|
||||
|
||||
// head is fully removed from the index map.
|
||||
assert.Nil(t, hm.QueryIndex(head.localIndexId))
|
||||
}
|
||||
|
||||
// TestHostMap_DeleteHostInfo_DivergentVpnAddrs covers chained hostinfos for the same peer whose
|
||||
// vpnAddrs sets differ (a re-handshake cert added a second address). Deleting the superset node
|
||||
// must not promote a sibling to an address it does not own.
|
||||
func TestHostMap_DeleteHostInfo_DivergentVpnAddrs(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
// sub owns only a; super (a newer handshake) owns a and b.
|
||||
sub := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
super := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(sub, f)
|
||||
hm.unlockedAddHostInfo(super, f)
|
||||
|
||||
assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{2}, chainIds(t, hm, b))
|
||||
|
||||
// Delete super: a promotes to sub (which owns it); b has no remaining owner and must be
|
||||
// removed, not dangled at sub (which does not own b).
|
||||
assert.False(t, hm.DeleteHostInfo(super))
|
||||
assert.Equal(t, []uint32{1}, chainIds(t, hm, a))
|
||||
assert.Empty(t, chainIds(t, hm, b))
|
||||
assert.Equal(t, sub, hm.QueryVpnAddr(a))
|
||||
assert.Nil(t, hm.QueryVpnAddr(b))
|
||||
assert.Nil(t, hm.QueryIndex(super.localIndexId))
|
||||
|
||||
// Deleting sub cleans up fully.
|
||||
assert.True(t, hm.DeleteHostInfo(sub))
|
||||
assert.Nil(t, hm.QueryVpnAddr(a))
|
||||
assertHostMapInvariants(t, hm)
|
||||
}
|
||||
|
||||
// TestHostMap_AddDivergentOverlap covers a new hostinfo claiming addresses currently owned by two
|
||||
// DIFFERENT hostinfos. The old single shared next/prev chain overwrote a pointer and orphaned one
|
||||
// of them (in Indexes but unreachable via its address); independent per-address lists cannot.
|
||||
func TestHostMap_AddDivergentOverlap(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
f := &Interface{}
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
hiA := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
|
||||
hiP := &HostInfo{vpnAddrs: []netip.Addr{b}, localIndexId: 2}
|
||||
hm.unlockedAddHostInfo(hiA, f)
|
||||
hm.unlockedAddHostInfo(hiP, f)
|
||||
|
||||
hiB := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 3}
|
||||
hm.unlockedAddHostInfo(hiB, f)
|
||||
|
||||
assert.Equal(t, []uint32{3, 1}, chainIds(t, hm, a))
|
||||
assert.Equal(t, []uint32{3, 2}, chainIds(t, hm, b))
|
||||
// hiA is still reachable via its address (not orphaned) and still indexed.
|
||||
assert.Contains(t, chainIds(t, hm, a), hiA.localIndexId)
|
||||
assert.NotNil(t, hm.QueryIndex(hiA.localIndexId))
|
||||
}
|
||||
|
||||
// TestHostMap_MaxHostInfosPerVpnIp_MultipleVpnAddrs verifies the MaxHostInfosPerVpnIp overflow prune
|
||||
// (unlockedInnerAddHostInfo calls unlockedDeleteHostInfo on the oldest node once the chain is too long)
|
||||
// still behaves when hostinfos carry more than one vpnAddr. The pruned node is always the tail, so it is
|
||||
// primary for none of the addresses, and both address chains must stay consistent afterwards.
|
||||
func TestHostMap_MaxHostInfosPerVpnIp_MultipleVpnAddrs(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
hm := newHostMap(l)
|
||||
|
||||
f := &Interface{}
|
||||
|
||||
a := netip.MustParseAddr("0.0.0.1")
|
||||
b := netip.MustParseAddr("0.0.0.2")
|
||||
|
||||
// Add one more than the cap, newest last so it becomes head. Every hostinfo owns both a and b.
|
||||
hostinfos := make([]*HostInfo, 0, MaxHostInfosPerVpnIp+1)
|
||||
for i := 0; i <= MaxHostInfosPerVpnIp; i++ {
|
||||
hostinfos = append(hostinfos, &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: uint32(i + 1)})
|
||||
}
|
||||
// Add oldest first (highest index in our slice) so the very first one added is the overflow victim.
|
||||
for i := len(hostinfos) - 1; i >= 0; i-- {
|
||||
hm.unlockedAddHostInfo(hostinfos[i], f)
|
||||
}
|
||||
|
||||
oldest := hostinfos[len(hostinfos)-1]
|
||||
|
||||
// The oldest hostinfo was pruned from both lists and the index map.
|
||||
assert.Nil(t, hm.QueryIndex(oldest.localIndexId))
|
||||
|
||||
// Both addresses hold exactly MaxHostInfosPerVpnIp survivors in the same order; oldest is absent.
|
||||
require.Len(t, chainIds(t, hm, a), MaxHostInfosPerVpnIp)
|
||||
assert.Equal(t, chainIds(t, hm, a), chainIds(t, hm, b), "both addresses must list the same survivors in the same order")
|
||||
assert.NotContains(t, chainIds(t, hm, a), oldest.localIndexId)
|
||||
assert.Equal(t, hm.QueryVpnAddr(a), hm.QueryVpnAddr(b))
|
||||
}
|
||||
|
||||
func TestHostMap_reload(t *testing.T) {
|
||||
|
||||
@@ -37,7 +37,7 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
|
||||
// routes packets from the Nebula addr to the Nebula addr through the Nebula
|
||||
// TUN device.
|
||||
if immediatelyForwardToSelf {
|
||||
_, err := f.readers[q].Write(packet)
|
||||
_, err := f.queues[q].Write(packet)
|
||||
if err != nil {
|
||||
f.l.Error("Failed to forward to tun", "error", err)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
|
||||
}
|
||||
|
||||
func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
|
||||
if !f.firewall.InSendReject {
|
||||
if !f.firewall.OutboundSendReject {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,14 +96,14 @@ func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := f.readers[q].Write(out)
|
||||
_, err := f.queues[q].Write(out)
|
||||
if err != nil {
|
||||
f.l.Error("Failed to write to tun", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Interface) rejectOutside(packet []byte, ci *ConnectionState, hostinfo *HostInfo, nb, out []byte, q int) {
|
||||
if !f.firewall.OutSendReject {
|
||||
if !f.firewall.InboundSendReject {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ func (f *Interface) SendVia(via *HostInfo,
|
||||
via.logger(f.l).Info("Failed to EncryptDanger in sendVia", "error", err)
|
||||
return
|
||||
}
|
||||
err = f.writers[0].WriteTo(out, via.remote)
|
||||
err = f.writers[0].WriteTo(out, via.GetRemote())
|
||||
if err != nil {
|
||||
via.logger(f.l).Info("Failed to WriteTo in sendVia", "error", err)
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
||||
if ci.eKey == nil {
|
||||
return
|
||||
}
|
||||
useRelay := !remote.IsValid() && !hostinfo.remote.IsValid()
|
||||
useRelay := !remote.IsValid() && !hostinfo.GetRemote().IsValid()
|
||||
fullOut := out
|
||||
|
||||
if useRelay {
|
||||
@@ -391,7 +391,6 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
||||
"error", err,
|
||||
"udpAddr", remote,
|
||||
"counter", c,
|
||||
"attemptedCounter", c,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -404,8 +403,8 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
||||
"udpAddr", remote,
|
||||
)
|
||||
}
|
||||
} else if hostinfo.remote.IsValid() {
|
||||
err = f.writers[q].WriteTo(out, hostinfo.remote)
|
||||
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
||||
err = f.writers[q].WriteTo(out, hr)
|
||||
if err != nil {
|
||||
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
|
||||
"error", err,
|
||||
|
||||
+142
-59
@@ -4,9 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -14,11 +15,14 @@ import (
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/rcrowley/go-metrics"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/firewall"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/overlay"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/slackhq/nebula/util"
|
||||
)
|
||||
|
||||
const mtu = 9001
|
||||
@@ -47,7 +51,19 @@ type InterfaceConfig struct {
|
||||
reQueryWait time.Duration
|
||||
|
||||
ConntrackCacheTimeout time.Duration
|
||||
l *slog.Logger
|
||||
|
||||
// CpuAffinity, when non-empty, names the CPUs each TUN reader goroutine
|
||||
// should pin to. Queue i pins to CpuAffinity[i % len(CpuAffinity)] —
|
||||
// shorter lists than `routines` cycle. Empty list keeps the default
|
||||
// pin-to-(i % NumCPU) behavior. Only consulted when PinThreads is true.
|
||||
CpuAffinity []int
|
||||
// PinThreads controls whether each TUN reader OS thread is pinned to a
|
||||
// single CPU (via tun.pin_threads, default true). Pinning keeps each
|
||||
// goroutine's UDP sends on one XPS-selected NIC TX ring so per-flow
|
||||
// packets stay ordered on the wire.
|
||||
PinThreads bool
|
||||
|
||||
l *slog.Logger
|
||||
}
|
||||
|
||||
type Interface struct {
|
||||
@@ -71,7 +87,16 @@ type Interface struct {
|
||||
routines int
|
||||
disconnectInvalid atomic.Bool
|
||||
closed atomic.Bool
|
||||
relayManager *relayManager
|
||||
// cpuAffinity, when non-empty, names the CPUs each TUN reader goroutine
|
||||
// should pin to. Queue i pins to cpuAffinity[i % len(cpuAffinity)].
|
||||
// Empty falls back to the default pin-to-(allowed CPU) behavior.
|
||||
// Only consulted when pinThreads is true.
|
||||
cpuAffinity []int
|
||||
// pinThreads controls whether listenIn pins each TUN reader OS thread to
|
||||
// a CPU at all (tun.pin_threads, default true). When false, threads are
|
||||
// left free to migrate as on stock nebula.
|
||||
pinThreads bool
|
||||
relayManager *relayManager
|
||||
|
||||
tryPromoteEvery atomic.Uint32
|
||||
reQueryEvery atomic.Uint32
|
||||
@@ -88,7 +113,7 @@ type Interface struct {
|
||||
|
||||
ctx context.Context
|
||||
writers []udp.Conn
|
||||
readers []io.ReadWriteCloser
|
||||
queues []tio.Queue
|
||||
wg sync.WaitGroup
|
||||
|
||||
// fatalErr holds the first unexpected reader error that caused shutdown.
|
||||
@@ -187,7 +212,6 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
||||
routines: c.routines,
|
||||
version: c.version,
|
||||
writers: make([]udp.Conn, c.routines),
|
||||
readers: make([]io.ReadWriteCloser, c.routines),
|
||||
myVpnNetworks: cs.myVpnNetworks,
|
||||
myVpnNetworksTable: cs.myVpnNetworksTable,
|
||||
myVpnAddrs: cs.myVpnAddrs,
|
||||
@@ -196,6 +220,8 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
||||
relayManager: c.relayManager,
|
||||
connectionManager: c.connectionManager,
|
||||
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
||||
cpuAffinity: c.CpuAffinity,
|
||||
pinThreads: c.PinThreads,
|
||||
|
||||
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
||||
messageMetrics: c.MessageMetrics,
|
||||
@@ -213,6 +239,9 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
||||
|
||||
ifce.connectionManager.intf = ifce
|
||||
|
||||
// Held until Close so waiting on the interface blocks until the resources are actually released
|
||||
ifce.wg.Add(1)
|
||||
|
||||
return ifce, nil
|
||||
}
|
||||
|
||||
@@ -235,38 +264,37 @@ func (f *Interface) activate() error {
|
||||
"boringcrypto", boringEnabled(),
|
||||
)
|
||||
|
||||
if f.routines > 1 {
|
||||
if !f.inside.SupportsMultiqueue() || !f.outside.SupportsMultipleReaders() {
|
||||
f.routines = 1
|
||||
f.l.Warn("routines is not supported on this platform, falling back to a single routine")
|
||||
}
|
||||
if f.routines > 1 && !f.outside.SupportsMultipleReaders() {
|
||||
f.routines = 1
|
||||
f.l.Warn("multiple udp readers are not supported on this platform, falling back to a single routine")
|
||||
}
|
||||
|
||||
// Prepare the tun queues. A device that can't open that many hands back
|
||||
// fewer (a single queue on platforms without multiqueue support) and we
|
||||
// size the reader routines to what we actually got.
|
||||
queues, err := f.inside.Queues(f.routines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(queues) < f.routines {
|
||||
f.l.Warn("tun multiqueue is not supported on this platform, falling back to fewer routines",
|
||||
"requested", f.routines, "opened", len(queues))
|
||||
f.routines = len(queues)
|
||||
}
|
||||
f.queues = queues
|
||||
|
||||
metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines))
|
||||
|
||||
// Prepare n tun queues
|
||||
var reader io.ReadWriteCloser = f.inside
|
||||
for i := 0; i < f.routines; i++ {
|
||||
if i > 0 {
|
||||
reader, err = f.inside.NewMultiQueueReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f.readers[i] = reader
|
||||
}
|
||||
|
||||
f.wg.Add(1) // for us to wait on Close() to return
|
||||
// On error the caller owns the cleanup, Control.Start cancels the service context
|
||||
// before releasing our resources so a waiter never observes a live context
|
||||
if err = f.inside.Activate(); err != nil {
|
||||
f.wg.Done()
|
||||
f.inside.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Interface) run() (func() error, error) {
|
||||
func (f *Interface) run() {
|
||||
// Launch n queues to read packets from udp
|
||||
for i := 0; i < f.routines; i++ {
|
||||
f.wg.Go(func() {
|
||||
@@ -277,17 +305,18 @@ func (f *Interface) run() (func() error, error) {
|
||||
// Launch n queues to read packets from tun dev
|
||||
for i := 0; i < f.routines; i++ {
|
||||
f.wg.Go(func() {
|
||||
f.listenIn(f.readers[i], i)
|
||||
f.listenIn(f.queues[i], i)
|
||||
})
|
||||
}
|
||||
|
||||
return func() error {
|
||||
f.wg.Wait()
|
||||
if e := f.fatalErr.Load(); e != nil {
|
||||
return *e
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *Interface) wait() error {
|
||||
f.wg.Wait()
|
||||
if e := f.fatalErr.Load(); e != nil {
|
||||
return *e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onFatal stores the first fatal reader error, and calls triggerShutdown if it was the first one
|
||||
@@ -320,7 +349,10 @@ func (f *Interface) listenOut(i int) {
|
||||
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintext[:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get())
|
||||
})
|
||||
|
||||
if err != nil && !f.closed.Load() {
|
||||
// An error after teardown began is shutdown noise, the closed flag covers resources
|
||||
// Close releases itself and the cancelled ctx covers ones torn down by their owners
|
||||
// reacting to it, like the user device pipes
|
||||
if err != nil && !f.closed.Load() && f.ctx.Err() == nil {
|
||||
f.l.Error("Error while reading inbound packet, closing", "error", err)
|
||||
f.onFatal(err)
|
||||
}
|
||||
@@ -328,8 +360,29 @@ func (f *Interface) listenOut(i int) {
|
||||
f.l.Debug("underlay reader is done", "reader", i)
|
||||
}
|
||||
|
||||
func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
||||
packet := make([]byte, mtu)
|
||||
func (f *Interface) listenIn(queue tio.Queue, i int) {
|
||||
// Pinning this thread (and goroutine) to a single CPU keeps every UDP send from this goroutine going through
|
||||
// the same TX ring on the nic (XPS selects the ring by CPU), so the wire sees per-flow order. Skip entirely
|
||||
// when tun.pin_threads is false.
|
||||
if f.pinThreads {
|
||||
var cpu int
|
||||
if n := len(f.cpuAffinity); n > 0 {
|
||||
// Explicit tun.cpu_affinity list wins; parseCpuAffinity already
|
||||
// validated the entries against the allowed CPU set.
|
||||
cpu = f.cpuAffinity[i%n]
|
||||
} else if allowed, err := util.AllowedCPUs(); err == nil && len(allowed) > 0 {
|
||||
// Default: spread queues across the CPUs we're actually allowed to
|
||||
// run on. Under a cpuset/taskset mask these aren't 0..NumCPU-1, so
|
||||
// i % NumCPU would pick unrunnable IDs and every pin would fail.
|
||||
cpu = allowed[i%len(allowed)]
|
||||
} else {
|
||||
cpu = i % runtime.NumCPU()
|
||||
}
|
||||
if err := util.PinThreadToCPU(cpu); err != nil {
|
||||
f.l.Warn("failed to pin tun reader to CPU", "queue", i, "cpu", cpu, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]byte, mtu)
|
||||
fwPacket := &firewall.Packet{}
|
||||
nb := make([]byte, 12, 12)
|
||||
@@ -337,16 +390,21 @@ func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
||||
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
|
||||
|
||||
for {
|
||||
n, err := reader.Read(packet)
|
||||
pkts, err := queue.Read()
|
||||
if err != nil {
|
||||
if !f.closed.Load() {
|
||||
// Same shutdown noise handling as listenOut
|
||||
if !f.closed.Load() && f.ctx.Err() == nil {
|
||||
f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i)
|
||||
f.onFatal(err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
f.consumeInsidePacket(packet[:n], fwPacket, nb, out, i, conntrackCache.Get())
|
||||
for _, pkt := range pkts {
|
||||
// borrowed: pkt.Bytes is owned by the queue and only valid until
|
||||
// the next Read; consumeInsidePacket reads it synchronously.
|
||||
f.consumeInsidePacket(pkt.Bytes, fwPacket, nb, out, i, conntrackCache.Get())
|
||||
}
|
||||
}
|
||||
|
||||
f.l.Debug("overlay reader is done", "reader", i)
|
||||
@@ -375,13 +433,22 @@ func (f *Interface) reloadDisconnectInvalid(c *config.C) {
|
||||
}
|
||||
|
||||
func (f *Interface) reloadFirewall(c *config.C) {
|
||||
//TODO: need to trigger/detect if the certificate changed too
|
||||
if c.HasChanged("firewall") == false {
|
||||
cs := f.pki.getCertState()
|
||||
curCert := cs.getCertificate(cert.Version2)
|
||||
if curCert == nil {
|
||||
curCert = cs.getCertificate(cert.Version1)
|
||||
}
|
||||
|
||||
// The firewall builds its routableNetworks set from the certificate's UnsafeNetworks at construction.
|
||||
// Check to see if that set has changed, and if so, rebuild the firewall.
|
||||
certUnsafeChanged := curCert != nil && !slices.Equal(curCert.UnsafeNetworks(), f.firewall.unsafeNetworks)
|
||||
|
||||
if !c.HasChanged("firewall") && !certUnsafeChanged {
|
||||
f.l.Debug("No firewall config change detected")
|
||||
return
|
||||
}
|
||||
|
||||
fw, err := NewFirewallFromConfig(f.l, f.pki.getCertState(), c)
|
||||
fw, err := NewFirewallFromConfig(f.l, cs, c)
|
||||
if err != nil {
|
||||
f.l.Error("Error while creating firewall during reload", "error", err)
|
||||
return
|
||||
@@ -491,26 +558,34 @@ func (f *Interface) emitStats(ctx context.Context, i time.Duration) {
|
||||
certInitiatingVersion := metrics.GetOrRegisterGauge("certificate.initiating_version", nil)
|
||||
certMaxVersion := metrics.GetOrRegisterGauge("certificate.max_version", nil)
|
||||
|
||||
emit := func() {
|
||||
f.firewall.EmitStats()
|
||||
f.handshakeManager.EmitStats()
|
||||
udpStats()
|
||||
|
||||
certState := f.pki.getCertState()
|
||||
defaultCrt := certState.GetDefaultCertificate()
|
||||
certExpirationGauge.Update(int64(defaultCrt.NotAfter().Sub(time.Now()) / time.Second))
|
||||
certInitiatingVersion.Update(int64(defaultCrt.Version()))
|
||||
|
||||
// Report the max certificate version we are capable of using
|
||||
if certState.v2Cert != nil {
|
||||
certMaxVersion.Update(int64(certState.v2Cert.Version()))
|
||||
} else {
|
||||
certMaxVersion.Update(int64(certState.v1Cert.Version()))
|
||||
}
|
||||
}
|
||||
|
||||
// Prime gauges so a Prometheus scrape that lands before the first tick
|
||||
// sees real values instead of the zero defaults (issue #907).
|
||||
emit()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
f.firewall.EmitStats()
|
||||
f.handshakeManager.EmitStats()
|
||||
udpStats()
|
||||
|
||||
certState := f.pki.getCertState()
|
||||
defaultCrt := certState.GetDefaultCertificate()
|
||||
certExpirationGauge.Update(int64(defaultCrt.NotAfter().Sub(time.Now()) / time.Second))
|
||||
certInitiatingVersion.Update(int64(defaultCrt.Version()))
|
||||
|
||||
// Report the max certificate version we are capable of using
|
||||
if certState.v2Cert != nil {
|
||||
certMaxVersion.Update(int64(certState.v2Cert.Version()))
|
||||
} else {
|
||||
certMaxVersion.Update(int64(certState.v1Cert.Version()))
|
||||
}
|
||||
emit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,9 +598,15 @@ func (f *Interface) GetCertState() *CertState {
|
||||
return f.pki.getCertState()
|
||||
}
|
||||
|
||||
// Close releases the interface's resources: the udp sockets and the tun device.
|
||||
// It is idempotent and safe to call at any point in the lifecycle, including on an interface that never activated,
|
||||
// calls after the first return nil without doing anything.
|
||||
func (f *Interface) Close() error {
|
||||
if !f.closed.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
f.closed.Store(true)
|
||||
|
||||
// Release the udp readers
|
||||
for i, u := range f.writers {
|
||||
@@ -541,6 +622,8 @@ func (f *Interface) Close() error {
|
||||
if closeErr != nil {
|
||||
errs = append(errs, closeErr)
|
||||
}
|
||||
|
||||
// Release the construction token so waiters know the resources are gone
|
||||
f.wg.Done()
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/firewall"
|
||||
"github.com/slackhq/nebula/overlay/overlaytest"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test_emitStats_primesGauges covers issue #907: a Prometheus scrape that
|
||||
// landed before the first ticker fire used to read 0 for the cert gauges.
|
||||
// emitStats now primes the gauges before entering the ticker loop. We assert
|
||||
// the gauge is zero before the first call and non-zero after.
|
||||
func Test_emitStats_primesGauges(t *testing.T) {
|
||||
defer metrics.DefaultRegistry.UnregisterAll()
|
||||
|
||||
l := test.NewLogger()
|
||||
hostMap := newHostMap(l)
|
||||
preferredRanges := []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}
|
||||
hostMap.preferredRanges.Store(&preferredRanges)
|
||||
|
||||
notAfter := time.Now().Add(time.Hour)
|
||||
cs := &CertState{
|
||||
initiatingVersion: cert.Version1,
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{version: cert.Version1, notAfter: notAfter},
|
||||
v1Credential: nil,
|
||||
}
|
||||
|
||||
lh := newTestLighthouse()
|
||||
ifce := &Interface{
|
||||
hostMap: hostMap,
|
||||
inside: &overlaytest.NoopTun{},
|
||||
outside: &udp.NoopConn{},
|
||||
firewall: &Firewall{Conntrack: &FirewallConntrack{Conns: map[firewall.Packet]*conn{}}},
|
||||
lightHouse: lh,
|
||||
pki: &PKI{},
|
||||
handshakeManager: NewHandshakeManager(l, hostMap, lh, &udp.NoopConn{}, defaultHandshakeConfig),
|
||||
l: l,
|
||||
// On linux, udp.NewUDPStatsEmitter indexes writers[0] and asserts to
|
||||
// *udp.StdConn. A zero value works: getMemInfo sees a nil rawConn,
|
||||
// returns an error, and the emitter falls through to a no-op.
|
||||
writers: []udp.Conn{&udp.StdConn{}},
|
||||
}
|
||||
ifce.pki.cs.Store(cs)
|
||||
|
||||
ttlGauge := metrics.GetOrRegisterGauge("certificate.ttl_seconds", nil)
|
||||
require.Zero(t, ttlGauge.Value(), "gauge should be zero before emitStats runs")
|
||||
|
||||
// Pre-cancel the context so emitStats returns after priming the gauges
|
||||
// without ever reading from ticker.C. The one hour interval is just a
|
||||
// belt-and-suspenders, the test does not expect the ticker to fire.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
ifce.emitStats(ctx, time.Hour)
|
||||
|
||||
ttl := ttlGauge.Value()
|
||||
assert.Positive(t, ttl, "ttl gauge should be primed by emitStats before its first tick")
|
||||
assert.LessOrEqual(t, ttl, int64(3600))
|
||||
assert.Equal(t, int64(cert.Version1), metrics.GetOrRegisterGauge("certificate.initiating_version", nil).Value())
|
||||
assert.Equal(t, int64(cert.Version1), metrics.GetOrRegisterGauge("certificate.max_version", nil).Value())
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestReloadFirewall_CertUnsafeNetworksChanged verifies that reloadFirewall
|
||||
// rebuilds the firewall when only the certificate's UnsafeNetworks have changed,
|
||||
// even if the firewall section of the YAML has not.
|
||||
func TestReloadFirewall_CertUnsafeNetworksChanged(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
|
||||
vpnNet := netip.MustParsePrefix("10.0.0.1/24")
|
||||
initialUnsafe := []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")}
|
||||
|
||||
// dummyCert avoids dragging the real signing pipeline into a unit test.
|
||||
c1 := &dummyCert{
|
||||
version: cert.Version2,
|
||||
networks: []netip.Prefix{vpnNet},
|
||||
unsafeNetworks: initialUnsafe,
|
||||
}
|
||||
pki := &PKI{}
|
||||
pki.cs.Store(&CertState{v2Cert: c1, initiatingVersion: cert.Version2})
|
||||
|
||||
rawYAML := `firewall:
|
||||
outbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
inbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
`
|
||||
cfg := config.NewC(l)
|
||||
require.NoError(t, cfg.LoadString(rawYAML))
|
||||
|
||||
fw, err := NewFirewallFromConfig(l, pki.getCertState(), cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, initialUnsafe, fw.unsafeNetworks)
|
||||
|
||||
f := &Interface{
|
||||
pki: pki,
|
||||
firewall: fw,
|
||||
l: l,
|
||||
}
|
||||
|
||||
// Swap the cert with a different UnsafeNetworks set.
|
||||
newUnsafe := []netip.Prefix{
|
||||
netip.MustParsePrefix("198.51.100.0/24"),
|
||||
netip.MustParsePrefix("203.0.113.0/24"),
|
||||
}
|
||||
c2 := &dummyCert{
|
||||
version: cert.Version2,
|
||||
networks: []netip.Prefix{vpnNet},
|
||||
unsafeNetworks: newUnsafe,
|
||||
}
|
||||
pki.cs.Store(&CertState{v2Cert: c2, initiatingVersion: cert.Version2})
|
||||
|
||||
// Reload with the same YAML so HasChanged("firewall") reports false.
|
||||
require.NoError(t, cfg.ReloadConfigString(rawYAML))
|
||||
require.False(t, cfg.HasChanged("firewall"))
|
||||
|
||||
f.reloadFirewall(cfg)
|
||||
|
||||
assert.NotSame(t, fw, f.firewall, "firewall pointer should have been replaced")
|
||||
assert.Equal(t, newUnsafe, f.firewall.unsafeNetworks)
|
||||
assert.True(t, f.firewall.routableNetworks.Contains(netip.MustParseAddr("203.0.113.5")))
|
||||
}
|
||||
|
||||
// TestReloadFirewall_NoChange verifies that reloadFirewall is a no-op when
|
||||
// neither the firewall config nor the cert's UnsafeNetworks have changed.
|
||||
func TestReloadFirewall_NoChange(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
|
||||
vpnNet := netip.MustParsePrefix("10.0.0.1/24")
|
||||
unsafe := []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")}
|
||||
|
||||
c1 := &dummyCert{
|
||||
version: cert.Version2,
|
||||
networks: []netip.Prefix{vpnNet},
|
||||
unsafeNetworks: unsafe,
|
||||
}
|
||||
pki := &PKI{}
|
||||
pki.cs.Store(&CertState{v2Cert: c1, initiatingVersion: cert.Version2})
|
||||
|
||||
rawYAML := `firewall:
|
||||
outbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
inbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
`
|
||||
cfg := config.NewC(l)
|
||||
require.NoError(t, cfg.LoadString(rawYAML))
|
||||
|
||||
fw, err := NewFirewallFromConfig(l, pki.getCertState(), cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
f := &Interface{
|
||||
pki: pki,
|
||||
firewall: fw,
|
||||
l: l,
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.ReloadConfigString(rawYAML))
|
||||
f.reloadFirewall(cfg)
|
||||
|
||||
assert.Same(t, fw, f.firewall, "firewall should not have been replaced")
|
||||
}
|
||||
+290
-19
@@ -4,26 +4,54 @@ import (
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
const (
|
||||
// Need 96 bytes for the largest reject packet:
|
||||
// MaxIPv4RejectPacketSize is the largest IPv4 reject packet:
|
||||
// - 20 byte ipv4 header
|
||||
// - 8 byte icmpv4 header
|
||||
// - 68 byte body (60 byte max orig ipv4 header + 8 byte orig icmpv4 header)
|
||||
MaxRejectPacketSize = ipv4.HeaderLen + 8 + 60 + 8
|
||||
maxIPv4RejectPacketSize = ipv4.HeaderLen + 8 + 60 + 8
|
||||
|
||||
// MaxRejectPacketSize is sized for the largest possible reject packet (IPv6):
|
||||
// - 40 byte ipv6 header
|
||||
// - 8 byte icmpv6 header
|
||||
// - up to 1000 byte body (original packet, possibly truncated. We want to stay
|
||||
// under the MTU with Nebula overhead included)
|
||||
maxIPv6RejectPacketSize = ipv6.HeaderLen + 8 + 1000
|
||||
|
||||
MaxRejectPacketSize = maxIPv6RejectPacketSize
|
||||
)
|
||||
|
||||
func CreateRejectPacket(packet []byte, out []byte) []byte {
|
||||
if len(packet) < ipv4.HeaderLen || int(packet[0]>>4) != ipv4.Version {
|
||||
if len(packet) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch packet[9] {
|
||||
case 6: // tcp
|
||||
return ipv4CreateRejectTCPPacket(packet, out)
|
||||
version := int(packet[0] >> 4)
|
||||
switch version {
|
||||
case ipv4.Version:
|
||||
if len(packet) < ipv4.HeaderLen {
|
||||
return nil
|
||||
}
|
||||
// Do not send reject packets for non-first fragments
|
||||
if packet[6]&0x1f != 0 || packet[7] != 0 {
|
||||
return nil
|
||||
}
|
||||
switch packet[9] {
|
||||
case 6: // tcp
|
||||
return ipv4CreateRejectTCPPacket(packet, out)
|
||||
default:
|
||||
return ipv4CreateRejectICMPPacket(packet, out)
|
||||
}
|
||||
case ipv6.Version:
|
||||
if len(packet) < ipv6.HeaderLen {
|
||||
return nil
|
||||
}
|
||||
return ipv6CreateRejectPacket(packet, out)
|
||||
default:
|
||||
return ipv4CreateRejectICMPPacket(packet, out)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +63,17 @@ func ipv4CreateRejectICMPPacket(packet []byte, out []byte) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ICMP reply includes original header and first 8 bytes of the packet
|
||||
packetLen := len(packet)
|
||||
if packetLen > ihl+8 {
|
||||
packetLen = ihl + 8
|
||||
// Do not generate ICMP errors in response to ICMP error packets
|
||||
if packet[9] == 1 && len(packet) > ihl {
|
||||
icmpType := packet[ihl]
|
||||
if icmpType == 3 || icmpType == 4 || icmpType == 5 || icmpType == 11 || icmpType == 12 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ICMP reply includes original header and first 8 bytes of the packet
|
||||
packetLen := min(len(packet), ihl+8)
|
||||
|
||||
outLen := ipv4.HeaderLen + 8 + packetLen
|
||||
if outLen > cap(out) {
|
||||
return nil
|
||||
@@ -71,14 +104,14 @@ func ipv4CreateRejectICMPPacket(packet []byte, out []byte) []byte {
|
||||
|
||||
// ICMP Destination Unreachable
|
||||
icmpOut := out[ipv4.HeaderLen:]
|
||||
icmpOut[0] = 3 // type (Destination unreachable)
|
||||
icmpOut[1] = 3 // code (Port unreachable error)
|
||||
icmpOut[2] = 0 // checksum
|
||||
icmpOut[3] = 0 // .
|
||||
icmpOut[4] = 0 // unused
|
||||
icmpOut[5] = 0 // .
|
||||
icmpOut[6] = 0 // .
|
||||
icmpOut[7] = 0 // .
|
||||
icmpOut[0] = 3 // type (Destination unreachable)
|
||||
icmpOut[1] = 13 // code (Communication administratively prohibited)
|
||||
icmpOut[2] = 0 // checksum
|
||||
icmpOut[3] = 0 // .
|
||||
icmpOut[4] = 0 // unused
|
||||
icmpOut[5] = 0 // .
|
||||
icmpOut[6] = 0 // .
|
||||
icmpOut[7] = 0 // .
|
||||
|
||||
// Copy original IP header and first 8 bytes as body
|
||||
copy(icmpOut[8:], packet[:packetLen])
|
||||
@@ -165,7 +198,193 @@ func ipv4CreateRejectTCPPacket(packet []byte, out []byte) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
func ipv6CreateRejectPacket(packet []byte, out []byte) []byte {
|
||||
proto, offset, isFragment := ipv6FindUpperProtocol(packet)
|
||||
if isFragment {
|
||||
return nil
|
||||
}
|
||||
switch proto {
|
||||
case 6: // tcp
|
||||
return ipv6CreateRejectTCPPacket(packet, out, offset)
|
||||
default:
|
||||
return ipv6CreateRejectICMPPacket(packet, out, proto, offset)
|
||||
}
|
||||
}
|
||||
|
||||
func ipv6CreateRejectICMPPacket(packet []byte, out []byte, proto uint8, offset int) []byte {
|
||||
// Do not generate ICMPv6 errors in response to ICMPv6 error packets
|
||||
if proto == 58 && len(packet) > offset {
|
||||
icmpType := packet[offset]
|
||||
if icmpType >= 1 && icmpType <= 4 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Include as much of the original packet as possible, up to 1000 bytes,
|
||||
// so the response fits comfortably within any tunnel MTU.
|
||||
packetLen := min(len(packet), 1000)
|
||||
|
||||
outLen := ipv6.HeaderLen + 8 + packetLen
|
||||
if outLen > cap(out) {
|
||||
return nil
|
||||
}
|
||||
|
||||
out = out[:outLen]
|
||||
|
||||
// IPv6 header
|
||||
ipHdr := out[0:ipv6.HeaderLen]
|
||||
ipHdr[0] = ipv6.Version << 4 // version, traffic class (high bits)
|
||||
ipHdr[1] = 0 // traffic class (low bits), flow label (high bits)
|
||||
ipHdr[2] = 0 // flow label
|
||||
ipHdr[3] = 0 // flow label
|
||||
|
||||
payloadLen := uint16(outLen - ipv6.HeaderLen)
|
||||
binary.BigEndian.PutUint16(ipHdr[4:], payloadLen) // payload length
|
||||
ipHdr[6] = 58 // next header (ICMPv6)
|
||||
ipHdr[7] = 64 // hop limit
|
||||
|
||||
// Swap dest / src IPs (each 16 bytes, src at 8, dst at 24)
|
||||
copy(ipHdr[8:24], packet[24:40])
|
||||
copy(ipHdr[24:40], packet[8:24])
|
||||
|
||||
// ICMPv6 Destination Unreachable
|
||||
icmpOut := out[ipv6.HeaderLen:]
|
||||
icmpOut[0] = 1 // type (Destination Unreachable)
|
||||
icmpOut[1] = 1 // code (Communication with destination administratively prohibited)
|
||||
icmpOut[2] = 0 // checksum
|
||||
icmpOut[3] = 0 // .
|
||||
icmpOut[4] = 0 // unused
|
||||
icmpOut[5] = 0 // .
|
||||
icmpOut[6] = 0 // .
|
||||
icmpOut[7] = 0 // .
|
||||
|
||||
copy(icmpOut[8:], packet[:packetLen])
|
||||
|
||||
// ICMPv6 checksum uses a pseudo-header
|
||||
csum := ipv6PseudoheaderChecksum(ipHdr[8:24], ipHdr[24:40], 58, uint32(payloadLen))
|
||||
binary.BigEndian.PutUint16(icmpOut[2:], tcpipChecksum(icmpOut, csum))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func ipv6CreateRejectTCPPacket(packet []byte, out []byte, offset int) []byte {
|
||||
const tcpLen = 20
|
||||
|
||||
if len(packet) < offset+tcpLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
outLen := ipv6.HeaderLen + tcpLen
|
||||
if outLen > cap(out) {
|
||||
return nil
|
||||
}
|
||||
|
||||
out = out[:outLen]
|
||||
|
||||
// IPv6 header
|
||||
ipHdr := out[0:ipv6.HeaderLen]
|
||||
ipHdr[0] = ipv6.Version << 4 // version, traffic class (high bits)
|
||||
ipHdr[1] = 0 // traffic class (low bits), flow label (high bits)
|
||||
ipHdr[2] = 0 // flow label
|
||||
ipHdr[3] = 0 // flow label
|
||||
|
||||
binary.BigEndian.PutUint16(ipHdr[4:], tcpLen) // payload length
|
||||
ipHdr[6] = 6 // next header (TCP)
|
||||
ipHdr[7] = 64 // hop limit
|
||||
|
||||
// Swap dest / src IPs
|
||||
copy(ipHdr[8:24], packet[24:40])
|
||||
copy(ipHdr[24:40], packet[8:24])
|
||||
|
||||
// TCP RST
|
||||
tcpIn := packet[offset:]
|
||||
var ackSeq, seq uint32
|
||||
outFlags := byte(0b00000100) // RST
|
||||
|
||||
inAck := tcpIn[13]&0b00010000 != 0
|
||||
if inAck {
|
||||
seq = binary.BigEndian.Uint32(tcpIn[8:])
|
||||
} else {
|
||||
inSyn := uint32((tcpIn[13] & 0b00000010) >> 1)
|
||||
inFin := uint32(tcpIn[13] & 0b00000001)
|
||||
ackSeq = binary.BigEndian.Uint32(tcpIn[4:]) + inSyn + inFin + uint32(len(tcpIn)) - uint32(tcpIn[12]>>4)<<2
|
||||
outFlags |= 0b00010000 // ACK
|
||||
}
|
||||
|
||||
tcpOut := out[ipv6.HeaderLen:]
|
||||
// Swap dest / src ports
|
||||
copy(tcpOut[0:2], tcpIn[2:4])
|
||||
copy(tcpOut[2:4], tcpIn[0:2])
|
||||
binary.BigEndian.PutUint32(tcpOut[4:], seq)
|
||||
binary.BigEndian.PutUint32(tcpOut[8:], ackSeq)
|
||||
tcpOut[12] = (tcpLen >> 2) << 4 // data offset, reserved, NS
|
||||
tcpOut[13] = outFlags // CWR, ECE, URG, ACK, PSH, RST, SYN, FIN
|
||||
tcpOut[14] = 0 // window size
|
||||
tcpOut[15] = 0 // .
|
||||
tcpOut[16] = 0 // checksum
|
||||
tcpOut[17] = 0 // .
|
||||
tcpOut[18] = 0 // URG Pointer
|
||||
tcpOut[19] = 0 // .
|
||||
|
||||
// Calculate checksum with IPv6 pseudo-header
|
||||
csum := ipv6PseudoheaderChecksum(ipHdr[8:24], ipHdr[24:40], 6, tcpLen)
|
||||
binary.BigEndian.PutUint16(tcpOut[16:], tcpipChecksum(tcpOut, csum))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func ipv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragment bool) {
|
||||
nextHeader = packet[6]
|
||||
offset = ipv6.HeaderLen
|
||||
|
||||
for {
|
||||
switch nextHeader {
|
||||
case 0, 43, 60: // Hop-by-Hop, Routing, Destination
|
||||
if len(packet) < offset+2 {
|
||||
return nextHeader, offset, isFragment
|
||||
}
|
||||
nextHeader = packet[offset]
|
||||
offset += (int(packet[offset+1]) + 1) << 3
|
||||
|
||||
case 44: // Fragment
|
||||
if len(packet) < offset+8 {
|
||||
return nextHeader, offset, isFragment
|
||||
}
|
||||
if packet[offset+2] != 0 || packet[offset+3]&0xf8 != 0 {
|
||||
isFragment = true
|
||||
}
|
||||
nextHeader = packet[offset]
|
||||
offset += 8
|
||||
|
||||
case 51: // AH
|
||||
if len(packet) < offset+2 {
|
||||
return nextHeader, offset, isFragment
|
||||
}
|
||||
nextHeader = packet[offset]
|
||||
offset += (int(packet[offset+1]) + 2) << 2
|
||||
|
||||
default:
|
||||
return nextHeader, offset, isFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CreateICMPEchoResponse(packet, out []byte) []byte {
|
||||
if len(packet) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch packet[0] >> 4 {
|
||||
case 4:
|
||||
return createICMPv4EchoResponse(packet, out)
|
||||
case 6:
|
||||
return createICMPv6EchoResponse(packet, out)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createICMPv4EchoResponse(packet, out []byte) []byte {
|
||||
// Return early if this is not a simple ICMP Echo Request
|
||||
//TODO: make constants out of these
|
||||
if !(len(packet) >= 28 && len(packet) <= 9001 && packet[0] == 0x45 && packet[9] == 0x01 && packet[20] == 0x08) {
|
||||
@@ -199,6 +418,43 @@ func CreateICMPEchoResponse(packet, out []byte) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
func createICMPv6EchoResponse(packet, out []byte) []byte {
|
||||
// IPv6 header (40 bytes) + ICMPv6 header (8 bytes minimum)
|
||||
if len(packet) < ipv6.HeaderLen+8 || len(packet) > 9001 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next Header must be ICMPv6 (58)
|
||||
if packet[6] != 58 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ICMPv6 type must be Echo Request (128)
|
||||
if packet[ipv6.HeaderLen] != 128 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out = out[:len(packet)]
|
||||
copy(out, packet)
|
||||
|
||||
// Swap src/dst addresses (bytes 8-23 and 24-39)
|
||||
copy(out[8:24], packet[24:40])
|
||||
copy(out[24:40], packet[8:24])
|
||||
|
||||
// Change ICMPv6 type to Echo Reply (129)
|
||||
icmp := out[ipv6.HeaderLen:]
|
||||
icmp[0] = 129
|
||||
icmp[2] = 0
|
||||
icmp[3] = 0
|
||||
|
||||
// ICMPv6 checksum uses a pseudo-header with src, dst, length, and next header
|
||||
payloadLen := uint32(len(icmp))
|
||||
csum := ipv6PseudoheaderChecksum(out[8:24], out[24:40], 58, payloadLen)
|
||||
binary.BigEndian.PutUint16(icmp[2:], tcpipChecksum(icmp, csum))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// calculates the TCP/IP checksum defined in rfc1071. The passed-in
|
||||
// csum is any initial checksum data that's already been computed.
|
||||
//
|
||||
@@ -236,3 +492,18 @@ func ipv4PseudoheaderChecksum(src, dst []byte, proto, length uint32) (csum uint3
|
||||
csum += length >> 16
|
||||
return csum
|
||||
}
|
||||
|
||||
// based on:
|
||||
// - https://github.com/google/gopacket/blob/v1.1.19/layers/tcpip.go#L37-L48
|
||||
func ipv6PseudoheaderChecksum(src, dst []byte, proto, length uint32) (csum uint32) {
|
||||
for i := 0; i < 16; i += 2 {
|
||||
csum += uint32(src[i]) << 8
|
||||
csum += uint32(src[i+1])
|
||||
csum += uint32(dst[i]) << 8
|
||||
csum += uint32(dst[i+1])
|
||||
}
|
||||
csum += proto
|
||||
csum += length & 0xffff
|
||||
csum += length >> 16
|
||||
return csum
|
||||
}
|
||||
|
||||
+404
-1
@@ -1,11 +1,13 @@
|
||||
package iputil
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
func Test_CreateRejectPacket(t *testing.T) {
|
||||
@@ -43,7 +45,7 @@ func Test_CreateRejectPacket(t *testing.T) {
|
||||
}
|
||||
b = append(b, []byte{0, 3, 0, 4, 0, 0, 0, 0}...)
|
||||
|
||||
expectedLen = MaxRejectPacketSize
|
||||
expectedLen = maxIPv4RejectPacketSize
|
||||
out = make([]byte, MaxRejectPacketSize)
|
||||
rejectPacket = CreateRejectPacket(b, out)
|
||||
assert.NotNil(t, rejectPacket)
|
||||
@@ -71,3 +73,404 @@ func Test_CreateRejectPacket(t *testing.T) {
|
||||
assert.NotNil(t, rejectPacket)
|
||||
assert.Len(t, rejectPacket, expectedLen)
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacket_NoFragment(t *testing.T) {
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
|
||||
// IPv4: non-zero fragment offset should not generate reject packet
|
||||
h := ipv4.Header{
|
||||
Len: 20,
|
||||
Src: net.IPv4(10, 0, 0, 1),
|
||||
Dst: net.IPv4(10, 0, 0, 2),
|
||||
Protocol: 17, // UDP
|
||||
}
|
||||
b, err := h.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("h.Marshal: %v", err)
|
||||
}
|
||||
b = append(b, make([]byte, 8)...)
|
||||
// Set fragment offset to non-zero (byte 6-7, offset in 8-byte units)
|
||||
b[6] = 0x00
|
||||
b[7] = 0x01
|
||||
assert.Nil(t, CreateRejectPacket(b, out))
|
||||
|
||||
// MF flag with zero offset (first fragment) should still generate reject
|
||||
b[6] = 0x20 // MF flag set
|
||||
b[7] = 0x00
|
||||
assert.NotNil(t, CreateRejectPacket(b, out))
|
||||
|
||||
// Non-fragment should still generate reject packet
|
||||
b[6] = 0x00
|
||||
b[7] = 0x00
|
||||
assert.NotNil(t, CreateRejectPacket(b, out))
|
||||
|
||||
// DF flag only (not a fragment) should still generate reject packet
|
||||
b[6] = 0x40
|
||||
b[7] = 0x00
|
||||
assert.NotNil(t, CreateRejectPacket(b, out))
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_NoFragment(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1")
|
||||
dst := net.ParseIP("fd00::2")
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
|
||||
// IPv6 with Fragment header and non-zero offset should not generate reject
|
||||
fragHeader := []byte{
|
||||
17, // next header: UDP
|
||||
0, // reserved
|
||||
0, 9, // fragment offset=1 (shifted left 3), M=1
|
||||
0, 0, 0, 1, // identification
|
||||
}
|
||||
udpPayload := make([]byte, 8)
|
||||
payload := append(fragHeader, udpPayload...)
|
||||
packet := makeIPv6Packet(src, dst, 44, payload) // next header 44 = Fragment
|
||||
assert.Nil(t, CreateRejectPacket(packet, out))
|
||||
|
||||
// Fragment header with zero offset (first fragment) should still generate reject
|
||||
fragHeader[2] = 0
|
||||
fragHeader[3] = 1 // offset=0, M=1
|
||||
payload = append(fragHeader, udpPayload...)
|
||||
packet = makeIPv6Packet(src, dst, 44, payload)
|
||||
assert.NotNil(t, CreateRejectPacket(packet, out))
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacket_NoICMPError(t *testing.T) {
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
|
||||
// ICMP error types should not generate reject packets
|
||||
icmpErrorTypes := []byte{3, 4, 5, 11, 12}
|
||||
for _, icmpType := range icmpErrorTypes {
|
||||
h := ipv4.Header{
|
||||
Len: 20,
|
||||
Src: net.IPv4(10, 0, 0, 1),
|
||||
Dst: net.IPv4(10, 0, 0, 2),
|
||||
Protocol: 1, // ICMP
|
||||
}
|
||||
|
||||
b, err := h.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("h.Marshal: %v", err)
|
||||
}
|
||||
b = append(b, icmpType, 0, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
rejectPacket := CreateRejectPacket(b, out)
|
||||
assert.Nil(t, rejectPacket, "ICMP type %d should not generate a reject packet", icmpType)
|
||||
}
|
||||
|
||||
// ICMP non-error types should still generate reject packets
|
||||
icmpNonErrorTypes := []byte{0, 8, 13, 14}
|
||||
for _, icmpType := range icmpNonErrorTypes {
|
||||
h := ipv4.Header{
|
||||
Len: 20,
|
||||
Src: net.IPv4(10, 0, 0, 1),
|
||||
Dst: net.IPv4(10, 0, 0, 2),
|
||||
Protocol: 1, // ICMP
|
||||
}
|
||||
|
||||
b, err := h.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("h.Marshal: %v", err)
|
||||
}
|
||||
b = append(b, icmpType, 0, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
rejectPacket := CreateRejectPacket(b, out)
|
||||
assert.NotNil(t, rejectPacket, "ICMP type %d should generate a reject packet", icmpType)
|
||||
}
|
||||
}
|
||||
|
||||
func makeIPv6Packet(src, dst net.IP, nextHeader uint8, payload []byte) []byte {
|
||||
b := make([]byte, ipv6.HeaderLen+len(payload))
|
||||
b[0] = ipv6.Version << 4
|
||||
binary.BigEndian.PutUint16(b[4:], uint16(len(payload)))
|
||||
b[6] = nextHeader
|
||||
b[7] = 64
|
||||
copy(b[8:24], src.To16())
|
||||
copy(b[24:40], dst.To16())
|
||||
copy(b[ipv6.HeaderLen:], payload)
|
||||
return b
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_ICMP(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1")
|
||||
dst := net.ParseIP("fd00::2")
|
||||
|
||||
// Small UDP packet: entire original included in body
|
||||
udpPayload := make([]byte, 20)
|
||||
udpPayload[0] = 0x00 // src port high
|
||||
udpPayload[1] = 0x50 // src port low (80)
|
||||
udpPayload[2] = 0x01 // dst port high
|
||||
udpPayload[3] = 0xBB // dst port low (443)
|
||||
packet := makeIPv6Packet(src, dst, 17, udpPayload)
|
||||
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
rejectPacket := CreateRejectPacket(packet, out)
|
||||
assert.NotNil(t, rejectPacket)
|
||||
|
||||
// Small packet fits entirely: 40 (ipv6 hdr) + 8 (icmpv6 hdr) + 60 (original)
|
||||
expectedLen := ipv6.HeaderLen + 8 + len(packet)
|
||||
assert.Len(t, rejectPacket, expectedLen)
|
||||
|
||||
// Verify version
|
||||
assert.Equal(t, byte(ipv6.Version<<4), rejectPacket[0]&0xf0)
|
||||
// Verify next header is ICMPv6 (58)
|
||||
assert.Equal(t, byte(58), rejectPacket[6])
|
||||
// Verify src/dst are swapped
|
||||
assert.Equal(t, dst.To16(), net.IP(rejectPacket[8:24]))
|
||||
assert.Equal(t, src.To16(), net.IP(rejectPacket[24:40]))
|
||||
// Verify ICMPv6 type=1 (Dest Unreachable), code=1 (Administratively prohibited)
|
||||
assert.Equal(t, byte(1), rejectPacket[ipv6.HeaderLen])
|
||||
assert.Equal(t, byte(1), rejectPacket[ipv6.HeaderLen+1])
|
||||
// Verify entire original packet is included in body
|
||||
assert.Equal(t, packet, rejectPacket[ipv6.HeaderLen+8:])
|
||||
|
||||
// Large packet: body is truncated to 1000 bytes
|
||||
largePkt := makeIPv6Packet(src, dst, 17, make([]byte, 1200))
|
||||
rejectPacket = CreateRejectPacket(largePkt, out)
|
||||
assert.NotNil(t, rejectPacket)
|
||||
assert.Len(t, rejectPacket, ipv6.HeaderLen+8+1000)
|
||||
assert.Equal(t, largePkt[:1000], rejectPacket[ipv6.HeaderLen+8:])
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_TCP(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1")
|
||||
dst := net.ParseIP("fd00::2")
|
||||
|
||||
// TCP SYN packet (next header 6)
|
||||
tcpPayload := make([]byte, 20)
|
||||
tcpPayload[0] = 0x00 // src port high
|
||||
tcpPayload[1] = 0x50 // src port low (80)
|
||||
tcpPayload[2] = 0x01 // dst port high
|
||||
tcpPayload[3] = 0xBB // dst port low (443)
|
||||
binary.BigEndian.PutUint32(tcpPayload[4:], 1000) // seq
|
||||
binary.BigEndian.PutUint32(tcpPayload[8:], 0) // ack seq
|
||||
tcpPayload[12] = (20 >> 2) << 4 // data offset
|
||||
tcpPayload[13] = 0b00000010 // SYN flag
|
||||
|
||||
packet := makeIPv6Packet(src, dst, 6, tcpPayload)
|
||||
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
rejectPacket := CreateRejectPacket(packet, out)
|
||||
assert.NotNil(t, rejectPacket)
|
||||
|
||||
// Expected: 40 (ipv6 hdr) + 20 (tcp RST)
|
||||
expectedLen := ipv6.HeaderLen + 20
|
||||
assert.Len(t, rejectPacket, expectedLen)
|
||||
|
||||
// Verify version
|
||||
assert.Equal(t, byte(ipv6.Version<<4), rejectPacket[0]&0xf0)
|
||||
// Verify next header is TCP (6)
|
||||
assert.Equal(t, byte(6), rejectPacket[6])
|
||||
// Verify src/dst are swapped
|
||||
assert.Equal(t, dst.To16(), net.IP(rejectPacket[8:24]))
|
||||
assert.Equal(t, src.To16(), net.IP(rejectPacket[24:40]))
|
||||
// Verify ports are swapped
|
||||
tcpOut := rejectPacket[ipv6.HeaderLen:]
|
||||
assert.Equal(t, uint16(443), binary.BigEndian.Uint16(tcpOut[0:2]))
|
||||
assert.Equal(t, uint16(80), binary.BigEndian.Uint16(tcpOut[2:4]))
|
||||
// RST+ACK flags (since input was SYN without ACK)
|
||||
assert.Equal(t, byte(0b00010100), tcpOut[13])
|
||||
// ack_seq = original seq (1000) + SYN (1) + FIN (0) + segment data (0)
|
||||
assert.Equal(t, uint32(1001), binary.BigEndian.Uint32(tcpOut[8:]))
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_TCPWithACK(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1")
|
||||
dst := net.ParseIP("fd00::2")
|
||||
|
||||
// TCP packet with ACK set
|
||||
tcpPayload := make([]byte, 20)
|
||||
tcpPayload[0] = 0x00
|
||||
tcpPayload[1] = 0x50
|
||||
tcpPayload[2] = 0x01
|
||||
tcpPayload[3] = 0xBB
|
||||
binary.BigEndian.PutUint32(tcpPayload[4:], 1000) // seq
|
||||
binary.BigEndian.PutUint32(tcpPayload[8:], 2000) // ack seq
|
||||
tcpPayload[12] = (20 >> 2) << 4 // data offset
|
||||
tcpPayload[13] = 0b00010000 // ACK flag
|
||||
|
||||
packet := makeIPv6Packet(src, dst, 6, tcpPayload)
|
||||
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
rejectPacket := CreateRejectPacket(packet, out)
|
||||
assert.NotNil(t, rejectPacket)
|
||||
|
||||
tcpOut := rejectPacket[ipv6.HeaderLen:]
|
||||
// RST only (no ACK) since input had ACK
|
||||
assert.Equal(t, byte(0b00000100), tcpOut[13])
|
||||
// seq = original ack_seq
|
||||
assert.Equal(t, uint32(2000), binary.BigEndian.Uint32(tcpOut[4:]))
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_NoICMPError(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1")
|
||||
dst := net.ParseIP("fd00::2")
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
|
||||
// ICMPv6 error types (1-4) should not generate reject packets
|
||||
for icmpType := byte(1); icmpType <= 4; icmpType++ {
|
||||
payload := make([]byte, 8)
|
||||
payload[0] = icmpType
|
||||
packet := makeIPv6Packet(src, dst, 58, payload)
|
||||
|
||||
rejectPacket := CreateRejectPacket(packet, out)
|
||||
assert.Nil(t, rejectPacket, "ICMPv6 type %d should not generate a reject packet", icmpType)
|
||||
}
|
||||
|
||||
// ICMPv6 non-error types should still generate reject packets
|
||||
nonErrorTypes := []byte{128, 129, 133, 134}
|
||||
for _, icmpType := range nonErrorTypes {
|
||||
payload := make([]byte, 8)
|
||||
payload[0] = icmpType
|
||||
packet := makeIPv6Packet(src, dst, 58, payload)
|
||||
|
||||
rejectPacket := CreateRejectPacket(packet, out)
|
||||
assert.NotNil(t, rejectPacket, "ICMPv6 type %d should generate a reject packet", icmpType)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_TooShort(t *testing.T) {
|
||||
// Packet too short to be valid IPv6
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
assert.Nil(t, CreateRejectPacket([]byte{0x60}, out))
|
||||
assert.Nil(t, CreateRejectPacket(make([]byte, 39), out))
|
||||
}
|
||||
|
||||
func Test_CreateRejectPacketIPv6_ExtensionHeaders(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1")
|
||||
dst := net.ParseIP("fd00::2")
|
||||
|
||||
// IPv6 + Hop-by-Hop extension header + TCP
|
||||
hopByHop := []byte{
|
||||
6, // next header: TCP
|
||||
0, // length (8 bytes total)
|
||||
0, 0, // padding
|
||||
0, 0, 0, 0,
|
||||
}
|
||||
tcpPayload := make([]byte, 20)
|
||||
tcpPayload[0] = 0x00
|
||||
tcpPayload[1] = 0x50
|
||||
tcpPayload[2] = 0x01
|
||||
tcpPayload[3] = 0xBB
|
||||
binary.BigEndian.PutUint32(tcpPayload[4:], 1000)
|
||||
binary.BigEndian.PutUint32(tcpPayload[8:], 2000)
|
||||
tcpPayload[12] = (20 >> 2) << 4
|
||||
tcpPayload[13] = 0b00010000 // ACK
|
||||
|
||||
payload := append(hopByHop, tcpPayload...)
|
||||
packet := makeIPv6Packet(src, dst, 0, payload) // next header 0 = Hop-by-Hop
|
||||
|
||||
out := make([]byte, MaxRejectPacketSize)
|
||||
rejectPacket := CreateRejectPacket(packet, out)
|
||||
assert.NotNil(t, rejectPacket)
|
||||
|
||||
// Should produce TCP RST
|
||||
expectedLen := ipv6.HeaderLen + 20
|
||||
assert.Len(t, rejectPacket, expectedLen)
|
||||
assert.Equal(t, byte(6), rejectPacket[6]) // next header is TCP
|
||||
tcpOut := rejectPacket[ipv6.HeaderLen:]
|
||||
assert.Equal(t, byte(0b00000100), tcpOut[13]) // RST only
|
||||
}
|
||||
|
||||
func TestCreateICMPEchoResponse_IPv4(t *testing.T) {
|
||||
// Build a simple IPv4 ICMP Echo Request
|
||||
packet := make([]byte, 28)
|
||||
packet[0] = 0x45 // version 4, IHL 5
|
||||
binary.BigEndian.PutUint16(packet[2:], uint16(28)) // total length
|
||||
packet[8] = 64 // TTL
|
||||
packet[9] = 1 // protocol ICMP
|
||||
copy(packet[12:16], net.IPv4(10, 0, 0, 1).To4()) // src
|
||||
copy(packet[16:20], net.IPv4(10, 0, 0, 2).To4()) // dst
|
||||
packet[20] = 8 // ICMP Echo Request
|
||||
|
||||
out := make([]byte, len(packet))
|
||||
result := CreateICMPEchoResponse(packet, out)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, byte(0x45), result[0])
|
||||
// src/dst swapped
|
||||
assert.Equal(t, net.IPv4(10, 0, 0, 2).To4(), net.IP(result[12:16]))
|
||||
assert.Equal(t, net.IPv4(10, 0, 0, 1).To4(), net.IP(result[16:20]))
|
||||
// ICMP Echo Reply
|
||||
assert.Equal(t, byte(0), result[20])
|
||||
}
|
||||
|
||||
func TestCreateICMPEchoResponse_IPv6(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1").To16()
|
||||
dst := net.ParseIP("fd00::2").To16()
|
||||
|
||||
// Build an IPv6 ICMPv6 Echo Request packet
|
||||
// IPv6 header (40 bytes) + ICMPv6 (8 bytes)
|
||||
packet := make([]byte, 48)
|
||||
packet[0] = 0x60 // version 6
|
||||
payloadLen := uint16(8) // ICMPv6 header only
|
||||
binary.BigEndian.PutUint16(packet[4:], payloadLen)
|
||||
packet[6] = 58 // Next Header: ICMPv6
|
||||
packet[7] = 64 // Hop Limit
|
||||
copy(packet[8:24], src) // src address
|
||||
copy(packet[24:40], dst) // dst address
|
||||
|
||||
// ICMPv6 Echo Request
|
||||
icmp := packet[40:]
|
||||
icmp[0] = 128 // type: Echo Request
|
||||
icmp[1] = 0 // code
|
||||
binary.BigEndian.PutUint16(icmp[4:], 1) // identifier
|
||||
binary.BigEndian.PutUint16(icmp[6:], 1) // sequence number
|
||||
|
||||
// Compute correct checksum for the request
|
||||
csum := ipv6PseudoheaderChecksum(src, dst, 58, uint32(payloadLen))
|
||||
binary.BigEndian.PutUint16(icmp[2:], tcpipChecksum(icmp, csum))
|
||||
|
||||
out := make([]byte, len(packet))
|
||||
result := CreateICMPEchoResponse(packet, out)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Version should still be 6
|
||||
assert.Equal(t, byte(6), result[0]>>4)
|
||||
// src/dst swapped
|
||||
assert.Equal(t, dst, net.IP(result[8:24]))
|
||||
assert.Equal(t, src, net.IP(result[24:40]))
|
||||
// ICMPv6 Echo Reply type
|
||||
assert.Equal(t, byte(129), result[40])
|
||||
|
||||
// Verify checksum is valid (tcpipChecksum returns 0 when data+checksum is correct)
|
||||
respIcmp := result[40:]
|
||||
verifyCsum := ipv6PseudoheaderChecksum(result[8:24], result[24:40], 58, uint32(payloadLen))
|
||||
assert.Equal(t, uint16(0), tcpipChecksum(respIcmp, verifyCsum))
|
||||
}
|
||||
|
||||
func TestCreateICMPEchoResponse_IPv6_NotEchoRequest(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1").To16()
|
||||
dst := net.ParseIP("fd00::2").To16()
|
||||
|
||||
packet := make([]byte, 48)
|
||||
packet[0] = 0x60
|
||||
binary.BigEndian.PutUint16(packet[4:], 8)
|
||||
packet[6] = 58
|
||||
packet[7] = 64
|
||||
copy(packet[8:24], src)
|
||||
copy(packet[24:40], dst)
|
||||
|
||||
// ICMPv6 type 1 (Destination Unreachable) - not Echo Request
|
||||
packet[40] = 1
|
||||
|
||||
out := make([]byte, len(packet))
|
||||
result := CreateICMPEchoResponse(packet, out)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestCreateICMPEchoResponse_IPv6_NotICMPv6(t *testing.T) {
|
||||
src := net.ParseIP("fd00::1").To16()
|
||||
dst := net.ParseIP("fd00::2").To16()
|
||||
|
||||
packet := make([]byte, 48)
|
||||
packet[0] = 0x60
|
||||
binary.BigEndian.PutUint16(packet[4:], 8)
|
||||
packet[6] = 6 // TCP, not ICMPv6
|
||||
packet[7] = 64
|
||||
copy(packet[8:24], src)
|
||||
copy(packet[24:40], dst)
|
||||
|
||||
out := make([]byte, len(packet))
|
||||
result := CreateICMPEchoResponse(packet, out)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
+31
-6
@@ -272,16 +272,18 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
|
||||
//NOTE: many things will get much simpler when we combine static_host_map and lighthouse.hosts in config
|
||||
if initial || c.HasChanged("static_host_map") || c.HasChanged("static_map.cadence") || c.HasChanged("static_map.network") || c.HasChanged("static_map.lookup_timeout") {
|
||||
// Clean up. Entries still in the static_host_map will be re-built.
|
||||
// Entries no longer present must have their (possible) background DNS goroutines stopped.
|
||||
if existingStaticList := lh.staticList.Load(); existingStaticList != nil {
|
||||
ourselves := lh.myVpnNetworks[0].Addr()
|
||||
oldStaticList := lh.staticList.Load()
|
||||
if oldStaticList != nil {
|
||||
lh.RLock()
|
||||
for staticVpnAddr := range *existingStaticList {
|
||||
for staticVpnAddr := range *oldStaticList {
|
||||
if am, ok := lh.addrMap[staticVpnAddr]; ok && am != nil {
|
||||
am.hr.Cancel()
|
||||
am.ResetForOwner(ourselves)
|
||||
}
|
||||
}
|
||||
lh.RUnlock()
|
||||
}
|
||||
|
||||
// Build a new list based on current config.
|
||||
staticList := make(map[netip.Addr]struct{})
|
||||
err := lh.loadStaticMap(c, staticList)
|
||||
@@ -289,6 +291,21 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// For entries removed from static_host_map, stop the DNS goroutine and drop the cached addrs.
|
||||
// All addrs must come from the lighthouses now that it's no longer a static host.
|
||||
if oldStaticList != nil {
|
||||
lh.RLock()
|
||||
for staticVpnAddr := range *oldStaticList {
|
||||
if _, stillStatic := staticList[staticVpnAddr]; stillStatic {
|
||||
continue
|
||||
}
|
||||
if am, ok := lh.addrMap[staticVpnAddr]; ok && am != nil {
|
||||
am.ClearHostnameResults()
|
||||
}
|
||||
}
|
||||
lh.RUnlock()
|
||||
}
|
||||
|
||||
lh.staticList.Store(&staticList)
|
||||
if !initial {
|
||||
if c.HasChanged("static_host_map") {
|
||||
@@ -1401,6 +1418,9 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
|
||||
|
||||
remoteAllowList := lhh.lh.GetRemoteAllowList()
|
||||
for _, a := range n.Details.V4AddrPorts {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
b := protoV4AddrPortToNetAddrPort(a)
|
||||
if remoteAllowList.Allow(detailsVpnAddr, b.Addr()) {
|
||||
lhh.lh.punchy.Schedule(b, detailsVpnAddr)
|
||||
@@ -1408,6 +1428,9 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
|
||||
}
|
||||
|
||||
for _, a := range n.Details.V6AddrPorts {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
b := protoV6AddrPortToNetAddrPort(a)
|
||||
if remoteAllowList.Allow(detailsVpnAddr, b.Addr()) {
|
||||
lhh.lh.punchy.Schedule(b, detailsVpnAddr)
|
||||
@@ -1437,7 +1460,7 @@ func protoV6AddrPortToNetAddrPort(ap *V6AddrPort) netip.AddrPort {
|
||||
b := [16]byte{}
|
||||
binary.BigEndian.PutUint64(b[:8], ap.Hi)
|
||||
binary.BigEndian.PutUint64(b[8:], ap.Lo)
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(b), uint16(ap.Port))
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(b).Unmap(), uint16(ap.Port))
|
||||
}
|
||||
|
||||
func netAddrToProtoAddr(addr netip.Addr) *Addr {
|
||||
@@ -1477,7 +1500,9 @@ func (d *NebulaMetaDetails) GetRelays() []netip.Addr {
|
||||
|
||||
if len(d.RelayVpnAddrs) > 0 {
|
||||
for _, r := range d.RelayVpnAddrs {
|
||||
relays = append(relays, protoAddrToNetAddr(r))
|
||||
if r != nil {
|
||||
relays = append(relays, protoAddrToNetAddr(r))
|
||||
}
|
||||
}
|
||||
}
|
||||
return relays
|
||||
|
||||
@@ -303,6 +303,132 @@ func TestLighthouse_reload(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestLighthouse_reloadStaticHostMap verifies that reloading static_host_map applies the new
|
||||
// config rather than appending to it. See issue #718.
|
||||
func TestLighthouse_reloadStaticHostMap(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
c := config.NewC(l)
|
||||
c.Settings["lighthouse"] = map[string]any{"am_lighthouse": true}
|
||||
c.Settings["listen"] = map[string]any{"port": 4242}
|
||||
c.Settings["static_host_map"] = map[string]any{
|
||||
"10.128.0.2": []any{"1.1.1.1:4242"},
|
||||
}
|
||||
|
||||
myVpnNet := netip.MustParsePrefix("10.128.0.1/24")
|
||||
nt := new(bart.Lite)
|
||||
nt.Insert(myVpnNet)
|
||||
cs := &CertState{
|
||||
myVpnNetworks: []netip.Prefix{myVpnNet},
|
||||
myVpnNetworksTable: nt,
|
||||
}
|
||||
|
||||
lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
staticHost := netip.MustParseAddr("10.128.0.2")
|
||||
otherHost := netip.MustParseAddr("10.128.0.3")
|
||||
|
||||
// Capture the RemoteList pointer up front; an in-flight handshake would hold the same one
|
||||
// on hostinfo.remotes, so it must reflect every reload below.
|
||||
pinned := lh.Query(staticHost)
|
||||
require.NotNil(t, pinned)
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:4242")}, pinned.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
// Replace the remote address. The new address should be the only entry.
|
||||
nc := map[string]any{
|
||||
"static_host_map": map[string]any{
|
||||
"10.128.0.2": []any{"2.2.2.2:4242"},
|
||||
},
|
||||
}
|
||||
rc, err := yaml.Marshal(nc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ReloadConfigString(string(rc)))
|
||||
|
||||
rl := lh.Query(staticHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Same(t, pinned, rl, "RemoteList pointer must stay stable so in-flight handshakes pick up the change")
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("2.2.2.2:4242")}, rl.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
// Reload back to the original IP. Mirrors the round-trip in issue #718 step 6-8 where
|
||||
// the buggy reload produced [1.1.1.1, 2.2.2.2, 1.1.1.1] instead of [1.1.1.1].
|
||||
nc = map[string]any{
|
||||
"static_host_map": map[string]any{
|
||||
"10.128.0.2": []any{"1.1.1.1:4242"},
|
||||
},
|
||||
}
|
||||
rc, err = yaml.Marshal(nc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ReloadConfigString(string(rc)))
|
||||
|
||||
rl = lh.Query(staticHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Same(t, pinned, rl)
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:4242")}, rl.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
// Reload with the same config. An unchanged entry must not duplicate.
|
||||
require.NoError(t, c.ReloadConfigString(string(rc)))
|
||||
|
||||
rl = lh.Query(staticHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Same(t, pinned, rl)
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:4242")}, rl.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
// Switch back to 2.2.2.2 so the rest of the test continues against a known address.
|
||||
nc = map[string]any{
|
||||
"static_host_map": map[string]any{
|
||||
"10.128.0.2": []any{"2.2.2.2:4242"},
|
||||
},
|
||||
}
|
||||
rc, err = yaml.Marshal(nc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ReloadConfigString(string(rc)))
|
||||
|
||||
// Add a second host alongside the first. Both should be present, neither duplicated.
|
||||
nc = map[string]any{
|
||||
"static_host_map": map[string]any{
|
||||
"10.128.0.2": []any{"2.2.2.2:4242"},
|
||||
"10.128.0.3": []any{"3.3.3.3:4242"},
|
||||
},
|
||||
}
|
||||
rc, err = yaml.Marshal(nc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ReloadConfigString(string(rc)))
|
||||
|
||||
rl = lh.Query(staticHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Same(t, pinned, rl, "adding a sibling entry must not displace the existing RemoteList")
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("2.2.2.2:4242")}, rl.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
rl = lh.Query(otherHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("3.3.3.3:4242")}, rl.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
// Drop the first host entirely. The vpnAddr is no longer marked static, our owner
|
||||
// contribution is cleared, but the addrMap entry stays in place so non-static cache
|
||||
// data (from lighthouse queries) on the same RemoteList isn't lost. In-flight handshakes
|
||||
// that already had the pointer see an empty address list rather than retrying stale ones.
|
||||
nc = map[string]any{
|
||||
"static_host_map": map[string]any{
|
||||
"10.128.0.3": []any{"3.3.3.3:4242"},
|
||||
},
|
||||
}
|
||||
rc, err = yaml.Marshal(nc)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ReloadConfigString(string(rc)))
|
||||
|
||||
_, isStatic := lh.GetStaticHostList()[staticHost]
|
||||
assert.False(t, isStatic)
|
||||
|
||||
rl = lh.Query(staticHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Same(t, pinned, rl)
|
||||
assert.Empty(t, rl.CopyAddrs([]netip.Prefix{}))
|
||||
|
||||
rl = lh.Query(otherHost)
|
||||
require.NotNil(t, rl)
|
||||
assert.Equal(t, []netip.AddrPort{netip.MustParseAddrPort("3.3.3.3:4242")}, rl.CopyAddrs([]netip.Prefix{}))
|
||||
}
|
||||
|
||||
func newLHHostRequest(fromAddr netip.AddrPort, myVpnIp, queryVpnIp netip.Addr, lhh *LightHouseHandler) testLhReply {
|
||||
req := &NebulaMeta{
|
||||
Type: NebulaMeta_HostQuery,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -130,6 +131,17 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
udpConns := make([]udp.Conn, routines)
|
||||
port := c.GetInt("listen.port", 0)
|
||||
|
||||
// Callers get no handle to these until the Control is returned, release them on any error.
|
||||
defer func() {
|
||||
if reterr != nil {
|
||||
for _, u := range udpConns {
|
||||
if u != nil {
|
||||
_ = u.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !configTest {
|
||||
rawListenHost := c.GetString("listen.host", "0.0.0.0")
|
||||
var listenHost netip.Addr
|
||||
@@ -194,7 +206,7 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
handshakeManager := NewHandshakeManager(l, hostMap, lightHouse, udpConns[0], handshakeConfig)
|
||||
lightHouse.handshakeTrigger = handshakeManager.trigger
|
||||
|
||||
ds, err := newDnsServerFromConfig(ctx, l, pki.getCertState(), hostMap, c)
|
||||
ds, err := newDnsServerFromConfig(ctx, l, pki, hostMap, c)
|
||||
if err != nil {
|
||||
l.Warn("Failed to start DNS responder", "error", err)
|
||||
}
|
||||
@@ -220,6 +232,8 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
relayManager: NewRelayManager(ctx, l, hostMap, c),
|
||||
punchy: punchy,
|
||||
ConntrackCacheTimeout: conntrackCacheTimeout,
|
||||
CpuAffinity: parseCpuAffinity(c, l, routines),
|
||||
PinThreads: c.GetBool("tun.pin_threads", true),
|
||||
l: l,
|
||||
}
|
||||
|
||||
@@ -271,6 +285,70 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCpuAffinity reads `tun.cpu_affinity` from the config — a list of
|
||||
// integer CPU IDs, one per TUN reader goroutine. Empty / unset returns nil
|
||||
// (listenIn falls back to spreading queues across the allowed CPU set).
|
||||
// Length mismatch with `routines` is a warning, not an error: shorter lists
|
||||
// are modulo-cycled across queues, longer lists' tail is ignored. Invalid
|
||||
// entries (non-integer, or a CPU ID we're not allowed to run on) are also a
|
||||
// warning and disable the override entirely so we don't silently pin to the
|
||||
// wrong CPU. Entries are validated against the process's current affinity
|
||||
// mask (util.AllowedCPUs) rather than 0..NumCPU-1: under a cgroup cpuset or
|
||||
// taskset the runnable IDs are frequently not that contiguous range, and
|
||||
// pinning to an unrunnable ID always fails. If the allowed set can't be
|
||||
// determined we fall back to a plain non-negative check.
|
||||
func parseCpuAffinity(c *config.C, l *slog.Logger, routines int) []int {
|
||||
raw := c.Get("tun.cpu_affinity")
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
rv, ok := raw.([]any)
|
||||
if !ok {
|
||||
l.Warn("tun.cpu_affinity must be a list of integers; ignoring", "value", raw)
|
||||
return nil
|
||||
}
|
||||
// allowed is the set of CPU IDs we're actually permitted to run on. A nil
|
||||
// slice (unsupported platform or lookup error) means "can't tell", so we
|
||||
// only apply the weaker non-negative check in that case.
|
||||
allowed, err := util.AllowedCPUs()
|
||||
if err != nil {
|
||||
l.Warn("could not determine allowed CPUs; validating tun.cpu_affinity against non-negative only", "error", err)
|
||||
allowed = nil
|
||||
}
|
||||
cpus := make([]int, 0, len(rv))
|
||||
for i, e := range rv {
|
||||
var cpu int
|
||||
switch v := e.(type) {
|
||||
case int:
|
||||
cpu = v
|
||||
case int64:
|
||||
cpu = int(v)
|
||||
case float64:
|
||||
cpu = int(v)
|
||||
default:
|
||||
l.Warn("tun.cpu_affinity entry not an integer; ignoring affinity",
|
||||
"index", i, "value", e)
|
||||
return nil
|
||||
}
|
||||
if cpu < 0 {
|
||||
l.Warn("tun.cpu_affinity entry out of range; ignoring affinity",
|
||||
"index", i, "cpu", cpu)
|
||||
return nil
|
||||
}
|
||||
if len(allowed) > 0 && !slices.Contains(allowed, cpu) {
|
||||
l.Warn("tun.cpu_affinity entry not in allowed CPU set; ignoring affinity",
|
||||
"index", i, "cpu", cpu, "allowed", allowed)
|
||||
return nil
|
||||
}
|
||||
cpus = append(cpus, cpu)
|
||||
}
|
||||
if len(cpus) != routines {
|
||||
l.Warn("tun.cpu_affinity length doesn't match routines; queues will modulo-cycle through the list",
|
||||
"affinity_len", len(cpus), "routines", routines)
|
||||
}
|
||||
return cpus
|
||||
}
|
||||
|
||||
func moduleVersion() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/slackhq/nebula/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseCpuAffinity(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
|
||||
// newConfig returns a config.C with tun.cpu_affinity set to v. A nil v
|
||||
// leaves the key unset.
|
||||
newConfig := func(v any) *config.C {
|
||||
c := config.NewC(l)
|
||||
if v != nil {
|
||||
c.Settings["tun"] = map[string]any{"cpu_affinity": v}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// unset -> nil (listenIn falls back to spreading across the allowed set)
|
||||
assert.Nil(t, parseCpuAffinity(newConfig(nil), l, 1))
|
||||
|
||||
// Pick a CPU we're actually allowed to run on so a valid list survives
|
||||
// validation regardless of the host's affinity mask.
|
||||
allowed, _ := util.AllowedCPUs()
|
||||
validCPU := 0
|
||||
if len(allowed) > 0 {
|
||||
validCPU = allowed[0]
|
||||
}
|
||||
|
||||
// valid list -> parsed through unchanged
|
||||
assert.Equal(t, []int{validCPU, validCPU}, parseCpuAffinity(newConfig([]any{validCPU, validCPU}), l, 2))
|
||||
|
||||
// a negative entry is out of range on every platform -> disables the override
|
||||
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU, -1}), l, 2))
|
||||
|
||||
// a non-integer entry -> disables the override
|
||||
assert.Nil(t, parseCpuAffinity(newConfig([]any{validCPU, "not-a-cpu"}), l, 2))
|
||||
|
||||
// a CPU id outside the allowed set -> disables the override. Only assertable
|
||||
// where we can enumerate the allowed set (e.g. linux); 1<<20 is far beyond
|
||||
// any representable CPU id so it can never be in the mask.
|
||||
if len(allowed) > 0 {
|
||||
assert.Nil(t, parseCpuAffinity(newConfig([]any{1 << 20}), l, 1))
|
||||
}
|
||||
}
|
||||
+22
-15
@@ -150,7 +150,8 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
|
||||
case header.TestReply:
|
||||
// No-op, useful for the Roaming and connectionManager side-effects above
|
||||
case header.TestRequest:
|
||||
f.send(header.Test, header.TestReply, ci, hostinfo, out, nb, out)
|
||||
//recycle the input packet ciphertext as our output buffer
|
||||
f.send(header.Test, header.TestReply, ci, hostinfo, out, nb, packet)
|
||||
default:
|
||||
hostinfo.logger(f.l).Error("IsValidSubType was true, but unexpected test subtype seen", "from", via, "header", h)
|
||||
return
|
||||
@@ -181,6 +182,13 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Advance the replay window now that the frame is authenticated
|
||||
if !hostinfo.ConnectionState.window.Update(f.l, h.MessageCounter) {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(f.l).Debug("dropping out of window relay packet", "header", h)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Successfully validated the thing. Get rid of the Relay header.
|
||||
signedPayload = signedPayload[header.Len:]
|
||||
// Pull the Roaming parts up here, and return in all call paths.
|
||||
@@ -194,8 +202,7 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
|
||||
// The only way this happens is if hostmap has an index to the correct HostInfo, but the HostInfo is missing
|
||||
// its internal mapping. This should never happen.
|
||||
hostinfo.logger(f.l).Error("HostInfo missing remote relay index",
|
||||
"vpnAddrs", hostinfo.vpnAddrs,
|
||||
"remoteIndex", h.RemoteIndex,
|
||||
"relayRemoteIndex", h.RemoteIndex,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -218,8 +225,8 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
|
||||
if err != nil {
|
||||
hostinfo.logger(f.l).Info("Failed to find target host info by ip",
|
||||
"relayTo", relay.PeerAddr,
|
||||
"relayFrom", hostinfo.vpnAddrs[0],
|
||||
"error", err,
|
||||
"hostinfo.vpnAddrs", hostinfo.vpnAddrs,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -270,7 +277,8 @@ func (f *Interface) sendCloseTunnel(h *HostInfo) {
|
||||
}
|
||||
|
||||
func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
|
||||
if !via.IsRelayed && hostinfo.remote != via.UdpAddr {
|
||||
curRemote := hostinfo.GetRemote()
|
||||
if !via.IsRelayed && curRemote != via.UdpAddr {
|
||||
if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(f.l).Debug("lighthouse.remote_allow_list denied roaming", "newAddr", via.UdpAddr)
|
||||
@@ -282,7 +290,7 @@ func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(f.l).Debug("Suppressing roam back to previous remote",
|
||||
"suppressSeconds", RoamingSuppressSeconds,
|
||||
"udpAddr", hostinfo.remote,
|
||||
"udpAddr", curRemote,
|
||||
"newAddr", via.UdpAddr,
|
||||
)
|
||||
}
|
||||
@@ -290,11 +298,11 @@ func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
|
||||
}
|
||||
|
||||
hostinfo.logger(f.l).Info("Host roamed to new udp ip/port.",
|
||||
"udpAddr", hostinfo.remote,
|
||||
"udpAddr", curRemote,
|
||||
"newAddr", via.UdpAddr,
|
||||
)
|
||||
hostinfo.lastRoam = time.Now()
|
||||
hostinfo.lastRoamRemote = hostinfo.remote
|
||||
hostinfo.lastRoamRemote = curRemote
|
||||
hostinfo.SetRemote(via.UdpAddr)
|
||||
}
|
||||
|
||||
@@ -414,16 +422,14 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
|
||||
if dataLen <= offset+1 {
|
||||
break
|
||||
}
|
||||
|
||||
next = int(data[offset+1]+2) << 2
|
||||
next = (int(data[offset+1]) + 2) << 2
|
||||
|
||||
default:
|
||||
// Normal ipv6 header length processing
|
||||
if dataLen <= offset+1 {
|
||||
break
|
||||
}
|
||||
|
||||
next = int(data[offset+1]+1) << 3
|
||||
next = (int(data[offset+1]) + 1) << 3
|
||||
}
|
||||
|
||||
if next <= 0 {
|
||||
@@ -536,7 +542,7 @@ func (f *Interface) handleOutsideMessagePacket(hostinfo *HostInfo, out []byte, p
|
||||
return
|
||||
}
|
||||
|
||||
_, err = f.readers[q].Write(out)
|
||||
_, err = f.queues[q].Write(out)
|
||||
if err != nil {
|
||||
f.l.Error("Failed to write to tun", "error", err)
|
||||
}
|
||||
@@ -583,10 +589,11 @@ func (f *Interface) handleRecvError(addr netip.AddrPort, h *header.H) {
|
||||
return
|
||||
}
|
||||
|
||||
if hostinfo.remote.IsValid() && hostinfo.remote != addr {
|
||||
hr := hostinfo.GetRemote()
|
||||
if hr.IsValid() && hr != addr {
|
||||
f.l.Info("Someone spoofing recv_errors?",
|
||||
"addr", addr,
|
||||
"hostinfoRemote", hostinfo.remote,
|
||||
"hostinfoRemote", hr,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -640,3 +640,38 @@ func serializeAH(ah *layers.IPSecAH) []byte {
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// Test_newPacket_v6ExtHeaderOverflow is a regression test for the IPv6 extension-header
|
||||
// length uint8 overflow in parseV6. A Destination-Options header with HdrExtLen=255 spans
|
||||
// (255+1)*8 = 2048 bytes, so the real transport header sits at offset 2088. Before the fix
|
||||
// the advance was computed in uint8 and wrapped to 0 (then clamped to 8), so the firewall
|
||||
// read the transport header ~2KB too early from attacker-controlled option bytes while the
|
||||
// host OS parses the real header, a firewall port/proto bypass. The fix makes parseV6 land
|
||||
// on the same offset the host does.
|
||||
func Test_newPacket_v6ExtHeaderOverflow(t *testing.T) {
|
||||
p := &firewall.Packet{}
|
||||
|
||||
const (
|
||||
hdrLen = 40 // IPv6 header
|
||||
extLen = 2048 // (255+1)*8, the true Destination-Options header size
|
||||
realTCPAt = hdrLen + extLen // 2088, where the host reads the transport header
|
||||
forgedTCPAt = hdrLen + 8 // 48, where the pre-fix wrapped+clamped walk landed
|
||||
)
|
||||
|
||||
pkt := make([]byte, realTCPAt+4)
|
||||
pkt[0] = 0x60 // version 6
|
||||
pkt[6] = byte(layers.IPProtocolIPv6Destination) // NextHeader -> Destination Options
|
||||
pkt[40] = byte(firewall.ProtoTCP) // Dest-Options NextHeader -> TCP
|
||||
pkt[41] = 255 // HdrExtLen = 255
|
||||
|
||||
// Forged transport header at the pre-fix (wrong) offset: dst port 443.
|
||||
binary.BigEndian.PutUint16(pkt[forgedTCPAt+2:forgedTCPAt+4], 443)
|
||||
// Real transport header at the offset the host actually uses: dst port 22.
|
||||
binary.BigEndian.PutUint16(pkt[realTCPAt+2:realTCPAt+4], 22)
|
||||
|
||||
require.NoError(t, newPacket(pkt, true, p))
|
||||
assert.Equal(t, uint8(firewall.ProtoTCP), p.Protocol)
|
||||
// LocalPort is the destination port for incoming traffic. It must be the real port (22)
|
||||
// the host delivers to, not the forged 443 at the overflowed offset.
|
||||
assert.Equal(t, uint16(22), p.LocalPort, "firewall must parse the real transport header, not the overflowed offset")
|
||||
}
|
||||
|
||||
+13
-3
@@ -4,15 +4,25 @@ import (
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
)
|
||||
|
||||
// defaultBatchBufSize is the per-Queue scratch size for Read. 65535 covers
|
||||
// any single IP packet.
|
||||
const defaultBatchBufSize = 65535
|
||||
|
||||
type Device interface {
|
||||
io.ReadWriteCloser
|
||||
io.Closer
|
||||
Activate() error
|
||||
Networks() []netip.Prefix
|
||||
Name() string
|
||||
RoutesFor(netip.Addr) routing.Gateways
|
||||
SupportsMultiqueue() bool
|
||||
NewMultiQueueReader() (io.ReadWriteCloser, error)
|
||||
// Queues returns the device's packet queues, opening additional ones as
|
||||
// needed until there are n. Platforms without multiqueue support return
|
||||
// their single queue regardless of n, so callers must size reader loops
|
||||
// to len(result), not n; implementations never return more than n. An
|
||||
// error means a queue that should have opened could not; the caller owns
|
||||
// cleanup via Close. Called once, during interface activation.
|
||||
Queues(n int) ([]tio.Queue, error)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
package overlaytest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
)
|
||||
|
||||
@@ -31,20 +30,16 @@ func (NoopTun) Name() string {
|
||||
return "noop"
|
||||
}
|
||||
|
||||
func (NoopTun) Read([]byte) (int, error) {
|
||||
return 0, nil
|
||||
func (NoopTun) Read() ([]tio.Packet, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (NoopTun) Write([]byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (NoopTun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (NoopTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, errors.New("unsupported")
|
||||
func (NoopTun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{NoopTun{}}, nil
|
||||
}
|
||||
|
||||
func (NoopTun) Close() error {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package tio
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// blockOn parks the calling goroutine until fd is ready (events is POLLIN for
|
||||
// reads, POLLOUT for writes) or shutdownFd signals teardown. It builds the
|
||||
// pollfd array on the stack every call, so concurrent callers on the same
|
||||
// Queue never share Revents storage.
|
||||
//
|
||||
// Returns os.ErrClosed when shutdown was signaled (POLLIN on shutdownFd)
|
||||
// or either fd reported a problem condition (POLLHUP|POLLNVAL|POLLERR).
|
||||
func blockOn(fd, shutdownFd int32, events int16) error {
|
||||
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
|
||||
pfds := [2]unix.PollFd{
|
||||
{Fd: fd, Events: events},
|
||||
{Fd: shutdownFd, Events: unix.POLLIN},
|
||||
}
|
||||
var err error
|
||||
for {
|
||||
_, err = unix.Poll(pfds[:], -1)
|
||||
if err != unix.EINTR {
|
||||
break
|
||||
}
|
||||
}
|
||||
tunEvents := pfds[0].Revents
|
||||
shutdownEvents := pfds[1].Revents
|
||||
// Check err before trusting the potentially bogus bits we just got.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
|
||||
return os.ErrClosed
|
||||
}
|
||||
if tunEvents&problemFlags != 0 {
|
||||
return os.ErrClosed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package tio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type pollQueueSet struct {
|
||||
pq []*Poll
|
||||
// pqi is exactly the same as pq, but stored as the interface type
|
||||
pqi []Queue
|
||||
shutdownFd int
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func NewPollQueueSet() (QueueSet, error) {
|
||||
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create eventfd: %w", err)
|
||||
}
|
||||
|
||||
out := &pollQueueSet{
|
||||
pq: []*Poll{},
|
||||
pqi: []Queue{},
|
||||
shutdownFd: shutdownFd,
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *pollQueueSet) Queues() []Queue {
|
||||
return c.pqi
|
||||
}
|
||||
|
||||
func (c *pollQueueSet) Add(fd int) error {
|
||||
x, err := newPoll(fd, c.shutdownFd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.pq = append(c.pq, x)
|
||||
c.pqi = append(c.pqi, x)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *pollQueueSet) wakeForShutdown() error {
|
||||
var buf [8]byte
|
||||
binary.NativeEndian.PutUint64(buf[:], 1)
|
||||
_, err := unix.Write(int(c.shutdownFd), buf[:])
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *pollQueueSet) Close() error {
|
||||
if c.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
|
||||
// Wake any reader blocked in poll so it observes POLLIN on the shutdown
|
||||
// eventfd and returns os.ErrClosed.
|
||||
if err := c.wakeForShutdown(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
// Close the per-queue tun fds; this also unblocks any in-flight reads.
|
||||
// The per-queue Close deliberately leaves shutdownFd alone - it belongs
|
||||
// to this container.
|
||||
for _, x := range c.pq {
|
||||
if err := x.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the shutdown eventfd last: every reader's pollfd set references
|
||||
// it, so it must outlive the wake + per-queue teardown above.
|
||||
if err := unix.Close(c.shutdownFd); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
c.shutdownFd = -1
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package tio
|
||||
|
||||
import "io"
|
||||
|
||||
// singleQueue adapts a legacy one-datagram-per-Read source into a Queue.
|
||||
// Read fills a private scratch buffer and returns exactly one Packet whose
|
||||
// Bytes borrow from that buffer, valid only until the next Read, per the
|
||||
// Queue contract. Single-reader like every Queue; Write is exactly as safe
|
||||
// for concurrent use as the underlying source's Write.
|
||||
type singleQueue struct {
|
||||
rw io.ReadWriter
|
||||
closer io.Closer // nil: Close is a no-op (the source is shared and owned elsewhere)
|
||||
buf []byte
|
||||
ret [1]Packet
|
||||
}
|
||||
|
||||
// NewSingleQueue wraps a one-datagram-per-Read ReadWriteCloser (a legacy tun
|
||||
// device) into a Queue. bufSize is the per-queue read scratch size and must
|
||||
// be at least the largest datagram the source can return. Close closes rwc.
|
||||
func NewSingleQueue(rwc io.ReadWriteCloser, bufSize int) Queue {
|
||||
return &singleQueue{rw: rwc, closer: rwc, buf: make([]byte, bufSize)}
|
||||
}
|
||||
|
||||
// NewSingleQueueNoClose is NewSingleQueue for a source owned by someone else,
|
||||
// e.g. several queues sharing one device. Close on the returned Queue is a
|
||||
// no-op so one queue can't tear the shared source out from under its
|
||||
// siblings; the owner remains responsible for closing the source itself.
|
||||
func NewSingleQueueNoClose(rw io.ReadWriter, bufSize int) Queue {
|
||||
return &singleQueue{rw: rw, buf: make([]byte, bufSize)}
|
||||
}
|
||||
|
||||
func (q *singleQueue) Read() ([]Packet, error) {
|
||||
n, err := q.rw.Read(q.buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.ret[0] = Packet{Bytes: q.buf[:n]}
|
||||
return q.ret[:], nil
|
||||
}
|
||||
|
||||
func (q *singleQueue) Write(p []byte) (int, error) {
|
||||
return q.rw.Write(p)
|
||||
}
|
||||
|
||||
func (q *singleQueue) Close() error {
|
||||
if q.closer == nil {
|
||||
return nil
|
||||
}
|
||||
return q.closer.Close()
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package tio
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// QueueSet holds one or many Queue objects and helps close them in an orderly way.
|
||||
type QueueSet interface {
|
||||
io.Closer
|
||||
Queues() []Queue
|
||||
|
||||
// Add takes a tun fd, adds it to the set, and prepares it for use as a Queue.
|
||||
Add(fd int) error
|
||||
}
|
||||
|
||||
// Queue is a readable/writable packet queue. Concurrency contract: a single
|
||||
// read goroutine drives Read; plain Write is safe for concurrent callers.
|
||||
type Queue interface {
|
||||
io.Closer
|
||||
|
||||
// Read returns one or more packets. The returned Packet.Bytes slices
|
||||
// are borrowed from the Queue's internal buffer and are only valid
|
||||
// until the next Read or Close on this Queue - callers must encrypt
|
||||
// or copy each slice before the next call. Single-reader only: not
|
||||
// safe for concurrent Reads (it reuses per-queue rx scratch each call).
|
||||
Read() ([]Packet, error)
|
||||
|
||||
// Write emits a single packet on the plaintext (outside→inside)
|
||||
// delivery path. Safe for concurrent use.
|
||||
Write(p []byte) (int, error)
|
||||
}
|
||||
|
||||
// Packet is the unit Queue.Read returns. Bytes points into the queue's
|
||||
// internal buffer and is only valid until the next Read or Close on the
|
||||
// queue that produced it.
|
||||
type Packet struct {
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// Clone returns a Packet whose Bytes is a freshly allocated copy of p.Bytes,
|
||||
// safe to retain past the next Read or Close on the originating Queue.
|
||||
// Use this only when a caller genuinely needs to outlive the borrowed-slice
|
||||
// contract — the hot path reads should continue to consume the borrow
|
||||
// synchronously to avoid the allocation.
|
||||
func (p Packet) Clone() Packet {
|
||||
if p.Bytes == nil {
|
||||
return p
|
||||
}
|
||||
cp := make([]byte, len(p.Bytes))
|
||||
copy(cp, p.Bytes)
|
||||
return Packet{Bytes: cp}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package tio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Maximum size we accept for a single read from a TUN. 65535 covers any
|
||||
// single IP packet.
|
||||
const tunReadBufSize = 65535
|
||||
|
||||
type Poll struct {
|
||||
fd int
|
||||
shutdownFd int
|
||||
closed atomic.Bool
|
||||
|
||||
readBuf []byte
|
||||
batchRet [1]Packet
|
||||
}
|
||||
|
||||
// newPoll wraps an existing tun fd. On failure it does NOT close fd: the
|
||||
// caller owns fd and is the sole closer (see pollQueueSet.Add callers in
|
||||
// overlay/tun_linux.go, which unix.Close on Add error). This keeps closes
|
||||
// at exactly one on every path.
|
||||
func newPoll(fd int, shutdownFd int) (*Poll, error) {
|
||||
if err := unix.SetNonblock(fd, true); err != nil {
|
||||
return nil, fmt.Errorf("failed to set Poll device as nonblocking: %w", err)
|
||||
}
|
||||
|
||||
out := &Poll{
|
||||
fd: fd,
|
||||
shutdownFd: shutdownFd,
|
||||
readBuf: make([]byte, tunReadBufSize),
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// blockOnRead waits until the Poll fd is readable or shutdown has been signaled.
|
||||
// Returns os.ErrClosed if Close was called.
|
||||
func (t *Poll) blockOnRead() error {
|
||||
return blockOn(int32(t.fd), int32(t.shutdownFd), unix.POLLIN)
|
||||
}
|
||||
|
||||
func (t *Poll) blockOnWrite() error {
|
||||
return blockOn(int32(t.fd), int32(t.shutdownFd), unix.POLLOUT)
|
||||
}
|
||||
|
||||
func (t *Poll) Read() ([]Packet, error) {
|
||||
n, err := t.readOne(t.readBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.batchRet[0] = Packet{Bytes: t.readBuf[:n]}
|
||||
return t.batchRet[:], nil
|
||||
}
|
||||
|
||||
func (t *Poll) readOne(to []byte) (int, error) {
|
||||
for {
|
||||
n, errno := unix.Read(t.fd, to)
|
||||
if errno == nil {
|
||||
return n, nil
|
||||
}
|
||||
switch errno {
|
||||
case unix.EAGAIN:
|
||||
if err := t.blockOnRead(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case unix.EINTR:
|
||||
// retry
|
||||
case unix.EBADF:
|
||||
return 0, os.ErrClosed
|
||||
default:
|
||||
return 0, errno
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write is safe for concurrent use
|
||||
func (t *Poll) Write(from []byte) (int, error) {
|
||||
for {
|
||||
n, errno := unix.Write(t.fd, from)
|
||||
if errno == nil {
|
||||
return n, nil
|
||||
}
|
||||
switch errno {
|
||||
case unix.EAGAIN:
|
||||
if err := t.blockOnWrite(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case unix.EINTR:
|
||||
// retry
|
||||
case unix.EBADF:
|
||||
return 0, os.ErrClosed
|
||||
default:
|
||||
return 0, errno
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Poll) Close() error {
|
||||
if t.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
//shutdownFd is owned by the container, so we should not close it
|
||||
// Close the underlying fd but do NOT null t.fd: a reader may still be
|
||||
// loading it in readOne, and mutating the field would race that load.
|
||||
// It gets EBADF -> os.ErrClosed (or wakes via the shutdown eventfd's
|
||||
// ppoll first). closed.Swap already guarantees we only close once.
|
||||
return unix.Close(t.fd)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//go:build linux && !android && !e2e_testing
|
||||
// +build linux,!android,!e2e_testing
|
||||
|
||||
package tio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// newReadPipe returns a read fd. The matching write fd is registered for cleanup.
|
||||
// The caller takes ownership of the read fd (pass it into a QueueSet).
|
||||
func newReadPipe(t *testing.T) int {
|
||||
t.Helper()
|
||||
var fds [2]int
|
||||
if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
|
||||
t.Fatalf("pipe2: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = unix.Close(fds[1]) })
|
||||
return fds[0]
|
||||
}
|
||||
|
||||
func TestPoll_WakeForShutdown_WakesFriends(t *testing.T) {
|
||||
pipe1 := newReadPipe(t)
|
||||
pipe2 := newReadPipe(t)
|
||||
parent, err := NewPollQueueSet()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, parent.Add(pipe1))
|
||||
require.NoError(t, parent.Add(pipe2))
|
||||
t.Cleanup(func() {
|
||||
_ = unix.Close(pipe1)
|
||||
_ = unix.Close(pipe2)
|
||||
})
|
||||
|
||||
readers := parent.Queues()
|
||||
errs := make([]error, len(readers))
|
||||
var wg sync.WaitGroup
|
||||
for i, r := range readers {
|
||||
wg.Add(1)
|
||||
go func(i int, r Queue) {
|
||||
defer wg.Done()
|
||||
_, errs[i] = r.Read()
|
||||
}(i, r)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if err := parent.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() { wg.Wait(); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("readers did not wake")
|
||||
}
|
||||
|
||||
for i, err := range errs {
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPoll_ConcurrentWrite_NoRace hammers a single Poll queue from two writer
|
||||
// goroutines while a reader drains the other end of the pipe. The writers
|
||||
// overflow the pipe buffer, so both repeatedly park in blockOnWrite at the same
|
||||
// time — the exact scenario that raced on the old shared writePoll member
|
||||
// array. Run under -race; a shared-array regression trips the detector here.
|
||||
func TestPoll_ConcurrentWrite_NoRace(t *testing.T) {
|
||||
var fds [2]int
|
||||
require.NoError(t, unix.Pipe2(fds[:], unix.O_CLOEXEC))
|
||||
readFd, writeFd := fds[0], fds[1]
|
||||
|
||||
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = unix.Close(shutdownFd) })
|
||||
|
||||
p, err := newPoll(writeFd, shutdownFd)
|
||||
require.NoError(t, err)
|
||||
|
||||
const writers = 2
|
||||
const perWriter = 4000
|
||||
payload := make([]byte, 100)
|
||||
total := writers * perWriter * len(payload)
|
||||
|
||||
// Reader: drain the read end (blocking) until every writer's bytes are
|
||||
// consumed, so the writers keep making progress rather than wedging on a
|
||||
// permanently full pipe.
|
||||
readDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readDone)
|
||||
buf := make([]byte, 4096)
|
||||
got := 0
|
||||
for got < total {
|
||||
n, rerr := unix.Read(readFd, buf)
|
||||
got += n
|
||||
if rerr != nil {
|
||||
if rerr == unix.EINTR {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
if n == 0 { // EOF
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < writers; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < perWriter; i++ {
|
||||
if _, werr := p.Write(payload); werr != nil {
|
||||
t.Errorf("write: %v", werr)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case <-readDone:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("reader did not drain")
|
||||
}
|
||||
|
||||
require.NoError(t, p.Close())
|
||||
_ = unix.Close(readFd)
|
||||
}
|
||||
|
||||
// TestPoll_NewPoll_DoesNotCloseFdOnFailure pins the ownership rule: when
|
||||
// newPoll fails, it must leave fd open so the caller (pollQueueSet.Add's
|
||||
// callers in tun_linux.go) is the sole closer. If newPoll also closed fd,
|
||||
// the poll path would double-close on Add error. We force the failure with
|
||||
// an O_PATH descriptor: fcntl(F_SETFL) — which SetNonblock performs — is not
|
||||
// permitted on O_PATH fds and fails with EBADF, while the fd itself stays
|
||||
// open so we can observe that newPoll left it alone.
|
||||
func TestPoll_NewPoll_DoesNotCloseFdOnFailure(t *testing.T) {
|
||||
fd, err := unix.Open("/", unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = unix.Close(fd) })
|
||||
|
||||
p, err := newPoll(fd, 1)
|
||||
require.Error(t, err, "SetNonblock on an O_PATH fd should fail")
|
||||
require.Nil(t, p)
|
||||
|
||||
// If newPoll had closed fd, F_GETFD would report it closed. It staying
|
||||
// open proves newPoll left the fd for the caller to close exactly once.
|
||||
require.True(t, fdOpen(t, fd), "newPoll must not close fd on failure; caller is the sole closer")
|
||||
}
|
||||
|
||||
func TestPoll_Close_Idempotent(t *testing.T) {
|
||||
tf, err := newPoll(newReadPipe(t), 1)
|
||||
require.NoError(t, err)
|
||||
if err := tf.Close(); err != nil {
|
||||
t.Fatalf("first Close: %v", err)
|
||||
}
|
||||
if err := tf.Close(); err != nil {
|
||||
t.Fatalf("second Close should be a no-op, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// fdOpen reports whether fd currently refers to an open file description.
|
||||
// A closed (or never-allocated) fd makes F_GETFD fail with EBADF.
|
||||
func fdOpen(t *testing.T, fd int) bool {
|
||||
t.Helper()
|
||||
_, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, unix.EBADF) {
|
||||
return false
|
||||
}
|
||||
t.Fatalf("unexpected fcntl(F_GETFD) error on fd %d: %v", fd, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// TestPollQueueSet_Close_ClosesShutdownFd is the regression test for the
|
||||
// leaked shutdown eventfd: the container that owns shutdownFd must close it in
|
||||
// Close, and a second Close must be a safe no-op.
|
||||
func TestPollQueueSet_Close_ClosesShutdownFd(t *testing.T) {
|
||||
qs, err := NewPollQueueSet()
|
||||
require.NoError(t, err)
|
||||
c, ok := qs.(*pollQueueSet)
|
||||
require.True(t, ok)
|
||||
require.NoError(t, qs.Add(newReadPipe(t)))
|
||||
|
||||
shutdownFd := c.shutdownFd
|
||||
require.True(t, fdOpen(t, shutdownFd), "shutdown eventfd should be open before Close")
|
||||
|
||||
require.NoError(t, qs.Close())
|
||||
require.False(t, fdOpen(t, shutdownFd), "shutdown eventfd should be closed after Close")
|
||||
|
||||
// Second Close must not touch fds (shutdownFd is now -1) and must return nil.
|
||||
require.NoError(t, qs.Close())
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
)
|
||||
@@ -40,6 +41,7 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
|
||||
|
||||
err := t.reload(c, true)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
||||
return r
|
||||
}
|
||||
|
||||
func (t tun) Activate() error {
|
||||
func (t *tun) Activate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -95,10 +97,6 @@ func (t *tun) Name() string {
|
||||
return "android"
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for android")
|
||||
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
+93
-39
@@ -6,7 +6,6 @@ package overlay
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
netroute "golang.org/x/net/route"
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
type tun struct {
|
||||
io.ReadWriteCloser
|
||||
f *os.File
|
||||
Device string
|
||||
vpnNetworks []netip.Prefix
|
||||
DefaultMTU int
|
||||
@@ -31,9 +31,6 @@ type tun struct {
|
||||
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
||||
linkAddr *netroute.LinkAddr
|
||||
l *slog.Logger
|
||||
|
||||
// cache out buffer since we need to prepend 4 bytes for tun metadata
|
||||
out []byte
|
||||
}
|
||||
|
||||
type ifReq struct {
|
||||
@@ -124,11 +121,11 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
|
||||
}
|
||||
|
||||
t := &tun{
|
||||
ReadWriteCloser: os.NewFile(uintptr(fd), ""),
|
||||
Device: name,
|
||||
vpnNetworks: vpnNetworks,
|
||||
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
|
||||
l: l,
|
||||
f: os.NewFile(uintptr(fd), ""),
|
||||
Device: name,
|
||||
vpnNetworks: vpnNetworks,
|
||||
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
|
||||
l: l,
|
||||
}
|
||||
|
||||
err = t.reload(c, true)
|
||||
@@ -158,8 +155,8 @@ func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, e
|
||||
}
|
||||
|
||||
func (t *tun) Close() error {
|
||||
if t.ReadWriteCloser != nil {
|
||||
return t.ReadWriteCloser.Close()
|
||||
if t.f != nil {
|
||||
return t.f.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -502,42 +499,103 @@ func delRoute(prefix netip.Prefix, gateway netroute.Addr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
|
||||
// calls go through libSystem's pinned trampoline. A raw syscall.Syscall(SYS_WRITEV/SYS_READV, ...)
|
||||
// on darwin/arm64 emits an SVC #0x80 trap (see $GOROOT/src/syscall/asm_darwin_arm64.s), the path
|
||||
// Apple keeps warning they will eventually disallow. We pull the low-level stubs instead of calling
|
||||
// unix.Writev/unix.Readv because those take [][]byte and rebuild the []Iovec every call, which
|
||||
// heap-allocates the header; linkname'ing the stubs lets us hand them our own stack-allocated
|
||||
// iovecs. See golang/go#78049.
|
||||
|
||||
//go:linkname tunWritev golang.org/x/sys/unix.writev
|
||||
//go:noescape
|
||||
func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
//go:linkname tunReadv golang.org/x/sys/unix.readv
|
||||
//go:noescape
|
||||
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
// Read pulls one IP packet off the utun device, scattering the 4 byte protocol header away from
|
||||
// the packet so the payload lands directly in to.
|
||||
func (t *tun) Read(to []byte) (int, error) {
|
||||
buf := make([]byte, len(to)+4)
|
||||
var head [4]byte
|
||||
|
||||
n, err := t.ReadWriteCloser.Read(buf)
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
copy(to, buf[4:])
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Read(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &to[0], Len: uint64(len(to))},
|
||||
}
|
||||
n, callErr = tunReadv(int(fd), iovecs)
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
if n < 4 {
|
||||
return 0, nil
|
||||
}
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
// Write is only valid for single threaded use
|
||||
// Write pushes one IP packet onto the utun device.
|
||||
func (t *tun) Write(from []byte) (int, error) {
|
||||
buf := t.out
|
||||
if cap(buf) < len(from)+4 {
|
||||
buf = make([]byte, len(from)+4)
|
||||
t.out = buf
|
||||
}
|
||||
buf = buf[:len(from)+4]
|
||||
|
||||
if len(from) == 0 {
|
||||
return 0, syscall.EIO
|
||||
}
|
||||
|
||||
// Determine the IP Family for the NULL L2 Header
|
||||
ipVer := from[0] >> 4
|
||||
if ipVer == 4 {
|
||||
buf[3] = syscall.AF_INET
|
||||
} else if ipVer == 6 {
|
||||
buf[3] = syscall.AF_INET6
|
||||
} else {
|
||||
var head [4]byte
|
||||
switch ipVer {
|
||||
case 4:
|
||||
head[3] = syscall.AF_INET
|
||||
case 6:
|
||||
head[3] = syscall.AF_INET6
|
||||
default:
|
||||
return 0, fmt.Errorf("unable to determine IP version from packet")
|
||||
}
|
||||
|
||||
copy(buf[4:], from)
|
||||
// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := t.ReadWriteCloser.Write(buf)
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Write(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &from[0], Len: uint64(len(from))},
|
||||
}
|
||||
n, callErr = tunWritev(int(fd), iovecs)
|
||||
// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
|
||||
// constants into error interfaces on every call.
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
func (t *tun) Networks() []netip.Prefix {
|
||||
@@ -548,10 +606,6 @@ func (t *tun) Name() string {
|
||||
return t.Device
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin")
|
||||
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
+26
-24
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/slackhq/nebula/iputil"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,23 @@ type disabledTun struct {
|
||||
l *slog.Logger
|
||||
}
|
||||
|
||||
// Read hands the next queued packet to a reader, copying it into b. Reads
|
||||
// from concurrent queues are safe: the channel receive serializes them and
|
||||
// each queue copies into its own private scratch buffer.
|
||||
func (t *disabledTun) Read(b []byte) (int, error) {
|
||||
r, ok := <-t.read
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
t.tx.Inc(1)
|
||||
if t.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
t.l.Debug("Write payload", "raw", prettyPacket(r))
|
||||
}
|
||||
|
||||
return copy(b, r), nil
|
||||
}
|
||||
|
||||
func newDisabledTun(vpnNetworks []netip.Prefix, queueLen int, metricsEnabled bool, l *slog.Logger) *disabledTun {
|
||||
tun := &disabledTun{
|
||||
vpnNetworks: vpnNetworks,
|
||||
@@ -57,24 +75,6 @@ func (*disabledTun) Name() string {
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
func (t *disabledTun) Read(b []byte) (int, error) {
|
||||
r, ok := <-t.read
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if len(r) > len(b) {
|
||||
return 0, fmt.Errorf("packet larger than mtu: %d > %d bytes", len(r), len(b))
|
||||
}
|
||||
|
||||
t.tx.Inc(1)
|
||||
if t.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
t.l.Debug("Write payload", "raw", prettyPacket(r))
|
||||
}
|
||||
|
||||
return copy(b, r), nil
|
||||
}
|
||||
|
||||
func (t *disabledTun) handleICMPEchoRequest(b []byte) bool {
|
||||
out := make([]byte, len(b))
|
||||
out = iputil.CreateICMPEchoResponse(b, out)
|
||||
@@ -106,12 +106,14 @@ func (t *disabledTun) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (t *disabledTun) SupportsMultiqueue() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *disabledTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return t, nil
|
||||
func (t *disabledTun) Queues(n int) ([]tio.Queue, error) {
|
||||
out := make([]tio.Queue, n)
|
||||
for i := range out {
|
||||
// NoClose: the shared channel and metrics are owned by the
|
||||
// disabledTun; Close on the device tears them down once for everybody.
|
||||
out[i] = tio.NewSingleQueueNoClose(t, defaultBatchBufSize)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t *disabledTun) Close() error {
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
//go:build linux && !android && !e2e_testing
|
||||
// +build linux,!android,!e2e_testing
|
||||
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// newReadPipe returns a read fd. The matching write fd is registered for cleanup.
|
||||
// The caller takes ownership of the read fd (pass it to newTunFd / newFriend).
|
||||
func newReadPipe(t *testing.T) int {
|
||||
t.Helper()
|
||||
var fds [2]int
|
||||
if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
|
||||
t.Fatalf("pipe2: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = unix.Close(fds[1]) })
|
||||
return fds[0]
|
||||
}
|
||||
|
||||
func TestTunFile_WakeForShutdown_UnblocksRead(t *testing.T) {
|
||||
tf, err := newTunFd(newReadPipe(t))
|
||||
if err != nil {
|
||||
t.Fatalf("newTunFd: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = tf.Close() })
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := tf.Read(make([]byte, 64))
|
||||
done <- err
|
||||
}()
|
||||
|
||||
// Verify Read is actually blocked in poll.
|
||||
select {
|
||||
case err := <-done:
|
||||
t.Fatalf("Read returned before shutdown signal: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
if err := tf.wakeForShutdown(); err != nil {
|
||||
t.Fatalf("wakeForShutdown: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
t.Fatalf("expected os.ErrClosed, got %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Read did not wake on shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunFile_WakeForShutdown_WakesFriends(t *testing.T) {
|
||||
parent, err := newTunFd(newReadPipe(t))
|
||||
if err != nil {
|
||||
t.Fatalf("newTunFd: %v", err)
|
||||
}
|
||||
friend, err := parent.newFriend(newReadPipe(t))
|
||||
if err != nil {
|
||||
_ = parent.Close()
|
||||
t.Fatalf("newFriend: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = friend.Close()
|
||||
_ = parent.Close()
|
||||
})
|
||||
|
||||
readers := []*tunFile{parent, friend}
|
||||
errs := make([]error, len(readers))
|
||||
var wg sync.WaitGroup
|
||||
for i, r := range readers {
|
||||
wg.Add(1)
|
||||
go func(i int, r *tunFile) {
|
||||
defer wg.Done()
|
||||
_, errs[i] = r.Read(make([]byte, 64))
|
||||
}(i, r)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if err := parent.wakeForShutdown(); err != nil {
|
||||
t.Fatalf("wakeForShutdown: %v", err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() { wg.Wait(); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("readers did not wake")
|
||||
}
|
||||
|
||||
for i, err := range errs {
|
||||
if !errors.Is(err, os.ErrClosed) {
|
||||
t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunFile_Close_Idempotent(t *testing.T) {
|
||||
tf, err := newTunFd(newReadPipe(t))
|
||||
if err != nil {
|
||||
t.Fatalf("newTunFd: %v", err)
|
||||
}
|
||||
if err := tf.Close(); err != nil {
|
||||
t.Fatalf("first Close: %v", err)
|
||||
}
|
||||
if err := tf.Close(); err != nil {
|
||||
t.Fatalf("second Close should be a no-op, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
"github.com/gaissmai/bart"
|
||||
|
||||
"github.com/slackhq/nebula/config"
|
||||
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
netroute "golang.org/x/net/route"
|
||||
@@ -561,12 +560,8 @@ func (t *tun) Name() string {
|
||||
return t.Device
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for freebsd")
|
||||
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
func (t *tun) addRoutes(logErrors bool) error {
|
||||
@@ -659,7 +654,6 @@ func addRoute(prefix netip.Prefix, gateway netroute.Addr) error {
|
||||
return fmt.Errorf("failed to create route.RouteMessage for change: %w", err)
|
||||
}
|
||||
_, err = unix.Write(sock, data[:])
|
||||
fmt.Println("DOING CHANGE")
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("failed to write route.RouteMessage to socket: %w", err)
|
||||
|
||||
+11
-6
@@ -16,8 +16,10 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type tun struct {
|
||||
@@ -33,6 +35,12 @@ func newTun(_ *config.C, _ *slog.Logger, _ []netip.Prefix, _ bool) (*tun, error)
|
||||
}
|
||||
|
||||
func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
|
||||
if err := unix.SetNonblock(deviceFd, true); err != nil {
|
||||
// We own the fd from the moment it is handed to us, same as the reload error path below
|
||||
_ = unix.Close(deviceFd)
|
||||
return nil, fmt.Errorf("failed to set the tun fd to non-blocking mode: %w", err)
|
||||
}
|
||||
|
||||
file := os.NewFile(uintptr(deviceFd), "/dev/tun")
|
||||
t := &tun{
|
||||
vpnNetworks: vpnNetworks,
|
||||
@@ -42,6 +50,7 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
|
||||
|
||||
err := t.reload(c, true)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -151,10 +160,6 @@ func (t *tun) Name() string {
|
||||
return "iOS"
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for ios")
|
||||
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
+75
-240
@@ -4,9 +4,7 @@
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
@@ -19,180 +17,15 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// tunFile wraps a TUN file descriptor with poll-based reads. The FD provided will be changed to non-blocking.
|
||||
// A shared eventfd allows Close to wake all readers blocked in poll.
|
||||
type tunFile struct {
|
||||
fd int
|
||||
shutdownFd int
|
||||
lastOne bool
|
||||
readPoll [2]unix.PollFd
|
||||
writePoll [2]unix.PollFd
|
||||
closed bool
|
||||
}
|
||||
|
||||
// newFriend makes a tunFile for a MultiQueueReader that copies the shutdown eventfd from the parent tun
|
||||
func (r *tunFile) newFriend(fd int) (*tunFile, error) {
|
||||
if err := unix.SetNonblock(fd, true); err != nil {
|
||||
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
|
||||
}
|
||||
return &tunFile{
|
||||
fd: fd,
|
||||
shutdownFd: r.shutdownFd,
|
||||
readPoll: [2]unix.PollFd{
|
||||
{Fd: int32(fd), Events: unix.POLLIN},
|
||||
{Fd: int32(r.shutdownFd), Events: unix.POLLIN},
|
||||
},
|
||||
writePoll: [2]unix.PollFd{
|
||||
{Fd: int32(fd), Events: unix.POLLOUT},
|
||||
{Fd: int32(r.shutdownFd), Events: unix.POLLIN},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTunFd(fd int) (*tunFile, error) {
|
||||
if err := unix.SetNonblock(fd, true); err != nil {
|
||||
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
|
||||
}
|
||||
|
||||
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create eventfd: %w", err)
|
||||
}
|
||||
|
||||
out := &tunFile{
|
||||
fd: fd,
|
||||
shutdownFd: shutdownFd,
|
||||
lastOne: true,
|
||||
readPoll: [2]unix.PollFd{
|
||||
{Fd: int32(fd), Events: unix.POLLIN},
|
||||
{Fd: int32(shutdownFd), Events: unix.POLLIN},
|
||||
},
|
||||
writePoll: [2]unix.PollFd{
|
||||
{Fd: int32(fd), Events: unix.POLLOUT},
|
||||
{Fd: int32(shutdownFd), Events: unix.POLLIN},
|
||||
},
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *tunFile) blockOnRead() error {
|
||||
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
|
||||
var err error
|
||||
for {
|
||||
_, err = unix.Poll(r.readPoll[:], -1)
|
||||
if err != unix.EINTR {
|
||||
break
|
||||
}
|
||||
}
|
||||
//always reset these!
|
||||
tunEvents := r.readPoll[0].Revents
|
||||
shutdownEvents := r.readPoll[1].Revents
|
||||
r.readPoll[0].Revents = 0
|
||||
r.readPoll[1].Revents = 0
|
||||
//do the err check before trusting the potentially bogus bits we just got
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
|
||||
return os.ErrClosed
|
||||
} else if tunEvents&problemFlags != 0 {
|
||||
return os.ErrClosed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *tunFile) blockOnWrite() error {
|
||||
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
|
||||
var err error
|
||||
for {
|
||||
_, err = unix.Poll(r.writePoll[:], -1)
|
||||
if err != unix.EINTR {
|
||||
break
|
||||
}
|
||||
}
|
||||
//always reset these!
|
||||
tunEvents := r.writePoll[0].Revents
|
||||
shutdownEvents := r.writePoll[1].Revents
|
||||
r.writePoll[0].Revents = 0
|
||||
r.writePoll[1].Revents = 0
|
||||
//do the err check before trusting the potentially bogus bits we just got
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
|
||||
return os.ErrClosed
|
||||
} else if tunEvents&problemFlags != 0 {
|
||||
return os.ErrClosed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *tunFile) Read(buf []byte) (int, error) {
|
||||
for {
|
||||
if n, err := unix.Read(r.fd, buf); err == nil {
|
||||
return n, nil
|
||||
} else if err == unix.EAGAIN {
|
||||
if err = r.blockOnRead(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
continue
|
||||
} else if err == unix.EINTR {
|
||||
continue
|
||||
} else if err == unix.EBADF {
|
||||
return 0, os.ErrClosed
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tunFile) Write(buf []byte) (int, error) {
|
||||
for {
|
||||
if n, err := unix.Write(r.fd, buf); err == nil {
|
||||
return n, nil
|
||||
} else if err == unix.EAGAIN {
|
||||
if err = r.blockOnWrite(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
continue
|
||||
} else if err == unix.EINTR {
|
||||
continue
|
||||
} else if err == unix.EBADF {
|
||||
return 0, os.ErrClosed
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tunFile) wakeForShutdown() error {
|
||||
var buf [8]byte
|
||||
binary.NativeEndian.PutUint64(buf[:], 1)
|
||||
_, err := unix.Write(int(r.readPoll[1].Fd), buf[:])
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *tunFile) Close() error {
|
||||
if r.closed { // avoid closing more than once. Technically a fd could get re-used, which would be a problem
|
||||
return nil
|
||||
}
|
||||
r.closed = true
|
||||
if r.lastOne {
|
||||
_ = unix.Close(r.shutdownFd)
|
||||
}
|
||||
return unix.Close(r.fd)
|
||||
}
|
||||
|
||||
type tun struct {
|
||||
*tunFile
|
||||
readers []*tunFile
|
||||
readers tio.QueueSet
|
||||
closeLock sync.Mutex
|
||||
Device string
|
||||
vpnNetworks []netip.Prefix
|
||||
@@ -249,44 +82,57 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
|
||||
// openTunDev opens /dev/net/tun, creating the device node first if it's
|
||||
// missing (docker containers occasionally omit it).
|
||||
func openTunDev() (int, error) {
|
||||
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
// If /dev/net/tun doesn't exist, try to create it (will happen in docker)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll("/dev/net", 0755)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
|
||||
}
|
||||
err = unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create /dev/net/tun: %w", err)
|
||||
}
|
||||
|
||||
fd, err = unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("created /dev/net/tun, but still failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil {
|
||||
return fd, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return -1, err
|
||||
}
|
||||
if err = os.MkdirAll("/dev/net", 0755); err != nil {
|
||||
return -1, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
|
||||
}
|
||||
if err = unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200))); err != nil {
|
||||
return -1, fmt.Errorf("failed to create /dev/net/tun: %w", err)
|
||||
}
|
||||
fd, err = unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("created /dev/net/tun, but still failed: %w", err)
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// tunSetIff runs TUNSETIFF with the given flags and returns the kernel-chosen
|
||||
// device name on success.
|
||||
func tunSetIff(fd int, name string, flags uint16) (string, error) {
|
||||
var req ifReq
|
||||
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI)
|
||||
req.Flags = flags
|
||||
copy(req.Name[:], name)
|
||||
if err := ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Trim(string(req.Name[:]), "\x00"), nil
|
||||
}
|
||||
|
||||
func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
|
||||
baseFlags := uint16(unix.IFF_TUN | unix.IFF_NO_PI)
|
||||
if multiqueue {
|
||||
req.Flags |= unix.IFF_MULTI_QUEUE
|
||||
baseFlags |= unix.IFF_MULTI_QUEUE
|
||||
}
|
||||
nameStr := c.GetString("tun.dev", "")
|
||||
copy(req.Name[:], nameStr)
|
||||
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, &NameError{
|
||||
Name: nameStr,
|
||||
Underlying: err,
|
||||
}
|
||||
|
||||
fd, err := openTunDev()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, err := tunSetIff(fd, nameStr, baseFlags)
|
||||
if err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, &NameError{Name: nameStr, Underlying: err}
|
||||
}
|
||||
name := strings.Trim(string(req.Name[:]), "\x00")
|
||||
|
||||
t, err := newTunGeneric(c, l, fd, vpnNetworks)
|
||||
if err != nil {
|
||||
@@ -298,16 +144,22 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// newTunGeneric does all the stuff common to different tun initialization paths. It will close your files on error.
|
||||
// newTunGeneric does all the stuff common to different tun initialization
|
||||
// paths. It will close your files on error.
|
||||
func newTunGeneric(c *config.C, l *slog.Logger, fd int, vpnNetworks []netip.Prefix) (*tun, error) {
|
||||
tfd, err := newTunFd(fd)
|
||||
qs, err := tio.NewPollQueueSet()
|
||||
if err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
err = qs.Add(fd)
|
||||
if err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &tun{
|
||||
tunFile: tfd,
|
||||
readers: []*tunFile{tfd},
|
||||
readers: qs,
|
||||
closeLock: sync.Mutex{},
|
||||
vpnNetworks: vpnNetworks,
|
||||
TXQueueLen: c.GetInt("tun.tx_queue", 500),
|
||||
@@ -406,36 +258,41 @@ func (t *tun) reload(c *config.C, initial bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return true
|
||||
// Queues opens additional kernel multiqueue fds until the device has n
|
||||
// queues, then returns them all. The first queue was opened by newTun.
|
||||
func (t *tun) Queues(n int) ([]tio.Queue, error) {
|
||||
for len(t.readers.Queues()) < n {
|
||||
if err := t.addQueue(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t.readers.Queues(), nil
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
// addQueue opens one more IFF_MULTI_QUEUE fd on the device and adds it to
|
||||
// the queue set.
|
||||
func (t *tun) addQueue() error {
|
||||
t.closeLock.Lock()
|
||||
defer t.closeLock.Unlock()
|
||||
|
||||
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var req ifReq
|
||||
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
|
||||
copy(req.Name[:], t.Device)
|
||||
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
|
||||
flags := uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
|
||||
if _, err = tunSetIff(fd, t.Device, flags); err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := t.tunFile.newFriend(fd)
|
||||
err = t.readers.Add(fd)
|
||||
if err != nil {
|
||||
_ = unix.Close(fd)
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
t.readers = append(t.readers, out)
|
||||
|
||||
return out, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
||||
@@ -878,32 +735,10 @@ func (t *tun) Close() error {
|
||||
t.routeChan = nil
|
||||
}
|
||||
|
||||
// Signal all readers blocked in poll to wake up and exit
|
||||
_ = t.tunFile.wakeForShutdown()
|
||||
|
||||
if t.ioctlFd > 0 {
|
||||
_ = unix.Close(int(t.ioctlFd))
|
||||
t.ioctlFd = 0
|
||||
}
|
||||
|
||||
for i := range t.readers {
|
||||
if i == 0 {
|
||||
continue //we want to close the zeroth reader last
|
||||
}
|
||||
err := t.readers[i].Close()
|
||||
if err != nil {
|
||||
t.l.Error("error closing tun reader", "reader", i, "error", err)
|
||||
} else {
|
||||
t.l.Info("closed tun reader", "reader", i)
|
||||
}
|
||||
}
|
||||
|
||||
//this is t.readers[0] too
|
||||
err := t.tunFile.Close()
|
||||
if err != nil {
|
||||
t.l.Error("error closing tun reader", "reader", 0, "error", err)
|
||||
} else {
|
||||
t.l.Info("closed tun reader", "reader", 0)
|
||||
}
|
||||
return err
|
||||
return t.readers.Close()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package overlay
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
netroute "golang.org/x/net/route"
|
||||
@@ -390,12 +390,8 @@ func (t *tun) Name() string {
|
||||
return t.Device
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for netbsd")
|
||||
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
func (t *tun) addRoutes(logErrors bool) error {
|
||||
|
||||
+85
-30
@@ -6,7 +6,6 @@ package overlay
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
netroute "golang.org/x/net/route"
|
||||
@@ -57,8 +57,6 @@ type tun struct {
|
||||
l *slog.Logger
|
||||
f *os.File
|
||||
fd int
|
||||
// cache out buffer since we need to prepend 4 bytes for tun metadata
|
||||
out []byte
|
||||
}
|
||||
|
||||
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
|
||||
@@ -124,42 +122,103 @@ func (t *tun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
|
||||
// calls go through libc's pinned trampoline. OpenBSD's pinsyscall protection rejects a raw
|
||||
// syscall.Syscall(SYS_WRITEV/SYS_READV, ...) because it doesn't originate from a libc-pinned
|
||||
// address, so we can't use the syscall.Syscall pattern that freebsd / netbsd use. We pull the
|
||||
// low-level stubs instead of calling unix.Writev/unix.Readv because those take [][]byte and rebuild
|
||||
// the []Iovec every call, which heap-allocates the header; linkname'ing the stubs lets us hand them
|
||||
// our own stack-allocated iovecs. See golang/go#78049.
|
||||
|
||||
//go:linkname tunWritev golang.org/x/sys/unix.writev
|
||||
//go:noescape
|
||||
func tunWritev(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
//go:linkname tunReadv golang.org/x/sys/unix.readv
|
||||
//go:noescape
|
||||
func tunReadv(fd int, iovecs []unix.Iovec) (n int, err error)
|
||||
|
||||
// Read pulls one IP packet off the tun device, scattering the 4 byte protocol header away from
|
||||
// the packet so the payload lands directly in to.
|
||||
func (t *tun) Read(to []byte) (int, error) {
|
||||
buf := make([]byte, len(to)+4)
|
||||
var head [4]byte
|
||||
|
||||
n, err := t.f.Read(buf)
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
copy(to, buf[4:])
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Read(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &to[0], Len: uint64(len(to))},
|
||||
}
|
||||
n, callErr = tunReadv(int(fd), iovecs)
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
if n < 4 {
|
||||
return 0, nil
|
||||
}
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
// Write is only valid for single threaded use
|
||||
// Write pushes one IP packet onto the tun device.
|
||||
func (t *tun) Write(from []byte) (int, error) {
|
||||
buf := t.out
|
||||
if cap(buf) < len(from)+4 {
|
||||
buf = make([]byte, len(from)+4)
|
||||
t.out = buf
|
||||
}
|
||||
buf = buf[:len(from)+4]
|
||||
|
||||
if len(from) == 0 {
|
||||
return 0, syscall.EIO
|
||||
}
|
||||
|
||||
// Determine the IP Family for the NULL L2 Header
|
||||
ipVer := from[0] >> 4
|
||||
if ipVer == 4 {
|
||||
buf[3] = syscall.AF_INET
|
||||
} else if ipVer == 6 {
|
||||
buf[3] = syscall.AF_INET6
|
||||
} else {
|
||||
var head [4]byte
|
||||
switch ipVer {
|
||||
case 4:
|
||||
head[3] = syscall.AF_INET
|
||||
case 6:
|
||||
head[3] = syscall.AF_INET6
|
||||
default:
|
||||
return 0, fmt.Errorf("unable to determine IP version from packet")
|
||||
}
|
||||
|
||||
copy(buf[4:], from)
|
||||
// Grab rc as a local so the compiler can devirtualize the call and keep the closure on the stack.
|
||||
rc, err := t.f.SyscallConn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := t.f.Write(buf)
|
||||
return n - 4, err
|
||||
var n int
|
||||
var callErr error
|
||||
err = rc.Write(func(fd uintptr) bool {
|
||||
iovecs := []unix.Iovec{
|
||||
{Base: &head[0], Len: 4},
|
||||
{Base: &from[0], Len: uint64(len(from))},
|
||||
}
|
||||
n, callErr = tunWritev(int(fd), iovecs)
|
||||
// Type-assert to syscall.Errno so the EAGAIN/EWOULDBLOCK/EINTR check doesn't box the errno
|
||||
// constants into error interfaces on every call.
|
||||
if errno, ok := callErr.(syscall.Errno); ok && errno.Temporary() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if callErr != nil {
|
||||
return 0, callErr
|
||||
}
|
||||
|
||||
return n - 4, nil
|
||||
}
|
||||
|
||||
func (t *tun) addIp(cidr netip.Prefix) error {
|
||||
@@ -310,12 +369,8 @@ func (t *tun) Name() string {
|
||||
return t.Device
|
||||
}
|
||||
|
||||
func (t *tun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for openbsd")
|
||||
func (t *tun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
func (t *tun) addRoutes(logErrors bool) error {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
)
|
||||
@@ -177,10 +178,6 @@ func (t *TestTun) Read(b []byte) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (t *TestTun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented")
|
||||
func (t *TestTun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, udp.MTU)}, nil
|
||||
}
|
||||
|
||||
+7
-11
@@ -6,7 +6,6 @@ package overlay
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
"github.com/slackhq/nebula/util"
|
||||
"github.com/slackhq/nebula/wintun"
|
||||
@@ -47,6 +47,10 @@ type winTun struct {
|
||||
tun *wintun.NativeTun
|
||||
}
|
||||
|
||||
func (t *winTun) Read(b []byte) (int, error) {
|
||||
return t.tun.Read(b, 0)
|
||||
}
|
||||
|
||||
func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (Device, error) {
|
||||
return nil, fmt.Errorf("newTunFromFd not supported in Windows")
|
||||
}
|
||||
@@ -255,20 +259,12 @@ func (t *winTun) Name() string {
|
||||
return t.Device
|
||||
}
|
||||
|
||||
func (t *winTun) Read(b []byte) (int, error) {
|
||||
return t.tun.Read(b, 0)
|
||||
}
|
||||
|
||||
func (t *winTun) Write(b []byte) (int, error) {
|
||||
return t.tun.Write(b, 0)
|
||||
}
|
||||
|
||||
func (t *winTun) SupportsMultiqueue() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *winTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, fmt.Errorf("TODO: multiqueue not implemented for windows")
|
||||
func (t *winTun) Queues(int) ([]tio.Queue, error) {
|
||||
return []tio.Queue{tio.NewSingleQueue(t, defaultBatchBufSize)}, nil
|
||||
}
|
||||
|
||||
func (t *winTun) Close() error {
|
||||
|
||||
+13
-6
@@ -6,6 +6,7 @@ import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
"github.com/slackhq/nebula/routing"
|
||||
)
|
||||
|
||||
@@ -46,12 +47,16 @@ func (d *UserDevice) RoutesFor(ip netip.Addr) routing.Gateways {
|
||||
return routing.Gateways{routing.NewGateway(ip, 1)}
|
||||
}
|
||||
|
||||
func (d *UserDevice) SupportsMultiqueue() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *UserDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return d, nil
|
||||
func (d *UserDevice) Queues(n int) ([]tio.Queue, error) {
|
||||
out := make([]tio.Queue, n)
|
||||
for i := range out {
|
||||
// All queues share the underlying pipes (the io.Pipe serializes
|
||||
// concurrent callers) but each owns a private scratch buffer so
|
||||
// concurrent Reads across queues never alias. NoClose: the pipes are
|
||||
// owned by the UserDevice and torn down once by UserDevice.Close.
|
||||
out[i] = tio.NewSingleQueueNoClose(d, defaultBatchBufSize)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
|
||||
@@ -61,9 +66,11 @@ func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
|
||||
func (d *UserDevice) Read(p []byte) (n int, err error) {
|
||||
return d.outboundReader.Read(p)
|
||||
}
|
||||
|
||||
func (d *UserDevice) Write(p []byte) (n int, err error) {
|
||||
return d.inboundWriter.Write(p)
|
||||
}
|
||||
|
||||
func (d *UserDevice) Close() error {
|
||||
d.inboundWriter.Close()
|
||||
d.outboundWriter.Close()
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/overlay/tio"
|
||||
)
|
||||
|
||||
// newTestUserDevice returns the concrete *UserDevice so tests can reach Pipe()
|
||||
// and the internal queue plumbing.
|
||||
func newTestUserDevice(t *testing.T) *UserDevice {
|
||||
t.Helper()
|
||||
dev, err := NewUserDevice([]netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")})
|
||||
if err != nil {
|
||||
t.Fatalf("NewUserDevice: %v", err)
|
||||
}
|
||||
ud, ok := dev.(*UserDevice)
|
||||
if !ok {
|
||||
t.Fatalf("NewUserDevice returned %T, want *UserDevice", dev)
|
||||
}
|
||||
return ud
|
||||
}
|
||||
|
||||
// TestUserDeviceReadersDistinctBuffers ensures each Queue is actually different
|
||||
func TestUserDeviceReadersDistinctBuffers(t *testing.T) {
|
||||
d := newTestUserDevice(t)
|
||||
|
||||
readers, err := d.Queues(2)
|
||||
if err != nil {
|
||||
t.Fatalf("Queues: %v", err)
|
||||
}
|
||||
if len(readers) != 2 {
|
||||
t.Fatalf("Queues(2) returned %d queues, want 2", len(readers))
|
||||
}
|
||||
|
||||
// Distinct queue objects.
|
||||
if readers[0] == readers[1] {
|
||||
t.Fatal("Queues(2) returned the same queue object twice")
|
||||
}
|
||||
|
||||
// Drive one packet through each queue and confirm the borrowed bytes from
|
||||
// the first read are NOT clobbered by the second read. With a shared
|
||||
// buffer, reading pkt1 into q1 would corrupt q0's still-borrowed slice.
|
||||
_, ow := d.Pipe()
|
||||
|
||||
pkt0 := []byte("packet-zero-aaaaaaaa")
|
||||
pkt1 := []byte("packet-one-bbbbbbbbb")
|
||||
|
||||
// The pipe is unbuffered, so writes block until a reader consumes them.
|
||||
// Serialize: write pkt0 (read on q0), then write pkt1 (read on q1).
|
||||
go func() {
|
||||
if _, err := ow.Write(pkt0); err != nil {
|
||||
t.Errorf("write pkt0: %v", err)
|
||||
}
|
||||
if _, err := ow.Write(pkt1); err != nil {
|
||||
t.Errorf("write pkt1: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got0, err := readers[0].Read()
|
||||
if err != nil {
|
||||
t.Fatalf("q0.Read: %v", err)
|
||||
}
|
||||
if len(got0) != 1 || string(got0[0].Bytes) != string(pkt0) {
|
||||
t.Fatalf("q0 first read = %q, want %q", firstBytes(got0), pkt0)
|
||||
}
|
||||
// Hold onto q0's borrowed slice across q1's read.
|
||||
borrowed := got0[0].Bytes
|
||||
|
||||
got1, err := readers[1].Read()
|
||||
if err != nil {
|
||||
t.Fatalf("q1.Read: %v", err)
|
||||
}
|
||||
if len(got1) != 1 || string(got1[0].Bytes) != string(pkt1) {
|
||||
t.Fatalf("q1 read = %q, want %q", firstBytes(got1), pkt1)
|
||||
}
|
||||
|
||||
// q0's borrowed bytes must still hold pkt0 - a shared buffer would now
|
||||
// show pkt1's contents.
|
||||
if string(borrowed) != string(pkt0) {
|
||||
t.Fatalf("q0 borrowed bytes were clobbered by q1's read: got %q, want %q", borrowed, pkt0)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserDeviceReadersConcurrentRace exercises two queues reading distinct
|
||||
// packets concurrently. Run it under `go test -race`: with the old
|
||||
// shared-buffer implementation the concurrent Reads raced on readBuf/batchRet
|
||||
// and corrupted each other's returned slices.
|
||||
func TestUserDeviceReadersConcurrentRace(t *testing.T) {
|
||||
d := newTestUserDevice(t)
|
||||
readers, err := d.Queues(2)
|
||||
if err != nil {
|
||||
t.Fatalf("Queues: %v", err)
|
||||
}
|
||||
_, ow := d.Pipe()
|
||||
|
||||
const iterations = 200
|
||||
|
||||
errs := make(chan error, 3)
|
||||
|
||||
// Each reader parks in Read on the shared outboundReader; io.Pipe hands
|
||||
// each write to whichever reader is currently waiting. We only care that
|
||||
// concurrent Reads into distinct buffers are race-free, so any parked
|
||||
// reader may serve any write.
|
||||
var wg sync.WaitGroup
|
||||
run := func(idx int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < iterations; i++ {
|
||||
pkts, err := readers[idx].Read()
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if len(pkts) != 1 {
|
||||
errs <- fmt.Errorf("reader %d: got %d packets, want 1", idx, len(pkts))
|
||||
return
|
||||
}
|
||||
// Touch every byte of the borrowed slice while the other reader
|
||||
// may be mid-Read; a shared buffer would race here.
|
||||
total := 0
|
||||
for _, c := range pkts[0].Bytes {
|
||||
total += int(c)
|
||||
}
|
||||
_ = total
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(2)
|
||||
go run(0)
|
||||
go run(1)
|
||||
|
||||
// Feed 2*iterations packets. io.Pipe copies each write straight into the
|
||||
// waiting reader's private buffer, so reusing buf between writes is safe.
|
||||
go func() {
|
||||
buf := make([]byte, 32)
|
||||
for i := 0; i < 2*iterations; i++ {
|
||||
for j := range buf {
|
||||
buf[j] = byte(i + j)
|
||||
}
|
||||
if _, err := ow.Write(buf); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
select {
|
||||
case err := <-errs:
|
||||
t.Fatalf("concurrent reader failed: %v", err)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func firstBytes(p []tio.Packet) []byte {
|
||||
if len(p) == 0 {
|
||||
return nil
|
||||
}
|
||||
return p[0].Bytes
|
||||
}
|
||||
@@ -174,9 +174,9 @@ func (p *Punchy) SendPunch(hostinfo *HostInfo) {
|
||||
|
||||
if p.punchEverything.Load() {
|
||||
p.sendPunchToAllRemotes(hostinfo)
|
||||
} else if hostinfo.remote.IsValid() {
|
||||
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
||||
p.metricPunchyTx.Inc(1)
|
||||
p.punchConn.WriteTo([]byte{1}, hostinfo.remote)
|
||||
p.punchConn.WriteTo([]byte{1}, hr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+87
-43
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
@@ -57,14 +58,25 @@ func (rm *relayManager) GetUseRelays() bool {
|
||||
// For each candidate relay it either kicks off a handshake to the relay, sends a CreateRelayRequest, retransmits
|
||||
// one that may have been lost, or, once the relay is Established, forwards the in-progress
|
||||
// stage 0 handshake packet for vpnIp through it.
|
||||
func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *HostInfo, stage0 []byte) {
|
||||
func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hh *HandshakeHostInfo, stage0 []byte) {
|
||||
hostinfo := hh.hostinfo
|
||||
if !rm.GetUseRelays() || len(hostinfo.remotes.relays) == 0 {
|
||||
hh.lastRelays = nil
|
||||
return
|
||||
}
|
||||
|
||||
hostinfo.logger(rm.l).Info("Attempt to relay through hosts", "relays", hostinfo.remotes.relays)
|
||||
relays := hostinfo.remotes.relays
|
||||
listLevel := slog.LevelDebug
|
||||
prior := hh.lastRelays
|
||||
if !slices.Equal(relays, prior) {
|
||||
listLevel = slog.LevelInfo
|
||||
hh.lastRelays = slices.Clone(relays)
|
||||
}
|
||||
hl := hostinfo.logger(rm.l)
|
||||
hl.Log(context.Background(), listLevel, "Attempt to relay through hosts", "relays", relays)
|
||||
|
||||
// Send a RelayRequest to all known Relay IP's
|
||||
for _, relay := range hostinfo.remotes.relays {
|
||||
for _, relay := range relays {
|
||||
// Don't relay through the host I'm trying to connect to
|
||||
if relay == vpnIp {
|
||||
continue
|
||||
@@ -75,20 +87,30 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
continue
|
||||
}
|
||||
|
||||
// Each relay's per-attempt log fires at Info on the first time we hit it and Debug after that.
|
||||
level := slog.LevelInfo
|
||||
if slices.Contains(prior, relay) {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
relayHostInfo := rm.hostmap.QueryVpnAddr(relay)
|
||||
if relayHostInfo == nil || !relayHostInfo.remote.IsValid() {
|
||||
hostinfo.logger(rm.l).Info("Establish tunnel to relay target", "relay", relay.String())
|
||||
if relayHostInfo == nil || !relayHostInfo.GetRemote().IsValid() {
|
||||
hl.Log(context.Background(), level, "Establish tunnel to relay target", "relay", relay.String())
|
||||
f.Handshake(relay)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check the relay HostInfo to see if we already established a relay through
|
||||
existingRelay, ok := relayHostInfo.relayState.QueryRelayForByIp(vpnIp)
|
||||
if !ok {
|
||||
// No relays exist or requested yet.
|
||||
if relayHostInfo.remote.IsValid() {
|
||||
if relayHostInfo.GetRemote().IsValid() {
|
||||
idx, err := AddRelay(rm.l, relayHostInfo, rm.hostmap, vpnIp, nil, TerminalType, Requested)
|
||||
if err != nil {
|
||||
hostinfo.logger(rm.l).Info("Failed to add relay to hostmap", "relay", relay.String(), "error", err)
|
||||
// No local relay state was installed, so a CreateRelayRequest would hand the
|
||||
// peer an index we could never resolve. Skip it.
|
||||
hl.Info("Failed to add relay to hostmap", "relay", relay.String(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
m := NebulaControl{
|
||||
@@ -99,12 +121,12 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
switch relayHostInfo.GetCert().Certificate.Version() {
|
||||
case cert.Version1:
|
||||
if !f.myVpnAddrs[0].Is4() {
|
||||
hostinfo.logger(rm.l).Error("can not establish v1 relay with a v6 network because the relay is not running a current nebula version")
|
||||
hl.Error("can not establish v1 relay with a v6 network because the relay is not running a current nebula version")
|
||||
continue
|
||||
}
|
||||
|
||||
if !vpnIp.Is4() {
|
||||
hostinfo.logger(rm.l).Error("can not establish v1 relay with a v6 remote network because the relay is not running a current nebula version")
|
||||
hl.Error("can not establish v1 relay with a v6 remote network because the relay is not running a current nebula version")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -116,16 +138,16 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
m.RelayFromAddr = netAddrToProtoAddr(f.myVpnAddrs[0])
|
||||
m.RelayToAddr = netAddrToProtoAddr(vpnIp)
|
||||
default:
|
||||
hostinfo.logger(rm.l).Error("Unknown certificate version found while creating relay")
|
||||
hl.Error("Unknown certificate version found while creating relay")
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := m.Marshal()
|
||||
if err != nil {
|
||||
hostinfo.logger(rm.l).Error("Failed to marshal Control message to create relay", "error", err)
|
||||
hl.Error("Failed to marshal Control message to create relay", "error", err)
|
||||
} else {
|
||||
f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||
rm.l.Info("send CreateRelayRequest",
|
||||
rm.l.Log(context.Background(), level, "send CreateRelayRequest",
|
||||
"relayFrom", f.myVpnAddrs[0],
|
||||
"relayTo", vpnIp,
|
||||
"initiatorRelayIndex", idx,
|
||||
@@ -138,14 +160,14 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
|
||||
switch existingRelay.State {
|
||||
case Established:
|
||||
hostinfo.logger(rm.l).Info("Send handshake via relay", "relay", relay.String())
|
||||
hl.Log(context.Background(), level, "Send handshake via relay", "relay", relay.String())
|
||||
f.SendVia(relayHostInfo, existingRelay, stage0, make([]byte, 12), make([]byte, mtu), false)
|
||||
case Disestablished:
|
||||
// Mark this relay as 'requested'
|
||||
relayHostInfo.relayState.UpdateRelayForByIpState(vpnIp, Requested)
|
||||
fallthrough
|
||||
case Requested:
|
||||
hostinfo.logger(rm.l).Info("Re-send CreateRelay request", "relay", relay.String())
|
||||
hl.Log(context.Background(), level, "Re-send CreateRelay request", "relay", relay.String())
|
||||
// Re-send the CreateRelay request, in case the previous one was lost.
|
||||
m := NebulaControl{
|
||||
Type: NebulaControl_CreateRelayRequest,
|
||||
@@ -155,12 +177,12 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
switch relayHostInfo.GetCert().Certificate.Version() {
|
||||
case cert.Version1:
|
||||
if !f.myVpnAddrs[0].Is4() {
|
||||
hostinfo.logger(rm.l).Error("can not establish v1 relay with a v6 network because the relay is not running a current nebula version")
|
||||
hl.Error("can not establish v1 relay with a v6 network because the relay is not running a current nebula version")
|
||||
continue
|
||||
}
|
||||
|
||||
if !vpnIp.Is4() {
|
||||
hostinfo.logger(rm.l).Error("can not establish v1 relay with a v6 remote network because the relay is not running a current nebula version")
|
||||
hl.Error("can not establish v1 relay with a v6 remote network because the relay is not running a current nebula version")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -172,16 +194,16 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
m.RelayFromAddr = netAddrToProtoAddr(f.myVpnAddrs[0])
|
||||
m.RelayToAddr = netAddrToProtoAddr(vpnIp)
|
||||
default:
|
||||
hostinfo.logger(rm.l).Error("Unknown certificate version found while creating relay")
|
||||
hl.Error("Unknown certificate version found while creating relay")
|
||||
continue
|
||||
}
|
||||
msg, err := m.Marshal()
|
||||
if err != nil {
|
||||
hostinfo.logger(rm.l).Error("Failed to marshal Control message to create relay", "error", err)
|
||||
hl.Error("Failed to marshal Control message to create relay", "error", err)
|
||||
} else {
|
||||
// This must send over the hostinfo, not over hm.Hosts[ip]
|
||||
f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||
rm.l.Info("send CreateRelayRequest",
|
||||
rm.l.Log(context.Background(), level, "send CreateRelayRequest",
|
||||
"relayFrom", f.myVpnAddrs[0],
|
||||
"relayTo", vpnIp,
|
||||
"initiatorRelayIndex", existingRelay.LocalIndex,
|
||||
@@ -192,7 +214,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
|
||||
// PeerRequested only occurs in Forwarding relays, not Terminal relays, and this is a Terminal relay case.
|
||||
fallthrough
|
||||
default:
|
||||
hostinfo.logger(rm.l).Error("Relay unexpected state",
|
||||
hl.Error("Relay unexpected state",
|
||||
"vpnIp", vpnIp,
|
||||
"state", existingRelay.State,
|
||||
"relay", relay,
|
||||
@@ -218,7 +240,12 @@ func AddRelay(l *slog.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.
|
||||
// Avoid standing up a relay that can't be used since only the primary hostinfo
|
||||
// will be pointed to by the relay logic
|
||||
//TODO: if there was an existing primary and it had relay state, should we merge?
|
||||
hm.unlockedMakePrimary(relayHostInfo)
|
||||
if !hm.unlockedMakePrimary(relayHostInfo) {
|
||||
// The tunnel was torn down after the caller grabbed relayHostInfo. A relay standing
|
||||
// on an unlinked hostinfo would never carry traffic, and its Relays entry could
|
||||
// never be reclaimed since the delete-time cleanup has already run.
|
||||
return 0, errors.New("relay hostinfo is no longer in the hostmap")
|
||||
}
|
||||
|
||||
hm.Relays[index] = relayHostInfo
|
||||
newRelay := Relay{
|
||||
@@ -290,6 +317,22 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
|
||||
v = cert.Version2
|
||||
}
|
||||
|
||||
// validate:
|
||||
switch msg.Type {
|
||||
case NebulaControl_CreateRelayRequest, NebulaControl_CreateRelayResponse:
|
||||
if msg.RelayFromAddr == nil {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
h.logger(f.l).Debug("Control message received with nil RelayFromAddr", "type", msg.Type)
|
||||
}
|
||||
return
|
||||
} else if msg.RelayToAddr == nil {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
h.logger(f.l).Debug("Control message received with nil RelayToAddr", "type", msg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case NebulaControl_CreateRelayRequest:
|
||||
rm.handleCreateRelayRequest(v, h, f, msg)
|
||||
@@ -299,17 +342,17 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
|
||||
}
|
||||
|
||||
func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) {
|
||||
//nil-checks for protoAddrToNetAddr handled by caller
|
||||
relayFrom := protoAddrToNetAddr(m.RelayFromAddr)
|
||||
relayTo := protoAddrToNetAddr(m.RelayToAddr)
|
||||
rm.l.Info("handleCreateRelayResponse",
|
||||
"relayFrom", protoAddrToNetAddr(m.RelayFromAddr),
|
||||
"relayTo", protoAddrToNetAddr(m.RelayToAddr),
|
||||
"relayFrom", relayFrom,
|
||||
"relayTo", relayTo,
|
||||
"initiatorRelayIndex", m.InitiatorRelayIndex,
|
||||
"responderRelayIndex", m.ResponderRelayIndex,
|
||||
"vpnAddrs", h.vpnAddrs,
|
||||
)
|
||||
|
||||
target := m.RelayToAddr
|
||||
targetAddr := protoAddrToNetAddr(target)
|
||||
|
||||
relay, err := rm.EstablishRelay(h, m)
|
||||
if err != nil {
|
||||
rm.l.Error("Failed to update relay for relayTo", "error", err)
|
||||
@@ -325,7 +368,7 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
|
||||
rm.l.Error("Can't find a HostInfo for peer", "relayTo", relay.PeerAddr)
|
||||
return
|
||||
}
|
||||
peerRelay, ok := peerHostInfo.relayState.QueryRelayForByIp(targetAddr)
|
||||
peerRelay, ok := peerHostInfo.relayState.QueryRelayForByIp(relayTo)
|
||||
if !ok {
|
||||
rm.l.Error("peerRelay does not have Relay state for relayTo", "relayTo", peerHostInfo.vpnAddrs[0])
|
||||
return
|
||||
@@ -335,19 +378,19 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
|
||||
// I initiated the request to this peer, but haven't heard back from the peer yet. I must wait for this peer
|
||||
// to respond to complete the connection.
|
||||
case PeerRequested, Disestablished, Established:
|
||||
peerHostInfo.relayState.UpdateRelayForByIpState(targetAddr, Established)
|
||||
peerHostInfo.relayState.UpdateRelayForByIpState(relayTo, Established)
|
||||
resp := NebulaControl{
|
||||
Type: NebulaControl_CreateRelayResponse,
|
||||
ResponderRelayIndex: peerRelay.LocalIndex,
|
||||
InitiatorRelayIndex: peerRelay.RemoteIndex,
|
||||
}
|
||||
|
||||
peer := peerHostInfo.vpnAddrs[0]
|
||||
if v == cert.Version1 {
|
||||
peer := peerHostInfo.vpnAddrs[0]
|
||||
if !peer.Is4() {
|
||||
rm.l.Error("Refusing to CreateRelayResponse for a v1 relay with an ipv6 address",
|
||||
"relayFrom", peer,
|
||||
"relayTo", target,
|
||||
"relayTo", relayTo,
|
||||
"initiatorRelayIndex", resp.InitiatorRelayIndex,
|
||||
"responderRelayIndex", resp.ResponderRelayIndex,
|
||||
"vpnAddrs", peerHostInfo.vpnAddrs,
|
||||
@@ -357,30 +400,31 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
|
||||
|
||||
b := peer.As4()
|
||||
resp.OldRelayFromAddr = binary.BigEndian.Uint32(b[:])
|
||||
b = targetAddr.As4()
|
||||
b = relayTo.As4()
|
||||
resp.OldRelayToAddr = binary.BigEndian.Uint32(b[:])
|
||||
} else {
|
||||
resp.RelayFromAddr = netAddrToProtoAddr(peerHostInfo.vpnAddrs[0])
|
||||
resp.RelayToAddr = target
|
||||
resp.RelayFromAddr = netAddrToProtoAddr(peer)
|
||||
resp.RelayToAddr = m.RelayToAddr
|
||||
}
|
||||
|
||||
msg, err := resp.Marshal()
|
||||
if err != nil {
|
||||
rm.l.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err)
|
||||
} else {
|
||||
f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||
rm.l.Info("send CreateRelayResponse",
|
||||
"relayFrom", resp.RelayFromAddr,
|
||||
"relayTo", resp.RelayToAddr,
|
||||
"initiatorRelayIndex", resp.InitiatorRelayIndex,
|
||||
"responderRelayIndex", resp.ResponderRelayIndex,
|
||||
"vpnAddrs", peerHostInfo.vpnAddrs,
|
||||
)
|
||||
return
|
||||
}
|
||||
f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||
rm.l.Info("send CreateRelayResponse",
|
||||
"relayFrom", peer,
|
||||
"relayTo", relayTo,
|
||||
"initiatorRelayIndex", resp.InitiatorRelayIndex,
|
||||
"responderRelayIndex", resp.ResponderRelayIndex,
|
||||
"vpnAddrs", peerHostInfo.vpnAddrs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) {
|
||||
//nil-checks for protoAddrToNetAddr handled by caller
|
||||
from := protoAddrToNetAddr(m.RelayFromAddr)
|
||||
target := protoAddrToNetAddr(m.RelayToAddr)
|
||||
|
||||
@@ -490,7 +534,7 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
|
||||
f.Handshake(target)
|
||||
return
|
||||
}
|
||||
if !peer.remote.IsValid() {
|
||||
if !peer.GetRemote().IsValid() {
|
||||
// Only create relays to peers for whom I have a direct connection
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestStartRelaysLogDedupe verifies that repeated attempts with the same relay set drop the log
|
||||
// chatter to Debug, mirroring how the normal handshake retry loop quiets down once it's already
|
||||
// announced its targets.
|
||||
func TestStartRelaysLogDedupe(t *testing.T) {
|
||||
vpnIp := netip.MustParseAddr("100.64.99.4")
|
||||
otherRelay := netip.MustParseAddr("100.64.99.5")
|
||||
|
||||
newHH := func() *HandshakeHostInfo {
|
||||
// Use the target's own vpnIp as the "relay" so the loop body skips it without
|
||||
// touching any sender-side state. That isolates the test to the level-selection
|
||||
// behavior of the top-level "Attempt to relay through hosts" log.
|
||||
hostinfo := &HostInfo{
|
||||
vpnAddrs: []netip.Addr{vpnIp},
|
||||
localIndexId: 1,
|
||||
remotes: NewRemoteList([]netip.Addr{vpnIp}, nil),
|
||||
}
|
||||
hostinfo.remotes.relays = []netip.Addr{vpnIp}
|
||||
return &HandshakeHostInfo{hostinfo: hostinfo}
|
||||
}
|
||||
|
||||
// Park any extra relay addresses we'll introduce mid-test in myVpnAddrsTable so the loop
|
||||
// body always skips before touching f.Handshake (which would need a real handshakeManager).
|
||||
addrTable := new(bart.Lite)
|
||||
addrTable.Insert(netip.PrefixFrom(otherRelay, otherRelay.BitLen()))
|
||||
f := &Interface{myVpnAddrsTable: addrTable}
|
||||
|
||||
newRM := func(buf *bytes.Buffer) *relayManager {
|
||||
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelDebug)
|
||||
rm := &relayManager{l: l, hostmap: newHostMap(l)}
|
||||
rm.useRelays.Store(true)
|
||||
return rm
|
||||
}
|
||||
|
||||
const msg = `msg="Attempt to relay through hosts"`
|
||||
|
||||
t.Run("first attempt logs at Info", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
rm := newRM(&buf)
|
||||
hh := newHH()
|
||||
rm.StartRelays(f, vpnIp, hh, nil)
|
||||
assert.Equal(t, []netip.Addr{vpnIp}, hh.lastRelays, "lastRelays should record the relay set we just attempted")
|
||||
assert.Contains(t, buf.String(), "level=INFO "+msg, "expected Info level on first attempt")
|
||||
})
|
||||
|
||||
t.Run("repeat attempt with same relays drops to Debug", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
rm := newRM(&buf)
|
||||
hh := newHH()
|
||||
rm.StartRelays(f, vpnIp, hh, nil)
|
||||
first := append([]netip.Addr(nil), hh.lastRelays...)
|
||||
buf.Reset()
|
||||
rm.StartRelays(f, vpnIp, hh, nil)
|
||||
assert.Equal(t, first, hh.lastRelays)
|
||||
assert.Contains(t, buf.String(), "level=DEBUG "+msg, "expected Debug level on identical retry")
|
||||
assert.NotContains(t, buf.String(), "level=INFO "+msg, "Info should not fire on identical retry")
|
||||
})
|
||||
|
||||
t.Run("changed relay list bumps back to Info", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
rm := newRM(&buf)
|
||||
hh := newHH()
|
||||
rm.StartRelays(f, vpnIp, hh, nil)
|
||||
buf.Reset()
|
||||
|
||||
// The lighthouse handed us a new set this round.
|
||||
hh.hostinfo.remotes.relays = []netip.Addr{vpnIp, otherRelay}
|
||||
|
||||
rm.StartRelays(f, vpnIp, hh, nil)
|
||||
assert.Equal(t, []netip.Addr{vpnIp, otherRelay}, hh.lastRelays)
|
||||
assert.Contains(t, buf.String(), "level=INFO "+msg, "expected Info when the relay list changes")
|
||||
})
|
||||
|
||||
t.Run("disabled relays clears lastRelays and emits no Attempt log", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
rm := newRM(&buf)
|
||||
rm.useRelays.Store(false)
|
||||
hh := newHH()
|
||||
hh.lastRelays = []netip.Addr{vpnIp}
|
||||
|
||||
rm.StartRelays(f, vpnIp, hh, nil)
|
||||
assert.Nil(t, hh.lastRelays, "with relays disabled lastRelays should be cleared")
|
||||
assert.NotContains(t, buf.String(), msg, "should not log when we shortcut out")
|
||||
})
|
||||
}
|
||||
@@ -239,6 +239,31 @@ func (r *RemoteList) unlockedSetHostnamesResults(hr *hostnamesResults) {
|
||||
r.hr = hr
|
||||
}
|
||||
|
||||
// ResetForOwner zeros the reported address slices for the given owner and marks the addrs list dirty.
|
||||
// Any pending hostname resolution will be canceled.
|
||||
func (r *RemoteList) ResetForOwner(ownerVpnAddr netip.Addr) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.hr.Cancel()
|
||||
if c, ok := r.cache[ownerVpnAddr]; ok {
|
||||
if c.v4 != nil {
|
||||
c.v4.reported = c.v4.reported[:0]
|
||||
}
|
||||
if c.v6 != nil {
|
||||
c.v6.reported = c.v6.reported[:0]
|
||||
}
|
||||
}
|
||||
r.shouldRebuild = true
|
||||
}
|
||||
|
||||
// ClearHostnameResults cancels the in-flight DNS resolver goroutine (if any) and drops the resolved IP cache.
|
||||
func (r *RemoteList) ClearHostnameResults() {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.unlockedSetHostnamesResults(nil)
|
||||
r.shouldRebuild = true
|
||||
}
|
||||
|
||||
// Len locks and reports the size of the deduplicated address list
|
||||
// The deduplication work may need to occur here, so you must pass preferredRanges
|
||||
func (r *RemoteList) Len(preferredRanges []netip.Prefix) int {
|
||||
@@ -319,6 +344,9 @@ func (r *RemoteList) CopyCache() *CacheMap {
|
||||
}
|
||||
|
||||
for _, a := range mc.v4.reported {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
c.Reported = append(c.Reported, protoV4AddrPortToNetAddrPort(a))
|
||||
}
|
||||
}
|
||||
@@ -329,6 +357,9 @@ func (r *RemoteList) CopyCache() *CacheMap {
|
||||
}
|
||||
|
||||
for _, a := range mc.v6.reported {
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
c.Reported = append(c.Reported, protoV6AddrPortToNetAddrPort(a))
|
||||
}
|
||||
}
|
||||
@@ -557,6 +588,9 @@ func (r *RemoteList) unlockedCollect() {
|
||||
}
|
||||
|
||||
for _, v := range c.v4.reported {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
u := protoV4AddrPortToNetAddrPort(v)
|
||||
if !r.unlockedIsBad(u) {
|
||||
addrs = append(addrs, u)
|
||||
@@ -573,6 +607,9 @@ func (r *RemoteList) unlockedCollect() {
|
||||
}
|
||||
|
||||
for _, v := range c.v6.reported {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
u := protoV6AddrPortToNetAddrPort(v)
|
||||
if !r.unlockedIsBad(u) {
|
||||
addrs = append(addrs, u)
|
||||
|
||||
@@ -6,8 +6,22 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// trackedHostnameResults builds a *hostnamesResults with a known cancel function and a
|
||||
// pre-populated ips map so tests can assert cancellation and verify previously-resolved
|
||||
// IPs survive a cancel without spinning up a real DNS resolver.
|
||||
func trackedHostnameResults(cancelFn func(), addrs ...string) *hostnamesResults {
|
||||
hr := &hostnamesResults{cancelFn: cancelFn}
|
||||
ips := map[netip.AddrPort]struct{}{}
|
||||
for _, a := range addrs {
|
||||
ips[netip.MustParseAddrPort(a)] = struct{}{}
|
||||
}
|
||||
hr.ips.Store(&ips)
|
||||
return hr
|
||||
}
|
||||
|
||||
func TestRemoteList_Rebuild(t *testing.T) {
|
||||
rl := NewRemoteList([]netip.Addr{netip.MustParseAddr("0.0.0.0")}, nil)
|
||||
rl.unlockedSetV4(
|
||||
@@ -112,6 +126,81 @@ func TestRemoteList_Rebuild(t *testing.T) {
|
||||
assert.Equal(t, "172.31.0.1:10101", rl.addrs[9].String())
|
||||
}
|
||||
|
||||
func TestRemoteList_ResetForOwner(t *testing.T) {
|
||||
ourselves := netip.MustParseAddr("10.0.0.1")
|
||||
otherOwner := netip.MustParseAddr("10.0.0.2")
|
||||
vpnAddr := netip.MustParseAddr("10.0.0.99")
|
||||
|
||||
rl := NewRemoteList([]netip.Addr{vpnAddr}, nil)
|
||||
rl.unlockedSetV4(ourselves, vpnAddr,
|
||||
[]*V4AddrPort{newIp4AndPortFromString("1.1.1.1:4242")},
|
||||
func(netip.Addr, *V4AddrPort) bool { return true },
|
||||
)
|
||||
rl.unlockedSetV6(ourselves, vpnAddr,
|
||||
[]*V6AddrPort{newIp6AndPortFromString("[1::1]:4242")},
|
||||
func(netip.Addr, *V6AddrPort) bool { return true },
|
||||
)
|
||||
rl.unlockedSetV4(otherOwner, vpnAddr,
|
||||
[]*V4AddrPort{newIp4AndPortFromString("2.2.2.2:4242")},
|
||||
func(netip.Addr, *V4AddrPort) bool { return true },
|
||||
)
|
||||
|
||||
canceled := 0
|
||||
hr := trackedHostnameResults(func() { canceled++ }, "3.3.3.3:4242")
|
||||
rl.Lock()
|
||||
rl.unlockedSetHostnamesResults(hr)
|
||||
rl.Unlock()
|
||||
|
||||
rl.ResetForOwner(ourselves)
|
||||
|
||||
rl.RLock()
|
||||
defer rl.RUnlock()
|
||||
assert.Empty(t, rl.cache[ourselves].v4.reported, "our v4 reported should be cleared")
|
||||
assert.Empty(t, rl.cache[ourselves].v6.reported, "our v6 reported should be cleared")
|
||||
assert.Len(t, rl.cache[otherOwner].v4.reported, 1, "other owner's contribution must be preserved")
|
||||
assert.Equal(t, "2.2.2.2:4242", protoV4AddrPortToNetAddrPort(rl.cache[otherOwner].v4.reported[0]).String())
|
||||
assert.Equal(t, 1, canceled, "DNS resolution goroutine should be canceled")
|
||||
assert.Same(t, hr, rl.hr, "hostnamesResults must be preserved so DNS-resolved IPs keep feeding addrs until replaced")
|
||||
assert.NotEmpty(t, rl.hr.GetAddrs(), "previously-resolved IPs should still be readable after cancel")
|
||||
assert.True(t, rl.shouldRebuild, "shouldRebuild must be set so the next Rebuild recomputes addrs")
|
||||
}
|
||||
|
||||
func TestRemoteList_ResetForOwner_NoEntry(t *testing.T) {
|
||||
// An owner with no cache entry must not panic; shouldRebuild is still set and any
|
||||
// existing hostnamesResults is canceled.
|
||||
rl := NewRemoteList([]netip.Addr{netip.MustParseAddr("10.0.0.99")}, nil)
|
||||
canceled := 0
|
||||
rl.Lock()
|
||||
rl.unlockedSetHostnamesResults(trackedHostnameResults(func() { canceled++ }, "3.3.3.3:4242"))
|
||||
rl.Unlock()
|
||||
|
||||
rl.ResetForOwner(netip.MustParseAddr("10.0.0.1"))
|
||||
|
||||
rl.RLock()
|
||||
defer rl.RUnlock()
|
||||
assert.Equal(t, 1, canceled)
|
||||
assert.True(t, rl.shouldRebuild)
|
||||
}
|
||||
|
||||
func TestRemoteList_ClearHostnameResults(t *testing.T) {
|
||||
rl := NewRemoteList([]netip.Addr{netip.MustParseAddr("10.0.0.99")}, nil)
|
||||
|
||||
canceled := 0
|
||||
hr := trackedHostnameResults(func() { canceled++ }, "3.3.3.3:4242")
|
||||
rl.Lock()
|
||||
rl.unlockedSetHostnamesResults(hr)
|
||||
rl.Unlock()
|
||||
require.NotEmpty(t, hr.GetAddrs(), "hostnamesResults should have its fastrack IPs populated")
|
||||
|
||||
rl.ClearHostnameResults()
|
||||
|
||||
rl.RLock()
|
||||
defer rl.RUnlock()
|
||||
assert.Equal(t, 1, canceled, "DNS resolution goroutine should be canceled")
|
||||
assert.Nil(t, rl.hr, "hostnamesResults should be dropped")
|
||||
assert.True(t, rl.shouldRebuild)
|
||||
}
|
||||
|
||||
func BenchmarkFullRebuild(b *testing.B) {
|
||||
rl := NewRemoteList([]netip.Addr{netip.MustParseAddr("0.0.0.0")}, nil)
|
||||
rl.unlockedSetV4(
|
||||
|
||||
+16
-8
@@ -43,12 +43,25 @@ type Service struct {
|
||||
}
|
||||
}
|
||||
|
||||
func New(control *nebula.Control) (*Service, error) {
|
||||
wait, err := control.Start()
|
||||
func New(control *nebula.Control) (_ *Service, reterr error) {
|
||||
// Check this before Start so a failure doesn't leave a running nebula
|
||||
device, ok := control.Device().(*overlay.UserDevice)
|
||||
if !ok {
|
||||
return nil, errors.New("must be using user device")
|
||||
}
|
||||
|
||||
err := control.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Anything that fails after a successful Start must tear nebula back down
|
||||
defer func() {
|
||||
if reterr != nil {
|
||||
control.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := control.Context()
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
s := Service{
|
||||
@@ -57,11 +70,6 @@ func New(control *nebula.Control) (*Service, error) {
|
||||
}
|
||||
s.mu.listeners = map[uint16]*tcpListener{}
|
||||
|
||||
device, ok := control.Device().(*overlay.UserDevice)
|
||||
if !ok {
|
||||
return nil, errors.New("must be using user device")
|
||||
}
|
||||
|
||||
s.ipstack = stack.New(stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
|
||||
@@ -147,7 +155,7 @@ func New(control *nebula.Control) (*Service, error) {
|
||||
// Add the nebula wait function to the group so a fatal reader error
|
||||
// propagates out through errgroup.Wait().
|
||||
eg.Go(func() error {
|
||||
return wait()
|
||||
return control.Wait()
|
||||
})
|
||||
|
||||
return &s, nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -18,6 +19,8 @@ type SSHServer struct {
|
||||
|
||||
certChecker *ssh.CertChecker
|
||||
|
||||
// authLock guards trustedKeys and trustedCAs
|
||||
authLock sync.RWMutex
|
||||
// Map of user -> authorized keys
|
||||
trustedKeys map[string]map[string]bool
|
||||
trustedCAs []ssh.PublicKey
|
||||
@@ -45,6 +48,8 @@ func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
|
||||
|
||||
cc := ssh.CertChecker{
|
||||
IsUserAuthority: func(auth ssh.PublicKey) bool {
|
||||
s.authLock.RLock()
|
||||
defer s.authLock.RUnlock()
|
||||
for _, ca := range s.trustedCAs {
|
||||
if bytes.Equal(ca.Marshal(), auth.Marshal()) {
|
||||
return true
|
||||
@@ -57,6 +62,8 @@ func NewSSHServer(ctx context.Context, l *slog.Logger) (*SSHServer, error) {
|
||||
pk := string(pubKey.Marshal())
|
||||
fp := ssh.FingerprintSHA256(pubKey)
|
||||
|
||||
s.authLock.RLock()
|
||||
defer s.authLock.RUnlock()
|
||||
tk, ok := s.trustedKeys[c.User()]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown user %s", c.User())
|
||||
@@ -105,11 +112,15 @@ func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error {
|
||||
}
|
||||
|
||||
func (s *SSHServer) ClearTrustedCAs() {
|
||||
s.authLock.Lock()
|
||||
s.trustedCAs = []ssh.PublicKey{}
|
||||
s.authLock.Unlock()
|
||||
}
|
||||
|
||||
func (s *SSHServer) ClearAuthorizedKeys() {
|
||||
s.authLock.Lock()
|
||||
s.trustedKeys = make(map[string]map[string]bool)
|
||||
s.authLock.Unlock()
|
||||
}
|
||||
|
||||
// AddTrustedCA adds a trusted CA for user certificates
|
||||
@@ -119,7 +130,9 @@ func (s *SSHServer) AddTrustedCA(pubKey string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.authLock.Lock()
|
||||
s.trustedCAs = append(s.trustedCAs, pk)
|
||||
s.authLock.Unlock()
|
||||
s.l.Info("Trusted CA key", "sshKey", pubKey)
|
||||
return nil
|
||||
}
|
||||
@@ -131,6 +144,7 @@ func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.authLock.Lock()
|
||||
tk, ok := s.trustedKeys[user]
|
||||
if !ok {
|
||||
tk = make(map[string]bool)
|
||||
@@ -138,6 +152,7 @@ func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
|
||||
}
|
||||
|
||||
tk[string(pk.Marshal())] = true
|
||||
s.authLock.Unlock()
|
||||
s.l.Info("Authorized ssh key",
|
||||
"sshKey", pubKey,
|
||||
"sshUser", user,
|
||||
|
||||
@@ -331,7 +331,7 @@ func loadStatsConfig(c *config.C) (statsConfig, error) {
|
||||
}
|
||||
|
||||
cfg.interval = c.GetDuration("stats.interval", 0)
|
||||
if cfg.interval == 0 {
|
||||
if cfg.interval <= 0 {
|
||||
return cfg, fmt.Errorf("stats.interval was an invalid duration: %s", c.GetString("stats.interval", ""))
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -175,8 +175,8 @@ func (u *StdConn) ListenOut(r EncReader) error {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
|
||||
u.l.Error("unexpected udp socket receive error", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
r(netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port()), buffer[:n])
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//go:build linux && !android && !e2e_testing
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// PinThreadToCPU restricts the calling OS thread to the given CPU via
|
||||
// sched_setaffinity(2). Combined with runtime.LockOSThread on the
|
||||
// goroutine, this prevents the kernel from migrating us across CPUs and
|
||||
// in turn keeps every UDP send from this goroutine going through the
|
||||
// same XPS-selected TX ring, eliminating the wire-side reorder that
|
||||
// otherwise fragments one nebula flow across multiple rings.
|
||||
func PinThreadToCPU(cpu int) error {
|
||||
runtime.LockOSThread()
|
||||
var set unix.CPUSet
|
||||
set.Zero()
|
||||
set.Set(cpu)
|
||||
return unix.SchedSetaffinity(0, &set)
|
||||
}
|
||||
|
||||
// AllowedCPUs returns the CPU IDs the calling process is currently allowed to
|
||||
// run on, as reported by sched_getaffinity(2). Under a cgroup cpuset or a
|
||||
// `taskset` mask the allowed IDs are frequently not the contiguous range
|
||||
// 0..NumCPU-1 (e.g. pinned to CPUs 4-7: NumCPU reports 4 while the valid IDs
|
||||
// are 4,5,6,7). Callers that need a real CPU to pin to must choose from this
|
||||
// set rather than assuming i % NumCPU is runnable, or every pin fails.
|
||||
func AllowedCPUs() ([]int, error) {
|
||||
var set unix.CPUSet
|
||||
if err := unix.SchedGetaffinity(0, &set); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cpus := make([]int, 0, set.Count())
|
||||
for cpu := 0; cpu < len(set)*64; cpu++ {
|
||||
if set.IsSet(cpu) {
|
||||
cpus = append(cpus, cpu)
|
||||
}
|
||||
}
|
||||
return cpus, nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build !linux || android || e2e_testing
|
||||
|
||||
package util
|
||||
|
||||
// PinThreadToCPU is a no-op outside Linux: only Linux exposes a stable
|
||||
// per-thread CPU affinity API and only Linux has XPS-driven TX ring
|
||||
// selection in the first place. On every other platform there's nothing
|
||||
// to fix here.
|
||||
func PinThreadToCPU(_ int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllowedCPUs has no meaningful answer off Linux (no sched_getaffinity), so it
|
||||
// reports "unknown" by returning a nil slice and nil error. Callers treat an
|
||||
// empty result as "fall back to the default CPU choice".
|
||||
func AllowedCPUs() ([]int, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user