mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 21:16:59 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7423d39f9 | |||
| befba57366 | |||
| 2d128a3254 | |||
| c8980d34cf | |||
| 98f264cf14 | |||
| aa44f4c7c9 | |||
| 419157c407 | |||
| 0864852d33 | |||
| 2b5aec9a18 | |||
| f0665bee20 | |||
| 11da0baab1 | |||
| 608904b9dd | |||
| fd1c52127f |
@@ -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
|
name: Build Linux/BSD All
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
mv build/*.tar.gz release
|
mv build/*.tar.gz release
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-latest
|
name: linux-latest
|
||||||
path: release
|
path: release
|
||||||
@@ -32,15 +32,12 @@ jobs:
|
|||||||
build-windows:
|
build-windows:
|
||||||
name: Build Windows
|
name: Build Windows
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -57,15 +54,8 @@ jobs:
|
|||||||
mkdir build\dist\windows
|
mkdir build\dist\windows
|
||||||
mv dist\windows\wintun 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
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-latest
|
name: windows-latest
|
||||||
path: build
|
path: build
|
||||||
@@ -76,16 +66,16 @@ jobs:
|
|||||||
HAS_SIGNING_CREDS: ${{ secrets.AC_USERNAME != '' }}
|
HAS_SIGNING_CREDS: ${{ secrets.AC_USERNAME != '' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Import certificates
|
- name: Import certificates
|
||||||
if: env.HAS_SIGNING_CREDS == 'true'
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
uses: Apple-Actions/import-codesign-certs@v7
|
uses: Apple-Actions/import-codesign-certs@v5
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
||||||
p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
||||||
@@ -114,7 +104,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: darwin-latest
|
name: darwin-latest
|
||||||
path: ./release/*
|
path: ./release/*
|
||||||
@@ -134,25 +124,25 @@ jobs:
|
|||||||
# be overwritten
|
# be overwritten
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-latest
|
name: linux-latest
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push images
|
- name: Build and push images
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
@@ -163,20 +153,17 @@ jobs:
|
|||||||
mkdir -p build/linux-{amd64,arm64}
|
mkdir -p build/linux-{amd64,arm64}
|
||||||
tar -zxvf artifacts/nebula-linux-amd64.tar.gz -C build/linux-amd64/
|
tar -zxvf artifacts/nebula-linux-amd64.tar.gz -C build/linux-amd64/
|
||||||
tar -zxvf artifacts/nebula-linux-arm64.tar.gz -C build/linux-arm64/
|
tar -zxvf artifacts/nebula-linux-arm64.tar.gz -C build/linux-arm64/
|
||||||
docker buildx build . --push -f docker/Dockerfile --platform linux/amd64,linux/arm64 \
|
docker buildx build . --push -f docker/Dockerfile --platform linux/amd64,linux/arm64 --tag "${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG}" --tag "${DOCKER_IMAGE_REPO}:${GITHUB_REF#refs/tags/v}"
|
||||||
--build-arg VERSION="${GITHUB_REF#refs/tags/v}" \
|
|
||||||
--build-arg REVISION="${GITHUB_SHA}" \
|
|
||||||
--tag "${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG}" --tag "${DOCKER_IMAGE_REPO}:${GITHUB_REF#refs/tags/v}"
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create and Upload Release
|
name: Create and Upload Release
|
||||||
needs: [build-linux, build-darwin, build-windows]
|
needs: [build-linux, build-darwin, build-windows]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
@@ -222,11 +209,10 @@ jobs:
|
|||||||
id: create_release
|
id: create_release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
cd artifacts
|
cd artifacts
|
||||||
gh release create \
|
gh release create \
|
||||||
--verify-tag \
|
--verify-tag \
|
||||||
--title "Release ${GITHUB_REF_NAME}" \
|
--title "Release ${{ github.ref_name }}" \
|
||||||
"${GITHUB_REF_NAME}" \
|
"${{ github.ref_name }}" \
|
||||||
SHASUM256.txt *-latest/*.zip *-latest/*.tar.gz
|
SHASUM256.txt *-latest/*.zip *-latest/*.tar.gz
|
||||||
|
|||||||
@@ -14,119 +14,38 @@ on:
|
|||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
smoke-extra-libvirt:
|
smoke-extra:
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'smoke-test-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
|
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:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: add hashicorp source
|
- 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
|
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
|
- name: install vagrant
|
||||||
run: |
|
run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox
|
||||||
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: ${{ matrix.target }}
|
- name: freebsd-amd64
|
||||||
run: make smoke-vagrant/${{ matrix.target }}
|
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.
|
- name: netbsd-amd64
|
||||||
smoke-extra-virtualbox:
|
run: make smoke-vagrant/netbsd-amd64
|
||||||
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: linux-386
|
- name: linux-386
|
||||||
run: make smoke-vagrant/linux-386
|
run: make smoke-vagrant/linux-386
|
||||||
|
|
||||||
|
- name: linux-amd64-ipv6disable
|
||||||
|
run: make smoke-vagrant/linux-amd64-ipv6disable
|
||||||
|
|
||||||
timeout-minutes: 30
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
@@ -36,14 +36,6 @@ jobs:
|
|||||||
working-directory: ./.github/workflows/smoke
|
working-directory: ./.github/workflows/smoke
|
||||||
run: ./smoke.sh
|
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
|
- name: setup relay docker image
|
||||||
working-directory: ./.github/workflows/smoke
|
working-directory: ./.github/workflows/smoke
|
||||||
run: ./build-relay.sh
|
run: ./build-relay.sh
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ relay:
|
|||||||
am_relay: true
|
am_relay: true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# TEST-NET-3 placeholder IPs; smoke-relay.sh seds them to real container IPs.
|
export LIGHTHOUSES="192.168.100.1 172.17.0.2:4242"
|
||||||
# Mapping: .2 lighthouse1, .3 host2, .4 host3, .5 host4.
|
export REMOTE_ALLOW_LIST='{"172.17.0.4/32": false, "172.17.0.5/32": false}'
|
||||||
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}'
|
|
||||||
|
|
||||||
HOST="host2" ../genconfig.sh >host2.yml <<EOF
|
HOST="host2" ../genconfig.sh >host2.yml <<EOF
|
||||||
relay:
|
relay:
|
||||||
@@ -27,7 +25,7 @@ relay:
|
|||||||
- 192.168.100.1
|
- 192.168.100.1
|
||||||
EOF
|
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
|
HOST="host3" ../genconfig.sh >host3.yml
|
||||||
|
|
||||||
|
|||||||
@@ -5,28 +5,9 @@ set -e -x
|
|||||||
rm -rf ./build
|
rm -rf ./build
|
||||||
mkdir ./build
|
mkdir ./build
|
||||||
|
|
||||||
if [ "$SMOKE_OVERLAY_IPV6" ]
|
# TODO: Assumes your docker bridge network is a /24, and the first container that launches will be .1
|
||||||
then
|
# - We could make this better by launching the lighthouse first and then fetching what IP it is.
|
||||||
LIGHTHOUSE_NIP="fd00:4242:0:0:0:ffff:c0a8:6401"
|
NET="$(docker network inspect bridge -f '{{ range .IPAM.Config }}{{ .Subnet }}{{ end }}' | cut -d. -f1-3)"
|
||||||
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"
|
|
||||||
|
|
||||||
(
|
(
|
||||||
cd build
|
cd build
|
||||||
@@ -44,24 +25,24 @@ LIGHTHOUSE_IP="203.0.113.2"
|
|||||||
../genconfig.sh >lighthouse1.yml
|
../genconfig.sh >lighthouse1.yml
|
||||||
|
|
||||||
HOST="host2" \
|
HOST="host2" \
|
||||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
LIGHTHOUSES="192.168.100.1 $NET.2:4242" \
|
||||||
../genconfig.sh >host2.yml
|
../genconfig.sh >host2.yml
|
||||||
|
|
||||||
HOST="host3" \
|
HOST="host3" \
|
||||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
LIGHTHOUSES="192.168.100.1 $NET.2:4242" \
|
||||||
INBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
INBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
||||||
../genconfig.sh >host3.yml
|
../genconfig.sh >host3.yml
|
||||||
|
|
||||||
HOST="host4" \
|
HOST="host4" \
|
||||||
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
LIGHTHOUSES="192.168.100.1 $NET.2:4242" \
|
||||||
OUTBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
OUTBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
|
||||||
../genconfig.sh >host4.yml
|
../genconfig.sh >host4.yml
|
||||||
|
|
||||||
../../../../nebula-cert ca -curve "${CURVE:-25519}" -name "Smoke Test"
|
../../../../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 "lighthouse1" -groups "lighthouse,lighthouse1" -ip "192.168.100.1/24"
|
||||||
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "$HOST2_NIP/24"
|
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "192.168.100.2/24"
|
||||||
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "$HOST3_NIP/24"
|
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "192.168.100.3/24"
|
||||||
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "$HOST4_NIP/24"
|
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "192.168.100.4/24"
|
||||||
)
|
)
|
||||||
|
|
||||||
docker build -t "nebula:${NAME:-smoke}" .
|
docker build -t "nebula:${NAME:-smoke}" .
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ set -o pipefail
|
|||||||
|
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
|
|
||||||
NETWORK="nebula-smoke-relay"
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo
|
echo
|
||||||
echo " *** cleanup"
|
echo " *** cleanup"
|
||||||
@@ -18,53 +16,22 @@ cleanup() {
|
|||||||
then
|
then
|
||||||
docker kill lighthouse1 host2 host3 host4
|
docker kill lighthouse1 host2 host3 host4
|
||||||
fi
|
fi
|
||||||
docker network rm "$NETWORK" >/dev/null 2>&1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup EXIT
|
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 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 host2 --rm 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 host3 --rm 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 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
|
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
|
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
|
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
|
sleep 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
@@ -109,13 +76,7 @@ docker exec host4 sh -c 'kill 1'
|
|||||||
docker exec host3 sh -c 'kill 1'
|
docker exec host3 sh -c 'kill 1'
|
||||||
docker exec host2 sh -c 'kill 1'
|
docker exec host2 sh -c 'kill 1'
|
||||||
docker exec lighthouse1 sh -c 'kill 1'
|
docker exec lighthouse1 sh -c 'kill 1'
|
||||||
|
sleep 5
|
||||||
# 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
|
|
||||||
|
|
||||||
if [ "$(jobs -r)" ]
|
if [ "$(jobs -r)" ]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ export VAGRANT_CWD="$PWD/vagrant-$1"
|
|||||||
|
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
|
|
||||||
NETWORK="nebula-smoke"
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo
|
echo
|
||||||
echo " *** cleanup"
|
echo " *** cleanup"
|
||||||
@@ -21,51 +19,21 @@ cleanup() {
|
|||||||
docker kill lighthouse1 host2
|
docker kill lighthouse1 host2
|
||||||
fi
|
fi
|
||||||
vagrant destroy -f
|
vagrant destroy -f
|
||||||
docker network rm "$NETWORK" >/dev/null 2>&1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup EXIT
|
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}"
|
CONTAINER="nebula:${NAME:-smoke}"
|
||||||
|
|
||||||
docker run --name lighthouse1 --rm "$CONTAINER" -config lighthouse1.yml -test
|
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 up
|
||||||
vagrant ssh -c "cd /nebula && /nebula/$1-nebula -config host3.yml -test" -- -T
|
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
|
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
|
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] /' &
|
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
|
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
|
vagrant ssh -c "sudo xargs kill </nebula/pid" -- -T
|
||||||
docker exec host2 sh -c 'kill 1'
|
docker exec host2 sh -c 'kill 1'
|
||||||
docker exec lighthouse1 sh -c 'kill 1'
|
docker exec lighthouse1 sh -c 'kill 1'
|
||||||
|
sleep 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
|
|
||||||
|
|
||||||
if [ "$(jobs -r)" ]
|
if [ "$(jobs -r)" ]
|
||||||
then
|
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
|
mkdir -p logs
|
||||||
|
|
||||||
NETWORK="nebula-smoke"
|
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo
|
echo
|
||||||
echo " *** cleanup"
|
echo " *** cleanup"
|
||||||
@@ -18,103 +16,57 @@ cleanup() {
|
|||||||
then
|
then
|
||||||
docker kill lighthouse1 host2 host3 host4
|
docker kill lighthouse1 host2 host3 host4
|
||||||
fi
|
fi
|
||||||
docker network rm "$NETWORK" >/dev/null 2>&1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup EXIT
|
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}"
|
CONTAINER="nebula:${NAME:-smoke}"
|
||||||
|
|
||||||
docker run --name lighthouse1 --rm "$CONTAINER" -config lighthouse1.yml -test
|
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
|
||||||
docker run --name host3 --rm -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" "$CONTAINER" -config host3.yml -test
|
docker run --name host3 --rm "$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 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
|
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
|
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
|
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
|
sleep 1
|
||||||
|
|
||||||
# grab tcpdump pcaps for debugging
|
# 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 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 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 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 host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
|
||||||
|
|
||||||
docker exec host2 ncat -nklv 2000 &
|
docker exec host2 ncat -nklv 0.0.0.0 2000 &
|
||||||
docker exec host3 ncat -nklv 2000 &
|
docker exec host3 ncat -nklv 0.0.0.0 2000 &
|
||||||
docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 4000 &
|
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
|
||||||
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 3000 &
|
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 &
|
||||||
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 3000 &
|
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
echo " *** Testing ping from lighthouse1"
|
echo " *** Testing ping from lighthouse1"
|
||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
docker exec lighthouse1 ping -c1 $HOST2_NIP
|
docker exec lighthouse1 ping -c1 192.168.100.2
|
||||||
docker exec lighthouse1 ping -c1 $HOST3_NIP
|
docker exec lighthouse1 ping -c1 192.168.100.3
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
echo " *** Testing ping from host2"
|
echo " *** Testing ping from host2"
|
||||||
echo
|
echo
|
||||||
set -x
|
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
|
# 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
|
set +x
|
||||||
echo
|
echo
|
||||||
@@ -122,34 +74,34 @@ echo " *** Testing ncat from host2"
|
|||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
# Should fail because not allowed by host3 inbound firewall
|
# Should fail because not allowed by host3 inbound firewall
|
||||||
! docker exec host2 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
|
! docker exec host2 ncat -nzv -w5 192.168.100.3 2000 || exit 1
|
||||||
! docker exec host2 ncat -nzuv -w5 $HOST3_NIP 3000 | grep -q host3 || exit 1
|
! docker exec host2 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
echo " *** Testing ping from host3"
|
echo " *** Testing ping from host3"
|
||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
docker exec host3 ping -c1 $LIGHTHOUSE_NIP
|
docker exec host3 ping -c1 192.168.100.1
|
||||||
docker exec host3 ping -c1 $HOST2_NIP
|
docker exec host3 ping -c1 192.168.100.2
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
echo " *** Testing ncat from host3"
|
echo " *** Testing ncat from host3"
|
||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
docker exec host3 ncat -nzv -w5 $HOST2_NIP 2000
|
docker exec host3 ncat -nzv -w5 192.168.100.2 2000
|
||||||
docker exec host3 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2
|
docker exec host3 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
echo " *** Testing ping from host4"
|
echo " *** Testing ping from host4"
|
||||||
echo
|
echo
|
||||||
set -x
|
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
|
# Should fail because not allowed by host4 outbound firewall
|
||||||
! docker exec host4 ping -c1 $HOST2_NIP -w5 || exit 1
|
! docker exec host4 ping -c1 192.168.100.2 -w5 || exit 1
|
||||||
! docker exec host4 ping -c1 $HOST3_NIP -w5 || exit 1
|
! docker exec host4 ping -c1 192.168.100.3 -w5 || exit 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
@@ -157,34 +109,27 @@ echo " *** Testing ncat from host4"
|
|||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
# Should fail because not allowed by host4 outbound firewall
|
# 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 192.168.100.2 2000 || exit 1
|
||||||
! docker exec host4 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
|
! docker exec host4 ncat -nzv -w5 192.168.100.3 2000 || exit 1
|
||||||
! docker exec host4 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2 || exit 1
|
! docker exec host4 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2 || exit 1
|
||||||
! docker exec host4 ncat -nzuv -w5 $HOST3_NIP 3000 | grep -q host3 || exit 1
|
! docker exec host4 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
echo " *** Testing conntrack"
|
echo " *** Testing conntrack"
|
||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
|
# host2 can ping host3 now that host3 pinged it first
|
||||||
# host4's outbound firewall only allows ICMP to the lighthouse, so host4
|
docker exec host2 ping -c1 192.168.100.3
|
||||||
# cannot initiate UDP to host2. Once host2 initiates a flow to host4:4000,
|
# host4 can ping host2 once conntrack established
|
||||||
# conntrack must let host4's listener reply on that flow. If it doesn't,
|
docker exec host2 ping -c1 192.168.100.4
|
||||||
# the echo back from host4 never reaches host2.
|
docker exec host4 ping -c1 192.168.100.2
|
||||||
docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv $HOST4_NIP 4000" | grep -q helloagainfromhost4
|
|
||||||
|
|
||||||
docker exec host4 sh -c 'kill 1'
|
docker exec host4 sh -c 'kill 1'
|
||||||
docker exec host3 sh -c 'kill 1'
|
docker exec host3 sh -c 'kill 1'
|
||||||
docker exec host2 sh -c 'kill 1'
|
docker exec host2 sh -c 'kill 1'
|
||||||
docker exec lighthouse1 sh -c 'kill 1'
|
docker exec lighthouse1 sh -c 'kill 1'
|
||||||
|
sleep 5
|
||||||
# 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
|
|
||||||
|
|
||||||
if [ "$(jobs -r)" ]
|
if [ "$(jobs -r)" ]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
Vagrant.configure("2") do |config|
|
Vagrant.configure("2") do |config|
|
||||||
config.vm.box = "bento/ubuntu-24.04"
|
config.vm.box = "ubuntu/jammy64"
|
||||||
|
|
||||||
config.vm.synced_folder "../build", "/nebula"
|
config.vm.synced_folder "../build", "/nebula"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
Vagrant.configure("2") do |config|
|
Vagrant.configure("2") do |config|
|
||||||
config.vm.box = "DefinedNet/netbsd10"
|
config.vm.box = "generic/netbsd9"
|
||||||
|
|
||||||
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
Vagrant.configure("2") do |config|
|
Vagrant.configure("2") do |config|
|
||||||
config.vm.box = "DefinedNet/openbsd78"
|
config.vm.box = "generic/openbsd7"
|
||||||
|
|
||||||
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
||||||
end
|
end
|
||||||
|
|||||||
+80
-98
@@ -13,138 +13,120 @@ on:
|
|||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
static:
|
test-linux:
|
||||||
name: Static checks
|
name: Build all and test on ubuntu-linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Install goimports
|
- name: Build
|
||||||
run: go install golang.org/x/tools/cmd/goimports@latest
|
run: make all
|
||||||
|
|
||||||
- name: gofmt
|
|
||||||
run: |
|
|
||||||
if [ "$(find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -l)" ]
|
|
||||||
then
|
|
||||||
find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -d
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Vet
|
- name: Vet
|
||||||
run: make vet
|
run: make vet
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
version: v2.12
|
version: v2.5
|
||||||
|
|
||||||
test:
|
- name: Test
|
||||||
name: Test ${{ matrix.name }}
|
run: make test
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
- name: End 2 end
|
||||||
fail-fast: false
|
run: make e2evv
|
||||||
matrix:
|
|
||||||
include:
|
- name: Build test mobile
|
||||||
- name: linux
|
run: make build-test-mobile
|
||||||
os: ubuntu-latest
|
|
||||||
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
- uses: actions/upload-artifact@v4
|
||||||
test-cmd: make test
|
with:
|
||||||
e2e-cmd: make e2evv
|
name: e2e packet flow linux-latest
|
||||||
- name: linux-boringcrypto
|
path: e2e/mermaid/linux-latest
|
||||||
os: ubuntu-latest
|
if-no-files-found: warn
|
||||||
build-cmd: make bin-boringcrypto
|
|
||||||
test-cmd: make test-boringcrypto
|
test-linux-boringcrypto:
|
||||||
e2e-cmd: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
name: Build and test on linux with boringcrypto
|
||||||
- name: linux-pkcs11
|
runs-on: ubuntu-latest
|
||||||
os: ubuntu-latest
|
|
||||||
build-cmd: make bin-pkcs11
|
|
||||||
test-cmd: make test-pkcs11
|
|
||||||
e2e-cmd: ''
|
|
||||||
- name: macos
|
|
||||||
os: macos-latest
|
|
||||||
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
|
||||||
test-cmd: make test
|
|
||||||
e2e-cmd: make e2evv
|
|
||||||
- name: windows
|
|
||||||
os: windows-latest
|
|
||||||
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
|
||||||
test-cmd: make test
|
|
||||||
e2e-cmd: make e2evv
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ${{ matrix.build-cmd }}
|
run: make bin-boringcrypto
|
||||||
|
|
||||||
- name: Cross-build darwin-amd64
|
|
||||||
if: matrix.name == 'macos'
|
|
||||||
run: GOARCH=amd64 go build -o /tmp/nebula-amd64 ./cmd/nebula && GOARCH=amd64 go build -o /tmp/nebula-cert-amd64 ./cmd/nebula-cert
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: ${{ matrix.test-cmd }}
|
run: make test-boringcrypto
|
||||||
|
|
||||||
- name: End 2 end
|
- name: End 2 end
|
||||||
if: matrix.e2e-cmd != ''
|
run: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
||||||
run: ${{ matrix.e2e-cmd }}
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v7
|
test-linux-pkcs11:
|
||||||
if: matrix.e2e-cmd != '' && always()
|
name: Build and test on linux with pkcs11
|
||||||
with:
|
|
||||||
name: e2e packet flow ${{ matrix.name }}
|
|
||||||
path: e2e/mermaid/
|
|
||||||
if-no-files-found: warn
|
|
||||||
|
|
||||||
cross-build:
|
|
||||||
name: Cross-build ${{ matrix.name }}
|
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
go-version: '1.25'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build ${{ matrix.name }}
|
- name: Build
|
||||||
run: make -j"$(nproc)" ${{ matrix.make-target }}
|
run: make bin-pkcs11
|
||||||
|
|
||||||
finish:
|
- name: Test
|
||||||
name: CI status
|
run: make test-pkcs11
|
||||||
if: always()
|
|
||||||
needs: [static, test, cross-build]
|
test:
|
||||||
runs-on: ubuntu-latest
|
name: Build and test on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Fail if any upstream job failed
|
- uses: actions/checkout@v4
|
||||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
|
||||||
run: |
|
|
||||||
echo "upstream results: ${{ toJSON(needs) }}"
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: All upstream jobs passed
|
- uses: actions/setup-go@v6
|
||||||
run: echo "ok"
|
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:
|
linters:
|
||||||
default: none
|
default: none
|
||||||
enable:
|
enable:
|
||||||
- sloglint
|
|
||||||
- testifylint
|
- 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:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
presets:
|
presets:
|
||||||
|
|||||||
+3
-209
@@ -7,211 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Changed
|
||||||
|
|
||||||
- Reload the firewall when the unsafe networks in the certificate change. (#1719)
|
- `default_local_cidr_any` now defaults to false, meaning that any firewall rule
|
||||||
- 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
|
|
||||||
intended to target an `unsafe_routes` entry must explicitly declare it via the
|
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
|
`local_cidr` field. This is almost always the intended behavior. This flag is
|
||||||
deprecated and will be removed in a future release. (#1373)
|
deprecated and will be removed in a future release.
|
||||||
- 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)
|
|
||||||
|
|
||||||
## [1.9.4] - 2024-09-09
|
## [1.9.4] - 2024-09-09
|
||||||
|
|
||||||
@@ -870,14 +671,7 @@ created.)
|
|||||||
|
|
||||||
- Initial public release.
|
- Initial public release.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.10.3...HEAD
|
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.9.4...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
|
|
||||||
[1.9.4]: https://github.com/slackhq/nebula/releases/tag/v1.9.4
|
[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.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.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-amd64 \
|
||||||
windows-arm64
|
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:
|
e2e:
|
||||||
$(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./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: $(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)
|
docker: docker/linux-$(shell go env GOARCH)
|
||||||
|
|
||||||
release: $(ALL:%=build/nebula-%.tar.gz)
|
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/ && ./build-relay.sh
|
||||||
cd .github/workflows/smoke/ && ./smoke-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: BUILD_ARGS = -race
|
||||||
smoke-docker-race: CGO_ENABLED = 1
|
smoke-docker-race: CGO_ENABLED = 1
|
||||||
smoke-docker-race: smoke-docker
|
smoke-docker-race: smoke-docker
|
||||||
@@ -280,5 +236,5 @@ smoke-vagrant/%: bin-docker build/%/nebula
|
|||||||
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
||||||
|
|
||||||
.FORCE:
|
.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
|
.DEFAULT_GOAL := bin
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Check the [releases](https://github.com/slackhq/nebula/releases/latest) page for
|
|||||||
docker pull nebulaoss/nebula
|
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)
|
- [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)
|
- [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)
|
## 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:
|
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. 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.
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package nebula
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/overlay"
|
||||||
|
"github.com/slackhq/nebula/udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// batchPipelines tracks whether the inside device can operate on packet batches
|
||||||
|
// and, if so, holds the shared packet pool sized for the virtio headroom and
|
||||||
|
// payload limits advertised by the device. It also owns the fan-in/fan-out
|
||||||
|
// queues between the TUN readers, encrypt/decrypt workers, and the UDP writers.
|
||||||
|
type batchPipelines struct {
|
||||||
|
enabled bool
|
||||||
|
inside overlay.BatchCapableDevice
|
||||||
|
headroom int
|
||||||
|
payloadCap int
|
||||||
|
pool *overlay.PacketPool
|
||||||
|
batchSize int
|
||||||
|
routines int
|
||||||
|
rxQueues []chan *overlay.Packet
|
||||||
|
txQueues []chan queuedDatagram
|
||||||
|
tunQueues []chan *overlay.Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
type queuedDatagram struct {
|
||||||
|
packet *overlay.Packet
|
||||||
|
addr netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) init(device overlay.Device, routines int, queueDepth int, maxSegments int) {
|
||||||
|
if device == nil || routines <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bcap, ok := device.(overlay.BatchCapableDevice)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headroom := bcap.BatchHeadroom()
|
||||||
|
payload := bcap.BatchPayloadCap()
|
||||||
|
if maxSegments < 1 {
|
||||||
|
maxSegments = 1
|
||||||
|
}
|
||||||
|
requiredPayload := udp.MTU * maxSegments
|
||||||
|
if payload < requiredPayload {
|
||||||
|
payload = requiredPayload
|
||||||
|
}
|
||||||
|
batchSize := bcap.BatchSize()
|
||||||
|
if headroom <= 0 || payload <= 0 || batchSize <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bp.enabled = true
|
||||||
|
bp.inside = bcap
|
||||||
|
bp.headroom = headroom
|
||||||
|
bp.payloadCap = payload
|
||||||
|
bp.batchSize = batchSize
|
||||||
|
bp.routines = routines
|
||||||
|
bp.pool = overlay.NewPacketPool(headroom, payload)
|
||||||
|
queueCap := batchSize * defaultBatchQueueDepthFactor
|
||||||
|
if queueDepth > 0 {
|
||||||
|
queueCap = queueDepth
|
||||||
|
}
|
||||||
|
if queueCap < batchSize {
|
||||||
|
queueCap = batchSize
|
||||||
|
}
|
||||||
|
bp.rxQueues = make([]chan *overlay.Packet, routines)
|
||||||
|
bp.txQueues = make([]chan queuedDatagram, routines)
|
||||||
|
bp.tunQueues = make([]chan *overlay.Packet, routines)
|
||||||
|
for i := 0; i < routines; i++ {
|
||||||
|
bp.rxQueues[i] = make(chan *overlay.Packet, queueCap)
|
||||||
|
bp.txQueues[i] = make(chan queuedDatagram, queueCap)
|
||||||
|
bp.tunQueues[i] = make(chan *overlay.Packet, queueCap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) Pool() *overlay.PacketPool {
|
||||||
|
if bp == nil || !bp.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bp.pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) Enabled() bool {
|
||||||
|
return bp != nil && bp.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) batchSizeHint() int {
|
||||||
|
if bp == nil || bp.batchSize <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return bp.batchSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) rxQueue(i int) chan *overlay.Packet {
|
||||||
|
if bp == nil || !bp.enabled || i < 0 || i >= len(bp.rxQueues) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bp.rxQueues[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) txQueue(i int) chan queuedDatagram {
|
||||||
|
if bp == nil || !bp.enabled || i < 0 || i >= len(bp.txQueues) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bp.txQueues[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) tunQueue(i int) chan *overlay.Packet {
|
||||||
|
if bp == nil || !bp.enabled || i < 0 || i >= len(bp.tunQueues) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bp.tunQueues[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) txQueueLen(i int) int {
|
||||||
|
q := bp.txQueue(i)
|
||||||
|
if q == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) tunQueueLen(i int) int {
|
||||||
|
q := bp.tunQueue(i)
|
||||||
|
if q == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) enqueueRx(i int, pkt *overlay.Packet) bool {
|
||||||
|
q := bp.rxQueue(i)
|
||||||
|
if q == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
q <- pkt
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) enqueueTx(i int, pkt *overlay.Packet, addr netip.AddrPort) bool {
|
||||||
|
q := bp.txQueue(i)
|
||||||
|
if q == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
q <- queuedDatagram{packet: pkt, addr: addr}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) enqueueTun(i int, pkt *overlay.Packet) bool {
|
||||||
|
q := bp.tunQueue(i)
|
||||||
|
if q == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
q <- pkt
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *batchPipelines) newPacket() *overlay.Packet {
|
||||||
|
if bp == nil || !bp.enabled || bp.pool == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bp.pool.Get()
|
||||||
|
}
|
||||||
@@ -1,263 +1,157 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"math"
|
|
||||||
mathbits "math/bits"
|
|
||||||
|
|
||||||
"github.com/rcrowley/go-metrics"
|
"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 {
|
type Bits struct {
|
||||||
length uint64
|
length uint64
|
||||||
lengthMask uint64
|
|
||||||
current uint64
|
current uint64
|
||||||
bits []uint64
|
bits []bool
|
||||||
|
firstSeen bool
|
||||||
lostCounter metrics.Counter
|
lostCounter metrics.Counter
|
||||||
dupeCounter metrics.Counter
|
dupeCounter metrics.Counter
|
||||||
outOfWindowCounter metrics.Counter
|
outOfWindowCounter metrics.Counter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBits(length uint64) *Bits {
|
func NewBits(bits uint64) *Bits {
|
||||||
if length == 0 || length&(length-1) != 0 {
|
return &Bits{
|
||||||
panic(fmt.Sprintf("Bits length must be a power of two, got %d", length))
|
length: bits,
|
||||||
}
|
bits: make([]bool, bits, bits),
|
||||||
|
|
||||||
nWords := length / bitsPerWord
|
|
||||||
if nWords == 0 {
|
|
||||||
nWords = 1
|
|
||||||
}
|
|
||||||
b := &Bits{
|
|
||||||
length: length,
|
|
||||||
lengthMask: length - 1,
|
|
||||||
bits: make([]uint64, nWords),
|
|
||||||
current: 0,
|
current: 0,
|
||||||
lostCounter: metrics.GetOrRegisterCounter("network.packets.lost", nil),
|
lostCounter: metrics.GetOrRegisterCounter("network.packets.lost", nil),
|
||||||
dupeCounter: metrics.GetOrRegisterCounter("network.packets.duplicate", nil),
|
dupeCounter: metrics.GetOrRegisterCounter("network.packets.duplicate", nil),
|
||||||
outOfWindowCounter: metrics.GetOrRegisterCounter("network.packets.out_of_window", 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 {
|
func (b *Bits) Check(l logrus.FieldLogger, 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 {
|
|
||||||
// If i is the next number, return true.
|
// 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.strictlyWithinWindow(i) {
|
// If i is within the window, check if it's been set already. The first window will fail this check
|
||||||
return !b.get(i)
|
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
|
// Not within the window
|
||||||
if l.Enabled(context.Background(), slog.LevelDebug) {
|
l.Debugf("rejected a packet (top) %d %d\n", b.current, i)
|
||||||
l.Debug("rejected a packet (top)", "current", b.current, "incoming", i)
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update has three branches:
|
func (b *Bits) Update(l *logrus.Logger, i uint64) bool {
|
||||||
// - i == b.current+1: fast path; advance the cursor by one and lose-count
|
// If i is the next number, return true and update current.
|
||||||
// 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.
|
|
||||||
if i == b.current+1 {
|
if i == b.current+1 {
|
||||||
pos := i & b.lengthMask
|
// Report missed packets, we can only understand what was missed after the first window has been gone through
|
||||||
word := pos >> 6
|
if i > b.length && b.bits[i%b.length] == false {
|
||||||
mask := uint64(1) << (pos & 63)
|
|
||||||
w := b.bits[word]
|
|
||||||
if i > b.length && w&mask == 0 {
|
|
||||||
b.lostCounter.Inc(1)
|
b.lostCounter.Inc(1)
|
||||||
}
|
}
|
||||||
b.bits[word] = w | mask
|
b.bits[i%b.length] = true
|
||||||
b.current = i
|
b.current = i
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return b.updateSlow(l, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateSlow handles jumps, in-window backfill, dupes, and out-of-window.
|
// If i packet is greater than current but less than the maximum length of our bitmap,
|
||||||
func (b *Bits) updateSlow(l *slog.Logger, i uint64) bool {
|
// flip everything in between to false and move ahead.
|
||||||
// If i is a jump, adjust the window, record lost, update current, and return true
|
if i > b.current && i < b.current+b.length {
|
||||||
if i > b.current {
|
// In between current and i need to be zero'd to allow those packets to come in later
|
||||||
end := i
|
for n := b.current + 1; n < i; n++ {
|
||||||
if end > b.current+b.length {
|
b.bits[n%b.length] = false
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anything past the new window can never be backfilled, so it's lost.
|
b.bits[i%b.length] = true
|
||||||
if i > b.current+b.length {
|
b.current = i
|
||||||
lost += int64(i - b.current - b.length)
|
//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.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
|
b.current = i
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If i is within the current window but below the current counter, check to see if it's a duplicate
|
// Allow for the 0 packet to come in within the first window
|
||||||
if b.strictlyWithinWindow(i) {
|
if i == 0 && b.firstSeen == false && b.current < b.length {
|
||||||
pos := i & b.lengthMask
|
b.firstSeen = true
|
||||||
word := pos >> 6
|
b.bits[i%b.length] = true
|
||||||
mask := uint64(1) << (pos & 63)
|
return true
|
||||||
w := b.bits[word]
|
}
|
||||||
if b.current == i || w&mask != 0 {
|
|
||||||
if l.Enabled(context.Background(), slog.LevelDebug) {
|
// If i is within the window of current minus length (the total pat window size),
|
||||||
l.Debug("Receive window",
|
// allow it and flip to true but to NOT change current. We also have to account for the first window
|
||||||
"accepted", false,
|
if ((b.current >= b.length && i > b.current-b.length) || (b.current < b.length && i < b.length)) && i <= b.current {
|
||||||
"currentCounter", b.current,
|
if b.current == i {
|
||||||
"incomingCounter", i,
|
if l.Level >= logrus.DebugLevel {
|
||||||
"reason", "duplicate",
|
l.WithField("receiveWindow", m{"accepted": false, "currentCounter": b.current, "incomingCounter": i, "reason": "duplicate"}).
|
||||||
)
|
Debug("Receive window")
|
||||||
}
|
}
|
||||||
b.dupeCounter.Inc(1)
|
b.dupeCounter.Inc(1)
|
||||||
return false
|
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
|
return true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In all other cases, fail and don't change current.
|
// In all other cases, fail and don't change current.
|
||||||
b.outOfWindowCounter.Inc(1)
|
b.outOfWindowCounter.Inc(1)
|
||||||
if l.Enabled(context.Background(), slog.LevelDebug) {
|
if l.Level >= logrus.DebugLevel {
|
||||||
l.Debug("Receive window",
|
l.WithField("accepted", false).
|
||||||
"accepted", false,
|
WithField("currentCounter", b.current).
|
||||||
"currentCounter", b.current,
|
WithField("incomingCounter", i).
|
||||||
"incomingCounter", i,
|
WithField("reason", "nonsense").
|
||||||
"reason", "nonsense",
|
Debug("Receive window")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return false
|
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"
|
"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) {
|
func TestBits(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
b := NewBits(16)
|
b := NewBits(10)
|
||||||
assert.EqualValues(t, 16, b.length)
|
|
||||||
|
// make sure it is the right size
|
||||||
|
assert.Len(t, b.bits, 10)
|
||||||
|
|
||||||
// This is initialized to zero - receive one. This should work.
|
// This is initialized to zero - receive one. This should work.
|
||||||
|
|
||||||
assert.True(t, b.Check(l, 1))
|
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)
|
assert.EqualValues(t, 1, b.current)
|
||||||
g := []bool{true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}
|
g := []bool{false, true, false, false, false, false, false, false, false, false}
|
||||||
assert.Equal(t, g, b.snapshot())
|
assert.Equal(t, g, b.bits)
|
||||||
|
|
||||||
// Receive two
|
// Receive two
|
||||||
assert.True(t, b.Check(l, 2))
|
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)
|
assert.EqualValues(t, 2, b.current)
|
||||||
g = []bool{true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false}
|
g = []bool{false, true, true, false, false, false, false, false, false, false}
|
||||||
assert.Equal(t, g, b.snapshot())
|
assert.Equal(t, g, b.bits)
|
||||||
|
|
||||||
// Receive two again - it will fail
|
// Receive two again - it will fail
|
||||||
assert.False(t, b.Check(l, 2))
|
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)
|
assert.EqualValues(t, 2, b.current)
|
||||||
|
|
||||||
// Jump ahead to 25, which clears the window and sets slot 25%16 = 9.
|
// Jump ahead to 15, which should clear everything and set the 6th element
|
||||||
assert.True(t, b.Check(l, 25))
|
assert.True(t, b.Check(l, 15))
|
||||||
assert.True(t, b.Update(l, 25))
|
u = b.Update(l, 15)
|
||||||
assert.EqualValues(t, 25, b.current)
|
assert.True(t, u)
|
||||||
g = []bool{false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false}
|
assert.EqualValues(t, 15, b.current)
|
||||||
assert.Equal(t, g, b.snapshot())
|
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]).
|
// Mark 14, which is allowed because it is in the window
|
||||||
assert.True(t, b.Check(l, 24))
|
assert.True(t, b.Check(l, 14))
|
||||||
assert.True(t, b.Update(l, 24))
|
u = b.Update(l, 14)
|
||||||
assert.EqualValues(t, 25, b.current)
|
assert.True(t, u)
|
||||||
g = []bool{false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false}
|
assert.EqualValues(t, 15, b.current)
|
||||||
assert.Equal(t, g, b.snapshot())
|
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.Check(l, 5))
|
||||||
assert.False(t, b.Update(l, 5))
|
u = b.Update(l, 5)
|
||||||
assert.EqualValues(t, 25, b.current)
|
assert.False(t, u)
|
||||||
g = []bool{false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false}
|
assert.EqualValues(t, 15, b.current)
|
||||||
assert.Equal(t, g, b.snapshot())
|
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
|
// make sure we handle wrapping around once to the current position
|
||||||
// length=16, packets 1 and 17 share slot 1.
|
b = NewBits(10)
|
||||||
b = NewBits(16)
|
|
||||||
assert.True(t, b.Update(l, 1))
|
assert.True(t, b.Update(l, 1))
|
||||||
assert.True(t, b.Update(l, 17))
|
assert.True(t, b.Update(l, 11))
|
||||||
assert.Equal(t, []bool{false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}, b.snapshot())
|
assert.Equal(t, []bool{false, true, false, false, false, false, false, false, false, false}, b.bits)
|
||||||
|
|
||||||
// Walk through a few windows in order
|
// Walk through a few windows in order
|
||||||
b = NewBits(16)
|
b = NewBits(10)
|
||||||
for i := uint64(1); i <= 100; i++ {
|
for i := uint64(0); i <= 100; i++ {
|
||||||
assert.True(t, b.Check(l, i), "Error while checking %v", 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.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) {
|
func TestBitsDupeCounter(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
b := NewBits(16)
|
b := NewBits(10)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
@@ -139,300 +102,127 @@ func TestBitsDupeCounter(t *testing.T) {
|
|||||||
|
|
||||||
func TestBitsOutOfWindowCounter(t *testing.T) {
|
func TestBitsOutOfWindowCounter(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
b := NewBits(16)
|
b := NewBits(10)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
|
|
||||||
// Jump to 20 (warmup branch + 4 past-window packets).
|
|
||||||
assert.True(t, b.Update(l, 20))
|
assert.True(t, b.Update(l, 20))
|
||||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||||
|
|
||||||
// 9 single-step advances, each evicts a slot whose bit was cleared during
|
assert.True(t, b.Update(l, 21))
|
||||||
// the jump above and whose value was never seen, so each contributes 1
|
assert.True(t, b.Update(l, 22))
|
||||||
// to lostCounter.
|
assert.True(t, b.Update(l, 23))
|
||||||
for n := uint64(21); n <= 29; n++ {
|
assert.True(t, b.Update(l, 24))
|
||||||
assert.True(t, b.Update(l, n))
|
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())
|
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.False(t, b.Update(l, 0))
|
||||||
assert.Equal(t, int64(1), b.outOfWindowCounter.Count())
|
assert.Equal(t, int64(1), b.outOfWindowCounter.Count())
|
||||||
|
|
||||||
// 4 from the Update(20) jump + 9 from 21..29.
|
//tODO: make sure lostcounter doesn't increase in orderly increment
|
||||||
assert.Equal(t, int64(13), b.lostCounter.Count())
|
assert.Equal(t, int64(20), b.lostCounter.Count())
|
||||||
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
||||||
assert.Equal(t, int64(1), b.outOfWindowCounter.Count())
|
assert.Equal(t, int64(1), b.outOfWindowCounter.Count())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBitsLostCounter(t *testing.T) {
|
func TestBitsLostCounter(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
b := NewBits(16)
|
b := NewBits(10)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
|
|
||||||
// Walk 20..29 like the original, just with a bigger window. Same
|
//assert.True(t, b.Update(0))
|
||||||
// reasoning as TestBitsOutOfWindowCounter: 4 past-window from Update(20),
|
assert.True(t, b.Update(l, 0))
|
||||||
// then 9 more from the unit advances.
|
assert.True(t, b.Update(l, 20))
|
||||||
for n := uint64(20); n <= 29; n++ {
|
assert.True(t, b.Update(l, 21))
|
||||||
assert.True(t, b.Update(l, n))
|
assert.True(t, b.Update(l, 22))
|
||||||
}
|
assert.True(t, b.Update(l, 23))
|
||||||
assert.Equal(t, int64(13), b.lostCounter.Count())
|
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.dupeCounter.Count())
|
||||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||||
|
|
||||||
b = NewBits(16)
|
b = NewBits(10)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
|
|
||||||
// Update(15) clears the warmup window (no lost), sets slot 15.
|
assert.True(t, b.Update(l, 0))
|
||||||
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.Equal(t, int64(0), b.lostCounter.Count())
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
assert.True(t, b.Update(l, 9))
|
assert.True(t, b.Update(l, 9))
|
||||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
assert.True(t, b.Update(l, 2))
|
// 10 will set 0 index, 0 was already set, no lost packets
|
||||||
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.
|
|
||||||
assert.True(t, b.Update(l, 10))
|
assert.True(t, b.Update(l, 10))
|
||||||
assert.Equal(t, int64(0), b.lostCounter.Count())
|
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.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())
|
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||||
|
// Now let's fill in the window, should end up with 8 lost packets
|
||||||
// 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.
|
|
||||||
assert.True(t, b.Update(l, 12))
|
assert.True(t, b.Update(l, 12))
|
||||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
|
||||||
assert.True(t, b.Update(l, 13))
|
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.True(t, b.Update(l, 15))
|
||||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
|
||||||
assert.True(t, b.Update(l, 16))
|
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
|
// Jump ahead by a window size
|
||||||
// double-counted, never zeroed.
|
assert.True(t, b.Update(l, 29))
|
||||||
assert.Equal(t, int64(1), b.lostCounter.Count())
|
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.dupeCounter.Count())
|
||||||
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBitsWarmupOvershoot exercises the jump path's warmup arm with an
|
func BenchmarkBits(b *testing.B) {
|
||||||
// overshoot past one full window. NewBits leaves current=0 with only slot 0
|
z := NewBits(10)
|
||||||
// "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)
|
|
||||||
for n := 0; n < b.N; n++ {
|
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
|
//go:build boringcrypto
|
||||||
|
// +build boringcrypto
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
|
|||||||
+9
-58
@@ -1,14 +1,11 @@
|
|||||||
package cert
|
package cert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,46 +29,22 @@ func NewCAPool() *CAPool {
|
|||||||
// If the pool contains any expired certificates, an ErrExpired will be
|
// If the pool contains any expired certificates, an ErrExpired will be
|
||||||
// returned along with the pool. The caller must handle any such errors.
|
// returned along with the pool. The caller must handle any such errors.
|
||||||
func NewCAPoolFromPEM(caPEMs []byte) (*CAPool, error) {
|
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()
|
pool := NewCAPool()
|
||||||
|
var err error
|
||||||
var expired bool
|
var expired bool
|
||||||
|
for {
|
||||||
scanner := bufio.NewScanner(r)
|
caPEMs, err = pool.AddCAFromPEM(caPEMs)
|
||||||
scanner.Split(SplitPEM)
|
if errors.Is(err, ErrExpired) {
|
||||||
|
expired = true
|
||||||
for scanner.Scan() {
|
err = nil
|
||||||
pemBytes := scanner.Bytes()
|
|
||||||
|
|
||||||
block, rest := pem.Decode(pemBytes)
|
|
||||||
if len(bytes.TrimSpace(rest)) > 0 {
|
|
||||||
return nil, ErrInvalidPEMBlock
|
|
||||||
}
|
}
|
||||||
if block == nil {
|
|
||||||
return nil, ErrInvalidPEMBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := unmarshalCertificateBlock(block)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(caPEMs) == 0 || strings.TrimSpace(string(caPEMs)) == "" {
|
||||||
err = pool.AddCA(c)
|
break
|
||||||
if errors.Is(err, ErrExpired) {
|
|
||||||
expired = true
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, ErrInvalidPEMBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
if expired {
|
if expired {
|
||||||
return pool, ErrExpired
|
return pool, ErrExpired
|
||||||
@@ -168,23 +141,10 @@ func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCerti
|
|||||||
return nil, err
|
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{
|
cc := CachedCertificate{
|
||||||
Certificate: c,
|
Certificate: c,
|
||||||
InvertedGroups: make(map[string]struct{}),
|
InvertedGroups: make(map[string]struct{}),
|
||||||
Fingerprint: fp,
|
Fingerprint: fp,
|
||||||
fingerprint2: fp2,
|
|
||||||
signerFingerprint: signer.Fingerprint,
|
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
|
// 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.
|
// is a cheaper operation to perform as a result.
|
||||||
func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error {
|
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)
|
_, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -217,10 +172,6 @@ func (ncp *CAPool) verify(c Certificate, now time.Time, certFp string, signerFp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer.Certificate.Curve() != c.Curve() {
|
|
||||||
return nil, ErrCurveMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
if signer.Certificate.Expired(now) {
|
if signer.Certificate.Expired(now) {
|
||||||
return nil, ErrRootExpired
|
return nil, ErrRootExpired
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-128
@@ -1,14 +1,10 @@
|
|||||||
package cert
|
package cert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert/p256"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -115,60 +111,6 @@ k+coOv04r+zh33ISyhbsafnYduN17p2eD7CmHvHuerguXD9f32gcxo/KsFCKEjMe
|
|||||||
assert.Len(t, ppppp.CAs, 1)
|
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) {
|
func TestCertificateV1_Verify(t *testing.T) {
|
||||||
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
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)
|
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)
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
require.EqualError(t, err, "certificate is in the block list")
|
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()
|
caPool.ResetCertBlocklist()
|
||||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -254,7 +187,7 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
caPool = NewCAPool()
|
caPool = NewCAPool()
|
||||||
b, err = caPool.AddCAFromPEM(caPem)
|
b, err := caPool.AddCAFromPEM(caPem)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, b)
|
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"})
|
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)
|
_, 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)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,15 +394,6 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
|
|||||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
require.EqualError(t, err, "certificate is in the block list")
|
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()
|
caPool.ResetCertBlocklist()
|
||||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -497,7 +411,7 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
caPool = NewCAPool()
|
caPool = NewCAPool()
|
||||||
b, err = caPool.AddCAFromPEM(caPem)
|
b, err := caPool.AddCAFromPEM(caPem)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, b)
|
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"})
|
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)
|
_, 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)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,31 +558,3 @@ func TestCertificateV2_Verify_Subnets(t *testing.T) {
|
|||||||
_, err = caPool.VerifyCertificate(time.Now(), c)
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
require.NoError(t, err)
|
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"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert/p256"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Version uint8
|
type Version uint8
|
||||||
@@ -112,9 +110,6 @@ type CachedCertificate struct {
|
|||||||
InvertedGroups map[string]struct{}
|
InvertedGroups map[string]struct{}
|
||||||
Fingerprint string
|
Fingerprint string
|
||||||
signerFingerprint 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 {
|
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.
|
// 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
|
// 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.
|
// 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) {
|
func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certificate, error) {
|
||||||
if publicKey == nil {
|
if publicKey == nil {
|
||||||
return nil, ErrNoPeerStaticKey
|
return nil, ErrNoPeerStaticKey
|
||||||
@@ -157,31 +151,3 @@ func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certific
|
|||||||
|
|
||||||
return c, nil
|
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 {
|
switch c.details.curve {
|
||||||
case Curve_CURVE25519:
|
case Curve_CURVE25519:
|
||||||
if len(key) != ed25519.PublicKeySize {
|
|
||||||
return false //avoids a panic internal to ed25519
|
|
||||||
}
|
|
||||||
return ed25519.Verify(key, b, c.signature)
|
return ed25519.Verify(key, b, c.signature)
|
||||||
case Curve_P256:
|
case Curve_P256:
|
||||||
pubKey, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), key)
|
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),
|
unsafeNetworks: make([]netip.Prefix, len(rc.Details.Subnets)/2),
|
||||||
notBefore: time.Unix(rc.Details.NotBefore, 0),
|
notBefore: time.Unix(rc.Details.NotBefore, 0),
|
||||||
notAfter: time.Unix(rc.Details.NotAfter, 0),
|
notAfter: time.Unix(rc.Details.NotAfter, 0),
|
||||||
publicKey: nil,
|
publicKey: make([]byte, len(rc.Details.PublicKey)),
|
||||||
isCA: rc.Details.IsCA,
|
isCA: rc.Details.IsCA,
|
||||||
curve: rc.Details.Curve,
|
curve: rc.Details.Curve,
|
||||||
},
|
},
|
||||||
@@ -440,19 +437,12 @@ func unmarshalCertificateV1(b []byte, publicKey []byte) (*certificateV1, error)
|
|||||||
copy(nc.details.groups, rc.Details.Groups)
|
copy(nc.details.groups, rc.Details.Groups)
|
||||||
nc.details.issuer = hex.EncodeToString(rc.Details.Issuer)
|
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(publicKey) > 0 {
|
||||||
if len(rc.Details.PublicKey) != 0 {
|
nc.details.publicKey = publicKey
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy(nc.details.publicKey, rc.Details.PublicKey)
|
||||||
|
|
||||||
var ip netip.Addr
|
var ip netip.Addr
|
||||||
for i, rawIp := range rc.Details.Ips {
|
for i, rawIp := range rc.Details.Ips {
|
||||||
if i%2 == 0 {
|
if i%2 == 0 {
|
||||||
|
|||||||
+1
-63
@@ -62,62 +62,6 @@ func TestCertificateV1_Marshal(t *testing.T) {
|
|||||||
assert.Equal(t, nc.Groups(), nc2.Groups())
|
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) {
|
func TestCertificateV1_PublicKeyPem(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
@@ -155,19 +99,13 @@ func TestCertificateV1_PublicKeyPem(t *testing.T) {
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
-----END NEBULA P256 PUBLIC KEY-----
|
-----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)
|
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
nc.details.curve = Curve_P256
|
nc.details.curve = Curve_P256
|
||||||
nc.details.publicKey = pubP256Key
|
nc.details.publicKey = pubP256Key
|
||||||
assert.Equal(t, Curve_P256, nc.Curve())
|
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())
|
assert.True(t, nc.IsCA())
|
||||||
|
|
||||||
nc.details.isCA = false
|
nc.details.isCA = false
|
||||||
|
|||||||
+1
-10
@@ -151,9 +151,6 @@ func (c *certificateV2) CheckSignature(key []byte) bool {
|
|||||||
|
|
||||||
switch c.curve {
|
switch c.curve {
|
||||||
case Curve_CURVE25519:
|
case Curve_CURVE25519:
|
||||||
if len(key) != ed25519.PublicKeySize {
|
|
||||||
return false //avoids a panic internal to ed25519
|
|
||||||
}
|
|
||||||
return ed25519.Verify(key, b, c.signature)
|
return ed25519.Verify(key, b, c.signature)
|
||||||
case Curve_P256:
|
case Curve_P256:
|
||||||
pubKey, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), key)
|
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
|
// Maybe grab the public key
|
||||||
var rawPublicKey cryptobyte.String
|
var rawPublicKey cryptobyte.String
|
||||||
if len(publicKey) > 0 {
|
if len(publicKey) > 0 {
|
||||||
// If a public key is passed in, then the handshake certificate must
|
rawPublicKey = publicKey
|
||||||
// not have a public key present
|
|
||||||
if input.PeekASN1Tag(TagCertPublicKey) {
|
|
||||||
return nil, ErrCertPubkeyPresent
|
|
||||||
}
|
|
||||||
rawPublicKey = make(cryptobyte.String, len(publicKey))
|
|
||||||
copy(rawPublicKey, publicKey)
|
|
||||||
} else if !input.ReadOptionalASN1(&rawPublicKey, nil, TagCertPublicKey) {
|
} else if !input.ReadOptionalASN1(&rawPublicKey, nil, TagCertPublicKey) {
|
||||||
return nil, ErrBadFormat
|
return nil, ErrBadFormat
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-60
@@ -76,58 +76,6 @@ func TestCertificateV2_Marshal(t *testing.T) {
|
|||||||
assert.Equal(t, nc.Groups(), nc2.Groups())
|
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) {
|
func TestCertificateV2_PublicKeyPem(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
@@ -166,19 +114,12 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
-----END NEBULA P256 PUBLIC KEY-----
|
-----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)
|
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
nc.curve = Curve_P256
|
nc.curve = Curve_P256
|
||||||
nc.publicKey = pubP256Key
|
nc.publicKey = pubP256Key
|
||||||
assert.Equal(t, Curve_P256, nc.Curve())
|
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())
|
assert.True(t, nc.IsCA())
|
||||||
|
|
||||||
nc.details.isCA = false
|
nc.details.isCA = false
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ qrlJ69wer3ZUHFXA
|
|||||||
assert.Nil(t, k)
|
assert.Nil(t, k)
|
||||||
assert.Equal(t, rest, invalidPem)
|
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.
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
|
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
|
||||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
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")
|
ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
|
||||||
ErrCaNotFound = errors.New("could not find ca for the certificate")
|
ErrCaNotFound = errors.New("could not find ca for the certificate")
|
||||||
ErrUnknownVersion = errors.New("certificate version unrecognized")
|
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")
|
ErrInvalidPEMBlock = errors.New("input did not contain a valid PEM encoded block")
|
||||||
ErrInvalidPEMCertificateBanner = errors.New("bytes did not contain a proper certificate banner")
|
ErrInvalidPEMCertificateBanner = errors.New("bytes did not contain a proper certificate banner")
|
||||||
|
|||||||
+4
-10
@@ -13,12 +13,6 @@ import (
|
|||||||
"golang.org/x/crypto/ed25519"
|
"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
|
// 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) {
|
func NewTestCaCert(version Version, curve Curve, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (Certificate, []byte, []byte, []byte) {
|
||||||
var err error
|
var err error
|
||||||
@@ -40,10 +34,10 @@ func NewTestCaCert(version Version, curve Curve, before, after time.Time, networ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if before.IsZero() {
|
if before.IsZero() {
|
||||||
before = testCertNow.Add(time.Second * -60)
|
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
}
|
}
|
||||||
if after.IsZero() {
|
if after.IsZero() {
|
||||||
after = testCertNow.Add(time.Second * 60)
|
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &TBSCertificate{
|
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
|
// 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) {
|
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() {
|
if before.IsZero() {
|
||||||
before = testCertNow.Add(time.Second * -60)
|
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if after.IsZero() {
|
if after.IsZero() {
|
||||||
after = testCertNow.Add(time.Second * 60)
|
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(networks) == 0 {
|
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)
|
|
||||||
}
|
|
||||||
+113
-102
@@ -1,66 +1,14 @@
|
|||||||
package cert
|
package cert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"encoding/hex"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ed25519"
|
"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
|
const ( //cert banners
|
||||||
CertificateBanner = "NEBULA CERTIFICATE"
|
CertificateBanner = "NEBULA CERTIFICATE"
|
||||||
CertificateV2Banner = "NEBULA CERTIFICATE V2"
|
CertificateV2Banner = "NEBULA CERTIFICATE V2"
|
||||||
@@ -91,7 +39,19 @@ func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) {
|
|||||||
return nil, r, ErrInvalidPEMBlock
|
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 {
|
if err != nil {
|
||||||
return nil, r, err
|
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 {
|
func marshalCertPublicKeyToPEM(c Certificate) []byte {
|
||||||
if c.IsCA() {
|
if c.IsCA() {
|
||||||
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
|
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
|
||||||
@@ -142,15 +88,12 @@ func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
|||||||
case Curve_CURVE25519:
|
case Curve_CURVE25519:
|
||||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
|
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
|
||||||
case Curve_P256:
|
case Curve_P256:
|
||||||
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PublicKeyBanner, Bytes: b})
|
return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
|
||||||
default:
|
default:
|
||||||
return nil
|
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) {
|
func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||||
k, r := pem.Decode(b)
|
k, r := pem.Decode(b)
|
||||||
if k == nil {
|
if k == nil {
|
||||||
@@ -159,10 +102,10 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
|||||||
var expectedLen int
|
var expectedLen int
|
||||||
var curve Curve
|
var curve Curve
|
||||||
switch k.Type {
|
switch k.Type {
|
||||||
case X25519PublicKeyBanner:
|
case X25519PublicKeyBanner, Ed25519PublicKeyBanner:
|
||||||
expectedLen = 32
|
expectedLen = 32
|
||||||
curve = Curve_CURVE25519
|
curve = Curve_CURVE25519
|
||||||
case P256PublicKeyBanner:
|
case P256PublicKeyBanner, ECDSAP256PublicKeyBanner:
|
||||||
// Uncompressed
|
// Uncompressed
|
||||||
expectedLen = 65
|
expectedLen = 65
|
||||||
curve = Curve_P256
|
curve = Curve_P256
|
||||||
@@ -175,33 +118,6 @@ func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
|||||||
return k.Bytes, r, curve, nil
|
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 {
|
func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||||
switch curve {
|
switch curve {
|
||||||
case Curve_CURVE25519:
|
case Curve_CURVE25519:
|
||||||
@@ -224,6 +140,101 @@ func MarshalSigningPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
issuerBytes, err := func() ([]byte, error) {
|
||||||
|
issuer := c.Issuer()
|
||||||
|
if issuer == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
decoded, err := hex.DecodeString(issuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode issuer fingerprint: %w", err)
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return nil, rest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey := c.PublicKey()
|
||||||
|
if pubKey != nil {
|
||||||
|
pubKey = append([]byte(nil), pubKey...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := c.Signature()
|
||||||
|
if sig != nil {
|
||||||
|
sig = append([]byte(nil), sig...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NebulaCertificate{
|
||||||
|
Details: NebulaCertificateDetails{
|
||||||
|
Name: c.Name(),
|
||||||
|
NotBefore: c.NotBefore(),
|
||||||
|
NotAfter: c.NotAfter(),
|
||||||
|
PublicKey: pubKey,
|
||||||
|
IsCA: c.IsCA(),
|
||||||
|
Issuer: issuerBytes,
|
||||||
|
Curve: c.Curve(),
|
||||||
|
},
|
||||||
|
Signature: sig,
|
||||||
|
cert: c,
|
||||||
|
}, rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuerString returns the issuer in hex format for compatibility
|
||||||
|
func (n *NebulaCertificate) IssuerString() string {
|
||||||
|
if n.Details.Issuer == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(n.Details.Issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate returns the underlying certificate (read-only)
|
||||||
|
func (n *NebulaCertificate) Certificate() Certificate {
|
||||||
|
return n.cert
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalPrivateKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
// UnmarshalPrivateKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||||
// consumed data or an error on failure
|
// consumed data or an error on failure
|
||||||
func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||||
|
|||||||
+71
-167
@@ -1,88 +1,12 @@
|
|||||||
package cert
|
package cert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestUnmarshalCertificateFromPEM(t *testing.T) {
|
||||||
goodCert := []byte(`
|
goodCert := []byte(`
|
||||||
# A good cert
|
# A good cert
|
||||||
@@ -120,7 +44,7 @@ bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
|
|||||||
assert.Equal(t, rest, invalidPem)
|
assert.Equal(t, rest, invalidPem)
|
||||||
require.EqualError(t, err, "bytes did not contain a proper certificate banner")
|
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.
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
cert, rest, err = UnmarshalCertificateFromPEM(rest)
|
cert, rest, err = UnmarshalCertificateFromPEM(rest)
|
||||||
assert.Nil(t, cert)
|
assert.Nil(t, cert)
|
||||||
@@ -182,7 +106,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|||||||
assert.Equal(t, rest, invalidPem)
|
assert.Equal(t, rest, invalidPem)
|
||||||
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA private key banner")
|
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.
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
||||||
assert.Nil(t, k)
|
assert.Nil(t, k)
|
||||||
@@ -244,7 +168,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|||||||
assert.Equal(t, rest, invalidPem)
|
assert.Equal(t, rest, invalidPem)
|
||||||
require.EqualError(t, err, "bytes did not contain a proper private key banner")
|
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.
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
||||||
assert.Nil(t, k)
|
assert.Nil(t, k)
|
||||||
@@ -255,6 +179,60 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|||||||
func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
|
func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pubKey := []byte(`# A good key
|
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-----
|
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
-----END NEBULA X25519 PUBLIC KEY-----
|
-----END NEBULA X25519 PUBLIC KEY-----
|
||||||
@@ -265,7 +243,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
-----END NEBULA P256 PUBLIC KEY-----
|
-----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-----
|
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAA=
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
@@ -286,118 +264,44 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
-END NEBULA X25519 PUBLIC KEY-----`)
|
-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)
|
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||||
assert.Len(t, k, 32)
|
assert.Len(t, k, 32)
|
||||||
require.NoError(t, err)
|
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)
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
|
||||||
// P256 key
|
// Success test case
|
||||||
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
assert.Len(t, k, 65)
|
assert.Len(t, k, 65)
|
||||||
require.NoError(t, err)
|
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)
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
|
||||||
// Reject a signing public key (Ed25519/ECDSA banner)
|
// Success test case
|
||||||
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
k, rest, curve, 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)
|
|
||||||
assert.Len(t, k, 65)
|
assert.Len(t, k, 65)
|
||||||
require.NoError(t, err)
|
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)
|
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
|
// Fail due to short key
|
||||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
assert.Nil(t, k)
|
assert.Nil(t, k)
|
||||||
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||||
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||||
|
|
||||||
// Fail due to invalid banner
|
// Fail due to invalid banner
|
||||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
assert.Nil(t, k)
|
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)
|
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.
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
assert.Nil(t, k)
|
assert.Nil(t, k)
|
||||||
assert.Equal(t, rest, invalidPem)
|
assert.Equal(t, rest, invalidPem)
|
||||||
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert/p256"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TBSCertificate represents a certificate intended to be signed.
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if curve == Curve_P256 {
|
|
||||||
sig, err = p256.Normalize(sig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.setSignature(sig)
|
err = c.setSignature(sig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert/p256"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -90,48 +89,3 @@ func TestCertificateV1_SignP256(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, uc)
|
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
-62
@@ -14,12 +14,6 @@ import (
|
|||||||
"golang.org/x/crypto/ed25519"
|
"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
|
// 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) {
|
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
|
var err error
|
||||||
@@ -41,10 +35,10 @@ func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
if before.IsZero() {
|
if before.IsZero() {
|
||||||
before = testCertNow.Add(time.Second * -60)
|
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
}
|
}
|
||||||
if after.IsZero() {
|
if after.IsZero() {
|
||||||
after = testCertNow.Add(time.Second * 60)
|
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &cert.TBSCertificate{
|
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
|
// 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) {
|
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() {
|
if before.IsZero() {
|
||||||
before = testCertNow.Add(time.Second * -60)
|
before = time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if after.IsZero() {
|
if after.IsZero() {
|
||||||
after = testCertNow.Add(time.Second * 60)
|
after = time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pub, priv []byte
|
var pub, priv []byte
|
||||||
@@ -169,55 +163,3 @@ func P256Keypair() ([]byte, []byte) {
|
|||||||
pubkey := privkey.PublicKey()
|
pubkey := privkey.PublicKey()
|
||||||
return pubkey.Bytes(), privkey.Bytes()
|
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 {
|
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
|
||||||
return err
|
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 {
|
if err := mustFlagString("out-crt", cf.outCertPath); err != nil {
|
||||||
return err
|
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
|
var passphrase []byte
|
||||||
if !isP11 && *cf.encryption {
|
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 {
|
if len(passphrase) == 0 {
|
||||||
for i := 0; i < 5; i++ {
|
return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext")
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,16 +258,14 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
|||||||
Curve: curve,
|
Curve: curve,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isP11 && !isStdio(*cf.outKeyPath) {
|
if !isP11 {
|
||||||
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
||||||
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
|
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isStdio(*cf.outCertPath) {
|
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
||||||
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
|
||||||
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var c cert.Certificate
|
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)
|
b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
err = os.WriteFile(*cf.outKeyPath, b, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-key: %s", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||||
}
|
}
|
||||||
@@ -356,7 +329,6 @@ func caSummary() string {
|
|||||||
func caHelp(out io.Writer) {
|
func caHelp(out io.Writer) {
|
||||||
cf := newCaFlags()
|
cf := newCaFlags()
|
||||||
out.Write([]byte("Usage of " + os.Args[0] + " " + caSummary() + "\n"))
|
out.Write([]byte("Usage of " + os.Args[0] + " " + caSummary() + "\n"))
|
||||||
out.Write([]byte(stdioHelpText))
|
|
||||||
cf.set.SetOutput(out)
|
cf.set.SetOutput(out)
|
||||||
cf.set.PrintDefaults()
|
cf.set.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ func Test_caHelp(t *testing.T) {
|
|||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"Usage of "+os.Args[0]+" ca <flags>: create a self signed certificate authority\n"+
|
"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"+
|
" -argon-iterations uint\n"+
|
||||||
" \tOptional: Argon2 iterations parameter used for encrypted private key passphrase (default 1)\n"+
|
" \tOptional: Argon2 iterations parameter used for encrypted private key passphrase (default 1)\n"+
|
||||||
" -argon-memory uint\n"+
|
" -argon-memory uint\n"+
|
||||||
@@ -85,7 +84,7 @@ func Test_ca(t *testing.T) {
|
|||||||
err: nil,
|
err: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
pwPromptEB := "Enter passphrase: "
|
pwPromptOb := "Enter passphrase: "
|
||||||
|
|
||||||
// required args
|
// required args
|
||||||
assertHelpError(t, ca(
|
assertHelpError(t, ca(
|
||||||
@@ -169,19 +168,8 @@ func Test_ca(t *testing.T) {
|
|||||||
eb.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()}
|
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))
|
require.NoError(t, ca(args, ob, eb, testpw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Equal(t, pwPromptOb, 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.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
|
||||||
|
|
||||||
// read encrypted key file and verify default params
|
// read encrypted key file and verify default params
|
||||||
rb, _ = os.ReadFile(keyF.Name())
|
rb, _ = os.ReadFile(keyF.Name())
|
||||||
@@ -201,15 +189,15 @@ func Test_ca(t *testing.T) {
|
|||||||
assert.Empty(t, b)
|
assert.Empty(t, b)
|
||||||
assert.Len(t, lKey, 64)
|
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(keyF.Name())
|
||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.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()}
|
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))
|
require.Error(t, ca(args, ob, eb, errpw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Equal(t, pwPromptOb, ob.String())
|
||||||
assert.Equal(t, pwPromptEB, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// test when user fails to enter a password
|
// test when user fails to enter a password
|
||||||
os.Remove(keyF.Name())
|
os.Remove(keyF.Name())
|
||||||
@@ -218,8 +206,8 @@ func Test_ca(t *testing.T) {
|
|||||||
eb.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()}
|
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")
|
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(pwPromptOb, 5), ob.String()) // prompts 5 times before giving up
|
||||||
assert.Equal(t, strings.Repeat(pwPromptEB, 5), eb.String()) // prompts 5 times before giving up
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// create valid cert/key for overwrite tests
|
// create valid cert/key for overwrite tests
|
||||||
os.Remove(keyF.Name())
|
os.Remove(keyF.Name())
|
||||||
@@ -248,67 +236,3 @@ func Test_ca(t *testing.T) {
|
|||||||
os.Remove(keyF.Name())
|
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 {
|
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if *cf.outKeyPath != "" {
|
|
||||||
return newHelpErrorf("cannot set -out-key with -pkcs11")
|
|
||||||
}
|
}
|
||||||
if err = mustFlagString("out-pub", cf.outPubPath); err != nil {
|
if err = mustFlagString("out-pub", cf.outPubPath); err != nil {
|
||||||
return err
|
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 {
|
if isP11 {
|
||||||
p11Client, err := pkclient.FromUrl(*cf.p11url)
|
p11Client, err := pkclient.FromUrl(*cf.p11url)
|
||||||
if err != nil {
|
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)
|
return fmt.Errorf("error while getting public key: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = writeOutput(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-key: %s", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-pub: %s", err)
|
return fmt.Errorf("error while writing out-pub: %s", err)
|
||||||
}
|
}
|
||||||
@@ -112,7 +102,6 @@ func keygenSummary() string {
|
|||||||
func keygenHelp(out io.Writer) {
|
func keygenHelp(out io.Writer) {
|
||||||
cf := newKeygenFlags()
|
cf := newKeygenFlags()
|
||||||
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + keygenSummary() + "\n"))
|
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + keygenSummary() + "\n"))
|
||||||
_, _ = out.Write([]byte(stdioHelpText))
|
|
||||||
cf.set.SetOutput(out)
|
cf.set.SetOutput(out)
|
||||||
cf.set.PrintDefaults()
|
cf.set.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ func Test_keygenHelp(t *testing.T) {
|
|||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
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"+
|
"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"+
|
" -curve string\n"+
|
||||||
" \tECDH Curve (25519, P256) (default \"25519\")\n"+
|
" \tECDH Curve (25519, P256) (default \"25519\")\n"+
|
||||||
" -out-key string\n"+
|
" -out-key string\n"+
|
||||||
@@ -94,43 +93,3 @@ func Test_keygen(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, lPub, 32)
|
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"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A version string that can be set with
|
|
||||||
//
|
|
||||||
// -ldflags "-X main.Build=SOMEVERSION"
|
|
||||||
//
|
|
||||||
// at compile-time.
|
|
||||||
var Build string
|
var Build string
|
||||||
|
|
||||||
func init() {
|
|
||||||
if Build == "" {
|
|
||||||
info, ok := debug.ReadBuildInfo()
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Build = strings.TrimPrefix(info.Main.Version, "v")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type helpError struct {
|
type helpError struct {
|
||||||
s string
|
s string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ func (pr StdinPasswordReader) ReadPassword() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
// Terminal echo is off while reading, so the user's Enter key does not
|
fmt.Println()
|
||||||
// produce a visible newline. Emit one on stderr to match the prompt.
|
|
||||||
fmt.Fprintln(os.Stderr)
|
|
||||||
|
|
||||||
return password, err
|
return password, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,23 +40,11 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims ioClaims
|
rawCert, err := os.ReadFile(*pf.path)
|
||||||
if err := reserveInputs(&claims, "path", *pf.path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := reserveOutputs(&claims, "out-qr", *pf.outQRPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rawCert, err := readInput("path", *pf.path, &claims)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read cert; %s", err)
|
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 c cert.Certificate
|
||||||
var qrBytes []byte
|
var qrBytes []byte
|
||||||
part := 0
|
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)
|
return fmt.Errorf("error while unmarshaling cert: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !qrToStdout {
|
if *pf.json {
|
||||||
if *pf.json {
|
jsonCerts = append(jsonCerts, c)
|
||||||
jsonCerts = append(jsonCerts, c)
|
} else {
|
||||||
} else {
|
_, _ = out.Write([]byte(c.String()))
|
||||||
_, _ = out.Write([]byte(c.String()))
|
_, _ = out.Write([]byte("\n"))
|
||||||
_, _ = out.Write([]byte("\n"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *pf.outQRPath != "" {
|
if *pf.outQRPath != "" {
|
||||||
@@ -93,7 +79,7 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
part++
|
part++
|
||||||
}
|
}
|
||||||
|
|
||||||
if *pf.json && !qrToStdout {
|
if *pf.json {
|
||||||
b, _ := json.Marshal(jsonCerts)
|
b, _ := json.Marshal(jsonCerts)
|
||||||
_, _ = out.Write(b)
|
_, _ = out.Write(b)
|
||||||
_, _ = out.Write([]byte("\n"))
|
_, _ = 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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||||
}
|
}
|
||||||
@@ -121,7 +107,6 @@ func printSummary() string {
|
|||||||
func printHelp(out io.Writer) {
|
func printHelp(out io.Writer) {
|
||||||
pf := newPrintFlags()
|
pf := newPrintFlags()
|
||||||
out.Write([]byte("Usage of " + os.Args[0] + " " + printSummary() + "\n"))
|
out.Write([]byte("Usage of " + os.Args[0] + " " + printSummary() + "\n"))
|
||||||
out.Write([]byte(stdioHelpText))
|
|
||||||
pf.set.SetOutput(out)
|
pf.set.SetOutput(out)
|
||||||
pf.set.PrintDefaults()
|
pf.set.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ func Test_printHelp(t *testing.T) {
|
|||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"Usage of "+os.Args[0]+" print <flags>: prints details about a certificate\n"+
|
"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"+
|
" -json\n"+
|
||||||
" \tOptional: outputs certificates in json format\n"+
|
" \tOptional: outputs certificates in json format\n"+
|
||||||
" -out-qr string\n"+
|
" -out-qr string\n"+
|
||||||
@@ -179,44 +178,6 @@ func Test_printCert(t *testing.T) {
|
|||||||
ob.String(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
assert.Empty(t, eb.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
|
// NewTestCaCert will generate a CA cert
|
||||||
|
|||||||
+47
-75
@@ -43,7 +43,7 @@ type signFlags struct {
|
|||||||
func newSignFlags() *signFlags {
|
func newSignFlags() *signFlags {
|
||||||
sf := signFlags{set: flag.NewFlagSet("sign", flag.ContinueOnError)}
|
sf := signFlags{set: flag.NewFlagSet("sign", flag.ContinueOnError)}
|
||||||
sf.set.Usage = func() {}
|
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.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.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")
|
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 != "" {
|
if !isP11 && *sf.inPubPath != "" && *sf.outKeyPath != "" {
|
||||||
return newHelpErrorf("cannot set both -in-pub and -out-key")
|
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 v4Networks []netip.Prefix
|
||||||
var v6Networks []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)
|
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 curve cert.Curve
|
||||||
var caKey []byte
|
var caKey []byte
|
||||||
|
|
||||||
if !isP11 {
|
if !isP11 {
|
||||||
var rawCAKey []byte
|
var rawCAKey []byte
|
||||||
rawCAKey, err = readInput("ca-key", *sf.caKeyPath, &claims)
|
rawCAKey, err := os.ReadFile(*sf.caKeyPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while reading ca-key: %s", err)
|
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
|
// naively attempt to decode the private key as though it is not encrypted
|
||||||
caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
|
caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
|
||||||
if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
|
if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
|
||||||
|
// ask for a passphrase until we get one
|
||||||
var passphrase []byte
|
var passphrase []byte
|
||||||
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
for i := 0; i < 5; i++ {
|
||||||
if len(passphrase) == 0 {
|
out.Write([]byte("Enter passphrase: "))
|
||||||
// ask for a passphrase until we get one
|
passphrase, err = pr.ReadPassword()
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
errOut.Write([]byte("Enter passphrase: "))
|
|
||||||
passphrase, err = pr.ReadPassword()
|
|
||||||
|
|
||||||
if errors.Is(err, ErrNoTerminal) {
|
if errors.Is(err, ErrNoTerminal) {
|
||||||
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("error reading password: %s", err)
|
return fmt.Errorf("error reading password: %s", err)
|
||||||
}
|
|
||||||
|
|
||||||
if len(passphrase) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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)
|
curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while parsing encrypted ca-key: %s", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while reading ca-crt: %s", err)
|
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")
|
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 no duration is given, expire one second before the root expires
|
||||||
if *sf.duration <= 0 {
|
if *sf.duration <= 0 {
|
||||||
*sf.duration = time.Until(caCert.NotAfter()) - time.Second*1
|
*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 != "" {
|
if *sf.inPubPath != "" {
|
||||||
var pubCurve cert.Curve
|
var pubCurve cert.Curve
|
||||||
rawPub, err := readInput("in-pub", *sf.inPubPath, &claims)
|
rawPub, err := os.ReadFile(*sf.inPubPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while reading in-pub: %s", err)
|
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)
|
pub, rawPriv = newKeypair(curve)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isStdio(*sf.outCertPath) {
|
if *sf.outKeyPath == "" {
|
||||||
if _, err := os.Stat(*sf.outCertPath); err == nil {
|
*sf.outKeyPath = *sf.name + ".key"
|
||||||
return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
|
}
|
||||||
}
|
|
||||||
|
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
|
var crts []cert.Certificate
|
||||||
@@ -302,19 +277,21 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
notBefore := time.Now()
|
notBefore := time.Now()
|
||||||
notAfter := notBefore.Add(*sf.duration)
|
notAfter := notBefore.Add(*sf.duration)
|
||||||
|
|
||||||
switch version {
|
if version == 0 || version == cert.Version1 {
|
||||||
case cert.Version1:
|
// Make sure we at least have an ip
|
||||||
// Make sure we have only one ipv4 address
|
|
||||||
if len(v4Networks) != 1 {
|
if len(v4Networks) != 1 {
|
||||||
return newHelpErrorf("invalid -networks definition: v1 certificates can only have a single ipv4 address")
|
return newHelpErrorf("invalid -networks definition: v1 certificates can only have a single ipv4 address")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v6Networks) > 0 {
|
if version == cert.Version1 {
|
||||||
return newHelpErrorf("invalid -networks definition: v1 certificates can only contain ipv4 addresses")
|
// 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 {
|
if len(v6UnsafeNetworks) > 0 {
|
||||||
return newHelpErrorf("invalid -unsafe-networks definition: v1 certificates can only contain ipv4 addresses")
|
return newHelpErrorf("invalid -unsafe-networks definition: v1 certificates can only be ipv4")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &cert.TBSCertificate{
|
t := &cert.TBSCertificate{
|
||||||
@@ -344,8 +321,9 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
}
|
}
|
||||||
|
|
||||||
crts = append(crts, nc)
|
crts = append(crts, nc)
|
||||||
|
}
|
||||||
|
|
||||||
case cert.Version2:
|
if version == 0 || version == cert.Version2 {
|
||||||
t := &cert.TBSCertificate{
|
t := &cert.TBSCertificate{
|
||||||
Version: cert.Version2,
|
Version: cert.Version2,
|
||||||
Name: *sf.name,
|
Name: *sf.name,
|
||||||
@@ -373,19 +351,14 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
}
|
}
|
||||||
|
|
||||||
crts = append(crts, nc)
|
crts = append(crts, nc)
|
||||||
default:
|
|
||||||
// this should be unreachable
|
|
||||||
return fmt.Errorf("invalid version: %d", version)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isP11 && *sf.inPubPath == "" {
|
if !isP11 && *sf.inPubPath == "" {
|
||||||
if !isStdio(*sf.outKeyPath) {
|
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
||||||
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-key: %s", err)
|
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...)
|
b = append(b, sb...)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = writeOutput(*sf.outCertPath, b, 0600, out)
|
err = os.WriteFile(*sf.outCertPath, b, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||||
}
|
}
|
||||||
@@ -461,7 +434,6 @@ func signSummary() string {
|
|||||||
func signHelp(out io.Writer) {
|
func signHelp(out io.Writer) {
|
||||||
sf := newSignFlags()
|
sf := newSignFlags()
|
||||||
out.Write([]byte("Usage of " + os.Args[0] + " " + signSummary() + "\n"))
|
out.Write([]byte("Usage of " + os.Args[0] + " " + signSummary() + "\n"))
|
||||||
out.Write([]byte(stdioHelpText))
|
|
||||||
sf.set.SetOutput(out)
|
sf.set.SetOutput(out)
|
||||||
sf.set.PrintDefaults()
|
sf.set.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ func Test_signHelp(t *testing.T) {
|
|||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"Usage of "+os.Args[0]+" sign <flags>: create and sign a certificate\n"+
|
"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"+
|
" -ca-crt string\n"+
|
||||||
" \tOptional: path to the signing CA cert (default \"ca.crt\")\n"+
|
" \tOptional: path to the signing CA cert (default \"ca.crt\")\n"+
|
||||||
" -ca-key string\n"+
|
" -ca-key string\n"+
|
||||||
@@ -56,7 +55,7 @@ func Test_signHelp(t *testing.T) {
|
|||||||
" -unsafe-networks string\n"+
|
" -unsafe-networks string\n"+
|
||||||
" \tOptional: comma separated list of ip address and network in CIDR notation. Unsafe networks this cert can route for\n"+
|
" \tOptional: comma separated list of ip address and network in CIDR notation. Unsafe networks this cert can route for\n"+
|
||||||
" -version uint\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(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -205,7 +204,7 @@ func Test_signCert(t *testing.T) {
|
|||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.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"}
|
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, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
@@ -377,20 +376,8 @@ func Test_signCert(t *testing.T) {
|
|||||||
// test with the proper password
|
// 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"}
|
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))
|
require.NoError(t, signCert(args, ob, eb, testpw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||||
assert.Equal(t, "Enter passphrase: ", eb.String())
|
|
||||||
|
|
||||||
// test with the proper password in the environment
|
|
||||||
os.Remove(crtF.Name())
|
|
||||||
os.Remove(keyF.Name())
|
|
||||||
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
|
||||||
os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase))
|
|
||||||
ob.Reset()
|
|
||||||
eb.Reset()
|
|
||||||
require.NoError(t, signCert(args, ob, eb, testpw))
|
|
||||||
assert.Empty(t, ob.String())
|
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
|
||||||
|
|
||||||
// test with the wrong password
|
// test with the wrong password
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
@@ -399,19 +386,8 @@ func Test_signCert(t *testing.T) {
|
|||||||
testpw.password = []byte("invalid password")
|
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"}
|
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))
|
require.Error(t, signCert(args, ob, eb, testpw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Equal(t, "Enter passphrase: ", 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.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
|
||||||
|
|
||||||
// test with the user not entering a password
|
// test with the user not entering a password
|
||||||
ob.Reset()
|
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"}
|
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))
|
require.Error(t, signCert(args, ob, eb, nopw))
|
||||||
// normally the user hitting enter on the prompt would add newlines between these
|
// 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: ", ob.String())
|
||||||
assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// test an error condition
|
// test an error condition
|
||||||
ob.Reset()
|
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"}
|
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))
|
require.Error(t, signCert(args, ob, eb, errpw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Equal(t, "Enter passphrase: ", ob.String())
|
||||||
assert.Equal(t, "Enter passphrase: ", eb.String())
|
assert.Empty(t, 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())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
@@ -39,26 +40,24 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims ioClaims
|
rawCACert, err := os.ReadFile(*vf.caPath)
|
||||||
if err := reserveInputs(&claims,
|
|
||||||
"ca", *vf.caPath,
|
|
||||||
"crt", *vf.certPath,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
caReader, err := openInput("ca", *vf.caPath, &claims)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while reading ca: %w", err)
|
return fmt.Errorf("error while reading ca: %w", err)
|
||||||
}
|
}
|
||||||
defer caReader.Close()
|
|
||||||
|
|
||||||
caPool, err := cert.NewCAPoolFromPEMReader(caReader)
|
caPool := cert.NewCAPool()
|
||||||
if err != nil && !errors.Is(err, cert.ErrExpired) {
|
for {
|
||||||
return fmt.Errorf("error while adding ca cert to pool: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read crt: %w", err)
|
return fmt.Errorf("unable to read crt: %w", err)
|
||||||
}
|
}
|
||||||
@@ -93,7 +92,6 @@ func verifySummary() string {
|
|||||||
func verifyHelp(out io.Writer) {
|
func verifyHelp(out io.Writer) {
|
||||||
vf := newVerifyFlags()
|
vf := newVerifyFlags()
|
||||||
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + verifySummary() + "\n"))
|
_, _ = out.Write([]byte("Usage of " + os.Args[0] + " " + verifySummary() + "\n"))
|
||||||
_, _ = out.Write([]byte(stdioHelpText))
|
|
||||||
vf.set.SetOutput(out)
|
vf.set.SetOutput(out)
|
||||||
vf.set.PrintDefaults()
|
vf.set.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ func Test_verifyHelp(t *testing.T) {
|
|||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"Usage of "+os.Args[0]+" verify <flags>: verifies a certificate isn't expired and was signed by a trusted authority.\n"+
|
"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"+
|
" -ca string\n"+
|
||||||
" \tRequired: path to a file containing one or more ca certificates\n"+
|
" \tRequired: path to a file containing one or more ca certificates\n"+
|
||||||
" -crt string\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)
|
err = verify([]string{"-ca", caFile.Name(), "-crt", "does_not_exist"}, ob, eb)
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.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
|
// make a ca for later
|
||||||
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
@@ -123,46 +122,3 @@ func Test_verify(t *testing.T) {
|
|||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
require.NoError(t, err)
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/sirupsen/logrus"
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/slackhq/nebula/logging"
|
func HookLogger(l *logrus.Logger) {
|
||||||
)
|
// Do nothing, let the logs flow to stdout/stderr
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,54 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"fmt"
|
||||||
"log/slog"
|
"io/ioutil"
|
||||||
"strings"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/slackhq/nebula/logging"
|
"github.com/kardianos/service"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newPlatformLogger returns a *slog.Logger that routes every log record
|
// HookLogger routes the logrus logs through the service logger so that they end up in the Windows Event Viewer
|
||||||
// through the Windows service logger so records end up in the Windows
|
// logrus output will be discarded
|
||||||
// Event Log. All the heavy lifting (level management, format swap,
|
func HookLogger(l *logrus.Logger) {
|
||||||
// timestamp toggle, WithAttrs/WithGroup) comes from logging.NewHandler;
|
l.AddHook(newLogHook(logger))
|
||||||
// this file only contributes:
|
l.SetOutput(ioutil.Discard)
|
||||||
//
|
|
||||||
// - 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})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventLogWriter forwards slog-formatted lines to the Windows service
|
type logHook struct {
|
||||||
// logger at the severity most recently stashed by severityTag.Handle.
|
sl service.Logger
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *eventLogWriter) Write(p []byte) (int, error) {
|
func newLogHook(sl service.Logger) *logHook {
|
||||||
line := strings.TrimRight(string(p), "\n")
|
return &logHook{sl: sl}
|
||||||
switch {
|
}
|
||||||
case w.level >= slog.LevelError:
|
|
||||||
return len(p), logger.Error(line)
|
func (h *logHook) Fire(entry *logrus.Entry) error {
|
||||||
case w.level >= slog.LevelWarn:
|
line, err := entry.String()
|
||||||
return len(p), logger.Warning(line)
|
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:
|
default:
|
||||||
return len(p), logger.Info(line)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// severityTag embeds *logging.Handler to pick up everything it does for
|
func (h *logHook) Levels() []logrus.Level {
|
||||||
// free (Enabled, SetLevel, GetLevel, SetFormat, GetFormat,
|
return logrus.AllLevels
|
||||||
// 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}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-55
@@ -4,12 +4,10 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula"
|
"github.com/slackhq/nebula"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/logging"
|
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,17 +18,6 @@ import (
|
|||||||
// at compile-time.
|
// at compile-time.
|
||||||
var Build string
|
var Build string
|
||||||
|
|
||||||
func init() {
|
|
||||||
if Build == "" {
|
|
||||||
info, ok := debug.ReadBuildInfo()
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Build = strings.TrimPrefix(info.Main.Version, "v")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
serviceFlag := flag.String("service", "", "Control the system service.")
|
serviceFlag := flag.String("service", "", "Control the system service.")
|
||||||
configPath := flag.String("config", "", "Path to either a file or directory to load configuration from")
|
configPath := flag.String("config", "", "Path to either a file or directory to load configuration from")
|
||||||
@@ -50,30 +37,20 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := logging.NewLogger(os.Stdout)
|
|
||||||
|
|
||||||
if *serviceFlag != "" {
|
if *serviceFlag != "" {
|
||||||
if *configTest {
|
doService(configPath, configTest, Build, serviceFlag)
|
||||||
fmt.Println("-test is not supported with -service, run the config test without -service")
|
os.Exit(1)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := doService(configPath, Build, serviceFlag); err != nil {
|
|
||||||
l.Error("Service command failed", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *configPath == "" {
|
if *configPath == "" {
|
||||||
p, err := config.DefaultPath()
|
fmt.Println("-config flag must be set")
|
||||||
if err != nil {
|
flag.Usage()
|
||||||
fmt.Println(err)
|
os.Exit(1)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
*configPath = p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := logrus.New()
|
||||||
|
l.Out = os.Stdout
|
||||||
|
|
||||||
c := config.NewC(l)
|
c := config.NewC(l)
|
||||||
err := c.Load(*configPath)
|
err := c.Load(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,16 +58,6 @@ func main() {
|
|||||||
os.Exit(1)
|
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)
|
ctrl, err := nebula.Main(c, *configTest, Build, l, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.LogWithContextIfNeeded("Failed to start", err, l)
|
util.LogWithContextIfNeeded("Failed to start", err, l)
|
||||||
@@ -98,19 +65,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !*configTest {
|
if !*configTest {
|
||||||
if err := ctrl.Start(); err != nil {
|
ctrl.Start()
|
||||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
ctrl.ShutdownBlock()
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula"
|
"github.com/slackhq/nebula"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/logging"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger service.Logger
|
var logger service.Logger
|
||||||
|
|
||||||
type program struct {
|
type program struct {
|
||||||
configPath *string
|
configPath *string
|
||||||
|
configTest *bool
|
||||||
build string
|
build string
|
||||||
control *nebula.Control
|
control *nebula.Control
|
||||||
}
|
}
|
||||||
@@ -23,7 +25,8 @@ func (p *program) Start(s service.Service) error {
|
|||||||
// Start should not block.
|
// Start should not block.
|
||||||
logger.Info("Nebula service starting.")
|
logger.Info("Nebula service starting.")
|
||||||
|
|
||||||
l := newPlatformLogger()
|
l := logrus.New()
|
||||||
|
HookLogger(l)
|
||||||
|
|
||||||
c := config.NewC(l)
|
c := config.NewC(l)
|
||||||
err := c.Load(*p.configPath)
|
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)
|
return fmt.Errorf("failed to load config: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := logging.ApplyConfig(l, c); err != nil {
|
p.control, err = nebula.Main(c, *p.configTest, Build, l, 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.control.Start(); err != nil {
|
p.control.Start()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nebula can stop itself on a fatal packet reader error, make sure to log it if it happens.
|
|
||||||
go func() {
|
|
||||||
if err := p.control.Wait(); err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("Nebula stopped due to fatal error: %v", err))
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) Stop(s service.Service) error {
|
func (p *program) Stop(s service.Service) error {
|
||||||
logger.Info("Nebula service stopping.")
|
logger.Info("Nebula service stopping.")
|
||||||
if p.control == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.control.Stop()
|
p.control.Stop()
|
||||||
|
|
||||||
// block until nebula has fully drained before reporting stopped.
|
|
||||||
// error logging is handled by Start.
|
|
||||||
_ = p.control.Wait()
|
|
||||||
return nil
|
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 == "" {
|
if *configPath == "" {
|
||||||
p, err := config.DefaultPath()
|
ex, err := os.Executable()
|
||||||
if err != nil {
|
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{
|
svcConfig := &service.Config{
|
||||||
@@ -92,22 +78,23 @@ func doService(configPath *string, build string, serviceFlag *string) error {
|
|||||||
|
|
||||||
prg := &program{
|
prg := &program{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
|
configTest: configTest,
|
||||||
build: build,
|
build: build,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here are what the different loggers are doing:
|
// 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
|
// - `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)
|
// - `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)
|
s, err := service.New(prg, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := make(chan error, 5)
|
errs := make(chan error, 5)
|
||||||
logger, err = s.Logger(errs)
|
logger, err = s.Logger(errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -122,17 +109,18 @@ func doService(configPath *string, build string, serviceFlag *string) error {
|
|||||||
|
|
||||||
switch *serviceFlag {
|
switch *serviceFlag {
|
||||||
case "run":
|
case "run":
|
||||||
if err := s.Run(); err != nil {
|
err = s.Run()
|
||||||
// Route any errors to the system logger and report the failure
|
if err != nil {
|
||||||
|
// Route any errors to the system logger
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
default:
|
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)
|
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"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula"
|
"github.com/slackhq/nebula"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/logging"
|
|
||||||
"github.com/slackhq/nebula/util"
|
"github.com/slackhq/nebula/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,17 +18,6 @@ import (
|
|||||||
// at compile-time.
|
// at compile-time.
|
||||||
var Build string
|
var Build string
|
||||||
|
|
||||||
func init() {
|
|
||||||
if Build == "" {
|
|
||||||
info, ok := debug.ReadBuildInfo()
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Build = strings.TrimPrefix(info.Main.Version, "v")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configPath := flag.String("config", "", "Path to either a file or directory to load configuration from")
|
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")
|
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 == "" {
|
if *configPath == "" {
|
||||||
p, err := config.DefaultPath()
|
fmt.Println("-config flag must be set")
|
||||||
if err != nil {
|
flag.Usage()
|
||||||
fmt.Println(err)
|
os.Exit(1)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
*configPath = p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l := logging.NewLogger(os.Stdout)
|
l := logrus.New()
|
||||||
|
l.Out = os.Stdout
|
||||||
|
|
||||||
c := config.NewC(l)
|
c := config.NewC(l)
|
||||||
err := c.Load(*configPath)
|
err := c.Load(*configPath)
|
||||||
@@ -67,16 +52,6 @@ func main() {
|
|||||||
os.Exit(1)
|
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)
|
ctrl, err := nebula.Main(c, *configTest, Build, l, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.LogWithContextIfNeeded("Failed to start", err, l)
|
util.LogWithContextIfNeeded("Failed to start", err, l)
|
||||||
@@ -84,20 +59,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !*configTest {
|
if !*configTest {
|
||||||
if err := ctrl.Start(); err != nil {
|
ctrl.Start()
|
||||||
util.LogWithContextIfNeeded("Error while running", err, l)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
go ctrl.ShutdownBlock()
|
|
||||||
notifyReady(l)
|
notifyReady(l)
|
||||||
|
ctrl.ShutdownBlock()
|
||||||
if err := ctrl.Wait(); err != nil {
|
|
||||||
l.Error("Nebula stopped due to fatal error", "error", err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Info("Goodbye")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SdNotifyReady tells systemd the service is ready and dependent services can now be started
|
// 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
|
// https://www.freedesktop.org/software/systemd/man/systemd.service.html
|
||||||
const SdNotifyReady = "READY=1"
|
const SdNotifyReady = "READY=1"
|
||||||
|
|
||||||
func notifyReady(l *slog.Logger) {
|
func notifyReady(l *logrus.Logger) {
|
||||||
sockName := os.Getenv("NOTIFY_SOCKET")
|
sockName := os.Getenv("NOTIFY_SOCKET")
|
||||||
if sockName == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.DialTimeout("unixgram", sockName, time.Second)
|
conn, err := net.DialTimeout("unixgram", sockName, time.Second)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
err = conn.SetWriteDeadline(time.Now().Add(time.Second))
|
err = conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = conn.Write([]byte(SdNotifyReady)); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("notified systemd the service is ready")
|
l.Debugln("notified systemd the service is ready")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "log/slog"
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
func notifyReady(_ *slog.Logger) {
|
func notifyReady(_ *logrus.Logger) {
|
||||||
// No init service to notify
|
// No init service to notify
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-16
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -17,7 +16,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"go.yaml.in/yaml/v3"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type C struct {
|
type C struct {
|
||||||
@@ -26,11 +26,11 @@ type C struct {
|
|||||||
Settings map[string]any
|
Settings map[string]any
|
||||||
oldSettings map[string]any
|
oldSettings map[string]any
|
||||||
callbacks []func(*C)
|
callbacks []func(*C)
|
||||||
l *slog.Logger
|
l *logrus.Logger
|
||||||
reloadLock sync.Mutex
|
reloadLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewC(l *slog.Logger) *C {
|
func NewC(l *logrus.Logger) *C {
|
||||||
return &C{
|
return &C{
|
||||||
Settings: make(map[string]any),
|
Settings: make(map[string]any),
|
||||||
l: l,
|
l: l,
|
||||||
@@ -107,18 +107,12 @@ func (c *C) HasChanged(k string) bool {
|
|||||||
|
|
||||||
newVals, err := yaml.Marshal(nv)
|
newVals, err := yaml.Marshal(nv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.l.Error("Error while marshaling new config",
|
c.l.WithField("config_path", k).WithError(err).Error("Error while marshaling new config")
|
||||||
"config_path", k,
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oldVals, err := yaml.Marshal(ov)
|
oldVals, err := yaml.Marshal(ov)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.l.Error("Error while marshaling old config",
|
c.l.WithField("config_path", k).WithError(err).Error("Error while marshaling old config")
|
||||||
"config_path", k,
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(newVals) != string(oldVals)
|
return string(newVals) != string(oldVals)
|
||||||
@@ -160,10 +154,7 @@ func (c *C) ReloadConfig() {
|
|||||||
|
|
||||||
err := c.Load(c.path)
|
err := c.Load(c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.l.Error("Error occurred while reloading config",
|
c.l.WithField("config_path", c.path).WithError(err).Error("Error occurred while reloading config")
|
||||||
"config_path", c.path,
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.yaml.in/yaml/v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Load(t *testing.T) {
|
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))
|
|
||||||
}
|
|
||||||
+115
-85
@@ -5,12 +5,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcrowley/go-metrics"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/header"
|
"github.com/slackhq/nebula/header"
|
||||||
@@ -44,16 +45,19 @@ type connectionManager struct {
|
|||||||
inactivityTimeout atomic.Int64
|
inactivityTimeout atomic.Int64
|
||||||
dropInactive atomic.Bool
|
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{
|
cm := &connectionManager{
|
||||||
hostMap: hm,
|
hostMap: hm,
|
||||||
l: l,
|
l: l,
|
||||||
punchy: p,
|
punchy: p,
|
||||||
relayUsed: make(map[uint32]struct{}),
|
relayUsed: make(map[uint32]struct{}),
|
||||||
relayUsedLock: &sync.RWMutex{},
|
relayUsedLock: &sync.RWMutex{},
|
||||||
|
metricsTxPunchy: metrics.GetOrRegisterCounter("messages.tx.punchy", nil),
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.reload(c, true)
|
cm.reload(c, true)
|
||||||
@@ -81,10 +85,9 @@ func (cm *connectionManager) reload(c *config.C, initial bool) {
|
|||||||
old := cm.getInactivityTimeout()
|
old := cm.getInactivityTimeout()
|
||||||
cm.inactivityTimeout.Store((int64)(c.GetDuration("tunnels.inactivity_timeout", 10*time.Minute)))
|
cm.inactivityTimeout.Store((int64)(c.GetDuration("tunnels.inactivity_timeout", 10*time.Minute)))
|
||||||
if !initial {
|
if !initial {
|
||||||
cm.l.Info("Inactivity timeout has changed",
|
cm.l.WithField("oldDuration", old).
|
||||||
"oldDuration", old,
|
WithField("newDuration", cm.getInactivityTimeout()).
|
||||||
"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()
|
old := cm.dropInactive.Load()
|
||||||
cm.dropInactive.Store(c.GetBool("tunnels.drop_inactive", false))
|
cm.dropInactive.Store(c.GetBool("tunnels.drop_inactive", false))
|
||||||
if !initial {
|
if !initial {
|
||||||
cm.l.Info("Drop inactive setting has changed",
|
cm.l.WithField("oldBool", old).
|
||||||
"oldBool", old,
|
WithField("newBool", cm.dropInactive.Load()).
|
||||||
"newBool", cm.dropInactive.Load(),
|
Info("Drop inactive setting has changed")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,12 +107,11 @@ func (cm *connectionManager) getInactivityTimeout() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) In(h *HostInfo) {
|
func (cm *connectionManager) In(h *HostInfo) {
|
||||||
h.markIn()
|
h.in.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Out records outbound traffic and reports whether the local network changed since this tunnel last sent.
|
func (cm *connectionManager) Out(h *HostInfo) {
|
||||||
func (cm *connectionManager) Out(h *HostInfo) bool {
|
h.out.Store(true)
|
||||||
return h.markOut(cm.intf.rebindEpoch.Load())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) RelayUsed(localIndex uint32) {
|
func (cm *connectionManager) RelayUsed(localIndex uint32) {
|
||||||
@@ -129,13 +130,22 @@ func (cm *connectionManager) RelayUsed(localIndex uint32) {
|
|||||||
// getAndResetTrafficCheck returns if there was any inbound or outbound traffic within the last tick and
|
// getAndResetTrafficCheck returns if there was any inbound or outbound traffic within the last tick and
|
||||||
// resets the state for this local index
|
// resets the state for this local index
|
||||||
func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time) (bool, bool) {
|
func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time) (bool, bool) {
|
||||||
in, out := h.takeTraffic()
|
in := h.in.Swap(false)
|
||||||
|
out := h.out.Swap(false)
|
||||||
if in || out {
|
if in || out {
|
||||||
h.lastUsed = now
|
h.lastUsed = now
|
||||||
}
|
}
|
||||||
return in, out
|
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) {
|
func (cm *connectionManager) Start(ctx context.Context) {
|
||||||
clockSource := time.NewTicker(cm.trafficTimer.t.tickDuration)
|
clockSource := time.NewTicker(cm.trafficTimer.t.tickDuration)
|
||||||
defer clockSource.Stop()
|
defer clockSource.Stop()
|
||||||
@@ -246,7 +256,7 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
|||||||
var err error
|
var err error
|
||||||
index, err = AddRelay(cm.l, newhostinfo, cm.hostMap, r.PeerAddr, nil, r.Type, Requested)
|
index, err = AddRelay(cm.l, newhostinfo, cm.hostMap, r.PeerAddr, nil, r.Type, Requested)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
@@ -294,16 +304,16 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
|||||||
|
|
||||||
msg, err := req.Marshal()
|
msg, err := req.Marshal()
|
||||||
if err != nil {
|
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 {
|
} else {
|
||||||
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
|
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
|
||||||
cm.l.Info("send CreateRelayRequest",
|
cm.l.WithFields(logrus.Fields{
|
||||||
"relayFrom", relayFrom,
|
"relayFrom": req.RelayFromAddr,
|
||||||
"relayTo", relayTo,
|
"relayTo": req.RelayToAddr,
|
||||||
"initiatorRelayIndex", req.InitiatorRelayIndex,
|
"initiatorRelayIndex": req.InitiatorRelayIndex,
|
||||||
"responderRelayIndex", req.ResponderRelayIndex,
|
"responderRelayIndex": req.ResponderRelayIndex,
|
||||||
"vpnAddrs", newhostinfo.vpnAddrs,
|
"vpnAddrs": newhostinfo.vpnAddrs}).
|
||||||
)
|
Info("send CreateRelayRequest")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +325,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
|
|
||||||
hostinfo := cm.hostMap.Indexes[localIndex]
|
hostinfo := cm.hostMap.Indexes[localIndex]
|
||||||
if hostinfo == nil {
|
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
|
return doNothing, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,12 +345,12 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
// A hostinfo is determined alive if there is incoming traffic
|
// A hostinfo is determined alive if there is incoming traffic
|
||||||
if inTraffic {
|
if inTraffic {
|
||||||
decision := doNothing
|
decision := doNothing
|
||||||
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
if cm.l.Level >= logrus.DebugLevel {
|
||||||
hostinfo.logger(cm.l).Debug("Tunnel status",
|
hostinfo.logger(cm.l).
|
||||||
"tunnelCheck", m{"state": "alive", "method": "passive"},
|
WithField("tunnelCheck", m{"state": "alive", "method": "passive"}).
|
||||||
)
|
Debug("Tunnel status")
|
||||||
}
|
}
|
||||||
hostinfo.setPendingDeletion(false)
|
hostinfo.pendingDeletion.Store(false)
|
||||||
|
|
||||||
if mainHostInfo {
|
if mainHostInfo {
|
||||||
decision = tryRehandshake
|
decision = tryRehandshake
|
||||||
@@ -357,17 +367,17 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
|
|
||||||
if !outTraffic {
|
if !outTraffic {
|
||||||
// Send a punch packet to keep the NAT state alive
|
// Send a punch packet to keep the NAT state alive
|
||||||
cm.punchy.SendPunch(hostinfo)
|
cm.sendPunch(hostinfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return decision, hostinfo, primary
|
return decision, hostinfo, primary
|
||||||
}
|
}
|
||||||
|
|
||||||
if hostinfo.isPendingDeletion() {
|
if hostinfo.pendingDeletion.Load() {
|
||||||
// We have already sent a test packet and nothing was returned, this hostinfo is dead
|
// We have already sent a test packet and nothing was returned, this hostinfo is dead
|
||||||
hostinfo.logger(cm.l).Info("Tunnel status",
|
hostinfo.logger(cm.l).
|
||||||
"tunnelCheck", m{"state": "dead", "method": "active"},
|
WithField("tunnelCheck", m{"state": "dead", "method": "active"}).
|
||||||
)
|
Info("Tunnel status")
|
||||||
|
|
||||||
return deleteTunnel, hostinfo, nil
|
return deleteTunnel, hostinfo, nil
|
||||||
}
|
}
|
||||||
@@ -378,43 +388,44 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
inactiveFor, isInactive := cm.isInactive(hostinfo, now)
|
inactiveFor, isInactive := cm.isInactive(hostinfo, now)
|
||||||
if isInactive {
|
if isInactive {
|
||||||
// Tunnel is inactive, tear it down
|
// Tunnel is inactive, tear it down
|
||||||
hostinfo.logger(cm.l).Info("Dropping tunnel due to inactivity",
|
hostinfo.logger(cm.l).
|
||||||
"inactiveDuration", inactiveFor,
|
WithField("inactiveDuration", inactiveFor).
|
||||||
"primary", mainHostInfo,
|
WithField("primary", mainHostInfo).
|
||||||
)
|
Info("Dropping tunnel due to inactivity")
|
||||||
|
|
||||||
return closeTunnel, hostinfo, primary
|
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.
|
// 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.
|
// Just maintain NAT state if configured to do so.
|
||||||
cm.punchy.SendPunch(hostinfo)
|
cm.sendPunch(hostinfo)
|
||||||
cm.trafficTimer.Add(hostinfo.localIndexId, cm.checkInterval)
|
cm.trafficTimer.Add(hostinfo.localIndexId, cm.checkInterval)
|
||||||
return doNothing, nil, nil
|
return doNothing, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// We aren't receiving traffic but we are sending it. The outbound
|
if cm.punchy.GetTargetEverything() {
|
||||||
// traffic itself refreshes the primary remote's NAT state; this
|
// This is similar to the old punchy behavior with a slight optimization.
|
||||||
// fans out to non-primary remotes, but only if target_all_remotes
|
// We aren't receiving traffic but we are sending it, punch on all known
|
||||||
// is configured.
|
// ips in case we need to re-prime NAT state
|
||||||
cm.punchy.SendPunchToAll(hostinfo)
|
cm.sendPunch(hostinfo)
|
||||||
|
}
|
||||||
|
|
||||||
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
if cm.l.Level >= logrus.DebugLevel {
|
||||||
hostinfo.logger(cm.l).Debug("Tunnel status",
|
hostinfo.logger(cm.l).
|
||||||
"tunnelCheck", m{"state": "testing", "method": "active"},
|
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
|
// Send a test packet to trigger an authenticated tunnel test, this should suss out any lingering tunnel issues
|
||||||
decision = sendTestPacket
|
decision = sendTestPacket
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
if cm.l.Level >= logrus.DebugLevel {
|
||||||
hostinfo.logger(cm.l).Debug("Hostinfo sadness")
|
hostinfo.logger(cm.l).Debugf("Hostinfo sadness")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hostinfo.setPendingDeletion(true)
|
hostinfo.pendingDeletion.Store(true)
|
||||||
cm.trafficTimer.Add(hostinfo.localIndexId, cm.pendingDeletionInterval)
|
cm.trafficTimer.Add(hostinfo.localIndexId, cm.pendingDeletionInterval)
|
||||||
return decision, hostinfo, nil
|
return decision, hostinfo, nil
|
||||||
}
|
}
|
||||||
@@ -482,16 +493,14 @@ func (cm *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostI
|
|||||||
return false //cert is still valid! yay!
|
return false //cert is still valid! yay!
|
||||||
} else if err == cert.ErrBlockListed { //avoiding errors.Is for speed
|
} else if err == cert.ErrBlockListed { //avoiding errors.Is for speed
|
||||||
// Block listed certificates should always be disconnected
|
// Block listed certificates should always be disconnected
|
||||||
hostinfo.logger(cm.l).Info("Remote certificate is blocked, tearing down the tunnel",
|
hostinfo.logger(cm.l).WithError(err).
|
||||||
"error", err,
|
WithField("fingerprint", remoteCert.Fingerprint).
|
||||||
"fingerprint", remoteCert.Fingerprint,
|
Info("Remote certificate is blocked, tearing down the tunnel")
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
} else if cm.intf.disconnectInvalid.Load() {
|
} else if cm.intf.disconnectInvalid.Load() {
|
||||||
hostinfo.logger(cm.l).Info("Remote certificate is no longer valid, tearing down the tunnel",
|
hostinfo.logger(cm.l).WithError(err).
|
||||||
"error", err,
|
WithField("fingerprint", remoteCert.Fingerprint).
|
||||||
"fingerprint", remoteCert.Fingerprint,
|
Info("Remote certificate is no longer valid, tearing down the tunnel")
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
//if we reach here, the cert is no longer valid, but we're configured to keep tunnels from now-invalid certs open
|
//if we reach here, the cert is no longer valid, but we're configured to keep tunnels from now-invalid certs open
|
||||||
@@ -499,17 +508,41 @@ func (cm *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
|
||||||
cs := cm.intf.pki.getCertState()
|
cs := cm.intf.pki.getCertState()
|
||||||
curCrt := hostinfo.ConnectionState.myCert
|
curCrt := hostinfo.ConnectionState.myCert
|
||||||
curCrtVersion := curCrt.Version()
|
curCrtVersion := curCrt.Version()
|
||||||
myCrt := cs.getCertificate(curCrtVersion)
|
myCrt := cs.getCertificate(curCrtVersion)
|
||||||
if myCrt == nil {
|
if myCrt == nil {
|
||||||
cm.l.Info("Re-handshaking with remote",
|
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs).
|
||||||
"vpnAddrs", hostinfo.vpnAddrs,
|
WithField("version", curCrtVersion).
|
||||||
"version", curCrtVersion,
|
WithField("reason", "local certificate removed").
|
||||||
"reason", "local certificate removed",
|
Info("Re-handshaking with remote")
|
||||||
)
|
|
||||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -517,12 +550,11 @@ func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
|
|||||||
if peerCrt != nil && curCrtVersion < peerCrt.Certificate.Version() {
|
if peerCrt != nil && curCrtVersion < peerCrt.Certificate.Version() {
|
||||||
// if our certificate version is less than theirs, and we have a matching version available, rehandshake?
|
// if our certificate version is less than theirs, and we have a matching version available, rehandshake?
|
||||||
if cs.getCertificate(peerCrt.Certificate.Version()) != nil {
|
if cs.getCertificate(peerCrt.Certificate.Version()) != nil {
|
||||||
cm.l.Info("Re-handshaking with remote",
|
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs).
|
||||||
"vpnAddrs", hostinfo.vpnAddrs,
|
WithField("version", curCrtVersion).
|
||||||
"version", curCrtVersion,
|
WithField("peerVersion", peerCrt.Certificate.Version()).
|
||||||
"peerVersion", peerCrt.Certificate.Version(),
|
WithField("reason", "local certificate version lower than peer, attempting to correct").
|
||||||
"reason", "local certificate version lower than peer, attempting to correct",
|
Info("Re-handshaking with remote")
|
||||||
)
|
|
||||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], func(hh *HandshakeHostInfo) {
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], func(hh *HandshakeHostInfo) {
|
||||||
hh.initiatingVersionOverride = peerCrt.Certificate.Version()
|
hh.initiatingVersionOverride = peerCrt.Certificate.Version()
|
||||||
})
|
})
|
||||||
@@ -530,19 +562,17 @@ func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !bytes.Equal(curCrt.Signature(), myCrt.Signature()) {
|
if !bytes.Equal(curCrt.Signature(), myCrt.Signature()) {
|
||||||
cm.l.Info("Re-handshaking with remote",
|
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs).
|
||||||
"vpnAddrs", hostinfo.vpnAddrs,
|
WithField("reason", "local certificate is not current").
|
||||||
"reason", "local certificate is not current",
|
Info("Re-handshaking with remote")
|
||||||
)
|
|
||||||
|
|
||||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if curCrtVersion < cs.initiatingVersion {
|
if curCrtVersion < cs.initiatingVersion {
|
||||||
cm.l.Info("Re-handshaking with remote",
|
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs).
|
||||||
"vpnAddrs", hostinfo.vpnAddrs,
|
WithField("reason", "current cert version < pki.initiatingVersion").
|
||||||
"reason", "current cert version < pki.initiatingVersion",
|
Info("Re-handshaking with remote")
|
||||||
)
|
|
||||||
|
|
||||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||||
return
|
return
|
||||||
|
|||||||
+63
-60
@@ -7,9 +7,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/flynn/noise"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/overlay/overlaytest"
|
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
"github.com/slackhq/nebula/udp"
|
"github.com/slackhq/nebula/udp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -25,7 +25,6 @@ func newTestLighthouse() *LightHouse {
|
|||||||
lighthouses := []netip.Addr{}
|
lighthouses := []netip.Addr{}
|
||||||
staticList := map[netip.Addr]struct{}{}
|
staticList := map[netip.Addr]struct{}{}
|
||||||
|
|
||||||
lh.localAddrsFn = func(*LocalAllowList) []netip.Addr { return nil }
|
|
||||||
lh.lighthouses.Store(&lighthouses)
|
lh.lighthouses.Store(&lighthouses)
|
||||||
lh.staticList.Store(&staticList)
|
lh.staticList.Store(&staticList)
|
||||||
|
|
||||||
@@ -47,13 +46,13 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
initiatingVersion: cert.Version1,
|
initiatingVersion: cert.Version1,
|
||||||
privateKey: []byte{},
|
privateKey: []byte{},
|
||||||
v1Cert: &dummyCert{version: cert.Version1},
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
v1Credential: nil,
|
v1HandshakeBytes: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &overlaytest.NoopTun{},
|
inside: &test.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -64,9 +63,9 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
ifce.pki.cs.Store(cs)
|
ifce.pki.cs.Store(cs)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(test.NewLogger())
|
conf := config.NewC(l)
|
||||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
punchy := NewPunchyFromConfig(l, conf)
|
||||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
p := []byte("")
|
p := []byte("")
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
@@ -80,31 +79,32 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
hostinfo.ConnectionState = &ConnectionState{
|
||||||
myCert: &dummyCert{version: cert.Version1},
|
myCert: &dummyCert{version: cert.Version1},
|
||||||
|
H: &noise.HandshakeState{},
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|
||||||
// We saw traffic out to vpnIp
|
// We saw traffic out to vpnIp
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.True(t, hostinfo.sentSinceCheck())
|
assert.True(t, hostinfo.out.Load())
|
||||||
assert.True(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.True(t, hostinfo.in.Load())
|
||||||
|
|
||||||
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
|
|
||||||
// Do another traffic check tick, this host should be pending deletion now
|
// Do another traffic check tick, this host should be pending deletion now
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
assert.True(t, hostinfo.sentSinceCheck())
|
assert.True(t, hostinfo.out.Load())
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.True(t, hostinfo.isPendingDeletion())
|
assert.True(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
@@ -129,13 +129,13 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
initiatingVersion: cert.Version1,
|
initiatingVersion: cert.Version1,
|
||||||
privateKey: []byte{},
|
privateKey: []byte{},
|
||||||
v1Cert: &dummyCert{version: cert.Version1},
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
v1Credential: nil,
|
v1HandshakeBytes: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &overlaytest.NoopTun{},
|
inside: &test.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -146,9 +146,9 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
ifce.pki.cs.Store(cs)
|
ifce.pki.cs.Store(cs)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(test.NewLogger())
|
conf := config.NewC(l)
|
||||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
punchy := NewPunchyFromConfig(l, conf)
|
||||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
p := []byte("")
|
p := []byte("")
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
@@ -162,39 +162,40 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
hostinfo.ConnectionState = &ConnectionState{
|
||||||
myCert: &dummyCert{version: cert.Version1},
|
myCert: &dummyCert{version: cert.Version1},
|
||||||
|
H: &noise.HandshakeState{},
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|
||||||
// We saw traffic out to vpnIp
|
// We saw traffic out to vpnIp
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.True(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.True(t, hostinfo.in.Load())
|
||||||
assert.True(t, hostinfo.sentSinceCheck())
|
assert.True(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
|
|
||||||
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
|
|
||||||
// Do another traffic check tick, this host should be pending deletion now
|
// Do another traffic check tick, this host should be pending deletion now
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.True(t, hostinfo.isPendingDeletion())
|
assert.True(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
// We saw traffic, should no longer be pending deletion
|
// We saw traffic, should no longer be pending deletion
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
}
|
}
|
||||||
@@ -213,13 +214,13 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
initiatingVersion: cert.Version1,
|
initiatingVersion: cert.Version1,
|
||||||
privateKey: []byte{},
|
privateKey: []byte{},
|
||||||
v1Cert: &dummyCert{version: cert.Version1},
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
v1Credential: nil,
|
v1HandshakeBytes: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &overlaytest.NoopTun{},
|
inside: &test.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -230,12 +231,12 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
ifce.pki.cs.Store(cs)
|
ifce.pki.cs.Store(cs)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(test.NewLogger())
|
conf := config.NewC(l)
|
||||||
conf.Settings["tunnels"] = map[string]any{
|
conf.Settings["tunnels"] = map[string]any{
|
||||||
"drop_inactive": true,
|
"drop_inactive": true,
|
||||||
}
|
}
|
||||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
punchy := NewPunchyFromConfig(l, conf)
|
||||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||||
assert.True(t, nc.dropInactive.Load())
|
assert.True(t, nc.dropInactive.Load())
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
|
|
||||||
@@ -247,37 +248,38 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
hostinfo.ConnectionState = &ConnectionState{
|
||||||
myCert: &dummyCert{version: cert.Version1},
|
myCert: &dummyCert{version: cert.Version1},
|
||||||
|
H: &noise.HandshakeState{},
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|
||||||
// Do a traffic check tick, in and out should be cleared but should not be pending deletion
|
// Do a traffic check tick, in and out should be cleared but should not be pending deletion
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.True(t, hostinfo.sentSinceCheck())
|
assert.True(t, hostinfo.out.Load())
|
||||||
assert.True(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.True(t, hostinfo.in.Load())
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
decision, _, _ := nc.makeTrafficDecision(hostinfo.localIndexId, now)
|
decision, _, _ := nc.makeTrafficDecision(hostinfo.localIndexId, now)
|
||||||
assert.Equal(t, tryRehandshake, decision)
|
assert.Equal(t, tryRehandshake, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
|
|
||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*5))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*5))
|
||||||
assert.Equal(t, doNothing, decision)
|
assert.Equal(t, doNothing, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
|
|
||||||
// Do another traffic check tick, should still not be pending deletion
|
// Do another traffic check tick, should still not be pending deletion
|
||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*10))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*10))
|
||||||
assert.Equal(t, doNothing, decision)
|
assert.Equal(t, doNothing, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
@@ -285,9 +287,9 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Minute*10))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Minute*10))
|
||||||
assert.Equal(t, closeTunnel, decision)
|
assert.Equal(t, closeTunnel, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.isPendingDeletion())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.False(t, hostinfo.sentSinceCheck())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
assert.False(t, hostinfo.in.Load())
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
}
|
}
|
||||||
@@ -337,15 +339,15 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
|||||||
cachedPeerCert, err := ncp.VerifyCertificate(now.Add(time.Second), peerCert)
|
cachedPeerCert, err := ncp.VerifyCertificate(now.Add(time.Second), peerCert)
|
||||||
|
|
||||||
cs := &CertState{
|
cs := &CertState{
|
||||||
privateKey: []byte{},
|
privateKey: []byte{},
|
||||||
v1Cert: &dummyCert{},
|
v1Cert: &dummyCert{},
|
||||||
v1Credential: nil,
|
v1HandshakeBytes: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &overlaytest.NoopTun{},
|
inside: &test.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -358,9 +360,9 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
|||||||
ifce.disconnectInvalid.Store(true)
|
ifce.disconnectInvalid.Store(true)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(test.NewLogger())
|
conf := config.NewC(l)
|
||||||
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
punchy := NewPunchyFromConfig(l, conf)
|
||||||
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
ifce.connectionManager = nc
|
ifce.connectionManager = nc
|
||||||
|
|
||||||
@@ -369,6 +371,7 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
|||||||
ConnectionState: &ConnectionState{
|
ConnectionState: &ConnectionState{
|
||||||
myCert: &dummyCert{},
|
myCert: &dummyCert{},
|
||||||
peerCert: cachedPeerCert,
|
peerCert: cachedPeerCert,
|
||||||
|
H: &noise.HandshakeState{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|||||||
+59
-71
@@ -1,49 +1,86 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/flynn/noise"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/handshake"
|
|
||||||
"github.com/slackhq/nebula/header"
|
|
||||||
"github.com/slackhq/nebula/noiseutil"
|
"github.com/slackhq/nebula/noiseutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ReplayWindow = 1024
|
const ReplayWindow = 1024
|
||||||
|
|
||||||
type ConnectionState struct {
|
type ConnectionState struct {
|
||||||
eKey noiseutil.CipherState
|
eKey *NebulaCipherState
|
||||||
dKey noiseutil.CipherState
|
dKey *NebulaCipherState
|
||||||
|
H *noise.HandshakeState
|
||||||
myCert cert.Certificate
|
myCert cert.Certificate
|
||||||
peerCert *cert.CachedCertificate
|
peerCert *cert.CachedCertificate
|
||||||
initiator bool
|
initiator bool
|
||||||
messageCounter atomic.Uint64
|
messageCounter atomic.Uint64
|
||||||
window *Bits
|
window *Bits
|
||||||
decryptLock sync.Mutex
|
|
||||||
writeLock sync.Mutex
|
writeLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// newConnectionStateFromResult builds a fully-populated ConnectionState from a
|
func NewConnectionState(l *logrus.Logger, cs *CertState, crt cert.Certificate, initiator bool, pattern noise.HandshakePattern) (*ConnectionState, error) {
|
||||||
// completed handshake.Result. It seeds messageCounter and the replay window so
|
var dhFunc noise.DHFunc
|
||||||
// that the post-handshake message indices already used on the wire don't count
|
switch crt.Curve() {
|
||||||
// as missed traffic in the data plane.
|
case cert.Curve_CURVE25519:
|
||||||
func newConnectionStateFromResult(r *handshake.Result) *ConnectionState {
|
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{
|
ci := &ConnectionState{
|
||||||
myCert: r.MyCert,
|
H: hs,
|
||||||
initiator: r.Initiator,
|
initiator: initiator,
|
||||||
peerCert: r.RemoteCert,
|
window: b,
|
||||||
eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
|
myCert: crt,
|
||||||
dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
|
|
||||||
window: NewBits(ReplayWindow),
|
|
||||||
}
|
}
|
||||||
ci.messageCounter.Add(r.MessageIndex)
|
// always start the counter from 2, as packet 1 and packet 2 are handshake packets.
|
||||||
for i := uint64(1); i <= r.MessageIndex; i++ {
|
ci.messageCounter.Add(2)
|
||||||
ci.window.Update(nil, i)
|
|
||||||
}
|
return ci, nil
|
||||||
return ci
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
||||||
@@ -57,52 +94,3 @@ func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
|||||||
func (cs *ConnectionState) Curve() cert.Curve {
|
func (cs *ConnectionState) Curve() cert.Curve {
|
||||||
return cs.myCert.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())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+18
-120
@@ -2,33 +2,17 @@ package nebula
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"log/slog"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/header"
|
"github.com/slackhq/nebula/header"
|
||||||
"github.com/slackhq/nebula/overlay"
|
"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
|
// 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
|
// 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 {
|
type Control struct {
|
||||||
stateLock sync.Mutex
|
|
||||||
state RunState
|
|
||||||
|
|
||||||
f *Interface
|
f *Interface
|
||||||
l *slog.Logger
|
l *logrus.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
sshStart func()
|
sshStart func()
|
||||||
statsStart func()
|
statsStart func()
|
||||||
dnsStart func()
|
dnsStart func()
|
||||||
lighthouseStart func()
|
lighthouseStart func()
|
||||||
networkChangeStart func(rebind func())
|
|
||||||
connectionManagerStart func(context.Context)
|
connectionManagerStart func(context.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,31 +49,10 @@ type ControlHostInfo struct {
|
|||||||
CurrentRelaysThroughMe []netip.Addr `json:"currentRelaysThroughMe"`
|
CurrentRelaysThroughMe []netip.Addr `json:"currentRelaysThroughMe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start actually runs nebula, this is a nonblocking call.
|
// Start actually runs nebula, this is a nonblocking call. To block use Control.ShutdownBlock()
|
||||||
// Use Wait to block until nebula has fully stopped and to learn whether a fatal reader error caused the shutdown.
|
func (c *Control) Start() {
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate the interface
|
// Activate the interface
|
||||||
err := c.f.activate()
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call all the delayed funcs that waited patiently for the interface to be created.
|
// Call all the delayed funcs that waited patiently for the interface to be created.
|
||||||
if c.sshStart != nil {
|
if c.sshStart != nil {
|
||||||
@@ -105,9 +64,6 @@ func (c *Control) Start() error {
|
|||||||
if c.dnsStart != nil {
|
if c.dnsStart != nil {
|
||||||
go c.dnsStart()
|
go c.dnsStart()
|
||||||
}
|
}
|
||||||
if c.networkChangeStart != nil {
|
|
||||||
go c.networkChangeStart(c.RebindUDPServer)
|
|
||||||
}
|
|
||||||
if c.connectionManagerStart != nil {
|
if c.connectionManagerStart != nil {
|
||||||
go c.connectionManagerStart(c.ctx)
|
go c.connectionManagerStart(c.ctx)
|
||||||
}
|
}
|
||||||
@@ -115,70 +71,25 @@ func (c *Control) Start() error {
|
|||||||
c.lighthouseStart()
|
c.lighthouseStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.f.triggerShutdown = c.Stop
|
|
||||||
|
|
||||||
// Start reading packets.
|
// Start reading packets.
|
||||||
c.f.run()
|
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 {
|
func (c *Control) Context() context.Context {
|
||||||
return c.ctx
|
return c.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop tears nebula down, closing all tunnels and releasing everything it holds.
|
// Stop signals nebula to shutdown and close all tunnels, returns after the shutdown is complete
|
||||||
// Use Wait to block until the shutdown has completed.
|
|
||||||
// A Control that has been stopped cannot be started again, Start will return ErrAlreadyStopped.
|
|
||||||
func (c *Control) Stop() {
|
func (c *Control) Stop() {
|
||||||
c.stateLock.Lock()
|
// Stop the handshakeManager (and other services), to prevent new tunnels from
|
||||||
switch c.state {
|
// being created while we're shutting them all down.
|
||||||
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
|
|
||||||
c.cancel()
|
c.cancel()
|
||||||
|
|
||||||
c.CloseAllTunnels(false)
|
c.CloseAllTunnels(false)
|
||||||
|
|
||||||
c.stateLock.Lock()
|
|
||||||
c.state = StateStopped
|
|
||||||
if err := c.f.Close(); err != nil {
|
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()
|
c.l.Info("Goodbye")
|
||||||
}
|
|
||||||
|
|
||||||
// Wait blocks until nebula has fully stopped, either via Stop or an internal fatal error,
|
|
||||||
// and returns the first fatal packet reader error if there was one.
|
|
||||||
// It is safe to call from multiple goroutines and at any point in the lifecycle,
|
|
||||||
// but a Wait on a Control that is never started and never stopped will block forever.
|
|
||||||
func (c *Control) Wait() error {
|
|
||||||
return c.f.wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
||||||
@@ -189,30 +100,19 @@ func (c *Control) ShutdownBlock() {
|
|||||||
|
|
||||||
rawSig := <-sigChan
|
rawSig := <-sigChan
|
||||||
sig := rawSig.String()
|
sig := rawSig.String()
|
||||||
c.l.Info("Caught signal, shutting down", "signal", sig)
|
c.l.WithField("signal", sig).Info("Caught signal, shutting down")
|
||||||
c.Stop()
|
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() {
|
func (c *Control) RebindUDPServer() {
|
||||||
c.stateLock.Lock()
|
_ = c.f.outside.Rebind()
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger a lighthouse update, useful for mobile clients that should have an update interval of 0
|
// Trigger a lighthouse update, useful for mobile clients that should have an update interval of 0
|
||||||
c.f.lightHouse.SendUpdate()
|
c.f.lightHouse.SendUpdate()
|
||||||
|
|
||||||
// Let the main interface know that we rebound so that underlying tunnels know to trigger punches from their remotes
|
// Let the main interface know that we rebound so that underlying tunnels know to trigger punches from their remotes
|
||||||
c.f.rebindEpoch.Add(1)
|
c.f.rebindCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListHostmapHosts returns details about the actual or pending (handshaking) hostmap by vpn ip
|
// ListHostmapHosts returns details about the actual or pending (handshaking) hostmap by vpn ip
|
||||||
@@ -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.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
|
||||||
c.f.closeTunnel(h)
|
c.f.closeTunnel(h)
|
||||||
|
|
||||||
c.l.Debug("Sending close tunnel message",
|
c.l.WithField("vpnAddrs", h.vpnAddrs).WithField("udpAddr", h.remote).
|
||||||
"vpnAddrs", h.vpnAddrs,
|
Debug("Sending close tunnel message")
|
||||||
"udpAddr", h.GetRemote(),
|
|
||||||
)
|
|
||||||
closed++
|
closed++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +282,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
|||||||
RemoteAddrs: h.remotes.CopyAddrs(preferredRanges),
|
RemoteAddrs: h.remotes.CopyAddrs(preferredRanges),
|
||||||
CurrentRelaysToMe: h.relayState.CopyRelayIps(),
|
CurrentRelaysToMe: h.relayState.CopyRelayIps(),
|
||||||
CurrentRelaysThroughMe: h.relayState.CopyRelayForIps(),
|
CurrentRelaysThroughMe: h.relayState.CopyRelayForIps(),
|
||||||
CurrentRemote: h.GetRemote(),
|
CurrentRemote: h.remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, a := range h.vpnAddrs {
|
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
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/test"
|
"github.com/slackhq/nebula/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
||||||
@@ -45,7 +43,8 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
|||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
crt := &dummyCert{}
|
crt := &dummyCert{}
|
||||||
hi := &HostInfo{
|
hm.unlockedAddHostInfo(&HostInfo{
|
||||||
|
remote: remote1,
|
||||||
remotes: remotes,
|
remotes: remotes,
|
||||||
ConnectionState: &ConnectionState{
|
ConnectionState: &ConnectionState{
|
||||||
peerCert: &cert.CachedCertificate{Certificate: crt},
|
peerCert: &cert.CachedCertificate{Certificate: crt},
|
||||||
@@ -58,14 +57,13 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
|||||||
relayForByAddr: map[netip.Addr]*Relay{},
|
relayForByAddr: map[netip.Addr]*Relay{},
|
||||||
relayForByIdx: map[uint32]*Relay{},
|
relayForByIdx: map[uint32]*Relay{},
|
||||||
},
|
},
|
||||||
}
|
}, &Interface{})
|
||||||
hi.remote.Store(&remote1)
|
|
||||||
hm.unlockedAddHostInfo(hi, &Interface{})
|
|
||||||
|
|
||||||
vpnIp2, ok := netip.AddrFromSlice(ipNet2.IP)
|
vpnIp2, ok := netip.AddrFromSlice(ipNet2.IP)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
hi2 := &HostInfo{
|
hm.unlockedAddHostInfo(&HostInfo{
|
||||||
|
remote: remote1,
|
||||||
remotes: remotes,
|
remotes: remotes,
|
||||||
ConnectionState: &ConnectionState{
|
ConnectionState: &ConnectionState{
|
||||||
peerCert: nil,
|
peerCert: nil,
|
||||||
@@ -78,16 +76,13 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
|||||||
relayForByAddr: map[netip.Addr]*Relay{},
|
relayForByAddr: map[netip.Addr]*Relay{},
|
||||||
relayForByIdx: map[uint32]*Relay{},
|
relayForByIdx: map[uint32]*Relay{},
|
||||||
},
|
},
|
||||||
}
|
}, &Interface{})
|
||||||
hi2.remote.Store(&remote1)
|
|
||||||
hm.unlockedAddHostInfo(hi2, &Interface{})
|
|
||||||
|
|
||||||
c := Control{
|
c := Control{
|
||||||
state: StateReady,
|
|
||||||
f: &Interface{
|
f: &Interface{
|
||||||
hostMap: hm,
|
hostMap: hm,
|
||||||
},
|
},
|
||||||
l: test.NewLogger(),
|
l: logrus.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
thi := c.GetHostInfoByVpnAddr(vpnIp, false)
|
thi := c.GetHostInfoByVpnAddr(vpnIp, false)
|
||||||
@@ -124,153 +119,3 @@ func assertFields(t *testing.T, expected []string, actualStruct any) {
|
|||||||
|
|
||||||
assert.Equal(t, expected, fields)
|
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
|
//go:build e2e_testing
|
||||||
|
// +build e2e_testing
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/slackhq/nebula/header"
|
"github.com/slackhq/nebula/header"
|
||||||
"github.com/slackhq/nebula/overlay"
|
"github.com/slackhq/nebula/overlay"
|
||||||
"github.com/slackhq/nebula/udp"
|
"github.com/slackhq/nebula/udp"
|
||||||
@@ -20,9 +23,7 @@ func (c *Control) WaitForType(msgType header.MessageType, subType header.Message
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
pipeTo.InjectUDPPacket(p)
|
pipeTo.InjectUDPPacket(p)
|
||||||
match := h.Type == msgType && h.Subtype == subType
|
if h.Type == msgType && h.Subtype == subType {
|
||||||
p.Release()
|
|
||||||
if match {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,9 +39,7 @@ func (c *Control) WaitForTypeByIndex(toIndex uint32, msgType header.MessageType,
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
pipeTo.InjectUDPPacket(p)
|
pipeTo.InjectUDPPacket(p)
|
||||||
match := h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType
|
if h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType {
|
||||||
p.Release()
|
|
||||||
if match {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,15 +91,65 @@ func (c *Control) GetTunTxChan() <-chan []byte {
|
|||||||
return c.f.inside.(*overlay.TestTun).TxPackets
|
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.
|
// InjectUDPPacket will inject a packet into the udp side of nebula
|
||||||
// The copy comes from the freelist so steady-state alloc is zero.
|
|
||||||
func (c *Control) InjectUDPPacket(p *udp.Packet) {
|
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.
|
// InjectTunUDPPacket puts a udp packet on the tun interface. Using UDP here because it's a simpler protocol
|
||||||
func (c *Control) InjectTunPacket(packet []byte) {
|
func (c *Control) InjectTunUDPPacket(toAddr netip.Addr, toPort uint16, fromAddr netip.Addr, fromPort uint16, data []byte) {
|
||||||
c.f.inside.(*overlay.TestTun).Send(packet)
|
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 {
|
func (c *Control) GetVpnAddrs() []netip.Addr {
|
||||||
@@ -108,19 +157,7 @@ func (c *Control) GetVpnAddrs() []netip.Addr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) GetUDPAddr() netip.AddrPort {
|
func (c *Control) GetUDPAddr() netip.AddrPort {
|
||||||
return c.f.outside.(*udp.TesterConn).GetAddr()
|
return c.f.outside.(*udp.TesterConn).Addr
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
||||||
@@ -137,18 +174,6 @@ func (c *Control) GetHostmap() *HostMap {
|
|||||||
return c.f.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 {
|
func (c *Control) GetCertState() *CertState {
|
||||||
return c.f.pki.getCertState()
|
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))
|
tree:add(pf_version, tvbuf:range(0,1))
|
||||||
local type = tree:add(pf_type, 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
|
if nebula_type == 0 then
|
||||||
local stage = tvbuf(8,8):uint64()
|
local stage = tvbuf(8,8):uint64()
|
||||||
tree:add(pf_subtype_handshake, tvbuf:range(1,1))
|
tree:add(pf_subtype_handshake, tvbuf:range(1,1))
|
||||||
@@ -84,24 +84,30 @@ end
|
|||||||
|
|
||||||
function nebula.prefs_changed()
|
function nebula.prefs_changed()
|
||||||
if default_settings.all_ports == nebula.prefs.all_ports and default_settings.port == nebula.prefs.port then
|
if default_settings.all_ports == nebula.prefs.all_ports and default_settings.port == nebula.prefs.port then
|
||||||
|
-- Nothing changed, bail
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Remove all existing registrations
|
-- Remove our old dissector
|
||||||
DissectorTable.get("udp.port"):remove_all(nebula)
|
DissectorTable.get("udp.port"):remove_all(nebula)
|
||||||
|
|
||||||
if nebula.prefs.all_ports then
|
if nebula.prefs.all_ports and default_settings.all_ports ~= nebula.prefs.all_ports then
|
||||||
-- Register on every port for hole punch capture
|
default_settings.all_port = nebula.prefs.all_ports
|
||||||
|
|
||||||
for i=0, 65535 do
|
for i=0, 65535 do
|
||||||
DissectorTable.get("udp.port"):add(i, nebula)
|
DissectorTable.get("udp.port"):add(i, nebula)
|
||||||
end
|
end
|
||||||
else
|
|
||||||
-- Register on the configured port only
|
-- no need to establish again on specific ports
|
||||||
DissectorTable.get("udp.port"):add(nebula.prefs.port, nebula)
|
return
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
DissectorTable.get("udp.port"):add(default_settings.port, nebula)
|
DissectorTable.get("udp.port"):add(default_settings.port, nebula)
|
||||||
|
|||||||
+74
-322
@@ -1,277 +1,68 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
|
"github.com/gaissmai/bart"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/config"
|
"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
|
sync.RWMutex
|
||||||
l *slog.Logger
|
l *logrus.Logger
|
||||||
ctx context.Context
|
dnsMap4 map[string]netip.Addr
|
||||||
dnsMap4 map[string]netip.Addr
|
dnsMap6 map[string]netip.Addr
|
||||||
dnsMap6 map[string]netip.Addr
|
hostMap *HostMap
|
||||||
hostMap *HostMap
|
myVpnAddrsTable *bart.Lite
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDnsServerFromConfig builds a dnsServer, applies the initial config, and
|
func newDnsRecords(l *logrus.Logger, cs *CertState, hostMap *HostMap) *dnsRecords {
|
||||||
// registers a reload callback. The reload callback is registered before the
|
return &dnsRecords{
|
||||||
// initial config is applied, so a SIGHUP can later enable, fix, or disable
|
l: l,
|
||||||
// DNS even if the initial application failed.
|
dnsMap4: make(map[string]netip.Addr),
|
||||||
//
|
dnsMap6: make(map[string]netip.Addr),
|
||||||
// The dnsServer internally gates on `lighthouse.serve_dns &&
|
hostMap: hostMap,
|
||||||
// lighthouse.am_lighthouse`. Start and Add are safe to call unconditionally,
|
myVpnAddrsTable: cs.myVpnAddrsTable,
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start binds and serves the DNS responder. Blocks until Stop is called or
|
func (d *dnsRecords) Query(q uint16, data string) netip.Addr {
|
||||||
// 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) {
|
|
||||||
data = strings.ToLower(data)
|
data = strings.ToLower(data)
|
||||||
d.RLock()
|
d.RLock()
|
||||||
defer d.RUnlock()
|
defer d.RUnlock()
|
||||||
addr4, haveV4 := d.dnsMap4[data]
|
|
||||||
addr6, haveV6 := d.dnsMap6[data]
|
|
||||||
nameExists := haveV4 || haveV6
|
|
||||||
switch q {
|
switch q {
|
||||||
case dns.TypeA:
|
case dns.TypeA:
|
||||||
if haveV4 {
|
if r, ok := d.dnsMap4[data]; ok {
|
||||||
return addr4, nameExists
|
return r
|
||||||
}
|
}
|
||||||
case dns.TypeAAAA:
|
case dns.TypeAAAA:
|
||||||
if haveV6 {
|
if r, ok := d.dnsMap6[data]; ok {
|
||||||
return addr6, nameExists
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return netip.Addr{}, nameExists
|
return netip.Addr{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dnsServer) QueryCert(data string) string {
|
func (d *dnsRecords) QueryCert(data string) string {
|
||||||
if len(data) < 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
ip, err := netip.ParseAddr(data[:len(data)-1])
|
ip, err := netip.ParseAddr(data[:len(data)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
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)
|
hostinfo := d.hostMap.QueryVpnAddr(ip)
|
||||||
if hostinfo == nil {
|
if hostinfo == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -289,67 +80,8 @@ func (d *dnsServer) QueryCert(data string) string {
|
|||||||
return string(b)
|
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`
|
// 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) {
|
func (d *dnsRecords) Add(host string, addresses []netip.Addr) {
|
||||||
if !d.enabled.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
host = strings.ToLower(host)
|
host = strings.ToLower(host)
|
||||||
d.Lock()
|
d.Lock()
|
||||||
defer d.Unlock()
|
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)
|
a, _, _ := net.SplitHostPort(addr)
|
||||||
b, err := netip.ParseAddr(a)
|
b, err := netip.ParseAddr(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -380,32 +112,17 @@ func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
cs := d.certState()
|
|
||||||
if cs == nil || cs.myVpnAddrsTable == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
//if we found it in this table, it's good
|
//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) {
|
func (d *dnsRecords) 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
|
|
||||||
for _, q := range m.Question {
|
for _, q := range m.Question {
|
||||||
switch q.Qtype {
|
switch q.Qtype {
|
||||||
case dns.TypeA, dns.TypeAAAA:
|
case dns.TypeA, dns.TypeAAAA:
|
||||||
qType := dns.TypeToString[q.Qtype]
|
qType := dns.TypeToString[q.Qtype]
|
||||||
if debugEnabled {
|
d.l.Debugf("Query for %s %s", qType, q.Name)
|
||||||
d.l.Debug("DNS query", "type", qType, "name", q.Name)
|
ip := d.Query(q.Qtype, q.Name)
|
||||||
}
|
|
||||||
ip, nameExists := d.Query(q.Qtype, q.Name)
|
|
||||||
if nameExists {
|
|
||||||
anyNameExists = true
|
|
||||||
}
|
|
||||||
if ip.IsValid() {
|
if ip.IsValid() {
|
||||||
rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
|
rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -417,9 +134,7 @@ func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
|||||||
if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) {
|
if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if debugEnabled {
|
d.l.Debugf("Query for TXT %s", q.Name)
|
||||||
d.l.Debug("DNS query", "type", "TXT", "name", q.Name)
|
|
||||||
}
|
|
||||||
ip := d.QueryCert(q.Name)
|
ip := d.QueryCert(q.Name)
|
||||||
if ip != "" {
|
if ip != "" {
|
||||||
rr, err := dns.NewRR(fmt.Sprintf("%s TXT %s", q.Name, 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
|
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 := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
m.Compress = false
|
m.Compress = false
|
||||||
@@ -448,6 +163,21 @@ func (d *dnsServer) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
w.WriteMsg(m)
|
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 {
|
func getDnsServerAddr(c *config.C) string {
|
||||||
dnsHost := strings.TrimSpace(c.GetString("lighthouse.dns.host", ""))
|
dnsHost := strings.TrimSpace(c.GetString("lighthouse.dns.host", ""))
|
||||||
// Old guidance was to provide the literal `[::]` in `lighthouse.dns.host` but that won't resolve.
|
// 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)))
|
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
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gaissmai/bart"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/cert_test"
|
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestParsequery(t *testing.T) {
|
||||||
l := slog.New(slog.DiscardHandler)
|
l := logrus.New()
|
||||||
hostMap := &HostMap{}
|
hostMap := &HostMap{}
|
||||||
ds := &dnsServer{
|
ds := newDnsRecords(l, &CertState{}, hostMap)
|
||||||
l: l,
|
|
||||||
dnsMap4: make(map[string]netip.Addr),
|
|
||||||
dnsMap6: make(map[string]netip.Addr),
|
|
||||||
hostMap: hostMap,
|
|
||||||
}
|
|
||||||
ds.enabled.Store(true)
|
|
||||||
addrs := []netip.Addr{
|
addrs := []netip.Addr{
|
||||||
netip.MustParseAddr("1.2.3.4"),
|
netip.MustParseAddr("1.2.3.4"),
|
||||||
netip.MustParseAddr("1.2.3.5"),
|
netip.MustParseAddr("1.2.3.5"),
|
||||||
@@ -48,56 +21,18 @@ func TestParsequery(t *testing.T) {
|
|||||||
netip.MustParseAddr("fd01::25"),
|
netip.MustParseAddr("fd01::25"),
|
||||||
}
|
}
|
||||||
ds.Add("test.com.com", addrs)
|
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 := &dns.Msg{}
|
||||||
m.SetQuestion("test.com.com", dns.TypeA)
|
m.SetQuestion("test.com.com", dns.TypeA)
|
||||||
ds.parseQuery(m, nil)
|
ds.parseQuery(m, nil)
|
||||||
assert.NotNil(t, m.Answer)
|
assert.NotNil(t, m.Answer)
|
||||||
assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String())
|
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 = &dns.Msg{}
|
||||||
m.SetQuestion("test.com.com", dns.TypeAAAA)
|
m.SetQuestion("test.com.com", dns.TypeAAAA)
|
||||||
ds.parseQuery(m, nil)
|
ds.parseQuery(m, nil)
|
||||||
assert.NotNil(t, m.Answer)
|
assert.NotNil(t, m.Answer)
|
||||||
assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String())
|
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) {
|
func Test_getDnsServerAddr(t *testing.T) {
|
||||||
@@ -136,496 +71,3 @@ func Test_getDnsServerAddr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, "[::]:1", getDnsServerAddr(c))
|
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
|
FROM gcr.io/distroless/static:latest
|
||||||
|
|
||||||
ARG TARGETOS TARGETARCH
|
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 /nebula
|
||||||
COPY build/$TARGETOS-$TARGETARCH/nebula-cert /nebula-cert
|
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"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula"
|
"github.com/slackhq/nebula"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/cert_test"
|
"github.com/slackhq/nebula/cert_test"
|
||||||
"github.com/slackhq/nebula/e2e/router"
|
"github.com/slackhq/nebula/e2e/router"
|
||||||
"github.com/slackhq/nebula/header"
|
"github.com/slackhq/nebula/header"
|
||||||
"github.com/slackhq/nebula/overlay"
|
|
||||||
"github.com/slackhq/nebula/udp"
|
"github.com/slackhq/nebula/udp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.yaml.in/yaml/v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkHotPath(b *testing.B) {
|
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{})
|
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)
|
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them", "10.128.0.2/24", nil)
|
||||||
|
|
||||||
// Put their info in our lighthouse
|
// Put their info in our lighthouse
|
||||||
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||||
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
|
||||||
|
|
||||||
// Start the servers
|
// Start the servers
|
||||||
myControl.Start()
|
myControl.Start()
|
||||||
@@ -39,68 +38,16 @@ func BenchmarkHotPath(b *testing.B) {
|
|||||||
r := router.NewR(b, myControl, theirControl)
|
r := router.NewR(b, myControl, theirControl)
|
||||||
r.CancelFlowLogs()
|
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++ {
|
for n := 0; n < b.N; n++ {
|
||||||
myControl.InjectTunPacket(prebuilt)
|
myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
|
||||||
// Release the TUN-side bytes back to the harness freelist; the bench
|
_ = r.RouteForAllUntilTxTun(theirControl)
|
||||||
// just confirms a packet arrived, the contents aren't inspected.
|
|
||||||
overlay.ReleaseTunBuf(r.RouteForAllUntilTxTun(theirControl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
myControl.Stop()
|
myControl.Stop()
|
||||||
theirControl.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) {
|
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{})
|
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, 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)
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side")
|
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")
|
t.Log("Have them consume my stage 0 packet. They have a tunnel now")
|
||||||
theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
|
theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
|
||||||
@@ -150,44 +97,7 @@ func TestGoodHandshake(t *testing.T) {
|
|||||||
theirControl.Stop()
|
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) {
|
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{})
|
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)
|
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()
|
evilControl.Start()
|
||||||
|
|
||||||
t.Log("Start the handshake process, we will route until we see the evil tunnel closed")
|
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{}
|
h := &header.H{}
|
||||||
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
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) {
|
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{})
|
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)
|
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()
|
evilControl.Start()
|
||||||
|
|
||||||
t.Log("Start the handshake process, we will route until we see the evil tunnel closed")
|
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{}
|
h := &header.H{}
|
||||||
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
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) {
|
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
|
// 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
|
// But will eventually collapse down to a single tunnel
|
||||||
|
|
||||||
@@ -368,8 +276,8 @@ func TestStage1Race(t *testing.T) {
|
|||||||
theirControl.Start()
|
theirControl.Start()
|
||||||
|
|
||||||
t.Log("Trigger a handshake to start on both me and them")
|
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")))
|
myControl.InjectTunUDPPacket(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")))
|
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||||
|
|
||||||
t.Log("Get both stage 1 handshake packets")
|
t.Log("Get both stage 1 handshake packets")
|
||||||
myHsForThem := myControl.GetFromUDP(true)
|
myHsForThem := myControl.GetFromUDP(true)
|
||||||
@@ -405,7 +313,7 @@ func TestStage1Race(t *testing.T) {
|
|||||||
|
|
||||||
r.Log("Spin until connection manager tears down a tunnel")
|
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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
t.Log("Connection manager hasn't ticked yet")
|
t.Log("Connection manager hasn't ticked yet")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -428,7 +336,6 @@ func TestStage1Race(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUncleanShutdownRaceLoser(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{})
|
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, 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)
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
r.Log("Trigger a handshake from me to them")
|
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)
|
p := r.RouteForAllUntilTxTun(theirControl)
|
||||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
||||||
|
|
||||||
r.Log("Nuke my hostmap")
|
r.Log("Nuke my hostmap")
|
||||||
myHostmap := myControl.GetHostmap()
|
myHostmap := myControl.GetHostmap()
|
||||||
myHostmap.Lock()
|
|
||||||
myHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
myHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
||||||
myHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
myHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
||||||
myHostmap.RemoteIndexes = 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)
|
p = r.RouteForAllUntilTxTun(theirControl)
|
||||||
assertUdpPacket(t, []byte("Hi from me again"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
|
|
||||||
r.Log("Wait for the dead index to go away")
|
r.Log("Wait for the dead index to go away")
|
||||||
start := theirControl.GetHostmapIndexCount()
|
start := len(theirControl.GetHostmap().Indexes)
|
||||||
for {
|
for {
|
||||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
if theirControl.GetHostmapIndexCount() < start {
|
if len(theirControl.GetHostmap().Indexes) < start {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -480,7 +385,6 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUncleanShutdownRaceWinner(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{})
|
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, 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)
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
r.Log("Trigger a handshake from me to them")
|
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)
|
p := r.RouteForAllUntilTxTun(theirControl)
|
||||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
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")
|
r.Log("Nuke my hostmap")
|
||||||
theirHostmap := theirControl.GetHostmap()
|
theirHostmap := theirControl.GetHostmap()
|
||||||
theirHostmap.Lock()
|
|
||||||
theirHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
theirHostmap.Hosts = map[netip.Addr]*nebula.HostInfo{}
|
||||||
theirHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
theirHostmap.Indexes = map[uint32]*nebula.HostInfo{}
|
||||||
theirHostmap.RemoteIndexes = 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)
|
p = r.RouteForAllUntilTxTun(myControl)
|
||||||
assertUdpPacket(t, []byte("Hi from them again"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
|
assertUdpPacket(t, []byte("Hi from them again"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
|
||||||
r.RenderHostmaps("Derp hostmaps", myControl, theirControl)
|
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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
|
|
||||||
r.Log("Wait for the dead index to go away")
|
r.Log("Wait for the dead index to go away")
|
||||||
start := myControl.GetHostmapIndexCount()
|
start := len(myControl.GetHostmap().Indexes)
|
||||||
for {
|
for {
|
||||||
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
if myControl.GetHostmapIndexCount() < start {
|
if len(myControl.GetHostmap().Indexes) < start {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -534,7 +436,6 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRelays(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{})
|
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}})
|
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}})
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
t.Log("Trigger a handshake from me to them via the relay")
|
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)
|
|
||||||
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")))
|
|
||||||
|
|
||||||
p := r.RouteForAllUntilTxTun(theirControl)
|
p := r.RouteForAllUntilTxTun(theirControl)
|
||||||
r.Log("Assert the tunnel works")
|
r.Log("Assert the tunnel works")
|
||||||
@@ -594,7 +465,6 @@ func TestRelaysDontCareAboutIps(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReestablishRelays(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{})
|
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}})
|
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}})
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
t.Log("Trigger a handshake from me to them via the relay")
|
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)
|
p := r.RouteForAllUntilTxTun(theirControl)
|
||||||
r.Log("Assert the tunnel works")
|
r.Log("Assert the tunnel works")
|
||||||
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
|
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")
|
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)
|
p = r.RouteForAllUntilTxTun(myControl)
|
||||||
r.Log("Assert the tunnel works")
|
r.Log("Assert the tunnel works")
|
||||||
@@ -632,12 +502,12 @@ func TestReestablishRelays(t *testing.T) {
|
|||||||
r.Log("Close the tunnel")
|
r.Log("Close the tunnel")
|
||||||
relayControl.CloseTunnel(theirVpnIpNet[0].Addr(), true)
|
relayControl.CloseTunnel(theirVpnIpNet[0].Addr(), true)
|
||||||
|
|
||||||
start := myControl.GetHostmapIndexCount()
|
start := len(myControl.GetHostmap().Indexes)
|
||||||
curIndexes := myControl.GetHostmapIndexCount()
|
curIndexes := len(myControl.GetHostmap().Indexes)
|
||||||
for curIndexes >= start {
|
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)
|
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 {
|
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||||
return router.RouteAndExit
|
return router.RouteAndExit
|
||||||
@@ -654,7 +524,7 @@ func TestReestablishRelays(t *testing.T) {
|
|||||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||||
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||||
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
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)
|
p = r.RouteForAllUntilTxTun(theirControl)
|
||||||
r.Log("Assert the tunnel works")
|
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")
|
t.Log("Assert the tunnel works the other way, too")
|
||||||
for {
|
for {
|
||||||
t.Log("RouteForAllUntilTxTun")
|
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)
|
p = r.RouteForAllUntilTxTun(myControl)
|
||||||
r.Log("Assert the tunnel works")
|
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) {
|
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
|
//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{})
|
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}})
|
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)
|
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")
|
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")))
|
myControl.InjectTunUDPPacket(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")))
|
theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
|
||||||
|
|
||||||
r.Log("Wait for a packet from them to me")
|
r.Log("Wait for a packet from them to me")
|
||||||
p := r.RouteForAllUntilTxTun(myControl)
|
p := r.RouteForAllUntilTxTun(myControl)
|
||||||
@@ -838,12 +643,12 @@ func TestStage1RaceRelays(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStage1RaceRelays2(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
|
//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{})
|
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}})
|
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}})
|
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}})
|
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
|
// Teach my how to get to the relay and that their can be reached via the relay
|
||||||
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||||
@@ -865,41 +670,49 @@ func TestStage1RaceRelays2(t *testing.T) {
|
|||||||
theirControl.Start()
|
theirControl.Start()
|
||||||
|
|
||||||
r.Log("Get a tunnel between me and relay")
|
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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), myControl, relayControl, r)
|
||||||
|
|
||||||
r.Log("Get a tunnel between them and relay")
|
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)
|
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")
|
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")))
|
l.Info("Trigger a handshake from both them and me via relay to them and 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.RouteUntilAfterMsgType(myControl, header.Control, header.MessageNone)
|
//r.RouteUntilAfterMsgType(myControl, header.Control, header.MessageNone)
|
||||||
//r.RouteUntilAfterMsgType(theirControl, 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.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.RouteForAllUntilTxTun(theirControl)
|
||||||
|
|
||||||
r.Log("Assert the tunnel works")
|
r.Log("Assert the tunnel works")
|
||||||
|
l.Info("Assert the tunnel works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
|
|
||||||
t.Log("Wait until we remove extra tunnels")
|
t.Log("Wait until we remove extra tunnels")
|
||||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
l.Info("Wait until we remove extra tunnels")
|
||||||
myControl.GetHostmapIndexCount(),
|
l.WithFields(
|
||||||
theirControl.GetHostmapIndexCount(),
|
logrus.Fields{
|
||||||
relayControl.GetHostmapIndexCount(),
|
"myControl": len(myControl.GetHostmap().Indexes),
|
||||||
)
|
"theirControl": len(theirControl.GetHostmap().Indexes),
|
||||||
hostInfos := myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
"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
|
retries := 60
|
||||||
for hostInfos > 6 && retries > 0 {
|
for hostInfos > 6 && retries > 0 {
|
||||||
hostInfos = myControl.GetHostmapIndexCount() + theirControl.GetHostmapIndexCount() + relayControl.GetHostmapIndexCount()
|
hostInfos = len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
|
||||||
t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
|
l.WithFields(
|
||||||
myControl.GetHostmapIndexCount(),
|
logrus.Fields{
|
||||||
theirControl.GetHostmapIndexCount(),
|
"myControl": len(myControl.GetHostmap().Indexes),
|
||||||
relayControl.GetHostmapIndexCount(),
|
"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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
t.Log("Connection manager hasn't ticked yet")
|
t.Log("Connection manager hasn't ticked yet")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -907,6 +720,7 @@ func TestStage1RaceRelays2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.Log("Assert the tunnel works")
|
r.Log("Assert the tunnel works")
|
||||||
|
l.Info("Assert the tunnel works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
|
|
||||||
myControl.Stop()
|
myControl.Stop()
|
||||||
@@ -915,7 +729,6 @@ func TestStage1RaceRelays2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRehandshakingRelays(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{})
|
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}})
|
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}})
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
t.Log("Trigger a handshake from me to them via the relay")
|
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)
|
p := r.RouteForAllUntilTxTun(theirControl)
|
||||||
r.Log("Assert the tunnel works")
|
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)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
||||||
// We should have two hostinfos on all sides
|
// We should have two hostinfos on all sides
|
||||||
for myControl.GetHostmapIndexCount() != 2 {
|
for len(myControl.GetHostmap().Indexes) != 2 {
|
||||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
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")
|
r.Log("Assert the relay tunnel still works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.Log("yupitdoes")
|
r.Log("yupitdoes")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
t.Logf("myControl hostinfos got cleaned up!")
|
t.Logf("myControl hostinfos got cleaned up!")
|
||||||
for theirControl.GetHostmapIndexCount() != 2 {
|
for len(theirControl.GetHostmap().Indexes) != 2 {
|
||||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
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")
|
r.Log("Assert the relay tunnel still works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.Log("yupitdoes")
|
r.Log("yupitdoes")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
t.Logf("theirControl hostinfos got cleaned up!")
|
t.Logf("theirControl hostinfos got cleaned up!")
|
||||||
for relayControl.GetHostmapIndexCount() != 2 {
|
for len(relayControl.GetHostmap().Indexes) != 2 {
|
||||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
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")
|
r.Log("Assert the relay tunnel still works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.Log("yupitdoes")
|
r.Log("yupitdoes")
|
||||||
@@ -1019,7 +832,6 @@ func TestRehandshakingRelays(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRehandshakingRelaysPrimary(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
|
// 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{})
|
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}})
|
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()
|
theirControl.Start()
|
||||||
|
|
||||||
t.Log("Trigger a handshake from me to them via the relay")
|
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)
|
p := r.RouteForAllUntilTxTun(theirControl)
|
||||||
r.Log("Assert the tunnel works")
|
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)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
r.RenderHostmaps("working hostmaps", myControl, relayControl, theirControl)
|
||||||
// We should have two hostinfos on all sides
|
// We should have two hostinfos on all sides
|
||||||
for myControl.GetHostmapIndexCount() != 2 {
|
for len(myControl.GetHostmap().Indexes) != 2 {
|
||||||
t.Logf("Waiting for myControl hostinfos (%v != 2) to get cleaned up from lack of use...", myControl.GetHostmapIndexCount())
|
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")
|
r.Log("Assert the relay tunnel still works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.Log("yupitdoes")
|
r.Log("yupitdoes")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
t.Logf("myControl hostinfos got cleaned up!")
|
t.Logf("myControl hostinfos got cleaned up!")
|
||||||
for theirControl.GetHostmapIndexCount() != 2 {
|
for len(theirControl.GetHostmap().Indexes) != 2 {
|
||||||
t.Logf("Waiting for theirControl hostinfos (%v != 2) to get cleaned up from lack of use...", theirControl.GetHostmapIndexCount())
|
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")
|
r.Log("Assert the relay tunnel still works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.Log("yupitdoes")
|
r.Log("yupitdoes")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
t.Logf("theirControl hostinfos got cleaned up!")
|
t.Logf("theirControl hostinfos got cleaned up!")
|
||||||
for relayControl.GetHostmapIndexCount() != 2 {
|
for len(relayControl.GetHostmap().Indexes) != 2 {
|
||||||
t.Logf("Waiting for relayControl hostinfos (%v != 2) to get cleaned up from lack of use...", relayControl.GetHostmapIndexCount())
|
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")
|
r.Log("Assert the relay tunnel still works")
|
||||||
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
|
||||||
r.Log("yupitdoes")
|
r.Log("yupitdoes")
|
||||||
@@ -1124,7 +936,6 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRehandshaking(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{})
|
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)
|
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)
|
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))
|
theirConfig.ReloadConfigString(string(rc))
|
||||||
|
|
||||||
r.Log("Spin until there is only 1 tunnel")
|
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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
t.Log("Connection manager hasn't ticked yet")
|
t.Log("Connection manager hasn't ticked yet")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -1220,7 +1031,6 @@ func TestRehandshaking(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRehandshakingLoser(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
|
// 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
|
// 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{})
|
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))
|
myConfig.ReloadConfigString(string(rc))
|
||||||
|
|
||||||
r.Log("Spin until there is only 1 tunnel")
|
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)
|
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
|
||||||
t.Log("Connection manager hasn't ticked yet")
|
t.Log("Connection manager hasn't ticked yet")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -1319,7 +1129,6 @@ func TestRehandshakingLoser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRaceRegression(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
|
// 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
|
// 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
|
// caused a cross-linked hostinfo
|
||||||
@@ -1343,8 +1152,8 @@ func TestRaceRegression(t *testing.T) {
|
|||||||
//them rx stage:2 initiatorIndex=120607833 responderIndex=4209862089
|
//them rx stage:2 initiatorIndex=120607833 responderIndex=4209862089
|
||||||
|
|
||||||
t.Log("Start both handshakes")
|
t.Log("Start both handshakes")
|
||||||
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"))
|
||||||
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"))
|
||||||
|
|
||||||
t.Log("Get both stage 1")
|
t.Log("Get both stage 1")
|
||||||
myStage1ForThem := myControl.GetFromUDP(true)
|
myStage1ForThem := myControl.GetFromUDP(true)
|
||||||
@@ -1380,7 +1189,6 @@ func TestRaceRegression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestV2NonPrimaryWithLighthouse(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{})
|
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}})
|
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()
|
myControl.Stop()
|
||||||
theirControl.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()
|
|
||||||
}
|
|
||||||
|
|||||||
+36
-141
@@ -4,6 +4,7 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,27 +12,25 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula"
|
"github.com/slackhq/nebula"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/cert_test"
|
"github.com/slackhq/nebula/cert_test"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/e2e/router"
|
"github.com/slackhq/nebula/e2e/router"
|
||||||
"github.com/slackhq/nebula/logging"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"gopkg.in/yaml.v3"
|
||||||
"go.yaml.in/yaml/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type m = map[string]any
|
type m = map[string]any
|
||||||
|
|
||||||
// newSimpleServer creates a nebula instance with many assumptions
|
// 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) {
|
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
|
var vpnNetworks []netip.Prefix
|
||||||
for _, sn := range strings.Split(sVpnNetworks, ",") {
|
for _, sn := range strings.Split(sVpnNetworks, ",") {
|
||||||
vpnIpNet, err := netip.ParsePrefix(strings.TrimSpace(sn))
|
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
|
budpIp[3] = 239
|
||||||
udpAddr = netip.AddrPortFrom(netip.AddrFrom16(budpIp), 4242)
|
udpAddr = netip.AddrPortFrom(netip.AddrFrom16(budpIp), 4242)
|
||||||
}
|
}
|
||||||
return newSimpleServerWithUdp(v, caCrt, caKey, name, sVpnNetworks, udpAddr, overrides)
|
_, _, myPrivKey, myPEM := cert_test.NewTestCert(v, cert.Curve_CURVE25519, caCrt, caKey, name, time.Now(), time.Now().Add(5*time.Minute), vpnNetworks, nil, []string{})
|
||||||
}
|
|
||||||
|
|
||||||
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{})
|
|
||||||
|
|
||||||
caB, err := caCrt.MarshalPEM()
|
caB, err := caCrt.MarshalPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,7 +76,11 @@ func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certific
|
|||||||
"port": "any",
|
"port": "any",
|
||||||
"host": "any",
|
"host": "any",
|
||||||
}},
|
}},
|
||||||
"inbound": firewallInbound,
|
"inbound": []m{{
|
||||||
|
"proto": "any",
|
||||||
|
"port": "any",
|
||||||
|
"host": "any",
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
//"handshakes": m{
|
//"handshakes": m{
|
||||||
// "try_interval": "1s",
|
// "try_interval": "1s",
|
||||||
@@ -134,7 +90,8 @@ func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certific
|
|||||||
"port": udpAddr.Port(),
|
"port": udpAddr.Port(),
|
||||||
},
|
},
|
||||||
"logging": m{
|
"logging": m{
|
||||||
"level": testLogLevelName(),
|
"timestamp_format": fmt.Sprintf("%v 15:04:05.000000", name),
|
||||||
|
"level": l.Level.String(),
|
||||||
},
|
},
|
||||||
"timers": m{
|
"timers": m{
|
||||||
"pending_deletion_interval": 2,
|
"pending_deletion_interval": 2,
|
||||||
@@ -235,7 +192,8 @@ func newServer(caCrt []cert.Certificate, certs []cert.Certificate, key []byte, o
|
|||||||
"port": udpAddr.Port(),
|
"port": udpAddr.Port(),
|
||||||
},
|
},
|
||||||
"logging": m{
|
"logging": m{
|
||||||
"level": testLogLevelName(),
|
"timestamp_format": fmt.Sprintf("%v 15:04:05.000000", certs[0].Name()),
|
||||||
|
"level": l.Level.String(),
|
||||||
},
|
},
|
||||||
"timers": m{
|
"timers": m{
|
||||||
"pending_deletion_interval": 2,
|
"pending_deletion_interval": 2,
|
||||||
@@ -292,26 +250,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
|
// 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)
|
bPacket := r.RouteForAllUntilTxTun(controlA)
|
||||||
assertUdpPacket(t, []byte("Hi from B"), bPacket, vpnIpB, vpnIpA, 90, 80)
|
assertUdpPacket(t, []byte("Hi from B"), bPacket, vpnIpB, vpnIpA, 90, 80)
|
||||||
|
|
||||||
// And once more from me to them
|
// 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)
|
aPacket := r.RouteForAllUntilTxTun(controlB)
|
||||||
assertUdpPacket(t, []byte("Hello from A"), aPacket, vpnIpA, vpnIpB, 90, 80)
|
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
|
// Get both host infos
|
||||||
//TODO: CERT-V2 we may want to loop over each vpnAddr and assert all the things
|
//TODO: CERT-V2 we may want to loop over each vpnAddr and assert all the things
|
||||||
hBinA := controlA.GetHostInfoByVpnAddr(vpnNetsB[0].Addr(), false)
|
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)
|
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
|
// Check that both vpn and real addr are correct
|
||||||
assert.EqualValues(t, getAddrs(vpnNetsB), hBinA.VpnAddrs, "Host B VpnIp is wrong in control A")
|
assert.EqualValues(t, getAddrs(vpnNetsB), hBinA.VpnAddrs, "Host B VpnIp is wrong in control A")
|
||||||
@@ -325,7 +283,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")
|
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() {
|
if toIp.Is6() {
|
||||||
assertUdpPacket6(t, expected, b, fromIp, toIp, fromPort, toPort)
|
assertUdpPacket6(t, expected, b, fromIp, toIp, fromPort, toPort)
|
||||||
} else {
|
} else {
|
||||||
@@ -333,7 +291,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)
|
packet := gopacket.NewPacket(b, layers.LayerTypeIPv6, gopacket.Lazy)
|
||||||
v6 := packet.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
v6 := packet.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
|
||||||
assert.NotNil(t, v6, "No ipv6 data found")
|
assert.NotNil(t, v6, "No ipv6 data found")
|
||||||
@@ -352,7 +310,7 @@ func assertUdpPacket6(t testing.TB, expected, b []byte, fromIp, toIp netip.Addr,
|
|||||||
assert.Equal(t, expected, data.Payload(), "Data was incorrect")
|
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)
|
packet := gopacket.NewPacket(b, layers.LayerTypeIPv4, gopacket.Lazy)
|
||||||
v4 := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
|
v4 := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
|
||||||
assert.NotNil(t, v4, "No ipv4 data found")
|
assert.NotNil(t, v4, "No ipv4 data found")
|
||||||
@@ -379,87 +337,24 @@ func getAddrs(ns []netip.Prefix) []netip.Addr {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestLogger() *slog.Logger {
|
func NewTestLogger() *logrus.Logger {
|
||||||
|
l := logrus.New()
|
||||||
|
|
||||||
v := os.Getenv("TEST_LOGS")
|
v := os.Getenv("TEST_LOGS")
|
||||||
if v == "" {
|
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 {
|
switch v {
|
||||||
case "2":
|
case "2":
|
||||||
level = slog.LevelDebug
|
l.SetLevel(logrus.DebugLevel)
|
||||||
case "3":
|
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}))
|
|
||||||
}
|
return l
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,19 +24,6 @@ import (
|
|||||||
"golang.org/x/exp/maps"
|
"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 {
|
type R struct {
|
||||||
// Simple map of the ip:port registered on a control to the control
|
// Simple map of the ip:port registered on a control to the control
|
||||||
// Basically a router, right?
|
// Basically a router, right?
|
||||||
@@ -48,28 +34,12 @@ type R struct {
|
|||||||
|
|
||||||
// A last used map, if an inbound packet hit the inNat map then
|
// 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
|
// 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
|
// A map of vpn ip to the nebula control it belongs to
|
||||||
vpnControls map[netip.Addr]*nebula.Control
|
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
|
ignoreFlows []ignoreFlow
|
||||||
flow []flowEntry
|
flow []flowEntry
|
||||||
|
|
||||||
@@ -114,28 +84,6 @@ type packet struct {
|
|||||||
packet *udp.Packet
|
packet *udp.Packet
|
||||||
tun bool // a packet pulled off a tun device
|
tun bool // a packet pulled off a tun device
|
||||||
rx bool // the packet was received by a udp 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() {
|
func (p *packet) WasReceived() {
|
||||||
@@ -153,9 +101,6 @@ const (
|
|||||||
ExitNow ExitType = 1
|
ExitNow ExitType = 1
|
||||||
// RouteAndExit routes this packet and exits immediately afterwards
|
// RouteAndExit routes this packet and exits immediately afterwards
|
||||||
RouteAndExit ExitType = 2
|
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
|
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 {
|
func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
// t.Name() contains a slash for subtests, so the flow log can land in a nested directory
|
if err := os.MkdirAll("mermaid", 0755); err != nil {
|
||||||
fn := filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name()))
|
|
||||||
if err := os.MkdirAll(filepath.Dir(fn), 0755); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,10 +119,10 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
|||||||
controls: make(map[netip.AddrPort]*nebula.Control),
|
controls: make(map[netip.AddrPort]*nebula.Control),
|
||||||
vpnControls: make(map[netip.Addr]*nebula.Control),
|
vpnControls: make(map[netip.Addr]*nebula.Control),
|
||||||
inNat: make(map[netip.AddrPort]*nebula.Control),
|
inNat: make(map[netip.AddrPort]*nebula.Control),
|
||||||
outNat: make(map[outNatKey]netip.AddrPort),
|
outNat: make(map[string]netip.AddrPort),
|
||||||
flow: []flowEntry{},
|
flow: []flowEntry{},
|
||||||
ignoreFlows: []ignoreFlow{},
|
ignoreFlows: []ignoreFlow{},
|
||||||
fn: fn,
|
fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())),
|
||||||
t: t,
|
t: t,
|
||||||
cancelRender: cancel,
|
cancelRender: cancel,
|
||||||
}
|
}
|
||||||
@@ -210,10 +153,8 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-clockSource.C:
|
case <-clockSource.C:
|
||||||
r.Lock()
|
|
||||||
r.renderHostmaps("clock tick")
|
r.renderHostmaps("clock tick")
|
||||||
r.renderFlow()
|
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.
|
// RenderFlow renders the packet flow seen up until now and stops further automatic renders from happening.
|
||||||
func (r *R) RenderFlow() {
|
func (r *R) RenderFlow() {
|
||||||
r.cancelRender()
|
r.cancelRender()
|
||||||
r.Lock()
|
|
||||||
defer r.Unlock()
|
|
||||||
r.renderFlow()
|
r.renderFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelFlowLogs stops flow logs from being tracked and destroys any logs already collected
|
// CancelFlowLogs stops flow logs from being tracked and destroys any logs already collected
|
||||||
func (r *R) CancelFlowLogs() {
|
func (r *R) CancelFlowLogs() {
|
||||||
r.cancelRender()
|
r.cancelRender()
|
||||||
r.Lock()
|
|
||||||
r.flow = nil
|
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() {
|
func (r *R) renderFlow() {
|
||||||
if r.flow == nil {
|
if r.flow == nil {
|
||||||
return
|
return
|
||||||
@@ -276,7 +211,7 @@ func (r *R) renderFlow() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := e.packet.fromAddr()
|
addr := e.packet.from.GetUDPAddr()
|
||||||
if _, ok := participants[addr]; ok {
|
if _, ok := participants[addr]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -295,6 +230,7 @@ func (r *R) renderFlow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Print packets
|
// Print packets
|
||||||
|
h := &header.H{}
|
||||||
for _, e := range r.flow {
|
for _, e := range r.flow {
|
||||||
if e.packet == nil {
|
if e.packet == nil {
|
||||||
//fmt.Fprintf(f, " note over %s: %s\n", strings.Join(participantsVals, ", "), e.note)
|
//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))
|
fmt.Fprintln(f, r.formatUdpPacket(p))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
if err := h.Parse(p.packet.Data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
line := "--x"
|
line := "--x"
|
||||||
if p.rx {
|
if p.rx {
|
||||||
line = "->>"
|
line = "->>"
|
||||||
}
|
}
|
||||||
|
|
||||||
detail := fmt.Sprintf("%s(%s), index %v, counter: %v",
|
fmt.Fprintf(f,
|
||||||
p.h.TypeName(), p.h.SubTypeName(), p.h.RemoteIndex, p.h.MessageCounter)
|
" %s%s%s: %s(%s), index %v, counter: %v\n",
|
||||||
if p.parseErr != nil {
|
normalizeName(p.from.GetUDPAddr().String()),
|
||||||
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()),
|
|
||||||
line,
|
line,
|
||||||
normalizeName(p.toAddr().String()),
|
normalizeName(p.to.GetUDPAddr().String()),
|
||||||
detail,
|
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)))
|
r.renderHostmaps(fmt.Sprintf("Packet %v", len(r.flow)))
|
||||||
|
|
||||||
var h header.H
|
if len(r.ignoreFlows) > 0 {
|
||||||
var parseErr error
|
var h header.H
|
||||||
if !tun {
|
err := h.Parse(p.Data)
|
||||||
parseErr = h.Parse(p.Data)
|
if err != nil {
|
||||||
}
|
panic(err)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A packet we could not parse has no type to match against, so no rule can ignore it
|
for _, i := range r.ignoreFlows {
|
||||||
if parseErr == nil && i.messageType == h.Type && i.subType == h.Subtype {
|
if !tun {
|
||||||
return nil
|
if i.messageType == h.Type && i.subType == h.Subtype {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if i.tun.HasValue && i.tun.IsTrue {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fp := &packet{
|
fp := &packet{
|
||||||
from: from,
|
from: from,
|
||||||
to: to,
|
to: to,
|
||||||
packet: p.Copy(),
|
packet: p.Copy(),
|
||||||
tun: tun,
|
tun: tun,
|
||||||
h: h,
|
|
||||||
parseErr: parseErr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.flow = append(r.flow, flowEntry{packet: fp})
|
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())
|
panic("No control for udp tx " + a.String())
|
||||||
}
|
}
|
||||||
fp := r.unlockedInjectFlow(sender, c, p, false)
|
fp := r.unlockedInjectFlow(sender, c, p, false)
|
||||||
c.InjectUDPPacket(p) // copies internally; original is ours to release
|
c.InjectUDPPacket(p)
|
||||||
fp.WasReceived()
|
fp.WasReceived()
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteForAllUntilTxTun will route for everyone and return when a packet is seen on the receiver's tun.
|
// RouteForAllUntilTxTun will route for everyone and return when a packet is seen on receivers tun
|
||||||
// If a control's UDP TX address can't be matched to a registered control, we panic.
|
// If the router doesn't have the nebula controller for that address, we panic
|
||||||
//
|
|
||||||
// For allocation-sensitive callers (hot-path benchmarks, in particular relay
|
|
||||||
// benches with 3+ controls), call EnableFanIn() first.
|
|
||||||
func (r *R) RouteForAllUntilTxTun(receiver *nebula.Control) []byte {
|
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)
|
sc := make([]reflect.SelectCase, len(r.controls)+1)
|
||||||
cm := make([]*nebula.Control, 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 := 0
|
||||||
i := 1
|
sc[i] = reflect.SelectCase{
|
||||||
|
Dir: reflect.SelectRecv,
|
||||||
|
Chan: reflect.ValueOf(receiver.GetTunTxChan()),
|
||||||
|
Send: reflect.Value{},
|
||||||
|
}
|
||||||
|
cm[i] = receiver
|
||||||
|
|
||||||
|
i++
|
||||||
for _, c := range r.controls {
|
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
|
cm[i] = c
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
r.selRecvCtl = receiver
|
|
||||||
r.selCases = sc
|
for {
|
||||||
r.selCtls = cm
|
x, rx, _ := reflect.Select(sc)
|
||||||
return sc, cm
|
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.
|
// 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 {
|
switch e {
|
||||||
case ExitNow:
|
case ExitNow:
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case RouteAndExit:
|
case RouteAndExit:
|
||||||
@@ -689,13 +529,8 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
|||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
fp.WasReceived()
|
fp.WasReceived()
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
return
|
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:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
@@ -706,7 +541,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.Unlock()
|
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) {
|
func (r *R) RouteForAllUntilAfterMsgTypeTo(receiver *nebula.Control, msgType header.MessageType, subType header.MessageSubType) {
|
||||||
h := &header.H{}
|
h := &header.H{}
|
||||||
r.RouteForAllExitFunc(func(p *udp.Packet, r *nebula.Control) ExitType {
|
r.RouteForAllExitFunc(func(p *udp.Packet, r *nebula.Control) ExitType {
|
||||||
@@ -886,7 +641,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
|||||||
switch e {
|
switch e {
|
||||||
case ExitNow:
|
case ExitNow:
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case RouteAndExit:
|
case RouteAndExit:
|
||||||
@@ -894,13 +648,8 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
|||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
fp.WasReceived()
|
fp.WasReceived()
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
return
|
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:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
@@ -910,7 +659,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
|||||||
panic(fmt.Sprintf("Unknown exitFunc return: %v", e))
|
panic(fmt.Sprintf("Unknown exitFunc return: %v", e))
|
||||||
}
|
}
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -954,20 +702,19 @@ func (r *R) FlushAll() {
|
|||||||
}
|
}
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
r.Unlock()
|
r.Unlock()
|
||||||
p.Release()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getControl performs or seeds NAT translation and returns the control for toAddr, p from fields may change
|
// 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
|
// 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 {
|
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
|
p.From = newAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
c, ok := r.inNat[toAddr]
|
c, ok := r.inNat[toAddr]
|
||||||
if ok {
|
if ok {
|
||||||
r.outNat[outNatKey{from: c.GetUDPAddr(), to: fromAddr}] = toAddr
|
r.outNat[c.GetUDPAddr().String()+":"+fromAddr.String()] = toAddr
|
||||||
return c
|
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
-255
@@ -12,15 +12,11 @@ import (
|
|||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/cert_test"
|
"github.com/slackhq/nebula/cert_test"
|
||||||
"github.com/slackhq/nebula/e2e/router"
|
"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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDropInactiveTunnels(t *testing.T) {
|
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
|
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||||
// under ideal conditions
|
// under ideal conditions
|
||||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
@@ -43,8 +39,8 @@ func TestDropInactiveTunnels(t *testing.T) {
|
|||||||
r.Log("Go inactive and wait for the tunnels to get dropped")
|
r.Log("Go inactive and wait for the tunnels to get dropped")
|
||||||
waitStart := time.Now()
|
waitStart := time.Now()
|
||||||
for {
|
for {
|
||||||
myIndexes := myControl.GetHostmapIndexCount()
|
myIndexes := len(myControl.GetHostmap().Indexes)
|
||||||
theirIndexes := theirControl.GetHostmapIndexCount()
|
theirIndexes := len(theirControl.GetHostmap().Indexes)
|
||||||
if myIndexes == 0 && theirIndexes == 0 {
|
if myIndexes == 0 && theirIndexes == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -65,7 +61,6 @@ func TestDropInactiveTunnels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCertUpgrade(t *testing.T) {
|
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
|
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||||
// under ideal conditions
|
// under ideal conditions
|
||||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
@@ -160,7 +155,6 @@ func TestCertUpgrade(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCertDowngrade(t *testing.T) {
|
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
|
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||||
// under ideal conditions
|
// under ideal conditions
|
||||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
@@ -259,7 +253,6 @@ func TestCertDowngrade(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCertMismatchCorrection(t *testing.T) {
|
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
|
// The goal of this test is to ensure the shortest inactivity timeout will close the tunnel on both sides
|
||||||
// under ideal conditions
|
// under ideal conditions
|
||||||
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
@@ -325,249 +318,3 @@ func TestCertMismatchCorrection(t *testing.T) {
|
|||||||
myControl.Stop()
|
myControl.Stop()
|
||||||
theirControl.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
|
# max, net.core.rmem_max and net.core.wmem_max
|
||||||
#read_buffer: 10485760
|
#read_buffer: 10485760
|
||||||
#write_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
|
# 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
|
# 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.
|
# 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
|
# valid values: always, never, private
|
||||||
# This setting is reloadable.
|
# This setting is reloadable.
|
||||||
#send_recv_error: always
|
#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.
|
# 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,
|
# 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.
|
# 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:
|
punchy:
|
||||||
# Continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings
|
# Continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings
|
||||||
# This setting is reloadable.
|
|
||||||
punch: true
|
punch: true
|
||||||
|
|
||||||
# respond means that a node you are trying to reach will connect back out to you if your hole punching fails
|
# 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
|
# this is extremely useful if one node is behind a difficult nat, such as a symmetric NAT
|
||||||
# Default is false
|
# Default is false
|
||||||
# This setting is reloadable.
|
|
||||||
#respond: true
|
#respond: true
|
||||||
|
|
||||||
# delays a punch response for misbehaving NATs, default is 1 second.
|
# delays a punch response for misbehaving NATs, default is 1 second.
|
||||||
# This setting is reloadable.
|
|
||||||
#delay: 1s
|
#delay: 1s
|
||||||
|
|
||||||
# set the delay before attempting punchy.respond. Default is 5 seconds. respond must be true to take effect.
|
# 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
|
#respond_delay: 5s
|
||||||
|
|
||||||
# Cipher allows you to choose between the available ciphers for your network. Options are chachapoly or aes
|
# 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 SSH CA public keys. These are the public keys of the CAs that are allowed to sign SSH keys for access.
|
||||||
#trusted_cas:
|
#trusted_cas:
|
||||||
#- "ssh public key string"
|
#- "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.
|
# EXPERIMENTAL: relay support for networks that can't establish direct connections.
|
||||||
relay:
|
relay:
|
||||||
@@ -302,24 +272,6 @@ tun:
|
|||||||
# metric: 100
|
# metric: 100
|
||||||
# install: true
|
# 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
|
# 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.
|
# in nebula configuration files. Default false, not reloadable.
|
||||||
#use_system_route_table: false
|
#use_system_route_table: false
|
||||||
@@ -330,21 +282,24 @@ tun:
|
|||||||
|
|
||||||
# Configure logging level
|
# Configure logging level
|
||||||
logging:
|
logging:
|
||||||
# trace, debug, info, warn, or error. Default is info and is reloadable.
|
# panic, fatal, error, warning, info, or debug. Default is info and is reloadable.
|
||||||
# fatal and panic are accepted for backwards compatibility and map to error.
|
#NOTE: Debug mode can log remotely controlled/untrusted data which can quickly fill a disk in some
|
||||||
#NOTE: Debug and trace modes 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.
|
||||||
# scenarios. Debug and trace logging are also CPU intensive and will decrease performance overall.
|
# Only enable debug logging while actively investigating an issue.
|
||||||
# Only enable debug or trace logging while actively investigating an issue.
|
|
||||||
level: info
|
level: info
|
||||||
# json or text formats currently available. Default is text.
|
# json or text formats currently available. Default is text
|
||||||
format: 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
|
#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:
|
#stats:
|
||||||
#type: graphite
|
#type: graphite
|
||||||
#prefix: nebula
|
#prefix: nebula
|
||||||
@@ -362,12 +317,10 @@ logging:
|
|||||||
# enables counter metrics for meta packets
|
# enables counter metrics for meta packets
|
||||||
# e.g.: `messages.tx.handshake`
|
# e.g.: `messages.tx.handshake`
|
||||||
# NOTE: `message.{tx,rx}.recv_error` is always emitted
|
# NOTE: `message.{tx,rx}.recv_error` is always emitted
|
||||||
# Not reloadable.
|
|
||||||
#message_metrics: false
|
#message_metrics: false
|
||||||
|
|
||||||
# enables detailed counter metrics for lighthouse packets
|
# enables detailed counter metrics for lighthouse packets
|
||||||
# e.g.: `lighthouse.rx.HostQuery`
|
# e.g.: `lighthouse.rx.HostQuery`
|
||||||
# Not reloadable.
|
|
||||||
#lighthouse_metrics: false
|
#lighthouse_metrics: false
|
||||||
|
|
||||||
# Handshake Manager Settings
|
# Handshake Manager Settings
|
||||||
@@ -405,7 +358,7 @@ firewall:
|
|||||||
# `drop` (default): silently drop the packet.
|
# `drop` (default): silently drop the packet.
|
||||||
# `reject`: send a reject reply.
|
# `reject`: send a reject reply.
|
||||||
# - For TCP, this will be a RST "Connection Reset" packet.
|
# - 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
|
outbound_action: drop
|
||||||
inbound_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
|
# 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)
|
# 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).
|
# - 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`
|
# proto: `any`, `tcp`, `udp`, or `icmp`
|
||||||
# a port specification is ignored if proto is `icmp`
|
|
||||||
# host: `any` or a literal hostname, ie `test-host`
|
# host: `any` or a literal hostname, ie `test-host`
|
||||||
# group: `any` or a literal group name, ie `default-group`
|
# 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
|
# 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.
|
# 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. `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. This can be used to filter destinations when using unsafe_routes.
|
||||||
# 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.
|
# 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.
|
# 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
|
# ca_name: An issuing CA name
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula"
|
"github.com/slackhq/nebula"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/logging"
|
|
||||||
"github.com/slackhq/nebula/overlay"
|
"github.com/slackhq/nebula/overlay"
|
||||||
"github.com/slackhq/nebula/service"
|
"github.com/slackhq/nebula/service"
|
||||||
)
|
)
|
||||||
@@ -64,7 +64,8 @@ pki:
|
|||||||
return err
|
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)
|
ctrl, err := nebula.Main(&cfg, false, "custom-app", logger, overlay.NewUserDeviceFromConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,15 +8,6 @@ Before=sshd.service
|
|||||||
Type=notify
|
Type=notify
|
||||||
NotifyAccess=main
|
NotifyAccess=main
|
||||||
SyslogIdentifier=nebula
|
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
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml
|
ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
+200
-302
@@ -1,16 +1,13 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"log/slog"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,13 +15,14 @@ import (
|
|||||||
|
|
||||||
"github.com/gaissmai/bart"
|
"github.com/gaissmai/bart"
|
||||||
"github.com/rcrowley/go-metrics"
|
"github.com/rcrowley/go-metrics"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"github.com/slackhq/nebula/firewall"
|
"github.com/slackhq/nebula/firewall"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FirewallInterface interface {
|
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 {
|
type conn struct {
|
||||||
@@ -44,8 +42,8 @@ type Firewall struct {
|
|||||||
InRules *FirewallTable
|
InRules *FirewallTable
|
||||||
OutRules *FirewallTable
|
OutRules *FirewallTable
|
||||||
|
|
||||||
InboundSendReject bool
|
InSendReject bool
|
||||||
OutboundSendReject bool
|
OutSendReject bool
|
||||||
|
|
||||||
//TODO: we should have many more options for TCP, an option for ICMP, and mimic the kernel a bit better
|
//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
|
// https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt
|
||||||
@@ -58,9 +56,8 @@ type Firewall struct {
|
|||||||
routableNetworks *bart.Lite
|
routableNetworks *bart.Lite
|
||||||
|
|
||||||
// assignedNetworks is a list of vpn networks assigned to us in the certificate.
|
// assignedNetworks is a list of vpn networks assigned to us in the certificate.
|
||||||
assignedNetworks []netip.Prefix
|
assignedNetworks []netip.Prefix
|
||||||
// unsafeNetworks is the list of unsafe networks issued to us in the certificate
|
hasUnsafeNetworks bool
|
||||||
unsafeNetworks []netip.Prefix
|
|
||||||
|
|
||||||
rules string
|
rules string
|
||||||
rulesVersion uint16
|
rulesVersion uint16
|
||||||
@@ -69,7 +66,7 @@ type Firewall struct {
|
|||||||
incomingMetrics firewallMetrics
|
incomingMetrics firewallMetrics
|
||||||
outgoingMetrics firewallMetrics
|
outgoingMetrics firewallMetrics
|
||||||
|
|
||||||
l *slog.Logger
|
l *logrus.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type firewallMetrics struct {
|
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.
|
// 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.
|
// 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
|
//TODO: error on 0 duration
|
||||||
var tmin, tmax time.Duration
|
var tmin, tmax time.Duration
|
||||||
|
|
||||||
@@ -159,9 +156,10 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
|
|||||||
assignedNetworks = append(assignedNetworks, network)
|
assignedNetworks = append(assignedNetworks, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafeNetworks := c.UnsafeNetworks()
|
hasUnsafeNetworks := false
|
||||||
for _, n := range unsafeNetworks {
|
for _, n := range c.UnsafeNetworks() {
|
||||||
routableNetworks.Insert(n)
|
routableNetworks.Insert(n)
|
||||||
|
hasUnsafeNetworks = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Firewall{
|
return &Firewall{
|
||||||
@@ -169,15 +167,15 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
|
|||||||
Conns: make(map[firewall.Packet]*conn),
|
Conns: make(map[firewall.Packet]*conn),
|
||||||
TimerWheel: NewTimerWheel[firewall.Packet](tmin, tmax),
|
TimerWheel: NewTimerWheel[firewall.Packet](tmin, tmax),
|
||||||
},
|
},
|
||||||
InRules: newFirewallTable(),
|
InRules: newFirewallTable(),
|
||||||
OutRules: newFirewallTable(),
|
OutRules: newFirewallTable(),
|
||||||
TCPTimeout: tcpTimeout,
|
TCPTimeout: tcpTimeout,
|
||||||
UDPTimeout: UDPTimeout,
|
UDPTimeout: UDPTimeout,
|
||||||
DefaultTimeout: defaultTimeout,
|
DefaultTimeout: defaultTimeout,
|
||||||
routableNetworks: routableNetworks,
|
routableNetworks: routableNetworks,
|
||||||
assignedNetworks: assignedNetworks,
|
assignedNetworks: assignedNetworks,
|
||||||
unsafeNetworks: unsafeNetworks,
|
hasUnsafeNetworks: hasUnsafeNetworks,
|
||||||
l: l,
|
l: l,
|
||||||
|
|
||||||
incomingMetrics: firewallMetrics{
|
incomingMetrics: firewallMetrics{
|
||||||
droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_addr", nil),
|
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)
|
certificate := cs.getCertificate(cert.Version2)
|
||||||
if certificate == nil {
|
if certificate == nil {
|
||||||
certificate = cs.getCertificate(cert.Version1)
|
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")
|
inboundAction := c.GetString("firewall.inbound_action", "drop")
|
||||||
switch inboundAction {
|
switch inboundAction {
|
||||||
case "reject":
|
case "reject":
|
||||||
fw.InboundSendReject = true
|
fw.InSendReject = true
|
||||||
case "drop":
|
case "drop":
|
||||||
fw.InboundSendReject = false
|
fw.InSendReject = false
|
||||||
default:
|
default:
|
||||||
l.Warn("invalid firewall.inbound_action, defaulting to `drop`", "action", inboundAction)
|
l.WithField("action", inboundAction).Warn("invalid firewall.inbound_action, defaulting to `drop`")
|
||||||
fw.InboundSendReject = false
|
fw.InSendReject = false
|
||||||
}
|
}
|
||||||
|
|
||||||
outboundAction := c.GetString("firewall.outbound_action", "drop")
|
outboundAction := c.GetString("firewall.outbound_action", "drop")
|
||||||
switch outboundAction {
|
switch outboundAction {
|
||||||
case "reject":
|
case "reject":
|
||||||
fw.OutboundSendReject = true
|
fw.OutSendReject = true
|
||||||
case "drop":
|
case "drop":
|
||||||
fw.OutboundSendReject = false
|
fw.OutSendReject = false
|
||||||
default:
|
default:
|
||||||
l.Warn("invalid firewall.outbound_action, defaulting to `drop`", "action", outboundAction)
|
l.WithField("action", inboundAction).Warn("invalid firewall.outbound_action, defaulting to `drop`")
|
||||||
fw.OutboundSendReject = false
|
fw.OutSendReject = false
|
||||||
}
|
}
|
||||||
|
|
||||||
err := AddFirewallRulesFromConfig(l, false, c, fw)
|
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.
|
// 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 (
|
var (
|
||||||
ft *FirewallTable
|
ft *FirewallTable
|
||||||
fp firewallPort
|
fp firewallPort
|
||||||
@@ -267,12 +290,6 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
|
|||||||
case firewall.ProtoUDP:
|
case firewall.ProtoUDP:
|
||||||
fp = ft.UDP
|
fp = ft.UDP
|
||||||
case firewall.ProtoICMP, firewall.ProtoICMPv6:
|
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
|
fp = ft.ICMP
|
||||||
case firewall.ProtoAny:
|
case firewall.ProtoAny:
|
||||||
fp = ft.AnyProto
|
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)
|
return fmt.Errorf("unknown protocol %v", proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need this rule string because we generate a hash. Removing this will break firewall reload.
|
return fp.addRule(f, startPort, endPort, groups, host, ip, localIp, caName, caSha)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRuleHash returns a hash representation of all inbound and outbound rules
|
// 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)
|
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
|
var table string
|
||||||
if inbound {
|
if inbound {
|
||||||
table = "firewall.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 {
|
for i, t := range rs {
|
||||||
|
var groups []string
|
||||||
r, err := convertRule(l, t, table, i)
|
r, err := convertRule(l, t, table, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s rule #%v; %s", table, i, err)
|
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)
|
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)
|
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
|
var sPort, errPort string
|
||||||
if r.Code != "" {
|
if r.Code != "" {
|
||||||
errPort = "code"
|
errPort = "code"
|
||||||
@@ -357,55 +373,42 @@ func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw Fi
|
|||||||
sPort = r.Port
|
sPort = r.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
var proto uint8
|
startPort, endPort, err := parsePort(sPort)
|
||||||
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)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
|
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Cidr != "" && r.Cidr != "any" {
|
var proto uint8
|
||||||
_, err = netip.ParsePrefix(r.Cidr)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("%s rule #%v; cidr did not parse; %s", table, i, err)
|
return fmt.Errorf("%s rule #%v; cidr did not parse; %s", table, i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.LocalCidr != "" && r.LocalCidr != "any" {
|
var localCidr netip.Prefix
|
||||||
_, err = netip.ParsePrefix(r.LocalCidr)
|
if r.LocalCidr != "" {
|
||||||
|
localCidr, err = netip.ParsePrefix(r.LocalCidr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s rule #%v; local_cidr did not parse; %s", table, i, err)
|
return fmt.Errorf("%s rule #%v; local_cidr did not parse; %s", table, i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if warning := r.sanity(); warning != nil {
|
err = fw.AddRule(inbound, proto, startPort, endPort, groups, r.Host, cidr, localCidr, r.CAName, r.CASha)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s rule #%v; `%s`", table, i, err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrUnknownNetworkType = errors.New("unknown network type")
|
var ErrInvalidRemoteIP = errors.New("remote IP is not in remote certificate subnets")
|
||||||
var ErrPeerRejected = errors.New("remote address is not within a network that we handle")
|
var ErrInvalidLocalIP = errors.New("local IP is not in list of handled local IPs")
|
||||||
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 ErrNoMatchingRule = errors.New("no matching rule in firewall table")
|
var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
|
||||||
|
|
||||||
// Drop returns an error if the packet should be dropped, explaining why. It
|
// Drop returns an error if the packet should be dropped, explaining why. It
|
||||||
// returns nil if the packet should not be dropped.
|
// 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 {
|
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
|
// Check if we spoke to this tuple, if we did then allow this packet
|
||||||
if h.networks == nil {
|
if f.inConns(fp, h, caPool, localCache) {
|
||||||
// Simple case: Certificate has one address and no unsafe networks
|
return nil
|
||||||
if h.vpnAddrs[0] != fp.RemoteAddr {
|
}
|
||||||
|
|
||||||
|
// Make sure remote address matches nebula certificate
|
||||||
|
if h.networks != nil {
|
||||||
|
if !h.networks.Contains(fp.RemoteAddr) {
|
||||||
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
||||||
return ErrInvalidRemoteIP
|
return ErrInvalidRemoteIP
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nwType, ok := h.networks.Lookup(fp.RemoteAddr)
|
// Simple case: Certificate has one address and no unsafe networks
|
||||||
if !ok {
|
if h.vpnAddrs[0] != fp.RemoteAddr {
|
||||||
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
f.metrics(incoming).droppedRemoteAddr.Inc(1)
|
||||||
return ErrInvalidRemoteIP
|
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
|
// 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
|
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
|
table := f.OutRules
|
||||||
if incoming {
|
if incoming {
|
||||||
table = f.InRules
|
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
|
// firewall object is created
|
||||||
func (f *Firewall) Destroy() {
|
func (f *Firewall) Destroy() {
|
||||||
//TODO: clean references if/when needed
|
//TODO: clean references if/when needed
|
||||||
@@ -502,11 +490,9 @@ func (f *Firewall) EmitStats() {
|
|||||||
metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
|
metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) bool {
|
func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache *firewall.ConntrackCache) bool {
|
||||||
if localCache != nil {
|
if localCache != nil && localCache.Has(fp) {
|
||||||
if _, ok := localCache[fp]; ok {
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
conntrack := f.Conntrack
|
conntrack := f.Conntrack
|
||||||
conntrack.Lock()
|
conntrack.Lock()
|
||||||
@@ -534,26 +520,26 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
|
|||||||
|
|
||||||
// We now know which firewall table to check against
|
// We now know which firewall table to check against
|
||||||
if !table.match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
|
if !table.match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Level >= logrus.DebugLevel {
|
||||||
h.logger(f.l).Debug("dropping old conntrack entry, does not match new ruleset",
|
h.logger(f.l).
|
||||||
"fwPacket", fp,
|
WithField("fwPacket", fp).
|
||||||
"incoming", c.incoming,
|
WithField("incoming", c.incoming).
|
||||||
"rulesVersion", f.rulesVersion,
|
WithField("rulesVersion", f.rulesVersion).
|
||||||
"oldRulesVersion", c.rulesVersion,
|
WithField("oldRulesVersion", c.rulesVersion).
|
||||||
)
|
Debugln("dropping old conntrack entry, does not match new ruleset")
|
||||||
}
|
}
|
||||||
delete(conntrack.Conns, fp)
|
delete(conntrack.Conns, fp)
|
||||||
conntrack.Unlock()
|
conntrack.Unlock()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Level >= logrus.DebugLevel {
|
||||||
h.logger(f.l).Debug("keeping old conntrack entry, does match new ruleset",
|
h.logger(f.l).
|
||||||
"fwPacket", fp,
|
WithField("fwPacket", fp).
|
||||||
"incoming", c.incoming,
|
WithField("incoming", c.incoming).
|
||||||
"rulesVersion", f.rulesVersion,
|
WithField("rulesVersion", f.rulesVersion).
|
||||||
"oldRulesVersion", c.rulesVersion,
|
WithField("oldRulesVersion", c.rulesVersion).
|
||||||
)
|
Debugln("keeping old conntrack entry, does match new ruleset")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.rulesVersion = f.rulesVersion
|
c.rulesVersion = f.rulesVersion
|
||||||
@@ -571,7 +557,7 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
|
|||||||
conntrack.Unlock()
|
conntrack.Unlock()
|
||||||
|
|
||||||
if localCache != nil {
|
if localCache != nil {
|
||||||
localCache[fp] = struct{}{}
|
localCache.Add(fp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -652,7 +638,7 @@ func (ft *FirewallTable) match(p firewall.Packet, incoming bool, c *cert.CachedC
|
|||||||
return false
|
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 {
|
if startPort > endPort {
|
||||||
return fmt.Errorf("start port was lower than end port")
|
return fmt.Errorf("start port was lower than end port")
|
||||||
}
|
}
|
||||||
@@ -665,7 +651,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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,13 +665,6 @@ func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCer
|
|||||||
return false
|
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
|
var port int32
|
||||||
|
|
||||||
if p.Fragment {
|
if p.Fragment {
|
||||||
@@ -703,7 +682,7 @@ func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCer
|
|||||||
return fp[firewall.PortAny].match(p, c, caPool)
|
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 {
|
fr := func() *FirewallRule {
|
||||||
return &FirewallRule{
|
return &FirewallRule{
|
||||||
Hosts: make(map[string]*firewallLocalCIDR),
|
Hosts: make(map[string]*firewallLocalCIDR),
|
||||||
@@ -717,14 +696,14 @@ func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, cidr, l
|
|||||||
fc.Any = fr()
|
fc.Any = fr()
|
||||||
}
|
}
|
||||||
|
|
||||||
return fc.Any.addRule(f, groups, host, cidr, localCidr)
|
return fc.Any.addRule(f, groups, host, ip, localIp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if caSha != "" {
|
if caSha != "" {
|
||||||
if _, ok := fc.CAShas[caSha]; !ok {
|
if _, ok := fc.CAShas[caSha]; !ok {
|
||||||
fc.CAShas[caSha] = fr()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -734,7 +713,7 @@ func (fc *FirewallCA) addRule(f *Firewall, groups []string, host string, cidr, l
|
|||||||
if _, ok := fc.CANames[caName]; !ok {
|
if _, ok := fc.CANames[caName]; !ok {
|
||||||
fc.CANames[caName] = fr()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -766,24 +745,24 @@ func (fc *FirewallCA) match(p firewall.Packet, c *cert.CachedCertificate, caPool
|
|||||||
return fc.CANames[s.Certificate.Name()].match(p, c)
|
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 {
|
flc := func() *firewallLocalCIDR {
|
||||||
return &firewallLocalCIDR{
|
return &firewallLocalCIDR{
|
||||||
LocalCIDR: new(bart.Lite),
|
LocalCIDR: new(bart.Lite),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fr.isAny(groups, host, cidr) {
|
if fr.isAny(groups, host, ip) {
|
||||||
if fr.Any == nil {
|
if fr.Any == nil {
|
||||||
fr.Any = flc()
|
fr.Any = flc()
|
||||||
}
|
}
|
||||||
|
|
||||||
return fr.Any.addRule(f, localCidr)
|
return fr.Any.addRule(f, localCIDR)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(groups) > 0 {
|
if len(groups) > 0 {
|
||||||
nlc := flc()
|
nlc := flc()
|
||||||
err := nlc.addRule(f, localCidr)
|
err := nlc.addRule(f, localCIDR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -799,46 +778,44 @@ func (fr *FirewallRule) addRule(f *Firewall, groups []string, host, cidr, localC
|
|||||||
if nlc == nil {
|
if nlc == nil {
|
||||||
nlc = flc()
|
nlc = flc()
|
||||||
}
|
}
|
||||||
err := nlc.addRule(f, localCidr)
|
err := nlc.addRule(f, localCIDR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fr.Hosts[host] = nlc
|
fr.Hosts[host] = nlc
|
||||||
}
|
}
|
||||||
|
|
||||||
if cidr != "" {
|
if ip.IsValid() {
|
||||||
c, err := netip.ParsePrefix(cidr)
|
nlc, _ := fr.CIDR.Get(ip)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nlc, _ := fr.CIDR.Get(c)
|
|
||||||
if nlc == nil {
|
if nlc == nil {
|
||||||
nlc = flc()
|
nlc = flc()
|
||||||
}
|
}
|
||||||
err = nlc.addRule(f, localCidr)
|
err := nlc.addRule(f, localCIDR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fr.CIDR.Insert(c, nlc)
|
fr.CIDR.Insert(ip, nlc)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fr *FirewallRule) isAny(groups []string, host string, cidr string) bool {
|
func (fr *FirewallRule) isAny(groups []string, host string, ip netip.Prefix) bool {
|
||||||
if len(groups) == 0 && host == "" && cidr == "" {
|
if len(groups) == 0 && host == "" && !ip.IsValid() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(groups, "any") {
|
for _, group := range groups {
|
||||||
return true
|
if group == "any" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if host == "any" {
|
if host == "any" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if cidr == "any" {
|
if ip.IsValid() && ip.Bits() == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,14 +867,9 @@ func (fr *FirewallRule) match(p firewall.Packet, c *cert.CachedCertificate) bool
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (flc *firewallLocalCIDR) addRule(f *Firewall, localCidr string) error {
|
func (flc *firewallLocalCIDR) addRule(f *Firewall, localIp netip.Prefix) error {
|
||||||
if localCidr == "any" {
|
if !localIp.IsValid() {
|
||||||
flc.Any = true
|
if !f.hasUnsafeNetworks || f.defaultLocalCIDRAny {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if localCidr == "" {
|
|
||||||
if len(f.unsafeNetworks) == 0 || f.defaultLocalCIDRAny {
|
|
||||||
flc.Any = true
|
flc.Any = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -907,13 +879,12 @@ func (flc *firewallLocalCIDR) addRule(f *Firewall, localCidr string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
|
} else if localIp.Bits() == 0 {
|
||||||
|
flc.Any = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := netip.ParsePrefix(localCidr)
|
flc.LocalCIDR.Insert(localIp)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
flc.LocalCIDR.Insert(c)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,6 +905,7 @@ type rule struct {
|
|||||||
Code string
|
Code string
|
||||||
Proto string
|
Proto string
|
||||||
Host string
|
Host string
|
||||||
|
Group string
|
||||||
Groups []string
|
Groups []string
|
||||||
Cidr string
|
Cidr string
|
||||||
LocalCidr string
|
LocalCidr string
|
||||||
@@ -941,7 +913,7 @@ type rule struct {
|
|||||||
CASha string
|
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{}
|
r := rule{}
|
||||||
|
|
||||||
m, ok := p.(map[string]any)
|
m, ok := p.(map[string]any)
|
||||||
@@ -972,14 +944,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")
|
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",
|
l.Warnf("%s rule #%v; group was an array with a single value, converting to simple value", table, i)
|
||||||
"table", table,
|
|
||||||
"rule", i,
|
|
||||||
)
|
|
||||||
m["group"] = v[0]
|
m["group"] = v[0]
|
||||||
}
|
}
|
||||||
|
r.Group = toString("group", m)
|
||||||
singleGroup := toString("group", m)
|
|
||||||
|
|
||||||
if rg, ok := m["groups"]; ok {
|
if rg, ok := m["groups"]; ok {
|
||||||
switch reflect.TypeOf(rg).Kind() {
|
switch reflect.TypeOf(rg).Kind() {
|
||||||
@@ -996,122 +964,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
|
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
|
func parsePort(s string) (startPort, endPort int32, err error) {
|
||||||
// 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
|
|
||||||
if s == "any" {
|
if s == "any" {
|
||||||
return firewall.PortAny, firewall.PortAny, nil
|
startPort = firewall.PortAny
|
||||||
}
|
|
||||||
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 {
|
|
||||||
endPort = 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
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-25
@@ -1,65 +1,101 @@
|
|||||||
package firewall
|
package firewall
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"sync"
|
||||||
"log/slog"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConntrackCache is used as a local routine cache to know if a given flow
|
// ConntrackCache is used as a local routine cache to know if a given flow
|
||||||
// has been seen in the conntrack table.
|
// has been seen in the conntrack table.
|
||||||
type ConntrackCache map[Packet]struct{}
|
type ConntrackCache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
entries map[Packet]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConntrackCache() *ConntrackCache {
|
||||||
|
return &ConntrackCache{entries: make(map[Packet]struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConntrackCache) Has(p Packet) bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
_, ok := c.entries[p]
|
||||||
|
c.mu.Unlock()
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConntrackCache) Add(p Packet) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.entries[p] = struct{}{}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConntrackCache) Len() int {
|
||||||
|
if c == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
l := len(c.entries)
|
||||||
|
c.mu.Unlock()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConntrackCache) Reset(capHint int) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.entries = make(map[Packet]struct{}, capHint)
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
type ConntrackCacheTicker struct {
|
type ConntrackCacheTicker struct {
|
||||||
cacheV uint64
|
cacheV uint64
|
||||||
cacheTick atomic.Uint64
|
cacheTick atomic.Uint64
|
||||||
|
|
||||||
l *slog.Logger
|
cache *ConntrackCache
|
||||||
cache ConntrackCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConntrackCacheTicker(ctx context.Context, l *slog.Logger, d time.Duration) *ConntrackCacheTicker {
|
func NewConntrackCacheTicker(d time.Duration) *ConntrackCacheTicker {
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &ConntrackCacheTicker{
|
c := &ConntrackCacheTicker{cache: newConntrackCache()}
|
||||||
l: l,
|
|
||||||
cache: ConntrackCache{},
|
|
||||||
}
|
|
||||||
|
|
||||||
go c.tick(ctx, d)
|
go c.tick(d)
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConntrackCacheTicker) tick(ctx context.Context, d time.Duration) {
|
func (c *ConntrackCacheTicker) tick(d time.Duration) {
|
||||||
t := time.NewTicker(d)
|
|
||||||
defer t.Stop()
|
|
||||||
for {
|
for {
|
||||||
select {
|
time.Sleep(d)
|
||||||
case <-ctx.Done():
|
c.cacheTick.Add(1)
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
c.cacheTick.Add(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get checks if the cache ticker has moved to the next version before returning
|
// Get checks if the cache ticker has moved to the next version before returning
|
||||||
// the map. If it has moved, we reset the map.
|
// 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 {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if tick := c.cacheTick.Load(); tick != c.cacheV {
|
if tick := c.cacheTick.Load(); tick != c.cacheV {
|
||||||
c.cacheV = tick
|
c.cacheV = tick
|
||||||
if ll := len(c.cache); ll > 0 {
|
if ll := c.cache.Len(); ll > 0 {
|
||||||
if c.l.Enabled(context.Background(), slog.LevelDebug) {
|
if l.Level == logrus.DebugLevel {
|
||||||
c.l.Debug("resetting conntrack cache", "len", ll)
|
l.WithField("len", ll).Debug("resetting conntrack cache")
|
||||||
}
|
}
|
||||||
c.cache = make(ConntrackCache, ll)
|
c.cache.Reset(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 {
|
type Packet struct {
|
||||||
LocalAddr netip.Addr
|
LocalAddr netip.Addr
|
||||||
RemoteAddr netip.Addr
|
RemoteAddr netip.Addr
|
||||||
// LocalPort is the destination port for incoming traffic, or the source port for outgoing. Zero for ICMP.
|
LocalPort uint16
|
||||||
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
|
|
||||||
RemotePort uint16
|
RemotePort uint16
|
||||||
Protocol uint8
|
Protocol uint8
|
||||||
Fragment bool
|
Fragment bool
|
||||||
@@ -49,8 +46,6 @@ func (fp Packet) MarshalJSON() ([]byte, error) {
|
|||||||
proto = "tcp"
|
proto = "tcp"
|
||||||
case ProtoICMP:
|
case ProtoICMP:
|
||||||
proto = "icmp"
|
proto = "icmp"
|
||||||
case ProtoICMPv6:
|
|
||||||
proto = "icmpv6"
|
|
||||||
case ProtoUDP:
|
case ProtoUDP:
|
||||||
proto = "udp"
|
proto = "udp"
|
||||||
default:
|
default:
|
||||||
|
|||||||
+124
-779
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,38 @@
|
|||||||
module github.com/slackhq/nebula
|
module github.com/slackhq/nebula
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.2
|
dario.cat/mergo v1.0.2
|
||||||
filippo.io/bigmod v0.1.0
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||||
github.com/armon/go-radix v1.0.0
|
github.com/armon/go-radix v1.0.0
|
||||||
|
github.com/cilium/ebpf v0.12.3
|
||||||
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432
|
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432
|
||||||
github.com/flynn/noise v1.1.0
|
github.com/flynn/noise v1.1.0
|
||||||
github.com/gaissmai/bart v0.28.0
|
github.com/gaissmai/bart v0.25.0
|
||||||
github.com/gogo/protobuf v1.3.2
|
github.com/gogo/protobuf v1.3.2
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/kardianos/service v1.3.0
|
github.com/kardianos/service v1.2.4
|
||||||
github.com/miekg/dns v1.1.72
|
github.com/miekg/dns v1.1.68
|
||||||
github.com/miekg/pkcs11 v1.1.2
|
github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b
|
||||||
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f
|
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
|
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
|
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
go.uber.org/goleak v1.3.0
|
golang.org/x/crypto v0.43.0
|
||||||
go.yaml.in/yaml/v3 v3.0.4
|
|
||||||
golang.org/x/crypto v0.54.0
|
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||||
golang.org/x/net v0.57.0
|
golang.org/x/net v0.45.0
|
||||||
golang.org/x/sync v0.22.0
|
golang.org/x/sync v0.17.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.37.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.36.0
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.8
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe
|
gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe
|
||||||
)
|
)
|
||||||
@@ -50,7 +49,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // 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/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=
|
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 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
@@ -19,6 +17,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
|
||||||
|
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
|
||||||
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps=
|
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps=
|
||||||
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM=
|
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -26,8 +26,10 @@ 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/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 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||||
github.com/gaissmai/bart v0.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko=
|
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
|
||||||
github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
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.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.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=
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
@@ -66,8 +68,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/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.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
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.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
|
||||||
github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
@@ -80,15 +82,16 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
|||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
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/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/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.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||||
github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
|
github.com/miekg/pkcs11 v1.1.2-0.20231115102856-9078ad6b9d4b h1:J/AzCvg5z0Hn1rqZUJjpbzALUmkKX0Zwbc/i4fw7Sfk=
|
||||||
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
@@ -133,6 +136,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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
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.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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
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=
|
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
|
||||||
@@ -155,23 +160,21 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
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 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
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-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-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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
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 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
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/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.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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
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-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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -182,8 +185,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-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-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.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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
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/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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -191,8 +194,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-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-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.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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
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-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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -206,13 +209,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-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-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-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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -223,8 +227,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-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-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.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -233,8 +237,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/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 h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
|
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 v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
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/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-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
@@ -243,8 +247,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.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
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.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.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user