mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 10:26:59 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4b7f624da | |||
| 1c069a8e42 | |||
| 0d8bd11818 | |||
| 5128e2653e | |||
| c73b2dfbc7 | |||
| 3dea761530 | |||
| b394112ad9 |
@@ -1,116 +0,0 @@
|
||||
name: Code-sign Windows binaries
|
||||
description: >
|
||||
Sign every .exe under a given path in place via the DefinedNet code-signer
|
||||
Lambda. If `role` or `bucket` is empty, logs a notice and skips signing so
|
||||
forks and dev branches without AWS access still produce usable builds.
|
||||
|
||||
inputs:
|
||||
path:
|
||||
description: "Directory whose .exe files should be signed in place"
|
||||
required: true
|
||||
role:
|
||||
description: "IAM role ARN to assume via OIDC; empty disables signing"
|
||||
required: false
|
||||
default: ""
|
||||
bucket:
|
||||
description: "S3 staging bucket the code-signer Lambda reads from; empty disables signing"
|
||||
required: false
|
||||
default: ""
|
||||
region:
|
||||
description: "AWS region for the role and Lambda"
|
||||
required: false
|
||||
default: "us-east-2"
|
||||
function-name:
|
||||
description: "Code-signer Lambda function name"
|
||||
required: false
|
||||
default: "code-signer"
|
||||
key-prefix:
|
||||
description: "S3 key prefix to write under; defaults to code-signing/<owner>/<repo> of the calling repo"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Skip notice
|
||||
if: inputs.role == '' || inputs.bucket == ''
|
||||
shell: sh
|
||||
run: echo "::notice::code-signer role or bucket not set; skipping code signing."
|
||||
|
||||
- name: Configure AWS credentials
|
||||
if: inputs.role != '' && inputs.bucket != ''
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: ${{ inputs.role }}
|
||||
aws-region: ${{ inputs.region }}
|
||||
# Default is 12 retries to ride out IAM trust-policy propagation; once
|
||||
# the role is stable we want a real misconfiguration to fail fast.
|
||||
retry-max-attempts: 5
|
||||
|
||||
- name: Sign .exe files
|
||||
if: inputs.role != '' && inputs.bucket != ''
|
||||
shell: sh
|
||||
env:
|
||||
SIGN_PATH: ${{ inputs.path }}
|
||||
BUCKET: ${{ inputs.bucket }}
|
||||
FUNCTION_NAME: ${{ inputs.function-name }}
|
||||
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
|
||||
do
|
||||
rel=${path#"$SIGN_PATH"/}
|
||||
file=$(basename "$path")
|
||||
name=${file%.exe}
|
||||
prefix="${KEY_PREFIX}/${RUN}"
|
||||
src="${prefix}/unsigned/${rel}"
|
||||
dst="${prefix}/signed/${rel}"
|
||||
|
||||
echo "::group::Sign ${rel}"
|
||||
echo "Uploading unsigned to s3://${BUCKET}/${src}"
|
||||
aws s3 cp --no-progress "$path" "s3://${BUCKET}/${src}" >/dev/null
|
||||
|
||||
echo "Invoking ${FUNCTION_NAME} Lambda"
|
||||
payload=$(jq -nc \
|
||||
--arg s "$src" \
|
||||
--arg d "$dst" \
|
||||
--arg p "$name" \
|
||||
'{source_key: $s, dest_key: $d, program_name: $p}')
|
||||
meta=$(aws lambda invoke \
|
||||
--function-name "$FUNCTION_NAME" \
|
||||
--cli-binary-format raw-in-base64-out \
|
||||
--payload "$payload" \
|
||||
--output json \
|
||||
/tmp/sign-resp.json)
|
||||
if echo "$meta" | jq -e '.FunctionError != null' >/dev/null
|
||||
then
|
||||
echo "::endgroup::"
|
||||
echo "::error::code-signer Lambda failed for ${rel}"
|
||||
cat /tmp/sign-resp.json >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Downloading signed back to ${path}"
|
||||
aws s3 cp --no-progress "s3://${BUCKET}/${dst}" "$path" >/dev/null
|
||||
|
||||
aws s3 rm "s3://${BUCKET}/${src}" >/dev/null 2>&1 || true
|
||||
aws s3 rm "s3://${BUCKET}/${dst}" >/dev/null 2>&1 || true
|
||||
|
||||
# Sanity-check the bytes we got back actually carry an Authenticode
|
||||
# signature that this machine can validate end to end.
|
||||
status=$(powershell -NoProfile -Command "(Get-AuthenticodeSignature -FilePath '$path').Status" | tr -d '\r')
|
||||
if [ "$status" != "Valid" ]
|
||||
then
|
||||
echo "::endgroup::"
|
||||
echo "::error::${rel} signature status: ${status} (expected Valid)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Signed ${rel} (sha256=$(jq -r '.sha256' /tmp/sign-resp.json), status=${status})"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
@@ -0,0 +1,34 @@
|
||||
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@v4
|
||||
|
||||
- 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,11 +10,11 @@ jobs:
|
||||
name: Build Linux/BSD All
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
mv build/*.tar.gz release
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-latest
|
||||
path: release
|
||||
@@ -32,15 +32,12 @@ jobs:
|
||||
build-windows:
|
||||
name: Build Windows
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build
|
||||
@@ -57,15 +54,8 @@ jobs:
|
||||
mkdir build\dist\windows
|
||||
mv dist\windows\wintun build\dist\windows\
|
||||
|
||||
- name: Code-sign
|
||||
uses: ./.github/actions/code-sign
|
||||
with:
|
||||
path: build
|
||||
role: ${{ secrets.DEFINED_CODE_SIGNER_ROLE }}
|
||||
bucket: ${{ secrets.DEFINED_CODE_SIGNER_BUCKET }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-latest
|
||||
path: build
|
||||
@@ -76,16 +66,16 @@ jobs:
|
||||
HAS_SIGNING_CREDS: ${{ secrets.AC_USERNAME != '' }}
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Import certificates
|
||||
if: env.HAS_SIGNING_CREDS == 'true'
|
||||
uses: Apple-Actions/import-codesign-certs@v7
|
||||
uses: Apple-Actions/import-codesign-certs@v5
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
||||
p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
||||
@@ -114,7 +104,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: darwin-latest
|
||||
path: ./release/*
|
||||
@@ -134,25 +124,25 @@ jobs:
|
||||
# be overwritten
|
||||
- name: Checkout code
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux-latest
|
||||
path: artifacts
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push images
|
||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||
@@ -163,20 +153,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 \
|
||||
--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}"
|
||||
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}"
|
||||
|
||||
release:
|
||||
name: Create and Upload Release
|
||||
needs: [build-linux, build-darwin, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
@@ -222,11 +209,10 @@ jobs:
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
cd artifacts
|
||||
gh release create \
|
||||
--verify-tag \
|
||||
--title "Release ${GITHUB_REF_NAME}" \
|
||||
"${GITHUB_REF_NAME}" \
|
||||
--title "Release ${{ github.ref_name }}" \
|
||||
"${{ github.ref_name }}" \
|
||||
SHASUM256.txt *-latest/*.zip *-latest/*.tar.gz
|
||||
|
||||
@@ -14,119 +14,38 @@ on:
|
||||
- 'go.sum'
|
||||
jobs:
|
||||
|
||||
smoke-extra-libvirt:
|
||||
smoke-extra:
|
||||
if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'smoke-test-extra')
|
||||
name: ${{ matrix.target }}
|
||||
name: Run extra smoke tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- freebsd-amd64
|
||||
- openbsd-amd64
|
||||
- netbsd-amd64
|
||||
- linux-amd64-ipv6disable
|
||||
env:
|
||||
VAGRANT_DEFAULT_PROVIDER: libvirt
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: add hashicorp source
|
||||
run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||
|
||||
- name: install vagrant and libvirt
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y vagrant libvirt-daemon-system libvirt-dev
|
||||
sudo chmod 666 /dev/kvm
|
||||
sudo usermod -aG libvirt $(whoami)
|
||||
sudo chmod 666 /var/run/libvirt/libvirt-sock
|
||||
vagrant plugin install vagrant-libvirt
|
||||
- name: install vagrant
|
||||
run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox
|
||||
|
||||
- name: ${{ matrix.target }}
|
||||
run: make smoke-vagrant/${{ matrix.target }}
|
||||
- name: freebsd-amd64
|
||||
run: make smoke-vagrant/freebsd-amd64
|
||||
|
||||
timeout-minutes: 30
|
||||
- name: openbsd-amd64
|
||||
run: make smoke-vagrant/openbsd-amd64
|
||||
|
||||
# linux-386 needs VirtualBox, which conflicts with KVM/libvirt -- isolated job.
|
||||
smoke-extra-virtualbox:
|
||||
if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'smoke-test-extra')
|
||||
name: linux-386
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VAGRANT_DEFAULT_PROVIDER: virtualbox
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: '1.26'
|
||||
check-latest: true
|
||||
|
||||
- name: add hashicorp source
|
||||
run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||
|
||||
- name: install vagrant and virtualbox
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y vagrant virtualbox
|
||||
sudo rmmod kvm_amd kvm_intel kvm 2>/dev/null || true
|
||||
- name: netbsd-amd64
|
||||
run: make smoke-vagrant/netbsd-amd64
|
||||
|
||||
- name: linux-386
|
||||
run: make smoke-vagrant/linux-386
|
||||
|
||||
- name: linux-amd64-ipv6disable
|
||||
run: make smoke-vagrant/linux-amd64-ipv6disable
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
smoke-windows:
|
||||
if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'smoke-test-extra')
|
||||
name: Run windows smoke test
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: '1.26'
|
||||
check-latest: true
|
||||
|
||||
# WSL2 + Ubuntu so the smoke can run a real linux peer with its own
|
||||
# netns. iputils-ping is needed for the in-WSL ping check. WSL1 has no
|
||||
# real kernel and would lack /dev/net/tun, so we have to force WSL2.
|
||||
- uses: Vampire/setup-wsl@v3
|
||||
with:
|
||||
distribution: Ubuntu-24.04
|
||||
additional-packages: iputils-ping iproute2
|
||||
|
||||
# Vampire/setup-wsl provisions WSL1 even when the WSL2 platform is present.
|
||||
# Convert the distro to WSL2 explicitly before we try to use /dev/net/tun.
|
||||
- name: convert distro to WSL2
|
||||
shell: pwsh
|
||||
run: |
|
||||
wsl --set-version Ubuntu-24.04 2
|
||||
wsl --shutdown
|
||||
wsl --list --verbose
|
||||
|
||||
- name: build windows nebula
|
||||
run: make bin-windows
|
||||
|
||||
- name: build linux nebula for WSL
|
||||
shell: bash
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: |
|
||||
mkdir -p build/linux-amd64
|
||||
go build -o build/linux-amd64/nebula ./cmd/nebula
|
||||
|
||||
- name: run smoke-windows
|
||||
shell: pwsh
|
||||
working-directory: ./.github/workflows/smoke
|
||||
run: ./smoke-windows.ps1
|
||||
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -18,11 +18,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: build
|
||||
@@ -36,14 +36,6 @@ 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
|
||||
|
||||
@@ -16,10 +16,8 @@ relay:
|
||||
am_relay: true
|
||||
EOF
|
||||
|
||||
# TEST-NET-3 placeholder IPs; smoke-relay.sh seds them to real container IPs.
|
||||
# Mapping: .2 lighthouse1, .3 host2, .4 host3, .5 host4.
|
||||
export LIGHTHOUSES="192.168.100.1 203.0.113.2:4242"
|
||||
export REMOTE_ALLOW_LIST='{"203.0.113.4/32": false, "203.0.113.5/32": false}'
|
||||
export LIGHTHOUSES="192.168.100.1 172.17.0.2:4242"
|
||||
export REMOTE_ALLOW_LIST='{"172.17.0.4/32": false, "172.17.0.5/32": false}'
|
||||
|
||||
HOST="host2" ../genconfig.sh >host2.yml <<EOF
|
||||
relay:
|
||||
@@ -27,7 +25,7 @@ relay:
|
||||
- 192.168.100.1
|
||||
EOF
|
||||
|
||||
export REMOTE_ALLOW_LIST='{"203.0.113.3/32": false}'
|
||||
export REMOTE_ALLOW_LIST='{"172.17.0.3/32": false}'
|
||||
|
||||
HOST="host3" ../genconfig.sh >host3.yml
|
||||
|
||||
|
||||
@@ -5,28 +5,9 @@ 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
|
||||
# sed the real container IPs in before starting nebula.
|
||||
#
|
||||
# Placeholder mapping (last octet == fixed container slot):
|
||||
# 203.0.113.2 -> lighthouse1, 203.0.113.3 -> host2,
|
||||
# 203.0.113.4 -> host3, 203.0.113.5 -> host4.
|
||||
LIGHTHOUSE_IP="203.0.113.2"
|
||||
# TODO: Assumes your docker bridge network is a /24, and the first container that launches will be .1
|
||||
# - We could make this better by launching the lighthouse first and then fetching what IP it is.
|
||||
NET="$(docker network inspect bridge -f '{{ range .IPAM.Config }}{{ .Subnet }}{{ end }}' | cut -d. -f1-3)"
|
||||
|
||||
(
|
||||
cd build
|
||||
@@ -44,24 +25,24 @@ LIGHTHOUSE_IP="203.0.113.2"
|
||||
../genconfig.sh >lighthouse1.yml
|
||||
|
||||
HOST="host2" \
|
||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||
LIGHTHOUSES="192.168.100.1 $NET.2:4242" \
|
||||
../genconfig.sh >host2.yml
|
||||
|
||||
HOST="host3" \
|
||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||
LIGHTHOUSES="192.168.100.1 $NET.2:4242" \
|
||||
INBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
||||
../genconfig.sh >host3.yml
|
||||
|
||||
HOST="host4" \
|
||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||
LIGHTHOUSES="192.168.100.1 $NET.2: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 "$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"
|
||||
../../../../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"
|
||||
)
|
||||
|
||||
docker build -t "nebula:${NAME:-smoke}" .
|
||||
|
||||
@@ -6,8 +6,6 @@ set -o pipefail
|
||||
|
||||
mkdir -p logs
|
||||
|
||||
NETWORK="nebula-smoke-relay"
|
||||
|
||||
cleanup() {
|
||||
echo
|
||||
echo " *** cleanup"
|
||||
@@ -18,53 +16,22 @@ cleanup() {
|
||||
then
|
||||
docker kill lighthouse1 host2 host3 host4
|
||||
fi
|
||||
docker network rm "$NETWORK" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create a dedicated smoke network with an explicit subnet (required for --ip
|
||||
# below). Probe a short list of candidates so a locally-used range doesn't
|
||||
# fail the whole test — we only need one to be free.
|
||||
docker network rm "$NETWORK" >/dev/null 2>&1 || true
|
||||
for candidate in 172.30.0.0/24 172.31.0.0/24 10.98.0.0/24 10.99.0.0/24 192.168.230.0/24; do
|
||||
if docker network create --subnet "$candidate" "$NETWORK" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ! docker network inspect "$NETWORK" >/dev/null 2>&1; then
|
||||
echo "failed to create $NETWORK: every candidate subnet is in use" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Derive container IPs from the network's assigned subnet. Slots: .2 lighthouse1,
|
||||
# .3 host2, .4 host3, .5 host4 — matches the placeholders in build-relay.sh.
|
||||
SUBNET="$(docker network inspect -f '{{(index .IPAM.Config 0).Subnet}}' "$NETWORK")"
|
||||
PREFIX="${SUBNET%/*}"
|
||||
PREFIX="${PREFIX%.*}"
|
||||
LIGHTHOUSE_IP="$PREFIX.2"
|
||||
HOST2_IP="$PREFIX.3"
|
||||
HOST3_IP="$PREFIX.4"
|
||||
HOST4_IP="$PREFIX.5"
|
||||
|
||||
# Sed the placeholder TEST-NET-3 IPs in the host configs to the real ones.
|
||||
for f in build/host2.yml build/host3.yml build/host4.yml; do
|
||||
sed "s|203\.0\.113\.|$PREFIX.|g" "$f" >"$f.tmp"
|
||||
mv "$f.tmp" "$f"
|
||||
done
|
||||
|
||||
docker run --name lighthouse1 --rm nebula:smoke-relay -config lighthouse1.yml -test
|
||||
docker run --name host2 --rm -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" nebula:smoke-relay -config host2.yml -test
|
||||
docker run --name host3 --rm -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" nebula:smoke-relay -config host3.yml -test
|
||||
docker run --name host4 --rm -v "$PWD/build/host4.yml:/nebula/host4.yml:ro" nebula:smoke-relay -config host4.yml -test
|
||||
docker run --name host2 --rm nebula:smoke-relay -config host2.yml -test
|
||||
docker run --name host3 --rm nebula:smoke-relay -config host3.yml -test
|
||||
docker run --name host4 --rm nebula:smoke-relay -config host4.yml -test
|
||||
|
||||
docker run --name lighthouse1 --network "$NETWORK" --ip "$LIGHTHOUSE_IP" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config lighthouse1.yml 2>&1 | tee logs/lighthouse1 | sed -u 's/^/ [lighthouse1] /' &
|
||||
docker run --name lighthouse1 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config lighthouse1.yml 2>&1 | tee logs/lighthouse1 | sed -u 's/^/ [lighthouse1] /' &
|
||||
sleep 1
|
||||
docker run --name host2 --network "$NETWORK" --ip "$HOST2_IP" -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config host2.yml 2>&1 | tee logs/host2 | sed -u 's/^/ [host2] /' &
|
||||
docker run --name host2 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config host2.yml 2>&1 | tee logs/host2 | sed -u 's/^/ [host2] /' &
|
||||
sleep 1
|
||||
docker run --name host3 --network "$NETWORK" --ip "$HOST3_IP" -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config host3.yml 2>&1 | tee logs/host3 | sed -u 's/^/ [host3] /' &
|
||||
docker run --name host3 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config host3.yml 2>&1 | tee logs/host3 | sed -u 's/^/ [host3] /' &
|
||||
sleep 1
|
||||
docker run --name host4 --network "$NETWORK" --ip "$HOST4_IP" -v "$PWD/build/host4.yml:/nebula/host4.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config host4.yml 2>&1 | tee logs/host4 | sed -u 's/^/ [host4] /' &
|
||||
docker run --name host4 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm nebula:smoke-relay -config host4.yml 2>&1 | tee logs/host4 | sed -u 's/^/ [host4] /' &
|
||||
sleep 1
|
||||
|
||||
set +x
|
||||
@@ -109,13 +76,7 @@ docker exec host4 sh -c 'kill 1'
|
||||
docker exec host3 sh -c 'kill 1'
|
||||
docker exec host2 sh -c 'kill 1'
|
||||
docker exec lighthouse1 sh -c 'kill 1'
|
||||
|
||||
# Wait up to 30s for all backgrounded jobs to exit rather than relying on a
|
||||
# fixed sleep.
|
||||
for _ in $(seq 1 30); do
|
||||
[ -z "$(jobs -r)" ] && break
|
||||
sleep 1
|
||||
done
|
||||
sleep 5
|
||||
|
||||
if [ "$(jobs -r)" ]
|
||||
then
|
||||
|
||||
@@ -8,8 +8,6 @@ export VAGRANT_CWD="$PWD/vagrant-$1"
|
||||
|
||||
mkdir -p logs
|
||||
|
||||
NETWORK="nebula-smoke"
|
||||
|
||||
cleanup() {
|
||||
echo
|
||||
echo " *** cleanup"
|
||||
@@ -21,51 +19,21 @@ cleanup() {
|
||||
docker kill lighthouse1 host2
|
||||
fi
|
||||
vagrant destroy -f
|
||||
docker network rm "$NETWORK" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create a dedicated smoke network with an explicit subnet (required for --ip
|
||||
# below). Probe a short list of candidates so a locally-used range doesn't
|
||||
# fail the whole test — we only need one to be free.
|
||||
docker network rm "$NETWORK" >/dev/null 2>&1 || true
|
||||
for candidate in 172.30.0.0/24 172.31.0.0/24 10.98.0.0/24 10.99.0.0/24 192.168.230.0/24; do
|
||||
if docker network create --subnet "$candidate" "$NETWORK" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ! docker network inspect "$NETWORK" >/dev/null 2>&1; then
|
||||
echo "failed to create $NETWORK: every candidate subnet is in use" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Derive container IPs from the network's assigned subnet. Slots: .2 lighthouse1,
|
||||
# .3 host2 — matches the placeholders in build.sh.
|
||||
SUBNET="$(docker network inspect -f '{{(index .IPAM.Config 0).Subnet}}' "$NETWORK")"
|
||||
PREFIX="${SUBNET%/*}"
|
||||
PREFIX="${PREFIX%.*}"
|
||||
LIGHTHOUSE_IP="$PREFIX.2"
|
||||
HOST2_IP="$PREFIX.3"
|
||||
|
||||
# Sed the placeholder TEST-NET-3 IPs in the host configs to the real ones.
|
||||
# This must happen before `vagrant up` rsyncs build/ into the VM for host3.
|
||||
for f in build/host2.yml build/host3.yml; do
|
||||
sed "s|203\.0\.113\.|$PREFIX.|g" "$f" >"$f.tmp"
|
||||
mv "$f.tmp" "$f"
|
||||
done
|
||||
|
||||
CONTAINER="nebula:${NAME:-smoke}"
|
||||
|
||||
docker run --name lighthouse1 --rm "$CONTAINER" -config lighthouse1.yml -test
|
||||
docker run --name host2 --rm -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" "$CONTAINER" -config host2.yml -test
|
||||
docker run --name host2 --rm "$CONTAINER" -config host2.yml -test
|
||||
|
||||
vagrant up
|
||||
vagrant ssh -c "cd /nebula && /nebula/$1-nebula -config host3.yml -test" -- -T
|
||||
|
||||
docker run --name lighthouse1 --network "$NETWORK" --ip "$LIGHTHOUSE_IP" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config lighthouse1.yml 2>&1 | tee logs/lighthouse1 | sed -u 's/^/ [lighthouse1] /' &
|
||||
docker run --name lighthouse1 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config lighthouse1.yml 2>&1 | tee logs/lighthouse1 | sed -u 's/^/ [lighthouse1] /' &
|
||||
sleep 1
|
||||
docker run --name host2 --network "$NETWORK" --ip "$HOST2_IP" -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host2.yml 2>&1 | tee logs/host2 | sed -u 's/^/ [host2] /' &
|
||||
docker run --name host2 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host2.yml 2>&1 | tee logs/host2 | sed -u 's/^/ [host2] /' &
|
||||
sleep 1
|
||||
vagrant ssh -c "cd /nebula && sudo sh -c 'echo \$\$ >/nebula/pid && exec /nebula/$1-nebula -config host3.yml'" 2>&1 -- -T | tee logs/host3 | sed -u 's/^/ [host3] /' &
|
||||
sleep 15
|
||||
@@ -128,14 +96,7 @@ vagrant ssh -c "ping -c1 192.168.100.2" -- -T
|
||||
vagrant ssh -c "sudo xargs kill </nebula/pid" -- -T
|
||||
docker exec host2 sh -c 'kill 1'
|
||||
docker exec lighthouse1 sh -c 'kill 1'
|
||||
|
||||
# Wait up to 30s for all backgrounded jobs to exit. vagrant ssh in particular
|
||||
# takes a beat to tear down after nebula exits on the VM, so a fixed sleep is
|
||||
# racy.
|
||||
for _ in $(seq 1 30); do
|
||||
[ -z "$(jobs -r)" ] && break
|
||||
sleep 1
|
||||
done
|
||||
sleep 1
|
||||
|
||||
if [ "$(jobs -r)" ]
|
||||
then
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Windows smoke test for the nebula tun + UDP + NLM code paths.
|
||||
#
|
||||
# Topology:
|
||||
# - lighthouse runs natively on the Windows host (wintun + windows UDP)
|
||||
# - peer runs inside WSL2 (Linux build of nebula, /dev/net/tun)
|
||||
#
|
||||
# WSL2 gives us a real netns boundary so the loopback fast-path on Windows
|
||||
# does not short-circuit the overlay -- when WSL pings the lighthouse VPN IP,
|
||||
# Linux has no idea that IP is local to the Windows host, so the packet is
|
||||
# forced through nebula. Same in reverse.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# wsl.exe emits UTF-16 LE by default which PowerShell reads as bytes, mangling
|
||||
# every captured string. WSL_UTF8 makes wsl.exe emit UTF-8 instead.
|
||||
$env:WSL_UTF8 = '1'
|
||||
|
||||
$RepoRoot = Resolve-Path "$PSScriptRoot\..\..\.."
|
||||
$Nebula = Join-Path $RepoRoot 'nebula.exe'
|
||||
$NebulaCert = Join-Path $RepoRoot 'nebula-cert.exe'
|
||||
$NebulaLinux = Join-Path $RepoRoot 'build\linux-amd64\nebula'
|
||||
|
||||
if (-not (Test-Path $Nebula)) { throw "missing $Nebula; run 'make bin-windows' first" }
|
||||
if (-not (Test-Path $NebulaCert)) { throw "missing $NebulaCert; run 'make bin-windows' first" }
|
||||
if (-not (Test-Path $NebulaLinux)) { throw "missing $NebulaLinux; build the linux nebula first" }
|
||||
|
||||
# Matches the distro installed by Vampire/setup-wsl in smoke-extra.yml.
|
||||
$Distro = 'Ubuntu-24.04'
|
||||
$listed = (wsl --list --quiet 2>$null) -join "`n"
|
||||
if ($listed -notmatch [regex]::Escape($Distro)) {
|
||||
throw "WSL distro $Distro not registered. Got: $listed"
|
||||
}
|
||||
Write-Host "Using WSL distro: $Distro"
|
||||
|
||||
# Windows host as seen from inside WSL: WSL's default-route gateway. We extract
|
||||
# it with a regex rather than awk fields so PowerShell does not eat any '$N'
|
||||
# tokens, and tabs/double-spaces in `ip route` output do not confuse a cut.
|
||||
$ipCmd = 'ip route show default | grep -oE "([0-9]+\.){3}[0-9]+" | head -1'
|
||||
$WindowsIp = (wsl -d $Distro -- bash -c $ipCmd).Trim()
|
||||
if (-not $WindowsIp) { throw "could not determine Windows host IP from WSL" }
|
||||
Write-Host "Windows host IP from WSL: $WindowsIp"
|
||||
|
||||
$WorkDir = Join-Path $env:TEMP 'nebula-smoke-windows'
|
||||
if (Test-Path $WorkDir) { Remove-Item -Recurse -Force $WorkDir }
|
||||
New-Item -ItemType Directory -Path $WorkDir | Out-Null
|
||||
|
||||
$WslDir = '/tmp/nebula-smoke'
|
||||
wsl -d $Distro -- bash -c "rm -rf $WslDir && mkdir -p $WslDir" | Out-Null
|
||||
|
||||
$DevName = 'nebula-smoke'
|
||||
$Ip1 = '192.168.241.1'
|
||||
$Ip2 = '192.168.241.2'
|
||||
$Port = 4242
|
||||
|
||||
& $NebulaCert ca -name 'smoke-ca' -out-crt "$WorkDir\ca.crt" -out-key "$WorkDir\ca.key"
|
||||
if ($LASTEXITCODE -ne 0) { throw "nebula-cert ca failed (exit $LASTEXITCODE)" }
|
||||
|
||||
& $NebulaCert sign -name 'lighthouse' -networks "$Ip1/24" -ca-crt "$WorkDir\ca.crt" -ca-key "$WorkDir\ca.key" -out-crt "$WorkDir\lighthouse.crt" -out-key "$WorkDir\lighthouse.key"
|
||||
if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign lighthouse failed (exit $LASTEXITCODE)" }
|
||||
|
||||
& $NebulaCert sign -name 'peer' -networks "$Ip2/24" -ca-crt "$WorkDir\ca.crt" -ca-key "$WorkDir\ca.key" -out-crt "$WorkDir\peer.crt" -out-key "$WorkDir\peer.key"
|
||||
if ($LASTEXITCODE -ne 0) { throw "nebula-cert sign peer failed (exit $LASTEXITCODE)" }
|
||||
|
||||
# Windows lighthouse config.
|
||||
@"
|
||||
pki:
|
||||
ca: $WorkDir\ca.crt
|
||||
cert: $WorkDir\lighthouse.crt
|
||||
key: $WorkDir\lighthouse.key
|
||||
static_host_map: {}
|
||||
lighthouse:
|
||||
am_lighthouse: true
|
||||
interval: 60
|
||||
hosts: []
|
||||
listen:
|
||||
host: 0.0.0.0
|
||||
port: $Port
|
||||
tun:
|
||||
disabled: false
|
||||
dev: $DevName
|
||||
drop_local_broadcast: false
|
||||
drop_multicast: false
|
||||
tx_queue: 500
|
||||
mtu: 1300
|
||||
network_category: private
|
||||
logging:
|
||||
level: info
|
||||
format: text
|
||||
firewall:
|
||||
outbound_action: drop
|
||||
inbound_action: drop
|
||||
conntrack:
|
||||
tcp_timeout: 12m
|
||||
udp_timeout: 3m
|
||||
default_timeout: 10m
|
||||
outbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
inbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
"@ | Out-File -FilePath "$WorkDir\lighthouse.yml" -Encoding utf8
|
||||
|
||||
# WSL peer config (paths are POSIX, deliberately).
|
||||
@"
|
||||
pki:
|
||||
ca: $WslDir/ca.crt
|
||||
cert: $WslDir/peer.crt
|
||||
key: $WslDir/peer.key
|
||||
static_host_map:
|
||||
"${Ip1}": ["${WindowsIp}:$Port"]
|
||||
lighthouse:
|
||||
am_lighthouse: false
|
||||
interval: 60
|
||||
hosts:
|
||||
- "${Ip1}"
|
||||
listen:
|
||||
host: 0.0.0.0
|
||||
port: 0
|
||||
tun:
|
||||
disabled: false
|
||||
dev: nebula1
|
||||
drop_local_broadcast: false
|
||||
drop_multicast: false
|
||||
tx_queue: 500
|
||||
mtu: 1300
|
||||
logging:
|
||||
level: info
|
||||
format: text
|
||||
firewall:
|
||||
outbound_action: drop
|
||||
inbound_action: drop
|
||||
conntrack:
|
||||
tcp_timeout: 12m
|
||||
udp_timeout: 3m
|
||||
default_timeout: 10m
|
||||
outbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
inbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
"@ | Out-File -FilePath "$WorkDir\peer.yml" -Encoding utf8
|
||||
|
||||
# Stage WSL artifacts. Convert Windows paths to WSL paths ourselves rather than
|
||||
# calling `wslpath`, because PowerShell's argument-passing to external EXEs
|
||||
# strips backslashes from path arguments in ways that are hard to escape around.
|
||||
function ConvertTo-WslPath {
|
||||
param([string]$WindowsPath)
|
||||
if ($WindowsPath -notmatch '^([A-Za-z]):\\(.*)$') {
|
||||
throw "cannot convert path to WSL: $WindowsPath"
|
||||
}
|
||||
return "/mnt/$($matches[1].ToLower())/$($matches[2].Replace('\','/'))"
|
||||
}
|
||||
|
||||
$WslWorkDir = ConvertTo-WslPath $WorkDir
|
||||
$WslNebulaPath = ConvertTo-WslPath $NebulaLinux
|
||||
wsl -d $Distro -- bash -c "cp '$WslWorkDir/ca.crt' '$WslWorkDir/peer.crt' '$WslWorkDir/peer.key' '$WslWorkDir/peer.yml' $WslDir/ && cp '$WslNebulaPath' $WslDir/nebula && chmod +x $WslDir/nebula"
|
||||
|
||||
# Make sure WSL has tun support and /dev/net/tun is usable before starting
|
||||
# nebula. Diagnostics first so a fail here points at the real problem (e.g.
|
||||
# WSL1 distros do not have a real kernel and will not have tun).
|
||||
Write-Host '=== WSL diagnostic ==='
|
||||
wsl --version 2>&1 | Out-Host
|
||||
wsl --list --verbose 2>&1 | Out-Host
|
||||
wsl -d $Distro -u root -- uname -a | Out-Host
|
||||
wsl -d $Distro -u root -- bash -c "modprobe tun 2>&1 || true; mkdir -p /dev/net; [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200; chmod 600 /dev/net/tun; ls -l /dev/net/tun"
|
||||
if ($LASTEXITCODE -ne 0) { throw "failed to prepare /dev/net/tun in WSL (TUN support missing?)" }
|
||||
|
||||
# Deliberately no New-NetFirewallRule calls here -- nebula's windows_bypass_wdf
|
||||
# feature is supposed to install WFP permit filters that let inbound traffic
|
||||
# through Windows Defender Firewall on its own. If this smoke regresses, that
|
||||
# feature regressed.
|
||||
|
||||
$lhOut = Join-Path $WorkDir 'lighthouse.out.log'
|
||||
$lhErr = Join-Path $WorkDir 'lighthouse.err.log'
|
||||
$lhProc = Start-Process -FilePath $Nebula -ArgumentList @('-config', "$WorkDir\lighthouse.yml") `
|
||||
-PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput $lhOut `
|
||||
-RedirectStandardError $lhErr
|
||||
|
||||
# Run nebula in WSL as root with no sudo + no shell wrapper. PowerShell's
|
||||
# Start-Process arg quoting mangles `bash -c "..."` strings that contain
|
||||
# spaces/redirections, so we skip bash entirely and let Start-Process do the
|
||||
# stdout/stderr capture itself.
|
||||
$peerOut = Join-Path $WorkDir 'peer.out.log'
|
||||
$peerErr = Join-Path $WorkDir 'peer.err.log'
|
||||
$peerProc = Start-Process -FilePath 'wsl' `
|
||||
-ArgumentList @('-d', $Distro, '-u', 'root', '--', "$WslDir/nebula", '-config', "$WslDir/peer.yml") `
|
||||
-PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput $peerOut `
|
||||
-RedirectStandardError $peerErr
|
||||
|
||||
function Wait-Until {
|
||||
param([scriptblock]$Predicate, [int]$TimeoutSec, [string]$What)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (& $Predicate) { return }
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
throw "timed out waiting for: $What"
|
||||
}
|
||||
|
||||
try {
|
||||
Wait-Until -TimeoutSec 30 -What "windows wintun adapter $DevName with NetworkCategory=Private" -Predicate {
|
||||
if ($lhProc.HasExited) { throw "lighthouse exited (code $($lhProc.ExitCode)) before tun was ready" }
|
||||
$p = Get-NetConnectionProfile -InterfaceAlias $DevName -ErrorAction SilentlyContinue
|
||||
$p -and ("$($p.NetworkCategory)" -ieq 'Private')
|
||||
}
|
||||
Write-Host "OK: $DevName NetworkCategory=Private"
|
||||
|
||||
Wait-Until -TimeoutSec 30 -What "WSL nebula1 with $Ip2" -Predicate {
|
||||
if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before tun was ready" }
|
||||
$r = wsl -d $Distro -u root -- bash -c "ip -o addr show nebula1 2>/dev/null | grep -q 'inet $Ip2' && echo yes"
|
||||
("$r").Trim() -eq 'yes'
|
||||
}
|
||||
Write-Host "OK: WSL nebula1 has $Ip2"
|
||||
|
||||
Wait-Until -TimeoutSec 30 -What "ping from WSL peer to windows lighthouse ($Ip1)" -Predicate {
|
||||
if ($peerProc.HasExited) { throw "peer exited (code $($peerProc.ExitCode)) before ping succeeded" }
|
||||
$r = wsl -d $Distro -u root -- bash -c "ping -c1 -W1 $Ip1 >/dev/null 2>&1 && echo OK"
|
||||
("$r").Trim() -eq 'OK'
|
||||
}
|
||||
Write-Host "OK: WSL peer -> windows lighthouse"
|
||||
|
||||
Wait-Until -TimeoutSec 30 -What "ping from windows lighthouse to WSL peer ($Ip2)" -Predicate {
|
||||
$null = & ping.exe -n 1 -w 1000 $Ip2
|
||||
$LASTEXITCODE -eq 0
|
||||
}
|
||||
Write-Host "OK: windows lighthouse -> WSL peer"
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'All smoke checks passed.'
|
||||
}
|
||||
catch {
|
||||
Write-Host ''
|
||||
Write-Host '=== lighthouse stdout ==='
|
||||
Get-Content $lhOut -ErrorAction SilentlyContinue | Out-Host
|
||||
Write-Host '=== lighthouse stderr ==='
|
||||
Get-Content $lhErr -ErrorAction SilentlyContinue | Out-Host
|
||||
Write-Host '=== peer stdout ==='
|
||||
Get-Content $peerOut -ErrorAction SilentlyContinue | Out-Host
|
||||
Write-Host '=== peer stderr ==='
|
||||
Get-Content $peerErr -ErrorAction SilentlyContinue | Out-Host
|
||||
Write-Host '=== nebula WFP filters ==='
|
||||
# Dump nebula-installed filters so we can verify they got registered with
|
||||
# the conditions we expect.
|
||||
$wfpDump = Join-Path $WorkDir 'wfp.xml'
|
||||
netsh wfp show filters file=$wfpDump 2>&1 | Out-Null
|
||||
if (Test-Path $wfpDump) {
|
||||
Select-String -Path $wfpDump -Pattern 'Nebula' -Context 0,80 -ErrorAction SilentlyContinue | Out-Host
|
||||
}
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
if (-not $lhProc.HasExited) {
|
||||
Stop-Process -Id $lhProc.Id -Force -ErrorAction SilentlyContinue
|
||||
$lhProc.WaitForExit(5000) | Out-Null
|
||||
}
|
||||
wsl -d $Distro -u root -- bash -c "pkill -f $WslDir/nebula 2>/dev/null; true" | Out-Null
|
||||
# pkill returns 1 when no match and wsl propagates that; the smoke is done
|
||||
# so we don't want it to leak into the script's exit code.
|
||||
$global:LASTEXITCODE = 0
|
||||
if ($peerProc -and -not $peerProc.HasExited) {
|
||||
Stop-Process -Id $peerProc.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ set -o pipefail
|
||||
|
||||
mkdir -p logs
|
||||
|
||||
NETWORK="nebula-smoke"
|
||||
|
||||
cleanup() {
|
||||
echo
|
||||
echo " *** cleanup"
|
||||
@@ -18,103 +16,57 @@ cleanup() {
|
||||
then
|
||||
docker kill lighthouse1 host2 host3 host4
|
||||
fi
|
||||
docker network rm "$NETWORK" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create a dedicated smoke network with an explicit subnet (required for --ip
|
||||
# below). Probe a short list of candidates so a locally-used range doesn't
|
||||
# fail the whole test — we only need one to be free.
|
||||
docker network rm "$NETWORK" >/dev/null 2>&1 || true
|
||||
for candidate in 172.30.0.0/24 172.31.0.0/24 10.98.0.0/24 10.99.0.0/24 192.168.230.0/24; do
|
||||
if docker network create --subnet "$candidate" "$NETWORK" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
if ! docker network inspect "$NETWORK" >/dev/null 2>&1; then
|
||||
echo "failed to create $NETWORK: every candidate subnet is in use" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Derive container IPs from the network's assigned subnet. Slots: .2 lighthouse1,
|
||||
# .3 host2, .4 host3, .5 host4 — matches the placeholders in build.sh.
|
||||
SUBNET="$(docker network inspect -f '{{(index .IPAM.Config 0).Subnet}}' "$NETWORK")"
|
||||
PREFIX="${SUBNET%/*}"
|
||||
PREFIX="${PREFIX%.*}"
|
||||
LIGHTHOUSE_IP="$PREFIX.2"
|
||||
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
|
||||
sed "s|203\.0\.113\.|$PREFIX.|g" "$f" >"$f.tmp"
|
||||
mv "$f.tmp" "$f"
|
||||
done
|
||||
|
||||
CONTAINER="nebula:${NAME:-smoke}"
|
||||
|
||||
docker run --name lighthouse1 --rm "$CONTAINER" -config lighthouse1.yml -test
|
||||
docker run --name host2 --rm -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" "$CONTAINER" -config host2.yml -test
|
||||
docker run --name host3 --rm -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" "$CONTAINER" -config host3.yml -test
|
||||
docker run --name host4 --rm -v "$PWD/build/host4.yml:/nebula/host4.yml:ro" "$CONTAINER" -config host4.yml -test
|
||||
docker run --name host2 --rm "$CONTAINER" -config host2.yml -test
|
||||
docker run --name host3 --rm "$CONTAINER" -config host3.yml -test
|
||||
docker run --name host4 --rm "$CONTAINER" -config host4.yml -test
|
||||
|
||||
docker run --name lighthouse1 --network "$NETWORK" --ip "$LIGHTHOUSE_IP" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config lighthouse1.yml 2>&1 | tee logs/lighthouse1 | sed -u 's/^/ [lighthouse1] /' &
|
||||
docker run --name lighthouse1 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config lighthouse1.yml 2>&1 | tee logs/lighthouse1 | sed -u 's/^/ [lighthouse1] /' &
|
||||
sleep 1
|
||||
docker run --name host2 --network "$NETWORK" --ip "$HOST2_IP" -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host2.yml 2>&1 | tee logs/host2 | sed -u 's/^/ [host2] /' &
|
||||
docker run --name host2 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host2.yml 2>&1 | tee logs/host2 | sed -u 's/^/ [host2] /' &
|
||||
sleep 1
|
||||
docker run --name host3 --network "$NETWORK" --ip "$HOST3_IP" -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host3.yml 2>&1 | tee logs/host3 | sed -u 's/^/ [host3] /' &
|
||||
docker run --name host3 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host3.yml 2>&1 | tee logs/host3 | sed -u 's/^/ [host3] /' &
|
||||
sleep 1
|
||||
docker run --name host4 --network "$NETWORK" --ip "$HOST4_IP" -v "$PWD/build/host4.yml:/nebula/host4.yml:ro" --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host4.yml 2>&1 | tee logs/host4 | sed -u 's/^/ [host4] /' &
|
||||
docker run --name host4 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN --rm "$CONTAINER" -config host4.yml 2>&1 | tee logs/host4 | sed -u 's/^/ [host4] /' &
|
||||
sleep 1
|
||||
|
||||
# grab tcpdump pcaps for debugging
|
||||
docker exec lighthouse1 tcpdump -i tun0 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap &
|
||||
docker exec lighthouse1 tcpdump -i nebula1 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap &
|
||||
docker exec lighthouse1 tcpdump -i eth0 -q -w - -U 2>logs/lighthouse1.outside.log >logs/lighthouse1.outside.pcap &
|
||||
docker exec host2 tcpdump -i tun0 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap &
|
||||
docker exec host2 tcpdump -i nebula1 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap &
|
||||
docker exec host2 tcpdump -i eth0 -q -w - -U 2>logs/host2.outside.log >logs/host2.outside.pcap &
|
||||
docker exec host3 tcpdump -i tun0 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap &
|
||||
docker exec host3 tcpdump -i nebula1 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap &
|
||||
docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host3.outside.pcap &
|
||||
docker exec host4 tcpdump -i tun0 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
|
||||
docker exec host4 tcpdump -i nebula1 -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 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 &
|
||||
docker exec host2 ncat -nklv 0.0.0.0 2000 &
|
||||
docker exec host3 ncat -nklv 0.0.0.0 2000 &
|
||||
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 &
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from lighthouse1"
|
||||
echo
|
||||
set -x
|
||||
docker exec lighthouse1 ping -c1 $HOST2_NIP
|
||||
docker exec lighthouse1 ping -c1 $HOST3_NIP
|
||||
docker exec lighthouse1 ping -c1 192.168.100.2
|
||||
docker exec lighthouse1 ping -c1 192.168.100.3
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from host2"
|
||||
echo
|
||||
set -x
|
||||
docker exec host2 ping -c1 $LIGHTHOUSE_NIP
|
||||
docker exec host2 ping -c1 192.168.100.1
|
||||
# Should fail because not allowed by host3 inbound firewall
|
||||
! docker exec host2 ping -c1 $HOST3_NIP -w5 || exit 1
|
||||
! docker exec host2 ping -c1 192.168.100.3 -w5 || exit 1
|
||||
|
||||
set +x
|
||||
echo
|
||||
@@ -122,34 +74,34 @@ echo " *** Testing ncat from host2"
|
||||
echo
|
||||
set -x
|
||||
# Should fail because not allowed by host3 inbound firewall
|
||||
! 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
|
||||
! 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
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from host3"
|
||||
echo
|
||||
set -x
|
||||
docker exec host3 ping -c1 $LIGHTHOUSE_NIP
|
||||
docker exec host3 ping -c1 $HOST2_NIP
|
||||
docker exec host3 ping -c1 192.168.100.1
|
||||
docker exec host3 ping -c1 192.168.100.2
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ncat from host3"
|
||||
echo
|
||||
set -x
|
||||
docker exec host3 ncat -nzv -w5 $HOST2_NIP 2000
|
||||
docker exec host3 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2
|
||||
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
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing ping from host4"
|
||||
echo
|
||||
set -x
|
||||
docker exec host4 ping -c1 $LIGHTHOUSE_NIP
|
||||
docker exec host4 ping -c1 192.168.100.1
|
||||
# Should fail because not allowed by host4 outbound firewall
|
||||
! docker exec host4 ping -c1 $HOST2_NIP -w5 || exit 1
|
||||
! docker exec host4 ping -c1 $HOST3_NIP -w5 || exit 1
|
||||
! docker exec host4 ping -c1 192.168.100.2 -w5 || exit 1
|
||||
! docker exec host4 ping -c1 192.168.100.3 -w5 || exit 1
|
||||
|
||||
set +x
|
||||
echo
|
||||
@@ -157,34 +109,27 @@ echo " *** Testing ncat from host4"
|
||||
echo
|
||||
set -x
|
||||
# Should fail because not allowed by host4 outbound firewall
|
||||
! 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
|
||||
! 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
|
||||
|
||||
set +x
|
||||
echo
|
||||
echo " *** Testing conntrack"
|
||||
echo
|
||||
set -x
|
||||
|
||||
# host4's outbound firewall only allows ICMP to the lighthouse, so host4
|
||||
# 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 $HOST4_NIP 4000" | grep -q helloagainfromhost4
|
||||
# host2 can ping host3 now that host3 pinged it first
|
||||
docker exec host2 ping -c1 192.168.100.3
|
||||
# host4 can ping host2 once conntrack established
|
||||
docker exec host2 ping -c1 192.168.100.4
|
||||
docker exec host4 ping -c1 192.168.100.2
|
||||
|
||||
docker exec host4 sh -c 'kill 1'
|
||||
docker exec host3 sh -c 'kill 1'
|
||||
docker exec host2 sh -c 'kill 1'
|
||||
docker exec lighthouse1 sh -c 'kill 1'
|
||||
|
||||
# Wait up to 30s for all backgrounded jobs to exit rather than relying on a
|
||||
# fixed sleep.
|
||||
for _ in $(seq 1 30); do
|
||||
[ -z "$(jobs -r)" ] && break
|
||||
sleep 1
|
||||
done
|
||||
sleep 5
|
||||
|
||||
if [ "$(jobs -r)" ]
|
||||
then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "bento/ubuntu-24.04"
|
||||
config.vm.box = "ubuntu/jammy64"
|
||||
|
||||
config.vm.synced_folder "../build", "/nebula"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "DefinedNet/netbsd10"
|
||||
config.vm.box = "generic/netbsd9"
|
||||
|
||||
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "DefinedNet/openbsd78"
|
||||
config.vm.box = "generic/openbsd7"
|
||||
|
||||
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
||||
end
|
||||
|
||||
+80
-98
@@ -13,138 +13,120 @@ on:
|
||||
- 'go.sum'
|
||||
jobs:
|
||||
|
||||
static:
|
||||
name: Static checks
|
||||
test-linux:
|
||||
name: Build all and test on ubuntu-linux
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
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
|
||||
- name: Build
|
||||
run: make all
|
||||
|
||||
- name: Vet
|
||||
run: make vet
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.12
|
||||
version: v2.5
|
||||
|
||||
test:
|
||||
name: Test ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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
|
||||
- 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@v4
|
||||
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@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build
|
||||
run: ${{ matrix.build-cmd }}
|
||||
|
||||
- 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
|
||||
run: make bin-boringcrypto
|
||||
|
||||
- name: Test
|
||||
run: ${{ matrix.test-cmd }}
|
||||
run: make test-boringcrypto
|
||||
|
||||
- name: End 2 end
|
||||
if: matrix.e2e-cmd != ''
|
||||
run: ${{ matrix.e2e-cmd }}
|
||||
run: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: matrix.e2e-cmd != '' && always()
|
||||
with:
|
||||
name: e2e packet flow ${{ matrix.name }}
|
||||
path: e2e/mermaid/
|
||||
if-no-files-found: warn
|
||||
|
||||
cross-build:
|
||||
name: Cross-build ${{ matrix.name }}
|
||||
test-linux-pkcs11:
|
||||
name: Build and test on linux with pkcs11
|
||||
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/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26'
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build ${{ matrix.name }}
|
||||
run: make -j"$(nproc)" ${{ matrix.make-target }}
|
||||
- name: Build
|
||||
run: make bin-pkcs11
|
||||
|
||||
finish:
|
||||
name: CI status
|
||||
if: always()
|
||||
needs: [static, test, cross-build]
|
||||
runs-on: ubuntu-latest
|
||||
- name: Test
|
||||
run: make test-pkcs11
|
||||
|
||||
test:
|
||||
name: Build and test on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-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
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: All upstream jobs passed
|
||||
run: echo "ok"
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
check-latest: true
|
||||
|
||||
- name: Build nebula
|
||||
run: go build ./cmd/nebula
|
||||
|
||||
- name: Build nebula-cert
|
||||
run: go build ./cmd/nebula-cert
|
||||
|
||||
- name: Vet
|
||||
run: make vet
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.5
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
- name: End 2 end
|
||||
run: make e2evv
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e packet flow ${{ matrix.os }}
|
||||
path: e2e/mermaid/${{ matrix.os }}
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -2,21 +2,7 @@ version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- sloglint
|
||||
- testifylint
|
||||
settings:
|
||||
sloglint:
|
||||
# Enforce key-value pair form for Info/Debug/Warn/Error/Log/With and
|
||||
# the package-level slog equivalents. Use l.Log(ctx, level, ...) for
|
||||
# custom levels instead of LogAttrs when you can.
|
||||
#
|
||||
# LogAttrs is also flagged by this rule because it takes ...slog.Attr;
|
||||
# the few legitimate sites (where attrs is built up as a []slog.Attr)
|
||||
# carry a //nolint:sloglint with rationale.
|
||||
kv-only: true
|
||||
# no-mixed-args is on by default: forbids mixing kv and attrs in one call.
|
||||
# discard-handler is on by default (since Go 1.24): suggests
|
||||
# slog.DiscardHandler over slog.NewTextHandler(io.Discard, nil).
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
|
||||
+3
-209
@@ -7,211 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.11.0] - 2026-07-23
|
||||
|
||||
See the [v1.11.0](https://github.com/slackhq/nebula/milestone/25?closed=1) milestone for a complete list of changes.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Logging has switched from logrus to Go's structured `slog`. Log output changes: levels are upper case
|
||||
(`level=INFO`), trace prints as `level=DEBUG-4`, timestamps are always RFC3339Nano and `logging.timestamp_format`
|
||||
is ignored, and some messages were reworded. Review any log parsing before upgrading. This is also an API break
|
||||
for embedders, as constructors now take a `*slog.Logger`. (#1672, #1734, #1621)
|
||||
- `firewall.inbound_action` and `firewall.outbound_action` (used to set reject vs. drop policy) were each being
|
||||
applied to the opposite direction, that is now corrected. This only affects how blocked packets are answered, not
|
||||
which packets the firewall allows or denies. If you set either of these you are getting the behavior of the other
|
||||
one today and likely want to swap them before upgrading. (#1798)
|
||||
- On Windows, Nebula now installs WFP PERMIT filters for the nebula adapter and the listener port by default. WFP
|
||||
sits below Windows Defender Firewall, so any WDF inbound rules you rely on for either will no longer apply. Set
|
||||
`tun.windows_bypass_wdf` and `listen.windows_bypass_wdf` to false to leave WDF in charge. (#1710)
|
||||
- On Windows, the nebula device is now set to the `private` network category instead of whatever Windows decided,
|
||||
which is usually `Public`. This makes the host firewall less restrictive on the overlay. Set
|
||||
`tun.network_category` to `unset` to keep the old behavior. (#1710)
|
||||
- Reject packets for non-TCP now use ICMP code 13, communication administratively prohibited, instead of code 3,
|
||||
port unreachable. Anything keying off the old code needs updating. (#1766, #1768)
|
||||
- The SSH debug server's profiling commands are now confined to `sshd.sandbox_dir`, which defaults to
|
||||
`$TMP/nebula-debug`. Relative paths resolve inside it and absolute paths outside it are rejected, so anything
|
||||
scripting `start-cpu-profile`, `save-heap-profile`, or `save-mutex-profile` with a path elsewhere needs the
|
||||
directory set. The directory is not created for you. (#1622)
|
||||
|
||||
### Added
|
||||
|
||||
- Sign the Windows release binaries. (#1718)
|
||||
- Generate IPv6 reject packets, matching the existing IPv4 behavior. (#1766, #1767, #1768)
|
||||
- Accept `-` in `nebula-cert` to read from stdin or write to stdout. (#1714)
|
||||
- Search for both `config.yml` and `config.yaml` in service and command line modes. (#1717)
|
||||
- Add version labels to the Docker/OCI images. (#1772)
|
||||
- Rebind the listener and re-query lighthouses on macOS when the underlay network changes, so devices moving
|
||||
between wifi and wired or between networks recover without waiting for dead tunnel detection. Controlled by
|
||||
`listen.rebind_on_network_change` (default `true`, not reloadable). (#1816)
|
||||
|
||||
### Changed
|
||||
|
||||
- Reload the firewall when the unsafe networks in the certificate change. (#1719)
|
||||
- Reconfigure, start, and stop the stats listener on a config reload instead of requiring a restart. (#1670)
|
||||
- Update a static host's addresses when they change on reload. (#1713)
|
||||
- Don't require a port on ICMP firewall rules. (#1609)
|
||||
- Connection track ICMP traffic. (#1602)
|
||||
- Return `NODATA` instead of `NXDOMAIN` from the DNS server for a name that exists but has no record of the
|
||||
requested type, so clients that query `AAAA` first (busybox/Alpine) fall through to `A`. (#1668)
|
||||
- Record the local host's details in the DNS server. (#1716)
|
||||
- Install Windows unsafe routes as link routes. (#1709)
|
||||
- Reduce relay handshake log spam, and only log a handshake send error at error level when the remote list
|
||||
changes. (#1733, #1765, #1810)
|
||||
- Start, stop, and reload subsystems (DNS, stats, conntrack, ssh, punchy) cleanly without leaking goroutines. (#1640, #1654, #1661, #1667, #1669, #1708, #1806, #1815)
|
||||
- `Control` is now safe to stop and wait on from any lifecycle state, and a new `Control.Wait` blocks until nebula
|
||||
has fully stopped and returns the first fatal reader error. Failed starts release the udp sockets and tun fd
|
||||
instead of leaking them. (#1794)
|
||||
- Trigger an immediate lighthouse update when reconnecting to or adding a lighthouse instead of waiting for the next update tick. (#1645)
|
||||
- Bring the Darwin and OpenBSD tun implementations in line with the other BSDs. (#1703)
|
||||
- Update to build against go v1.26. (#1818)
|
||||
- Various dependency updates. (#1586, #1587, #1604, #1617, #1618, #1627, #1628, #1629, #1652, #1664, #1665, #1697, #1721, #1732, #1742, #1743, #1750, #1763, #1771, #1782, #1800, #1807)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix a data race on a host's remote address that could send packets to the wrong address during a roam. (#1773)
|
||||
- Fix tunnels that could permanently escape connection manager monitoring. (#1752)
|
||||
- Fix a crash when reloading the SSH server's trusted keys. (#1787)
|
||||
- Fix hostmap corruption when a host has multiple overlay addresses. Each address now gets its own list instead of
|
||||
a single shared chain, which also fixes two latent bugs on the add and makePrimary paths. (#1788, #1790)
|
||||
- Apply `remote_allow_list` IPv4 rules to 4-in-6 mapped addresses. (#1786)
|
||||
- Don't panic in the DNS server on a short or empty query name. (#1635)
|
||||
- Advance the replay window on relayed packets so a relay drops replayed frames instead of re-forwarding them. (#1751)
|
||||
- Fix a race in relay state handling. (#1753)
|
||||
- Lock replay window updates so concurrent readers can't corrupt it. (#1802)
|
||||
- Reject malformed handshakes more reliably, including invalid ed25519 key lengths. (#1601, #1756)
|
||||
- Properly handle `closetunnel` packets. (#1638)
|
||||
- Fix an IPv6 extension-header length overflow that could make the firewall parse the wrong protocol and ports. (#1789)
|
||||
- Fix relay re-establishment when a handshake arrives over a relay entry that a one-sided teardown left
|
||||
`Disestablished`, which silently dropped every send until dead tunnel detection forced a re-handshake. (#1805)
|
||||
- Don't build new relay state on a tunnel that was just discarded. (#1796)
|
||||
- Don't delete the wrong pending hostinfo in the handshake manager. (#1811)
|
||||
- Don't call the packet reader after a UDP error on Darwin. (#1755)
|
||||
- Open the FreeBSD tun device non blocking. (#1666)
|
||||
|
||||
## [1.10.3] - 2026-02-06
|
||||
|
||||
### Security
|
||||
|
||||
- Fix an issue where blocklist bypass is possible when using curve P256 since the signature can have 2 valid representations.
|
||||
Both fingerprint representations will be tested against the blocklist.
|
||||
Any newly issued P256 based certificates will have their signature clamped to the low-s form.
|
||||
Nebula will assert the low-s signature form when validating certificates in a future version. [GHSA-69x3-g4r3-p962](https://github.com/slackhq/nebula/security/advisories/GHSA-69x3-g4r3-p962)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve error reporting if nebula fails to start due to a tun device naming issue. (#1588)
|
||||
|
||||
## [1.10.2] - 2026-01-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix panic when using `use_system_route_table` that was introduced in v1.10.1. (#1580)
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix some typos in comments. (#1582)
|
||||
- Dependency updates. (#1581)
|
||||
|
||||
## [1.10.1] - 2026-01-16
|
||||
|
||||
See the [v1.10.1](https://github.com/slackhq/nebula/milestone/26?closed=1) milestone for a complete list of changes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix a bug where an unsafe route derived from the system route table could be lost on a config reload. (#1573)
|
||||
- Fix the PEM banner for ECDSA P256 public keys. (#1552)
|
||||
- Fix a regression on Windows from 1.9.x where nebula could fall back to a less performant UDP listener if
|
||||
non-critical ioctls failed. (#1568)
|
||||
- Fix a bug in handshake processing when a peer sends an unexpected public key. (#1566)
|
||||
|
||||
### Added
|
||||
|
||||
- Add a config option to control accepting `recv_error` packets which defaults to `always`. (#1569)
|
||||
|
||||
### Changed
|
||||
|
||||
- Various dependency updates. (#1541, #1549, #1550, #1557, #1558, #1560, #1561, #1570, #1571)
|
||||
|
||||
## [1.10.0] - 2025-12-04
|
||||
|
||||
See the [v1.10.0](https://github.com/slackhq/nebula/milestone/16?closed=1) milestone for a complete list of changes.
|
||||
|
||||
### Added
|
||||
|
||||
- Support for ipv6 and multiple ipv4/6 addresses in the overlay.
|
||||
A new v2 ASN.1 based certificate format.
|
||||
Certificates now have a unified interface for external implementations.
|
||||
(#1212, #1216, #1345, #1359, #1381, #1419, #1464, #1466, #1451, #1476, #1467, #1481, #1399, #1488, #1492, #1495, #1468, #1521, #1535, #1538)
|
||||
- Add the ability to mark packets on linux to better target nebula packets in iptables/nftables. (#1331)
|
||||
- Add ECMP support for `unsafe_routes`. (#1332)
|
||||
- PKCS11 support for P256 keys when built with `pkcs11` tag (#1153, #1482)
|
||||
|
||||
### Changed
|
||||
|
||||
- **NOTE**: `default_local_cidr_any` now defaults to false, meaning that any firewall rule
|
||||
- `default_local_cidr_any` now defaults to false, meaning that any firewall rule
|
||||
intended to target an `unsafe_routes` entry must explicitly declare it via the
|
||||
`local_cidr` field. This is almost always the intended behavior. This flag is
|
||||
deprecated and will be removed in a future release. (#1373)
|
||||
- Improve logging when a relay is in use on an inbound packet. (#1533)
|
||||
- Avoid fatal errors if `rountines` is > 1 on systems that don't support more than 1 routine. (#1531)
|
||||
- Log a warning if a firewall rule contains an `any` that negates a more restrictive filter. (#1513)
|
||||
- Accept encrypted CA passphrase from an environment variable. (#1421)
|
||||
- Allow handshaking with any trusted remote. (#1509)
|
||||
- Log only the count of blocklisted certificate fingerprints instead of the entire list. (#1525)
|
||||
- Don't fatal when the ssh server is unable to be configured successfully. (#1520)
|
||||
- Update to build against go v1.25. (#1483)
|
||||
- Allow projects using `nebula` as a library with userspace networking to configure the `logger` and build version. (#1239)
|
||||
- Upgrade to `yaml.v3`. (#1148, #1371, #1438, #1478)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix a potential bug with udp ipv4 only on darwin. (#1532)
|
||||
- Improve lost packet statistics. (#1441, #1537)
|
||||
- Honor `remote_allow_list` in hole punch response. (#1186)
|
||||
- Fix a panic when `tun.use_system_route_table` is `true` and a route lacks a destination. (#1437)
|
||||
- Fix an issue when `tun.use_system_route_table: true` could result in heavy CPU utilization when many thousands of routes
|
||||
are present. (#1326)
|
||||
- Fix tests for 32 bit machines. (#1394)
|
||||
- Fix a possible 32bit integer underflow in config handling. (#1353)
|
||||
- Fix moving a udp address from one vpn address to another in the `static_host_map`
|
||||
which could cause rapid re-handshaking with an incorrect remote. (#1259)
|
||||
- Improve smoke tests in environments where the docker network is not the default. (#1347)
|
||||
|
||||
## [1.9.7] - 2025-10-10
|
||||
|
||||
### Security
|
||||
|
||||
- Fix an issue where Nebula could incorrectly accept and process a packet from an erroneous source IP when the sender's
|
||||
certificate is configured with unsafe_routes (cert v1/v2) or multiple IPs (cert v2). (#1494)
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable sending `recv_error` messages when a packet is received outside the allowable counter window. (#1459)
|
||||
- Improve error messages and remove some unnecessary fatal conditions in the Windows and generic udp listener. (#1453)
|
||||
|
||||
## [1.9.6] - 2025-7-15
|
||||
|
||||
### Added
|
||||
|
||||
- Support dropping inactive tunnels. This is disabled by default in this release but can be enabled with `tunnels.drop_inactive`. See example config for more details. (#1413)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Darwin freeze due to presence of some Network Extensions (#1426)
|
||||
- Ensure the same relay tunnel is always used when multiple relay tunnels are present (#1422)
|
||||
- Fix Windows freeze due to ICMP error handling (#1412)
|
||||
- Fix relay migration panic (#1403)
|
||||
|
||||
## [1.9.5] - 2024-12-05
|
||||
|
||||
### Added
|
||||
|
||||
- Gracefully ignore v2 certificates. (#1282)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix relays that refuse to re-establish after one of the remote tunnel pairs breaks. (#1277)
|
||||
deprecated and will be removed in a future release.
|
||||
|
||||
## [1.9.4] - 2024-09-09
|
||||
|
||||
@@ -870,14 +671,7 @@ created.)
|
||||
|
||||
- Initial public release.
|
||||
|
||||
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.10.3...HEAD
|
||||
[1.10.3]: https://github.com/slackhq/nebula/releases/tag/v1.10.3
|
||||
[1.10.2]: https://github.com/slackhq/nebula/releases/tag/v1.10.2
|
||||
[1.10.1]: https://github.com/slackhq/nebula/releases/tag/v1.10.1
|
||||
[1.10.0]: https://github.com/slackhq/nebula/releases/tag/v1.10.0
|
||||
[1.9.7]: https://github.com/slackhq/nebula/releases/tag/v1.9.7
|
||||
[1.9.6]: https://github.com/slackhq/nebula/releases/tag/v1.9.6
|
||||
[1.9.5]: https://github.com/slackhq/nebula/releases/tag/v1.9.5
|
||||
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.9.4...HEAD
|
||||
[1.9.4]: https://github.com/slackhq/nebula/releases/tag/v1.9.4
|
||||
[1.9.3]: https://github.com/slackhq/nebula/releases/tag/v1.9.3
|
||||
[1.9.2]: https://github.com/slackhq/nebula/releases/tag/v1.9.2
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
#ECCN:Open Source
|
||||
@@ -60,18 +60,6 @@ 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
|
||||
|
||||
@@ -94,35 +82,6 @@ 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)
|
||||
@@ -268,9 +227,6 @@ 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
|
||||
@@ -280,5 +236,5 @@ smoke-vagrant/%: bin-docker build/%/nebula
|
||||
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
||||
|
||||
.FORCE:
|
||||
.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/%
|
||||
.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/%
|
||||
.DEFAULT_GOAL := bin
|
||||
|
||||
@@ -57,7 +57,7 @@ Check the [releases](https://github.com/slackhq/nebula/releases/latest) page for
|
||||
docker pull nebulaoss/nebula
|
||||
```
|
||||
|
||||
#### Mobile ([source code](https://github.com/DefinedNet/mobile_nebula))
|
||||
#### Mobile
|
||||
|
||||
- [iOS](https://apps.apple.com/us/app/mobile-nebula/id1509587936?itsct=apps_box&itscg=30200)
|
||||
- [Android](https://play.google.com/store/apps/details?id=net.defined.mobile_nebula&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
|
||||
@@ -76,8 +76,6 @@ Nebula was created to provide a mechanism for groups of hosts to communicate sec
|
||||
|
||||
## Getting started (quickly)
|
||||
|
||||
**Don't want to manage your own PKI and lighthouses?** [Managed Nebula](https://www.defined.net/) from Defined Networking handles all of this for you.
|
||||
|
||||
To set up a Nebula network, you'll need:
|
||||
|
||||
#### 1. The [Nebula binaries](https://github.com/slackhq/nebula/releases) or [Distribution Packages](https://github.com/slackhq/nebula#distribution-packages) for your specific platform. Specifically you'll need `nebula-cert` and the specific nebula binary for each platform you use.
|
||||
|
||||
@@ -1,263 +1,157 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
mathbits "math/bits"
|
||||
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const bitsPerWord = 64
|
||||
|
||||
// Bits is a sliding-window anti-replay tracker. The window is stored as a
|
||||
// circular bitmap packed into uint64 words (8x denser than a []bool), so a
|
||||
// length-N window costs N/8 bytes. length must be a power of two.
|
||||
type Bits struct {
|
||||
length uint64
|
||||
lengthMask uint64
|
||||
current uint64
|
||||
bits []uint64
|
||||
bits []bool
|
||||
firstSeen bool
|
||||
lostCounter metrics.Counter
|
||||
dupeCounter metrics.Counter
|
||||
outOfWindowCounter metrics.Counter
|
||||
}
|
||||
|
||||
func NewBits(length uint64) *Bits {
|
||||
if length == 0 || length&(length-1) != 0 {
|
||||
panic(fmt.Sprintf("Bits length must be a power of two, got %d", length))
|
||||
}
|
||||
|
||||
nWords := length / bitsPerWord
|
||||
if nWords == 0 {
|
||||
nWords = 1
|
||||
}
|
||||
b := &Bits{
|
||||
length: length,
|
||||
lengthMask: length - 1,
|
||||
bits: make([]uint64, nWords),
|
||||
func NewBits(bits uint64) *Bits {
|
||||
return &Bits{
|
||||
length: bits,
|
||||
bits: make([]bool, bits, bits),
|
||||
current: 0,
|
||||
lostCounter: metrics.GetOrRegisterCounter("network.packets.lost", nil),
|
||||
dupeCounter: metrics.GetOrRegisterCounter("network.packets.duplicate", nil),
|
||||
outOfWindowCounter: metrics.GetOrRegisterCounter("network.packets.out_of_window", nil),
|
||||
}
|
||||
|
||||
// There is no counter value 0, mark it to avoid counting a lost packet later.
|
||||
b.bits[0] = 1
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Bits) get(i uint64) bool {
|
||||
pos := i & b.lengthMask
|
||||
//bit-shifting by 6 because i is a bit index, not a u64 index, and we need to find the u64 without bit in it
|
||||
return b.bits[pos>>6]&(uint64(1)<<(pos&63)) != 0
|
||||
}
|
||||
|
||||
func (b *Bits) set(i uint64) {
|
||||
pos := i & b.lengthMask
|
||||
b.bits[pos>>6] |= uint64(1) << (pos & 63)
|
||||
}
|
||||
|
||||
// clearRange clears `count` bits starting at circular position `startPos`
|
||||
// (already masked to [0, length)) and returns how many of them were set
|
||||
// before the clear. count must be in [1, length].
|
||||
func (b *Bits) clearRange(startPos, count uint64) uint64 {
|
||||
wasSet := uint64(0)
|
||||
if count >= b.length {
|
||||
for _, w := range b.bits {
|
||||
wasSet += uint64(mathbits.OnesCount64(w))
|
||||
}
|
||||
clear(b.bits)
|
||||
return wasSet
|
||||
}
|
||||
|
||||
pos := startPos
|
||||
remaining := count
|
||||
|
||||
// handle the potential partial word before pos becomes u64 aligned
|
||||
word := pos >> 6
|
||||
bit := pos & 63
|
||||
take := uint64(64) - bit
|
||||
if take > remaining {
|
||||
take = remaining
|
||||
}
|
||||
if take > b.length-pos {
|
||||
take = b.length - pos
|
||||
}
|
||||
var mask uint64
|
||||
if take == 64 {
|
||||
mask = math.MaxUint64
|
||||
} else {
|
||||
mask = ((uint64(1) << take) - 1) << bit
|
||||
}
|
||||
wasSet += uint64(mathbits.OnesCount64(b.bits[word] & mask))
|
||||
b.bits[word] &^= mask
|
||||
remaining -= take
|
||||
pos = (pos + take) & b.lengthMask
|
||||
|
||||
// Clear whole words, keeping track of the number of set bits
|
||||
for remaining >= 64 {
|
||||
word = pos >> 6
|
||||
wasSet += uint64(mathbits.OnesCount64(b.bits[word]))
|
||||
b.bits[word] = 0
|
||||
remaining -= 64
|
||||
pos = (pos + 64) & b.lengthMask
|
||||
}
|
||||
|
||||
// Clear the remaining partial word
|
||||
if remaining > 0 {
|
||||
word = pos >> 6
|
||||
mask = (uint64(1) << remaining) - 1
|
||||
wasSet += uint64(mathbits.OnesCount64(b.bits[word] & mask))
|
||||
b.bits[word] &^= mask
|
||||
}
|
||||
|
||||
return wasSet
|
||||
}
|
||||
|
||||
func (b *Bits) strictlyWithinWindow(i uint64) bool {
|
||||
// Handle the case where the window hasn't slid yet. This avoids u64 underflow.
|
||||
inWarmup := b.current < b.length
|
||||
if i < b.length && inWarmup {
|
||||
return true
|
||||
}
|
||||
|
||||
// Next, if the packet is in-window, see if we've seen it before
|
||||
if i > b.current-b.length {
|
||||
return true
|
||||
}
|
||||
return false //not within window!
|
||||
}
|
||||
|
||||
// Check returns true if i is within (or way out in front of) the window, and not a replay
|
||||
func (b *Bits) Check(l *slog.Logger, i uint64) bool {
|
||||
func (b *Bits) Check(l logrus.FieldLogger, i uint64) bool {
|
||||
// If i is the next number, return true.
|
||||
if i > b.current {
|
||||
if i > b.current || (i == 0 && b.firstSeen == false && b.current < b.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
if b.strictlyWithinWindow(i) {
|
||||
return !b.get(i)
|
||||
// If i is within the window, check if it's been set already. The first window will fail this check
|
||||
if i > b.current-b.length {
|
||||
return !b.bits[i%b.length]
|
||||
}
|
||||
|
||||
// If i is within the first window
|
||||
if i < b.length {
|
||||
return !b.bits[i%b.length]
|
||||
}
|
||||
|
||||
// Not within the window
|
||||
if l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
l.Debug("rejected a packet (top)", "current", b.current, "incoming", i)
|
||||
}
|
||||
l.Debugf("rejected a packet (top) %d %d\n", b.current, i)
|
||||
return false
|
||||
}
|
||||
|
||||
// Update has three branches:
|
||||
// - i == b.current+1: fast path; advance the cursor by one and lose-count
|
||||
// the slot we just stomped (only past warmup; see the i > b.length guard
|
||||
// below).
|
||||
// - i > b.current+1: jump path; clear all slots between current and i
|
||||
// (or up to a full window's worth, whichever is smaller) via clearRange,
|
||||
// then mark i. Two arms here: a warmup arm that handles the very first
|
||||
// window before the cursor has slid, and a steady-state arm that treats
|
||||
// every cleared empty slot as a lost packet.
|
||||
// - i <= b.current: in-window check for duplicates; out-of-window otherwise.
|
||||
//
|
||||
// NewBits seeds bits[0]=1 so counter 0 looks "received" — Update never
|
||||
// clears that marker during warmup (clearRange skips position 0 when
|
||||
// startPos=1), and once b.current >= b.length the marker is no longer
|
||||
// consulted. The marker prevents a fictitious "lost" hit on the first real
|
||||
// counter.
|
||||
func (b *Bits) Update(l *slog.Logger, i uint64) bool {
|
||||
// Fast path: i is the next expected counter. Split out so the function
|
||||
// stays small and avoids paying for the slow paths' slog argument-build
|
||||
// stack frame on every call. The bit read/test/write is inlined to
|
||||
// touch the backing word once.
|
||||
func (b *Bits) Update(l *logrus.Logger, i uint64) bool {
|
||||
// If i is the next number, return true and update current.
|
||||
if i == b.current+1 {
|
||||
pos := i & b.lengthMask
|
||||
word := pos >> 6
|
||||
mask := uint64(1) << (pos & 63)
|
||||
w := b.bits[word]
|
||||
if i > b.length && w&mask == 0 {
|
||||
// Report missed packets, we can only understand what was missed after the first window has been gone through
|
||||
if i > b.length && b.bits[i%b.length] == false {
|
||||
b.lostCounter.Inc(1)
|
||||
}
|
||||
b.bits[word] = w | mask
|
||||
b.bits[i%b.length] = true
|
||||
b.current = i
|
||||
return true
|
||||
}
|
||||
return b.updateSlow(l, i)
|
||||
}
|
||||
|
||||
// updateSlow handles jumps, in-window backfill, dupes, and out-of-window.
|
||||
func (b *Bits) updateSlow(l *slog.Logger, i uint64) bool {
|
||||
// If i is a jump, adjust the window, record lost, update current, and return true
|
||||
if i > b.current {
|
||||
end := i
|
||||
if end > b.current+b.length {
|
||||
end = b.current + b.length
|
||||
}
|
||||
count := end - b.current
|
||||
startPos := (b.current + 1) & b.lengthMask
|
||||
|
||||
var lost int64
|
||||
if b.current >= b.length {
|
||||
// Steady state: every cleared slot is past warmup, so any unset
|
||||
// bit we evict is a lost packet from the previous cycle.
|
||||
wasSet := b.clearRange(startPos, count)
|
||||
lost = int64(count) - int64(wasSet)
|
||||
} else {
|
||||
// Warmup (the very first window). Some cleared slots represent
|
||||
// packets <= length where eviction is not "lost" in the usual
|
||||
// sense. This branch is taken at most once per connection so we
|
||||
// don't bother optimizing it.
|
||||
for n := b.current + 1; n <= end; n++ {
|
||||
if !b.get(n) && n > b.length {
|
||||
lost++
|
||||
}
|
||||
}
|
||||
b.clearRange(startPos, count)
|
||||
// If i packet is greater than current but less than the maximum length of our bitmap,
|
||||
// flip everything in between to false and move ahead.
|
||||
if i > b.current && i < b.current+b.length {
|
||||
// In between current and i need to be zero'd to allow those packets to come in later
|
||||
for n := b.current + 1; n < i; n++ {
|
||||
b.bits[n%b.length] = false
|
||||
}
|
||||
|
||||
// Anything past the new window can never be backfilled, so it's lost.
|
||||
if i > b.current+b.length {
|
||||
lost += int64(i - b.current - b.length)
|
||||
b.bits[i%b.length] = true
|
||||
b.current = i
|
||||
//l.Debugf("missed %d packets between %d and %d\n", i-b.current, i, b.current)
|
||||
return true
|
||||
}
|
||||
|
||||
// If i is greater than the delta between current and the total length of our bitmap,
|
||||
// just flip everything in the map and move ahead.
|
||||
if i >= b.current+b.length {
|
||||
// The current window loss will be accounted for later, only record the jump as loss up until then
|
||||
lost := maxInt64(0, int64(i-b.current-b.length))
|
||||
//TODO: explain this
|
||||
if b.current == 0 {
|
||||
lost++
|
||||
}
|
||||
|
||||
for n := range b.bits {
|
||||
// Don't want to count the first window as a loss
|
||||
//TODO: this is likely wrong, we are wanting to track only the bit slots that we aren't going to track anymore and this is marking everything as missed
|
||||
//if b.bits[n] == false {
|
||||
// lost++
|
||||
//}
|
||||
b.bits[n] = false
|
||||
}
|
||||
|
||||
b.lostCounter.Inc(lost)
|
||||
|
||||
b.set(i)
|
||||
if l.Level >= logrus.DebugLevel {
|
||||
l.WithField("receiveWindow", m{"accepted": true, "currentCounter": b.current, "incomingCounter": i, "reason": "window shifting"}).
|
||||
Debug("Receive window")
|
||||
}
|
||||
b.bits[i%b.length] = true
|
||||
b.current = i
|
||||
return true
|
||||
}
|
||||
|
||||
// If i is within the current window but below the current counter, check to see if it's a duplicate
|
||||
if b.strictlyWithinWindow(i) {
|
||||
pos := i & b.lengthMask
|
||||
word := pos >> 6
|
||||
mask := uint64(1) << (pos & 63)
|
||||
w := b.bits[word]
|
||||
if b.current == i || w&mask != 0 {
|
||||
if l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
l.Debug("Receive window",
|
||||
"accepted", false,
|
||||
"currentCounter", b.current,
|
||||
"incomingCounter", i,
|
||||
"reason", "duplicate",
|
||||
)
|
||||
// Allow for the 0 packet to come in within the first window
|
||||
if i == 0 && b.firstSeen == false && b.current < b.length {
|
||||
b.firstSeen = true
|
||||
b.bits[i%b.length] = true
|
||||
return true
|
||||
}
|
||||
|
||||
// If i is within the window of current minus length (the total pat window size),
|
||||
// allow it and flip to true but to NOT change current. We also have to account for the first window
|
||||
if ((b.current >= b.length && i > b.current-b.length) || (b.current < b.length && i < b.length)) && i <= b.current {
|
||||
if b.current == i {
|
||||
if l.Level >= logrus.DebugLevel {
|
||||
l.WithField("receiveWindow", m{"accepted": false, "currentCounter": b.current, "incomingCounter": i, "reason": "duplicate"}).
|
||||
Debug("Receive window")
|
||||
}
|
||||
b.dupeCounter.Inc(1)
|
||||
return false
|
||||
}
|
||||
|
||||
b.bits[word] = w | mask
|
||||
if b.bits[i%b.length] == true {
|
||||
if l.Level >= logrus.DebugLevel {
|
||||
l.WithField("receiveWindow", m{"accepted": false, "currentCounter": b.current, "incomingCounter": i, "reason": "old duplicate"}).
|
||||
Debug("Receive window")
|
||||
}
|
||||
b.dupeCounter.Inc(1)
|
||||
return false
|
||||
}
|
||||
|
||||
b.bits[i%b.length] = true
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// In all other cases, fail and don't change current.
|
||||
b.outOfWindowCounter.Inc(1)
|
||||
if l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
l.Debug("Receive window",
|
||||
"accepted", false,
|
||||
"currentCounter", b.current,
|
||||
"incomingCounter", i,
|
||||
"reason", "nonsense",
|
||||
)
|
||||
if l.Level >= logrus.DebugLevel {
|
||||
l.WithField("accepted", false).
|
||||
WithField("currentCounter", b.current).
|
||||
WithField("incomingCounter", i).
|
||||
WithField("reason", "nonsense").
|
||||
Debug("Receive window")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func maxInt64(a, b int64) int64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
+117
-327
@@ -7,114 +7,77 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// snapshot returns the bitmap as a []bool of length b.length, for readable
|
||||
// test assertions against the now-packed []uint64 storage.
|
||||
func (b *Bits) snapshot() []bool {
|
||||
out := make([]bool, b.length)
|
||||
for i := uint64(0); i < b.length; i++ {
|
||||
out[i] = b.get(i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestBitsRequiresPowerOfTwo(t *testing.T) {
|
||||
assert.Panics(t, func() { NewBits(10) })
|
||||
assert.Panics(t, func() { NewBits(0) })
|
||||
assert.NotPanics(t, func() { NewBits(1) })
|
||||
assert.NotPanics(t, func() { NewBits(16) })
|
||||
assert.NotPanics(t, func() { NewBits(1024) })
|
||||
assert.NotPanics(t, func() { NewBits(16384) })
|
||||
}
|
||||
|
||||
func TestBits(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
assert.EqualValues(t, 16, b.length)
|
||||
b := NewBits(10)
|
||||
|
||||
// make sure it is the right size
|
||||
assert.Len(t, b.bits, 10)
|
||||
|
||||
// This is initialized to zero - receive one. This should work.
|
||||
|
||||
assert.True(t, b.Check(l, 1))
|
||||
assert.True(t, b.Update(l, 1))
|
||||
u := b.Update(l, 1)
|
||||
assert.True(t, u)
|
||||
assert.EqualValues(t, 1, b.current)
|
||||
g := []bool{true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.snapshot())
|
||||
g := []bool{false, true, false, false, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.bits)
|
||||
|
||||
// Receive two
|
||||
assert.True(t, b.Check(l, 2))
|
||||
assert.True(t, b.Update(l, 2))
|
||||
u = b.Update(l, 2)
|
||||
assert.True(t, u)
|
||||
assert.EqualValues(t, 2, b.current)
|
||||
g = []bool{true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.snapshot())
|
||||
g = []bool{false, true, true, false, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.bits)
|
||||
|
||||
// Receive two again - it will fail
|
||||
assert.False(t, b.Check(l, 2))
|
||||
assert.False(t, b.Update(l, 2))
|
||||
u = b.Update(l, 2)
|
||||
assert.False(t, u)
|
||||
assert.EqualValues(t, 2, b.current)
|
||||
|
||||
// Jump ahead to 25, which clears the window and sets slot 25%16 = 9.
|
||||
assert.True(t, b.Check(l, 25))
|
||||
assert.True(t, b.Update(l, 25))
|
||||
assert.EqualValues(t, 25, b.current)
|
||||
g = []bool{false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.snapshot())
|
||||
// Jump ahead to 15, which should clear everything and set the 6th element
|
||||
assert.True(t, b.Check(l, 15))
|
||||
u = b.Update(l, 15)
|
||||
assert.True(t, u)
|
||||
assert.EqualValues(t, 15, b.current)
|
||||
g = []bool{false, false, false, false, false, true, false, false, false, false}
|
||||
assert.Equal(t, g, b.bits)
|
||||
|
||||
// Mark 24, which is in window (current 25, length 16, window covers [10,25]).
|
||||
assert.True(t, b.Check(l, 24))
|
||||
assert.True(t, b.Update(l, 24))
|
||||
assert.EqualValues(t, 25, b.current)
|
||||
g = []bool{false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.snapshot())
|
||||
// Mark 14, which is allowed because it is in the window
|
||||
assert.True(t, b.Check(l, 14))
|
||||
u = b.Update(l, 14)
|
||||
assert.True(t, u)
|
||||
assert.EqualValues(t, 15, b.current)
|
||||
g = []bool{false, false, false, false, true, true, false, false, false, false}
|
||||
assert.Equal(t, g, b.bits)
|
||||
|
||||
// Mark 5, not allowed because 5 <= current-length (25-16=9).
|
||||
// Mark 5, which is not allowed because it is not in the window
|
||||
assert.False(t, b.Check(l, 5))
|
||||
assert.False(t, b.Update(l, 5))
|
||||
assert.EqualValues(t, 25, b.current)
|
||||
g = []bool{false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false}
|
||||
assert.Equal(t, g, b.snapshot())
|
||||
u = b.Update(l, 5)
|
||||
assert.False(t, u)
|
||||
assert.EqualValues(t, 15, b.current)
|
||||
g = []bool{false, false, false, false, true, true, false, false, false, false}
|
||||
assert.Equal(t, g, b.bits)
|
||||
|
||||
// Make sure we handle wrapping around once to the same slot. With
|
||||
// length=16, packets 1 and 17 share slot 1.
|
||||
b = NewBits(16)
|
||||
// make sure we handle wrapping around once to the current position
|
||||
b = NewBits(10)
|
||||
assert.True(t, b.Update(l, 1))
|
||||
assert.True(t, b.Update(l, 17))
|
||||
assert.Equal(t, []bool{false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}, b.snapshot())
|
||||
assert.True(t, b.Update(l, 11))
|
||||
assert.Equal(t, []bool{false, true, false, false, false, false, false, false, false, false}, b.bits)
|
||||
|
||||
// Walk through a few windows in order
|
||||
b = NewBits(16)
|
||||
for i := uint64(1); i <= 100; i++ {
|
||||
b = NewBits(10)
|
||||
for i := uint64(0); i <= 100; i++ {
|
||||
assert.True(t, b.Check(l, i), "Error while checking %v", i)
|
||||
assert.True(t, b.Update(l, i), "Error while updating %v", i)
|
||||
}
|
||||
|
||||
assert.False(t, b.Check(l, 1), "Out of window check")
|
||||
}
|
||||
|
||||
func TestBitsLargeJumps(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
|
||||
// length=16. Update(55) from current=0:
|
||||
// warmup, per-bit loop sees no n>16 with unset bits (slot 0 was set by
|
||||
// NewBits and gets re-evaluated when n=16; n=16 is not strictly > 16),
|
||||
// so the loop contributes 0. The jump exceeds the window so we record
|
||||
// 55 - 0 - 16 = 39 packets fell out the back.
|
||||
b := NewBits(16)
|
||||
b.lostCounter.Clear()
|
||||
assert.True(t, b.Update(l, 55))
|
||||
assert.Equal(t, int64(39), b.lostCounter.Count())
|
||||
|
||||
// Update(100): clears 16 slots starting at slot 56%16=8. Only slot 7 (for
|
||||
// packet 55) was set, so 16 - 1 = 15 evicted slots had unset bits.
|
||||
// Plus 100 - 55 - 16 = 29 packets fell past the window. Total 44.
|
||||
assert.True(t, b.Update(l, 100))
|
||||
assert.Equal(t, int64(39+44), b.lostCounter.Count())
|
||||
|
||||
// Update(200): same shape: 16 - 1 = 15 evicted unset, plus 200 - 100 - 16 = 84 past window. Total 99.
|
||||
assert.True(t, b.Update(l, 200))
|
||||
assert.Equal(t, int64(39+44+99), b.lostCounter.Count())
|
||||
}
|
||||
|
||||
func TestBitsDupeCounter(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
b := NewBits(10)
|
||||
b.lostCounter.Clear()
|
||||
b.dupeCounter.Clear()
|
||||
b.outOfWindowCounter.Clear()
|
||||
@@ -139,300 +102,127 @@ func TestBitsDupeCounter(t *testing.T) {
|
||||
|
||||
func TestBitsOutOfWindowCounter(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
b := NewBits(10)
|
||||
b.lostCounter.Clear()
|
||||
b.dupeCounter.Clear()
|
||||
b.outOfWindowCounter.Clear()
|
||||
|
||||
// Jump to 20 (warmup branch + 4 past-window packets).
|
||||
assert.True(t, b.Update(l, 20))
|
||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||
|
||||
// 9 single-step advances, each evicts a slot whose bit was cleared during
|
||||
// the jump above and whose value was never seen, so each contributes 1
|
||||
// to lostCounter.
|
||||
for n := uint64(21); n <= 29; n++ {
|
||||
assert.True(t, b.Update(l, n))
|
||||
}
|
||||
assert.True(t, b.Update(l, 21))
|
||||
assert.True(t, b.Update(l, 22))
|
||||
assert.True(t, b.Update(l, 23))
|
||||
assert.True(t, b.Update(l, 24))
|
||||
assert.True(t, b.Update(l, 25))
|
||||
assert.True(t, b.Update(l, 26))
|
||||
assert.True(t, b.Update(l, 27))
|
||||
assert.True(t, b.Update(l, 28))
|
||||
assert.True(t, b.Update(l, 29))
|
||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||
|
||||
// 0 is below current-length (29-16=13) so it falls outside the window.
|
||||
assert.False(t, b.Update(l, 0))
|
||||
assert.Equal(t, int64(1), b.outOfWindowCounter.Count())
|
||||
|
||||
// 4 from the Update(20) jump + 9 from 21..29.
|
||||
assert.Equal(t, int64(13), b.lostCounter.Count())
|
||||
//tODO: make sure lostcounter doesn't increase in orderly increment
|
||||
assert.Equal(t, int64(20), b.lostCounter.Count())
|
||||
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
||||
assert.Equal(t, int64(1), b.outOfWindowCounter.Count())
|
||||
}
|
||||
|
||||
func TestBitsLostCounter(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
b := NewBits(10)
|
||||
b.lostCounter.Clear()
|
||||
b.dupeCounter.Clear()
|
||||
b.outOfWindowCounter.Clear()
|
||||
|
||||
// Walk 20..29 like the original, just with a bigger window. Same
|
||||
// reasoning as TestBitsOutOfWindowCounter: 4 past-window from Update(20),
|
||||
// then 9 more from the unit advances.
|
||||
for n := uint64(20); n <= 29; n++ {
|
||||
assert.True(t, b.Update(l, n))
|
||||
}
|
||||
assert.Equal(t, int64(13), b.lostCounter.Count())
|
||||
//assert.True(t, b.Update(0))
|
||||
assert.True(t, b.Update(l, 0))
|
||||
assert.True(t, b.Update(l, 20))
|
||||
assert.True(t, b.Update(l, 21))
|
||||
assert.True(t, b.Update(l, 22))
|
||||
assert.True(t, b.Update(l, 23))
|
||||
assert.True(t, b.Update(l, 24))
|
||||
assert.True(t, b.Update(l, 25))
|
||||
assert.True(t, b.Update(l, 26))
|
||||
assert.True(t, b.Update(l, 27))
|
||||
assert.True(t, b.Update(l, 28))
|
||||
assert.True(t, b.Update(l, 29))
|
||||
assert.Equal(t, int64(20), b.lostCounter.Count())
|
||||
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||
|
||||
b = NewBits(16)
|
||||
b = NewBits(10)
|
||||
b.lostCounter.Clear()
|
||||
b.dupeCounter.Clear()
|
||||
b.outOfWindowCounter.Clear()
|
||||
|
||||
// Update(15) clears the warmup window (no lost), sets slot 15.
|
||||
assert.True(t, b.Update(l, 15))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
|
||||
// Update(16): slot 0 was already set (NewBits seeded it), and 16 is not
|
||||
// strictly > length, so nothing is recorded as lost.
|
||||
assert.True(t, b.Update(l, 16))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
|
||||
// Update(17): we jumped straight from 0 to 15, so slot 1 was cleared
|
||||
// (and never re-set). 17 > 16 is past warmup, so packet 1 is recorded lost.
|
||||
assert.True(t, b.Update(l, 17))
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
|
||||
// Fill in 18..30 in single steps. Each i evicts slot i%16. Slots 2..14
|
||||
// were all cleared during Update(15), and we never re-set any of them,
|
||||
// so each i in 18..30 is a fresh lost packet — 13 more.
|
||||
for n := uint64(18); n <= 30; n++ {
|
||||
assert.True(t, b.Update(l, n))
|
||||
}
|
||||
assert.Equal(t, int64(14), b.lostCounter.Count())
|
||||
|
||||
// Jump ahead by exactly one window size.
|
||||
assert.True(t, b.Update(l, 46))
|
||||
// end = min(46, 30+16) = 46, count = 16, all slots cleared. Before the
|
||||
// jump every slot 0..15 had been set (Update(15), (16), (17), 18..30),
|
||||
// so wasSet=16 and 46 == current+length means no past-window slack:
|
||||
// lost contribution = 0.
|
||||
assert.Equal(t, int64(14), b.lostCounter.Count())
|
||||
|
||||
// Walk 47..55. The Update(46) jump cleared every slot, so only slot 14
|
||||
// (for packet 46) is set when we start. Each subsequent unit step lands
|
||||
// on a slot that was cleared and is past warmup, so it counts as lost.
|
||||
// 9 more = 23.
|
||||
for n := uint64(47); n <= 55; n++ {
|
||||
assert.True(t, b.Update(l, n))
|
||||
}
|
||||
assert.Equal(t, int64(23), b.lostCounter.Count())
|
||||
|
||||
// Jump ahead by two windows: clears the window plus past-window loss.
|
||||
assert.True(t, b.Update(l, 87))
|
||||
// current=55, length=16. end = min(87, 71) = 71. count=16, all slots
|
||||
// cleared. Slots set before the clear are slots 14,15,0..7 (10 total).
|
||||
// Lost from clear = 16 - 10 = 6. Past window: 87 - 55 - 16 = 16. +22.
|
||||
assert.Equal(t, int64(45), b.lostCounter.Count())
|
||||
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||
}
|
||||
|
||||
func TestBitsLostCounterIssue1(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
b.lostCounter.Clear()
|
||||
b.dupeCounter.Clear()
|
||||
b.outOfWindowCounter.Clear()
|
||||
|
||||
// Receive 4, backfill 1, then 9, 2, 3, 5, 6, 7 (skip 8), 10, 11, 14.
|
||||
// Then jump to 25 — slot 25%16=9 is being evicted, but it had been set
|
||||
// (we received packet 9), so no spurious lost increment. The original
|
||||
// regression was about double-counting a missing packet when its slot
|
||||
// got cleared on a jump. With the jump path now using clearRange's
|
||||
// word-level wasSet count, the same semantics hold.
|
||||
assert.True(t, b.Update(l, 4))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 1))
|
||||
assert.True(t, b.Update(l, 0))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 9))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 2))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 3))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 5))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 6))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 7))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
// Skip packet 8.
|
||||
// 10 will set 0 index, 0 was already set, no lost packets
|
||||
assert.True(t, b.Update(l, 10))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
// 11 will set 1 index, 1 was missed, we should see 1 packet lost
|
||||
assert.True(t, b.Update(l, 11))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
|
||||
assert.True(t, b.Update(l, 14))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
|
||||
// Jump to 25. With length=16, slot 25%16=9 corresponds to packet 9
|
||||
// (which we DID receive), so its bit is set and no lost++ from that
|
||||
// eviction. The trace below shows the only loss is packet 8.
|
||||
assert.True(t, b.Update(l, 25))
|
||||
// current was 14, i=25. end=min(25,30)=25. count=11. startPos=15.
|
||||
// steady? current=14<16, so warmup branch: per-bit n=15..25, count those
|
||||
// with !get(n) AND n>16. n=17..25 are >16. Among slots 17%16=1..25%16=9
|
||||
// did we set slots 1..9 (packets 1..9)? Yes for all but slot 8 (packet 8
|
||||
// was skipped). n=24 maps to slot 8 which is FALSE → lost++. All other
|
||||
// n in 17..25 map to slots that are set. n=16 is not strictly > 16. So
|
||||
// lost = 1.
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
|
||||
// Fill in 12, 13, 15, 16. Each is below current=25 (in-window). 16 must
|
||||
// recheck slot 0 — it was set by NewBits and then cleared by the
|
||||
// Update(25) jump, so 16 backfills cleanly.
|
||||
// Now let's fill in the window, should end up with 8 lost packets
|
||||
assert.True(t, b.Update(l, 12))
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 13))
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 14))
|
||||
assert.True(t, b.Update(l, 15))
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 16))
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
assert.True(t, b.Update(l, 17))
|
||||
assert.True(t, b.Update(l, 18))
|
||||
assert.True(t, b.Update(l, 19))
|
||||
assert.Equal(t, int64(8), b.lostCounter.Count())
|
||||
|
||||
// We missed packet 8 above and that loss is still recorded once, never
|
||||
// double-counted, never zeroed.
|
||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||
// Jump ahead by a window size
|
||||
assert.True(t, b.Update(l, 29))
|
||||
assert.Equal(t, int64(8), b.lostCounter.Count())
|
||||
// Now lets walk ahead normally through the window, the missed packets should fill in
|
||||
assert.True(t, b.Update(l, 30))
|
||||
assert.True(t, b.Update(l, 31))
|
||||
assert.True(t, b.Update(l, 32))
|
||||
assert.True(t, b.Update(l, 33))
|
||||
assert.True(t, b.Update(l, 34))
|
||||
assert.True(t, b.Update(l, 35))
|
||||
assert.True(t, b.Update(l, 36))
|
||||
assert.True(t, b.Update(l, 37))
|
||||
assert.True(t, b.Update(l, 38))
|
||||
// 39 packets tracked, 22 seen, 17 lost
|
||||
assert.Equal(t, int64(17), b.lostCounter.Count())
|
||||
|
||||
// Jump ahead by 2 windows, should have recording 1 full window missing
|
||||
assert.True(t, b.Update(l, 58))
|
||||
assert.Equal(t, int64(27), b.lostCounter.Count())
|
||||
// Now lets walk ahead normally through the window, the missed packets should fill in from this window
|
||||
assert.True(t, b.Update(l, 59))
|
||||
assert.True(t, b.Update(l, 60))
|
||||
assert.True(t, b.Update(l, 61))
|
||||
assert.True(t, b.Update(l, 62))
|
||||
assert.True(t, b.Update(l, 63))
|
||||
assert.True(t, b.Update(l, 64))
|
||||
assert.True(t, b.Update(l, 65))
|
||||
assert.True(t, b.Update(l, 66))
|
||||
assert.True(t, b.Update(l, 67))
|
||||
// 68 packets tracked, 32 seen, 36 missed
|
||||
assert.Equal(t, int64(36), b.lostCounter.Count())
|
||||
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||
}
|
||||
|
||||
// TestBitsWarmupOvershoot exercises the jump path's warmup arm with an
|
||||
// overshoot past one full window. NewBits leaves current=0 with only slot 0
|
||||
// "set" by the marker. Jumping straight to length+k must (a) clear every
|
||||
// slot the jump straddles, (b) count only past-window slack (not the
|
||||
// in-window slots, which never had a "lost" tenant during warmup), and
|
||||
// (c) leave the cursor at the new counter so subsequent unit advances
|
||||
// count from steady state. The marker bit at slot 0 is irrelevant once
|
||||
// current >= length.
|
||||
func TestBitsWarmupOvershoot(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
b.lostCounter.Clear()
|
||||
|
||||
// Jump from current=0 to i=20 (length=16, overshoot=4).
|
||||
// Warmup arm: counts slots in [1..16] where bit unset and n>length.
|
||||
// Only n=16 was unset and >length: but slot 16%16=0 is the marker,
|
||||
// so b.get(16) reads bits[0]=1 and skips. Result: 0 lost from the loop.
|
||||
// Past-window: i - current - length = 20 - 0 - 16 = 4 lost.
|
||||
assert.True(t, b.Update(l, 20))
|
||||
assert.Equal(t, int64(4), b.lostCounter.Count())
|
||||
assert.Equal(t, uint64(20), b.current)
|
||||
|
||||
// Steady state now (current=20 >= length=16). Unit advance to 21
|
||||
// stomps slot 21%16=5, which was cleared by the jump and not reset,
|
||||
// so this is +1 lost.
|
||||
assert.True(t, b.Update(l, 21))
|
||||
assert.Equal(t, int64(5), b.lostCounter.Count())
|
||||
}
|
||||
|
||||
// TestBitsCheckAcrossWarmupBoundary pins the underflow trick in Check's
|
||||
// in-window clause. While in warmup, b.current-b.length underflows uint64
|
||||
// to a huge value so the first OR-clause is always false; the second
|
||||
// clause (i < length && current < length) carries the in-window check.
|
||||
// Once current >= length the regimes flip cleanly.
|
||||
func TestBitsCheckAcrossWarmupBoundary(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(16)
|
||||
|
||||
// Warmup: current=0. Check(0) must read the marker (set) and return false.
|
||||
assert.False(t, b.Check(l, 0), "marker slot should look already-received")
|
||||
// Warmup: any 0 < i < length is in-window and unset → accepted.
|
||||
for i := uint64(1); i < 16; i++ {
|
||||
assert.True(t, b.Check(l, i), "warmup in-window i=%d should be accepted", i)
|
||||
}
|
||||
// Warmup: i >= length but > current is "next number" so accepted.
|
||||
assert.True(t, b.Check(l, 16))
|
||||
assert.True(t, b.Check(l, 1_000_000))
|
||||
|
||||
// Cross into steady state.
|
||||
assert.True(t, b.Update(l, 100))
|
||||
// Now current=100, length=16. In-window range is [85..100].
|
||||
// 84 is just outside: the underflow clause activates; 84 > 100-16=84 is false.
|
||||
// And the warmup clause is false (current >= length). So out of window.
|
||||
assert.False(t, b.Check(l, 84))
|
||||
// 85 sits at the boundary. 85 > 84 is true → in window, unset → accept.
|
||||
assert.True(t, b.Check(l, 85))
|
||||
// 100 is current itself; not strictly greater, in-window, but already set.
|
||||
assert.False(t, b.Check(l, 100))
|
||||
// Way out: clearly out of window.
|
||||
assert.False(t, b.Check(l, 50))
|
||||
}
|
||||
|
||||
// TestBitsMarkerInvariant verifies the seeded bits[0]=1 marker behaves
|
||||
// correctly across warmup and beyond. Update should never clear the marker
|
||||
// during warmup (clearRange skips position 0 when startPos=1), and once
|
||||
// current >= length the marker is no longer consulted by Check/Update on
|
||||
// the live path — but it must still report counter 0 as a duplicate while
|
||||
// we are in warmup.
|
||||
func TestBitsMarkerInvariant(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
b := NewBits(8)
|
||||
|
||||
// Counter 0 is the seeded marker; Check sees it as already received.
|
||||
assert.False(t, b.Check(l, 0))
|
||||
// Update(0) at current=0 hits the duplicate branch.
|
||||
b.dupeCounter.Clear()
|
||||
assert.False(t, b.Update(l, 0))
|
||||
assert.Equal(t, int64(1), b.dupeCounter.Count())
|
||||
|
||||
// Walk forward through warmup; the marker must remain set.
|
||||
for n := uint64(1); n <= 7; n++ {
|
||||
assert.True(t, b.Update(l, n))
|
||||
}
|
||||
// Position 0 (the marker) should still read as set because we never
|
||||
// cleared it; Update(0) still looks like a duplicate.
|
||||
assert.False(t, b.Check(l, 0))
|
||||
|
||||
// Cross into steady state with a unit advance to 8: pos=0, evicts the
|
||||
// marker bit. The lost-counter guard (i > b.length) is false (8 == 8),
|
||||
// so this advance does NOT charge a lost packet — exactly what the
|
||||
// marker is there to prevent.
|
||||
b.lostCounter.Clear()
|
||||
assert.True(t, b.Update(l, 8))
|
||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||
// The slot at pos 0 is now occupied by counter 8.
|
||||
assert.False(t, b.Check(l, 8))
|
||||
}
|
||||
|
||||
// BenchmarkBitsUpdateInOrder is the steady-state hot path: each call is
|
||||
// i == current+1.
|
||||
func BenchmarkBitsUpdateInOrder(b *testing.B) {
|
||||
l := test.NewLogger()
|
||||
z := NewBits(16384)
|
||||
func BenchmarkBits(b *testing.B) {
|
||||
z := NewBits(10)
|
||||
for n := 0; n < b.N; n++ {
|
||||
z.Update(l, uint64(n)+1)
|
||||
}
|
||||
}
|
||||
for i := range z.bits {
|
||||
z.bits[i] = true
|
||||
}
|
||||
for i := range z.bits {
|
||||
z.bits[i] = false
|
||||
}
|
||||
|
||||
// BenchmarkBitsUpdateReorder simulates light reorder within the window:
|
||||
// every other packet arrives one slot behind its predecessor (forces the
|
||||
// in-window backfill branch).
|
||||
func BenchmarkBitsUpdateReorder(b *testing.B) {
|
||||
l := test.NewLogger()
|
||||
z := NewBits(16384)
|
||||
for n := 0; n < b.N; n++ {
|
||||
base := uint64(n) * 2
|
||||
z.Update(l, base+2)
|
||||
z.Update(l, base+1)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBitsUpdateLargeJumps stresses the clearRange word-level path.
|
||||
func BenchmarkBitsUpdateLargeJumps(b *testing.B) {
|
||||
l := test.NewLogger()
|
||||
z := NewBits(16384)
|
||||
for n := 0; n < b.N; n++ {
|
||||
z.Update(l, uint64(n+1)*1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//go:build boringcrypto
|
||||
// +build boringcrypto
|
||||
|
||||
package nebula
|
||||
|
||||
|
||||
+9
-58
@@ -1,14 +1,11 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -32,46 +29,22 @@ func NewCAPool() *CAPool {
|
||||
// If the pool contains any expired certificates, an ErrExpired will be
|
||||
// returned along with the pool. The caller must handle any such errors.
|
||||
func NewCAPoolFromPEM(caPEMs []byte) (*CAPool, error) {
|
||||
return NewCAPoolFromPEMReader(bytes.NewReader(caPEMs))
|
||||
}
|
||||
|
||||
// NewCAPoolFromPEMReader will create a new CA pool from the provided reader.
|
||||
// The reader must contain a PEM-encoded set of nebula certificates.
|
||||
func NewCAPoolFromPEMReader(r io.Reader) (*CAPool, error) {
|
||||
pool := NewCAPool()
|
||||
|
||||
var err error
|
||||
var expired bool
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(SplitPEM)
|
||||
|
||||
for scanner.Scan() {
|
||||
pemBytes := scanner.Bytes()
|
||||
|
||||
block, rest := pem.Decode(pemBytes)
|
||||
if len(bytes.TrimSpace(rest)) > 0 {
|
||||
return nil, ErrInvalidPEMBlock
|
||||
for {
|
||||
caPEMs, err = pool.AddCAFromPEM(caPEMs)
|
||||
if errors.Is(err, ErrExpired) {
|
||||
expired = true
|
||||
err = nil
|
||||
}
|
||||
if block == nil {
|
||||
return nil, ErrInvalidPEMBlock
|
||||
}
|
||||
|
||||
c, err := unmarshalCertificateBlock(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = pool.AddCA(c)
|
||||
if errors.Is(err, ErrExpired) {
|
||||
expired = true
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
if len(caPEMs) == 0 || strings.TrimSpace(string(caPEMs)) == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, ErrInvalidPEMBlock
|
||||
}
|
||||
|
||||
if expired {
|
||||
return pool, ErrExpired
|
||||
@@ -168,23 +141,10 @@ func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCerti
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pre nebula v1.10.3 could generate signatures in either high or low s form and validation
|
||||
// of signatures allowed for either. Nebula v1.10.3 and beyond clamps signature generation to low-s form
|
||||
// but validation still allows for either. Since a change in the signature bytes affects the fingerprint, we
|
||||
// need to test both forms until such a time comes that we enforce low-s form on signature validation.
|
||||
fp2, err := CalculateAlternateFingerprint(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not calculate alternate fingerprint to verify: %w", err)
|
||||
}
|
||||
if fp2 != "" && ncp.IsBlocklisted(fp2) {
|
||||
return nil, ErrBlockListed
|
||||
}
|
||||
|
||||
cc := CachedCertificate{
|
||||
Certificate: c,
|
||||
InvertedGroups: make(map[string]struct{}),
|
||||
Fingerprint: fp,
|
||||
fingerprint2: fp2,
|
||||
signerFingerprint: signer.Fingerprint,
|
||||
}
|
||||
|
||||
@@ -198,11 +158,6 @@ func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCerti
|
||||
// VerifyCachedCertificate is the same as VerifyCertificate other than it operates on a pre-verified structure and
|
||||
// is a cheaper operation to perform as a result.
|
||||
func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error {
|
||||
// Check any available alternate fingerprint forms for this certificate, re P256 high-s/low-s
|
||||
if c.fingerprint2 != "" && ncp.IsBlocklisted(c.fingerprint2) {
|
||||
return ErrBlockListed
|
||||
}
|
||||
|
||||
_, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint)
|
||||
return err
|
||||
}
|
||||
@@ -217,10 +172,6 @@ func (ncp *CAPool) verify(c Certificate, now time.Time, certFp string, signerFp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if signer.Certificate.Curve() != c.Curve() {
|
||||
return nil, ErrCurveMismatch
|
||||
}
|
||||
|
||||
if signer.Certificate.Expired(now) {
|
||||
return nil, ErrRootExpired
|
||||
}
|
||||
|
||||
+4
-128
@@ -1,14 +1,10 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert/p256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -115,60 +111,6 @@ k+coOv04r+zh33ISyhbsafnYduN17p2eD7CmHvHuerguXD9f32gcxo/KsFCKEjMe
|
||||
assert.Len(t, ppppp.CAs, 1)
|
||||
}
|
||||
|
||||
// oneByteReader wraps a reader to return at most 1 byte per Read call,
|
||||
// exercising the streaming accumulation logic in NewCAPoolFromPEMReader.
|
||||
type oneByteReader struct {
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (o *oneByteReader) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return o.r.Read(p[:1])
|
||||
}
|
||||
|
||||
func TestNewCAPoolFromPEMReader_EmptyReader(t *testing.T) {
|
||||
pool, err := NewCAPoolFromPEMReader(bytes.NewReader(nil))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, pool.CAs)
|
||||
|
||||
pool, err = NewCAPoolFromPEMReader(strings.NewReader(" \n\t\n "))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, pool.CAs)
|
||||
}
|
||||
|
||||
func TestNewCAPoolFromPEMReader_OneByteReads(t *testing.T) {
|
||||
ca1, _, _, pem1 := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(time.Hour), nil, nil, nil)
|
||||
ca2, _, _, pem2 := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(time.Hour), nil, nil, nil)
|
||||
|
||||
bundle := append(pem1, pem2...)
|
||||
pool, err := NewCAPoolFromPEMReader(&oneByteReader{r: bytes.NewReader(bundle)})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, pool.CAs, 2)
|
||||
|
||||
fp1, err := ca1.Fingerprint()
|
||||
require.NoError(t, err)
|
||||
fp2, err := ca2.Fingerprint()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, pool.CAs, fp1)
|
||||
assert.Contains(t, pool.CAs, fp2)
|
||||
}
|
||||
|
||||
func TestNewCAPoolFromPEMReader_TruncatedPEM(t *testing.T) {
|
||||
_, err := NewCAPoolFromPEMReader(strings.NewReader("-----BEGIN NEBULA CERTIFICATE-----\npartialdata"))
|
||||
assert.ErrorIs(t, err, ErrInvalidPEMBlock)
|
||||
}
|
||||
|
||||
func TestNewCAPoolFromPEMReader_TrailingGarbage(t *testing.T) {
|
||||
_, _, _, pem1 := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(time.Hour), nil, nil, nil)
|
||||
|
||||
bundle := append(pem1, []byte("some trailing garbage")...)
|
||||
_, err := NewCAPoolFromPEMReader(bytes.NewReader(bundle))
|
||||
assert.ErrorIs(t, err, ErrInvalidPEMBlock)
|
||||
}
|
||||
|
||||
func TestCertificateV1_Verify(t *testing.T) {
|
||||
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||
c, _, _, _ := NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test cert", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||
@@ -228,15 +170,6 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.EqualError(t, err, "certificate is in the block list")
|
||||
|
||||
// Create a copy of the cert and swap to the alternate form for the signature
|
||||
nc := c.Copy()
|
||||
b, err := p256.Swap(c.Signature())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, nc.(*certificateV1).setSignature(b))
|
||||
|
||||
_, err = caPool.VerifyCertificate(time.Now(), nc)
|
||||
require.EqualError(t, err, "certificate is in the block list")
|
||||
|
||||
caPool.ResetCertBlocklist()
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
@@ -254,7 +187,7 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
caPool = NewCAPool()
|
||||
b, err = caPool.AddCAFromPEM(caPem)
|
||||
b, err := caPool.AddCAFromPEM(caPem)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, b)
|
||||
|
||||
@@ -263,17 +196,7 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
|
||||
})
|
||||
|
||||
c, _, _, _ = NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
|
||||
cc, err := caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reset the blocklist and block the alternate form fingerprint
|
||||
caPool.ResetCertBlocklist()
|
||||
caPool.BlocklistFingerprint(cc.fingerprint2)
|
||||
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||
require.EqualError(t, err, "certificate is in the block list")
|
||||
|
||||
caPool.ResetCertBlocklist()
|
||||
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -471,15 +394,6 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.EqualError(t, err, "certificate is in the block list")
|
||||
|
||||
// Create a copy of the cert and swap to the alternate form for the signature
|
||||
nc := c.Copy()
|
||||
b, err := p256.Swap(c.Signature())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, nc.(*certificateV2).setSignature(b))
|
||||
|
||||
_, err = caPool.VerifyCertificate(time.Now(), nc)
|
||||
require.EqualError(t, err, "certificate is in the block list")
|
||||
|
||||
caPool.ResetCertBlocklist()
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
@@ -497,7 +411,7 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
caPool = NewCAPool()
|
||||
b, err = caPool.AddCAFromPEM(caPem)
|
||||
b, err := caPool.AddCAFromPEM(caPem)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, b)
|
||||
|
||||
@@ -506,17 +420,7 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
|
||||
})
|
||||
|
||||
c, _, _, _ = NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
|
||||
cc, err := caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reset the blocklist and block the alternate form fingerprint
|
||||
caPool.ResetCertBlocklist()
|
||||
caPool.BlocklistFingerprint(cc.fingerprint2)
|
||||
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||
require.EqualError(t, err, "certificate is in the block list")
|
||||
|
||||
caPool.ResetCertBlocklist()
|
||||
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -654,31 +558,3 @@ func TestCertificateV2_Verify_Subnets(t *testing.T) {
|
||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCertificateV2_CurveMismatch(t *testing.T) {
|
||||
caIp1 := mustParsePrefixUnmapped("10.0.0.0/16")
|
||||
caIp2 := mustParsePrefixUnmapped("192.168.0.0/24")
|
||||
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_P256, time.Now(), time.Now().Add(10*time.Minute), []netip.Prefix{caIp1, caIp2}, nil, []string{"test"})
|
||||
|
||||
caPem, err := ca.MarshalPEM()
|
||||
require.NoError(t, err)
|
||||
|
||||
caPool := NewCAPool()
|
||||
b, err := caPool.AddCAFromPEM(caPem)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, b)
|
||||
|
||||
// ip is outside the network
|
||||
cIp1 := mustParsePrefixUnmapped("10.0.0.1/24")
|
||||
c, _, _, _ := NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1}, nil, []string{"test"})
|
||||
|
||||
fp, _ := c.Fingerprint()
|
||||
_, err = caPool.verify(c, time.Now(), fp, c.Issuer())
|
||||
require.NoError(t, err)
|
||||
//
|
||||
c2 := c.(*certificateV2)
|
||||
c2.curve = Curve_CURVE25519
|
||||
fp, _ = c.Fingerprint()
|
||||
_, err = caPool.verify(c, time.Now(), fp, c.Issuer())
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert/p256"
|
||||
)
|
||||
|
||||
type Version uint8
|
||||
@@ -112,9 +110,6 @@ type CachedCertificate struct {
|
||||
InvertedGroups map[string]struct{}
|
||||
Fingerprint string
|
||||
signerFingerprint string
|
||||
|
||||
// A place to store a 2nd fingerprint if the certificate could have one, such as with P256
|
||||
fingerprint2 string
|
||||
}
|
||||
|
||||
func (cc *CachedCertificate) String() string {
|
||||
@@ -124,7 +119,6 @@ func (cc *CachedCertificate) String() string {
|
||||
// Recombine will attempt to unmarshal a certificate received in a handshake.
|
||||
// Handshakes save space by placing the peers public key in a different part of the packet, we have to
|
||||
// reassemble the actual certificate structure with that in mind.
|
||||
// Implementations MUST assert the public key is not in the raw certificate bytes if the passed in public key is not empty.
|
||||
func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certificate, error) {
|
||||
if publicKey == nil {
|
||||
return nil, ErrNoPeerStaticKey
|
||||
@@ -157,31 +151,3 @@ func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certific
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// CalculateAlternateFingerprint calculates a 2nd fingerprint representation for P256 certificates
|
||||
// CAPool blocklist testing through `VerifyCertificate` and `VerifyCachedCertificate` automatically performs this step.
|
||||
func CalculateAlternateFingerprint(c Certificate) (string, error) {
|
||||
if c.Curve() != Curve_P256 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
nc := c.Copy()
|
||||
b, err := p256.Swap(nc.Signature())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch v := nc.(type) {
|
||||
case *certificateV1:
|
||||
err = v.setSignature(b)
|
||||
case *certificateV2:
|
||||
err = v.setSignature(b)
|
||||
default:
|
||||
return "", ErrUnknownVersion
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return nc.Fingerprint()
|
||||
}
|
||||
|
||||
+4
-14
@@ -112,9 +112,6 @@ func (c *certificateV1) CheckSignature(key []byte) bool {
|
||||
}
|
||||
switch c.details.curve {
|
||||
case Curve_CURVE25519:
|
||||
if len(key) != ed25519.PublicKeySize {
|
||||
return false //avoids a panic internal to ed25519
|
||||
}
|
||||
return ed25519.Verify(key, b, c.signature)
|
||||
case Curve_P256:
|
||||
pubKey, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), key)
|
||||
@@ -429,7 +426,7 @@ func unmarshalCertificateV1(b []byte, publicKey []byte) (*certificateV1, error)
|
||||
unsafeNetworks: make([]netip.Prefix, len(rc.Details.Subnets)/2),
|
||||
notBefore: time.Unix(rc.Details.NotBefore, 0),
|
||||
notAfter: time.Unix(rc.Details.NotAfter, 0),
|
||||
publicKey: nil,
|
||||
publicKey: make([]byte, len(rc.Details.PublicKey)),
|
||||
isCA: rc.Details.IsCA,
|
||||
curve: rc.Details.Curve,
|
||||
},
|
||||
@@ -440,19 +437,12 @@ func unmarshalCertificateV1(b []byte, publicKey []byte) (*certificateV1, error)
|
||||
copy(nc.details.groups, rc.Details.Groups)
|
||||
nc.details.issuer = hex.EncodeToString(rc.Details.Issuer)
|
||||
|
||||
// If a public key is passed in as an argument, the certificate pubkey must be empty
|
||||
// and the passed-in pubkey copied into the cert.
|
||||
if len(publicKey) > 0 {
|
||||
if len(rc.Details.PublicKey) != 0 {
|
||||
return nil, ErrCertPubkeyPresent
|
||||
}
|
||||
nc.details.publicKey = make([]byte, len(publicKey))
|
||||
copy(nc.details.publicKey, publicKey)
|
||||
} else {
|
||||
nc.details.publicKey = make([]byte, len(rc.Details.PublicKey))
|
||||
copy(nc.details.publicKey, rc.Details.PublicKey)
|
||||
nc.details.publicKey = publicKey
|
||||
}
|
||||
|
||||
copy(nc.details.publicKey, rc.Details.PublicKey)
|
||||
|
||||
var ip netip.Addr
|
||||
for i, rawIp := range rc.Details.Ips {
|
||||
if i%2 == 0 {
|
||||
|
||||
+1
-63
@@ -62,62 +62,6 @@ func TestCertificateV1_Marshal(t *testing.T) {
|
||||
assert.Equal(t, nc.Groups(), nc2.Groups())
|
||||
}
|
||||
|
||||
func TestCertificateV1_Unmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
pubKey := []byte("1234567890abcedfghij1234567890ab")
|
||||
invalidPubkey := []byte("00000000000000000000000000000000")
|
||||
|
||||
nc := certificateV1{
|
||||
details: detailsV1{
|
||||
name: "testing",
|
||||
networks: []netip.Prefix{
|
||||
mustParsePrefixUnmapped("10.1.1.1/24"),
|
||||
mustParsePrefixUnmapped("10.1.1.2/16"),
|
||||
},
|
||||
unsafeNetworks: []netip.Prefix{
|
||||
mustParsePrefixUnmapped("9.1.1.2/24"),
|
||||
mustParsePrefixUnmapped("9.1.1.3/16"),
|
||||
},
|
||||
groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||
notBefore: before,
|
||||
notAfter: after,
|
||||
publicKey: pubKey,
|
||||
isCA: false,
|
||||
issuer: "1234567890abcedfghij1234567890ab",
|
||||
},
|
||||
signature: []byte("1234567890abcedfghij1234567890ab"),
|
||||
}
|
||||
|
||||
// This certificate has a pubkey included
|
||||
certWithPubkey, err := nc.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
// This certificate is missing the pubkey section
|
||||
certWithoutPubkey, err := nc.MarshalForHandshakes()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cert has no pubkey and no pubkey passed in must fail to validate
|
||||
isNil, err := unmarshalCertificateV1(certWithoutPubkey, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// Cert has different pubkey than one passed in must fail
|
||||
isNil, err = unmarshalCertificateV1(certWithPubkey, invalidPubkey)
|
||||
require.Nil(t, isNil)
|
||||
require.Error(t, err)
|
||||
|
||||
// Cert has pubkey and no pubkey argument works ok
|
||||
_, err = unmarshalCertificateV1(certWithPubkey, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cert has no pubkey and valid, correctly signed pubkey passed in
|
||||
nc2, err := unmarshalCertificateV1(certWithoutPubkey, pubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, pubKey, nc2.PublicKey())
|
||||
}
|
||||
|
||||
func TestCertificateV1_PublicKeyPem(t *testing.T) {
|
||||
t.Parallel()
|
||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
@@ -155,19 +99,13 @@ func TestCertificateV1_PublicKeyPem(t *testing.T) {
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA P256 PUBLIC KEY-----
|
||||
`)
|
||||
|
||||
pubP256KeyPemCA := []byte(`-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
`)
|
||||
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
|
||||
require.NoError(t, err)
|
||||
nc.details.curve = Curve_P256
|
||||
nc.details.publicKey = pubP256Key
|
||||
assert.Equal(t, Curve_P256, nc.Curve())
|
||||
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPemCA))
|
||||
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
|
||||
assert.True(t, nc.IsCA())
|
||||
|
||||
nc.details.isCA = false
|
||||
|
||||
+1
-10
@@ -151,9 +151,6 @@ func (c *certificateV2) CheckSignature(key []byte) bool {
|
||||
|
||||
switch c.curve {
|
||||
case Curve_CURVE25519:
|
||||
if len(key) != ed25519.PublicKeySize {
|
||||
return false //avoids a panic internal to ed25519
|
||||
}
|
||||
return ed25519.Verify(key, b, c.signature)
|
||||
case Curve_P256:
|
||||
pubKey, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), key)
|
||||
@@ -595,13 +592,7 @@ func unmarshalCertificateV2(b []byte, publicKey []byte, curve Curve) (*certifica
|
||||
// Maybe grab the public key
|
||||
var rawPublicKey cryptobyte.String
|
||||
if len(publicKey) > 0 {
|
||||
// If a public key is passed in, then the handshake certificate must
|
||||
// not have a public key present
|
||||
if input.PeekASN1Tag(TagCertPublicKey) {
|
||||
return nil, ErrCertPubkeyPresent
|
||||
}
|
||||
rawPublicKey = make(cryptobyte.String, len(publicKey))
|
||||
copy(rawPublicKey, publicKey)
|
||||
rawPublicKey = publicKey
|
||||
} else if !input.ReadOptionalASN1(&rawPublicKey, nil, TagCertPublicKey) {
|
||||
return nil, ErrBadFormat
|
||||
}
|
||||
|
||||
+1
-60
@@ -76,58 +76,6 @@ func TestCertificateV2_Marshal(t *testing.T) {
|
||||
assert.Equal(t, nc.Groups(), nc2.Groups())
|
||||
}
|
||||
|
||||
func TestCertificateV2_Unmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
pubKey := []byte("1234567890abcedfghij1234567890ab")
|
||||
|
||||
nc := certificateV2{
|
||||
details: detailsV2{
|
||||
name: "testing",
|
||||
networks: []netip.Prefix{
|
||||
mustParsePrefixUnmapped("10.1.1.2/16"),
|
||||
mustParsePrefixUnmapped("10.1.1.1/24"),
|
||||
},
|
||||
unsafeNetworks: []netip.Prefix{
|
||||
mustParsePrefixUnmapped("9.1.1.3/16"),
|
||||
mustParsePrefixUnmapped("9.1.1.2/24"),
|
||||
},
|
||||
groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||
notBefore: before,
|
||||
notAfter: after,
|
||||
isCA: false,
|
||||
issuer: "1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
signature: []byte("1234567890abcdef1234567890abcdef"),
|
||||
publicKey: pubKey,
|
||||
}
|
||||
|
||||
db, err := nc.details.Marshal()
|
||||
require.NoError(t, err)
|
||||
nc.rawDetails = db
|
||||
|
||||
certWithPubkey, err := nc.Marshal()
|
||||
require.NoError(t, err)
|
||||
//t.Log("Cert size:", len(b))
|
||||
certWithoutPubkey, err := nc.MarshalForHandshakes()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cert must not have a pubkey if one is passed in as an argument
|
||||
_, err = unmarshalCertificateV2(certWithPubkey, pubKey, Curve_CURVE25519)
|
||||
require.ErrorIs(t, err, ErrCertPubkeyPresent)
|
||||
|
||||
// Certs must have pubkeys
|
||||
_, err = unmarshalCertificateV2(certWithoutPubkey, nil, Curve_CURVE25519)
|
||||
require.ErrorIs(t, err, ErrBadFormat)
|
||||
|
||||
// Ensure proper unmarshal if a pubkey is passed in
|
||||
nc2, err := unmarshalCertificateV2(certWithoutPubkey, pubKey, Curve_CURVE25519)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, nc.PublicKey(), nc2.PublicKey())
|
||||
}
|
||||
|
||||
func TestCertificateV2_PublicKeyPem(t *testing.T) {
|
||||
t.Parallel()
|
||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
@@ -166,19 +114,12 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA P256 PUBLIC KEY-----
|
||||
`)
|
||||
|
||||
pubP256KeyPemCA := []byte(`-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
`)
|
||||
|
||||
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
|
||||
require.NoError(t, err)
|
||||
nc.curve = Curve_P256
|
||||
nc.publicKey = pubP256Key
|
||||
assert.Equal(t, Curve_P256, nc.Curve())
|
||||
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPemCA))
|
||||
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
|
||||
assert.True(t, nc.IsCA())
|
||||
|
||||
nc.details.isCA = false
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ qrlJ69wer3ZUHFXA
|
||||
assert.Nil(t, k)
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// Fail due to ivalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
|
||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||
|
||||
@@ -21,8 +21,6 @@ var (
|
||||
ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
|
||||
ErrCaNotFound = errors.New("could not find ca for the certificate")
|
||||
ErrUnknownVersion = errors.New("certificate version unrecognized")
|
||||
ErrCertPubkeyPresent = errors.New("certificate has unexpected pubkey present")
|
||||
ErrCurveMismatch = errors.New("certificate curve does not match CA")
|
||||
|
||||
ErrInvalidPEMBlock = errors.New("input did not contain a valid PEM encoded block")
|
||||
ErrInvalidPEMCertificateBanner = errors.New("bytes did not contain a proper certificate banner")
|
||||
|
||||
+4
-10
@@ -13,12 +13,6 @@ 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
|
||||
@@ -40,10 +34,10 @@ func NewTestCaCert(version Version, curve Curve, before, after time.Time, networ
|
||||
}
|
||||
|
||||
if before.IsZero() {
|
||||
before = testCertNow.Add(time.Second * -60)
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
}
|
||||
if after.IsZero() {
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
}
|
||||
|
||||
t := &TBSCertificate{
|
||||
@@ -76,11 +70,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 = testCertNow.Add(time.Second * -60)
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
}
|
||||
|
||||
if after.IsZero() {
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
}
|
||||
|
||||
if len(networks) == 0 {
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package p256
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"filippo.io/bigmod"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/cryptobyte/asn1"
|
||||
)
|
||||
|
||||
var halfN = new(big.Int).Rsh(elliptic.P256().Params().N, 1)
|
||||
var nMod *bigmod.Modulus
|
||||
|
||||
func init() {
|
||||
n, err := bigmod.NewModulus(elliptic.P256().Params().N.Bytes())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
nMod = n
|
||||
}
|
||||
|
||||
func IsNormalized(sig []byte) (bool, error) {
|
||||
r, s, err := parseSignature(sig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return checkLowS(r, s), nil
|
||||
}
|
||||
|
||||
func checkLowS(_, s []byte) bool {
|
||||
bigS := new(big.Int).SetBytes(s)
|
||||
// Check if S <= (N/2), because we want to include the midpoint in the set of low-s
|
||||
return bigS.Cmp(halfN) <= 0
|
||||
}
|
||||
|
||||
func swap(r, s []byte) ([]byte, []byte, error) {
|
||||
var err error
|
||||
bigS, err := bigmod.NewNat().SetBytes(s, nMod)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sNormalized := nMod.Nat().Sub(bigS, nMod)
|
||||
|
||||
result := sNormalized.Bytes(nMod)
|
||||
for len(result) > 1 && result[0] == 0 {
|
||||
result = result[1:]
|
||||
}
|
||||
|
||||
return r, result, nil
|
||||
}
|
||||
|
||||
func Normalize(sig []byte) ([]byte, error) {
|
||||
r, s, err := parseSignature(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if checkLowS(r, s) {
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
newR, newS, err := swap(r, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return encodeSignature(newR, newS)
|
||||
}
|
||||
|
||||
// Swap will change sig between its current form to the opposite high or low form.
|
||||
func Swap(sig []byte) ([]byte, error) {
|
||||
r, s, err := parseSignature(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newR, newS, err := swap(r, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return encodeSignature(newR, newS)
|
||||
}
|
||||
|
||||
// parseSignature taken exactly from crypto/ecdsa/ecdsa.go
|
||||
func parseSignature(sig []byte) (r, s []byte, err error) {
|
||||
var inner cryptobyte.String
|
||||
input := cryptobyte.String(sig)
|
||||
if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
|
||||
!input.Empty() ||
|
||||
!inner.ReadASN1Integer(&r) ||
|
||||
!inner.ReadASN1Integer(&s) ||
|
||||
!inner.Empty() {
|
||||
return nil, nil, errors.New("invalid ASN.1")
|
||||
}
|
||||
return r, s, nil
|
||||
}
|
||||
|
||||
func encodeSignature(r, s []byte) ([]byte, error) {
|
||||
var b cryptobyte.Builder
|
||||
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
|
||||
addASN1IntBytes(b, r)
|
||||
addASN1IntBytes(b, s)
|
||||
})
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// addASN1IntBytes encodes in ASN.1 a positive integer represented as
|
||||
// a big-endian byte slice with zero or more leading zeroes.
|
||||
func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) {
|
||||
for len(bytes) > 0 && bytes[0] == 0 {
|
||||
bytes = bytes[1:]
|
||||
}
|
||||
if len(bytes) == 0 {
|
||||
b.SetError(errors.New("invalid integer"))
|
||||
return
|
||||
}
|
||||
b.AddASN1(asn1.INTEGER, func(c *cryptobyte.Builder) {
|
||||
if bytes[0]&0x80 != 0 {
|
||||
c.AddUint8(0)
|
||||
}
|
||||
c.AddBytes(bytes)
|
||||
})
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package p256
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFlipping(t *testing.T) {
|
||||
priv, err1 := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err1)
|
||||
|
||||
out, err := ecdsa.SignASN1(rand.Reader, priv, []byte("big chungus"))
|
||||
require.NoError(t, err)
|
||||
|
||||
r, s, err := parseSignature(out)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, s1, err := swap(r, s)
|
||||
require.NoError(t, err)
|
||||
r, s2, err := swap(r, s1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, s, s2)
|
||||
require.NotEqual(t, s, s1)
|
||||
}
|
||||
+86
-102
@@ -1,66 +1,14 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
var ErrTruncatedPEMBlock = errors.New("truncated PEM block")
|
||||
|
||||
// SplitPEM is a split function for bufio.Scanner that returns each PEM block.
|
||||
func SplitPEM(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
// Look for the start of a PEM block
|
||||
start := bytes.Index(data, []byte("-----BEGIN "))
|
||||
if start == -1 {
|
||||
if atEOF && len(bytes.TrimSpace(data)) > 0 {
|
||||
// Non-whitespace content with no PEM block
|
||||
return 0, nil, ErrTruncatedPEMBlock
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), nil, nil
|
||||
}
|
||||
// Request more data
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Look for the end marker
|
||||
endMarkerStart := bytes.Index(data[start:], []byte("-----END "))
|
||||
if endMarkerStart == -1 {
|
||||
if atEOF {
|
||||
// Incomplete PEM block at EOF
|
||||
return 0, nil, ErrTruncatedPEMBlock
|
||||
}
|
||||
// Need more data to find the end
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Find the actual end of the END line (after the newline)
|
||||
endMarkerStart += start
|
||||
endLineEnd := bytes.IndexByte(data[endMarkerStart:], '\n')
|
||||
var end int
|
||||
if endLineEnd == -1 {
|
||||
if atEOF {
|
||||
// END marker without newline at EOF - take it anyway
|
||||
end = len(data)
|
||||
} else {
|
||||
// Need more data
|
||||
return 0, nil, nil
|
||||
}
|
||||
} else {
|
||||
end = endMarkerStart + endLineEnd + 1
|
||||
}
|
||||
|
||||
// Extract the PEM block
|
||||
pemBlock := data[start:end]
|
||||
|
||||
// Return the valid PEM block
|
||||
return end, pemBlock, nil
|
||||
}
|
||||
|
||||
const ( //cert banners
|
||||
CertificateBanner = "NEBULA CERTIFICATE"
|
||||
CertificateV2Banner = "NEBULA CERTIFICATE V2"
|
||||
@@ -91,7 +39,19 @@ func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) {
|
||||
return nil, r, ErrInvalidPEMBlock
|
||||
}
|
||||
|
||||
c, err := unmarshalCertificateBlock(p)
|
||||
var c Certificate
|
||||
var err error
|
||||
|
||||
switch p.Type {
|
||||
// Implementations must validate the resulting certificate contains valid information
|
||||
case CertificateBanner:
|
||||
c, err = unmarshalCertificateV1(p.Bytes, nil)
|
||||
case CertificateV2Banner:
|
||||
c, err = unmarshalCertificateV2(p.Bytes, nil, Curve_CURVE25519)
|
||||
default:
|
||||
return nil, r, ErrInvalidPEMCertificateBanner
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, r, err
|
||||
}
|
||||
@@ -100,20 +60,6 @@ func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) {
|
||||
|
||||
}
|
||||
|
||||
// unmarshalCertificateBlock decodes a single PEM block into a certificate.
|
||||
// It expects a Nebula certificate banner and returns ErrInvalidPEMCertificateBanner otherwise.
|
||||
func unmarshalCertificateBlock(block *pem.Block) (Certificate, error) {
|
||||
switch block.Type {
|
||||
// Implementations must validate the resulting certificate contains valid information
|
||||
case CertificateBanner:
|
||||
return unmarshalCertificateV1(block.Bytes, nil)
|
||||
case CertificateV2Banner:
|
||||
return unmarshalCertificateV2(block.Bytes, nil, Curve_CURVE25519)
|
||||
default:
|
||||
return nil, ErrInvalidPEMCertificateBanner
|
||||
}
|
||||
}
|
||||
|
||||
func marshalCertPublicKeyToPEM(c Certificate) []byte {
|
||||
if c.IsCA() {
|
||||
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
|
||||
@@ -142,15 +88,12 @@ func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||
case Curve_CURVE25519:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
|
||||
case Curve_P256:
|
||||
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PublicKeyBanner, Bytes: b})
|
||||
return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -159,10 +102,10 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||
var expectedLen int
|
||||
var curve Curve
|
||||
switch k.Type {
|
||||
case X25519PublicKeyBanner:
|
||||
case X25519PublicKeyBanner, Ed25519PublicKeyBanner:
|
||||
expectedLen = 32
|
||||
curve = Curve_CURVE25519
|
||||
case P256PublicKeyBanner:
|
||||
case P256PublicKeyBanner, ECDSAP256PublicKeyBanner:
|
||||
// Uncompressed
|
||||
expectedLen = 65
|
||||
curve = Curve_P256
|
||||
@@ -175,33 +118,6 @@ 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:
|
||||
@@ -275,3 +191,71 @@ func UnmarshalSigningPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error)
|
||||
}
|
||||
return k.Bytes, r, curve, nil
|
||||
}
|
||||
|
||||
// Backward compatibility functions for older API
|
||||
func MarshalX25519PublicKey(b []byte) []byte {
|
||||
return MarshalPublicKeyToPEM(Curve_CURVE25519, b)
|
||||
}
|
||||
|
||||
func MarshalX25519PrivateKey(b []byte) []byte {
|
||||
return MarshalPrivateKeyToPEM(Curve_CURVE25519, b)
|
||||
}
|
||||
|
||||
func MarshalPublicKey(curve Curve, b []byte) []byte {
|
||||
return MarshalPublicKeyToPEM(curve, b)
|
||||
}
|
||||
|
||||
func MarshalPrivateKey(curve Curve, b []byte) []byte {
|
||||
return MarshalPrivateKeyToPEM(curve, b)
|
||||
}
|
||||
|
||||
// NebulaCertificate is a compatibility wrapper for the old API
|
||||
type NebulaCertificate struct {
|
||||
Details NebulaCertificateDetails
|
||||
Signature []byte
|
||||
cert Certificate
|
||||
}
|
||||
|
||||
// NebulaCertificateDetails is a compatibility wrapper for certificate details
|
||||
type NebulaCertificateDetails struct {
|
||||
Name string
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
PublicKey []byte
|
||||
IsCA bool
|
||||
Issuer []byte
|
||||
Curve Curve
|
||||
}
|
||||
|
||||
// UnmarshalNebulaCertificateFromPEM provides backward compatibility with the old API
|
||||
func UnmarshalNebulaCertificateFromPEM(b []byte) (*NebulaCertificate, []byte, error) {
|
||||
c, rest, err := UnmarshalCertificateFromPEM(b)
|
||||
if err != nil {
|
||||
return nil, rest, err
|
||||
}
|
||||
|
||||
// Convert to old format
|
||||
nc := &NebulaCertificate{
|
||||
Details: NebulaCertificateDetails{
|
||||
Name: c.Name(),
|
||||
NotBefore: c.NotBefore(),
|
||||
NotAfter: c.NotAfter(),
|
||||
PublicKey: c.PublicKey(),
|
||||
IsCA: c.IsCA(),
|
||||
Curve: c.Curve(),
|
||||
},
|
||||
Signature: c.Signature(),
|
||||
cert: c,
|
||||
}
|
||||
|
||||
// Handle issuer
|
||||
if c.Issuer() != "" {
|
||||
issuerBytes, err := hex.DecodeString(c.Issuer())
|
||||
if err != nil {
|
||||
return nil, rest, fmt.Errorf("failed to decode issuer fingerprint: %w", err)
|
||||
}
|
||||
nc.Details.Issuer = issuerBytes
|
||||
}
|
||||
|
||||
return nc, rest, nil
|
||||
}
|
||||
|
||||
+71
-167
@@ -1,88 +1,12 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func scanAll(t *testing.T, input string) ([]string, error) {
|
||||
t.Helper()
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
scanner.Split(SplitPEM)
|
||||
var blocks []string
|
||||
for scanner.Scan() {
|
||||
blocks = append(blocks, scanner.Text())
|
||||
}
|
||||
return blocks, scanner.Err()
|
||||
}
|
||||
|
||||
func TestSplitPEM_Single(t *testing.T) {
|
||||
input := "-----BEGIN TEST-----\ndata\n-----END TEST-----\n"
|
||||
blocks, err := scanAll(t, input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 1)
|
||||
require.Equal(t, input, blocks[0])
|
||||
}
|
||||
|
||||
func TestSplitPEM_Multiple(t *testing.T) {
|
||||
block1 := "-----BEGIN TEST-----\naaa\n-----END TEST-----\n"
|
||||
block2 := "-----BEGIN TEST-----\nbbb\n-----END TEST-----\n"
|
||||
blocks, err := scanAll(t, block1+block2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 2)
|
||||
require.Equal(t, block1, blocks[0])
|
||||
require.Equal(t, block2, blocks[1])
|
||||
}
|
||||
|
||||
func TestSplitPEM_CommentsAndWhitespaceBetweenBlocks(t *testing.T) {
|
||||
input := "# comment\n\n-----BEGIN TEST-----\naaa\n-----END TEST-----\n\n# another comment\n\n-----BEGIN TEST-----\nbbb\n-----END TEST-----\n"
|
||||
blocks, err := scanAll(t, input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 2)
|
||||
}
|
||||
|
||||
func TestSplitPEM_Empty(t *testing.T) {
|
||||
blocks, err := scanAll(t, "")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, blocks)
|
||||
}
|
||||
|
||||
func TestSplitPEM_WhitespaceOnly(t *testing.T) {
|
||||
blocks, err := scanAll(t, " \n\t\n ")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, blocks)
|
||||
}
|
||||
|
||||
func TestSplitPEM_TrailingGarbage(t *testing.T) {
|
||||
input := "-----BEGIN TEST-----\ndata\n-----END TEST-----\ngarbage"
|
||||
blocks, err := scanAll(t, input)
|
||||
require.ErrorIs(t, err, ErrTruncatedPEMBlock)
|
||||
require.Len(t, blocks, 1)
|
||||
}
|
||||
|
||||
func TestSplitPEM_TruncatedBlock(t *testing.T) {
|
||||
input := "-----BEGIN TEST-----\npartial data with no end"
|
||||
_, err := scanAll(t, input)
|
||||
require.ErrorIs(t, err, ErrTruncatedPEMBlock)
|
||||
}
|
||||
|
||||
func TestSplitPEM_NoEndNewline(t *testing.T) {
|
||||
input := "-----BEGIN TEST-----\ndata\n-----END TEST-----"
|
||||
blocks, err := scanAll(t, input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 1)
|
||||
require.Equal(t, input, blocks[0])
|
||||
}
|
||||
|
||||
func TestSplitPEM_GarbageOnly(t *testing.T) {
|
||||
_, err := scanAll(t, "this is not PEM data")
|
||||
require.ErrorIs(t, err, ErrTruncatedPEMBlock)
|
||||
}
|
||||
|
||||
func TestUnmarshalCertificateFromPEM(t *testing.T) {
|
||||
goodCert := []byte(`
|
||||
# A good cert
|
||||
@@ -120,7 +44,7 @@ bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "bytes did not contain a proper certificate banner")
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// Fail due to ivalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
cert, rest, err = UnmarshalCertificateFromPEM(rest)
|
||||
assert.Nil(t, cert)
|
||||
@@ -182,7 +106,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA private key banner")
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// Fail due to ivalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
@@ -244,7 +168,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
assert.Equal(t, rest, invalidPem)
|
||||
require.EqualError(t, err, "bytes did not contain a proper private key banner")
|
||||
|
||||
// Fail due to invalid PEM format, because
|
||||
// Fail due to ivalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
@@ -255,6 +179,60 @@ 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 ivalid 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-----
|
||||
@@ -265,7 +243,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-----END NEBULA P256 PUBLIC KEY-----
|
||||
`)
|
||||
signingKey := []byte(`# A signing key has the wrong scope for this function
|
||||
oldPubP256Key := []byte(`# A good key
|
||||
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||
@@ -286,118 +264,44 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
-END NEBULA X25519 PUBLIC KEY-----`)
|
||||
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, signingKey, shortKey, invalidBanner, invalidPem)
|
||||
keyBundle := appendByteSlices(pubKey, pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem)
|
||||
|
||||
// X25519 key
|
||||
// Success test case
|
||||
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||
assert.Len(t, k, 32)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, signingKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(pubP256Key, oldPubP256Key, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_CURVE25519, curve)
|
||||
|
||||
// P256 key
|
||||
// Success test case
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(signingKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(oldPubP256Key, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, Curve_P256, curve)
|
||||
|
||||
// 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))
|
||||
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||
|
||||
// Fail due to short key
|
||||
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, _, 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, _, 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)
|
||||
// Success test case
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Len(t, k, 65)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rest, appendByteSlices(ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||
assert.Equal(t, rest, appendByteSlices(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)
|
||||
k, rest, curve, 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, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||
assert.Nil(t, k)
|
||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||
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
|
||||
// Fail due to ivalid PEM format, because
|
||||
// it's missing the requisite pre-encapsulation boundary.
|
||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||
k, rest, curve, 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")
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert/p256"
|
||||
)
|
||||
|
||||
// TBSCertificate represents a certificate intended to be signed.
|
||||
@@ -128,13 +126,6 @@ func (t *TBSCertificate) SignWith(signer Certificate, curve Curve, sp SignerLamb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if curve == Curve_P256 {
|
||||
sig, err = p256.Normalize(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = c.setSignature(sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert/p256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -90,48 +89,3 @@ func TestCertificateV1_SignP256(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, uc)
|
||||
}
|
||||
|
||||
func TestCertificate_SignP256_AlwaysNormalized(t *testing.T) {
|
||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
pubKey := []byte("01234567890abcedfghij1234567890ab1234567890abcedfghij1234567890ab")
|
||||
|
||||
tbs := TBSCertificate{
|
||||
Version: Version1,
|
||||
Name: "testing",
|
||||
Networks: []netip.Prefix{
|
||||
mustParsePrefixUnmapped("10.1.1.1/24"),
|
||||
mustParsePrefixUnmapped("10.1.1.2/16"),
|
||||
},
|
||||
UnsafeNetworks: []netip.Prefix{
|
||||
mustParsePrefixUnmapped("9.1.1.2/24"),
|
||||
mustParsePrefixUnmapped("9.1.1.3/16"),
|
||||
},
|
||||
Groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||
NotBefore: before,
|
||||
NotAfter: after,
|
||||
PublicKey: pubKey,
|
||||
IsCA: true,
|
||||
Curve: Curve_P256,
|
||||
}
|
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y)
|
||||
rawPriv := priv.D.FillBytes(make([]byte, 32))
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
if i&1 == 1 {
|
||||
tbs.Version = Version1
|
||||
} else {
|
||||
tbs.Version = Version2
|
||||
}
|
||||
c, err := tbs.Sign(nil, Curve_P256, rawPriv)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, c)
|
||||
assert.True(t, c.CheckSignature(pub))
|
||||
normie, err := p256.IsNormalized(c.Signature())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, normie)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-89
@@ -14,12 +14,6 @@ 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
|
||||
@@ -41,10 +35,10 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
|
||||
}
|
||||
|
||||
if before.IsZero() {
|
||||
before = testCertNow.Add(time.Second * -60)
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
}
|
||||
if after.IsZero() {
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
}
|
||||
|
||||
t := &cert.TBSCertificate{
|
||||
@@ -77,11 +71,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 = testCertNow.Add(time.Second * -60)
|
||||
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||
}
|
||||
|
||||
if after.IsZero() {
|
||||
after = testCertNow.Add(time.Second * 60)
|
||||
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||
}
|
||||
|
||||
var pub, priv []byte
|
||||
@@ -120,33 +114,6 @@ func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []by
|
||||
return c, pub, cert.MarshalPrivateKeyToPEM(curve, priv), pem
|
||||
}
|
||||
|
||||
func NewTestCertDifferentVersion(c cert.Certificate, v cert.Version, ca cert.Certificate, key []byte) (cert.Certificate, []byte) {
|
||||
nc := &cert.TBSCertificate{
|
||||
Version: v,
|
||||
Curve: c.Curve(),
|
||||
Name: c.Name(),
|
||||
Networks: c.Networks(),
|
||||
UnsafeNetworks: c.UnsafeNetworks(),
|
||||
Groups: c.Groups(),
|
||||
NotBefore: time.Unix(c.NotBefore().Unix(), 0),
|
||||
NotAfter: time.Unix(c.NotAfter().Unix(), 0),
|
||||
PublicKey: c.PublicKey(),
|
||||
IsCA: false,
|
||||
}
|
||||
|
||||
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pem, err := c.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return c, pem
|
||||
}
|
||||
|
||||
func X25519Keypair() ([]byte, []byte) {
|
||||
privkey := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {
|
||||
@@ -169,55 +136,3 @@ func P256Keypair() ([]byte, []byte) {
|
||||
pubkey := privkey.PublicKey()
|
||||
return pubkey.Bytes(), privkey.Bytes()
|
||||
}
|
||||
|
||||
// DummyCert is a minimal cert.Certificate implementation for testing error paths.
|
||||
type DummyCert struct {
|
||||
Version_ cert.Version
|
||||
Curve_ cert.Curve
|
||||
Groups_ []string
|
||||
IsCA_ bool
|
||||
Issuer_ string
|
||||
Name_ string
|
||||
Networks_ []netip.Prefix
|
||||
NotAfter_ time.Time
|
||||
NotBefore_ time.Time
|
||||
PublicKey_ []byte
|
||||
Signature_ []byte
|
||||
UnsafeNetworks_ []netip.Prefix
|
||||
}
|
||||
|
||||
func (d *DummyCert) Version() cert.Version { return d.Version_ }
|
||||
func (d *DummyCert) Curve() cert.Curve { return d.Curve_ }
|
||||
func (d *DummyCert) Groups() []string { return d.Groups_ }
|
||||
func (d *DummyCert) IsCA() bool { return d.IsCA_ }
|
||||
func (d *DummyCert) Issuer() string { return d.Issuer_ }
|
||||
func (d *DummyCert) Name() string { return d.Name_ }
|
||||
func (d *DummyCert) Networks() []netip.Prefix { return d.Networks_ }
|
||||
func (d *DummyCert) NotAfter() time.Time { return d.NotAfter_ }
|
||||
func (d *DummyCert) NotBefore() time.Time { return d.NotBefore_ }
|
||||
func (d *DummyCert) PublicKey() []byte { return d.PublicKey_ }
|
||||
func (d *DummyCert) Signature() []byte { return d.Signature_ }
|
||||
func (d *DummyCert) UnsafeNetworks() []netip.Prefix { return d.UnsafeNetworks_ }
|
||||
func (d *DummyCert) Fingerprint() (string, error) { return "", nil }
|
||||
func (d *DummyCert) CheckSignature(key []byte) bool { return false }
|
||||
func (d *DummyCert) MarshalForHandshakes() ([]byte, error) { return nil, nil }
|
||||
func (d *DummyCert) MarshalPEM() ([]byte, error) { return nil, nil }
|
||||
func (d *DummyCert) MarshalJSON() ([]byte, error) { return nil, nil }
|
||||
func (d *DummyCert) Marshal() ([]byte, error) { return nil, nil }
|
||||
func (d *DummyCert) String() string { return "dummy" }
|
||||
func (d *DummyCert) Copy() cert.Certificate { return d }
|
||||
func (d *DummyCert) VerifyPrivateKey(c cert.Curve, k []byte) error { return nil }
|
||||
func (d *DummyCert) Expired(time.Time) bool { return false }
|
||||
func (d *DummyCert) MarshalPublicKeyPEM() []byte { return nil }
|
||||
func (d *DummyCert) PublicKeyPEM() []byte { return nil }
|
||||
|
||||
// NewTestCAPool creates a CAPool from the given CA certificates, panicking on error.
|
||||
func NewTestCAPool(cas ...cert.Certificate) *cert.CAPool {
|
||||
pool := cert.NewCAPool()
|
||||
for _, ca := range cas {
|
||||
if err := pool.AddCA(ca); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
+22
-50
@@ -97,19 +97,6 @@ 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
|
||||
@@ -184,37 +171,25 @@ 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"))
|
||||
for i := 0; i < 5; i++ {
|
||||
out.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if err == ErrNoTerminal {
|
||||
return fmt.Errorf("out-key must be encrypted interactively")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error reading passphrase: %s", err)
|
||||
}
|
||||
|
||||
if len(passphrase) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
for i := 0; i < 5; i++ {
|
||||
errOut.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if err == ErrNoTerminal {
|
||||
return fmt.Errorf("out-key must be encrypted interactively")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error reading passphrase: %s", err)
|
||||
}
|
||||
|
||||
if len(passphrase) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext")
|
||||
}
|
||||
return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,16 +258,14 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
Curve: curve,
|
||||
}
|
||||
|
||||
if !isP11 && !isStdio(*cf.outKeyPath) {
|
||||
if !isP11 {
|
||||
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
if !isStdio(*cf.outCertPath) {
|
||||
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *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
|
||||
@@ -318,7 +291,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
|
||||
}
|
||||
|
||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||
err = os.WriteFile(*cf.outKeyPath, b, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
@@ -329,7 +302,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
||||
return fmt.Errorf("error while marshalling certificate: %s", err)
|
||||
}
|
||||
|
||||
err = writeOutput(*cf.outCertPath, b, 0600, out)
|
||||
err = os.WriteFile(*cf.outCertPath, b, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
||||
}
|
||||
@@ -340,7 +313,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 = writeOutput(*cf.outQRPath, b, 0600, out)
|
||||
err = os.WriteFile(*cf.outQRPath, b, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||
}
|
||||
@@ -356,7 +329,6 @@ 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,7 +27,6 @@ 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"+
|
||||
@@ -85,7 +84,7 @@ func Test_ca(t *testing.T) {
|
||||
err: nil,
|
||||
}
|
||||
|
||||
pwPromptEB := "Enter passphrase: "
|
||||
pwPromptOb := "Enter passphrase: "
|
||||
|
||||
// required args
|
||||
assertHelpError(t, ca(
|
||||
@@ -169,19 +168,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.Empty(t, ob.String())
|
||||
assert.Equal(t, pwPromptEB, eb.String())
|
||||
|
||||
// test encrypted key with passphrase environment variable
|
||||
os.Remove(keyF.Name())
|
||||
os.Remove(crtF.Name())
|
||||
ob.Reset()
|
||||
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()}
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase))
|
||||
require.NoError(t, ca(args, ob, eb, testpw))
|
||||
assert.Equal(t, pwPromptOb, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
||||
|
||||
// read encrypted key file and verify default params
|
||||
rb, _ = os.ReadFile(keyF.Name())
|
||||
@@ -201,15 +189,15 @@ func Test_ca(t *testing.T) {
|
||||
assert.Empty(t, b)
|
||||
assert.Len(t, lKey, 64)
|
||||
|
||||
// test when reading password results in an error
|
||||
// test when reading passsword results in an error
|
||||
os.Remove(keyF.Name())
|
||||
os.Remove(crtF.Name())
|
||||
ob.Reset()
|
||||
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.Empty(t, ob.String())
|
||||
assert.Equal(t, pwPromptEB, eb.String())
|
||||
assert.Equal(t, pwPromptOb, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// test when user fails to enter a password
|
||||
os.Remove(keyF.Name())
|
||||
@@ -218,8 +206,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.Empty(t, ob.String())
|
||||
assert.Equal(t, strings.Repeat(pwPromptEB, 5), eb.String()) // prompts 5 times before giving up
|
||||
assert.Equal(t, strings.Repeat(pwPromptOb, 5), ob.String()) // prompts 5 times before giving up
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// create valid cert/key for overwrite tests
|
||||
os.Remove(keyF.Name())
|
||||
@@ -248,67 +236,3 @@ 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,8 +42,6 @@ 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
|
||||
@@ -71,14 +69,6 @@ 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 {
|
||||
@@ -92,12 +82,12 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while getting public key: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = writeOutput(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
}
|
||||
err = writeOutput(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600, out)
|
||||
err = os.WriteFile(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-pub: %s", err)
|
||||
}
|
||||
@@ -112,7 +102,6 @@ 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,7 +20,6 @@ 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"+
|
||||
@@ -94,43 +93,3 @@ 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())
|
||||
}
|
||||
|
||||
@@ -5,28 +5,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A version string that can be set with
|
||||
//
|
||||
// -ldflags "-X main.Build=SOMEVERSION"
|
||||
//
|
||||
// at compile-time.
|
||||
var Build string
|
||||
|
||||
func init() {
|
||||
if Build == "" {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
Build = strings.TrimPrefix(info.Main.Version, "v")
|
||||
}
|
||||
}
|
||||
|
||||
type helpError struct {
|
||||
s string
|
||||
}
|
||||
|
||||
@@ -22,9 +22,7 @@ func (pr StdinPasswordReader) ReadPassword() ([]byte, error) {
|
||||
}
|
||||
|
||||
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
// 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)
|
||||
fmt.Println()
|
||||
|
||||
return password, err
|
||||
}
|
||||
|
||||
@@ -40,23 +40,11 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
rawCert, err := os.ReadFile(*pf.path)
|
||||
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
|
||||
@@ -69,13 +57,11 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while unmarshaling cert: %s", err)
|
||||
}
|
||||
|
||||
if !qrToStdout {
|
||||
if *pf.json {
|
||||
jsonCerts = append(jsonCerts, c)
|
||||
} else {
|
||||
_, _ = out.Write([]byte(c.String()))
|
||||
_, _ = out.Write([]byte("\n"))
|
||||
}
|
||||
if *pf.json {
|
||||
jsonCerts = append(jsonCerts, c)
|
||||
} else {
|
||||
_, _ = out.Write([]byte(c.String()))
|
||||
_, _ = out.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
if *pf.outQRPath != "" {
|
||||
@@ -93,7 +79,7 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
part++
|
||||
}
|
||||
|
||||
if *pf.json && !qrToStdout {
|
||||
if *pf.json {
|
||||
b, _ := json.Marshal(jsonCerts)
|
||||
_, _ = out.Write(b)
|
||||
_, _ = out.Write([]byte("\n"))
|
||||
@@ -105,7 +91,7 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return fmt.Errorf("error while generating qr code: %s", err)
|
||||
}
|
||||
|
||||
err = writeOutput(*pf.outQRPath, b, 0600, out)
|
||||
err = os.WriteFile(*pf.outQRPath, b, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||
}
|
||||
@@ -121,7 +107,6 @@ 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,7 +25,6 @@ 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"+
|
||||
@@ -179,44 +178,6 @@ 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
|
||||
|
||||
+47
-75
@@ -43,7 +43,7 @@ type signFlags struct {
|
||||
func newSignFlags() *signFlags {
|
||||
sf := signFlags{set: flag.NewFlagSet("sign", flag.ContinueOnError)}
|
||||
sf.set.Usage = func() {}
|
||||
sf.version = sf.set.Uint("version", 0, "Optional: version of the certificate format to use. The default is to match the version of the signing CA")
|
||||
sf.version = sf.set.Uint("version", 0, "Optional: version of the certificate format to use, the default is to create both v1 and v2 certificates.")
|
||||
sf.caKeyPath = sf.set.String("ca-key", "ca.key", "Optional: path to the signing CA key")
|
||||
sf.caCertPath = sf.set.String("ca-crt", "ca.crt", "Optional: path to the signing CA cert")
|
||||
sf.name = sf.set.String("name", "", "Required: name of the cert, usually a hostname")
|
||||
@@ -85,9 +85,6 @@ 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
|
||||
@@ -105,35 +102,13 @@ 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 = readInput("ca-key", *sf.caKeyPath, &claims)
|
||||
rawCAKey, err := os.ReadFile(*sf.caKeyPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca-key: %s", err)
|
||||
}
|
||||
@@ -141,28 +116,26 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
// naively attempt to decode the private key as though it is not encrypted
|
||||
caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
|
||||
if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
|
||||
// ask for a passphrase until we get one
|
||||
var passphrase []byte
|
||||
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
||||
if len(passphrase) == 0 {
|
||||
// ask for a passphrase until we get one
|
||||
for i := 0; i < 5; i++ {
|
||||
errOut.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
for i := 0; i < 5; i++ {
|
||||
out.Write([]byte("Enter passphrase: "))
|
||||
passphrase, err = pr.ReadPassword()
|
||||
|
||||
if errors.Is(err, ErrNoTerminal) {
|
||||
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error reading password: %s", err)
|
||||
}
|
||||
|
||||
if len(passphrase) > 0 {
|
||||
break
|
||||
}
|
||||
if errors.Is(err, ErrNoTerminal) {
|
||||
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error reading password: %s", err)
|
||||
}
|
||||
if len(passphrase) == 0 {
|
||||
return fmt.Errorf("cannot open encrypted ca-key without passphrase")
|
||||
|
||||
if len(passphrase) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(passphrase) == 0 {
|
||||
return fmt.Errorf("cannot open encrypted ca-key without passphrase")
|
||||
}
|
||||
|
||||
curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing encrypted ca-key: %s", err)
|
||||
@@ -172,7 +145,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
}
|
||||
|
||||
rawCACert, err := readInput("ca-crt", *sf.caCertPath, &claims)
|
||||
rawCACert, err := os.ReadFile(*sf.caCertPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca-crt: %s", err)
|
||||
}
|
||||
@@ -192,10 +165,6 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return fmt.Errorf("ca certificate is expired")
|
||||
}
|
||||
|
||||
if version == 0 {
|
||||
version = caCert.Version()
|
||||
}
|
||||
|
||||
// if no duration is given, expire one second before the root expires
|
||||
if *sf.duration <= 0 {
|
||||
*sf.duration = time.Until(caCert.NotAfter()) - time.Second*1
|
||||
@@ -270,7 +239,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
|
||||
if *sf.inPubPath != "" {
|
||||
var pubCurve cert.Curve
|
||||
rawPub, err := readInput("in-pub", *sf.inPubPath, &claims)
|
||||
rawPub, err := os.ReadFile(*sf.inPubPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading in-pub: %s", err)
|
||||
}
|
||||
@@ -291,10 +260,16 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
pub, rawPriv = newKeypair(curve)
|
||||
}
|
||||
|
||||
if !isStdio(*sf.outCertPath) {
|
||||
if _, err := os.Stat(*sf.outCertPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
var crts []cert.Certificate
|
||||
@@ -302,19 +277,21 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(*sf.duration)
|
||||
|
||||
switch version {
|
||||
case cert.Version1:
|
||||
// Make sure we have only one ipv4 address
|
||||
if version == 0 || version == cert.Version1 {
|
||||
// Make sure we at least have an ip
|
||||
if len(v4Networks) != 1 {
|
||||
return newHelpErrorf("invalid -networks definition: v1 certificates can only have a single ipv4 address")
|
||||
}
|
||||
|
||||
if len(v6Networks) > 0 {
|
||||
return newHelpErrorf("invalid -networks definition: v1 certificates can only contain ipv4 addresses")
|
||||
}
|
||||
if version == cert.Version1 {
|
||||
// If we are asked to mint a v1 certificate only then we cant just ignore any v6 addresses
|
||||
if len(v6Networks) > 0 {
|
||||
return newHelpErrorf("invalid -networks definition: v1 certificates can only be ipv4")
|
||||
}
|
||||
|
||||
if len(v6UnsafeNetworks) > 0 {
|
||||
return newHelpErrorf("invalid -unsafe-networks definition: v1 certificates can only contain ipv4 addresses")
|
||||
if len(v6UnsafeNetworks) > 0 {
|
||||
return newHelpErrorf("invalid -unsafe-networks definition: v1 certificates can only be ipv4")
|
||||
}
|
||||
}
|
||||
|
||||
t := &cert.TBSCertificate{
|
||||
@@ -344,8 +321,9 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
|
||||
crts = append(crts, nc)
|
||||
}
|
||||
|
||||
case cert.Version2:
|
||||
if version == 0 || version == cert.Version2 {
|
||||
t := &cert.TBSCertificate{
|
||||
Version: cert.Version2,
|
||||
Name: *sf.name,
|
||||
@@ -373,19 +351,14 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
}
|
||||
|
||||
crts = append(crts, nc)
|
||||
default:
|
||||
// this should be unreachable
|
||||
return fmt.Errorf("invalid version: %d", version)
|
||||
}
|
||||
|
||||
if !isP11 && *sf.inPubPath == "" {
|
||||
if !isStdio(*sf.outKeyPath) {
|
||||
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
||||
}
|
||||
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
||||
}
|
||||
|
||||
err = writeOutput(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||
err = os.WriteFile(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-key: %s", err)
|
||||
}
|
||||
@@ -400,7 +373,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
b = append(b, sb...)
|
||||
}
|
||||
|
||||
err = writeOutput(*sf.outCertPath, b, 0600, out)
|
||||
err = os.WriteFile(*sf.outCertPath, b, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
||||
}
|
||||
@@ -411,7 +384,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
||||
return fmt.Errorf("error while generating qr code: %s", err)
|
||||
}
|
||||
|
||||
err = writeOutput(*sf.outQRPath, b, 0600, out)
|
||||
err = os.WriteFile(*sf.outQRPath, b, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||
}
|
||||
@@ -461,7 +434,6 @@ 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,7 +27,6 @@ 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"+
|
||||
@@ -56,7 +55,7 @@ func Test_signHelp(t *testing.T) {
|
||||
" -unsafe-networks string\n"+
|
||||
" \tOptional: comma separated list of ip address and network in CIDR notation. Unsafe networks this cert can route for\n"+
|
||||
" -version uint\n"+
|
||||
" \tOptional: version of the certificate format to use. The default is to match the version of the signing CA\n",
|
||||
" \tOptional: version of the certificate format to use, the default is to create both v1 and v2 certificates.\n",
|
||||
ob.String(),
|
||||
)
|
||||
}
|
||||
@@ -205,7 +204,7 @@ func Test_signCert(t *testing.T) {
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "100::100/100"}
|
||||
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -unsafe-networks definition: v1 certificates can only contain ipv4 addresses")
|
||||
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -unsafe-networks definition: v1 certificates can only be ipv4")
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
@@ -377,20 +376,8 @@ 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.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.Equal(t, "Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
||||
|
||||
// test with the wrong password
|
||||
ob.Reset()
|
||||
@@ -399,19 +386,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.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||
|
||||
// test with the wrong password in environment
|
||||
ob.Reset()
|
||||
eb.Reset()
|
||||
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", "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.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing encrypted ca-key: invalid passphrase or corrupt private key")
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
||||
|
||||
// test with the user not entering a password
|
||||
ob.Reset()
|
||||
@@ -420,8 +396,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.Empty(t, ob.String())
|
||||
assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", eb.String())
|
||||
assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
|
||||
// test an error condition
|
||||
ob.Reset()
|
||||
@@ -429,106 +405,6 @@ 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.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())
|
||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
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"))
|
||||
}
|
||||
+13
-15
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
@@ -39,26 +40,24 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var claims ioClaims
|
||||
if err := reserveInputs(&claims,
|
||||
"ca", *vf.caPath,
|
||||
"crt", *vf.certPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caReader, err := openInput("ca", *vf.caPath, &claims)
|
||||
rawCACert, err := os.ReadFile(*vf.caPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading ca: %w", err)
|
||||
}
|
||||
defer caReader.Close()
|
||||
|
||||
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)
|
||||
caPool := cert.NewCAPool()
|
||||
for {
|
||||
rawCACert, err = caPool.AddCAFromPEM(rawCACert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while adding ca cert to pool: %w", err)
|
||||
}
|
||||
|
||||
if rawCACert == nil || len(rawCACert) == 0 || strings.TrimSpace(string(rawCACert)) == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rawCert, err := readInput("crt", *vf.certPath, &claims)
|
||||
rawCert, err := os.ReadFile(*vf.certPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read crt: %w", err)
|
||||
}
|
||||
@@ -93,7 +92,6 @@ 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,7 +23,6 @@ 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"+
|
||||
@@ -65,7 +64,7 @@ func Test_verify(t *testing.T) {
|
||||
err = verify([]string{"-ca", caFile.Name(), "-crt", "does_not_exist"}, ob, eb)
|
||||
assert.Empty(t, ob.String())
|
||||
assert.Empty(t, eb.String())
|
||||
require.ErrorIs(t, err, cert.ErrInvalidPEMBlock)
|
||||
require.EqualError(t, err, "error while adding ca cert to pool: input did not contain a valid PEM encoded block")
|
||||
|
||||
// make a ca for later
|
||||
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
@@ -123,46 +122,3 @@ 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`)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,8 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/slackhq/nebula/logging"
|
||||
)
|
||||
|
||||
// newPlatformLogger returns a *slog.Logger that writes to stdout. Non-Windows
|
||||
// platforms have no special sink to integrate with.
|
||||
func newPlatformLogger() *slog.Logger {
|
||||
return logging.NewLogger(os.Stdout)
|
||||
func HookLogger(l *logrus.Logger) {
|
||||
// Do nothing, let the logs flow to stdout/stderr
|
||||
}
|
||||
|
||||
@@ -1,86 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/kardianos/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// newPlatformLogger returns a *slog.Logger that routes every log record
|
||||
// through the Windows service logger so records end up in the Windows
|
||||
// Event Log. All the heavy lifting (level management, format swap,
|
||||
// timestamp toggle, WithAttrs/WithGroup) comes from logging.NewHandler;
|
||||
// this file only contributes:
|
||||
//
|
||||
// - an io.Writer that forwards each formatted line to the service
|
||||
// logger at the current record's Event Log severity, and
|
||||
// - a thin severityTag that embeds *logging.Handler and overrides
|
||||
// only Handle / WithAttrs / WithGroup, so Event Viewer's severity
|
||||
// column and severity-based filters keep working the way they did
|
||||
// before the slog migration.
|
||||
//
|
||||
// Format (text vs json) is carried by the embedded *logging.Handler, so
|
||||
// logging.format: json in config still produces JSON lines in Event
|
||||
// Viewer, same as the pre-slog logrus setup.
|
||||
func newPlatformLogger() *slog.Logger {
|
||||
w := &eventLogWriter{}
|
||||
return slog.New(&severityTag{Handler: logging.NewHandler(w), w: w})
|
||||
// HookLogger routes the logrus logs through the service logger so that they end up in the Windows Event Viewer
|
||||
// logrus output will be discarded
|
||||
func HookLogger(l *logrus.Logger) {
|
||||
l.AddHook(newLogHook(logger))
|
||||
l.SetOutput(ioutil.Discard)
|
||||
}
|
||||
|
||||
// eventLogWriter forwards slog-formatted lines to the Windows service
|
||||
// logger at the severity most recently stashed by severityTag.Handle.
|
||||
// The mutex serializes the stash + inner.Handle + Write cycle per record
|
||||
// across all concurrent goroutines; slog's builtin text/json handlers
|
||||
// each hold their own mutex around Write, but that only protects the
|
||||
// Write call itself, not our stash-then-handle sequence.
|
||||
type eventLogWriter struct {
|
||||
mu sync.Mutex
|
||||
level slog.Level
|
||||
type logHook struct {
|
||||
sl service.Logger
|
||||
}
|
||||
|
||||
func (w *eventLogWriter) Write(p []byte) (int, error) {
|
||||
line := strings.TrimRight(string(p), "\n")
|
||||
switch {
|
||||
case w.level >= slog.LevelError:
|
||||
return len(p), logger.Error(line)
|
||||
case w.level >= slog.LevelWarn:
|
||||
return len(p), logger.Warning(line)
|
||||
func newLogHook(sl service.Logger) *logHook {
|
||||
return &logHook{sl: sl}
|
||||
}
|
||||
|
||||
func (h *logHook) Fire(entry *logrus.Entry) error {
|
||||
line, err := entry.String()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
switch entry.Level {
|
||||
case logrus.PanicLevel:
|
||||
return h.sl.Error(line)
|
||||
case logrus.FatalLevel:
|
||||
return h.sl.Error(line)
|
||||
case logrus.ErrorLevel:
|
||||
return h.sl.Error(line)
|
||||
case logrus.WarnLevel:
|
||||
return h.sl.Warning(line)
|
||||
case logrus.InfoLevel:
|
||||
return h.sl.Info(line)
|
||||
case logrus.DebugLevel:
|
||||
return h.sl.Info(line)
|
||||
default:
|
||||
return len(p), logger.Info(line)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// severityTag embeds *logging.Handler to pick up everything it does for
|
||||
// free (Enabled, SetLevel, GetLevel, SetFormat, GetFormat,
|
||||
// SetDisableTimestamp) and overrides only Handle / WithAttrs / WithGroup
|
||||
// so each record's slog.Level is stashed on the writer before formatting
|
||||
// and so derived handlers stay wrapped as severityTag rather than
|
||||
// downgrading to bare *logging.Handler.
|
||||
type severityTag struct {
|
||||
*logging.Handler
|
||||
w *eventLogWriter
|
||||
}
|
||||
|
||||
func (s *severityTag) Handle(ctx context.Context, r slog.Record) error {
|
||||
s.w.mu.Lock()
|
||||
defer s.w.mu.Unlock()
|
||||
s.w.level = r.Level
|
||||
return s.Handler.Handle(ctx, r)
|
||||
}
|
||||
|
||||
func (s *severityTag) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
if len(attrs) == 0 {
|
||||
return s
|
||||
}
|
||||
return &severityTag{Handler: s.Handler.WithAttrs(attrs).(*logging.Handler), w: s.w}
|
||||
}
|
||||
|
||||
func (s *severityTag) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return s
|
||||
}
|
||||
return &severityTag{Handler: s.Handler.WithGroup(name).(*logging.Handler), w: s.w}
|
||||
func (h *logHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
+11
-55
@@ -4,12 +4,10 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/slackhq/nebula/util"
|
||||
)
|
||||
|
||||
@@ -20,17 +18,6 @@ import (
|
||||
// at compile-time.
|
||||
var Build string
|
||||
|
||||
func init() {
|
||||
if Build == "" {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
Build = strings.TrimPrefix(info.Main.Version, "v")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
serviceFlag := flag.String("service", "", "Control the system service.")
|
||||
configPath := flag.String("config", "", "Path to either a file or directory to load configuration from")
|
||||
@@ -50,30 +37,20 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
l := logging.NewLogger(os.Stdout)
|
||||
|
||||
if *serviceFlag != "" {
|
||||
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)
|
||||
}
|
||||
return
|
||||
doService(configPath, configTest, Build, serviceFlag)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *configPath == "" {
|
||||
p, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
*configPath = p
|
||||
fmt.Println("-config flag must be set")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
l := logrus.New()
|
||||
l.Out = os.Stdout
|
||||
|
||||
c := config.NewC(l)
|
||||
err := c.Load(*configPath)
|
||||
if err != nil {
|
||||
@@ -81,16 +58,6 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := logging.ApplyConfig(l, c); err != nil {
|
||||
fmt.Printf("failed to apply logging config: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
c.RegisterReloadCallback(func(c *config.C) {
|
||||
if err := logging.ApplyConfig(l, c); err != nil {
|
||||
l.Error("Failed to reconfigure logger on reload", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
ctrl, err := nebula.Main(c, *configTest, Build, l, nil)
|
||||
if err != nil {
|
||||
util.LogWithContextIfNeeded("Failed to start", err, l)
|
||||
@@ -98,19 +65,8 @@ func main() {
|
||||
}
|
||||
|
||||
if !*configTest {
|
||||
if err := ctrl.Start(); err != nil {
|
||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go ctrl.ShutdownBlock()
|
||||
|
||||
if err := ctrl.Wait(); err != nil {
|
||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
l.Info("Goodbye")
|
||||
ctrl.Start()
|
||||
ctrl.ShutdownBlock()
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
|
||||
@@ -4,17 +4,19 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
)
|
||||
|
||||
var logger service.Logger
|
||||
|
||||
type program struct {
|
||||
configPath *string
|
||||
configTest *bool
|
||||
build string
|
||||
control *nebula.Control
|
||||
}
|
||||
@@ -23,7 +25,8 @@ func (p *program) Start(s service.Service) error {
|
||||
// Start should not block.
|
||||
logger.Info("Nebula service starting.")
|
||||
|
||||
l := newPlatformLogger()
|
||||
l := logrus.New()
|
||||
HookLogger(l)
|
||||
|
||||
c := config.NewC(l)
|
||||
err := c.Load(*p.configPath)
|
||||
@@ -31,56 +34,39 @@ func (p *program) Start(s service.Service) error {
|
||||
return fmt.Errorf("failed to load config: %s", err)
|
||||
}
|
||||
|
||||
if err := logging.ApplyConfig(l, c); err != nil {
|
||||
return fmt.Errorf("failed to apply logging config: %s", err)
|
||||
}
|
||||
c.RegisterReloadCallback(func(c *config.C) {
|
||||
if err := logging.ApplyConfig(l, c); err != nil {
|
||||
l.Error("Failed to reconfigure logger on reload", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
p.control, err = nebula.Main(c, false, Build, l, nil)
|
||||
p.control, err = nebula.Main(c, *p.configTest, Build, l, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
p.control.Start()
|
||||
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 doService(configPath *string, build string, serviceFlag *string) error {
|
||||
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) {
|
||||
if *configPath == "" {
|
||||
p, err := config.DefaultPath()
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
*configPath = filepath.Dir(ex) + "/config.yaml"
|
||||
if !fileExists(*configPath) {
|
||||
*configPath = filepath.Dir(ex) + "/config.yml"
|
||||
}
|
||||
*configPath = p
|
||||
}
|
||||
|
||||
svcConfig := &service.Config{
|
||||
@@ -92,22 +78,23 @@ func doService(configPath *string, build string, serviceFlag *string) error {
|
||||
|
||||
prg := &program{
|
||||
configPath: configPath,
|
||||
configTest: configTest,
|
||||
build: build,
|
||||
}
|
||||
|
||||
// Here are what the different loggers are doing:
|
||||
// - `log` is the standard go log utility, meant to be used while the process is still attached to stdout/stderr
|
||||
// - `logger` is the service log utility that may be attached to a special place depending on OS (Windows will have it attached to the event log)
|
||||
// - in program.Start we build a *slog.Logger via newPlatformLogger; on non-Windows that is a stdout-backed slog logger, on Windows it routes records through the service logger
|
||||
// - above, in `Run` we create a `logrus.Logger` which is what nebula expects to use
|
||||
s, err := service.New(prg, svcConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
errs := make(chan error, 5)
|
||||
logger, err = s.Logger(errs)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -122,17 +109,18 @@ func doService(configPath *string, build string, serviceFlag *string) error {
|
||||
|
||||
switch *serviceFlag {
|
||||
case "run":
|
||||
if err := s.Run(); err != nil {
|
||||
// Route any errors to the system logger and report the failure
|
||||
err = s.Run()
|
||||
if err != nil {
|
||||
// Route any errors to the system logger
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := service.Control(s, *serviceFlag); err != nil {
|
||||
err := service.Control(s, *serviceFlag)
|
||||
if err != nil {
|
||||
log.Printf("Valid actions: %q\n", service.ControlAction)
|
||||
return err
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
//go:build linux && !android && !e2e_testing
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
cert_test "github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestControlStopClosesOnTimer reproduces the dnclient lifecycle: nebula runs as
|
||||
// a library, and on a config update dnclient calls Stop() in-process to tear the
|
||||
// old instance down before starting a new one. This boots a real nebula (real
|
||||
// blocking UDP sockets, tun disabled), lets it run, then Stop()s it on a timer
|
||||
// and asserts it actually closes. If the reader goroutines parked in recvmmsg
|
||||
// don't wake on Close(), Wait() blocks forever and this fails with a goroutine
|
||||
// dump instead of relying on a process signal to unstick them.
|
||||
func TestControlStopClosesOnTimer(t *testing.T) {
|
||||
l := test.NewLogger()
|
||||
dir := t.TempDir()
|
||||
|
||||
before := time.Now().Add(-time.Hour)
|
||||
after := time.Now().Add(time.Hour)
|
||||
ca, _, caKey, caPEM := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, before, after, nil, nil, nil)
|
||||
networks := []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")}
|
||||
_, _, keyPEM, certPEM := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, "close-on-timer", before, after, networks, nil, nil)
|
||||
|
||||
caPath := filepath.Join(dir, "ca.pem")
|
||||
certPath := filepath.Join(dir, "cert.pem")
|
||||
keyPath := filepath.Join(dir, "key.pem")
|
||||
require.NoError(t, os.WriteFile(caPath, caPEM, 0o600))
|
||||
require.NoError(t, os.WriteFile(certPath, certPEM, 0o600))
|
||||
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600))
|
||||
|
||||
// tun disabled so no device/root is needed; routines: 2 so we exercise the
|
||||
// multi-socket (SO_REUSEPORT) teardown, which is where dnclient runs.
|
||||
configBody := fmt.Sprintf(`
|
||||
pki:
|
||||
ca: %s
|
||||
cert: %s
|
||||
key: %s
|
||||
listen:
|
||||
host: 127.0.0.1
|
||||
port: 0
|
||||
tun:
|
||||
disabled: true
|
||||
firewall:
|
||||
outbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
inbound:
|
||||
- port: any
|
||||
proto: any
|
||||
host: any
|
||||
routines: 2
|
||||
`, caPath, certPath, keyPath)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "config.yml"), []byte(configBody), 0o600))
|
||||
|
||||
c := config.NewC(l)
|
||||
require.NoError(t, c.Load(dir))
|
||||
|
||||
ctrl, err := nebula.Main(c, false, "close-on-timer", l, nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ctrl.Start())
|
||||
|
||||
// Run like a live nebula, then close on a timer, exactly as dnclient does.
|
||||
<-time.NewTimer(5 * time.Second).C
|
||||
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
ctrl.Stop() // closes the udp sockets (shutdown(2)) and the tun
|
||||
ctrl.Wait() // blocks until every reader goroutine has returned
|
||||
close(stopped)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stopped:
|
||||
t.Log("nebula closed cleanly on timer")
|
||||
case <-time.After(10 * time.Second):
|
||||
buf := make([]byte, 1<<20)
|
||||
n := runtime.Stack(buf, true)
|
||||
t.Fatalf("nebula did NOT close within 10s of Stop(): a blocking reader never woke\n%s", buf[:n])
|
||||
}
|
||||
}
|
||||
+8
-44
@@ -4,12 +4,10 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/slackhq/nebula/util"
|
||||
)
|
||||
|
||||
@@ -20,17 +18,6 @@ import (
|
||||
// at compile-time.
|
||||
var Build string
|
||||
|
||||
func init() {
|
||||
if Build == "" {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
Build = strings.TrimPrefix(info.Main.Version, "v")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "", "Path to either a file or directory to load configuration from")
|
||||
configTest := flag.Bool("test", false, "Test the config and print the end result. Non zero exit indicates a faulty config")
|
||||
@@ -50,15 +37,13 @@ func main() {
|
||||
}
|
||||
|
||||
if *configPath == "" {
|
||||
p, err := config.DefaultPath()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
*configPath = p
|
||||
fmt.Println("-config flag must be set")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
l := logging.NewLogger(os.Stdout)
|
||||
l := logrus.New()
|
||||
l.Out = os.Stdout
|
||||
|
||||
c := config.NewC(l)
|
||||
err := c.Load(*configPath)
|
||||
@@ -67,16 +52,6 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := logging.ApplyConfig(l, c); err != nil {
|
||||
fmt.Printf("failed to apply logging config: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
c.RegisterReloadCallback(func(c *config.C) {
|
||||
if err := logging.ApplyConfig(l, c); err != nil {
|
||||
l.Error("Failed to reconfigure logger on reload", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
ctrl, err := nebula.Main(c, *configTest, Build, l, nil)
|
||||
if err != nil {
|
||||
util.LogWithContextIfNeeded("Failed to start", err, l)
|
||||
@@ -84,20 +59,9 @@ func main() {
|
||||
}
|
||||
|
||||
if !*configTest {
|
||||
if err := ctrl.Start(); err != nil {
|
||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go ctrl.ShutdownBlock()
|
||||
ctrl.Start()
|
||||
notifyReady(l)
|
||||
|
||||
if err := ctrl.Wait(); err != nil {
|
||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
l.Info("Goodbye")
|
||||
ctrl.ShutdownBlock()
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SdNotifyReady tells systemd the service is ready and dependent services can now be started
|
||||
@@ -12,30 +13,30 @@ import (
|
||||
// https://www.freedesktop.org/software/systemd/man/systemd.service.html
|
||||
const SdNotifyReady = "READY=1"
|
||||
|
||||
func notifyReady(l *slog.Logger) {
|
||||
func notifyReady(l *logrus.Logger) {
|
||||
sockName := os.Getenv("NOTIFY_SOCKET")
|
||||
if sockName == "" {
|
||||
l.Debug("NOTIFY_SOCKET systemd env var not set, not sending ready signal")
|
||||
l.Debugln("NOTIFY_SOCKET systemd env var not set, not sending ready signal")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("unixgram", sockName, time.Second)
|
||||
if err != nil {
|
||||
l.Error("failed to connect to systemd notification socket", "error", err)
|
||||
l.WithError(err).Error("failed to connect to systemd notification socket")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
if err != nil {
|
||||
l.Error("failed to set the write deadline for the systemd notification socket", "error", err)
|
||||
l.WithError(err).Error("failed to set the write deadline for the systemd notification socket")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = conn.Write([]byte(SdNotifyReady)); err != nil {
|
||||
l.Error("failed to signal the systemd notification socket", "error", err)
|
||||
l.WithError(err).Error("failed to signal the systemd notification socket")
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug("notified systemd the service is ready")
|
||||
l.Debugln("notified systemd the service is ready")
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
package main
|
||||
|
||||
import "log/slog"
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
func notifyReady(_ *slog.Logger) {
|
||||
func notifyReady(_ *logrus.Logger) {
|
||||
// No init service to notify
|
||||
}
|
||||
|
||||
+7
-16
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -17,7 +16,8 @@ import (
|
||||
"time"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type C struct {
|
||||
@@ -26,11 +26,11 @@ type C struct {
|
||||
Settings map[string]any
|
||||
oldSettings map[string]any
|
||||
callbacks []func(*C)
|
||||
l *slog.Logger
|
||||
l *logrus.Logger
|
||||
reloadLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewC(l *slog.Logger) *C {
|
||||
func NewC(l *logrus.Logger) *C {
|
||||
return &C{
|
||||
Settings: make(map[string]any),
|
||||
l: l,
|
||||
@@ -107,18 +107,12 @@ func (c *C) HasChanged(k string) bool {
|
||||
|
||||
newVals, err := yaml.Marshal(nv)
|
||||
if err != nil {
|
||||
c.l.Error("Error while marshaling new config",
|
||||
"config_path", k,
|
||||
"error", err,
|
||||
)
|
||||
c.l.WithField("config_path", k).WithError(err).Error("Error while marshaling new config")
|
||||
}
|
||||
|
||||
oldVals, err := yaml.Marshal(ov)
|
||||
if err != nil {
|
||||
c.l.Error("Error while marshaling old config",
|
||||
"config_path", k,
|
||||
"error", err,
|
||||
)
|
||||
c.l.WithField("config_path", k).WithError(err).Error("Error while marshaling old config")
|
||||
}
|
||||
|
||||
return string(newVals) != string(oldVals)
|
||||
@@ -160,10 +154,7 @@ func (c *C) ReloadConfig() {
|
||||
|
||||
err := c.Load(c.path)
|
||||
if err != nil {
|
||||
c.l.Error("Error occurred while reloading config",
|
||||
"config_path", c.path,
|
||||
"error", err,
|
||||
)
|
||||
c.l.WithField("config_path", c.path).WithError(err).Error("Error occurred while reloading config")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestConfig_Load(t *testing.T) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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))
|
||||
}
|
||||
+109
-115
@@ -5,12 +5,13 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/header"
|
||||
@@ -44,16 +45,19 @@ type connectionManager struct {
|
||||
inactivityTimeout atomic.Int64
|
||||
dropInactive atomic.Bool
|
||||
|
||||
l *slog.Logger
|
||||
metricsTxPunchy metrics.Counter
|
||||
|
||||
l *logrus.Logger
|
||||
}
|
||||
|
||||
func newConnectionManagerFromConfig(l *slog.Logger, c *config.C, hm *HostMap, p *Punchy) *connectionManager {
|
||||
func newConnectionManagerFromConfig(l *logrus.Logger, c *config.C, hm *HostMap, p *Punchy) *connectionManager {
|
||||
cm := &connectionManager{
|
||||
hostMap: hm,
|
||||
l: l,
|
||||
punchy: p,
|
||||
relayUsed: make(map[uint32]struct{}),
|
||||
relayUsedLock: &sync.RWMutex{},
|
||||
hostMap: hm,
|
||||
l: l,
|
||||
punchy: p,
|
||||
relayUsed: make(map[uint32]struct{}),
|
||||
relayUsedLock: &sync.RWMutex{},
|
||||
metricsTxPunchy: metrics.GetOrRegisterCounter("messages.tx.punchy", nil),
|
||||
}
|
||||
|
||||
cm.reload(c, true)
|
||||
@@ -81,10 +85,9 @@ func (cm *connectionManager) reload(c *config.C, initial bool) {
|
||||
old := cm.getInactivityTimeout()
|
||||
cm.inactivityTimeout.Store((int64)(c.GetDuration("tunnels.inactivity_timeout", 10*time.Minute)))
|
||||
if !initial {
|
||||
cm.l.Info("Inactivity timeout has changed",
|
||||
"oldDuration", old,
|
||||
"newDuration", cm.getInactivityTimeout(),
|
||||
)
|
||||
cm.l.WithField("oldDuration", old).
|
||||
WithField("newDuration", cm.getInactivityTimeout()).
|
||||
Info("Inactivity timeout has changed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +95,9 @@ func (cm *connectionManager) reload(c *config.C, initial bool) {
|
||||
old := cm.dropInactive.Load()
|
||||
cm.dropInactive.Store(c.GetBool("tunnels.drop_inactive", false))
|
||||
if !initial {
|
||||
cm.l.Info("Drop inactive setting has changed",
|
||||
"oldBool", old,
|
||||
"newBool", cm.dropInactive.Load(),
|
||||
)
|
||||
cm.l.WithField("oldBool", old).
|
||||
WithField("newBool", cm.dropInactive.Load()).
|
||||
Info("Drop inactive setting has changed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,6 +138,14 @@ 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()
|
||||
@@ -246,7 +256,7 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
||||
var err error
|
||||
index, err = AddRelay(cm.l, newhostinfo, cm.hostMap, r.PeerAddr, nil, r.Type, Requested)
|
||||
if err != nil {
|
||||
cm.l.Error("failed to migrate relay to new hostinfo", "error", err)
|
||||
cm.l.WithError(err).Error("failed to migrate relay to new hostinfo")
|
||||
continue
|
||||
}
|
||||
switch r.Type {
|
||||
@@ -294,16 +304,16 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
||||
|
||||
msg, err := req.Marshal()
|
||||
if err != nil {
|
||||
cm.l.Error("failed to marshal Control message to migrate relay", "error", err)
|
||||
cm.l.WithError(err).Error("failed to marshal Control message to migrate relay")
|
||||
} else {
|
||||
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||
cm.l.Info("send CreateRelayRequest",
|
||||
"relayFrom", relayFrom,
|
||||
"relayTo", relayTo,
|
||||
"initiatorRelayIndex", req.InitiatorRelayIndex,
|
||||
"responderRelayIndex", req.ResponderRelayIndex,
|
||||
"vpnAddrs", newhostinfo.vpnAddrs,
|
||||
)
|
||||
cm.l.WithFields(logrus.Fields{
|
||||
"relayFrom": req.RelayFromAddr,
|
||||
"relayTo": req.RelayToAddr,
|
||||
"initiatorRelayIndex": req.InitiatorRelayIndex,
|
||||
"responderRelayIndex": req.ResponderRelayIndex,
|
||||
"vpnAddrs": newhostinfo.vpnAddrs}).
|
||||
Info("send CreateRelayRequest")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +325,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
||||
|
||||
hostinfo := cm.hostMap.Indexes[localIndex]
|
||||
if hostinfo == nil {
|
||||
cm.l.Debug("Not found in hostmap", "localIndex", localIndex)
|
||||
cm.l.WithField("localIndex", localIndex).Debugln("Not found in hostmap")
|
||||
return doNothing, nil, nil
|
||||
}
|
||||
|
||||
@@ -335,15 +345,16 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
||||
// A hostinfo is determined alive if there is incoming traffic
|
||||
if inTraffic {
|
||||
decision := doNothing
|
||||
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(cm.l).Debug("Tunnel status",
|
||||
"tunnelCheck", m{"state": "alive", "method": "passive"},
|
||||
)
|
||||
if cm.l.Level >= logrus.DebugLevel {
|
||||
hostinfo.logger(cm.l).
|
||||
WithField("tunnelCheck", m{"state": "alive", "method": "passive"}).
|
||||
Debug("Tunnel status")
|
||||
}
|
||||
hostinfo.pendingDeletion.Store(false)
|
||||
|
||||
if mainHostInfo {
|
||||
decision = tryRehandshake
|
||||
|
||||
} else {
|
||||
if cm.shouldSwapPrimary(hostinfo) {
|
||||
decision = swapPrimary
|
||||
@@ -357,7 +368,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
||||
|
||||
if !outTraffic {
|
||||
// Send a punch packet to keep the NAT state alive
|
||||
cm.punchy.SendPunch(hostinfo)
|
||||
cm.sendPunch(hostinfo)
|
||||
}
|
||||
|
||||
return decision, hostinfo, primary
|
||||
@@ -365,9 +376,9 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
||||
|
||||
if hostinfo.pendingDeletion.Load() {
|
||||
// We have already sent a test packet and nothing was returned, this hostinfo is dead
|
||||
hostinfo.logger(cm.l).Info("Tunnel status",
|
||||
"tunnelCheck", m{"state": "dead", "method": "active"},
|
||||
)
|
||||
hostinfo.logger(cm.l).
|
||||
WithField("tunnelCheck", m{"state": "dead", "method": "active"}).
|
||||
Info("Tunnel status")
|
||||
|
||||
return deleteTunnel, hostinfo, nil
|
||||
}
|
||||
@@ -378,39 +389,40 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
||||
inactiveFor, isInactive := cm.isInactive(hostinfo, now)
|
||||
if isInactive {
|
||||
// Tunnel is inactive, tear it down
|
||||
hostinfo.logger(cm.l).Info("Dropping tunnel due to inactivity",
|
||||
"inactiveDuration", inactiveFor,
|
||||
"primary", mainHostInfo,
|
||||
)
|
||||
hostinfo.logger(cm.l).
|
||||
WithField("inactiveDuration", inactiveFor).
|
||||
WithField("primary", mainHostInfo).
|
||||
Info("Dropping tunnel due to inactivity")
|
||||
|
||||
return closeTunnel, hostinfo, primary
|
||||
}
|
||||
|
||||
// If we aren't sending or receiving traffic then its an unused tunnel and we don't to test the tunnel.
|
||||
// Just maintain NAT state if configured to do so.
|
||||
cm.punchy.SendPunch(hostinfo)
|
||||
cm.sendPunch(hostinfo)
|
||||
cm.trafficTimer.Add(hostinfo.localIndexId, cm.checkInterval)
|
||||
return doNothing, nil, nil
|
||||
}
|
||||
|
||||
// We aren't receiving traffic but we are sending it. The outbound
|
||||
// traffic itself refreshes the primary remote's NAT state; this
|
||||
// fans out to non-primary remotes, but only if target_all_remotes
|
||||
// is configured.
|
||||
cm.punchy.SendPunchToAll(hostinfo)
|
||||
if cm.punchy.GetTargetEverything() {
|
||||
// This is similar to the old punchy behavior with a slight optimization.
|
||||
// We aren't receiving traffic but we are sending it, punch on all known
|
||||
// ips in case we need to re-prime NAT state
|
||||
cm.sendPunch(hostinfo)
|
||||
}
|
||||
|
||||
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(cm.l).Debug("Tunnel status",
|
||||
"tunnelCheck", m{"state": "testing", "method": "active"},
|
||||
)
|
||||
if cm.l.Level >= logrus.DebugLevel {
|
||||
hostinfo.logger(cm.l).
|
||||
WithField("tunnelCheck", m{"state": "testing", "method": "active"}).
|
||||
Debug("Tunnel status")
|
||||
}
|
||||
|
||||
// Send a test packet to trigger an authenticated tunnel test, this should suss out any lingering tunnel issues
|
||||
decision = sendTestPacket
|
||||
|
||||
} else {
|
||||
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
hostinfo.logger(cm.l).Debug("Hostinfo sadness")
|
||||
if cm.l.Level >= logrus.DebugLevel {
|
||||
hostinfo.logger(cm.l).Debugf("Hostinfo sadness")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,10 +461,6 @@ func (cm *connectionManager) shouldSwapPrimary(current *HostInfo) bool {
|
||||
}
|
||||
|
||||
crt := cm.intf.pki.getCertState().getCertificate(current.ConnectionState.myCert.Version())
|
||||
if crt == nil {
|
||||
//my cert was reloaded away. We should definitely swap from this tunnel
|
||||
return true
|
||||
}
|
||||
// If this tunnel is using the latest certificate then we should swap it to primary for a bit and see if things
|
||||
// settle down.
|
||||
return bytes.Equal(current.ConnectionState.myCert.Signature(), crt.Signature())
|
||||
@@ -467,84 +475,70 @@ func (cm *connectionManager) swapPrimary(current, primary *HostInfo) {
|
||||
cm.hostMap.Unlock()
|
||||
}
|
||||
|
||||
// isInvalidCertificate decides if we should destroy a tunnel.
|
||||
// returns true if pki.disconnect_invalid is true and the certificate is no longer valid.
|
||||
// Blocklisted certificates will skip the pki.disconnect_invalid check and return true.
|
||||
// isInvalidCertificate will check if we should destroy a tunnel if pki.disconnect_invalid is true and
|
||||
// the certificate is no longer valid. Block listed certificates will skip the pki.disconnect_invalid
|
||||
// check and return true.
|
||||
func (cm *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostInfo) bool {
|
||||
remoteCert := hostinfo.GetCert()
|
||||
if remoteCert == nil {
|
||||
return false //don't tear down tunnels for handshakes in progress
|
||||
return false
|
||||
}
|
||||
|
||||
caPool := cm.intf.pki.GetCAPool()
|
||||
err := caPool.VerifyCachedCertificate(now, remoteCert)
|
||||
if err == nil {
|
||||
return false //cert is still valid! yay!
|
||||
} else if err == cert.ErrBlockListed { //avoiding errors.Is for speed
|
||||
// Block listed certificates should always be disconnected
|
||||
hostinfo.logger(cm.l).Info("Remote certificate is blocked, tearing down the tunnel",
|
||||
"error", err,
|
||||
"fingerprint", remoteCert.Fingerprint,
|
||||
)
|
||||
return true
|
||||
} else if cm.intf.disconnectInvalid.Load() {
|
||||
hostinfo.logger(cm.l).Info("Remote certificate is no longer valid, tearing down the tunnel",
|
||||
"error", err,
|
||||
"fingerprint", remoteCert.Fingerprint,
|
||||
)
|
||||
return true
|
||||
} else {
|
||||
//if we reach here, the cert is no longer valid, but we're configured to keep tunnels from now-invalid certs open
|
||||
return false
|
||||
}
|
||||
|
||||
if !cm.intf.disconnectInvalid.Load() && err != cert.ErrBlockListed {
|
||||
// Block listed certificates should always be disconnected
|
||||
return false
|
||||
}
|
||||
|
||||
hostinfo.logger(cm.l).WithError(err).
|
||||
WithField("fingerprint", remoteCert.Fingerprint).
|
||||
Info("Remote certificate is no longer valid, tearing down the tunnel")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (cm *connectionManager) sendPunch(hostinfo *HostInfo) {
|
||||
if !cm.punchy.GetPunch() {
|
||||
// Punching is disabled
|
||||
return
|
||||
}
|
||||
|
||||
if cm.intf.lightHouse.IsAnyLighthouseAddr(hostinfo.vpnAddrs) {
|
||||
// Do not punch to lighthouses, we assume our lighthouse update interval is good enough.
|
||||
// In the event the update interval is not sufficient to maintain NAT state then a publicly available lighthouse
|
||||
// would lose the ability to notify us and punchy.respond would become unreliable.
|
||||
return
|
||||
}
|
||||
|
||||
if cm.punchy.GetTargetEverything() {
|
||||
hostinfo.remotes.ForEach(cm.hostMap.GetPreferredRanges(), func(addr netip.AddrPort, preferred bool) {
|
||||
cm.metricsTxPunchy.Inc(1)
|
||||
cm.intf.outside.WriteTo([]byte{1}, addr)
|
||||
})
|
||||
|
||||
} else if hostinfo.remote.IsValid() {
|
||||
cm.metricsTxPunchy.Inc(1)
|
||||
cm.intf.outside.WriteTo([]byte{1}, hostinfo.remote)
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
|
||||
cs := cm.intf.pki.getCertState()
|
||||
curCrt := hostinfo.ConnectionState.myCert
|
||||
curCrtVersion := curCrt.Version()
|
||||
myCrt := cs.getCertificate(curCrtVersion)
|
||||
if myCrt == nil {
|
||||
cm.l.Info("Re-handshaking with remote",
|
||||
"vpnAddrs", hostinfo.vpnAddrs,
|
||||
"version", curCrtVersion,
|
||||
"reason", "local certificate removed",
|
||||
)
|
||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||
myCrt := cs.getCertificate(curCrt.Version())
|
||||
if curCrt.Version() >= cs.initiatingVersion && bytes.Equal(curCrt.Signature(), myCrt.Signature()) == true {
|
||||
// The current tunnel is using the latest certificate and version, no need to rehandshake.
|
||||
return
|
||||
}
|
||||
peerCrt := hostinfo.ConnectionState.peerCert
|
||||
if peerCrt != nil && curCrtVersion < peerCrt.Certificate.Version() {
|
||||
// if our certificate version is less than theirs, and we have a matching version available, rehandshake?
|
||||
if cs.getCertificate(peerCrt.Certificate.Version()) != nil {
|
||||
cm.l.Info("Re-handshaking with remote",
|
||||
"vpnAddrs", hostinfo.vpnAddrs,
|
||||
"version", curCrtVersion,
|
||||
"peerVersion", peerCrt.Certificate.Version(),
|
||||
"reason", "local certificate version lower than peer, attempting to correct",
|
||||
)
|
||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], func(hh *HandshakeHostInfo) {
|
||||
hh.initiatingVersionOverride = peerCrt.Certificate.Version()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(curCrt.Signature(), myCrt.Signature()) {
|
||||
cm.l.Info("Re-handshaking with remote",
|
||||
"vpnAddrs", hostinfo.vpnAddrs,
|
||||
"reason", "local certificate is not current",
|
||||
)
|
||||
|
||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||
return
|
||||
}
|
||||
if curCrtVersion < cs.initiatingVersion {
|
||||
cm.l.Info("Re-handshaking with remote",
|
||||
"vpnAddrs", hostinfo.vpnAddrs,
|
||||
"reason", "current cert version < pki.initiatingVersion",
|
||||
)
|
||||
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs).
|
||||
WithField("reason", "local certificate is not current").
|
||||
Info("Re-handshaking with remote")
|
||||
|
||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||
return
|
||||
}
|
||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||
}
|
||||
|
||||
+27
-24
@@ -7,9 +7,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/flynn/noise"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/overlay/overlaytest"
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -25,7 +25,6 @@ func newTestLighthouse() *LightHouse {
|
||||
lighthouses := []netip.Addr{}
|
||||
staticList := map[netip.Addr]struct{}{}
|
||||
|
||||
lh.localAddrsFn = func(*LocalAllowList) []netip.Addr { return nil }
|
||||
lh.lighthouses.Store(&lighthouses)
|
||||
lh.staticList.Store(&staticList)
|
||||
|
||||
@@ -47,13 +46,13 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
||||
initiatingVersion: cert.Version1,
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{version: cert.Version1},
|
||||
v1Credential: nil,
|
||||
v1HandshakeBytes: []byte{},
|
||||
}
|
||||
|
||||
lh := newTestLighthouse()
|
||||
ifce := &Interface{
|
||||
hostMap: hostMap,
|
||||
inside: &overlaytest.NoopTun{},
|
||||
inside: &test.NoopTun{},
|
||||
outside: &udp.NoopConn{},
|
||||
firewall: &Firewall{},
|
||||
lightHouse: lh,
|
||||
@@ -64,9 +63,9 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
||||
ifce.pki.cs.Store(cs)
|
||||
|
||||
// Create manager
|
||||
conf := config.NewC(test.NewLogger())
|
||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||
conf := config.NewC(l)
|
||||
punchy := NewPunchyFromConfig(l, conf)
|
||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||
nc.intf = ifce
|
||||
p := []byte("")
|
||||
nb := make([]byte, 12, 12)
|
||||
@@ -80,6 +79,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
||||
}
|
||||
hostinfo.ConnectionState = &ConnectionState{
|
||||
myCert: &dummyCert{version: cert.Version1},
|
||||
H: &noise.HandshakeState{},
|
||||
}
|
||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||
|
||||
@@ -129,13 +129,13 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
||||
initiatingVersion: cert.Version1,
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{version: cert.Version1},
|
||||
v1Credential: nil,
|
||||
v1HandshakeBytes: []byte{},
|
||||
}
|
||||
|
||||
lh := newTestLighthouse()
|
||||
ifce := &Interface{
|
||||
hostMap: hostMap,
|
||||
inside: &overlaytest.NoopTun{},
|
||||
inside: &test.NoopTun{},
|
||||
outside: &udp.NoopConn{},
|
||||
firewall: &Firewall{},
|
||||
lightHouse: lh,
|
||||
@@ -146,9 +146,9 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
||||
ifce.pki.cs.Store(cs)
|
||||
|
||||
// Create manager
|
||||
conf := config.NewC(test.NewLogger())
|
||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||
conf := config.NewC(l)
|
||||
punchy := NewPunchyFromConfig(l, conf)
|
||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||
nc.intf = ifce
|
||||
p := []byte("")
|
||||
nb := make([]byte, 12, 12)
|
||||
@@ -162,6 +162,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
||||
}
|
||||
hostinfo.ConnectionState = &ConnectionState{
|
||||
myCert: &dummyCert{version: cert.Version1},
|
||||
H: &noise.HandshakeState{},
|
||||
}
|
||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||
|
||||
@@ -213,13 +214,13 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
||||
initiatingVersion: cert.Version1,
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{version: cert.Version1},
|
||||
v1Credential: nil,
|
||||
v1HandshakeBytes: []byte{},
|
||||
}
|
||||
|
||||
lh := newTestLighthouse()
|
||||
ifce := &Interface{
|
||||
hostMap: hostMap,
|
||||
inside: &overlaytest.NoopTun{},
|
||||
inside: &test.NoopTun{},
|
||||
outside: &udp.NoopConn{},
|
||||
firewall: &Firewall{},
|
||||
lightHouse: lh,
|
||||
@@ -230,12 +231,12 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
||||
ifce.pki.cs.Store(cs)
|
||||
|
||||
// Create manager
|
||||
conf := config.NewC(test.NewLogger())
|
||||
conf := config.NewC(l)
|
||||
conf.Settings["tunnels"] = map[string]any{
|
||||
"drop_inactive": true,
|
||||
}
|
||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||
punchy := NewPunchyFromConfig(l, conf)
|
||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||
assert.True(t, nc.dropInactive.Load())
|
||||
nc.intf = ifce
|
||||
|
||||
@@ -247,6 +248,7 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
||||
}
|
||||
hostinfo.ConnectionState = &ConnectionState{
|
||||
myCert: &dummyCert{version: cert.Version1},
|
||||
H: &noise.HandshakeState{},
|
||||
}
|
||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||
|
||||
@@ -337,15 +339,15 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
||||
cachedPeerCert, err := ncp.VerifyCertificate(now.Add(time.Second), peerCert)
|
||||
|
||||
cs := &CertState{
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{},
|
||||
v1Credential: nil,
|
||||
privateKey: []byte{},
|
||||
v1Cert: &dummyCert{},
|
||||
v1HandshakeBytes: []byte{},
|
||||
}
|
||||
|
||||
lh := newTestLighthouse()
|
||||
ifce := &Interface{
|
||||
hostMap: hostMap,
|
||||
inside: &overlaytest.NoopTun{},
|
||||
inside: &test.NoopTun{},
|
||||
outside: &udp.NoopConn{},
|
||||
firewall: &Firewall{},
|
||||
lightHouse: lh,
|
||||
@@ -358,9 +360,9 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
||||
ifce.disconnectInvalid.Store(true)
|
||||
|
||||
// Create manager
|
||||
conf := config.NewC(test.NewLogger())
|
||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||
conf := config.NewC(l)
|
||||
punchy := NewPunchyFromConfig(l, conf)
|
||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||
nc.intf = ifce
|
||||
ifce.connectionManager = nc
|
||||
|
||||
@@ -369,6 +371,7 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
||||
ConnectionState: &ConnectionState{
|
||||
myCert: &dummyCert{},
|
||||
peerCert: cachedPeerCert,
|
||||
H: &noise.HandshakeState{},
|
||||
},
|
||||
}
|
||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||
|
||||
+59
-71
@@ -1,49 +1,86 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/flynn/noise"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/handshake"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/noiseutil"
|
||||
)
|
||||
|
||||
const ReplayWindow = 1024
|
||||
|
||||
type ConnectionState struct {
|
||||
eKey noiseutil.CipherState
|
||||
dKey noiseutil.CipherState
|
||||
eKey *NebulaCipherState
|
||||
dKey *NebulaCipherState
|
||||
H *noise.HandshakeState
|
||||
myCert cert.Certificate
|
||||
peerCert *cert.CachedCertificate
|
||||
initiator bool
|
||||
messageCounter atomic.Uint64
|
||||
window *Bits
|
||||
decryptLock sync.Mutex
|
||||
writeLock sync.Mutex
|
||||
}
|
||||
|
||||
// newConnectionStateFromResult builds a fully-populated ConnectionState from a
|
||||
// completed handshake.Result. It seeds messageCounter and the replay window so
|
||||
// that the post-handshake message indices already used on the wire don't count
|
||||
// as missed traffic in the data plane.
|
||||
func newConnectionStateFromResult(r *handshake.Result) *ConnectionState {
|
||||
func NewConnectionState(l *logrus.Logger, cs *CertState, crt cert.Certificate, initiator bool, pattern noise.HandshakePattern) (*ConnectionState, error) {
|
||||
var dhFunc noise.DHFunc
|
||||
switch crt.Curve() {
|
||||
case cert.Curve_CURVE25519:
|
||||
dhFunc = noise.DH25519
|
||||
case cert.Curve_P256:
|
||||
if cs.pkcs11Backed {
|
||||
dhFunc = noiseutil.DHP256PKCS11
|
||||
} else {
|
||||
dhFunc = noiseutil.DHP256
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid curve: %s", crt.Curve())
|
||||
}
|
||||
|
||||
var ncs noise.CipherSuite
|
||||
if cs.cipher == "chachapoly" {
|
||||
ncs = noise.NewCipherSuite(dhFunc, noise.CipherChaChaPoly, noise.HashSHA256)
|
||||
} else {
|
||||
ncs = noise.NewCipherSuite(dhFunc, noiseutil.CipherAESGCM, noise.HashSHA256)
|
||||
}
|
||||
|
||||
static := noise.DHKey{Private: cs.privateKey, Public: crt.PublicKey()}
|
||||
|
||||
b := NewBits(ReplayWindow)
|
||||
// Clear out bit 0, we never transmit it, and we don't want it showing as packet loss
|
||||
b.Update(l, 0)
|
||||
|
||||
hs, err := noise.NewHandshakeState(noise.Config{
|
||||
CipherSuite: ncs,
|
||||
Random: rand.Reader,
|
||||
Pattern: pattern,
|
||||
Initiator: initiator,
|
||||
StaticKeypair: static,
|
||||
//NOTE: These should come from CertState (pki.go) when we finally implement it
|
||||
PresharedKey: []byte{},
|
||||
PresharedKeyPlacement: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewConnectionState: %s", err)
|
||||
}
|
||||
|
||||
// The queue and ready params prevent a counter race that would happen when
|
||||
// sending stored packets and simultaneously accepting new traffic.
|
||||
ci := &ConnectionState{
|
||||
myCert: r.MyCert,
|
||||
initiator: r.Initiator,
|
||||
peerCert: r.RemoteCert,
|
||||
eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
|
||||
dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
|
||||
window: NewBits(ReplayWindow),
|
||||
H: hs,
|
||||
initiator: initiator,
|
||||
window: b,
|
||||
myCert: crt,
|
||||
}
|
||||
ci.messageCounter.Add(r.MessageIndex)
|
||||
for i := uint64(1); i <= r.MessageIndex; i++ {
|
||||
ci.window.Update(nil, i)
|
||||
}
|
||||
return ci
|
||||
// always start the counter from 2, as packet 1 and packet 2 are handshake packets.
|
||||
ci.messageCounter.Add(2)
|
||||
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
||||
@@ -57,52 +94,3 @@ func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
||||
func (cs *ConnectionState) Curve() cert.Curve {
|
||||
return cs.myCert.Curve()
|
||||
}
|
||||
|
||||
func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, out []byte, packet []byte, nb []byte) ([]byte, error) {
|
||||
var err error
|
||||
cs.decryptLock.Lock()
|
||||
result := cs.window.Check(l, messageCounter)
|
||||
cs.decryptLock.Unlock()
|
||||
if !result {
|
||||
return nil, ErrAlreadySeen
|
||||
}
|
||||
|
||||
out, err = cs.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs.decryptLock.Lock()
|
||||
result = cs.window.Update(l, messageCounter)
|
||||
cs.decryptLock.Unlock()
|
||||
if !result {
|
||||
return nil, ErrAlreadySeen
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// VerifyRelay verifies AEAD protected (but not encrypted) relay frames. packet must be length-checked by the caller.
|
||||
func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
|
||||
cs.decryptLock.Lock()
|
||||
result := cs.window.Check(l, messageCounter)
|
||||
cs.decryptLock.Unlock()
|
||||
if !result {
|
||||
return ErrAlreadySeen
|
||||
}
|
||||
|
||||
signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
|
||||
signatureValue := packet[len(packet)-cs.dKey.Overhead():]
|
||||
_, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs.decryptLock.Lock()
|
||||
result = cs.window.Update(l, messageCounter)
|
||||
cs.decryptLock.Unlock()
|
||||
if !result {
|
||||
return ErrAlreadySeen
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/flynn/noise"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
ct "github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/handshake"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// runTestHandshake runs a complete IX handshake between two freshly-built
|
||||
// peers and returns the initiator and responder Results. Used to produce
|
||||
// real cipher states for tests that need to exercise post-handshake glue.
|
||||
func runTestHandshake(t *testing.T) (initR, respR *handshake.Result) {
|
||||
t.Helper()
|
||||
|
||||
ca, _, caKey, _ := ct.NewTestCaCert(
|
||||
cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil,
|
||||
)
|
||||
caPool := ct.NewTestCAPool(ca)
|
||||
|
||||
makeCreds := func(name string, networks []netip.Prefix) handshake.GetCredentialFunc {
|
||||
c, _, rawKey, _ := ct.NewTestCert(
|
||||
cert.Version2, cert.Curve_CURVE25519, ca, caKey,
|
||||
name, ca.NotBefore(), ca.NotAfter(), networks, nil, nil,
|
||||
)
|
||||
priv, _, _, err := cert.UnmarshalPrivateKeyFromPEM(rawKey)
|
||||
require.NoError(t, err)
|
||||
hsBytes, err := c.MarshalForHandshakes()
|
||||
require.NoError(t, err)
|
||||
ncs := noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256)
|
||||
cred := handshake.NewCredential(c, hsBytes, priv, ncs)
|
||||
return func(v cert.Version) *handshake.Credential {
|
||||
if v == cert.Version2 {
|
||||
return cred
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
verifier := func(c cert.Certificate) (*cert.CachedCertificate, error) {
|
||||
return caPool.VerifyCertificate(time.Now(), c)
|
||||
}
|
||||
|
||||
initCreds := makeCreds("initiator", []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")})
|
||||
respCreds := makeCreds("responder", []netip.Prefix{netip.MustParsePrefix("10.0.0.2/24")})
|
||||
|
||||
initM, err := handshake.NewMachine(
|
||||
cert.Version2, initCreds, verifier,
|
||||
func() (uint32, error) { return 1000, nil },
|
||||
true, header.HandshakeIXPSK0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
respM, err := handshake.NewMachine(
|
||||
cert.Version2, respCreds, verifier,
|
||||
func() (uint32, error) { return 2000, nil },
|
||||
false, header.HandshakeIXPSK0,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg1, err := initM.Initiate(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, respR, err := respM.ProcessPacket(nil, msg1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, respR)
|
||||
|
||||
_, initR, err = initM.ProcessPacket(nil, resp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, initR)
|
||||
|
||||
return initR, respR
|
||||
}
|
||||
|
||||
func TestNewConnectionStateFromResult(t *testing.T) {
|
||||
initR, respR := runTestHandshake(t)
|
||||
|
||||
t.Run("initiator", func(t *testing.T) {
|
||||
ci := newConnectionStateFromResult(initR)
|
||||
assert.True(t, ci.initiator)
|
||||
assert.Equal(t, initR.MyCert, ci.myCert)
|
||||
assert.Equal(t, initR.RemoteCert, ci.peerCert)
|
||||
assert.NotNil(t, ci.eKey)
|
||||
assert.NotNil(t, ci.dKey)
|
||||
|
||||
// IX has 2 handshake messages; the next data-plane send is counter=3.
|
||||
assert.Equal(t, uint64(2), ci.messageCounter.Load(),
|
||||
"messageCounter must equal Result.MessageIndex so the next send is N+1")
|
||||
|
||||
// Both handshake counters must be marked seen so they don't appear lost.
|
||||
// Check returns false if an index has already been recorded.
|
||||
assert.False(t, ci.window.Check(nil, 1), "counter 1 must already be seen")
|
||||
assert.False(t, ci.window.Check(nil, 2), "counter 2 must already be seen")
|
||||
// Counter 3 is the next data-plane message and must NOT be pre-marked.
|
||||
assert.True(t, ci.window.Check(nil, 3), "counter 3 must not be pre-seeded")
|
||||
})
|
||||
|
||||
t.Run("responder", func(t *testing.T) {
|
||||
ci := newConnectionStateFromResult(respR)
|
||||
assert.False(t, ci.initiator)
|
||||
assert.Equal(t, respR.MyCert, ci.myCert)
|
||||
assert.Equal(t, respR.RemoteCert, ci.peerCert)
|
||||
assert.NotNil(t, ci.eKey)
|
||||
assert.NotNil(t, ci.dKey)
|
||||
assert.Equal(t, uint64(2), ci.messageCounter.Load())
|
||||
})
|
||||
}
|
||||
+17
-119
@@ -2,33 +2,17 @@ package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/overlay"
|
||||
)
|
||||
|
||||
type RunState int
|
||||
|
||||
const (
|
||||
StateUnknown RunState = iota
|
||||
StateReady
|
||||
StateStarted
|
||||
StateStopping
|
||||
StateStopped
|
||||
)
|
||||
|
||||
var ErrAlreadyStarted = errors.New("nebula is already started")
|
||||
var ErrAlreadyStopped = errors.New("nebula cannot be restarted")
|
||||
var ErrUnknownState = errors.New("nebula state is invalid")
|
||||
|
||||
// Every interaction here needs to take extra care to copy memory and not return or use arguments "as is" when touching
|
||||
// core. This means copying IP objects, slices, de-referencing pointers and taking the actual value, etc
|
||||
|
||||
@@ -42,18 +26,14 @@ type controlHostLister interface {
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
stateLock sync.Mutex
|
||||
state RunState
|
||||
|
||||
f *Interface
|
||||
l *slog.Logger
|
||||
l *logrus.Logger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
sshStart func()
|
||||
statsStart func()
|
||||
dnsStart func()
|
||||
lighthouseStart func()
|
||||
networkChangeStart func(rebind func())
|
||||
connectionManagerStart func(context.Context)
|
||||
}
|
||||
|
||||
@@ -69,31 +49,10 @@ type ControlHostInfo struct {
|
||||
CurrentRelaysThroughMe []netip.Addr `json:"currentRelaysThroughMe"`
|
||||
}
|
||||
|
||||
// Start actually runs nebula, this is a nonblocking call.
|
||||
// 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 ErrAlreadyStopped
|
||||
case StateStarted:
|
||||
return ErrAlreadyStarted
|
||||
default:
|
||||
return ErrUnknownState
|
||||
}
|
||||
|
||||
// Start actually runs nebula, this is a nonblocking call. To block use Control.ShutdownBlock()
|
||||
func (c *Control) Start() {
|
||||
// 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 err
|
||||
}
|
||||
c.f.activate()
|
||||
|
||||
// Call all the delayed funcs that waited patiently for the interface to be created.
|
||||
if c.sshStart != nil {
|
||||
@@ -105,9 +64,6 @@ func (c *Control) Start() error {
|
||||
if c.dnsStart != nil {
|
||||
go c.dnsStart()
|
||||
}
|
||||
if c.networkChangeStart != nil {
|
||||
go c.networkChangeStart(c.RebindUDPServer)
|
||||
}
|
||||
if c.connectionManagerStart != nil {
|
||||
go c.connectionManagerStart(c.ctx)
|
||||
}
|
||||
@@ -115,70 +71,25 @@ func (c *Control) Start() error {
|
||||
c.lighthouseStart()
|
||||
}
|
||||
|
||||
c.f.triggerShutdown = c.Stop
|
||||
|
||||
// Start reading packets.
|
||||
c.f.run()
|
||||
c.state = StateStarted
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) State() RunState {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
return c.state
|
||||
}
|
||||
|
||||
func (c *Control) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Stop signals nebula to shutdown and close all tunnels, returns after the shutdown is complete
|
||||
func (c *Control) Stop() {
|
||||
c.stateLock.Lock()
|
||||
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
|
||||
}
|
||||
|
||||
c.state = StateStopping
|
||||
c.stateLock.Unlock()
|
||||
|
||||
// Closing tunnels can be slow with a large hostmap, don't hold the lock for it
|
||||
// Stop the handshakeManager (and other services), to prevent new tunnels from
|
||||
// being created while we're shutting them all down.
|
||||
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.l.WithError(err).Error("Close interface failed")
|
||||
}
|
||||
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()
|
||||
c.l.Info("Goodbye")
|
||||
}
|
||||
|
||||
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
||||
@@ -189,24 +100,13 @@ func (c *Control) ShutdownBlock() {
|
||||
|
||||
rawSig := <-sigChan
|
||||
sig := rawSig.String()
|
||||
c.l.Info("Caught signal, shutting down", "signal", sig)
|
||||
c.l.WithField("signal", sig).Info("Caught signal, shutting down")
|
||||
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
|
||||
}
|
||||
|
||||
// A failure here means we are likely still pinned to the interface we came up on, so the rest of this is
|
||||
// unlikely to help. Say so instead of silently carrying on as if we rebound.
|
||||
if err := c.f.outside.Rebind(); err != nil {
|
||||
c.l.Error("Failed to rebind udp socket", "error", err)
|
||||
}
|
||||
_ = c.f.outside.Rebind()
|
||||
|
||||
// Trigger a lighthouse update, useful for mobile clients that should have an update interval of 0
|
||||
c.f.lightHouse.SendUpdate()
|
||||
@@ -337,10 +237,8 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
||||
c.f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
|
||||
c.f.closeTunnel(h)
|
||||
|
||||
c.l.Debug("Sending close tunnel message",
|
||||
"vpnAddrs", h.vpnAddrs,
|
||||
"udpAddr", h.GetRemote(),
|
||||
)
|
||||
c.l.WithField("vpnAddrs", h.vpnAddrs).WithField("udpAddr", h.remote).
|
||||
Debug("Sending close tunnel message")
|
||||
closed++
|
||||
}
|
||||
|
||||
@@ -384,7 +282,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
||||
RemoteAddrs: h.remotes.CopyAddrs(preferredRanges),
|
||||
CurrentRelaysToMe: h.relayState.CopyRelayIps(),
|
||||
CurrentRelaysThroughMe: h.relayState.CopyRelayForIps(),
|
||||
CurrentRemote: h.GetRemote(),
|
||||
CurrentRemote: h.remote,
|
||||
}
|
||||
|
||||
for i, a := range h.vpnAddrs {
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"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(p []byte) (int, error) {
|
||||
<-d.closedCh
|
||||
return 0, 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) SupportsMultiqueue() bool { return false }
|
||||
func (d *fakeDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||
return nil, errors.New("unsupported")
|
||||
}
|
||||
|
||||
// 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},
|
||||
readers: make([]io.ReadWriteCloser, 1),
|
||||
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
|
||||
}
|
||||
|
||||
func (d *multiqueueDevice) SupportsMultiqueue() bool { return true }
|
||||
|
||||
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},
|
||||
readers: make([]io.ReadWriteCloser, 2),
|
||||
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")
|
||||
}
|
||||
+8
-163
@@ -1,17 +1,15 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"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) {
|
||||
@@ -45,7 +43,8 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
|
||||
crt := &dummyCert{}
|
||||
hi := &HostInfo{
|
||||
hm.unlockedAddHostInfo(&HostInfo{
|
||||
remote: remote1,
|
||||
remotes: remotes,
|
||||
ConnectionState: &ConnectionState{
|
||||
peerCert: &cert.CachedCertificate{Certificate: crt},
|
||||
@@ -58,14 +57,13 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}
|
||||
hi.remote.Store(&remote1)
|
||||
hm.unlockedAddHostInfo(hi, &Interface{})
|
||||
}, &Interface{})
|
||||
|
||||
vpnIp2, ok := netip.AddrFromSlice(ipNet2.IP)
|
||||
assert.True(t, ok)
|
||||
|
||||
hi2 := &HostInfo{
|
||||
hm.unlockedAddHostInfo(&HostInfo{
|
||||
remote: remote1,
|
||||
remotes: remotes,
|
||||
ConnectionState: &ConnectionState{
|
||||
peerCert: nil,
|
||||
@@ -78,16 +76,13 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||
relayForByAddr: map[netip.Addr]*Relay{},
|
||||
relayForByIdx: map[uint32]*Relay{},
|
||||
},
|
||||
}
|
||||
hi2.remote.Store(&remote1)
|
||||
hm.unlockedAddHostInfo(hi2, &Interface{})
|
||||
}, &Interface{})
|
||||
|
||||
c := Control{
|
||||
state: StateReady,
|
||||
f: &Interface{
|
||||
hostMap: hm,
|
||||
},
|
||||
l: test.NewLogger(),
|
||||
l: logrus.New(),
|
||||
}
|
||||
|
||||
thi := c.GetHostInfoByVpnAddr(vpnIp, false)
|
||||
@@ -124,153 +119,3 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+62
-37
@@ -1,10 +1,13 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/slackhq/nebula/header"
|
||||
"github.com/slackhq/nebula/overlay"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
@@ -20,9 +23,7 @@ func (c *Control) WaitForType(msgType header.MessageType, subType header.Message
|
||||
panic(err)
|
||||
}
|
||||
pipeTo.InjectUDPPacket(p)
|
||||
match := h.Type == msgType && h.Subtype == subType
|
||||
p.Release()
|
||||
if match {
|
||||
if h.Type == msgType && h.Subtype == subType {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -38,9 +39,7 @@ func (c *Control) WaitForTypeByIndex(toIndex uint32, msgType header.MessageType,
|
||||
panic(err)
|
||||
}
|
||||
pipeTo.InjectUDPPacket(p)
|
||||
match := h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType
|
||||
p.Release()
|
||||
if match {
|
||||
if h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -92,15 +91,65 @@ func (c *Control) GetTunTxChan() <-chan []byte {
|
||||
return c.f.inside.(*overlay.TestTun).TxPackets
|
||||
}
|
||||
|
||||
// InjectUDPPacket injects a packet into the udp side. We copy internally so the caller keeps ownership of p.
|
||||
// The copy comes from the freelist so steady-state alloc is zero.
|
||||
// InjectUDPPacket will inject a packet into the udp side of nebula
|
||||
func (c *Control) InjectUDPPacket(p *udp.Packet) {
|
||||
c.f.outside.(*udp.TesterConn).Send(p.Copy())
|
||||
c.f.outside.(*udp.TesterConn).Send(p)
|
||||
}
|
||||
|
||||
// InjectTunPacket pushes an IP packet onto the tun interface.
|
||||
func (c *Control) InjectTunPacket(packet []byte) {
|
||||
c.f.inside.(*overlay.TestTun).Send(packet)
|
||||
// InjectTunUDPPacket puts a udp packet on the tun interface. Using UDP here because it's a simpler protocol
|
||||
func (c *Control) InjectTunUDPPacket(toAddr netip.Addr, toPort uint16, fromAddr netip.Addr, fromPort uint16, data []byte) {
|
||||
serialize := make([]gopacket.SerializableLayer, 0)
|
||||
var netLayer gopacket.NetworkLayer
|
||||
if toAddr.Is6() {
|
||||
if !fromAddr.Is6() {
|
||||
panic("Cant send ipv6 to ipv4")
|
||||
}
|
||||
ip := &layers.IPv6{
|
||||
Version: 6,
|
||||
NextHeader: layers.IPProtocolUDP,
|
||||
SrcIP: fromAddr.Unmap().AsSlice(),
|
||||
DstIP: toAddr.Unmap().AsSlice(),
|
||||
}
|
||||
serialize = append(serialize, ip)
|
||||
netLayer = ip
|
||||
} else {
|
||||
if !fromAddr.Is4() {
|
||||
panic("Cant send ipv4 to ipv6")
|
||||
}
|
||||
|
||||
ip := &layers.IPv4{
|
||||
Version: 4,
|
||||
TTL: 64,
|
||||
Protocol: layers.IPProtocolUDP,
|
||||
SrcIP: fromAddr.Unmap().AsSlice(),
|
||||
DstIP: toAddr.Unmap().AsSlice(),
|
||||
}
|
||||
serialize = append(serialize, ip)
|
||||
netLayer = ip
|
||||
}
|
||||
|
||||
udp := layers.UDP{
|
||||
SrcPort: layers.UDPPort(fromPort),
|
||||
DstPort: layers.UDPPort(toPort),
|
||||
}
|
||||
err := udp.SetNetworkLayerForChecksum(netLayer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buffer := gopacket.NewSerializeBuffer()
|
||||
opt := gopacket.SerializeOptions{
|
||||
ComputeChecksums: true,
|
||||
FixLengths: true,
|
||||
}
|
||||
|
||||
serialize = append(serialize, &udp, gopacket.Payload(data))
|
||||
err = gopacket.SerializeLayers(buffer, opt, serialize...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.f.inside.(*overlay.TestTun).Send(buffer.Bytes())
|
||||
}
|
||||
|
||||
func (c *Control) GetVpnAddrs() []netip.Addr {
|
||||
@@ -108,19 +157,7 @@ func (c *Control) GetVpnAddrs() []netip.Addr {
|
||||
}
|
||||
|
||||
func (c *Control) GetUDPAddr() netip.AddrPort {
|
||||
return c.f.outside.(*udp.TesterConn).GetAddr()
|
||||
}
|
||||
|
||||
// SetUDPAddr moves this node to a new underlay address, standing in for a laptop waking up on a different
|
||||
// network. Register the new address with the router as well or nothing will route back.
|
||||
func (c *Control) SetUDPAddr(addr netip.AddrPort) {
|
||||
c.f.outside.(*udp.TesterConn).SetAddr(addr)
|
||||
}
|
||||
|
||||
// SetLocalAddrsFn replaces underlay address discovery so a test can advertise its simulated address instead of
|
||||
// whatever this machine's NICs happen to be. Call it before Start, SendUpdate reads it from the update worker.
|
||||
func (c *Control) SetLocalAddrsFn(fn func(*LocalAllowList) []netip.Addr) {
|
||||
c.f.lightHouse.localAddrsFn = fn
|
||||
return c.f.outside.(*udp.TesterConn).Addr
|
||||
}
|
||||
|
||||
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
||||
@@ -137,18 +174,6 @@ 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
|
||||
}
|
||||
|
||||
func (c *Control) GetCertState() *CertState {
|
||||
return c.f.pki.getCertState()
|
||||
}
|
||||
|
||||
Vendored
+15
-9
@@ -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 = bit.band(tvbuf:range(0,1):uint(), 0x0F)
|
||||
local nebula_type = bit32.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))
|
||||
@@ -84,24 +84,30 @@ end
|
||||
|
||||
function nebula.prefs_changed()
|
||||
if default_settings.all_ports == nebula.prefs.all_ports and default_settings.port == nebula.prefs.port then
|
||||
-- Nothing changed, bail
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove all existing registrations
|
||||
-- Remove our old dissector
|
||||
DissectorTable.get("udp.port"):remove_all(nebula)
|
||||
|
||||
if nebula.prefs.all_ports then
|
||||
-- Register on every port for hole punch capture
|
||||
if nebula.prefs.all_ports and default_settings.all_ports ~= nebula.prefs.all_ports then
|
||||
default_settings.all_port = nebula.prefs.all_ports
|
||||
|
||||
for i=0, 65535 do
|
||||
DissectorTable.get("udp.port"):add(i, nebula)
|
||||
end
|
||||
else
|
||||
-- Register on the configured port only
|
||||
DissectorTable.get("udp.port"):add(nebula.prefs.port, nebula)
|
||||
|
||||
-- no need to establish again on specific ports
|
||||
return
|
||||
end
|
||||
|
||||
default_settings.all_ports = nebula.prefs.all_ports
|
||||
default_settings.port = nebula.prefs.port
|
||||
|
||||
if default_settings.all_ports ~= nebula.prefs.all_ports then
|
||||
-- Add our new port dissector
|
||||
default_settings.port = nebula.prefs.port
|
||||
DissectorTable.get("udp.port"):add(default_settings.port, nebula)
|
||||
end
|
||||
end
|
||||
|
||||
DissectorTable.get("udp.port"):add(default_settings.port, nebula)
|
||||
|
||||
+74
-322
@@ -1,277 +1,68 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula/config"
|
||||
)
|
||||
|
||||
type dnsServer struct {
|
||||
// This whole thing should be rewritten to use context
|
||||
|
||||
var dnsR *dnsRecords
|
||||
var dnsServer *dns.Server
|
||||
var dnsAddr string
|
||||
|
||||
type dnsRecords struct {
|
||||
sync.RWMutex
|
||||
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
|
||||
|
||||
// enabled mirrors `lighthouse.serve_dns && lighthouse.am_lighthouse`.
|
||||
// Start, Add, and reload consult it so callers don't need to know the
|
||||
// gating rules. When it toggles off via reload, accumulated records are
|
||||
// cleared so a later re-enable starts with a fresh map populated from
|
||||
// new handshakes.
|
||||
enabled atomic.Bool
|
||||
|
||||
serverMu sync.Mutex
|
||||
server *dns.Server
|
||||
// started is closed once `server` has finished binding (or after
|
||||
// ListenAndServe returns on a bind failure). Stop waits on it before
|
||||
// calling Shutdown to avoid the miekg/dns "server not started" race
|
||||
// where a Shutdown that arrives before bind completes is silently
|
||||
// ignored, leaving the listener running forever.
|
||||
started chan struct{}
|
||||
addr string
|
||||
l *logrus.Logger
|
||||
dnsMap4 map[string]netip.Addr
|
||||
dnsMap6 map[string]netip.Addr
|
||||
hostMap *HostMap
|
||||
myVpnAddrsTable *bart.Lite
|
||||
}
|
||||
|
||||
// newDnsServerFromConfig builds a dnsServer, applies the initial config, and
|
||||
// registers a reload callback. The reload callback is registered before the
|
||||
// initial config is applied, so a SIGHUP can later enable, fix, or disable
|
||||
// DNS even if the initial application failed.
|
||||
//
|
||||
// The dnsServer internally gates on `lighthouse.serve_dns &&
|
||||
// lighthouse.am_lighthouse`. Start and Add are safe to call unconditionally,
|
||||
// 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, 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,
|
||||
pki: pki,
|
||||
}
|
||||
ds.mux = dns.NewServeMux()
|
||||
ds.mux.HandleFunc(".", ds.handleDnsRequest)
|
||||
|
||||
c.RegisterReloadCallback(func(c *config.C) {
|
||||
if err := ds.reload(c, false); err != nil {
|
||||
ds.l.Error("Failed to reload DNS responder from config", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := ds.reload(c, true); err != nil {
|
||||
return ds, err
|
||||
}
|
||||
ds.seedSelf()
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// reload applies the latest config and reconciles the running state with it:
|
||||
// - enabled toggled on -> spawn a runner
|
||||
// - enabled toggled off -> stop the runner
|
||||
// - listen address changed (while running) -> restart on the new address
|
||||
// - everything else -> no-op
|
||||
//
|
||||
// On the initial call it only records configuration; Control.Start is what
|
||||
// launches the first runner via dnsStart.
|
||||
func (d *dnsServer) reload(c *config.C, initial bool) error {
|
||||
wantsDns := c.GetBool("lighthouse.serve_dns", false)
|
||||
amLighthouse := c.GetBool("lighthouse.am_lighthouse", false)
|
||||
enabled := wantsDns && amLighthouse
|
||||
newAddr := getDnsServerAddr(c)
|
||||
|
||||
d.serverMu.Lock()
|
||||
running := d.server != nil
|
||||
sameAddr := d.addr == newAddr
|
||||
d.addr = newAddr
|
||||
d.enabled.Store(enabled)
|
||||
d.serverMu.Unlock()
|
||||
|
||||
if initial {
|
||||
if wantsDns && !amLighthouse {
|
||||
d.l.Warn("DNS server refusing to run because this host is not a lighthouse.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
if running {
|
||||
d.Stop()
|
||||
}
|
||||
// Drop any records that accumulated while enabled; a later re-enable
|
||||
// will repopulate from fresh handshakes and a fresh seedSelf.
|
||||
d.clearRecords()
|
||||
return nil
|
||||
}
|
||||
|
||||
if !running {
|
||||
// Was disabled (or never started); bring it up now.
|
||||
go d.Start()
|
||||
} else if !sameAddr {
|
||||
// Stop clears the slot before shutting down, otherwise the Start below can find the dying server and refuse
|
||||
d.Stop()
|
||||
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
|
||||
}
|
||||
|
||||
// shutdownServer waits for the server to finish binding (so Shutdown actually
|
||||
// stops it rather than no-oping) and then shuts it down.
|
||||
func (d *dnsServer) shutdownServer(srv *dns.Server, started chan struct{}, reason string) {
|
||||
if srv == nil {
|
||||
return
|
||||
}
|
||||
if started != nil {
|
||||
<-started
|
||||
}
|
||||
if err := srv.Shutdown(); err != nil {
|
||||
d.l.Warn("Failed to shut down the DNS responder", "reason", reason, "error", err)
|
||||
func newDnsRecords(l *logrus.Logger, cs *CertState, hostMap *HostMap) *dnsRecords {
|
||||
return &dnsRecords{
|
||||
l: l,
|
||||
dnsMap4: make(map[string]netip.Addr),
|
||||
dnsMap6: make(map[string]netip.Addr),
|
||||
hostMap: hostMap,
|
||||
myVpnAddrsTable: cs.myVpnAddrsTable,
|
||||
}
|
||||
}
|
||||
|
||||
// Start binds and serves the DNS responder. Blocks until Stop is called or
|
||||
// the listener errors. Safe to call when DNS is disabled (returns
|
||||
// immediately). This is what Control.dnsStart points at.
|
||||
//
|
||||
// Must be invoked after the tun device is active so that lighthouse.dns.host
|
||||
// may bind to a nebula IP.
|
||||
func (d *dnsServer) Start() {
|
||||
if !d.enabled.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
d.serverMu.Lock()
|
||||
// Re-check enabled under the lock, a disable that raced our check above snapshots the slot under it too.
|
||||
// Two reloads in quick succession can both spawn a Start, the loser would orphan the live listener past Stop
|
||||
if d.ctx.Err() != nil || d.server != nil || !d.enabled.Load() {
|
||||
d.serverMu.Unlock()
|
||||
return
|
||||
}
|
||||
addr := d.addr
|
||||
server := &dns.Server{
|
||||
Addr: addr,
|
||||
Net: "udp",
|
||||
Handler: d.mux,
|
||||
NotifyStartedFunc: func() { close(started) },
|
||||
}
|
||||
d.server = server
|
||||
d.started = started
|
||||
d.serverMu.Unlock()
|
||||
|
||||
// Per-invocation ctx watcher. Exits when Start does, so we don't leak a
|
||||
// watcher per reload-driven restart.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
d.shutdownServer(server, started, "shutdown")
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
d.l.Info("Starting DNS responder", "dnsListener", addr)
|
||||
err := server.ListenAndServe()
|
||||
close(done)
|
||||
|
||||
// If the listener never bound (bind error) NotifyStartedFunc never fires,
|
||||
// so close started here to release any Stop caller waiting on it.
|
||||
select {
|
||||
case <-started:
|
||||
default:
|
||||
close(started)
|
||||
}
|
||||
|
||||
// Release our slot, unless a reload already replaced us, so a dead listener can't block a future Start
|
||||
d.serverMu.Lock()
|
||||
if d.server == server {
|
||||
d.server = nil
|
||||
d.started = nil
|
||||
}
|
||||
d.serverMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
d.l.Warn("Failed to run the DNS responder", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop shuts down the active server, if any. Idempotent.
|
||||
func (d *dnsServer) Stop() {
|
||||
d.serverMu.Lock()
|
||||
srv := d.server
|
||||
started := d.started
|
||||
d.server = nil
|
||||
d.started = nil
|
||||
d.serverMu.Unlock()
|
||||
d.shutdownServer(srv, started, "stop")
|
||||
}
|
||||
|
||||
// Query returns the address for the given name and query type. The second
|
||||
// return value reports whether the name is known at all (in either A or AAAA),
|
||||
// which lets callers distinguish NODATA from NXDOMAIN.
|
||||
func (d *dnsServer) Query(q uint16, data string) (netip.Addr, bool) {
|
||||
func (d *dnsRecords) Query(q uint16, data string) netip.Addr {
|
||||
data = strings.ToLower(data)
|
||||
d.RLock()
|
||||
defer d.RUnlock()
|
||||
addr4, haveV4 := d.dnsMap4[data]
|
||||
addr6, haveV6 := d.dnsMap6[data]
|
||||
nameExists := haveV4 || haveV6
|
||||
switch q {
|
||||
case dns.TypeA:
|
||||
if haveV4 {
|
||||
return addr4, nameExists
|
||||
if r, ok := d.dnsMap4[data]; ok {
|
||||
return r
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
if haveV6 {
|
||||
return addr6, nameExists
|
||||
if r, ok := d.dnsMap6[data]; ok {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, nameExists
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
func (d *dnsServer) QueryCert(data string) string {
|
||||
if len(data) < 2 {
|
||||
return ""
|
||||
}
|
||||
func (d *dnsRecords) QueryCert(data string) string {
|
||||
ip, err := netip.ParseAddr(data[:len(data)-1])
|
||||
if err != nil {
|
||||
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 ""
|
||||
@@ -289,67 +80,8 @@ func (d *dnsServer) QueryCert(data string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// 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`
|
||||
func (d *dnsServer) Add(host string, addresses []netip.Addr) {
|
||||
if !d.enabled.Load() {
|
||||
return
|
||||
}
|
||||
func (d *dnsRecords) Add(host string, addresses []netip.Addr) {
|
||||
host = strings.ToLower(host)
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
@@ -369,7 +101,7 @@ func (d *dnsServer) Add(host string, addresses []netip.Addr) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool {
|
||||
func (d *dnsRecords) isSelfNebulaOrLocalhost(addr string) bool {
|
||||
a, _, _ := net.SplitHostPort(addr)
|
||||
b, err := netip.ParseAddr(a)
|
||||
if err != nil {
|
||||
@@ -380,32 +112,17 @@ 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 cs.myVpnAddrsTable.Contains(b)
|
||||
return d.myVpnAddrsTable.Contains(b)
|
||||
}
|
||||
|
||||
func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
||||
debugEnabled := d.l.Enabled(context.Background(), slog.LevelDebug)
|
||||
// Per RFC 2308 §2.2, a name that exists but has no record of the requested
|
||||
// type must be answered with NOERROR and an empty answer section (NODATA),
|
||||
// not NXDOMAIN (RFC 2308 §2.1), which is reserved for names that do not
|
||||
// exist at all.
|
||||
anyNameExists := false
|
||||
func (d *dnsRecords) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
||||
for _, q := range m.Question {
|
||||
switch q.Qtype {
|
||||
case dns.TypeA, dns.TypeAAAA:
|
||||
qType := dns.TypeToString[q.Qtype]
|
||||
if debugEnabled {
|
||||
d.l.Debug("DNS query", "type", qType, "name", q.Name)
|
||||
}
|
||||
ip, nameExists := d.Query(q.Qtype, q.Name)
|
||||
if nameExists {
|
||||
anyNameExists = true
|
||||
}
|
||||
d.l.Debugf("Query for %s %s", qType, q.Name)
|
||||
ip := d.Query(q.Qtype, q.Name)
|
||||
if ip.IsValid() {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
|
||||
if err == nil {
|
||||
@@ -417,9 +134,7 @@ func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
||||
if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) {
|
||||
return
|
||||
}
|
||||
if debugEnabled {
|
||||
d.l.Debug("DNS query", "type", "TXT", "name", q.Name)
|
||||
}
|
||||
d.l.Debugf("Query for TXT %s", q.Name)
|
||||
ip := d.QueryCert(q.Name)
|
||||
if ip != "" {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s TXT %s", q.Name, ip))
|
||||
@@ -430,12 +145,12 @@ func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.Answer) == 0 && !anyNameExists {
|
||||
if len(m.Answer) == 0 {
|
||||
m.Rcode = dns.RcodeNameError
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnsServer) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
func (d *dnsRecords) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Compress = false
|
||||
@@ -448,6 +163,21 @@ func (d *dnsServer) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
|
||||
func dnsMain(l *logrus.Logger, cs *CertState, hostMap *HostMap, c *config.C) func() {
|
||||
dnsR = newDnsRecords(l, cs, hostMap)
|
||||
|
||||
// attach request handler func
|
||||
dns.HandleFunc(".", dnsR.handleDnsRequest)
|
||||
|
||||
c.RegisterReloadCallback(func(c *config.C) {
|
||||
reloadDns(l, c)
|
||||
})
|
||||
|
||||
return func() {
|
||||
startDns(l, c)
|
||||
}
|
||||
}
|
||||
|
||||
func getDnsServerAddr(c *config.C) string {
|
||||
dnsHost := strings.TrimSpace(c.GetString("lighthouse.dns.host", ""))
|
||||
// Old guidance was to provide the literal `[::]` in `lighthouse.dns.host` but that won't resolve.
|
||||
@@ -456,3 +186,25 @@ func getDnsServerAddr(c *config.C) string {
|
||||
}
|
||||
return net.JoinHostPort(dnsHost, strconv.Itoa(c.GetInt("lighthouse.dns.port", 53)))
|
||||
}
|
||||
|
||||
func startDns(l *logrus.Logger, c *config.C) {
|
||||
dnsAddr = getDnsServerAddr(c)
|
||||
dnsServer = &dns.Server{Addr: dnsAddr, Net: "udp"}
|
||||
l.WithField("dnsListener", dnsAddr).Info("Starting DNS responder")
|
||||
err := dnsServer.ListenAndServe()
|
||||
defer dnsServer.Shutdown()
|
||||
if err != nil {
|
||||
l.Errorf("Failed to start server: %s\n ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func reloadDns(l *logrus.Logger, c *config.C) {
|
||||
if dnsAddr == getDnsServerAddr(c) {
|
||||
l.Debug("No DNS server config change detected")
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug("Restarting DNS server")
|
||||
dnsServer.Shutdown()
|
||||
go startDns(l, c)
|
||||
}
|
||||
|
||||
+3
-561
@@ -1,46 +1,19 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type stubDNSWriter struct{}
|
||||
|
||||
func (stubDNSWriter) LocalAddr() net.Addr { return &net.UDPAddr{} }
|
||||
func (stubDNSWriter) RemoteAddr() net.Addr {
|
||||
return &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5353}
|
||||
}
|
||||
func (stubDNSWriter) Write([]byte) (int, error) { return 0, nil }
|
||||
func (stubDNSWriter) WriteMsg(*dns.Msg) error { return nil }
|
||||
func (stubDNSWriter) Close() error { return nil }
|
||||
func (stubDNSWriter) TsigStatus() error { return nil }
|
||||
func (stubDNSWriter) TsigTimersOnly(bool) {}
|
||||
func (stubDNSWriter) Hijack() {}
|
||||
|
||||
func TestParsequery(t *testing.T) {
|
||||
l := slog.New(slog.DiscardHandler)
|
||||
l := logrus.New()
|
||||
hostMap := &HostMap{}
|
||||
ds := &dnsServer{
|
||||
l: l,
|
||||
dnsMap4: make(map[string]netip.Addr),
|
||||
dnsMap6: make(map[string]netip.Addr),
|
||||
hostMap: hostMap,
|
||||
}
|
||||
ds.enabled.Store(true)
|
||||
ds := newDnsRecords(l, &CertState{}, hostMap)
|
||||
addrs := []netip.Addr{
|
||||
netip.MustParseAddr("1.2.3.4"),
|
||||
netip.MustParseAddr("1.2.3.5"),
|
||||
@@ -48,56 +21,18 @@ func TestParsequery(t *testing.T) {
|
||||
netip.MustParseAddr("fd01::25"),
|
||||
}
|
||||
ds.Add("test.com.com", addrs)
|
||||
ds.Add("v4only.com.com", []netip.Addr{netip.MustParseAddr("1.2.3.6")})
|
||||
ds.Add("v6only.com.com", []netip.Addr{netip.MustParseAddr("fd01::26")})
|
||||
|
||||
m := &dns.Msg{}
|
||||
m.SetQuestion("test.com.com", dns.TypeA)
|
||||
ds.parseQuery(m, nil)
|
||||
assert.NotNil(t, m.Answer)
|
||||
assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String())
|
||||
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||
|
||||
m = &dns.Msg{}
|
||||
m.SetQuestion("test.com.com", dns.TypeAAAA)
|
||||
ds.parseQuery(m, nil)
|
||||
assert.NotNil(t, m.Answer)
|
||||
assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String())
|
||||
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||
|
||||
// A known name with no record of the requested type should return NODATA
|
||||
// (NOERROR with empty answer), not NXDOMAIN.
|
||||
m = &dns.Msg{}
|
||||
m.SetQuestion("v4only.com.com", dns.TypeAAAA)
|
||||
ds.parseQuery(m, nil)
|
||||
assert.Empty(t, m.Answer)
|
||||
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||
|
||||
m = &dns.Msg{}
|
||||
m.SetQuestion("v6only.com.com", dns.TypeA)
|
||||
ds.parseQuery(m, nil)
|
||||
assert.Empty(t, m.Answer)
|
||||
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||
|
||||
// An unknown name should still return NXDOMAIN.
|
||||
m = &dns.Msg{}
|
||||
m.SetQuestion("unknown.com.com", dns.TypeA)
|
||||
ds.parseQuery(m, nil)
|
||||
assert.Empty(t, m.Answer)
|
||||
assert.Equal(t, dns.RcodeNameError, m.Rcode)
|
||||
|
||||
// short lookups should not fail
|
||||
m = &dns.Msg{}
|
||||
m.Question = []dns.Question{{Name: "", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}}
|
||||
ds.parseQuery(m, stubDNSWriter{})
|
||||
assert.Empty(t, m.Answer)
|
||||
assert.Equal(t, dns.RcodeNameError, m.Rcode)
|
||||
|
||||
m = &dns.Msg{}
|
||||
m.Question = []dns.Question{{Name: ".", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}}
|
||||
ds.parseQuery(m, stubDNSWriter{})
|
||||
assert.Empty(t, m.Answer)
|
||||
assert.Equal(t, dns.RcodeNameError, m.Rcode)
|
||||
}
|
||||
|
||||
func Test_getDnsServerAddr(t *testing.T) {
|
||||
@@ -136,496 +71,3 @@ func Test_getDnsServerAddr(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, "[::]:1", getDnsServerAddr(c))
|
||||
}
|
||||
|
||||
func newTestDnsServer(t *testing.T) (*dnsServer, *config.C) {
|
||||
t.Helper()
|
||||
sl := slog.New(slog.DiscardHandler)
|
||||
ds := &dnsServer{
|
||||
l: sl,
|
||||
ctx: context.Background(),
|
||||
dnsMap4: make(map[string]netip.Addr),
|
||||
dnsMap6: make(map[string]netip.Addr),
|
||||
hostMap: &HostMap{},
|
||||
}
|
||||
ds.mux = dns.NewServeMux()
|
||||
ds.mux.HandleFunc(".", ds.handleDnsRequest)
|
||||
return ds, config.NewC(nil)
|
||||
}
|
||||
|
||||
func setDnsConfig(c *config.C, host string, port string, amLighthouse, serveDns bool) {
|
||||
c.Settings["lighthouse"] = map[string]any{
|
||||
"am_lighthouse": amLighthouse,
|
||||
"serve_dns": serveDns,
|
||||
"dns": map[string]any{
|
||||
"host": host,
|
||||
"port": port,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsServer_reload_initial_disabled(t *testing.T) {
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", "0", true, false)
|
||||
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
assert.False(t, ds.enabled.Load())
|
||||
assert.Equal(t, "127.0.0.1:0", ds.addr)
|
||||
assert.Nil(t, ds.server)
|
||||
}
|
||||
|
||||
func TestDnsServer_reload_initial_enabled(t *testing.T) {
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", "0", true, true)
|
||||
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
assert.True(t, ds.enabled.Load())
|
||||
assert.Equal(t, "127.0.0.1:0", ds.addr)
|
||||
// initial never starts a runner; that's Control.Start's job
|
||||
assert.Nil(t, ds.server)
|
||||
}
|
||||
|
||||
func TestDnsServer_reload_initial_serveDnsWithoutLighthouse(t *testing.T) {
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", "0", false, true)
|
||||
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
// Wants DNS but isn't a lighthouse: gated off, no runner.
|
||||
assert.False(t, ds.enabled.Load())
|
||||
}
|
||||
|
||||
func TestDnsServer_reload_sameAddr_noOp(t *testing.T) {
|
||||
port := freeUDPPort(t)
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
go ds.Start()
|
||||
waitForBind(t, ds)
|
||||
|
||||
ds.serverMu.Lock()
|
||||
before := ds.server
|
||||
ds.serverMu.Unlock()
|
||||
require.NotNil(t, before)
|
||||
|
||||
// Same address, so the running listener must be left alone rather than rebuilt under live queries
|
||||
require.NoError(t, ds.reload(c, false))
|
||||
assert.True(t, ds.enabled.Load())
|
||||
|
||||
ds.serverMu.Lock()
|
||||
after := ds.server
|
||||
ds.serverMu.Unlock()
|
||||
assert.Same(t, before, after, "a same-address reload must not restart the listener")
|
||||
|
||||
ds.Stop()
|
||||
}
|
||||
|
||||
// The branch the old sameAddr test was accidentally hitting: enabled with nothing running means reload starts it.
|
||||
func TestDnsServer_reload_whenNotRunning_starts(t *testing.T) {
|
||||
port := freeUDPPort(t)
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
|
||||
// initial only records config, it never starts anything
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
ds.serverMu.Lock()
|
||||
assert.Nil(t, ds.server, "the initial reload must not start a listener")
|
||||
ds.serverMu.Unlock()
|
||||
|
||||
require.NoError(t, ds.reload(c, false))
|
||||
waitForBind(t, ds)
|
||||
|
||||
ds.serverMu.Lock()
|
||||
assert.NotNil(t, ds.server, "a reload with nothing running should bring DNS up")
|
||||
ds.serverMu.Unlock()
|
||||
|
||||
ds.Stop()
|
||||
}
|
||||
|
||||
func TestDnsServer_StartStop_lifecycle(t *testing.T) {
|
||||
// Bind to a real (random) UDP port so we exercise the actual
|
||||
// ListenAndServe + Shutdown plumbing including the started-chan race fix.
|
||||
port := freeUDPPort(t)
|
||||
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ds.Start()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
waitFor(t, func() bool {
|
||||
ds.serverMu.Lock()
|
||||
started := ds.started
|
||||
ds.serverMu.Unlock()
|
||||
if started == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-started:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ds.Stop()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Start did not return after Stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsServer_Stop_beforeBind_doesNotHang(t *testing.T) {
|
||||
// Stop called immediately after Start should not deadlock even if bind
|
||||
// hasn't completed yet. This exercises the started-chan close-on-bind-fail
|
||||
// path: by binding to an obviously bad port (privileged) we get a fast
|
||||
// bind error before NotifyStartedFunc fires.
|
||||
ds, c := newTestDnsServer(t)
|
||||
// Use a port that should fail to bind (negative would be invalid, use a
|
||||
// host that won't resolve to ensure listenUDP fails quickly).
|
||||
setDnsConfig(c, "256.256.256.256", "53", true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ds.Start()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Give Start a moment to attempt the bind and fail.
|
||||
select {
|
||||
case <-done:
|
||||
// Bind failed and Start returned; Stop should be a no-op.
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Start did not return after a bad bind")
|
||||
}
|
||||
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
ds.Stop()
|
||||
close(stopped)
|
||||
}()
|
||||
select {
|
||||
case <-stopped:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Stop hung after a failed bind")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
startReturned := make(chan struct{})
|
||||
go func() {
|
||||
ds.Start()
|
||||
close(startReturned)
|
||||
}()
|
||||
waitForBind(t, ds)
|
||||
|
||||
// Toggle serve_dns off; reload should shut the running server down.
|
||||
setDnsConfig(c, "127.0.0.1", port, true, false)
|
||||
require.NoError(t, ds.reload(c, false))
|
||||
select {
|
||||
case <-startReturned:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Start did not return after reload disabled DNS")
|
||||
}
|
||||
assert.False(t, ds.enabled.Load())
|
||||
}
|
||||
|
||||
func freeUDPPort(t *testing.T) string {
|
||||
t.Helper()
|
||||
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
port := conn.LocalAddr().(*net.UDPAddr).Port
|
||||
require.NoError(t, conn.Close())
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
|
||||
func waitForBind(t *testing.T, ds *dnsServer) {
|
||||
t.Helper()
|
||||
waitFor(t, func() bool {
|
||||
ds.serverMu.Lock()
|
||||
started := ds.started
|
||||
ds.serverMu.Unlock()
|
||||
if started == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-started:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func waitFor(t *testing.T, cond func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if cond() {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("timed out waiting for condition")
|
||||
}
|
||||
|
||||
// Two reloads in quick succession, or a HUP before Control.Start, can race two Starts at the same listener.
|
||||
func TestDnsServer_Start_isIdempotent(t *testing.T) {
|
||||
port := freeUDPPort(t)
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
go ds.Start()
|
||||
waitForBind(t, ds)
|
||||
|
||||
ds.serverMu.Lock()
|
||||
first := ds.server
|
||||
ds.serverMu.Unlock()
|
||||
require.NotNil(t, first)
|
||||
|
||||
// If the second Start replaces the tracked server, Stop kills the wrong one and the port leaks
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ds.Start()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatal("second Start never returned")
|
||||
}
|
||||
|
||||
ds.serverMu.Lock()
|
||||
second := ds.server
|
||||
ds.serverMu.Unlock()
|
||||
assert.Same(t, first, second, "a second Start must not replace the running server")
|
||||
|
||||
// The real proof, after Stop the port must actually be free
|
||||
ds.Stop()
|
||||
waitFor(t, func() bool {
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:"+port)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = pc.Close()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// An address change must actually end up listening on the new port. Start's guard refuses when a server is already
|
||||
// installed, so reload has to clear the slot before shutting the old one down.
|
||||
func TestDnsServer_reload_addrChange_restarts(t *testing.T) {
|
||||
first := freeUDPPort(t)
|
||||
second := freeUDPPort(t)
|
||||
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", first, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
go ds.Start()
|
||||
waitForBind(t, ds)
|
||||
|
||||
// Cycle a few times, the failure this guards against depends on which goroutine wins serverMu
|
||||
for i := range 8 {
|
||||
want := second
|
||||
if i%2 == 1 {
|
||||
want = first
|
||||
}
|
||||
setDnsConfig(c, "127.0.0.1", want, true, true)
|
||||
require.NoError(t, ds.reload(c, false))
|
||||
waitForBind(t, ds)
|
||||
|
||||
ds.serverMu.Lock()
|
||||
srv := ds.server
|
||||
ds.serverMu.Unlock()
|
||||
require.NotNil(t, srv, "reload left DNS down instead of restarting it")
|
||||
require.Equal(t, "127.0.0.1:"+want, srv.Addr, "reload should be serving the new address")
|
||||
}
|
||||
|
||||
// Land back on second so the port assertions below are meaningful
|
||||
setDnsConfig(c, "127.0.0.1", second, true, true)
|
||||
require.NoError(t, ds.reload(c, false))
|
||||
waitForBind(t, ds)
|
||||
|
||||
// The old port must be released and the new one actually held
|
||||
waitFor(t, func() bool {
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:"+first)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = pc.Close()
|
||||
return true
|
||||
})
|
||||
_, err := net.ListenPacket("udp", "127.0.0.1:"+second)
|
||||
require.Error(t, err, "the new address should be bound by the DNS responder")
|
||||
|
||||
ds.Stop()
|
||||
}
|
||||
|
||||
// A listener that dies on its own must release the slot, or a later same-addr reload sees it as running and no-ops.
|
||||
func TestDnsServer_Start_bindFailure_releasesSlot(t *testing.T) {
|
||||
port := freeUDPPort(t)
|
||||
blocker, err := net.ListenPacket("udp", "127.0.0.1:"+port)
|
||||
require.NoError(t, err)
|
||||
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
|
||||
ds.Start() // returns once the bind fails
|
||||
|
||||
ds.serverMu.Lock()
|
||||
assert.Nil(t, ds.server, "a listener that failed to bind must not stay parked in the slot")
|
||||
ds.serverMu.Unlock()
|
||||
|
||||
// With the slot released, a reload can retry once the port frees up
|
||||
require.NoError(t, blocker.Close())
|
||||
require.NoError(t, ds.reload(c, false))
|
||||
waitForBind(t, ds)
|
||||
|
||||
ds.serverMu.Lock()
|
||||
assert.NotNil(t, ds.server, "a same-addr reload should retry after a failed bind")
|
||||
ds.serverMu.Unlock()
|
||||
|
||||
ds.Stop()
|
||||
}
|
||||
|
||||
// A disable that lands while Start is between its unlocked check and the guard must not leave a listener behind.
|
||||
func TestDnsServer_Start_refusesWhenDisabledUnderLock(t *testing.T) {
|
||||
port := freeUDPPort(t)
|
||||
ds, c := newTestDnsServer(t)
|
||||
setDnsConfig(c, "127.0.0.1", port, true, true)
|
||||
require.NoError(t, ds.reload(c, true))
|
||||
require.True(t, ds.enabled.Load())
|
||||
|
||||
// Holding serverMu parks Start on the lock, the only way to land the disable in that window on purpose
|
||||
ds.serverMu.Lock()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ds.Start()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
ds.serverMu.Unlock()
|
||||
t.Fatal("Start returned early, the test never exercised the window")
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
}
|
||||
|
||||
// The disable reload's critical section. It sees nothing running, so it never calls Stop.
|
||||
ds.enabled.Store(false)
|
||||
ds.serverMu.Unlock()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatal("Start never returned")
|
||||
}
|
||||
|
||||
ds.serverMu.Lock()
|
||||
assert.Nil(t, ds.server, "Start must not install a listener a disable already cancelled")
|
||||
ds.serverMu.Unlock()
|
||||
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:"+port)
|
||||
require.NoError(t, err, "an orphaned listener is still holding the port")
|
||||
_ = pc.Close()
|
||||
}
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
//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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,577 +0,0 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"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"
|
||||
)
|
||||
|
||||
// makeHandshakePacket creates a handshake packet with the given parameters.
|
||||
func makeHandshakePacket(from, to netip.AddrPort, subtype header.MessageSubType, remoteIndex uint32, counter uint64) *udp.Packet {
|
||||
data := make([]byte, 200)
|
||||
header.Encode(data, header.Version, header.Handshake, subtype, remoteIndex, counter)
|
||||
for i := header.Len; i < len(data); i++ {
|
||||
data[i] = byte(i)
|
||||
}
|
||||
return &udp.Packet{To: to, From: from, Data: data}
|
||||
}
|
||||
|
||||
func TestHandshakeRetransmitDuplicate(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify the responder correctly handles receiving the same msg1 multiple times
|
||||
// (retransmission). The duplicate goes through CheckAndComplete -> ErrAlreadySeen
|
||||
// and the cached response is resent.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
t.Log("Trigger handshake from me to them")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
|
||||
t.Log("Grab my msg1")
|
||||
msg1 := myControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Inject msg1 into them, first time")
|
||||
theirControl.InjectUDPPacket(msg1)
|
||||
_ = theirControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Inject the SAME msg1 again, tests ErrAlreadySeen path")
|
||||
theirControl.InjectUDPPacket(msg1)
|
||||
resp2 := theirControl.GetFromUDP(true)
|
||||
assert.NotNil(t, resp2, "should get cached response on duplicate msg1")
|
||||
|
||||
t.Log("Complete handshake with cached response")
|
||||
myControl.InjectUDPPacket(resp2)
|
||||
myControl.WaitForType(1, 0, theirControl)
|
||||
|
||||
t.Log("Drain cached packet and verify tunnel works")
|
||||
cachedPacket := theirControl.GetFromTun(true)
|
||||
assertUdpPacket(t, []byte("Hi"), cachedPacket, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
t.Log("Verify only one tunnel exists on each side")
|
||||
assert.Len(t, myControl.ListHostmapHosts(false), 1)
|
||||
assert.Len(t, theirControl.ListHostmapHosts(false), 1)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeTruncatedPacketRecovery(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that a truncated handshake packet is ignored and the real
|
||||
// packet can still complete the handshake.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
t.Log("Trigger handshake")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
|
||||
t.Log("Get msg1 and deliver to responder")
|
||||
msg1 := myControl.GetFromUDP(true)
|
||||
theirControl.InjectUDPPacket(msg1)
|
||||
|
||||
t.Log("Get the real response")
|
||||
realResp := theirControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Truncate the response and inject, should be ignored")
|
||||
truncResp := realResp.Copy()
|
||||
truncResp.Data = truncResp.Data[:header.Len]
|
||||
myControl.InjectUDPPacket(truncResp)
|
||||
|
||||
t.Log("Verify pending handshake survived the truncated packet")
|
||||
assert.NotEmpty(t, myControl.ListHostmapHosts(true), "pending handshake should still exist")
|
||||
|
||||
t.Log("Inject real response, should complete handshake")
|
||||
myControl.InjectUDPPacket(realResp)
|
||||
myControl.WaitForType(1, 0, theirControl)
|
||||
|
||||
t.Log("Drain and verify tunnel")
|
||||
cachedPacket := theirControl.GetFromTun(true)
|
||||
assertUdpPacket(t, []byte("Hi"), cachedPacket, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeOrphanedMsg2Dropped(t *testing.T) {
|
||||
t.Parallel()
|
||||
// A msg2 arriving with no matching pending index should be silently dropped
|
||||
// with no response sent and no state changes.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
t.Log("Complete a normal handshake")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
r.RouteForAllUntilTxTun(theirControl)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
t.Log("Record hostmap state")
|
||||
myIndexes := len(myControl.ListHostmapIndexes(false))
|
||||
|
||||
t.Log("Inject a fake msg2 with unknown RemoteIndex")
|
||||
myControl.InjectUDPPacket(makeHandshakePacket(theirUdpAddr, myUdpAddr, header.HandshakeIXPSK0, 0xDEADBEEF, 2))
|
||||
|
||||
t.Log("Verify no new indexes created")
|
||||
assert.Equal(t, myIndexes, len(myControl.ListHostmapIndexes(false)))
|
||||
|
||||
t.Log("Verify no UDP response was sent")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.Nil(t, myControl.GetFromUDP(false), "should not send a response to orphaned msg2")
|
||||
|
||||
t.Log("Verify existing tunnel still works")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeUnknownMessageCounter(t *testing.T) {
|
||||
t.Parallel()
|
||||
// A handshake packet with an unexpected message counter should be silently
|
||||
// dropped with no side effects and no UDP response.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, _, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Inject handshake with MessageCounter=3")
|
||||
myControl.InjectUDPPacket(makeHandshakePacket(theirUdpAddr, myUdpAddr, header.HandshakeIXPSK0, 0, 3))
|
||||
|
||||
t.Log("Inject handshake with MessageCounter=99")
|
||||
myControl.InjectUDPPacket(makeHandshakePacket(theirUdpAddr, myUdpAddr, header.HandshakeIXPSK0, 0, 99))
|
||||
|
||||
t.Log("Verify no tunnels or pending handshakes")
|
||||
assert.Empty(t, myControl.ListHostmapHosts(false))
|
||||
assert.Empty(t, myControl.ListHostmapHosts(true))
|
||||
|
||||
t.Log("Verify no UDP response was sent")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.Nil(t, myControl.GetFromUDP(false))
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeUnknownSubtype(t *testing.T) {
|
||||
t.Parallel()
|
||||
// A handshake packet with an unknown subtype should be silently dropped.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, _, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, _, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Inject handshake with unknown subtype 99")
|
||||
myControl.InjectUDPPacket(makeHandshakePacket(theirUdpAddr, myUdpAddr, header.MessageSubType(99), 0, 1))
|
||||
|
||||
t.Log("Verify no tunnels or pending handshakes")
|
||||
assert.Empty(t, myControl.ListHostmapHosts(false))
|
||||
assert.Empty(t, myControl.ListHostmapHosts(true))
|
||||
|
||||
t.Log("Verify no UDP response was sent")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.Nil(t, myControl.GetFromUDP(false))
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeLateResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
// After a handshake times out, a late response should be silently ignored
|
||||
// with no new tunnels created.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", m{
|
||||
"handshakes": m{
|
||||
"try_interval": "200ms",
|
||||
"retries": 2,
|
||||
},
|
||||
})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger handshake from me")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
|
||||
t.Log("Grab msg1 but don't deliver")
|
||||
msg1 := myControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Wait for handshake to time out")
|
||||
for i := 0; i < 5; i++ {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
myControl.GetFromUDP(false)
|
||||
}
|
||||
|
||||
t.Log("Confirm no pending handshakes remain")
|
||||
assert.Empty(t, myControl.ListHostmapHosts(true))
|
||||
|
||||
t.Log("Deliver old msg1 to them, they create a tunnel")
|
||||
theirControl.InjectUDPPacket(msg1)
|
||||
resp := theirControl.GetFromUDP(true)
|
||||
assert.NotNil(t, resp)
|
||||
|
||||
t.Log("Inject late response into me, should be ignored")
|
||||
myControl.InjectUDPPacket(resp)
|
||||
|
||||
t.Log("No tunnel should exist on my side")
|
||||
assert.Empty(t, myControl.ListHostmapHosts(false))
|
||||
assert.Empty(t, myControl.ListHostmapHosts(true))
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeSelfConnectionRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that a node rejects a handshake containing its own VPN IP in the
|
||||
// peer cert. We do this by sending the initiator's own msg1 back to itself.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
|
||||
// Need a lighthouse entry to trigger a handshake
|
||||
myControl.InjectLightHouseAddr(netip.MustParseAddr("10.128.0.2"), netip.MustParseAddrPort("10.0.0.2:4242"))
|
||||
|
||||
myControl.Start()
|
||||
|
||||
t.Log("Trigger handshake from me")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(netip.MustParseAddr("10.128.0.2"), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
msg1 := myControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Drain any handshake retransmits before injecting")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
for myControl.GetFromUDP(false) != nil {
|
||||
}
|
||||
|
||||
t.Log("Feed my own msg1 back to me as if it came from someone else")
|
||||
selfMsg := msg1.Copy()
|
||||
selfMsg.From = netip.MustParseAddrPort("10.0.0.99:4242")
|
||||
selfMsg.To = myUdpAddr
|
||||
myControl.InjectUDPPacket(selfMsg)
|
||||
|
||||
t.Log("Verify no response was sent (self-connection rejected)")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Drain any further retransmits from the original handshake, then check
|
||||
// that none of them are a handshake response (MessageCounter=2)
|
||||
h := &header.H{}
|
||||
for {
|
||||
p := myControl.GetFromUDP(false)
|
||||
if p == nil {
|
||||
break
|
||||
}
|
||||
_ = h.Parse(p.Data)
|
||||
assert.NotEqual(t, uint64(2), h.MessageCounter,
|
||||
"should not send a stage 2 response to self-connection")
|
||||
}
|
||||
|
||||
t.Log("Verify no tunnel to myself was created")
|
||||
assert.Nil(t, myControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false))
|
||||
|
||||
myControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeMessageCounter0Dropped(t *testing.T) {
|
||||
t.Parallel()
|
||||
// MessageCounter=0 is not a valid handshake message and should be dropped.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, _, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
_, _, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.Start()
|
||||
|
||||
t.Log("Inject handshake with MessageCounter=0")
|
||||
myControl.InjectUDPPacket(makeHandshakePacket(theirUdpAddr, myUdpAddr, header.HandshakeIXPSK0, 0, 0))
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.Empty(t, myControl.ListHostmapHosts(false))
|
||||
assert.Empty(t, myControl.ListHostmapHosts(true))
|
||||
assert.Nil(t, myControl.GetFromUDP(false))
|
||||
|
||||
myControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeRemoteAllowList(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that a handshake from a blocked underlay IP is dropped with no
|
||||
// response and no state changes. Then verify the same packet from an
|
||||
// allowed IP succeeds.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", m{
|
||||
"lighthouse": m{
|
||||
"remote_allow_list": m{
|
||||
"10.0.0.0/8": true,
|
||||
"0.0.0.0/0": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
t.Log("Trigger handshake from them")
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
msg1 := theirControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Rewrite the source to a blocked IP and inject")
|
||||
blockedMsg := msg1.Copy()
|
||||
blockedMsg.From = netip.MustParseAddrPort("192.168.1.1:4242")
|
||||
myControl.InjectUDPPacket(blockedMsg)
|
||||
|
||||
t.Log("Verify no tunnel, no pending, no response from blocked source")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
assert.Empty(t, myControl.ListHostmapHosts(false))
|
||||
assert.Empty(t, myControl.ListHostmapHosts(true))
|
||||
assert.Nil(t, myControl.GetFromUDP(false), "should not respond to blocked source")
|
||||
|
||||
t.Log("Now inject the real packet from the allowed source")
|
||||
myControl.InjectUDPPacket(msg1)
|
||||
|
||||
t.Log("Verify handshake completes from allowed source")
|
||||
resp := myControl.GetFromUDP(true)
|
||||
assert.NotNil(t, resp)
|
||||
theirControl.InjectUDPPacket(resp)
|
||||
theirControl.WaitForType(1, 0, myControl)
|
||||
|
||||
t.Log("Drain cached packet and verify tunnel works")
|
||||
cachedPacket := myControl.GetFromTun(true)
|
||||
assertUdpPacket(t, []byte("Hi"), cachedPacket, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeAlreadySeenPreferredRemote(t *testing.T) {
|
||||
t.Parallel()
|
||||
// When a duplicate msg1 arrives via ErrAlreadySeen, verify the tunnel
|
||||
// remains functional and hostmap index count is stable.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
t.Log("Complete a normal handshake via the router")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi")))
|
||||
r.RouteForAllUntilTxTun(theirControl)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
t.Log("Record hostmap state")
|
||||
theirIndexes := len(theirControl.ListHostmapIndexes(false))
|
||||
hi := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
assert.NotNil(t, hi)
|
||||
originalRemote := hi.CurrentRemote
|
||||
|
||||
t.Log("Re-trigger traffic to cause a new handshake attempt (ErrAlreadySeen)")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("roam")))
|
||||
r.RouteForAllUntilTxTun(theirControl)
|
||||
|
||||
t.Log("Verify tunnel still works")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
t.Log("Verify remote is still valid and index count is stable")
|
||||
hi2 := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
assert.NotNil(t, hi2)
|
||||
assert.Equal(t, originalRemote, hi2.CurrentRemote)
|
||||
assert.Equal(t, theirIndexes, len(theirControl.ListHostmapIndexes(false)),
|
||||
"no extra indexes should be created from ErrAlreadySeen")
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeWrongResponderPacketStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that when the wrong host responds, the cached packets are
|
||||
// transferred to the new handshake, the evil tunnel is closed, evil's
|
||||
// address is blocked, and the correct tunnel is eventually established.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.100/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.99/24", nil)
|
||||
evilControl, evilVpnIpNet, evilUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "evil", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), evilUdpAddr)
|
||||
|
||||
r := router.NewR(t, myControl, theirControl, evilControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
evilControl.Start()
|
||||
|
||||
t.Log("Send multiple packets to them (cached during handshake)")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("packet1")))
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("packet2")))
|
||||
|
||||
t.Log("Route until evil tunnel is closed")
|
||||
h := &header.H{}
|
||||
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||
if err := h.Parse(p.Data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if h.Type == header.CloseTunnel && p.To == evilUdpAddr {
|
||||
return router.RouteAndExit
|
||||
}
|
||||
return router.KeepRouting
|
||||
})
|
||||
|
||||
t.Log("Verify evil's address is blocked in the new pending handshake")
|
||||
pendingHI := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), true)
|
||||
if pendingHI != nil {
|
||||
assert.NotContains(t, pendingHI.RemoteAddrs, evilUdpAddr,
|
||||
"evil's address should be blocked")
|
||||
}
|
||||
|
||||
t.Log("Inject correct lighthouse addr for them")
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
t.Log("Route until cached packets arrive at the real them")
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
assert.NotNil(t, p, "a cached packet should be delivered to the correct host")
|
||||
|
||||
t.Log("Verify the correct host has a tunnel")
|
||||
assertHostInfoPair(t, myUdpAddr, theirUdpAddr, myVpnIpNet, theirVpnIpNet, myControl, theirControl)
|
||||
|
||||
t.Log("Verify no hostinfo artifacts from evil remain")
|
||||
assert.Nil(t, myControl.GetHostInfoByVpnAddr(evilVpnIpNet[0].Addr(), true),
|
||||
"no pending hostinfo for evil")
|
||||
assert.Nil(t, myControl.GetHostInfoByVpnAddr(evilVpnIpNet[0].Addr(), false),
|
||||
"no main hostinfo for evil")
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
evilControl.Stop()
|
||||
}
|
||||
|
||||
func TestHandshakeRelayComplete(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that a relay handshake completes correctly and relay state is
|
||||
// properly maintained on all three nodes.
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
||||
|
||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
r := router.NewR(t, myControl, relayControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
myControl.Start()
|
||||
relayControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger handshake via relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi via relay")))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
assertUdpPacket(t, []byte("Hi via relay"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
|
||||
t.Log("Verify bidirectional tunnel via relay")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
t.Log("Verify relay state on my side shows relay-to-me")
|
||||
myHI := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
||||
assert.NotNil(t, myHI)
|
||||
assert.NotEmpty(t, myHI.CurrentRelaysToMe, "should have relay-to-me for them")
|
||||
|
||||
t.Log("Verify relay state on their side shows relay-to-me")
|
||||
theirHI := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
assert.NotNil(t, theirHI)
|
||||
assert.NotEmpty(t, theirHI.CurrentRelaysToMe, "should have relay-to-me for me")
|
||||
|
||||
t.Log("Verify relay node shows through-me relays")
|
||||
relayHI := relayControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
assert.NotNil(t, relayHI)
|
||||
|
||||
myControl.Stop()
|
||||
relayControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
// NOTE: Relay V1 cert + IPv6 rejection is not tested here because
|
||||
// BuildTunUDPPacket from a V4 node to a V6 address panics in the test
|
||||
// framework. The check is in handshake_manager.go handleOutbound relay
|
||||
// logic (lines ~304-313): if the relay host has a V1 cert and either
|
||||
// address is IPv6, the relay is skipped.
|
||||
|
||||
// NOTE: Relay reestablishment (Disestablished state transition) is covered
|
||||
// by the existing TestReestablishRelays in handshakes_test.go.
|
||||
+74
-525
@@ -11,26 +11,25 @@ import (
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/sirupsen/logrus"
|
||||
"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/overlay"
|
||||
"github.com/slackhq/nebula/udp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func BenchmarkHotPath(b *testing.B) {
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
// Put their info in our lighthouse
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
@@ -39,68 +38,16 @@ func BenchmarkHotPath(b *testing.B) {
|
||||
r := router.NewR(b, myControl, theirControl)
|
||||
r.CancelFlowLogs()
|
||||
|
||||
assertTunnel(b, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
// Pre-build the IP packet bytes once so the bench measures the data plane,
|
||||
// not gopacket SerializeLayers overhead.
|
||||
prebuilt := BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
// EnableFanIn switches the router to a 0-alloc routing path. Required
|
||||
// for hot-path benchmarks; would conflict with GetFromUDP-using tests.
|
||||
r.EnableFanIn()
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
myControl.InjectTunPacket(prebuilt)
|
||||
// Release the TUN-side bytes back to the harness freelist; the bench
|
||||
// just confirms a packet arrived, the contents aren't inspected.
|
||||
overlay.ReleaseTunBuf(r.RouteForAllUntilTxTun(theirControl))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
_ = r.RouteForAllUntilTxTun(theirControl)
|
||||
}
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func BenchmarkHotPathRelay(b *testing.B) {
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
||||
|
||||
// Teach my how to get to the relay and that their can be reached via the relay
|
||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(b, myControl, relayControl, theirControl)
|
||||
r.CancelFlowLogs()
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
relayControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
assertTunnel(b, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
|
||||
prebuilt := BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
r.EnableFanIn()
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
myControl.InjectTunPacket(prebuilt)
|
||||
overlay.ReleaseTunBuf(r.RouteForAllUntilTxTun(theirControl))
|
||||
}
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
relayControl.Stop()
|
||||
}
|
||||
|
||||
func TestGoodHandshake(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{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
@@ -113,7 +60,7 @@ func TestGoodHandshake(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
t.Log("Have them consume my stage 0 packet. They have a tunnel now")
|
||||
theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
|
||||
@@ -150,44 +97,7 @@ func TestGoodHandshake(t *testing.T) {
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestGoodHandshakeNoOverlap(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, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "2001::69/24", nil) //look ma, cross-stack!
|
||||
|
||||
// Put their info in our lighthouse
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
empty := []byte{}
|
||||
t.Log("do something to cause a handshake")
|
||||
myControl.GetF().SendMessageToVpnAddr(header.Test, header.MessageNone, theirVpnIpNet[0].Addr(), empty, empty, empty)
|
||||
|
||||
t.Log("Have them consume my stage 0 packet. They have a tunnel now")
|
||||
theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
|
||||
|
||||
t.Log("Get their stage 1 packet")
|
||||
stage1Packet := theirControl.GetFromUDP(true)
|
||||
|
||||
t.Log("Have me consume their stage 1 packet. I have a tunnel now")
|
||||
myControl.InjectUDPPacket(stage1Packet)
|
||||
|
||||
t.Log("Wait until we see a test packet come through to make sure we give the tunnel time to complete")
|
||||
myControl.WaitForType(header.Test, 0, theirControl)
|
||||
|
||||
t.Log("Make sure our host infos are correct")
|
||||
assertHostInfoPair(t, myUdpAddr, theirUdpAddr, myVpnIpNet, theirVpnIpNet, myControl, theirControl)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestWrongResponderHandshake(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{})
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.100/24", nil)
|
||||
@@ -207,7 +117,7 @@ func TestWrongResponderHandshake(t *testing.T) {
|
||||
evilControl.Start()
|
||||
|
||||
t.Log("Start the handshake process, we will route until we see the evil tunnel closed")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
h := &header.H{}
|
||||
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||
@@ -264,7 +174,6 @@ func TestWrongResponderHandshake(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWrongResponderHandshakeStaticHostMap(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{})
|
||||
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.99/24", nil)
|
||||
@@ -289,7 +198,7 @@ func TestWrongResponderHandshakeStaticHostMap(t *testing.T) {
|
||||
evilControl.Start()
|
||||
|
||||
t.Log("Start the handshake process, we will route until we see the evil tunnel closed")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
h := &header.H{}
|
||||
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||
@@ -347,7 +256,6 @@ func TestWrongResponderHandshakeStaticHostMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStage1Race(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This tests ensures that two hosts handshaking with each other at the same time will allow traffic to flow
|
||||
// But will eventually collapse down to a single tunnel
|
||||
|
||||
@@ -368,8 +276,8 @@ func TestStage1Race(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake to start on both me and them")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||
|
||||
t.Log("Get both stage 1 handshake packets")
|
||||
myHsForThem := myControl.GetFromUDP(true)
|
||||
@@ -405,7 +313,7 @@ func TestStage1Race(t *testing.T) {
|
||||
|
||||
r.Log("Spin until connection manager tears down a tunnel")
|
||||
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -428,7 +336,6 @@ func TestStage1Race(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUncleanShutdownRaceLoser(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{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
@@ -446,20 +353,18 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
r.Log("Trigger a handshake from me to them")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
|
||||
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")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me again"))
|
||||
p = r.RouteForAllUntilTxTun(theirControl)
|
||||
assertUdpPacket(t, []byte("Hi from me again"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
|
||||
@@ -467,10 +372,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 := theirControl.GetHostmapIndexCount()
|
||||
start := len(theirControl.GetHostmap().Indexes)
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
if theirControl.GetHostmapIndexCount() < start {
|
||||
if len(theirControl.GetHostmap().Indexes) < start {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -480,7 +385,6 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUncleanShutdownRaceWinner(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{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
@@ -498,7 +402,7 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
r.Log("Trigger a handshake from me to them")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
@@ -506,13 +410,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")))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them again"))
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
assertUdpPacket(t, []byte("Hi from them again"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
|
||||
r.RenderHostmaps("Derp hostmaps", myControl, theirControl)
|
||||
@@ -521,10 +423,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 := myControl.GetHostmapIndexCount()
|
||||
start := len(myControl.GetHostmap().Indexes)
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
if myControl.GetHostmapIndexCount() < start {
|
||||
if len(myControl.GetHostmap().Indexes) < start {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
@@ -534,7 +436,6 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRelays(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{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
@@ -555,37 +456,7 @@ func TestRelays(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
|
||||
}
|
||||
|
||||
func TestRelaysDontCareAboutIps(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", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "relay ", "2001::9999/24", m{"relay": m{"am_relay": true}})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
||||
|
||||
// Teach my how to get to the relay and that their can be reached via the relay
|
||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(t, myControl, relayControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
relayControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
@@ -594,7 +465,6 @@ func TestRelaysDontCareAboutIps(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReestablishRelays(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{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
@@ -615,14 +485,14 @@ func TestReestablishRelays(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
|
||||
t.Log("Ensure packet traversal from them to me via the relay")
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
@@ -632,12 +502,12 @@ func TestReestablishRelays(t *testing.T) {
|
||||
r.Log("Close the tunnel")
|
||||
relayControl.CloseTunnel(theirVpnIpNet[0].Addr(), true)
|
||||
|
||||
start := myControl.GetHostmapIndexCount()
|
||||
curIndexes := myControl.GetHostmapIndexCount()
|
||||
start := len(myControl.GetHostmap().Indexes)
|
||||
curIndexes := len(myControl.GetHostmap().Indexes)
|
||||
for curIndexes >= start {
|
||||
curIndexes = myControl.GetHostmapIndexCount()
|
||||
curIndexes = len(myControl.GetHostmap().Indexes)
|
||||
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")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me should fail"))
|
||||
|
||||
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||
return router.RouteAndExit
|
||||
@@ -654,7 +524,7 @@ func TestReestablishRelays(t *testing.T) {
|
||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p = r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
@@ -689,7 +559,7 @@ func TestReestablishRelays(t *testing.T) {
|
||||
t.Log("Assert the tunnel works the other way, too")
|
||||
for {
|
||||
t.Log("RouteForAllUntilTxTun")
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
@@ -725,72 +595,7 @@ func TestReestablishRelays(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestRelayHandshakeOverDisestablishedEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
// If them tears down the tunnel while me keeps Established relay state, me's next
|
||||
// handshake flows through the relay with no fresh CreateRelayRequest and lands on
|
||||
// them's Disestablished terminal relay entry. them must re-establish that entry, or
|
||||
// its first transmit deletes its only relay and the tunnel is born transmit-dead:
|
||||
// them can receive but every send is silently dropped.
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
||||
|
||||
// Teach my how to get to the relay and that their can be reached via the relay
|
||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(t, myControl, relayControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
relayControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||
oldIdx := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false).LocalIndex
|
||||
|
||||
t.Log("Close the tunnel on them only, marking their relay entry Disestablished")
|
||||
theirControl.CloseTunnel(myVpnIpNet[0].Addr(), true)
|
||||
|
||||
t.Log("Re-handshake from me, riding the still-Established relay state")
|
||||
myControl.ReHandshake(theirVpnIpNet[0].Addr())
|
||||
for {
|
||||
h := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
||||
if h != nil && h.LocalIndex != oldIdx && h.RemoteIndex != 0 {
|
||||
break
|
||||
}
|
||||
r.RouteForAllExitFunc(func(*udp.Packet, *nebula.Control) router.ExitType {
|
||||
return router.RouteAndExit
|
||||
})
|
||||
}
|
||||
|
||||
hAtThem := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
require.NotNil(t, hAtThem, "them should have completed the relayed handshake")
|
||||
require.Equal(t, []netip.Addr{relayVpnIpNet[0].Addr()}, hAtThem.CurrentRelaysToMe, "them should know a relay for the new tunnel")
|
||||
|
||||
t.Log("Send from them to me; their only relay entry must survive the transmit")
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
require.Never(t, func() bool {
|
||||
h := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
return h == nil || len(h.CurrentRelaysToMe) == 0
|
||||
}, time.Second, 10*time.Millisecond, "them deleted its only relay entry; the tunnel is permanently transmit-dead")
|
||||
|
||||
p = r.RouteForAllUntilTxTun(myControl)
|
||||
assertUdpPacket(t, []byte("Hi from them"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
|
||||
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
|
||||
}
|
||||
|
||||
func TestStage1RaceRelays(t *testing.T) {
|
||||
t.Parallel()
|
||||
//NOTE: this is a race between me and relay resulting in a full tunnel from me to them via relay
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
@@ -823,8 +628,8 @@ func TestStage1RaceRelays(t *testing.T) {
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), theirControl, relayControl, r)
|
||||
|
||||
r.Log("Trigger a handshake from both them and me via relay to them and me")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||
|
||||
r.Log("Wait for a packet from them to me")
|
||||
p := r.RouteForAllUntilTxTun(myControl)
|
||||
@@ -838,12 +643,12 @@ func TestStage1RaceRelays(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStage1RaceRelays2(t *testing.T) {
|
||||
t.Parallel()
|
||||
//NOTE: this is a race between me and relay resulting in a full tunnel from me to them via relay
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
||||
l := NewTestLogger()
|
||||
|
||||
// Teach my how to get to the relay and that their can be reached via the relay
|
||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||
@@ -865,41 +670,49 @@ func TestStage1RaceRelays2(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
r.Log("Get a tunnel between me and relay")
|
||||
l.Info("Get a tunnel between me and relay")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), myControl, relayControl, r)
|
||||
|
||||
r.Log("Get a tunnel between them and relay")
|
||||
l.Info("Get a tunnel between them and relay")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), theirControl, relayControl, r)
|
||||
|
||||
r.Log("Trigger a handshake from both them and me via relay to them and me")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
l.Info("Trigger a handshake from both them and me via relay to them and me")
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||
|
||||
//r.RouteUntilAfterMsgType(myControl, header.Control, header.MessageNone)
|
||||
//r.RouteUntilAfterMsgType(theirControl, header.Control, header.MessageNone)
|
||||
|
||||
r.Log("Wait for a packet from them to me; myControl")
|
||||
r.Log("Wait for a packet from them to me")
|
||||
l.Info("Wait for a packet from them to me; myControl")
|
||||
r.RouteForAllUntilTxTun(myControl)
|
||||
r.Log("Wait for a packet from them to me; theirControl")
|
||||
l.Info("Wait for a packet from them to me; theirControl")
|
||||
r.RouteForAllUntilTxTun(theirControl)
|
||||
|
||||
r.Log("Assert the tunnel works")
|
||||
l.Info("Assert the tunnel works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
|
||||
t.Log("Wait until we remove extra tunnels")
|
||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
||||
myControl.GetHostmapIndexCount(),
|
||||
theirControl.GetHostmapIndexCount(),
|
||||
relayControl.GetHostmapIndexCount(),
|
||||
)
|
||||
hostInfos := myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
||||
l.Info("Wait until we remove extra tunnels")
|
||||
l.WithFields(
|
||||
logrus.Fields{
|
||||
"myControl": len(myControl.GetHostmap().Indexes),
|
||||
"theirControl": len(theirControl.GetHostmap().Indexes),
|
||||
"relayControl": len(relayControl.GetHostmap().Indexes),
|
||||
}).Info("Waiting for hostinfos to be removed...")
|
||||
hostInfos := len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||
retries := 60
|
||||
for hostInfos > 6 && retries > 0 {
|
||||
hostInfos = myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
||||
myControl.GetHostmapIndexCount(),
|
||||
theirControl.GetHostmapIndexCount(),
|
||||
relayControl.GetHostmapIndexCount(),
|
||||
)
|
||||
hostInfos = len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||
l.WithFields(
|
||||
logrus.Fields{
|
||||
"myControl": len(myControl.GetHostmap().Indexes),
|
||||
"theirControl": len(theirControl.GetHostmap().Indexes),
|
||||
"relayControl": len(relayControl.GetHostmap().Indexes),
|
||||
}).Info("Waiting for hostinfos to be removed...")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -907,6 +720,7 @@ func TestStage1RaceRelays2(t *testing.T) {
|
||||
}
|
||||
|
||||
r.Log("Assert the tunnel works")
|
||||
l.Info("Assert the tunnel works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
|
||||
myControl.Stop()
|
||||
@@ -915,7 +729,6 @@ func TestStage1RaceRelays2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRehandshakingRelays(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{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||
relayControl, relayVpnIpNet, relayUdpAddr, relayConfig := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||
@@ -936,7 +749,7 @@ func TestRehandshakingRelays(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
@@ -992,24 +805,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 myControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
||||
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))
|
||||
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 theirControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
||||
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))
|
||||
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 relayControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
||||
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))
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
@@ -1019,7 +832,6 @@ func TestRehandshakingRelays(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRehandshakingRelaysPrimary(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This test is the same as TestRehandshakingRelays but one of the terminal types is a primary swap winner
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.128/24", m{"relay": m{"use_relays": true}})
|
||||
@@ -1041,7 +853,7 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
@@ -1097,24 +909,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 myControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
||||
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))
|
||||
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 theirControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
||||
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))
|
||||
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 relayControl.GetHostmapIndexCount() != 2 {
|
||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
||||
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))
|
||||
r.Log("Assert the relay tunnel still works")
|
||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
r.Log("yupitdoes")
|
||||
@@ -1124,7 +936,6 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRehandshaking(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{})
|
||||
myControl, myVpnIpNet, myUdpAddr, myConfig := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.2/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, theirConfig := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.1/24", nil)
|
||||
@@ -1191,7 +1002,7 @@ func TestRehandshaking(t *testing.T) {
|
||||
theirConfig.ReloadConfigString(string(rc))
|
||||
|
||||
r.Log("Spin until there is only 1 tunnel")
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -1220,7 +1031,6 @@ func TestRehandshaking(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRehandshakingLoser(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The purpose of this test is that the race loser renews their certificate and rehandshakes. The final tunnel
|
||||
// Should be the one with the new certificate
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
@@ -1291,7 +1101,7 @@ func TestRehandshakingLoser(t *testing.T) {
|
||||
myConfig.ReloadConfigString(string(rc))
|
||||
|
||||
r.Log("Spin until there is only 1 tunnel")
|
||||
for myControl.GetHostmapIndexCount()+theirControl.GetHostmapIndexCount() > 2 {
|
||||
for len(myControl.GetHostmap().Indexes)+len(theirControl.GetHostmap().Indexes) > 2 {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
t.Log("Connection manager hasn't ticked yet")
|
||||
time.Sleep(time.Second)
|
||||
@@ -1319,7 +1129,6 @@ func TestRehandshakingLoser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRaceRegression(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This test forces stage 1, stage 2, stage 1 to be received by me from them
|
||||
// We had a bug where we were not finding the duplicate handshake and responding to the final stage 1 which
|
||||
// caused a cross-linked hostinfo
|
||||
@@ -1343,8 +1152,8 @@ func TestRaceRegression(t *testing.T) {
|
||||
//them rx stage:2 initiatorIndex=120607833 responderIndex=4209862089
|
||||
|
||||
t.Log("Start both handshakes")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them")))
|
||||
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||
|
||||
t.Log("Get both stage 1")
|
||||
myStage1ForThem := myControl.GetFromUDP(true)
|
||||
@@ -1380,7 +1189,6 @@ func TestRaceRegression(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestV2NonPrimaryWithLighthouse(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{})
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh ", "10.128.0.1/24, ff::1/64", m{"lighthouse": m{"am_lighthouse": true}})
|
||||
|
||||
@@ -1419,262 +1227,3 @@ func TestV2NonPrimaryWithLighthouse(t *testing.T) {
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestV2NonPrimaryWithOffNetLighthouse(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{})
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh ", "2001::1/64", m{"lighthouse": m{"am_lighthouse": true}})
|
||||
|
||||
o := m{
|
||||
"static_host_map": m{
|
||||
lhVpnIpNet[0].Addr().String(): []string{lhUdpAddr.String()},
|
||||
},
|
||||
"lighthouse": m{
|
||||
"hosts": []string{lhVpnIpNet[0].Addr().String()},
|
||||
"local_allow_list": m{
|
||||
// Try and block our lighthouse updates from using the actual addresses assigned to this computer
|
||||
// If we start discovering addresses the test router doesn't know about then test traffic cant flow
|
||||
"10.0.0.0/24": true,
|
||||
"::/0": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version2, ca, caKey, "me ", "10.128.0.2/24, ff::2/64", o)
|
||||
theirControl, theirVpnIpNet, _, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24, ff::3/64", o)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(t, lhControl, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
// Start the servers
|
||||
lhControl.Start()
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Stand up an ipv6 tunnel between me and them")
|
||||
assert.True(t, myVpnIpNet[1].Addr().Is6())
|
||||
assert.True(t, theirVpnIpNet[1].Addr().Is6())
|
||||
assertTunnel(t, myVpnIpNet[1].Addr(), theirVpnIpNet[1].Addr(), myControl, theirControl, r)
|
||||
|
||||
lhControl.Stop()
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestLighthouseUpdateOnReload(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{})
|
||||
|
||||
// Create the lighthouse
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{"lighthouse": m{"am_lighthouse": true}})
|
||||
|
||||
// Create a client with NO lighthouse configured and a long update interval.
|
||||
// The initial SendUpdate at startup will be a no-op since no lighthouses are known.
|
||||
myControl, myVpnIpNet, _, myConfig := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", m{
|
||||
"lighthouse": m{
|
||||
"interval": 600,
|
||||
"local_allow_list": m{
|
||||
"10.0.0.0/24": true,
|
||||
"::/0": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
r := router.NewR(t, lhControl, myControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
lhControl.Start()
|
||||
myControl.Start()
|
||||
|
||||
// Drain any startup packets (there should be none meaningful)
|
||||
r.FlushAll()
|
||||
|
||||
// Verify lighthouse has no knowledge of the client
|
||||
assert.Nil(t, lhControl.QueryLighthouse(myVpnIpNet[0].Addr()))
|
||||
|
||||
// Build a new config that adds the lighthouse
|
||||
newSettings := make(m)
|
||||
for k, v := range myConfig.Settings {
|
||||
newSettings[k] = v
|
||||
}
|
||||
newSettings["static_host_map"] = m{
|
||||
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||
}
|
||||
newSettings["lighthouse"] = m{
|
||||
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||
"interval": 600,
|
||||
"local_allow_list": m{
|
||||
"10.0.0.0/24": true,
|
||||
"::/0": false,
|
||||
},
|
||||
}
|
||||
newCfg, err := yaml.Marshal(newSettings)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reload the config. The lighthouse.hosts change triggers TriggerUpdate,
|
||||
// which wakes the update worker. It calls SendUpdate, initiating a
|
||||
// handshake to the new lighthouse and caching the HostUpdateNotification.
|
||||
require.NoError(t, myConfig.ReloadConfigString(string(newCfg)))
|
||||
|
||||
// Route until the lighthouse receives the HostUpdateNotification.
|
||||
// This covers: handshake stage 1, stage 2, then the cached update.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
r.RouteForAllUntilAfterMsgTypeTo(lhControl, header.LightHouse, 0)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for lighthouse update after config reload")
|
||||
}
|
||||
|
||||
// Verify lighthouse now has the client's addresses
|
||||
assert.NotNil(t, lhControl.QueryLighthouse(myVpnIpNet[0].Addr()))
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", lhControl, myControl)
|
||||
lhControl.Stop()
|
||||
myControl.Stop()
|
||||
}
|
||||
|
||||
func TestGoodHandshakeUnsafeDest(t *testing.T) {
|
||||
t.Parallel()
|
||||
unsafePrefix := "192.168.6.0/24"
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(cert.Version2, ca, caKey, "spooky", "10.128.0.2/24", netip.MustParseAddrPort("10.64.0.2:4242"), unsafePrefix, nil)
|
||||
route := m{"route": unsafePrefix, "via": theirVpnIpNet[0].Addr().String()}
|
||||
myCfg := m{
|
||||
"tun": m{
|
||||
"unsafe_routes": []m{route},
|
||||
},
|
||||
}
|
||||
myControl, myVpnIpNet, myUdpAddr, myConfig := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.1/24", myCfg)
|
||||
t.Logf("my config %v", myConfig)
|
||||
// Put their info in our lighthouse
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
|
||||
spookyDest := netip.MustParseAddr("192.168.6.4")
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(spookyDest, 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
|
||||
|
||||
t.Log("Have them consume my stage 0 packet. They have a tunnel now")
|
||||
theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
|
||||
|
||||
t.Log("Get their stage 1 packet so that we can play with it")
|
||||
stage1Packet := theirControl.GetFromUDP(true)
|
||||
|
||||
t.Log("I consume a garbage packet with a proper nebula header for our tunnel")
|
||||
// this should log a statement and get ignored, allowing the real handshake packet to complete the tunnel
|
||||
badPacket := stage1Packet.Copy()
|
||||
badPacket.Data = badPacket.Data[:len(badPacket.Data)-header.Len]
|
||||
myControl.InjectUDPPacket(badPacket)
|
||||
|
||||
t.Log("Have me consume their real stage 1 packet. I have a tunnel now")
|
||||
myControl.InjectUDPPacket(stage1Packet)
|
||||
|
||||
t.Log("Wait until we see my cached packet come through")
|
||||
myControl.WaitForType(1, 0, theirControl)
|
||||
|
||||
t.Log("Make sure our host infos are correct")
|
||||
assertHostInfoPair(t, myUdpAddr, theirUdpAddr, myVpnIpNet, theirVpnIpNet, myControl, theirControl)
|
||||
|
||||
t.Log("Get that cached packet and make sure it looks right")
|
||||
myCachedPacket := theirControl.GetFromTun(true)
|
||||
assertUdpPacket(t, []byte("Hi from me"), myCachedPacket, myVpnIpNet[0].Addr(), spookyDest, 80, 80)
|
||||
|
||||
//reply
|
||||
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, spookyDest, 80, []byte("Hi from the spookyman")))
|
||||
//wait for reply
|
||||
theirControl.WaitForType(1, 0, myControl)
|
||||
theirCachedPacket := myControl.GetFromTun(true)
|
||||
assertUdpPacket(t, []byte("Hi from the spookyman"), theirCachedPacket, spookyDest, myVpnIpNet[0].Addr(), 80, 80)
|
||||
|
||||
t.Log("Do a bidirectional tunnel test")
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
|
||||
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()
|
||||
}
|
||||
|
||||
+34
-242
@@ -4,6 +4,7 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -11,27 +12,25 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/e2e/router"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type m = map[string]any
|
||||
|
||||
// newSimpleServer creates a nebula instance with many assumptions
|
||||
func newSimpleServer(v cert.Version, caCrt cert.Certificate, caKey []byte, name string, sVpnNetworks string, overrides m) (*nebula.Control, []netip.Prefix, netip.AddrPort, *config.C) {
|
||||
l := NewTestLogger()
|
||||
|
||||
var vpnNetworks []netip.Prefix
|
||||
for _, sn := range strings.Split(sVpnNetworks, ",") {
|
||||
vpnIpNet, err := netip.ParsePrefix(strings.TrimSpace(sn))
|
||||
@@ -57,54 +56,7 @@ func newSimpleServer(v cert.Version, caCrt cert.Certificate, caKey []byte, name
|
||||
budpIp[3] = 239
|
||||
udpAddr = netip.AddrPortFrom(netip.AddrFrom16(budpIp), 4242)
|
||||
}
|
||||
return newSimpleServerWithUdp(v, caCrt, caKey, name, sVpnNetworks, udpAddr, overrides)
|
||||
}
|
||||
|
||||
func newSimpleServerWithUdp(v cert.Version, caCrt cert.Certificate, caKey []byte, name string, sVpnNetworks string, udpAddr netip.AddrPort, overrides m) (*nebula.Control, []netip.Prefix, netip.AddrPort, *config.C) {
|
||||
return newSimpleServerWithUdpAndUnsafeNetworks(v, caCrt, caKey, name, sVpnNetworks, udpAddr, "", overrides)
|
||||
}
|
||||
|
||||
func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certificate, caKey []byte, name string, sVpnNetworks string, udpAddr netip.AddrPort, sUnsafeNetworks string, overrides m) (*nebula.Control, []netip.Prefix, netip.AddrPort, *config.C) {
|
||||
l := NewTestLogger()
|
||||
|
||||
var vpnNetworks []netip.Prefix
|
||||
for _, sn := range strings.Split(sVpnNetworks, ",") {
|
||||
vpnIpNet, err := netip.ParsePrefix(strings.TrimSpace(sn))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vpnNetworks = append(vpnNetworks, vpnIpNet)
|
||||
}
|
||||
|
||||
if len(vpnNetworks) == 0 {
|
||||
panic("no vpn networks")
|
||||
}
|
||||
|
||||
firewallInbound := []m{{
|
||||
"proto": "any",
|
||||
"port": "any",
|
||||
"host": "any",
|
||||
}}
|
||||
|
||||
var unsafeNetworks []netip.Prefix
|
||||
if sUnsafeNetworks != "" {
|
||||
firewallInbound = []m{{
|
||||
"proto": "any",
|
||||
"port": "any",
|
||||
"host": "any",
|
||||
"local_cidr": "0.0.0.0/0",
|
||||
}}
|
||||
|
||||
for _, sn := range strings.Split(sUnsafeNetworks, ",") {
|
||||
x, err := netip.ParsePrefix(strings.TrimSpace(sn))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
unsafeNetworks = append(unsafeNetworks, x)
|
||||
}
|
||||
}
|
||||
|
||||
_, _, myPrivKey, myPEM := cert_test.NewTestCert(v, cert.Curve_CURVE25519, caCrt, caKey, name, time.Now(), time.Now().Add(5*time.Minute), vpnNetworks, unsafeNetworks, []string{})
|
||||
_, _, myPrivKey, myPEM := cert_test.NewTestCert(v, cert.Curve_CURVE25519, caCrt, caKey, name, time.Now(), time.Now().Add(5*time.Minute), vpnNetworks, nil, []string{})
|
||||
|
||||
caB, err := caCrt.MarshalPEM()
|
||||
if err != nil {
|
||||
@@ -124,7 +76,11 @@ func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certific
|
||||
"port": "any",
|
||||
"host": "any",
|
||||
}},
|
||||
"inbound": firewallInbound,
|
||||
"inbound": []m{{
|
||||
"proto": "any",
|
||||
"port": "any",
|
||||
"host": "any",
|
||||
}},
|
||||
},
|
||||
//"handshakes": m{
|
||||
// "try_interval": "1s",
|
||||
@@ -134,7 +90,8 @@ func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certific
|
||||
"port": udpAddr.Port(),
|
||||
},
|
||||
"logging": m{
|
||||
"level": testLogLevelName(),
|
||||
"timestamp_format": fmt.Sprintf("%v 15:04:05.000000", name),
|
||||
"level": l.Level.String(),
|
||||
},
|
||||
"timers": m{
|
||||
"pending_deletion_interval": 2,
|
||||
@@ -172,108 +129,6 @@ func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certific
|
||||
return control, vpnNetworks, udpAddr, c
|
||||
}
|
||||
|
||||
// newServer creates a nebula instance with fewer assumptions
|
||||
func newServer(caCrt []cert.Certificate, certs []cert.Certificate, key []byte, overrides m) (*nebula.Control, []netip.Prefix, netip.AddrPort, *config.C) {
|
||||
l := NewTestLogger()
|
||||
|
||||
vpnNetworks := certs[len(certs)-1].Networks()
|
||||
|
||||
var udpAddr netip.AddrPort
|
||||
if vpnNetworks[0].Addr().Is4() {
|
||||
budpIp := vpnNetworks[0].Addr().As4()
|
||||
budpIp[1] -= 128
|
||||
udpAddr = netip.AddrPortFrom(netip.AddrFrom4(budpIp), 4242)
|
||||
} else {
|
||||
budpIp := vpnNetworks[0].Addr().As16()
|
||||
// beef for funsies
|
||||
budpIp[2] = 190
|
||||
budpIp[3] = 239
|
||||
udpAddr = netip.AddrPortFrom(netip.AddrFrom16(budpIp), 4242)
|
||||
}
|
||||
|
||||
caStr := ""
|
||||
for _, ca := range caCrt {
|
||||
x, err := ca.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
caStr += string(x)
|
||||
}
|
||||
certStr := ""
|
||||
for _, c := range certs {
|
||||
x, err := c.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
certStr += string(x)
|
||||
}
|
||||
|
||||
mc := m{
|
||||
"pki": m{
|
||||
"ca": caStr,
|
||||
"cert": certStr,
|
||||
"key": string(key),
|
||||
},
|
||||
//"tun": m{"disabled": true},
|
||||
"firewall": m{
|
||||
"outbound": []m{{
|
||||
"proto": "any",
|
||||
"port": "any",
|
||||
"host": "any",
|
||||
}},
|
||||
"inbound": []m{{
|
||||
"proto": "any",
|
||||
"port": "any",
|
||||
"host": "any",
|
||||
}},
|
||||
},
|
||||
//"handshakes": m{
|
||||
// "try_interval": "1s",
|
||||
//},
|
||||
"listen": m{
|
||||
"host": udpAddr.Addr().String(),
|
||||
"port": udpAddr.Port(),
|
||||
},
|
||||
"logging": m{
|
||||
"level": testLogLevelName(),
|
||||
},
|
||||
"timers": m{
|
||||
"pending_deletion_interval": 2,
|
||||
"connection_alive_interval": 2,
|
||||
},
|
||||
}
|
||||
|
||||
if overrides != nil {
|
||||
final := m{}
|
||||
err := mergo.Merge(&final, overrides, mergo.WithAppendSlice)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = mergo.Merge(&final, mc, mergo.WithAppendSlice)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mc = final
|
||||
}
|
||||
|
||||
cb, err := yaml.Marshal(mc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c := config.NewC(l)
|
||||
cStr := string(cb)
|
||||
c.LoadString(cStr)
|
||||
|
||||
control, err := nebula.Main(c, false, "e2e-test", l, nil)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return control, vpnNetworks, udpAddr, c
|
||||
}
|
||||
|
||||
type doneCb func()
|
||||
|
||||
func deadline(t *testing.T, seconds time.Duration) doneCb {
|
||||
@@ -292,26 +147,26 @@ func deadline(t *testing.T, seconds time.Duration) doneCb {
|
||||
}
|
||||
}
|
||||
|
||||
func assertTunnel(t testing.TB, vpnIpA, vpnIpB netip.Addr, controlA, controlB *nebula.Control, r *router.R) {
|
||||
func assertTunnel(t *testing.T, vpnIpA, vpnIpB netip.Addr, controlA, controlB *nebula.Control, r *router.R) {
|
||||
// Send a packet from them to me
|
||||
controlB.InjectTunPacket(BuildTunUDPPacket(vpnIpA, 80, vpnIpB, 90, []byte("Hi from B")))
|
||||
controlB.InjectTunUDPPacket(vpnIpA, 80, vpnIpB, 90, []byte("Hi from B"))
|
||||
bPacket := r.RouteForAllUntilTxTun(controlA)
|
||||
assertUdpPacket(t, []byte("Hi from B"), bPacket, vpnIpB, vpnIpA, 90, 80)
|
||||
|
||||
// And once more from me to them
|
||||
controlA.InjectTunPacket(BuildTunUDPPacket(vpnIpB, 80, vpnIpA, 90, []byte("Hello from A")))
|
||||
controlA.InjectTunUDPPacket(vpnIpB, 80, vpnIpA, 90, []byte("Hello from A"))
|
||||
aPacket := r.RouteForAllUntilTxTun(controlB)
|
||||
assertUdpPacket(t, []byte("Hello from A"), aPacket, vpnIpA, vpnIpB, 90, 80)
|
||||
}
|
||||
|
||||
func assertHostInfoPair(t testing.TB, addrA, addrB netip.AddrPort, vpnNetsA, vpnNetsB []netip.Prefix, controlA, controlB *nebula.Control) {
|
||||
func assertHostInfoPair(t *testing.T, addrA, addrB netip.AddrPort, vpnNetsA, vpnNetsB []netip.Prefix, controlA, controlB *nebula.Control) {
|
||||
// Get both host infos
|
||||
//TODO: CERT-V2 we may want to loop over each vpnAddr and assert all the things
|
||||
hBinA := controlA.GetHostInfoByVpnAddr(vpnNetsB[0].Addr(), false)
|
||||
require.NotNil(t, hBinA, "Host B was not found by vpnAddr in controlA")
|
||||
assert.NotNil(t, hBinA, "Host B was not found by vpnAddr in controlA")
|
||||
|
||||
hAinB := controlB.GetHostInfoByVpnAddr(vpnNetsA[0].Addr(), false)
|
||||
require.NotNil(t, hAinB, "Host A was not found by vpnAddr in controlB")
|
||||
assert.NotNil(t, hAinB, "Host A was not found by vpnAddr in controlB")
|
||||
|
||||
// Check that both vpn and real addr are correct
|
||||
assert.EqualValues(t, getAddrs(vpnNetsB), hBinA.VpnAddrs, "Host B VpnIp is wrong in control A")
|
||||
@@ -325,7 +180,7 @@ func assertHostInfoPair(t testing.TB, addrA, addrB netip.AddrPort, vpnNetsA, vpn
|
||||
assert.Equal(t, hBinA.RemoteIndex, hAinB.LocalIndex, "Host B remote index does not match host A local index")
|
||||
}
|
||||
|
||||
func assertUdpPacket(t testing.TB, expected, b []byte, fromIp, toIp netip.Addr, fromPort, toPort uint16) {
|
||||
func assertUdpPacket(t *testing.T, expected, b []byte, fromIp, toIp netip.Addr, fromPort, toPort uint16) {
|
||||
if toIp.Is6() {
|
||||
assertUdpPacket6(t, expected, b, fromIp, toIp, fromPort, toPort)
|
||||
} else {
|
||||
@@ -333,7 +188,7 @@ func assertUdpPacket(t testing.TB, expected, b []byte, fromIp, toIp netip.Addr,
|
||||
}
|
||||
}
|
||||
|
||||
func assertUdpPacket6(t testing.TB, expected, b []byte, fromIp, toIp netip.Addr, fromPort, toPort uint16) {
|
||||
func assertUdpPacket6(t *testing.T, expected, b []byte, fromIp, toIp netip.Addr, fromPort, toPort uint16) {
|
||||
packet := gopacket.NewPacket(b, layers.LayerTypeIPv6, gopacket.Lazy)
|
||||
v6 := packet.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
||||
assert.NotNil(t, v6, "No ipv6 data found")
|
||||
@@ -352,7 +207,7 @@ func assertUdpPacket6(t testing.TB, expected, b []byte, fromIp, toIp netip.Addr,
|
||||
assert.Equal(t, expected, data.Payload(), "Data was incorrect")
|
||||
}
|
||||
|
||||
func assertUdpPacket4(t testing.TB, expected, b []byte, fromIp, toIp netip.Addr, fromPort, toPort uint16) {
|
||||
func assertUdpPacket4(t *testing.T, expected, b []byte, fromIp, toIp netip.Addr, fromPort, toPort uint16) {
|
||||
packet := gopacket.NewPacket(b, layers.LayerTypeIPv4, gopacket.Lazy)
|
||||
v4 := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
|
||||
assert.NotNil(t, v4, "No ipv4 data found")
|
||||
@@ -379,87 +234,24 @@ func getAddrs(ns []netip.Prefix) []netip.Addr {
|
||||
return a
|
||||
}
|
||||
|
||||
func NewTestLogger() *slog.Logger {
|
||||
func NewTestLogger() *logrus.Logger {
|
||||
l := logrus.New()
|
||||
|
||||
v := os.Getenv("TEST_LOGS")
|
||||
if v == "" {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
l.SetOutput(io.Discard)
|
||||
l.SetLevel(logrus.PanicLevel)
|
||||
return l
|
||||
}
|
||||
|
||||
level := slog.LevelInfo
|
||||
switch v {
|
||||
case "2":
|
||||
level = slog.LevelDebug
|
||||
l.SetLevel(logrus.DebugLevel)
|
||||
case "3":
|
||||
level = logging.LevelTrace
|
||||
l.SetLevel(logrus.TraceLevel)
|
||||
default:
|
||||
l.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
|
||||
}
|
||||
|
||||
// testLogLevelName returns the level name string accepted by logging.ApplyConfig
|
||||
// for the current TEST_LOGS setting. Kept in sync with NewTestLogger.
|
||||
func testLogLevelName() string {
|
||||
switch os.Getenv("TEST_LOGS") {
|
||||
case "2":
|
||||
return "debug"
|
||||
case "3":
|
||||
return "trace"
|
||||
case "":
|
||||
return "info"
|
||||
}
|
||||
return "info"
|
||||
}
|
||||
|
||||
// BuildTunUDPPacket assembles an IP+UDP packet suitable for Control.InjectTunPacket.
|
||||
// Using UDP here because it's a simpler protocol.
|
||||
func BuildTunUDPPacket(toAddr netip.Addr, toPort uint16, fromAddr netip.Addr, fromPort uint16, data []byte) []byte {
|
||||
serialize := make([]gopacket.SerializableLayer, 0)
|
||||
var netLayer gopacket.NetworkLayer
|
||||
if toAddr.Is6() {
|
||||
if !fromAddr.Is6() {
|
||||
panic("Cant send ipv6 to ipv4")
|
||||
}
|
||||
ip := &layers.IPv6{
|
||||
Version: 6,
|
||||
NextHeader: layers.IPProtocolUDP,
|
||||
SrcIP: fromAddr.Unmap().AsSlice(),
|
||||
DstIP: toAddr.Unmap().AsSlice(),
|
||||
}
|
||||
serialize = append(serialize, ip)
|
||||
netLayer = ip
|
||||
} else {
|
||||
if !fromAddr.Is4() {
|
||||
panic("Cant send ipv4 to ipv6")
|
||||
}
|
||||
|
||||
ip := &layers.IPv4{
|
||||
Version: 4,
|
||||
TTL: 64,
|
||||
Protocol: layers.IPProtocolUDP,
|
||||
SrcIP: fromAddr.Unmap().AsSlice(),
|
||||
DstIP: toAddr.Unmap().AsSlice(),
|
||||
}
|
||||
serialize = append(serialize, ip)
|
||||
netLayer = ip
|
||||
}
|
||||
|
||||
udp := layers.UDP{
|
||||
SrcPort: layers.UDPPort(fromPort),
|
||||
DstPort: layers.UDPPort(toPort),
|
||||
}
|
||||
if err := udp.SetNetworkLayerForChecksum(netLayer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
buffer := gopacket.NewSerializeBuffer()
|
||||
opt := gopacket.SerializeOptions{
|
||||
ComputeChecksums: true,
|
||||
FixLengths: true,
|
||||
}
|
||||
|
||||
serialize = append(serialize, &udp, gopacket.Payload(data))
|
||||
if err := gopacket.SerializeLayers(buffer, opt, serialize...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/slackhq/nebula/e2e/router"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
// TestNoGoroutineLeaks brings up two nebula instances, completes a tunnel,
|
||||
// stops both, and asserts no goroutines leak past the shutdown. goleak's
|
||||
// retry mechanism gives the wg.Wait()-driven goroutines a moment to drain
|
||||
// before failing the assertion.
|
||||
//
|
||||
// Intentionally NOT t.Parallel()'d: concurrent tests would have their own
|
||||
// goroutines running and trip the assertion.
|
||||
func TestNoGoroutineLeaks(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", nil)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
r.RenderFlow()
|
||||
|
||||
// Settle period: Stop() is non-blocking; the wg-driven goroutines need
|
||||
// a moment to drain. goleak retries internally too, but a short explicit
|
||||
// settle reduces flakes when the suite is busy.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"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"
|
||||
)
|
||||
|
||||
// reportedAddrs is what the lighthouse would hand a peer asking where vpnAddr is.
|
||||
func reportedAddrs(t *testing.T, lh *nebula.Control, vpnAddr netip.Addr) []netip.AddrPort {
|
||||
t.Helper()
|
||||
cm := lh.QueryLighthouse(vpnAddr)
|
||||
if cm == nil {
|
||||
return nil
|
||||
}
|
||||
var out []netip.AddrPort
|
||||
for _, c := range *cm {
|
||||
out = append(out, c.Reported...)
|
||||
out = append(out, c.Learned...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// waitForLighthouseMsg routes until a lighthouse message lands on lh, or gives up. Reports whether one arrived.
|
||||
func waitForLighthouseMsg(t *testing.T, r *router.R, lh *nebula.Control, wait time.Duration) bool {
|
||||
t.Helper()
|
||||
h := &header.H{}
|
||||
return r.RouteForAllExitFuncOrTimeout(wait, func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||
if c != lh {
|
||||
return router.KeepRouting
|
||||
}
|
||||
// Punches are a single byte and never parse, they are just not what we are after
|
||||
if err := h.Parse(p.Data); err != nil {
|
||||
return router.KeepRouting
|
||||
}
|
||||
if h.Type == header.LightHouse {
|
||||
return router.RouteAndExit
|
||||
}
|
||||
return router.KeepRouting
|
||||
})
|
||||
}
|
||||
|
||||
// A laptop that changes networks has to tell the lighthouse promptly, otherwise the lighthouse keeps handing peers
|
||||
// the old address and their punches land nowhere. On a long lighthouse interval the only thing that closes that
|
||||
// window is the rebind, which on darwin the network change monitor drives. The e2e build compiles the monitor out,
|
||||
// so we call RebindUDPServer directly, which is the same thing the monitor does.
|
||||
func TestRebindSendsLighthouseUpdate(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{})
|
||||
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
||||
"lighthouse": m{"am_lighthouse": true},
|
||||
})
|
||||
|
||||
// 600s interval, so nothing scheduled can send an update during this test. A rebind is the only thing that can.
|
||||
myControl, _, _, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", m{
|
||||
"lighthouse": m{
|
||||
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||
"interval": 600,
|
||||
},
|
||||
"static_host_map": m{
|
||||
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||
},
|
||||
})
|
||||
|
||||
r := router.NewR(t, lhControl, myControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
lhControl.Start()
|
||||
myControl.Start()
|
||||
|
||||
// Let the startup registration finish, then clear everything it left behind
|
||||
require.True(t, waitForLighthouseMsg(t, r, lhControl, time.Second*5), "expected an initial registration")
|
||||
r.RouteFor(time.Millisecond * 400)
|
||||
|
||||
// Nothing should be talking to the lighthouse on its own now
|
||||
require.False(t, waitForLighthouseMsg(t, r, lhControl, time.Millisecond*200),
|
||||
"nothing should reach the lighthouse before the rebind")
|
||||
|
||||
myControl.RebindUDPServer()
|
||||
|
||||
assert.True(t, waitForLighthouseMsg(t, r, lhControl, time.Second*5),
|
||||
"a rebind should push an update to the lighthouse rather than waiting out the interval")
|
||||
|
||||
lhControl.Stop()
|
||||
myControl.Stop()
|
||||
}
|
||||
|
||||
// The other half of a rebind: every live tunnel requeries the lighthouse on its next send. That query is what makes
|
||||
// the lighthouse tell the peer to punch toward our new address, which is the part that actually revives a tunnel
|
||||
// whose remote NAT state died while we were on a different network.
|
||||
func TestRebindRequeriesPeersOnNextSend(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{})
|
||||
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
||||
"lighthouse": m{"am_lighthouse": true},
|
||||
})
|
||||
|
||||
lhCfg := m{
|
||||
"lighthouse": m{
|
||||
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||
"interval": 600,
|
||||
// Without this the peers advertise this machine's real addresses and then try to punch at them,
|
||||
// which the router has no route for.
|
||||
"local_allow_list": m{
|
||||
"10.0.0.0/24": true,
|
||||
"::/0": false,
|
||||
},
|
||||
},
|
||||
"static_host_map": m{
|
||||
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||
},
|
||||
}
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", lhCfg)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24", lhCfg)
|
||||
|
||||
r := router.NewR(t, lhControl, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
lhControl.Start()
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
r.RouteFor(time.Millisecond * 500)
|
||||
|
||||
// Point the peers at each other directly, this test is about the rebind and not about lighthouse discovery
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("initial")))
|
||||
r.RouteFor(time.Second)
|
||||
require.NotNil(t, myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false), "expected a tunnel to them")
|
||||
r.RouteFor(time.Millisecond * 300)
|
||||
|
||||
// Assert on what the peer sees rather than on lighthouse traffic. A query for them makes the lighthouse send
|
||||
// them a punch notification, which is the whole point. Our own update to the lighthouse sends them nothing,
|
||||
// so this cannot be satisfied by the update the rebind itself pushes.
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("quiet")))
|
||||
require.False(t, waitForLighthouseMsg(t, r, theirControl, time.Millisecond*300),
|
||||
"an ordinary send should not requery the lighthouse")
|
||||
|
||||
myControl.RebindUDPServer()
|
||||
r.RouteFor(time.Millisecond * 300) // let the update the rebind itself sends pass by
|
||||
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("after rebind")))
|
||||
assert.True(t, waitForLighthouseMsg(t, r, theirControl, time.Second*5),
|
||||
"the first send after a rebind should requery the lighthouse, which then tells the peer to punch at us")
|
||||
|
||||
lhControl.Stop()
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
// The scenario this whole thing exists for: a laptop sleeps at the office and wakes up at home on a new address.
|
||||
// Until it tells the lighthouse, the lighthouse keeps handing peers the office address, so their punches land
|
||||
// nowhere and the tunnel stays dead. On a long interval the rebind is the only thing that closes that window.
|
||||
func TestRebindAdvertisesNewAddressAfterMove(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{})
|
||||
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
||||
"lighthouse": m{"am_lighthouse": true},
|
||||
})
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", m{
|
||||
"lighthouse": m{
|
||||
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||
"interval": 600,
|
||||
},
|
||||
"static_host_map": m{
|
||||
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||
},
|
||||
})
|
||||
|
||||
// Advertise wherever we currently are rather than this machine's real NICs, read fresh each time so a move
|
||||
// is picked up.
|
||||
myControl.SetLocalAddrsFn(func(*nebula.LocalAllowList) []netip.Addr {
|
||||
return []netip.Addr{myControl.GetUDPAddr().Addr()}
|
||||
})
|
||||
|
||||
r := router.NewR(t, lhControl, myControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
lhControl.Start()
|
||||
myControl.Start()
|
||||
|
||||
require.True(t, waitForLighthouseMsg(t, r, lhControl, time.Second*5), "expected an initial registration")
|
||||
r.RouteFor(time.Millisecond * 400)
|
||||
|
||||
require.Contains(t, reportedAddrs(t, lhControl, myVpnIpNet[0].Addr()), myUdpAddr,
|
||||
"the lighthouse should know the address we started on")
|
||||
|
||||
// Wake up somewhere else
|
||||
newAddr := netip.MustParseAddrPort("10.0.0.99:4242")
|
||||
myControl.SetUDPAddr(newAddr)
|
||||
r.AddRoute(newAddr.Addr(), newAddr.Port(), myControl)
|
||||
|
||||
// Nothing has told the lighthouse, and with interval 600 nothing scheduled will
|
||||
r.RouteFor(time.Millisecond * 400)
|
||||
require.NotContains(t, reportedAddrs(t, lhControl, myVpnIpNet[0].Addr()), newAddr,
|
||||
"the lighthouse should still be handing out the old address before the rebind")
|
||||
|
||||
myControl.RebindUDPServer()
|
||||
require.True(t, waitForLighthouseMsg(t, r, lhControl, time.Second*5), "expected an update after the rebind")
|
||||
r.RouteFor(time.Millisecond * 400)
|
||||
|
||||
assert.Contains(t, reportedAddrs(t, lhControl, myVpnIpNet[0].Addr()), newAddr,
|
||||
"after the rebind the lighthouse should hand peers our new address")
|
||||
|
||||
lhControl.Stop()
|
||||
myControl.Stop()
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
//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/udp"
|
||||
)
|
||||
|
||||
// TestRecoveryTiming measures how long a tunnel takes to come back after the peer stops accepting our traffic,
|
||||
// which is what a laptop waking on a new network looks like from the peer's side: its NAT has no state for where
|
||||
// we are now, so everything we send disappears.
|
||||
//
|
||||
// It is a measurement, not a pass/fail assertion. Recovery is timed to the moment the peer punches back at us,
|
||||
// since that is when its NAT opens and the tunnel is usable again.
|
||||
//
|
||||
// go test -tags e2e_testing -v -run TestRecoveryTiming ./e2e/
|
||||
func TestRecoveryTiming(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
rebind bool
|
||||
}{
|
||||
{"no trigger", false},
|
||||
{"rebind counter", true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
d, lost := measureRecovery(t, tc.rebind)
|
||||
t.Logf("RESULT %-16s recovered in %-9v (%d packets lost)", tc.name, d.Round(time.Millisecond), lost)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// measureRecovery returns how long until the peer punched back, and how many of our packets died meanwhile. When
|
||||
// rebind is true we call RebindUDPServer once the tunnel goes dark, which is what the darwin network change
|
||||
// monitor does and what iOS has always done. When false, nothing tells nebula anything is wrong.
|
||||
func measureRecovery(t *testing.T, rebind bool) (time.Duration, int) {
|
||||
t.Helper()
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
|
||||
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
||||
"lighthouse": m{"am_lighthouse": true},
|
||||
})
|
||||
|
||||
peerCfg := m{
|
||||
"lighthouse": m{
|
||||
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||
"interval": 600,
|
||||
"local_allow_list": m{
|
||||
"10.0.0.0/24": true,
|
||||
"::/0": false,
|
||||
},
|
||||
},
|
||||
"static_host_map": m{
|
||||
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||
},
|
||||
}
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", peerCfg)
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24", peerCfg)
|
||||
|
||||
r := router.NewR(t, lhControl, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
defer func() {
|
||||
lhControl.Stop()
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}()
|
||||
|
||||
lhControl.Start()
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
r.RouteFor(time.Millisecond * 500)
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("establish")))
|
||||
r.RouteFor(time.Second)
|
||||
if myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false) == nil {
|
||||
t.Fatal("failed to establish the tunnel we are measuring")
|
||||
}
|
||||
r.RouteFor(time.Millisecond * 500)
|
||||
|
||||
// From here the peer's NAT has no state for us, everything we send it disappears
|
||||
start := time.Now()
|
||||
blackholed := 0
|
||||
var recovered time.Duration
|
||||
|
||||
if rebind {
|
||||
myControl.RebindUDPServer()
|
||||
}
|
||||
|
||||
// Keep the tun busy the way someone retrying a stalled connection would
|
||||
stop := make(chan struct{})
|
||||
defer close(stop)
|
||||
go func() {
|
||||
tick := time.NewTicker(time.Millisecond * 200)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-tick.C:
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(
|
||||
theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("retry")))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
r.RouteForAllExitFuncOrTimeout(time.Second*30, func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||
if c == theirControl && p.From == myControl.GetUDPAddr() {
|
||||
blackholed++
|
||||
return router.Drop
|
||||
}
|
||||
|
||||
// The peer reaching us directly is the moment its NAT opened, whether that is a punch or a handshake
|
||||
if c == myControl && p.From == theirUdpAddr {
|
||||
recovered = time.Since(start)
|
||||
return router.RouteAndExit
|
||||
}
|
||||
|
||||
return router.KeepRouting
|
||||
})
|
||||
|
||||
if recovered == 0 {
|
||||
t.Fatalf("no recovery within 30s (%d packets blackholed)", blackholed)
|
||||
}
|
||||
return recovered, blackholed
|
||||
}
|
||||
+84
-337
@@ -13,7 +13,6 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,19 +24,6 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// outNatKey is the (from, to) pair used by outNat. Comparable struct, so it works as a map key without the
|
||||
// allocation cost of a string-concat key.
|
||||
type outNatKey struct {
|
||||
from, to netip.AddrPort
|
||||
}
|
||||
|
||||
// fannedPacket pairs a UDP TX packet with its source control so the router can route it after popping from
|
||||
// the fan-in channel.
|
||||
type fannedPacket struct {
|
||||
from *nebula.Control
|
||||
pkt *udp.Packet
|
||||
}
|
||||
|
||||
type R struct {
|
||||
// Simple map of the ip:port registered on a control to the control
|
||||
// Basically a router, right?
|
||||
@@ -48,28 +34,12 @@ type R struct {
|
||||
|
||||
// A last used map, if an inbound packet hit the inNat map then
|
||||
// all return packets should use the same last used inbound address for the outbound sender
|
||||
outNat map[outNatKey]netip.AddrPort
|
||||
// map[from address + ":" + to address] => ip:port to rewrite in the udp packet to receiver
|
||||
outNat map[string]netip.AddrPort
|
||||
|
||||
// A map of vpn ip to the nebula control it belongs to
|
||||
vpnControls map[netip.Addr]*nebula.Control
|
||||
|
||||
// Cached select infrastructure for RouteForAllUntilTxTun.
|
||||
// The controls map is immutable after NewR so the cases are good for the test lifetime.
|
||||
// We only rebuild if a different receiver is asked.
|
||||
selRecvCtl *nebula.Control
|
||||
selCases []reflect.SelectCase
|
||||
selCtls []*nebula.Control
|
||||
|
||||
// Optional fan-in mode for hot-path benchmarks: one forwarder goroutine per control drains UDP TX into udpFanIn,
|
||||
// so RouteForAllUntilTxTun can do a fixed 2-way native select instead of paying reflect.Select per call.
|
||||
// Off by default (would otherwise interleave with tests that use GetFromUDP directly on the same control).
|
||||
// Enabled by EnableFanIn.
|
||||
udpFanIn chan fannedPacket
|
||||
stopFanIn chan struct{}
|
||||
fanInWG sync.WaitGroup
|
||||
fanInMu sync.Mutex
|
||||
fanInOn atomic.Bool
|
||||
|
||||
ignoreFlows []ignoreFlow
|
||||
flow []flowEntry
|
||||
|
||||
@@ -114,28 +84,6 @@ type packet struct {
|
||||
packet *udp.Packet
|
||||
tun bool // a packet pulled off a tun device
|
||||
rx bool // the packet was received by a udp device
|
||||
|
||||
// h is the nebula header, parsed once when the packet is recorded. parseErr says why there isn't one, which
|
||||
// the flow log reports rather than hiding. Punchy sends a single byte, so an unparseable packet is normal.
|
||||
h header.H
|
||||
parseErr error
|
||||
}
|
||||
|
||||
// fromAddr and toAddr are the addresses this packet actually travelled between. Reading them off the control
|
||||
// instead would misreport the whole history once a test moves a node. Tun packets are synthesized without
|
||||
// addresses, so they fall back to the control.
|
||||
func (p *packet) fromAddr() netip.AddrPort {
|
||||
if p.tun || !p.packet.From.IsValid() {
|
||||
return p.from.GetUDPAddr()
|
||||
}
|
||||
return p.packet.From
|
||||
}
|
||||
|
||||
func (p *packet) toAddr() netip.AddrPort {
|
||||
if p.tun || !p.packet.To.IsValid() {
|
||||
return p.to.GetUDPAddr()
|
||||
}
|
||||
return p.packet.To
|
||||
}
|
||||
|
||||
func (p *packet) WasReceived() {
|
||||
@@ -153,9 +101,6 @@ const (
|
||||
ExitNow ExitType = 1
|
||||
// RouteAndExit routes this packet and exits immediately afterwards
|
||||
RouteAndExit ExitType = 2
|
||||
// Drop discards this packet without delivering it and keeps routing. Use it to simulate a blackhole, such as
|
||||
// a restrictive NAT refusing traffic from an address it has not seen.
|
||||
Drop ExitType = 3
|
||||
)
|
||||
|
||||
type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
||||
@@ -166,9 +111,7 @@ type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
||||
func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// t.Name() contains a slash for subtests, so the flow log can land in a nested directory
|
||||
fn := filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name()))
|
||||
if err := os.MkdirAll(filepath.Dir(fn), 0755); err != nil {
|
||||
if err := os.MkdirAll("mermaid", 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -176,10 +119,10 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
||||
controls: make(map[netip.AddrPort]*nebula.Control),
|
||||
vpnControls: make(map[netip.Addr]*nebula.Control),
|
||||
inNat: make(map[netip.AddrPort]*nebula.Control),
|
||||
outNat: make(map[outNatKey]netip.AddrPort),
|
||||
outNat: make(map[string]netip.AddrPort),
|
||||
flow: []flowEntry{},
|
||||
ignoreFlows: []ignoreFlow{},
|
||||
fn: fn,
|
||||
fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())),
|
||||
t: t,
|
||||
cancelRender: cancel,
|
||||
}
|
||||
@@ -210,10 +153,8 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-clockSource.C:
|
||||
r.Lock()
|
||||
r.renderHostmaps("clock tick")
|
||||
r.renderFlow()
|
||||
r.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -239,21 +180,15 @@ func (r *R) AddRoute(ip netip.Addr, port uint16, c *nebula.Control) {
|
||||
// RenderFlow renders the packet flow seen up until now and stops further automatic renders from happening.
|
||||
func (r *R) RenderFlow() {
|
||||
r.cancelRender()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.renderFlow()
|
||||
}
|
||||
|
||||
// CancelFlowLogs stops flow logs from being tracked and destroys any logs already collected
|
||||
func (r *R) CancelFlowLogs() {
|
||||
r.cancelRender()
|
||||
r.Lock()
|
||||
r.flow = nil
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
// renderFlow writes the flow log to disk. Caller must hold r.Lock. renderFlow reads r.flow / r.additionalGraphs and
|
||||
// the *packet pointers stashed inside, all of which are mutated under the same lock by routing paths.
|
||||
func (r *R) renderFlow() {
|
||||
if r.flow == nil {
|
||||
return
|
||||
@@ -276,7 +211,7 @@ func (r *R) renderFlow() {
|
||||
continue
|
||||
}
|
||||
|
||||
addr := e.packet.fromAddr()
|
||||
addr := e.packet.from.GetUDPAddr()
|
||||
if _, ok := participants[addr]; ok {
|
||||
continue
|
||||
}
|
||||
@@ -295,6 +230,7 @@ func (r *R) renderFlow() {
|
||||
}
|
||||
|
||||
// Print packets
|
||||
h := &header.H{}
|
||||
for _, e := range r.flow {
|
||||
if e.packet == nil {
|
||||
//fmt.Fprintf(f, " note over %s: %s\n", strings.Join(participantsVals, ", "), e.note)
|
||||
@@ -306,22 +242,21 @@ func (r *R) renderFlow() {
|
||||
fmt.Fprintln(f, r.formatUdpPacket(p))
|
||||
|
||||
} else {
|
||||
if err := h.Parse(p.packet.Data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
line := "--x"
|
||||
if p.rx {
|
||||
line = "->>"
|
||||
}
|
||||
|
||||
detail := fmt.Sprintf("%s(%s), index %v, counter: %v",
|
||||
p.h.TypeName(), p.h.SubTypeName(), p.h.RemoteIndex, p.h.MessageCounter)
|
||||
if p.parseErr != nil {
|
||||
detail = fmt.Sprintf("unparsed, %v (%d bytes)", p.parseErr, len(p.packet.Data))
|
||||
}
|
||||
|
||||
fmt.Fprintf(f, " %s%s%s: %s\n",
|
||||
normalizeName(p.fromAddr().String()),
|
||||
fmt.Fprintf(f,
|
||||
" %s%s%s: %s(%s), index %v, counter: %v\n",
|
||||
normalizeName(p.from.GetUDPAddr().String()),
|
||||
line,
|
||||
normalizeName(p.toAddr().String()),
|
||||
detail,
|
||||
normalizeName(p.to.GetUDPAddr().String()),
|
||||
h.TypeName(), h.SubTypeName(), h.RemoteIndex, h.MessageCounter,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -435,34 +370,29 @@ func (r *R) unlockedInjectFlow(from, to *nebula.Control, p *udp.Packet, tun bool
|
||||
|
||||
r.renderHostmaps(fmt.Sprintf("Packet %v", len(r.flow)))
|
||||
|
||||
var h header.H
|
||||
var parseErr error
|
||||
if !tun {
|
||||
parseErr = h.Parse(p.Data)
|
||||
}
|
||||
|
||||
// Decide before copying, the copy comes from a freelist and an ignored packet would never be released
|
||||
for _, i := range r.ignoreFlows {
|
||||
if tun {
|
||||
if i.tun.HasValue && i.tun.IsTrue {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
if len(r.ignoreFlows) > 0 {
|
||||
var h header.H
|
||||
err := h.Parse(p.Data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// A packet we could not parse has no type to match against, so no rule can ignore it
|
||||
if parseErr == nil && i.messageType == h.Type && i.subType == h.Subtype {
|
||||
return nil
|
||||
for _, i := range r.ignoreFlows {
|
||||
if !tun {
|
||||
if i.messageType == h.Type && i.subType == h.Subtype {
|
||||
return nil
|
||||
}
|
||||
} else if i.tun.HasValue && i.tun.IsTrue {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fp := &packet{
|
||||
from: from,
|
||||
to: to,
|
||||
packet: p.Copy(),
|
||||
tun: tun,
|
||||
h: h,
|
||||
parseErr: parseErr,
|
||||
from: from,
|
||||
to: to,
|
||||
packet: p.Copy(),
|
||||
tun: tun,
|
||||
}
|
||||
|
||||
r.flow = append(r.flow, flowEntry{packet: fp})
|
||||
@@ -504,157 +434,68 @@ func (r *R) RouteUntilTxTun(sender *nebula.Control, receiver *nebula.Control) []
|
||||
panic("No control for udp tx " + a.String())
|
||||
}
|
||||
fp := r.unlockedInjectFlow(sender, c, p, false)
|
||||
c.InjectUDPPacket(p) // copies internally; original is ours to release
|
||||
c.InjectUDPPacket(p)
|
||||
fp.WasReceived()
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RouteForAllUntilTxTun will route for everyone and return when a packet is seen on the receiver's tun.
|
||||
// If a control's UDP TX address can't be matched to a registered control, we panic.
|
||||
//
|
||||
// For allocation-sensitive callers (hot-path benchmarks, in particular relay
|
||||
// benches with 3+ controls), call EnableFanIn() first.
|
||||
// RouteForAllUntilTxTun will route for everyone and return when a packet is seen on receivers tun
|
||||
// If the router doesn't have the nebula controller for that address, we panic
|
||||
func (r *R) RouteForAllUntilTxTun(receiver *nebula.Control) []byte {
|
||||
if r.fanInOn.Load() {
|
||||
return r.routeFanIn(receiver)
|
||||
}
|
||||
return r.routeReflect(receiver)
|
||||
}
|
||||
|
||||
// routeFanIn is the alloc-free path used when EnableFanIn is in effect.
|
||||
func (r *R) routeFanIn(receiver *nebula.Control) []byte {
|
||||
tunTx := receiver.GetTunTxChan()
|
||||
for {
|
||||
select {
|
||||
case p := <-tunTx:
|
||||
r.Lock()
|
||||
if r.flow != nil {
|
||||
np := udp.Packet{Data: make([]byte, len(p))}
|
||||
copy(np.Data, p)
|
||||
r.unlockedInjectFlow(receiver, receiver, &np, true)
|
||||
}
|
||||
r.Unlock()
|
||||
return p
|
||||
case fp := <-r.udpFanIn:
|
||||
r.routeUDP(fp.from, fp.pkt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// routeReflect is the default reflect.Select-based path. Pays the boxing allocation per call but doesn't interfere
|
||||
// with tests that pull packets directly from controls' UDP TX channels via GetFromUDP.
|
||||
func (r *R) routeReflect(receiver *nebula.Control) []byte {
|
||||
sc, cm := r.selectCasesFor(receiver)
|
||||
for {
|
||||
x, rx, _ := reflect.Select(sc)
|
||||
if x == 0 {
|
||||
p := rx.Interface().([]byte)
|
||||
r.Lock()
|
||||
if r.flow != nil {
|
||||
np := udp.Packet{Data: make([]byte, len(p))}
|
||||
copy(np.Data, p)
|
||||
r.unlockedInjectFlow(cm[x], cm[x], &np, true)
|
||||
}
|
||||
r.Unlock()
|
||||
return p
|
||||
}
|
||||
r.routeUDP(cm[x], rx.Interface().(*udp.Packet))
|
||||
}
|
||||
}
|
||||
|
||||
// EnableFanIn switches RouteForAllUntilTxTun to the alloc-free fan-in path.
|
||||
// One forwarder goroutine per registered control drains UDP TX into a shared channel that RouteForAllUntilTxTun selects
|
||||
// on alongside the receiver's TUN TX channel.
|
||||
func (r *R) EnableFanIn() {
|
||||
r.fanInMu.Lock()
|
||||
defer r.fanInMu.Unlock()
|
||||
if r.fanInOn.Load() {
|
||||
return
|
||||
}
|
||||
r.udpFanIn = make(chan fannedPacket, 32)
|
||||
r.stopFanIn = make(chan struct{})
|
||||
for _, c := range r.controls {
|
||||
r.startFanInWorker(c)
|
||||
}
|
||||
r.fanInOn.Store(true)
|
||||
r.t.Cleanup(r.stopFanInWorkers)
|
||||
}
|
||||
|
||||
// startFanInWorker spawns a goroutine that drains c's UDP TX into r.udpFanIn.
|
||||
func (r *R) startFanInWorker(c *nebula.Control) {
|
||||
r.fanInWG.Add(1)
|
||||
udpTx := c.GetUDPTxChan()
|
||||
go func() {
|
||||
defer r.fanInWG.Done()
|
||||
for {
|
||||
select {
|
||||
case <-r.stopFanIn:
|
||||
return
|
||||
case p := <-udpTx:
|
||||
select {
|
||||
case <-r.stopFanIn:
|
||||
p.Release()
|
||||
return
|
||||
case r.udpFanIn <- fannedPacket{from: c, pkt: p}:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// stopFanInWorkers signals the fan-in goroutines to exit and waits for them.
|
||||
func (r *R) stopFanInWorkers() {
|
||||
r.fanInMu.Lock()
|
||||
wasOn := r.fanInOn.Swap(false)
|
||||
r.fanInMu.Unlock()
|
||||
if !wasOn {
|
||||
return
|
||||
}
|
||||
close(r.stopFanIn)
|
||||
r.fanInWG.Wait()
|
||||
}
|
||||
|
||||
// routeUDP forwards a UDP TX packet from the named source control to the destination control derived from p.To,
|
||||
// releasing the source packet after InjectUDPPacket has copied its bytes into a fresh pool slot.
|
||||
func (r *R) routeUDP(from *nebula.Control, p *udp.Packet) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
a := from.GetUDPAddr()
|
||||
c := r.getControl(a, p.To, p)
|
||||
if c == nil {
|
||||
panic(fmt.Sprintf("No control for udp tx %s", p.To))
|
||||
}
|
||||
fp := r.unlockedInjectFlow(from, c, p, false)
|
||||
c.InjectUDPPacket(p) // copies internally; original is ours to release
|
||||
fp.WasReceived()
|
||||
p.Release()
|
||||
}
|
||||
|
||||
// selectCasesFor returns the SelectCase array used by routeReflect: one slot for the receiver's TUN TX channel followed
|
||||
// by one per control's UDP TX channel. Cached for the test lifetime, only rebuilt if the receiver changes.
|
||||
func (r *R) selectCasesFor(receiver *nebula.Control) ([]reflect.SelectCase, []*nebula.Control) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if r.selRecvCtl == receiver && r.selCases != nil {
|
||||
return r.selCases, r.selCtls
|
||||
}
|
||||
sc := make([]reflect.SelectCase, len(r.controls)+1)
|
||||
cm := make([]*nebula.Control, len(r.controls)+1)
|
||||
sc[0] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(receiver.GetTunTxChan())}
|
||||
cm[0] = receiver
|
||||
i := 1
|
||||
|
||||
i := 0
|
||||
sc[i] = reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(receiver.GetTunTxChan()),
|
||||
Send: reflect.Value{},
|
||||
}
|
||||
cm[i] = receiver
|
||||
|
||||
i++
|
||||
for _, c := range r.controls {
|
||||
sc[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.GetUDPTxChan())}
|
||||
sc[i] = reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(c.GetUDPTxChan()),
|
||||
Send: reflect.Value{},
|
||||
}
|
||||
|
||||
cm[i] = c
|
||||
i++
|
||||
}
|
||||
r.selRecvCtl = receiver
|
||||
r.selCases = sc
|
||||
r.selCtls = cm
|
||||
return sc, cm
|
||||
|
||||
for {
|
||||
x, rx, _ := reflect.Select(sc)
|
||||
r.Lock()
|
||||
|
||||
if x == 0 {
|
||||
// we are the tun tx, we can exit
|
||||
p := rx.Interface().([]byte)
|
||||
np := udp.Packet{Data: make([]byte, len(p))}
|
||||
copy(np.Data, p)
|
||||
|
||||
r.unlockedInjectFlow(cm[x], cm[x], &np, true)
|
||||
r.Unlock()
|
||||
return p
|
||||
|
||||
} else {
|
||||
// we are a udp tx, route and continue
|
||||
p := rx.Interface().(*udp.Packet)
|
||||
a := cm[x].GetUDPAddr()
|
||||
c := r.getControl(a, p.To, p)
|
||||
if c == nil {
|
||||
r.Unlock()
|
||||
panic(fmt.Sprintf("No control for udp tx %s", p.To))
|
||||
}
|
||||
fp := r.unlockedInjectFlow(cm[x], c, p, false)
|
||||
c.InjectUDPPacket(p)
|
||||
fp.WasReceived()
|
||||
}
|
||||
r.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// RouteExitFunc will call the whatDo func with each udp packet from sender.
|
||||
@@ -681,7 +522,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
||||
switch e {
|
||||
case ExitNow:
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
return
|
||||
|
||||
case RouteAndExit:
|
||||
@@ -689,13 +529,8 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
||||
receiver.InjectUDPPacket(p)
|
||||
fp.WasReceived()
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
return
|
||||
|
||||
case Drop:
|
||||
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||
r.unlockedInjectFlow(sender, receiver, p, false)
|
||||
|
||||
case KeepRouting:
|
||||
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
||||
receiver.InjectUDPPacket(p)
|
||||
@@ -706,7 +541,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
||||
}
|
||||
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,85 +560,6 @@ func (r *R) RouteUntilAfterMsgType(sender *nebula.Control, msgType header.Messag
|
||||
})
|
||||
}
|
||||
|
||||
// RouteFor routes everything that shows up for the given duration and then returns. Use it to let a test settle
|
||||
// deterministically rather than sleeping and hoping: a single FlushAll races a completing handshake, which queues
|
||||
// more packets right behind it.
|
||||
func (r *R) RouteFor(d time.Duration) {
|
||||
r.RouteForAllExitFuncOrTimeout(d, func(*udp.Packet, *nebula.Control) ExitType {
|
||||
return KeepRouting
|
||||
})
|
||||
}
|
||||
|
||||
// RouteForAllExitFuncOrTimeout is RouteForAllExitFunc with a deadline, reporting whether whatDo asked to exit
|
||||
// before time ran out. The unbounded version blocks forever on a quiet network, so this is what a test needs to
|
||||
// assert that something does NOT happen, or to route for a fixed settling period.
|
||||
func (r *R) RouteForAllExitFuncOrTimeout(timeout time.Duration, whatDo ExitFunc) bool {
|
||||
sc := make([]reflect.SelectCase, 0, len(r.controls)+1)
|
||||
cm := make([]*nebula.Control, 0, len(r.controls))
|
||||
|
||||
for _, c := range r.controls {
|
||||
sc = append(sc, reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(c.GetUDPTxChan()),
|
||||
Send: reflect.Value{},
|
||||
})
|
||||
cm = append(cm, c)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
sc = append(sc, reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(timer.C),
|
||||
Send: reflect.Value{},
|
||||
})
|
||||
|
||||
for {
|
||||
x, rx, _ := reflect.Select(sc)
|
||||
if x == len(cm) {
|
||||
return false
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
p := rx.Interface().(*udp.Packet)
|
||||
receiver := r.getControl(cm[x].GetUDPAddr(), p.To, p)
|
||||
if receiver == nil {
|
||||
r.Unlock()
|
||||
panic("Can't RouteForAllExitFuncOrTimeout for host: " + p.To.String())
|
||||
}
|
||||
|
||||
e := whatDo(p, receiver)
|
||||
switch e {
|
||||
case ExitNow:
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
return true
|
||||
|
||||
case RouteAndExit:
|
||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||
receiver.InjectUDPPacket(p)
|
||||
fp.WasReceived()
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
return true
|
||||
|
||||
case Drop:
|
||||
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||
r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||
|
||||
case KeepRouting:
|
||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||
receiver.InjectUDPPacket(p)
|
||||
fp.WasReceived()
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("Unknown exitFunc return: %v", e))
|
||||
}
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *R) RouteForAllUntilAfterMsgTypeTo(receiver *nebula.Control, msgType header.MessageType, subType header.MessageSubType) {
|
||||
h := &header.H{}
|
||||
r.RouteForAllExitFunc(func(p *udp.Packet, r *nebula.Control) ExitType {
|
||||
@@ -886,7 +641,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
||||
switch e {
|
||||
case ExitNow:
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
return
|
||||
|
||||
case RouteAndExit:
|
||||
@@ -894,13 +648,8 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
||||
receiver.InjectUDPPacket(p)
|
||||
fp.WasReceived()
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
return
|
||||
|
||||
case Drop:
|
||||
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||
r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||
|
||||
case KeepRouting:
|
||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||
receiver.InjectUDPPacket(p)
|
||||
@@ -910,7 +659,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
||||
panic(fmt.Sprintf("Unknown exitFunc return: %v", e))
|
||||
}
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,20 +702,19 @@ func (r *R) FlushAll() {
|
||||
}
|
||||
receiver.InjectUDPPacket(p)
|
||||
r.Unlock()
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
|
||||
// getControl performs or seeds NAT translation and returns the control for toAddr, p from fields may change
|
||||
// This is an internal router function, the caller must hold the lock
|
||||
func (r *R) getControl(fromAddr, toAddr netip.AddrPort, p *udp.Packet) *nebula.Control {
|
||||
if newAddr, ok := r.outNat[outNatKey{from: fromAddr, to: toAddr}]; ok {
|
||||
if newAddr, ok := r.outNat[fromAddr.String()+":"+toAddr.String()]; ok {
|
||||
p.From = newAddr
|
||||
}
|
||||
|
||||
c, ok := r.inNat[toAddr]
|
||||
if ok {
|
||||
r.outNat[outNatKey{from: c.GetUDPAddr(), to: fromAddr}] = toAddr
|
||||
r.outNat[c.GetUDPAddr().String()+":"+fromAddr.String()] = toAddr
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
//go:build e2e_testing
|
||||
// +build e2e_testing
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/cert_test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestSSHDLifecycle(t *testing.T) {
|
||||
// TestSSHDLifecycle exercises the in-process sshd through several config reloads and a Control.Stop.
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(
|
||||
cert.Version1, cert.Curve_CURVE25519,
|
||||
time.Now(), time.Now().Add(10*time.Minute),
|
||||
nil, nil, []string{},
|
||||
)
|
||||
|
||||
hostKeyPEM := generateSSHHostKey(t)
|
||||
clientSigner, clientAuthKey := generateSSHClientKey(t)
|
||||
sshdAddr := allocLoopbackPort(t)
|
||||
|
||||
overrides := m{
|
||||
"sshd": m{
|
||||
"enabled": true,
|
||||
"listen": sshdAddr,
|
||||
"host_key": hostKeyPEM,
|
||||
"authorized_users": []m{{
|
||||
"user": "tester",
|
||||
"keys": []string{clientAuthKey},
|
||||
}},
|
||||
},
|
||||
}
|
||||
control, _, _, _ := newSimpleServer(cert.Version1, ca, caKey, "sshd-test", "10.222.0.1/24", overrides)
|
||||
control.Start()
|
||||
t.Cleanup(func() { control.Stop() })
|
||||
|
||||
// sshd binds in a goroutine after Start returns; wait for it.
|
||||
require.Eventually(t, func() bool { return canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
|
||||
"sshd never started listening")
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
out := sshExecReload(t, sshdAddr, clientSigner)
|
||||
assert.Contains(t, out, "Reloading config", "reload cycle %d", i)
|
||||
require.Eventually(t, func() bool { return canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
|
||||
"sshd not listening after reload cycle %d", i)
|
||||
}
|
||||
|
||||
control.Stop()
|
||||
require.Eventually(t, func() bool { return !canDial(sshdAddr) }, 2*time.Second, 25*time.Millisecond,
|
||||
"sshd still listening after Control.Stop")
|
||||
}
|
||||
|
||||
func canDial(addr string) bool {
|
||||
c, err := net.DialTimeout("tcp", addr, 100*time.Millisecond)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = c.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// allocLoopbackPort grabs an unused TCP port on 127.0.0.1, closes it, and returns the address. There
|
||||
// is a small race between releasing the port and the sshd reclaiming it; in practice the OS keeps the
|
||||
// port available long enough for the test to bind it.
|
||||
func allocLoopbackPort(t *testing.T) string {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
addr := l.Addr().String()
|
||||
require.NoError(t, l.Close())
|
||||
return addr
|
||||
}
|
||||
|
||||
func generateSSHHostKey(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
block, err := ssh.MarshalPrivateKey(priv, "nebula-e2e-host")
|
||||
require.NoError(t, err)
|
||||
return string(pem.EncodeToMemory(block))
|
||||
}
|
||||
|
||||
func generateSSHClientKey(t *testing.T) (ssh.Signer, string) {
|
||||
t.Helper()
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
signer, err := ssh.NewSignerFromKey(priv)
|
||||
require.NoError(t, err)
|
||||
auth := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
|
||||
return signer, auth
|
||||
}
|
||||
|
||||
func sshExecReload(t *testing.T, addr string, signer ssh.Signer) string {
|
||||
t.Helper()
|
||||
cfg := &ssh.ClientConfig{
|
||||
User: "tester",
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
client, err := ssh.Dial("tcp", addr, cfg)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
sess, err := client.NewSession()
|
||||
require.NoError(t, err)
|
||||
defer sess.Close()
|
||||
|
||||
// reload tears the channel down before sending exit-status, so Output returns an error on the
|
||||
// channel close. The output buffer still has whatever the reload callback wrote before that.
|
||||
out, _ := sess.Output("reload")
|
||||
return string(out)
|
||||
}
|
||||
+2
-518
@@ -4,23 +4,15 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestDropInactiveTunnels(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||
// under ideal conditions
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
@@ -43,8 +35,8 @@ func TestDropInactiveTunnels(t *testing.T) {
|
||||
r.Log("Go inactive and wait for the tunnels to get dropped")
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||
if myIndexes == 0 && theirIndexes == 0 {
|
||||
break
|
||||
}
|
||||
@@ -63,511 +55,3 @@ func TestDropInactiveTunnels(t *testing.T) {
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestCertUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||
// under ideal conditions
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
caB, err := ca.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ca2, _, caKey2, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
|
||||
ca2B, err := ca2.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
caStr := fmt.Sprintf("%s\n%s", caB, ca2B)
|
||||
|
||||
myCert, _, myPrivKey, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca, caKey, "me", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.MustParsePrefix("10.128.0.1/24")}, nil, []string{})
|
||||
_, myCert2Pem := cert_test.NewTestCertDifferentVersion(myCert, cert.Version2, ca2, caKey2)
|
||||
|
||||
theirCert, _, theirPrivKey, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca, caKey, "them", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.MustParsePrefix("10.128.0.2/24")}, nil, []string{})
|
||||
theirCert2, _ := cert_test.NewTestCertDifferentVersion(theirCert, cert.Version2, ca2, caKey2)
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, myC := newServer([]cert.Certificate{ca, ca2}, []cert.Certificate{myCert}, myPrivKey, m{})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newServer([]cert.Certificate{ca, ca2}, []cert.Certificate{theirCert, theirCert2}, theirPrivKey, m{})
|
||||
|
||||
// Share our underlay information
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
r.Log("Assert the tunnel between me and them works")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
r.Log("yay")
|
||||
//todo ???
|
||||
time.Sleep(1 * time.Second)
|
||||
r.FlushAll()
|
||||
|
||||
mc := m{
|
||||
"pki": m{
|
||||
"ca": caStr,
|
||||
"cert": string(myCert2Pem),
|
||||
"key": string(myPrivKey),
|
||||
},
|
||||
//"tun": m{"disabled": true},
|
||||
"firewall": myC.Settings["firewall"],
|
||||
//"handshakes": m{
|
||||
// "try_interval": "1s",
|
||||
//},
|
||||
"listen": myC.Settings["listen"],
|
||||
"logging": myC.Settings["logging"],
|
||||
"timers": myC.Settings["timers"],
|
||||
}
|
||||
|
||||
cb, err := yaml.Marshal(mc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r.Logf("reload new v2-only config")
|
||||
err = myC.ReloadConfigString(string(cb))
|
||||
assert.NoError(t, err)
|
||||
r.Log("yay, spin until their sees it")
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
c := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
if c == nil {
|
||||
r.Log("nil")
|
||||
} else {
|
||||
version := c.Cert.Version()
|
||||
r.Logf("version %d", version)
|
||||
if version == cert.Version2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
since := time.Since(waitStart)
|
||||
if since > time.Second*10 {
|
||||
t.Fatal("Cert should be new by now")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestCertDowngrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||
// under ideal conditions
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
caB, err := ca.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ca2, _, caKey2, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
|
||||
ca2B, err := ca2.MarshalPEM()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
caStr := fmt.Sprintf("%s\n%s", caB, ca2B)
|
||||
|
||||
myCert, _, myPrivKey, myCertPem := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca, caKey, "me", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.MustParsePrefix("10.128.0.1/24")}, nil, []string{})
|
||||
myCert2, _ := cert_test.NewTestCertDifferentVersion(myCert, cert.Version2, ca2, caKey2)
|
||||
|
||||
theirCert, _, theirPrivKey, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca, caKey, "them", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.MustParsePrefix("10.128.0.2/24")}, nil, []string{})
|
||||
theirCert2, _ := cert_test.NewTestCertDifferentVersion(theirCert, cert.Version2, ca2, caKey2)
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, myC := newServer([]cert.Certificate{ca, ca2}, []cert.Certificate{myCert2}, myPrivKey, m{})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newServer([]cert.Certificate{ca, ca2}, []cert.Certificate{theirCert, theirCert2}, theirPrivKey, m{})
|
||||
|
||||
// Share our underlay information
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
r.Log("Assert the tunnel between me and them works")
|
||||
//assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
//r.Log("yay")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
r.Log("yay")
|
||||
//todo ???
|
||||
time.Sleep(1 * time.Second)
|
||||
r.FlushAll()
|
||||
|
||||
mc := m{
|
||||
"pki": m{
|
||||
"ca": caStr,
|
||||
"cert": string(myCertPem),
|
||||
"key": string(myPrivKey),
|
||||
},
|
||||
"firewall": myC.Settings["firewall"],
|
||||
"listen": myC.Settings["listen"],
|
||||
"logging": myC.Settings["logging"],
|
||||
"timers": myC.Settings["timers"],
|
||||
}
|
||||
|
||||
cb, err := yaml.Marshal(mc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r.Logf("reload new v1-only config")
|
||||
err = myC.ReloadConfigString(string(cb))
|
||||
assert.NoError(t, err)
|
||||
r.Log("yay, spin until their sees it")
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
c := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
c2 := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
||||
if c == nil || c2 == nil {
|
||||
r.Log("nil")
|
||||
} else {
|
||||
version := c.Cert.Version()
|
||||
theirVersion := c2.Cert.Version()
|
||||
r.Logf("version %d,%d", version, theirVersion)
|
||||
if version == cert.Version1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
since := time.Since(waitStart)
|
||||
if since > time.Second*5 {
|
||||
r.Log("it is unusual that the cert is not new yet, but not a failure yet")
|
||||
}
|
||||
if since > time.Second*10 {
|
||||
r.Log("wtf")
|
||||
t.Fatal("Cert should be new by now")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestCertMismatchCorrection(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||
// under ideal conditions
|
||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
ca2, _, caKey2, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||
|
||||
myCert, _, myPrivKey, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca, caKey, "me", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.MustParsePrefix("10.128.0.1/24")}, nil, []string{})
|
||||
myCert2, _ := cert_test.NewTestCertDifferentVersion(myCert, cert.Version2, ca2, caKey2)
|
||||
|
||||
theirCert, _, theirPrivKey, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca, caKey, "them", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{netip.MustParsePrefix("10.128.0.2/24")}, nil, []string{})
|
||||
theirCert2, _ := cert_test.NewTestCertDifferentVersion(theirCert, cert.Version2, ca2, caKey2)
|
||||
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newServer([]cert.Certificate{ca, ca2}, []cert.Certificate{myCert2}, myPrivKey, m{})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newServer([]cert.Certificate{ca, ca2}, []cert.Certificate{theirCert, theirCert2}, theirPrivKey, m{})
|
||||
|
||||
// Share our underlay information
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
r.Log("Assert the tunnel between me and them works")
|
||||
//assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||
//r.Log("yay")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
r.Log("yay")
|
||||
//todo ???
|
||||
time.Sleep(1 * time.Second)
|
||||
r.FlushAll()
|
||||
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
c := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
c2 := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
||||
if c == nil || c2 == nil {
|
||||
r.Log("nil")
|
||||
} else {
|
||||
version := c.Cert.Version()
|
||||
theirVersion := c2.Cert.Version()
|
||||
r.Logf("version %d,%d", version, theirVersion)
|
||||
if version == theirVersion {
|
||||
break
|
||||
}
|
||||
}
|
||||
since := time.Since(waitStart)
|
||||
if since > time.Second*5 {
|
||||
r.Log("wtf")
|
||||
}
|
||||
if since > time.Second*10 {
|
||||
r.Log("wtf")
|
||||
t.Fatal("Cert should be new by now")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
func TestCrossStackRelaysWork(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}})
|
||||
|
||||
//myVpnV4 := myVpnIpNet[0]
|
||||
myVpnV6 := myVpnIpNet[1]
|
||||
relayVpnV4 := relayVpnIpNet[0]
|
||||
relayVpnV6 := relayVpnIpNet[1]
|
||||
theirVpnV6 := theirVpnIpNet[0]
|
||||
|
||||
// Teach my how to get to the relay and that their can be reached 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)
|
||||
|
||||
// Build a router so we don't have to reason who gets which packet
|
||||
r := router.NewR(t, myControl, relayControl, theirControl)
|
||||
defer r.RenderFlow()
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
relayControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
t.Log("Trigger a handshake from me to them via the relay")
|
||||
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("Hi from me")))
|
||||
|
||||
p := r.RouteForAllUntilTxTun(theirControl)
|
||||
r.Log("Assert the tunnel works")
|
||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnV6.Addr(), theirVpnV6.Addr(), 80, 80)
|
||||
|
||||
t.Log("reply?")
|
||||
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)
|
||||
|
||||
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
|
||||
//t.Log("finish up")
|
||||
//myControl.Stop()
|
||||
//theirControl.Stop()
|
||||
//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{})
|
||||
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me", "10.128.0.1/24", m{"tunnels": m{"drop_inactive": true, "inactivity_timeout": "5s"}})
|
||||
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", m{"tunnels": m{"drop_inactive": true, "inactivity_timeout": "10m"}})
|
||||
|
||||
// Share our underlay information
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
|
||||
// Start the servers
|
||||
myControl.Start()
|
||||
theirControl.Start()
|
||||
|
||||
r := router.NewR(t, myControl, theirControl)
|
||||
|
||||
r.Log("Assert the tunnel between me and them works")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
|
||||
r.Log("Close the tunnel")
|
||||
myControl.CloseTunnel(theirVpnIpNet[0].Addr(), false)
|
||||
r.FlushAll()
|
||||
|
||||
waitStart := time.Now()
|
||||
for {
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 && theirIndexes == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
since := time.Since(waitStart)
|
||||
r.Logf("my tunnels: %v; their tunnels: %v; duration: %v", myIndexes, theirIndexes, since)
|
||||
if since > time.Second*6 {
|
||||
t.Fatal("Tunnel should have been declared inactive after 2 seconds and before 6 seconds")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
//r.FlushAll()
|
||||
}
|
||||
|
||||
r.Logf("Happy path success, tunnels were dropped within %v", time.Since(waitStart))
|
||||
|
||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||
r.Log("Assert another tunnel between me and them works")
|
||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||
hi := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
||||
if hi == nil {
|
||||
t.Fatal("There is no hostinfo for this tunnel")
|
||||
}
|
||||
myHi := theirControl.GetHostInfoByVpnAddr(myVpnIpNet[0].Addr(), false)
|
||||
if myHi == nil {
|
||||
t.Fatal("There is no hostinfo for my tunnel")
|
||||
}
|
||||
r.Log("It does")
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
hdr := header.H{
|
||||
Version: 1,
|
||||
Type: header.CloseTunnel,
|
||||
Subtype: 0,
|
||||
Reserved: 0,
|
||||
RemoteIndex: hi.RemoteIndex,
|
||||
MessageCounter: 5,
|
||||
}
|
||||
out, err := hdr.Encode(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkt := &udp.Packet{
|
||||
To: hi.CurrentRemote,
|
||||
From: myHi.CurrentRemote,
|
||||
Data: out,
|
||||
}
|
||||
r.InjectUDPPacket(myControl, theirControl, pkt)
|
||||
r.Log("Injected bogus close tunnel. Let's see!")
|
||||
waitStart = time.Now()
|
||||
for {
|
||||
myIndexes := myControl.GetHostmapIndexCount()
|
||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
||||
if myIndexes == 0 {
|
||||
t.Fatal("myIndexes should not be 0")
|
||||
}
|
||||
if theirIndexes == 0 {
|
||||
t.Fatal("theirIndexes should not be 0, they should have rejected this bogus packet")
|
||||
}
|
||||
|
||||
since := time.Since(waitStart)
|
||||
r.Logf("my tunnels: %v; their tunnels: %v; duration: %v", myIndexes, theirIndexes, since)
|
||||
if since > time.Second*4 {
|
||||
t.Log("The tunnel would have been gone by now")
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
r.FlushAll()
|
||||
}
|
||||
|
||||
myControl.Stop()
|
||||
theirControl.Stop()
|
||||
}
|
||||
|
||||
+18
-66
@@ -138,32 +138,12 @@ listen:
|
||||
# max, net.core.rmem_max and net.core.wmem_max
|
||||
#read_buffer: 10485760
|
||||
#write_buffer: 10485760
|
||||
|
||||
# On Windows only
|
||||
# When true, Nebula installs a WFP (Windows Filtering Platform) PERMIT filter scoped to UDP at the listener port.
|
||||
# WFP sits below Windows Defender Firewall, so this lets peer handshakes reach Nebula's outside socket regardless
|
||||
# of WDF's inbound rules.
|
||||
# Default true; set to false to leave WDF in charge of inbound decisions on the listener port. Not reloadable.
|
||||
#windows_bypass_wdf: true
|
||||
|
||||
# On macOS only
|
||||
# macOS scopes the udp socket to the interface it was created on, so moving between networks (wifi to wired,
|
||||
# office to home) leaves Nebula sending out an interface that no longer has a route. When true, Nebula watches
|
||||
# the routing socket and rebinds the listener once the change settles.
|
||||
# iOS does not use this, the host app drives the same rebind itself.
|
||||
# Default true. Not reloadable.
|
||||
#rebind_on_network_change: true
|
||||
|
||||
# By default, Nebula replies to packets it has no tunnel for with a "recv_error" packet. This packet helps speed up reconnection
|
||||
# in the case that Nebula on either side did not shut down cleanly. This response can be abused as a way to discover if Nebula is running
|
||||
# on a host though. This option lets you configure if you want to send "recv_error" packets always, never, or only to private network remotes.
|
||||
# valid values: always, never, private
|
||||
# This setting is reloadable.
|
||||
#send_recv_error: always
|
||||
# Similar to send_recv_error, this option lets you configure if you want to accept "recv_error" packets from remote hosts.
|
||||
# valid values: always, never, private
|
||||
# This setting is reloadable.
|
||||
#accept_recv_error: always
|
||||
# The so_sock option is a Linux-specific feature that allows all outgoing Nebula packets to be tagged with a specific identifier.
|
||||
# This tagging enables IP rule-based filtering. For example, it supports 0.0.0.0/0 unsafe_routes,
|
||||
# allowing for more precise routing decisions based on the packet tags. Default is 0 meaning no mark is set.
|
||||
@@ -179,21 +159,17 @@ listen:
|
||||
|
||||
punchy:
|
||||
# Continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings
|
||||
# This setting is reloadable.
|
||||
punch: true
|
||||
|
||||
# respond means that a node you are trying to reach will connect back out to you if your hole punching fails
|
||||
# this is extremely useful if one node is behind a difficult nat, such as a symmetric NAT
|
||||
# Default is false
|
||||
# This setting is reloadable.
|
||||
#respond: true
|
||||
|
||||
# delays a punch response for misbehaving NATs, default is 1 second.
|
||||
# This setting is reloadable.
|
||||
#delay: 1s
|
||||
|
||||
# set the delay before attempting punchy.respond. Default is 5 seconds. respond must be true to take effect.
|
||||
# This setting is reloadable.
|
||||
#respond_delay: 5s
|
||||
|
||||
# Cipher allows you to choose between the available ciphers for your network. Options are chachapoly or aes
|
||||
@@ -224,12 +200,6 @@ punchy:
|
||||
# Trusted SSH CA public keys. These are the public keys of the CAs that are allowed to sign SSH keys for access.
|
||||
#trusted_cas:
|
||||
#- "ssh public key string"
|
||||
# sandbox_dir restricts file paths for profiling commands (start-cpu-profile, save-heap-profile,
|
||||
# save-mutex-profile) to the specified directory. Relative paths will be resolved within this directory,
|
||||
# and absolute paths outside of it will be rejected. Default is $TMP/nebula-debug.
|
||||
# The directory is NOT automatically created.
|
||||
# Overriding this to "" is the same as "/" and will allow overwriting any path on the host.
|
||||
#sandbox_dir: /var/tmp/nebula-debug
|
||||
|
||||
# EXPERIMENTAL: relay support for networks that can't establish direct connections.
|
||||
relay:
|
||||
@@ -302,24 +272,6 @@ tun:
|
||||
# metric: 100
|
||||
# install: true
|
||||
|
||||
# On Windows only, sets the network category of the nebula interface. Without this, Windows often
|
||||
# leaves the network as "Unidentified" and treats it as Public, which makes the host firewall more
|
||||
# restrictive than you usually want for an overlay between trusted peers. Valid values:
|
||||
# private - treat the nebula network as a private/trusted network (default)
|
||||
# public - treat it as a public/untrusted network
|
||||
# domain - treat it as a domain-authenticated network
|
||||
# unset - leave whatever Windows decided alone
|
||||
# Not reloadable.
|
||||
#network_category: private
|
||||
|
||||
# On Windows only
|
||||
# When true, Nebula installs a WFP (Windows Filtering Platform) PERMIT filter scoped to the nebula adapter LUID.
|
||||
# WFP sits below Windows Defender Firewall, so this lets inbound traffic through regardless of WDF rules.
|
||||
# Filters are auto-removed when the adapter goes away.
|
||||
# See listen.windows_bypass_wdf for the matching control over inbound to nebula's outside UDP listener.
|
||||
# Default true; set to false to leave WDF in charge of inbound decisions on the nebula interface. Not reloadable.
|
||||
#windows_bypass_wdf: true
|
||||
|
||||
# On linux only, set to true to manage unsafe routes directly on the system route table with gateway routes instead of
|
||||
# in nebula configuration files. Default false, not reloadable.
|
||||
#use_system_route_table: false
|
||||
@@ -330,21 +282,24 @@ tun:
|
||||
|
||||
# Configure logging level
|
||||
logging:
|
||||
# trace, debug, info, warn, or error. Default is info and is reloadable.
|
||||
# fatal and panic are accepted for backwards compatibility and map to error.
|
||||
#NOTE: Debug and trace modes can log remotely controlled/untrusted data which can quickly fill a disk in some
|
||||
# scenarios. Debug and trace logging are also CPU intensive and will decrease performance overall.
|
||||
# Only enable debug or trace logging while actively investigating an issue.
|
||||
# panic, fatal, error, warning, info, or debug. Default is info and is reloadable.
|
||||
#NOTE: Debug mode can log remotely controlled/untrusted data which can quickly fill a disk in some
|
||||
# scenarios. Debug logging is also CPU intensive and will decrease performance overall.
|
||||
# Only enable debug logging while actively investigating an issue.
|
||||
level: info
|
||||
# json or text formats currently available. Default is text.
|
||||
# json or text formats currently available. Default is text
|
||||
format: text
|
||||
# Disable timestamp logging. Useful when output is redirected to a logging system that already adds timestamps. Default is false.
|
||||
# Disable timestamp logging. useful when output is redirected to logging system that already adds timestamps. Default is false
|
||||
#disable_timestamp: true
|
||||
# Timestamps use RFC3339Nano ("2006-01-02T15:04:05.999999999Z07:00") and are not configurable.
|
||||
# timestamp format is specified in Go time format, see:
|
||||
# https://golang.org/pkg/time/#pkg-constants
|
||||
# default when `format: json`: "2006-01-02T15:04:05Z07:00" (RFC3339)
|
||||
# default when `format: text`:
|
||||
# when TTY attached: seconds since beginning of execution
|
||||
# otherwise: "2006-01-02T15:04:05Z07:00" (RFC3339)
|
||||
# As an example, to log as RFC3339 with millisecond precision, set to:
|
||||
#timestamp_format: "2006-01-02T15:04:05.000Z07:00"
|
||||
|
||||
# The stats section is reloadable. A HUP may change the backend, toggle stats
|
||||
# on or off, switch the listen/host address, or pick up new DNS for the
|
||||
# configured graphite host.
|
||||
#stats:
|
||||
#type: graphite
|
||||
#prefix: nebula
|
||||
@@ -362,12 +317,10 @@ logging:
|
||||
# enables counter metrics for meta packets
|
||||
# e.g.: `messages.tx.handshake`
|
||||
# NOTE: `message.{tx,rx}.recv_error` is always emitted
|
||||
# Not reloadable.
|
||||
#message_metrics: false
|
||||
|
||||
# enables detailed counter metrics for lighthouse packets
|
||||
# e.g.: `lighthouse.rx.HostQuery`
|
||||
# Not reloadable.
|
||||
#lighthouse_metrics: false
|
||||
|
||||
# Handshake Manager Settings
|
||||
@@ -405,7 +358,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 "Destination unreachable: Communication administratively prohibited" packet.
|
||||
# - For other protocols, this will be an ICMP port unreachable packet.
|
||||
outbound_action: drop
|
||||
inbound_action: drop
|
||||
|
||||
@@ -425,14 +378,13 @@ firewall:
|
||||
# Rules are comprised of a protocol, port, and one or more of host, group, or CIDR
|
||||
# Logical evaluation is roughly: port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND (local cidr)
|
||||
# - port: Takes `0` or `any` as any, a single number `80`, a range `200-901`, or `fragment` to match second and further fragments of fragmented packets (since there is no port available).
|
||||
# code: same as port but makes more sense when talking about ICMP, TODO: this is not currently implemented in a way that works, use `any`
|
||||
# proto: `any`, `tcp`, `udp`, or `icmp`
|
||||
# a port specification is ignored if proto is `icmp`
|
||||
# host: `any` or a literal hostname, ie `test-host`
|
||||
# group: `any` or a literal group name, ie `default-group`
|
||||
# groups: Same as group but accepts a list of values. Multiple values are AND'd together and a certificate would have to contain all groups to pass
|
||||
# cidr: a remote CIDR, `0.0.0.0/0` is any ipv4 and `::/0` is any ipv6. `any` means any ip family and address.
|
||||
# local_cidr: a local CIDR, `0.0.0.0/0` is any ipv4 and `::/0` is any ipv6. `any` means any ip family and address.
|
||||
# This can be used to filter destinations when using unsafe_routes.
|
||||
# cidr: a remote CIDR, `0.0.0.0/0` is any ipv4 and `::/0` is any ipv6.
|
||||
# local_cidr: a local CIDR, `0.0.0.0/0` is any ipv4 and `::/0` is any ipv6. This can be used to filter destinations when using unsafe_routes.
|
||||
# By default, this is set to only the VPN (overlay) networks assigned via the certificate networks field unless `default_local_cidr_any` is set to true.
|
||||
# If there are unsafe_routes present in this config file, `local_cidr` should be set appropriately for the intended us case.
|
||||
# ca_name: An issuing CA name
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/logging"
|
||||
"github.com/slackhq/nebula/overlay"
|
||||
"github.com/slackhq/nebula/service"
|
||||
)
|
||||
@@ -64,7 +64,8 @@ pki:
|
||||
return err
|
||||
}
|
||||
|
||||
logger := logging.NewLogger(os.Stdout)
|
||||
logger := logrus.New()
|
||||
logger.Out = os.Stdout
|
||||
|
||||
ctrl, err := nebula.Main(&cfg, false, "custom-app", logger, overlay.NewUserDeviceFromConfig)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,15 +8,6 @@ Before=sshd.service
|
||||
Type=notify
|
||||
NotifyAccess=main
|
||||
SyslogIdentifier=nebula
|
||||
|
||||
# Uncomment to run as an unprivileged user with only CAP_NET_ADMIN. Requires a
|
||||
# nebula user that owns the config directory. Add CAP_NET_BIND_SERVICE to both
|
||||
# lines if any listener (lighthouse DNS, listen.port, stats, sshd) binds <1024.
|
||||
#User=nebula
|
||||
#Group=nebula
|
||||
#CapabilityBoundingSet=CAP_NET_ADMIN
|
||||
#AmbientCapabilities=CAP_NET_ADMIN
|
||||
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml
|
||||
Restart=always
|
||||
|
||||
+195
-295
@@ -1,16 +1,13 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,13 +15,14 @@ import (
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"github.com/slackhq/nebula/config"
|
||||
"github.com/slackhq/nebula/firewall"
|
||||
)
|
||||
|
||||
type FirewallInterface interface {
|
||||
AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error
|
||||
AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, addr, localAddr netip.Prefix, caName string, caSha string) error
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
@@ -44,8 +42,8 @@ type Firewall struct {
|
||||
InRules *FirewallTable
|
||||
OutRules *FirewallTable
|
||||
|
||||
InboundSendReject bool
|
||||
OutboundSendReject bool
|
||||
InSendReject bool
|
||||
OutSendReject 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,9 +56,8 @@ type Firewall struct {
|
||||
routableNetworks *bart.Lite
|
||||
|
||||
// assignedNetworks is a list of vpn networks assigned to us in the certificate.
|
||||
assignedNetworks []netip.Prefix
|
||||
// unsafeNetworks is the list of unsafe networks issued to us in the certificate
|
||||
unsafeNetworks []netip.Prefix
|
||||
assignedNetworks []netip.Prefix
|
||||
hasUnsafeNetworks bool
|
||||
|
||||
rules string
|
||||
rulesVersion uint16
|
||||
@@ -69,7 +66,7 @@ type Firewall struct {
|
||||
incomingMetrics firewallMetrics
|
||||
outgoingMetrics firewallMetrics
|
||||
|
||||
l *slog.Logger
|
||||
l *logrus.Logger
|
||||
}
|
||||
|
||||
type firewallMetrics struct {
|
||||
@@ -133,7 +130,7 @@ type firewallLocalCIDR struct {
|
||||
|
||||
// NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts.
|
||||
// The certificate provided should be the highest version loaded in memory.
|
||||
func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall {
|
||||
func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall {
|
||||
//TODO: error on 0 duration
|
||||
var tmin, tmax time.Duration
|
||||
|
||||
@@ -159,9 +156,10 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
|
||||
assignedNetworks = append(assignedNetworks, network)
|
||||
}
|
||||
|
||||
unsafeNetworks := c.UnsafeNetworks()
|
||||
for _, n := range unsafeNetworks {
|
||||
hasUnsafeNetworks := false
|
||||
for _, n := range c.UnsafeNetworks() {
|
||||
routableNetworks.Insert(n)
|
||||
hasUnsafeNetworks = true
|
||||
}
|
||||
|
||||
return &Firewall{
|
||||
@@ -169,15 +167,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,
|
||||
unsafeNetworks: unsafeNetworks,
|
||||
l: l,
|
||||
InRules: newFirewallTable(),
|
||||
OutRules: newFirewallTable(),
|
||||
TCPTimeout: tcpTimeout,
|
||||
UDPTimeout: UDPTimeout,
|
||||
DefaultTimeout: defaultTimeout,
|
||||
routableNetworks: routableNetworks,
|
||||
assignedNetworks: assignedNetworks,
|
||||
hasUnsafeNetworks: hasUnsafeNetworks,
|
||||
l: l,
|
||||
|
||||
incomingMetrics: firewallMetrics{
|
||||
droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_addr", nil),
|
||||
@@ -192,7 +190,7 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
|
||||
}
|
||||
}
|
||||
|
||||
func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewall, error) {
|
||||
func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) {
|
||||
certificate := cs.getCertificate(cert.Version2)
|
||||
if certificate == nil {
|
||||
certificate = cs.getCertificate(cert.Version1)
|
||||
@@ -216,23 +214,23 @@ func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewal
|
||||
inboundAction := c.GetString("firewall.inbound_action", "drop")
|
||||
switch inboundAction {
|
||||
case "reject":
|
||||
fw.InboundSendReject = true
|
||||
fw.InSendReject = true
|
||||
case "drop":
|
||||
fw.InboundSendReject = false
|
||||
fw.InSendReject = false
|
||||
default:
|
||||
l.Warn("invalid firewall.inbound_action, defaulting to `drop`", "action", inboundAction)
|
||||
fw.InboundSendReject = false
|
||||
l.WithField("action", inboundAction).Warn("invalid firewall.inbound_action, defaulting to `drop`")
|
||||
fw.InSendReject = false
|
||||
}
|
||||
|
||||
outboundAction := c.GetString("firewall.outbound_action", "drop")
|
||||
switch outboundAction {
|
||||
case "reject":
|
||||
fw.OutboundSendReject = true
|
||||
fw.OutSendReject = true
|
||||
case "drop":
|
||||
fw.OutboundSendReject = false
|
||||
fw.OutSendReject = false
|
||||
default:
|
||||
l.Warn("invalid firewall.outbound_action, defaulting to `drop`", "action", outboundAction)
|
||||
fw.OutboundSendReject = false
|
||||
l.WithField("action", inboundAction).Warn("invalid firewall.outbound_action, defaulting to `drop`")
|
||||
fw.OutSendReject = false
|
||||
}
|
||||
|
||||
err := AddFirewallRulesFromConfig(l, false, c, fw)
|
||||
@@ -249,7 +247,32 @@ func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewal
|
||||
}
|
||||
|
||||
// AddRule properly creates the in memory rule structure for a firewall table.
|
||||
func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
|
||||
func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, ip, localIp netip.Prefix, caName string, caSha string) error {
|
||||
// Under gomobile, stringing a nil pointer with fmt causes an abort in debug mode for iOS
|
||||
// https://github.com/golang/go/issues/14131
|
||||
sIp := ""
|
||||
if ip.IsValid() {
|
||||
sIp = ip.String()
|
||||
}
|
||||
lIp := ""
|
||||
if localIp.IsValid() {
|
||||
lIp = localIp.String()
|
||||
}
|
||||
|
||||
// We need this rule string because we generate a hash. Removing this will break firewall reload.
|
||||
ruleString := fmt.Sprintf(
|
||||
"incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
|
||||
incoming, proto, startPort, endPort, groups, host, sIp, lIp, caName, caSha,
|
||||
)
|
||||
f.rules += ruleString + "\n"
|
||||
|
||||
direction := "incoming"
|
||||
if !incoming {
|
||||
direction = "outgoing"
|
||||
}
|
||||
f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "ip": sIp, "localIp": lIp, "caName": caName, "caSha": caSha}).
|
||||
Info("Firewall rule added")
|
||||
|
||||
var (
|
||||
ft *FirewallTable
|
||||
fp firewallPort
|
||||
@@ -267,12 +290,6 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
|
||||
case firewall.ProtoUDP:
|
||||
fp = ft.UDP
|
||||
case firewall.ProtoICMP, firewall.ProtoICMPv6:
|
||||
//ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
|
||||
if startPort != firewall.PortAny {
|
||||
f.l.Warn("ignoring port specification for ICMP firewall rule", "startPort", startPort)
|
||||
}
|
||||
startPort = firewall.PortAny
|
||||
endPort = firewall.PortAny
|
||||
fp = ft.ICMP
|
||||
case firewall.ProtoAny:
|
||||
fp = ft.AnyProto
|
||||
@@ -280,22 +297,7 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
|
||||
return fmt.Errorf("unknown protocol %v", proto)
|
||||
}
|
||||
|
||||
// We need this rule string because we generate a hash. Removing this will break firewall reload.
|
||||
ruleString := fmt.Sprintf(
|
||||
"incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
|
||||
incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
|
||||
)
|
||||
f.rules += ruleString + "\n"
|
||||
|
||||
direction := "incoming"
|
||||
if !incoming {
|
||||
direction = "outgoing"
|
||||
}
|
||||
f.l.Info("Firewall rule added",
|
||||
"firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha},
|
||||
)
|
||||
|
||||
return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
|
||||
return fp.addRule(f, startPort, endPort, groups, host, ip, localIp, caName, caSha)
|
||||
}
|
||||
|
||||
// GetRuleHash returns a hash representation of all inbound and outbound rules
|
||||
@@ -316,7 +318,7 @@ func (f *Firewall) GetRuleHashes() string {
|
||||
return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)
|
||||
}
|
||||
|
||||
func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw FirewallInterface) error {
|
||||
func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error {
|
||||
var table string
|
||||
if inbound {
|
||||
table = "firewall.inbound"
|
||||
@@ -335,6 +337,7 @@ func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw Fi
|
||||
}
|
||||
|
||||
for i, t := range rs {
|
||||
var groups []string
|
||||
r, err := convertRule(l, t, table, i)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rule #%v; %s", table, i, err)
|
||||
@@ -344,10 +347,23 @@ func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw Fi
|
||||
return fmt.Errorf("%s rule #%v; only one of port or code should be provided", table, i)
|
||||
}
|
||||
|
||||
if r.Host == "" && len(r.Groups) == 0 && r.Cidr == "" && r.LocalCidr == "" && r.CAName == "" && r.CASha == "" {
|
||||
if r.Host == "" && len(r.Groups) == 0 && r.Group == "" && r.Cidr == "" && r.LocalCidr == "" && r.CAName == "" && r.CASha == "" {
|
||||
return fmt.Errorf("%s rule #%v; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided", table, i)
|
||||
}
|
||||
|
||||
if len(r.Groups) > 0 {
|
||||
groups = r.Groups
|
||||
}
|
||||
|
||||
if r.Group != "" {
|
||||
// Check if we have both groups and group provided in the rule config
|
||||
if len(groups) > 0 {
|
||||
return fmt.Errorf("%s rule #%v; only one of group or groups should be defined, both provided", table, i)
|
||||
}
|
||||
|
||||
groups = []string{r.Group}
|
||||
}
|
||||
|
||||
var sPort, errPort string
|
||||
if r.Code != "" {
|
||||
errPort = "code"
|
||||
@@ -357,55 +373,42 @@ func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw Fi
|
||||
sPort = r.Port
|
||||
}
|
||||
|
||||
var proto uint8
|
||||
var startPort, endPort int32
|
||||
switch r.Proto {
|
||||
case "any":
|
||||
proto = firewall.ProtoAny
|
||||
startPort, endPort, err = parsePort(sPort)
|
||||
case "tcp":
|
||||
proto = firewall.ProtoTCP
|
||||
startPort, endPort, err = parsePort(sPort)
|
||||
case "udp":
|
||||
proto = firewall.ProtoUDP
|
||||
startPort, endPort, err = parsePort(sPort)
|
||||
case "icmp":
|
||||
proto = firewall.ProtoICMP
|
||||
startPort = firewall.PortAny
|
||||
endPort = firewall.PortAny
|
||||
if sPort != "" {
|
||||
l.Warn("ignoring port specification for ICMP firewall rule", "port", sPort)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
|
||||
}
|
||||
startPort, endPort, err := parsePort(sPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
|
||||
}
|
||||
|
||||
if r.Cidr != "" && r.Cidr != "any" {
|
||||
_, err = netip.ParsePrefix(r.Cidr)
|
||||
var proto uint8
|
||||
switch r.Proto {
|
||||
case "any":
|
||||
proto = firewall.ProtoAny
|
||||
case "tcp":
|
||||
proto = firewall.ProtoTCP
|
||||
case "udp":
|
||||
proto = firewall.ProtoUDP
|
||||
case "icmp":
|
||||
proto = firewall.ProtoICMP
|
||||
default:
|
||||
return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
|
||||
}
|
||||
|
||||
var cidr netip.Prefix
|
||||
if r.Cidr != "" {
|
||||
cidr, err = netip.ParsePrefix(r.Cidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rule #%v; cidr did not parse; %s", table, i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.LocalCidr != "" && r.LocalCidr != "any" {
|
||||
_, err = netip.ParsePrefix(r.LocalCidr)
|
||||
var localCidr netip.Prefix
|
||||
if r.LocalCidr != "" {
|
||||
localCidr, err = netip.ParsePrefix(r.LocalCidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rule #%v; local_cidr did not parse; %s", table, i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if warning := r.sanity(); warning != nil {
|
||||
l.Warn("firewall rule sanity check",
|
||||
"table", table,
|
||||
"rule", i,
|
||||
"warning", warning,
|
||||
)
|
||||
}
|
||||
|
||||
err = fw.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha)
|
||||
err = fw.AddRule(inbound, proto, startPort, endPort, groups, r.Host, cidr, localCidr, r.CAName, r.CASha)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s rule #%v; `%s`", table, i, err)
|
||||
}
|
||||
@@ -414,40 +417,30 @@ func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw Fi
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrUnknownNetworkType = errors.New("unknown network type")
|
||||
var ErrPeerRejected = errors.New("remote address is not within a network that we handle")
|
||||
var ErrInvalidRemoteIP = errors.New("remote address is not in remote certificate networks")
|
||||
var ErrInvalidLocalIP = errors.New("local address is not in list of handled local addresses")
|
||||
var ErrInvalidRemoteIP = errors.New("remote IP is not in remote certificate subnets")
|
||||
var ErrInvalidLocalIP = errors.New("local IP is not in list of handled local IPs")
|
||||
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 {
|
||||
// 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
|
||||
if h.vpnAddrs[0] != fp.RemoteAddr {
|
||||
// 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
|
||||
if h.networks != nil {
|
||||
if !h.networks.Contains(fp.RemoteAddr) {
|
||||
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
||||
return ErrInvalidRemoteIP
|
||||
}
|
||||
} else {
|
||||
nwType, ok := h.networks.Lookup(fp.RemoteAddr)
|
||||
if !ok {
|
||||
// Simple case: Certificate has one address and no unsafe networks
|
||||
if h.vpnAddrs[0] != fp.RemoteAddr {
|
||||
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
||||
return ErrInvalidRemoteIP
|
||||
}
|
||||
switch nwType {
|
||||
case NetworkTypeVPN:
|
||||
break // nothing special
|
||||
case NetworkTypeVPNPeer:
|
||||
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
||||
return ErrPeerRejected // reject for now, one day this may have different FW rules
|
||||
case NetworkTypeUnsafe:
|
||||
break // nothing special, one day this may have different FW rules
|
||||
default:
|
||||
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
||||
return ErrUnknownNetworkType //should never happen
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we are supposed to be handling this local ip address
|
||||
@@ -456,11 +449,6 @@ 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
|
||||
@@ -486,7 +474,7 @@ func (f *Firewall) metrics(incoming bool) firewallMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy cleans up any known cyclical references so the object can be freed by GC. This should be called if a new
|
||||
// Destroy cleans up any known cyclical references so the object can be free'd my GC. This should be called if a new
|
||||
// firewall object is created
|
||||
func (f *Firewall) Destroy() {
|
||||
//TODO: clean references if/when needed
|
||||
@@ -534,26 +522,26 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
|
||||
|
||||
// We now know which firewall table to check against
|
||||
if !table.match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
h.logger(f.l).Debug("dropping old conntrack entry, does not match new ruleset",
|
||||
"fwPacket", fp,
|
||||
"incoming", c.incoming,
|
||||
"rulesVersion", f.rulesVersion,
|
||||
"oldRulesVersion", c.rulesVersion,
|
||||
)
|
||||
if f.l.Level >= logrus.DebugLevel {
|
||||
h.logger(f.l).
|
||||
WithField("fwPacket", fp).
|
||||
WithField("incoming", c.incoming).
|
||||
WithField("rulesVersion", f.rulesVersion).
|
||||
WithField("oldRulesVersion", c.rulesVersion).
|
||||
Debugln("dropping old conntrack entry, does not match new ruleset")
|
||||
}
|
||||
delete(conntrack.Conns, fp)
|
||||
conntrack.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
h.logger(f.l).Debug("keeping old conntrack entry, does match new ruleset",
|
||||
"fwPacket", fp,
|
||||
"incoming", c.incoming,
|
||||
"rulesVersion", f.rulesVersion,
|
||||
"oldRulesVersion", c.rulesVersion,
|
||||
)
|
||||
if f.l.Level >= logrus.DebugLevel {
|
||||
h.logger(f.l).
|
||||
WithField("fwPacket", fp).
|
||||
WithField("incoming", c.incoming).
|
||||
WithField("rulesVersion", f.rulesVersion).
|
||||
WithField("oldRulesVersion", c.rulesVersion).
|
||||
Debugln("keeping old conntrack entry, does match new ruleset")
|
||||
}
|
||||
|
||||
c.rulesVersion = f.rulesVersion
|
||||
@@ -652,7 +640,7 @@ func (ft *FirewallTable) match(p firewall.Packet, incoming bool, c *cert.CachedC
|
||||
return false
|
||||
}
|
||||
|
||||
func (fp firewallPort) addRule(f *Firewall, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
|
||||
func (fp firewallPort) addRule(f *Firewall, startPort int32, endPort int32, groups []string, host string, ip, localIp netip.Prefix, caName string, caSha string) error {
|
||||
if startPort > endPort {
|
||||
return fmt.Errorf("start port was lower than end port")
|
||||
}
|
||||
@@ -665,7 +653,7 @@ func (fp firewallPort) addRule(f *Firewall, startPort int32, endPort int32, grou
|
||||
}
|
||||
}
|
||||
|
||||
if err := fp[i].addRule(f, groups, host, cidr, localCidr, caName, caSha); err != nil {
|
||||
if err := fp[i].addRule(f, groups, host, ip, localIp, caName, caSha); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -679,13 +667,6 @@ func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCer
|
||||
return false
|
||||
}
|
||||
|
||||
// this branch is here to catch traffic from FirewallTable.Any.match and FirewallTable.ICMP.match
|
||||
if p.Protocol == firewall.ProtoICMP || p.Protocol == firewall.ProtoICMPv6 {
|
||||
// port numbers are re-used for connection tracking of ICMP,
|
||||
// but we don't want to actually filter on them.
|
||||
return fp[firewall.PortAny].match(p, c, caPool)
|
||||
}
|
||||
|
||||
var port int32
|
||||
|
||||
if p.Fragment {
|
||||
@@ -703,7 +684,7 @@ func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCer
|
||||
return fp[firewall.PortAny].match(p, c, caPool)
|
||||
}
|
||||
|
||||
func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, cidr, localCidr, caName, caSha string) error {
|
||||
func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, ip, localIp netip.Prefix, caName, caSha string) error {
|
||||
fr := func() *FirewallRule {
|
||||
return &FirewallRule{
|
||||
Hosts: make(map[string]*firewallLocalCIDR),
|
||||
@@ -717,14 +698,14 @@ func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, cidr, l
|
||||
fc.Any = fr()
|
||||
}
|
||||
|
||||
return fc.Any.addRule(f, groups, host, cidr, localCidr)
|
||||
return fc.Any.addRule(f, groups, host, ip, localIp)
|
||||
}
|
||||
|
||||
if caSha != "" {
|
||||
if _, ok := fc.CAShas[caSha]; !ok {
|
||||
fc.CAShas[caSha] = fr()
|
||||
}
|
||||
err := fc.CAShas[caSha].addRule(f, groups, host, cidr, localCidr)
|
||||
err := fc.CAShas[caSha].addRule(f, groups, host, ip, localIp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -734,7 +715,7 @@ func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, cidr, l
|
||||
if _, ok := fc.CANames[caName]; !ok {
|
||||
fc.CANames[caName] = fr()
|
||||
}
|
||||
err := fc.CANames[caName].addRule(f, groups, host, cidr, localCidr)
|
||||
err := fc.CANames[caName].addRule(f, groups, host, ip, localIp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -766,24 +747,24 @@ func (fc *FirewallCA) match(p firewall.Packet, c *cert.CachedCertificate, caPool
|
||||
return fc.CANames[s.Certificate.Name()].match(p, c)
|
||||
}
|
||||
|
||||
func (fr *FirewallRule) addRule(f *Firewall, groups []string, host, cidr, localCidr string) error {
|
||||
func (fr *FirewallRule) addRule(f *Firewall, groups []string, host string, ip, localCIDR netip.Prefix) error {
|
||||
flc := func() *firewallLocalCIDR {
|
||||
return &firewallLocalCIDR{
|
||||
LocalCIDR: new(bart.Lite),
|
||||
}
|
||||
}
|
||||
|
||||
if fr.isAny(groups, host, cidr) {
|
||||
if fr.isAny(groups, host, ip) {
|
||||
if fr.Any == nil {
|
||||
fr.Any = flc()
|
||||
}
|
||||
|
||||
return fr.Any.addRule(f, localCidr)
|
||||
return fr.Any.addRule(f, localCIDR)
|
||||
}
|
||||
|
||||
if len(groups) > 0 {
|
||||
nlc := flc()
|
||||
err := nlc.addRule(f, localCidr)
|
||||
err := nlc.addRule(f, localCIDR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -799,46 +780,44 @@ func (fr *FirewallRule) addRule(f *Firewall, groups []string, host, cidr, localC
|
||||
if nlc == nil {
|
||||
nlc = flc()
|
||||
}
|
||||
err := nlc.addRule(f, localCidr)
|
||||
err := nlc.addRule(f, localCIDR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fr.Hosts[host] = nlc
|
||||
}
|
||||
|
||||
if cidr != "" {
|
||||
c, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nlc, _ := fr.CIDR.Get(c)
|
||||
if ip.IsValid() {
|
||||
nlc, _ := fr.CIDR.Get(ip)
|
||||
if nlc == nil {
|
||||
nlc = flc()
|
||||
}
|
||||
err = nlc.addRule(f, localCidr)
|
||||
err := nlc.addRule(f, localCIDR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fr.CIDR.Insert(c, nlc)
|
||||
fr.CIDR.Insert(ip, nlc)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fr *FirewallRule) isAny(groups []string, host string, cidr string) bool {
|
||||
if len(groups) == 0 && host == "" && cidr == "" {
|
||||
func (fr *FirewallRule) isAny(groups []string, host string, ip netip.Prefix) bool {
|
||||
if len(groups) == 0 && host == "" && !ip.IsValid() {
|
||||
return true
|
||||
}
|
||||
|
||||
if slices.Contains(groups, "any") {
|
||||
return true
|
||||
for _, group := range groups {
|
||||
if group == "any" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if host == "any" {
|
||||
return true
|
||||
}
|
||||
|
||||
if cidr == "any" {
|
||||
if ip.IsValid() && ip.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -890,14 +869,9 @@ func (fr *FirewallRule) match(p firewall.Packet, c *cert.CachedCertificate) bool
|
||||
return false
|
||||
}
|
||||
|
||||
func (flc *firewallLocalCIDR) addRule(f *Firewall, localCidr string) error {
|
||||
if localCidr == "any" {
|
||||
flc.Any = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if localCidr == "" {
|
||||
if len(f.unsafeNetworks) == 0 || f.defaultLocalCIDRAny {
|
||||
func (flc *firewallLocalCIDR) addRule(f *Firewall, localIp netip.Prefix) error {
|
||||
if !localIp.IsValid() {
|
||||
if !f.hasUnsafeNetworks || f.defaultLocalCIDRAny {
|
||||
flc.Any = true
|
||||
return nil
|
||||
}
|
||||
@@ -907,13 +881,12 @@ func (flc *firewallLocalCIDR) addRule(f *Firewall, localCidr string) error {
|
||||
}
|
||||
return nil
|
||||
|
||||
} else if localIp.Bits() == 0 {
|
||||
flc.Any = true
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := netip.ParsePrefix(localCidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flc.LocalCIDR.Insert(c)
|
||||
flc.LocalCIDR.Insert(localIp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -934,6 +907,7 @@ type rule struct {
|
||||
Code string
|
||||
Proto string
|
||||
Host string
|
||||
Group string
|
||||
Groups []string
|
||||
Cidr string
|
||||
LocalCidr string
|
||||
@@ -941,7 +915,7 @@ type rule struct {
|
||||
CASha string
|
||||
}
|
||||
|
||||
func convertRule(l *slog.Logger, p any, table string, i int) (rule, error) {
|
||||
func convertRule(l *logrus.Logger, p any, table string, i int) (rule, error) {
|
||||
r := rule{}
|
||||
|
||||
m, ok := p.(map[string]any)
|
||||
@@ -972,14 +946,10 @@ func convertRule(l *slog.Logger, p any, table string, i int) (rule, error) {
|
||||
return r, errors.New("group should contain a single value, an array with more than one entry was provided")
|
||||
}
|
||||
|
||||
l.Warn("group was an array with a single value, converting to simple value",
|
||||
"table", table,
|
||||
"rule", i,
|
||||
)
|
||||
l.Warnf("%s rule #%v; group was an array with a single value, converting to simple value", table, i)
|
||||
m["group"] = v[0]
|
||||
}
|
||||
|
||||
singleGroup := toString("group", m)
|
||||
r.Group = toString("group", m)
|
||||
|
||||
if rg, ok := m["groups"]; ok {
|
||||
switch reflect.TypeOf(rg).Kind() {
|
||||
@@ -996,122 +966,52 @@ func convertRule(l *slog.Logger, p any, table string, i int) (rule, error) {
|
||||
}
|
||||
}
|
||||
|
||||
//flatten group vs groups
|
||||
if singleGroup != "" {
|
||||
// Check if we have both groups and group provided in the rule config
|
||||
if len(r.Groups) > 0 {
|
||||
return r, fmt.Errorf("only one of group or groups should be defined, both provided")
|
||||
}
|
||||
r.Groups = []string{singleGroup}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// sanity returns an error if the rule would be evaluated in a way that would short-circuit a configured check on a wildcard value
|
||||
// rules are evaluated as "port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND local_cidr"
|
||||
func (r *rule) sanity() error {
|
||||
//port, proto, local_cidr are AND, no need to check here
|
||||
//ca_sha and ca_name don't have a wildcard value, no need to check here
|
||||
groupsEmpty := len(r.Groups) == 0
|
||||
hostEmpty := r.Host == ""
|
||||
cidrEmpty := r.Cidr == ""
|
||||
|
||||
if (groupsEmpty && hostEmpty && cidrEmpty) == true {
|
||||
return nil //no content!
|
||||
}
|
||||
|
||||
groupsHasAny := slices.Contains(r.Groups, "any")
|
||||
if groupsHasAny && len(r.Groups) > 1 {
|
||||
return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the other groups specified", r.Groups)
|
||||
}
|
||||
|
||||
if r.Host == "any" {
|
||||
if !groupsEmpty {
|
||||
return fmt.Errorf("groups specified as %s, but host=any will match any host, regardless of groups", r.Groups)
|
||||
}
|
||||
|
||||
if !cidrEmpty {
|
||||
return fmt.Errorf("cidr specified as %s, but host=any will match any host, regardless of cidr", r.Cidr)
|
||||
}
|
||||
}
|
||||
|
||||
if groupsHasAny {
|
||||
if !hostEmpty && r.Host != "any" {
|
||||
return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified host %s", r.Groups, r.Host)
|
||||
}
|
||||
if !cidrEmpty {
|
||||
return fmt.Errorf("groups spec [%s] contains the group '\"any\". This rule will ignore the specified cidr %s", r.Groups, r.Cidr)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Code != "" {
|
||||
return fmt.Errorf("code specified as [%s]. Support for 'code' will be dropped in a future release, as it has never been functional", r.Code)
|
||||
}
|
||||
|
||||
//todo alert on cidr-any
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePort(s string) (int32, int32, error) {
|
||||
const notAPort int32 = -2
|
||||
func parsePort(s string) (startPort, endPort int32, err error) {
|
||||
if s == "any" {
|
||||
return firewall.PortAny, firewall.PortAny, nil
|
||||
}
|
||||
if s == "fragment" {
|
||||
return firewall.PortFragment, firewall.PortFragment, nil
|
||||
}
|
||||
if !strings.Contains(s, `-`) {
|
||||
rPort, err := parsePortValue("", s)
|
||||
if err != nil {
|
||||
return notAPort, notAPort, err
|
||||
}
|
||||
return rPort, rPort, nil
|
||||
}
|
||||
|
||||
sPorts := strings.SplitN(s, `-`, 2)
|
||||
for i := range sPorts {
|
||||
sPorts[i] = strings.Trim(sPorts[i], " ")
|
||||
}
|
||||
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
|
||||
return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
|
||||
}
|
||||
|
||||
startPort, err := parsePortValue("beginning range ", sPorts[0])
|
||||
if err != nil {
|
||||
return notAPort, notAPort, err
|
||||
}
|
||||
|
||||
endPort, err := parsePortValue("ending range ", sPorts[1])
|
||||
if err != nil {
|
||||
return notAPort, notAPort, err
|
||||
}
|
||||
|
||||
if startPort == firewall.PortAny {
|
||||
startPort = firewall.PortAny
|
||||
endPort = firewall.PortAny
|
||||
|
||||
} else if s == "fragment" {
|
||||
startPort = firewall.PortFragment
|
||||
endPort = firewall.PortFragment
|
||||
|
||||
} else if strings.Contains(s, `-`) {
|
||||
sPorts := strings.SplitN(s, `-`, 2)
|
||||
sPorts[0] = strings.Trim(sPorts[0], " ")
|
||||
sPorts[1] = strings.Trim(sPorts[1], " ")
|
||||
|
||||
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
|
||||
return 0, 0, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
|
||||
}
|
||||
|
||||
rStartPort, err := strconv.Atoi(sPorts[0])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
|
||||
}
|
||||
|
||||
rEndPort, err := strconv.Atoi(sPorts[1])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
|
||||
}
|
||||
|
||||
startPort = int32(rStartPort)
|
||||
endPort = int32(rEndPort)
|
||||
|
||||
if startPort == firewall.PortAny {
|
||||
endPort = firewall.PortAny
|
||||
}
|
||||
|
||||
} else {
|
||||
rPort, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("was not a number; `%s`", s)
|
||||
}
|
||||
startPort = int32(rPort)
|
||||
endPort = startPort
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
+10
-18
@@ -1,10 +1,10 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ConntrackCache is used as a local routine cache to know if a given flow
|
||||
@@ -15,49 +15,41 @@ type ConntrackCacheTicker struct {
|
||||
cacheV uint64
|
||||
cacheTick atomic.Uint64
|
||||
|
||||
l *slog.Logger
|
||||
cache ConntrackCache
|
||||
}
|
||||
|
||||
func NewConntrackCacheTicker(ctx context.Context, l *slog.Logger, d time.Duration) *ConntrackCacheTicker {
|
||||
func NewConntrackCacheTicker(d time.Duration) *ConntrackCacheTicker {
|
||||
if d == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := &ConntrackCacheTicker{
|
||||
l: l,
|
||||
cache: ConntrackCache{},
|
||||
}
|
||||
|
||||
go c.tick(ctx, d)
|
||||
go c.tick(d)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ConntrackCacheTicker) tick(ctx context.Context, d time.Duration) {
|
||||
t := time.NewTicker(d)
|
||||
defer t.Stop()
|
||||
func (c *ConntrackCacheTicker) tick(d time.Duration) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
c.cacheTick.Add(1)
|
||||
}
|
||||
time.Sleep(d)
|
||||
c.cacheTick.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Get checks if the cache ticker has moved to the next version before returning
|
||||
// the map. If it has moved, we reset the map.
|
||||
func (c *ConntrackCacheTicker) Get() ConntrackCache {
|
||||
func (c *ConntrackCacheTicker) Get(l *logrus.Logger) ConntrackCache {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if tick := c.cacheTick.Load(); tick != c.cacheV {
|
||||
c.cacheV = tick
|
||||
if ll := len(c.cache); ll > 0 {
|
||||
if c.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||
c.l.Debug("resetting conntrack cache", "len", ll)
|
||||
if l.Level == logrus.DebugLevel {
|
||||
l.WithField("len", ll).Debug("resetting conntrack cache")
|
||||
}
|
||||
c.cache = make(ConntrackCache, ll)
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/slackhq/nebula/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// The tests below pin the log format produced by ConntrackCacheTicker.Get
|
||||
// so changes cannot silently break what operators are grepping for. The
|
||||
// ticker's internal state (cache + cacheTick) is poked directly to avoid
|
||||
// racing a goroutine-driven tick in tests.
|
||||
|
||||
func newFixedTicker(t *testing.T, l *slog.Logger, cacheLen int) *ConntrackCacheTicker {
|
||||
t.Helper()
|
||||
c := &ConntrackCacheTicker{
|
||||
l: l,
|
||||
cache: make(ConntrackCache, cacheLen),
|
||||
}
|
||||
for i := 0; i < cacheLen; i++ {
|
||||
c.cache[Packet{LocalPort: uint16(i) + 1}] = struct{}{}
|
||||
}
|
||||
c.cacheTick.Store(1) // cacheV starts at 0, so Get() takes the reset path
|
||||
return c
|
||||
}
|
||||
|
||||
func TestConntrackCacheTicker_Get_TextFormat(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelDebug)
|
||||
|
||||
c := newFixedTicker(t, l, 3)
|
||||
c.Get()
|
||||
|
||||
assert.Equal(t, "level=DEBUG msg=\"resetting conntrack cache\" len=3\n", buf.String())
|
||||
}
|
||||
|
||||
func TestConntrackCacheTicker_Get_JSONFormat(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
l := test.NewJSONLoggerWithOutput(buf, slog.LevelDebug)
|
||||
|
||||
c := newFixedTicker(t, l, 2)
|
||||
c.Get()
|
||||
|
||||
assert.JSONEq(t, `{"level":"DEBUG","msg":"resetting conntrack cache","len":2}`, strings.TrimSpace(buf.String()))
|
||||
}
|
||||
|
||||
func TestConntrackCacheTicker_Get_QuietBelowDebug(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelInfo)
|
||||
|
||||
c := newFixedTicker(t, l, 5)
|
||||
c.Get()
|
||||
|
||||
assert.Empty(t, buf.String())
|
||||
}
|
||||
|
||||
func TestConntrackCacheTicker_Get_QuietWhenCacheEmpty(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelDebug)
|
||||
|
||||
c := newFixedTicker(t, l, 0)
|
||||
c.Get()
|
||||
|
||||
assert.Empty(t, buf.String())
|
||||
}
|
||||
+1
-6
@@ -22,10 +22,7 @@ const (
|
||||
type Packet struct {
|
||||
LocalAddr netip.Addr
|
||||
RemoteAddr netip.Addr
|
||||
// LocalPort is the destination port for incoming traffic, or the source port for outgoing. Zero for ICMP.
|
||||
LocalPort uint16
|
||||
// RemotePort is the source port for incoming traffic, or the destination port for outgoing.
|
||||
// For ICMP, it's the "identifier". This is only used for connection tracking, actual firewall rules will not filter on ICMP identifier
|
||||
LocalPort uint16
|
||||
RemotePort uint16
|
||||
Protocol uint8
|
||||
Fragment bool
|
||||
@@ -49,8 +46,6 @@ func (fp Packet) MarshalJSON() ([]byte, error) {
|
||||
proto = "tcp"
|
||||
case ProtoICMP:
|
||||
proto = "icmp"
|
||||
case ProtoICMPv6:
|
||||
proto = "icmpv6"
|
||||
case ProtoUDP:
|
||||
proto = "udp"
|
||||
default:
|
||||
|
||||
+124
-779
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,37 @@
|
||||
module github.com/slackhq/nebula
|
||||
|
||||
go 1.26.0
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
filippo.io/bigmod v0.1.0
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
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.29.0
|
||||
github.com/gaissmai/bart v0.25.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/kardianos/service v1.3.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/miekg/pkcs11 v1.1.2
|
||||
github.com/kardianos/service v1.2.4
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b
|
||||
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.54.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/net v0.45.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/term v0.36.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
google.golang.org/protobuf v1.36.8
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe
|
||||
)
|
||||
@@ -50,7 +48,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.36.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
|
||||
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -26,8 +24,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.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI=
|
||||
github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/gaissmai/bart v0.25.0 h1:eqiokVPqM3F94vJ0bTHXHtH91S8zkKL+bKh+BsGOsJM=
|
||||
github.com/gaissmai/bart v0.25.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 +64,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.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI=
|
||||
github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||
github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
|
||||
github.com/kardianos/service v1.2.4/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=
|
||||
@@ -85,10 +83,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
|
||||
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b h1:J/AzCvg5z0Hn1rqZUJjpbzALUmkKX0Zwbc/i4fw7Sfk=
|
||||
github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
@@ -133,6 +131,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
|
||||
@@ -155,23 +155,21 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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 +180,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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
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 +189,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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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=
|
||||
@@ -206,13 +204,14 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
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 +222,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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
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 +232,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 v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
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=
|
||||
@@ -243,8 +242,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package handshake
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"github.com/flynn/noise"
|
||||
"github.com/slackhq/nebula/cert"
|
||||
)
|
||||
|
||||
// Credential holds everything needed to participate in a handshake
|
||||
// at a given cert version. Version and Curve are read from Cert; the public
|
||||
// half of the static keypair likewise comes from Cert.PublicKey().
|
||||
type Credential struct {
|
||||
Cert cert.Certificate // the certificate
|
||||
Bytes []byte // pre-marshaled certificate bytes
|
||||
privateKey []byte // static private key (public half lives in Cert)
|
||||
cipherSuite noise.CipherSuite // pre-built cipher suite (DH + cipher + hash)
|
||||
}
|
||||
|
||||
// NewCredential creates a Credential with all material needed for handshake
|
||||
// participation. The cipherSuite should be pre-built by the caller with the
|
||||
// appropriate DH function, cipher, and hash.
|
||||
func NewCredential(
|
||||
c cert.Certificate,
|
||||
hsBytes []byte,
|
||||
privateKey []byte,
|
||||
cipherSuite noise.CipherSuite,
|
||||
) *Credential {
|
||||
return &Credential{
|
||||
Cert: c,
|
||||
Bytes: hsBytes,
|
||||
privateKey: privateKey,
|
||||
cipherSuite: cipherSuite,
|
||||
}
|
||||
}
|
||||
|
||||
// buildHandshakeState creates a noise.HandshakeState from this credential.
|
||||
func (hc *Credential) buildHandshakeState(initiator bool, pattern noise.HandshakePattern) (*noise.HandshakeState, error) {
|
||||
return noise.NewHandshakeState(noise.Config{
|
||||
CipherSuite: hc.cipherSuite,
|
||||
Random: rand.Reader,
|
||||
Pattern: pattern,
|
||||
Initiator: initiator,
|
||||
StaticKeypair: noise.DHKey{Private: hc.privateKey, Public: hc.Cert.PublicKey()},
|
||||
PresharedKey: []byte{},
|
||||
PresharedKeyPlacement: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// GetCredentialFunc returns the handshake credential for the given version,
|
||||
// or nil if that version is not available.
|
||||
//
|
||||
// Implementations must return credentials drawn from a snapshot stable for
|
||||
// the lifetime of any single Machine. The Machine may call this multiple
|
||||
// times during a handshake (e.g. when negotiating to the peer's version)
|
||||
// and assumes the underlying static keypair is consistent across calls.
|
||||
type GetCredentialFunc func(v cert.Version) *Credential
|
||||
@@ -1,22 +0,0 @@
|
||||
package handshake
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInitiateOnResponder = errors.New("initiate called on responder")
|
||||
ErrInitiateAlreadyCalled = errors.New("initiate already called")
|
||||
ErrInitiateNotCalled = errors.New("initiate must be called before ProcessPacket for initiators")
|
||||
ErrPacketTooShort = errors.New("packet too short")
|
||||
ErrPublicKeyMismatch = errors.New("public key mismatch between certificate and handshake")
|
||||
ErrIncompleteHandshake = errors.New("handshake completed without receiving required content")
|
||||
ErrMachineFailed = errors.New("handshake machine has failed")
|
||||
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")
|
||||
ErrMultiMessageUnsupported = errors.New("multi-message handshake patterns are not yet supported by the manager")
|
||||
ErrSubtypeMismatch = errors.New("packet subtype does not match handshake machine subtype")
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user