mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 20:57:02 +02:00
Compare commits
262 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98b8d9cccf | |||
| 72bf111209 | |||
| 1617897043 | |||
| f8775bb6ca | |||
| 15f0f0d5d0 | |||
| 7902ce674e | |||
| c2fbe215e6 | |||
| 94ac6db4ca | |||
| a60350e34e | |||
| 58f3b6fda7 | |||
| a99699e370 | |||
| 3615a79b8b | |||
| 147c202c27 | |||
| e290a6892f | |||
| 6c3972f464 | |||
| 861d3aabd7 | |||
| 86733864fe | |||
| ab736e4c6b | |||
| 5ecdd4eaa9 | |||
| 1b84bd0050 | |||
| 384610f81a | |||
| c1eea118f4 | |||
| 1e66c0d3ee | |||
| e5c0fdad8d | |||
| 7bd0bc285a | |||
| 942ee522e0 | |||
| 32149f3a93 | |||
| 19ad3bb904 | |||
| 647775d8c3 | |||
| abfeb502a8 | |||
| 0a953915bb | |||
| 6aa3363d85 | |||
| 95d98b1f4b | |||
| 6afca0f461 | |||
| 02471b4121 | |||
| 58ab7250f5 | |||
| 184cdc8586 | |||
| 7d3166a19d | |||
| fe1c5682f0 | |||
| e4cc80aaca | |||
| 16b302c11d | |||
| ab539f8a3f | |||
| b7d83b0500 | |||
| ef95b25fa3 | |||
| 36b38396af | |||
| 2e9117da5b | |||
| a690c904ba | |||
| e028e6bf1a | |||
| 3db406b8ac | |||
| eaad4896c1 | |||
| e6032f81aa | |||
| b041f306cb | |||
| 3a95495c63 | |||
| 873f94f465 | |||
| 72bad1603a | |||
| 0c1ad9bb48 | |||
| 074a123a4b | |||
| 04dea41f74 | |||
| 0d23377c65 | |||
| ffd5249cf5 | |||
| 625f58b84a | |||
| 99c5854e5c | |||
| 3c121e7ab1 | |||
| 6c7ebb0875 | |||
| 110ea8f45c | |||
| 398d67e2da | |||
| 696903d6d9 | |||
| c82db210ef | |||
| 1ada3d4dd9 | |||
| 5f920fdd7d | |||
| cba9ea5b1f | |||
| 83809a599a | |||
| 23c67bd8d8 | |||
| dd3a7ad03c | |||
| dd2ac5d655 | |||
| 76e82a5256 | |||
| eaf756ea6c | |||
| a82a8dc547 | |||
| 213dd46588 | |||
| 4fb5cdb4fa | |||
| ff91c37529 | |||
| b7e9939e92 | |||
| 33c2d7277c | |||
| f141cebe8d | |||
| 9ec8cf10f3 | |||
| 1ab1f71dba | |||
| d0f02ba873 | |||
| 5f890dbc34 | |||
| db85d61c23 | |||
| db9218b0be | |||
| 5f00ab4b74 | |||
| 2a1cc62001 | |||
| e753e6e93c | |||
| 32a7c04498 | |||
| 8c50fc3f60 | |||
| 2f4532f102 | |||
| 8c71f2f3f9 | |||
| 3d34cc9b74 | |||
| e80b9830a3 | |||
| 49e3c4649b | |||
| 72c04b90bd | |||
| 36ab1dbb97 | |||
| f77fe74192 | |||
| 24c9c704a0 | |||
| a5e81efe7b | |||
| b3194236aa | |||
| 3fae693c42 | |||
| 0ad5c771e9 | |||
| 6727113b2b | |||
| f8587956ba | |||
| 951d368faf | |||
| 91d1f4675a | |||
| 9f1aef53fa | |||
| 1aa1a0476f | |||
| 7760ccefba | |||
| 51308b845b | |||
| 422fc2ad1e | |||
| e8bb874e14 | |||
| 353ad1f271 | |||
| f573e8a266 | |||
| 42bee7cf17 | |||
| 02d8bcac68 | |||
| 0b02d982b2 | |||
| e1e92f017c | |||
| e5f60fa54f | |||
| bf49e78243 | |||
| 72a40007ea | |||
| ac3bd9cdd0 | |||
| 88379b89f5 | |||
| 1283ff0db4 | |||
| 523209ec0b | |||
| a4a6143b6a | |||
| 1b2d639b14 | |||
| 9933970e67 | |||
| d7a3f01465 | |||
| 69259e6307 | |||
| 2f71d6b22d | |||
| 3ec527e42c | |||
| 2d16940232 | |||
| cba294ffa4 | |||
| 48406f85da | |||
| 14a1af132e | |||
| 59e24b98bd | |||
| 56067afca2 | |||
| 64f202fa17 | |||
| 6d7cf611c9 | |||
| 83ae8077f5 | |||
| 12cf348c80 | |||
| a5ee928990 | |||
| 7aff313a17 | |||
| 297767b2e3 | |||
| 99faab505c | |||
| 584c2668b3 | |||
| 27ea667aee | |||
| 4df8bcb1f5 | |||
| 36c890eaad | |||
| 44001244f2 | |||
| a89f95182c | |||
| 6a8a2992ff | |||
| 3d94dfe6a1 | |||
| 3670e24fa0 | |||
| b348ee726e | |||
| a941b65114 | |||
| 17101d425f | |||
| 52f1908126 | |||
| 48f1ae98ba | |||
| 97b3972c11 | |||
| 0f305d5397 | |||
| 01909f4715 | |||
| 770147264d | |||
| fa8c013b97 | |||
| 2710f2af06 | |||
| ad6d3e6bac | |||
| 2b0aa74e85 | |||
| b126d88963 | |||
| 45c1d3eab3 | |||
| 634181ba66 | |||
| eb89839d13 | |||
| fb7f0c3657 | |||
| b1f53d8d25 | |||
| 8824eeaea2 | |||
| 071589f7c7 | |||
| f1e992f6dd | |||
| 1ea5f776d7 | |||
| 4cdeb284ef | |||
| 5cccd39465 | |||
| 8196c22b5a | |||
| 65cc253c19 | |||
| 73cfa7b5b1 | |||
| 768325c9b4 | |||
| 932e329164 | |||
| 4bea299265 | |||
| 5cff83b282 | |||
| 7da79685ff | |||
| 91eff03418 | |||
| 52623820c2 | |||
| c2420642a0 | |||
| b3a1f7b0a3 | |||
| 94142aded5 | |||
| b158eb0c4c | |||
| e4b7dbcfb0 | |||
| 882edf11d7 | |||
| d34c2b8e06 | |||
| 442a52879b | |||
| 061e733007 | |||
| 92a9248083 | |||
| 83ff2461e2 | |||
| 8536c57645 | |||
| 15b5a43300 | |||
| e5ce8966d6 | |||
| 2dc30fc300 | |||
| b8ea55eb90 | |||
| 4eb056af9d | |||
| e49f279004 | |||
| 459cb38a6d | |||
| 18279ed17b | |||
| c7fb3ad9cf | |||
| d4a7df3083 | |||
| e83a1c6c84 | |||
| f5d096dd2b | |||
| e2d6f4e444 | |||
| d99fd60e06 | |||
| e4bae15825 | |||
| 58ead4116f | |||
| e136d1d47a | |||
| d2adebf26d | |||
| 36bc9dd261 | |||
| 879852c32a | |||
| 75faa5f2e5 | |||
| 4444ed166a | |||
| f86953ca56 | |||
| 3de36c99b6 | |||
| 50473bd2a8 | |||
| 1d3c85338c | |||
| 2fb018ced8 | |||
| 088af8edb2 | |||
| 612637f529 | |||
| 94e89a1045 | |||
| f7540ad355 | |||
| 096179a8c9 | |||
| f8734ffa43 | |||
| c58e223b3d | |||
| c46ef43590 | |||
| 775c6bc83d | |||
| 13799f425d | |||
| 8a090e59d7 | |||
| 9feda811a6 | |||
| 750e4a81bf | |||
| 32d3a6e091 | |||
| 351dbd6059 | |||
| d97ed57a19 | |||
| 2b427a7e89 | |||
| 3e6c75573f | |||
| 3f6a7cb250 | |||
| 37415d57d0 | |||
| d3bf09ef8e | |||
| 999418d0e9 | |||
| 97dd8c53c9 | |||
| 9c175b4faf | |||
| 08ac65362e | |||
| 16eaae306a | |||
| 35603d1c39 |
@@ -1,13 +1,21 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
|
- name: 💨 Performance Issues
|
||||||
|
url: https://github.com/slackhq/nebula/discussions/new/choose
|
||||||
|
about: 'We ask that you create a discussion instead of an issue for performance-related questions. This allows us to have a more open conversation about the issue and helps us to better understand the problem.'
|
||||||
|
|
||||||
|
- name: 📄 Documentation Issues
|
||||||
|
url: https://github.com/definednet/nebula-docs
|
||||||
|
about: "If you've found an issue with the website documentation, please file it in the nebula-docs repository."
|
||||||
|
|
||||||
|
- name: 📱 Mobile Nebula Issues
|
||||||
|
url: https://github.com/definednet/mobile_nebula
|
||||||
|
about: "If you're using the mobile Nebula app and have found an issue, please file it in the mobile_nebula repository."
|
||||||
|
|
||||||
- name: 📘 Documentation
|
- name: 📘 Documentation
|
||||||
url: https://nebula.defined.net/docs/
|
url: https://nebula.defined.net/docs/
|
||||||
about: Review documentation.
|
about: 'The documentation is the best place to start if you are new to Nebula.'
|
||||||
|
|
||||||
- name: 💁 Support/Chat
|
- name: 💁 Support/Chat
|
||||||
url: https://join.slack.com/t/nebulaoss/shared_invite/enQtOTA5MDI4NDg3MTg4LTkwY2EwNTI4NzQyMzc0M2ZlODBjNWI3NTY1MzhiOThiMmZlZjVkMTI0NGY4YTMyNjUwMWEyNzNkZTJmYzQxOGU
|
url: https://join.slack.com/t/nebulaoss/shared_invite/zt-39pk4xopc-CUKlGcb5Z39dQ0cK1v7ehA
|
||||||
about: 'This issue tracker is not for support questions. Join us on Slack for assistance!'
|
about: 'For faster support, join us on Slack for assistance!'
|
||||||
|
|
||||||
- name: 📱 Mobile Nebula
|
|
||||||
url: https://github.com/definednet/mobile_nebula
|
|
||||||
about: 'This issue tracker is not for mobile support. Try the Mobile Nebula repo instead!'
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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,11 @@
|
|||||||
|
<!--
|
||||||
|
Thank you for taking the time to submit a pull request!
|
||||||
|
|
||||||
|
Please be sure to provide a clear description of what you're trying to achieve with the change.
|
||||||
|
|
||||||
|
- If you're submitting a new feature, please explain how to use it and document any new config options in the example config.
|
||||||
|
- If you're submitting a bugfix, please link the related issue or describe the circumstances surrounding the issue.
|
||||||
|
- If you're changing a default, explain why you believe the new default is appropriate for most users.
|
||||||
|
|
||||||
|
P.S. If you're only updating the README or other docs, please file a pull request here instead: https://github.com/DefinedNet/nebula-docs
|
||||||
|
-->
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
name: gofmt
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/gofmt.yml'
|
|
||||||
- '**.go'
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
gofmt:
|
|
||||||
name: Run gofmt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
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
|
|
||||||
+118
-33
@@ -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@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.26'
|
||||||
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@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: linux-latest
|
name: linux-latest
|
||||||
path: release
|
path: release
|
||||||
@@ -32,12 +32,15 @@ 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@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -54,8 +57,15 @@ 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@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: windows-latest
|
name: windows-latest
|
||||||
path: build
|
path: build
|
||||||
@@ -63,27 +73,81 @@ jobs:
|
|||||||
build-darwin:
|
build-darwin:
|
||||||
name: Build Universal Darwin
|
name: Build Universal Darwin
|
||||||
env:
|
env:
|
||||||
HAS_SIGNING_CREDS: ${{ secrets.AC_USERNAME != '' }}
|
HAS_SIGNING_CREDS: ${{ secrets.APPLE_SIGNING_ROLE_ARN != '' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
|
# GitHub holds ARNs, not credentials, and ARNs outlive a rotation
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: ${{ secrets.APPLE_SIGNING_ROLE_ARN }}
|
||||||
|
aws-region: us-east-2
|
||||||
|
|
||||||
|
# parse-json-secrets unpacks into SIGNING_* and ASC_*, masked on the way in
|
||||||
|
- name: Fetch signing credentials
|
||||||
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
|
uses: aws-actions/aws-secretsmanager-get-secrets@v3
|
||||||
|
with:
|
||||||
|
parse-json-secrets: true
|
||||||
|
secret-ids: |
|
||||||
|
SIGNING,${{ secrets.APPLE_SIGNING_DEVELOPER_ID_ARN }}
|
||||||
|
ASC,${{ secrets.APPLE_NOTARY_KEY_ARN }}
|
||||||
|
|
||||||
- name: Import certificates
|
- name: Import certificates
|
||||||
if: env.HAS_SIGNING_CREDS == 'true'
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
uses: Apple-Actions/import-codesign-certs@v3
|
uses: Apple-Actions/import-codesign-certs@v7
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
p12-file-base64: ${{ env.SIGNING_P12_BASE64 }}
|
||||||
p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ env.SIGNING_PASSWORD }}
|
||||||
|
|
||||||
|
# The action imports but does not check the chain validates, which is how a p12
|
||||||
|
# missing its intermediate reaches a failing codesign
|
||||||
|
- name: Check the identity is usable
|
||||||
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
|
run: |
|
||||||
|
: "${SIGNING_IDENTITY_SHA1:?empty, so the secret has no identity_sha1}"
|
||||||
|
identities=$(security find-identity -v -p codesigning signing_temp.keychain)
|
||||||
|
case "$identities" in
|
||||||
|
*"$SIGNING_IDENTITY_SHA1"*) ;;
|
||||||
|
*) printf '%s\n' "$identities" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# notarytool wants the key as a file
|
||||||
|
- name: Write the App Store Connect key
|
||||||
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/private_keys
|
||||||
|
chmod 700 ~/private_keys
|
||||||
|
key_path="$HOME/private_keys/AuthKey_${ASC_KEY_ID}.p8"
|
||||||
|
(umask 077; printf '%s\n' "$ASC_PRIVATE_KEY" > "$key_path")
|
||||||
|
echo "ASC_P8=$key_path" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Drop the credentials from the environment
|
||||||
|
if: env.HAS_SIGNING_CREDS == 'true'
|
||||||
|
run: |
|
||||||
|
# The action's own inventory, so a new field in a secret is covered
|
||||||
|
python3 -c '
|
||||||
|
import json, os
|
||||||
|
raw = os.environ.get("SECRETS_LIST_CLEAN_UP")
|
||||||
|
if raw is None and os.environ.get("SIGNING_P12_BASE64"):
|
||||||
|
raise SystemExit("SECRETS_LIST_CLEAN_UP is gone, fetched secrets are not being scrubbed")
|
||||||
|
keep = {"SIGNING_IDENTITY_SHA1", "ASC_KEY_ID", "ASC_ISSUER_ID"}
|
||||||
|
names = [n for n in json.loads(raw or "[]") if n not in keep]
|
||||||
|
print("\n".join(f"{n}=" for n in dict.fromkeys(names)))
|
||||||
|
' >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Build, sign, and notarize
|
- name: Build, sign, and notarize
|
||||||
env:
|
|
||||||
AC_USERNAME: ${{ secrets.AC_USERNAME }}
|
|
||||||
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
|
|
||||||
run: |
|
run: |
|
||||||
rm -rf release
|
rm -rf release
|
||||||
mkdir release
|
mkdir release
|
||||||
@@ -92,19 +156,36 @@ jobs:
|
|||||||
lipo -create -output ./release/nebula ./build/darwin-amd64/nebula ./build/darwin-arm64/nebula
|
lipo -create -output ./release/nebula ./build/darwin-amd64/nebula ./build/darwin-arm64/nebula
|
||||||
lipo -create -output ./release/nebula-cert ./build/darwin-amd64/nebula-cert ./build/darwin-arm64/nebula-cert
|
lipo -create -output ./release/nebula-cert ./build/darwin-amd64/nebula-cert ./build/darwin-arm64/nebula-cert
|
||||||
|
|
||||||
if [ -n "$AC_USERNAME" ]; then
|
# Unset in a fork, which has no credentials to sign with
|
||||||
codesign -s "10BC1FDDEB6CE753550156C0669109FAC49E4D1E" -f -v --timestamp --options=runtime -i "net.defined.nebula" ./release/nebula
|
if [ -n "$SIGNING_IDENTITY_SHA1" ]; then
|
||||||
codesign -s "10BC1FDDEB6CE753550156C0669109FAC49E4D1E" -f -v --timestamp --options=runtime -i "net.defined.nebula-cert" ./release/nebula-cert
|
codesign -s "$SIGNING_IDENTITY_SHA1" -f -v --timestamp --options=runtime -i "net.defined.nebula" ./release/nebula
|
||||||
|
codesign -s "$SIGNING_IDENTITY_SHA1" -f -v --timestamp --options=runtime -i "net.defined.nebula-cert" ./release/nebula-cert
|
||||||
fi
|
fi
|
||||||
|
|
||||||
zip -j release/nebula-darwin.zip release/nebula-cert release/nebula
|
zip -j release/nebula-darwin.zip release/nebula-cert release/nebula
|
||||||
|
|
||||||
if [ -n "$AC_USERNAME" ]; then
|
if [ -n "$ASC_P8" ]; then
|
||||||
xcrun notarytool submit ./release/nebula-darwin.zip --team-id "576H3XS7FP" --apple-id "$AC_USERNAME" --password "$AC_PASSWORD" --wait
|
xcrun notarytool submit ./release/nebula-darwin.zip --key "$ASC_P8" --key-id "$ASC_KEY_ID" --issuer "$ASC_ISSUER_ID" --wait
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Drop the signing key
|
||||||
|
if: always() && env.HAS_SIGNING_CREDS == 'true'
|
||||||
|
run: |
|
||||||
|
# Locked, not deleted: import-codesign-certs deletes it in its own post
|
||||||
|
# step and fails the job if it is already gone. Locked is unusable.
|
||||||
|
security lock-keychain signing_temp.keychain || true
|
||||||
|
rm -f "$ASC_P8"
|
||||||
|
# Nothing later in this job needs AWS
|
||||||
|
python3 -c '
|
||||||
|
import json, os
|
||||||
|
names = json.loads(os.environ.get("SECRETS_LIST_CLEAN_UP") or "[]")
|
||||||
|
names += ["ASC_P8", "SIGNING_IDENTITY_SHA1", "ASC_KEY_ID", "ASC_ISSUER_ID",
|
||||||
|
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]
|
||||||
|
print("\n".join(f"{n}=" for n in dict.fromkeys(names)))
|
||||||
|
' >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: darwin-latest
|
name: darwin-latest
|
||||||
path: ./release/*
|
path: ./release/*
|
||||||
@@ -124,25 +205,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@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
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@v3
|
uses: docker/login-action@v4
|
||||||
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@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Build and push images
|
- name: Build and push images
|
||||||
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
if: ${{ env.HAS_DOCKER_CREDS == 'true' }}
|
||||||
@@ -153,17 +234,20 @@ 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 --tag "${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG}" --tag "${DOCKER_IMAGE_REPO}:${GITHUB_REF#refs/tags/v}"
|
docker buildx build . --push -f docker/Dockerfile --platform linux/amd64,linux/arm64 \
|
||||||
|
--build-arg VERSION="${GITHUB_REF#refs/tags/v}" \
|
||||||
|
--build-arg REVISION="${GITHUB_SHA}" \
|
||||||
|
--tag "${DOCKER_IMAGE_REPO}:${DOCKER_IMAGE_TAG}" --tag "${DOCKER_IMAGE_REPO}:${GITHUB_REF#refs/tags/v}"
|
||||||
|
|
||||||
release:
|
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@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
@@ -209,10 +293,11 @@ 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,35 +14,119 @@ on:
|
|||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
smoke-extra:
|
smoke-extra-libvirt:
|
||||||
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: Run extra smoke tests
|
name: ${{ matrix.target }}
|
||||||
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@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: install vagrant
|
- name: add hashicorp source
|
||||||
run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox
|
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: freebsd-amd64
|
- name: install vagrant and libvirt
|
||||||
run: make smoke-vagrant/freebsd-amd64
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y vagrant libvirt-daemon-system libvirt-dev
|
||||||
|
sudo chmod 666 /dev/kvm
|
||||||
|
sudo usermod -aG libvirt $(whoami)
|
||||||
|
sudo chmod 666 /var/run/libvirt/libvirt-sock
|
||||||
|
vagrant plugin install vagrant-libvirt
|
||||||
|
|
||||||
- name: openbsd-amd64
|
- name: ${{ matrix.target }}
|
||||||
run: make smoke-vagrant/openbsd-amd64
|
run: make smoke-vagrant/${{ matrix.target }}
|
||||||
|
|
||||||
- name: netbsd-amd64
|
timeout-minutes: 30
|
||||||
run: make smoke-vagrant/netbsd-amd64
|
|
||||||
|
# linux-386 needs VirtualBox, which conflicts with KVM/libvirt -- isolated job.
|
||||||
|
smoke-extra-virtualbox:
|
||||||
|
if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'smoke-test-extra')
|
||||||
|
name: linux-386
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
VAGRANT_DEFAULT_PROVIDER: virtualbox
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: add hashicorp source
|
||||||
|
run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
|
||||||
|
- name: install vagrant and virtualbox
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y vagrant virtualbox
|
||||||
|
sudo rmmod kvm_amd kvm_intel kvm 2>/dev/null || true
|
||||||
|
|
||||||
- name: 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@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
@@ -36,6 +36,14 @@ 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,8 +16,10 @@ relay:
|
|||||||
am_relay: true
|
am_relay: true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
export LIGHTHOUSES="192.168.100.1 172.17.0.2:4242"
|
# TEST-NET-3 placeholder IPs; smoke-relay.sh seds them to real container IPs.
|
||||||
export REMOTE_ALLOW_LIST='{"172.17.0.4/32": false, "172.17.0.5/32": false}'
|
# Mapping: .2 lighthouse1, .3 host2, .4 host3, .5 host4.
|
||||||
|
export LIGHTHOUSES="192.168.100.1 203.0.113.2:4242"
|
||||||
|
export REMOTE_ALLOW_LIST='{"203.0.113.4/32": false, "203.0.113.5/32": false}'
|
||||||
|
|
||||||
HOST="host2" ../genconfig.sh >host2.yml <<EOF
|
HOST="host2" ../genconfig.sh >host2.yml <<EOF
|
||||||
relay:
|
relay:
|
||||||
@@ -25,7 +27,7 @@ relay:
|
|||||||
- 192.168.100.1
|
- 192.168.100.1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
export REMOTE_ALLOW_LIST='{"172.17.0.3/32": false}'
|
export REMOTE_ALLOW_LIST='{"203.0.113.3/32": false}'
|
||||||
|
|
||||||
HOST="host3" ../genconfig.sh >host3.yml
|
HOST="host3" ../genconfig.sh >host3.yml
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,29 @@ set -e -x
|
|||||||
rm -rf ./build
|
rm -rf ./build
|
||||||
mkdir ./build
|
mkdir ./build
|
||||||
|
|
||||||
|
if [ "$SMOKE_OVERLAY_IPV6" ]
|
||||||
|
then
|
||||||
|
LIGHTHOUSE_NIP="fd00:4242:0:0:0:ffff:c0a8:6401"
|
||||||
|
HOST2_NIP="fd00:4242:0:0:0:ffff:c0a8:6402"
|
||||||
|
HOST3_NIP="fd00:4242:0:0:0:ffff:c0a8:6403"
|
||||||
|
HOST4_NIP="fd00:4242:0:0:0:ffff:c0a8:6404"
|
||||||
|
else
|
||||||
|
LIGHTHOUSE_NIP="192.168.100.1"
|
||||||
|
HOST2_NIP="192.168.100.2"
|
||||||
|
HOST3_NIP="192.168.100.3"
|
||||||
|
HOST4_NIP="192.168.100.4"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Smoke containers run on a dedicated docker network whose subnet is allocated
|
||||||
|
# at smoke time, not known at build time. Configs are written with TEST-NET-3
|
||||||
|
# placeholder IPs (RFC 5737) and smoke.sh / smoke-vagrant.sh / smoke-relay.sh
|
||||||
|
# sed the real container IPs in before starting nebula.
|
||||||
|
#
|
||||||
|
# Placeholder mapping (last octet == fixed container slot):
|
||||||
|
# 203.0.113.2 -> lighthouse1, 203.0.113.3 -> host2,
|
||||||
|
# 203.0.113.4 -> host3, 203.0.113.5 -> host4.
|
||||||
|
LIGHTHOUSE_IP="203.0.113.2"
|
||||||
|
|
||||||
(
|
(
|
||||||
cd build
|
cd build
|
||||||
|
|
||||||
@@ -21,24 +44,24 @@ mkdir ./build
|
|||||||
../genconfig.sh >lighthouse1.yml
|
../genconfig.sh >lighthouse1.yml
|
||||||
|
|
||||||
HOST="host2" \
|
HOST="host2" \
|
||||||
LIGHTHOUSES="192.168.100.1 172.17.0.2:4242" \
|
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
|
||||||
../genconfig.sh >host2.yml
|
../genconfig.sh >host2.yml
|
||||||
|
|
||||||
HOST="host3" \
|
HOST="host3" \
|
||||||
LIGHTHOUSES="192.168.100.1 172.17.0.2:4242" \
|
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP: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="192.168.100.1 172.17.0.2:4242" \
|
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP: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 "192.168.100.1/24"
|
../../../../nebula-cert sign -name "lighthouse1" -groups "lighthouse,lighthouse1" -ip "$LIGHTHOUSE_NIP/24"
|
||||||
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "192.168.100.2/24"
|
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "$HOST2_NIP/24"
|
||||||
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "192.168.100.3/24"
|
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "$HOST3_NIP/24"
|
||||||
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "192.168.100.4/24"
|
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "$HOST4_NIP/24"
|
||||||
)
|
)
|
||||||
|
|
||||||
docker build -t "nebula:${NAME:-smoke}" .
|
docker build -t "nebula:${NAME:-smoke}" .
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ set -o pipefail
|
|||||||
|
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
|
|
||||||
|
NETWORK="nebula-smoke-relay"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo
|
echo
|
||||||
echo " *** cleanup"
|
echo " *** cleanup"
|
||||||
@@ -16,22 +18,53 @@ 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
|
||||||
|
|
||||||
docker run --name lighthouse1 --rm nebula:smoke-relay -config lighthouse1.yml -test
|
# Create a dedicated smoke network with an explicit subnet (required for --ip
|
||||||
docker run --name host2 --rm nebula:smoke-relay -config host2.yml -test
|
# below). Probe a short list of candidates so a locally-used range doesn't
|
||||||
docker run --name host3 --rm nebula:smoke-relay -config host3.yml -test
|
# fail the whole test — we only need one to be free.
|
||||||
docker run --name host4 --rm nebula:smoke-relay -config host4.yml -test
|
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
|
||||||
|
|
||||||
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] /' &
|
# Derive container IPs from the network's assigned subnet. Slots: .2 lighthouse1,
|
||||||
|
# .3 host2, .4 host3, .5 host4 — matches the placeholders in build-relay.sh.
|
||||||
|
SUBNET="$(docker network inspect -f '{{(index .IPAM.Config 0).Subnet}}' "$NETWORK")"
|
||||||
|
PREFIX="${SUBNET%/*}"
|
||||||
|
PREFIX="${PREFIX%.*}"
|
||||||
|
LIGHTHOUSE_IP="$PREFIX.2"
|
||||||
|
HOST2_IP="$PREFIX.3"
|
||||||
|
HOST3_IP="$PREFIX.4"
|
||||||
|
HOST4_IP="$PREFIX.5"
|
||||||
|
|
||||||
|
# Sed the placeholder TEST-NET-3 IPs in the host configs to the real ones.
|
||||||
|
for f in build/host2.yml build/host3.yml build/host4.yml; do
|
||||||
|
sed "s|203\.0\.113\.|$PREFIX.|g" "$f" >"$f.tmp"
|
||||||
|
mv "$f.tmp" "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
docker run --name lighthouse1 --rm nebula:smoke-relay -config lighthouse1.yml -test
|
||||||
|
docker run --name host2 --rm -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" nebula:smoke-relay -config host2.yml -test
|
||||||
|
docker run --name host3 --rm -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" nebula:smoke-relay -config host3.yml -test
|
||||||
|
docker run --name host4 --rm -v "$PWD/build/host4.yml:/nebula/host4.yml:ro" nebula:smoke-relay -config host4.yml -test
|
||||||
|
|
||||||
|
docker run --name 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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
@@ -76,7 +109,13 @@ 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,6 +8,8 @@ export VAGRANT_CWD="$PWD/vagrant-$1"
|
|||||||
|
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
|
|
||||||
|
NETWORK="nebula-smoke"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo
|
echo
|
||||||
echo " *** cleanup"
|
echo " *** cleanup"
|
||||||
@@ -19,23 +21,53 @@ 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 "$CONTAINER" -config host2.yml -test
|
docker run --name host2 --rm -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" "$CONTAINER" -config host2.yml -test
|
||||||
|
|
||||||
vagrant up
|
vagrant up
|
||||||
vagrant ssh -c "cd /nebula && /nebula/$1-nebula -config host3.yml -test"
|
vagrant ssh -c "cd /nebula && /nebula/$1-nebula -config host3.yml -test" -- -T
|
||||||
|
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
vagrant ssh -c "cd /nebula && sudo sh -c 'echo \$\$ >/nebula/pid && exec /nebula/$1-nebula -config host3.yml'" &
|
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
|
||||||
|
|
||||||
# grab tcpdump pcaps for debugging
|
# grab tcpdump pcaps for debugging
|
||||||
@@ -46,8 +78,8 @@ docker exec host2 tcpdump -i eth0 -q -w - -U 2>logs/host2.outside.log >logs/host
|
|||||||
# vagrant ssh -c "tcpdump -i nebula1 -q -w - -U" 2>logs/host3.inside.log >logs/host3.inside.pcap &
|
# vagrant ssh -c "tcpdump -i nebula1 -q -w - -U" 2>logs/host3.inside.log >logs/host3.inside.pcap &
|
||||||
# vagrant ssh -c "tcpdump -i eth0 -q -w - -U" 2>logs/host3.outside.log >logs/host3.outside.pcap &
|
# vagrant ssh -c "tcpdump -i eth0 -q -w - -U" 2>logs/host3.outside.log >logs/host3.outside.pcap &
|
||||||
|
|
||||||
docker exec host2 ncat -nklv 0.0.0.0 2000 &
|
#docker exec host2 ncat -nklv 0.0.0.0 2000 &
|
||||||
vagrant ssh -c "ncat -nklv 0.0.0.0 2000" &
|
#vagrant ssh -c "ncat -nklv 0.0.0.0 2000" &
|
||||||
#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 0.0.0.0 3000 &
|
||||||
#vagrant ssh -c "ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000" &
|
#vagrant ssh -c "ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000" &
|
||||||
|
|
||||||
@@ -68,11 +100,11 @@ 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 192.168.100.3 -w5 || exit 1
|
! docker exec host2 ping -c1 192.168.100.3 -w5 || exit 1
|
||||||
|
|
||||||
set +x
|
#set +x
|
||||||
echo
|
#echo
|
||||||
echo " *** Testing ncat from host2"
|
#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 192.168.100.3 2000 || exit 1
|
#! docker exec host2 ncat -nzv -w5 192.168.100.3 2000 || exit 1
|
||||||
#! docker exec host2 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
#! docker exec host2 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
||||||
@@ -82,21 +114,28 @@ echo
|
|||||||
echo " *** Testing ping from host3"
|
echo " *** Testing ping from host3"
|
||||||
echo
|
echo
|
||||||
set -x
|
set -x
|
||||||
vagrant ssh -c "ping -c1 192.168.100.1"
|
vagrant ssh -c "ping -c1 192.168.100.1" -- -T
|
||||||
vagrant ssh -c "ping -c1 192.168.100.2"
|
vagrant ssh -c "ping -c1 192.168.100.2" -- -T
|
||||||
|
|
||||||
set +x
|
#set +x
|
||||||
echo
|
#echo
|
||||||
echo " *** Testing ncat from host3"
|
#echo " *** Testing ncat from host3"
|
||||||
echo
|
#echo
|
||||||
set -x
|
#set -x
|
||||||
#vagrant ssh -c "ncat -nzv -w5 192.168.100.2 2000"
|
#vagrant ssh -c "ncat -nzv -w5 192.168.100.2 2000"
|
||||||
#vagrant ssh -c "ncat -nzuv -w5 192.168.100.2 3000" | grep -q host2
|
#vagrant ssh -c "ncat -nzuv -w5 192.168.100.2 3000" | grep -q host2
|
||||||
|
|
||||||
vagrant ssh -c "sudo xargs kill </nebula/pid"
|
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
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
#!/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,6 +6,8 @@ set -o pipefail
|
|||||||
|
|
||||||
mkdir -p logs
|
mkdir -p logs
|
||||||
|
|
||||||
|
NETWORK="nebula-smoke"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo
|
echo
|
||||||
echo " *** cleanup"
|
echo " *** cleanup"
|
||||||
@@ -16,57 +18,103 @@ 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 "$CONTAINER" -config host2.yml -test
|
docker run --name host2 --rm -v "$PWD/build/host2.yml:/nebula/host2.yml:ro" "$CONTAINER" -config host2.yml -test
|
||||||
docker run --name host3 --rm "$CONTAINER" -config host3.yml -test
|
docker run --name host3 --rm -v "$PWD/build/host3.yml:/nebula/host3.yml:ro" "$CONTAINER" -config host3.yml -test
|
||||||
docker run --name host4 --rm "$CONTAINER" -config host4.yml -test
|
docker run --name host4 --rm -v "$PWD/build/host4.yml:/nebula/host4.yml:ro" "$CONTAINER" -config host4.yml -test
|
||||||
|
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
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] /' &
|
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] /' &
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
# grab tcpdump pcaps for debugging
|
# grab tcpdump pcaps for debugging
|
||||||
docker exec lighthouse1 tcpdump -i nebula1 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap &
|
docker exec lighthouse1 tcpdump -i tun0 -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 nebula1 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap &
|
docker exec host2 tcpdump -i tun0 -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 nebula1 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap &
|
docker exec host3 tcpdump -i tun0 -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 nebula1 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
|
docker exec host4 tcpdump -i tun0 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
|
||||||
docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
|
docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
|
||||||
|
|
||||||
docker exec host2 ncat -nklv 0.0.0.0 2000 &
|
docker exec host2 ncat -nklv 2000 &
|
||||||
docker exec host3 ncat -nklv 0.0.0.0 2000 &
|
docker exec host3 ncat -nklv 2000 &
|
||||||
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
|
docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 4000 &
|
||||||
docker exec host3 ncat -e '/usr/bin/echo host3' -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 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 192.168.100.2
|
docker exec lighthouse1 ping -c1 $HOST2_NIP
|
||||||
docker exec lighthouse1 ping -c1 192.168.100.3
|
docker exec lighthouse1 ping -c1 $HOST3_NIP
|
||||||
|
|
||||||
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 192.168.100.1
|
docker exec host2 ping -c1 $LIGHTHOUSE_NIP
|
||||||
# Should fail because not allowed by host3 inbound firewall
|
# Should fail because not allowed by host3 inbound firewall
|
||||||
! docker exec host2 ping -c1 192.168.100.3 -w5 || exit 1
|
! docker exec host2 ping -c1 $HOST3_NIP -w5 || exit 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
@@ -74,34 +122,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 192.168.100.3 2000 || exit 1
|
! docker exec host2 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
|
||||||
! docker exec host2 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
! docker exec host2 ncat -nzuv -w5 $HOST3_NIP 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 192.168.100.1
|
docker exec host3 ping -c1 $LIGHTHOUSE_NIP
|
||||||
docker exec host3 ping -c1 192.168.100.2
|
docker exec host3 ping -c1 $HOST2_NIP
|
||||||
|
|
||||||
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 192.168.100.2 2000
|
docker exec host3 ncat -nzv -w5 $HOST2_NIP 2000
|
||||||
docker exec host3 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2
|
docker exec host3 ncat -nzuv -w5 $HOST2_NIP 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 192.168.100.1
|
docker exec host4 ping -c1 $LIGHTHOUSE_NIP
|
||||||
# Should fail because not allowed by host4 outbound firewall
|
# Should fail because not allowed by host4 outbound firewall
|
||||||
! docker exec host4 ping -c1 192.168.100.2 -w5 || exit 1
|
! docker exec host4 ping -c1 $HOST2_NIP -w5 || exit 1
|
||||||
! docker exec host4 ping -c1 192.168.100.3 -w5 || exit 1
|
! docker exec host4 ping -c1 $HOST3_NIP -w5 || exit 1
|
||||||
|
|
||||||
set +x
|
set +x
|
||||||
echo
|
echo
|
||||||
@@ -109,27 +157,34 @@ 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 192.168.100.2 2000 || exit 1
|
! docker exec host4 ncat -nzv -w5 $HOST2_NIP 2000 || exit 1
|
||||||
! docker exec host4 ncat -nzv -w5 192.168.100.3 2000 || exit 1
|
! docker exec host4 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
|
||||||
! docker exec host4 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2 || exit 1
|
! docker exec host4 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2 || exit 1
|
||||||
! docker exec host4 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
|
! docker exec host4 ncat -nzuv -w5 $HOST3_NIP 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
|
|
||||||
docker exec host2 ping -c1 192.168.100.3
|
# host4's outbound firewall only allows ICMP to the lighthouse, so host4
|
||||||
# host4 can ping host2 once conntrack established
|
# cannot initiate UDP to host2. Once host2 initiates a flow to host4:4000,
|
||||||
docker exec host2 ping -c1 192.168.100.4
|
# conntrack must let host4's listener reply on that flow. If it doesn't,
|
||||||
docker exec host4 ping -c1 192.168.100.2
|
# the echo back from host4 never reaches host2.
|
||||||
|
docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv $HOST4_NIP 4000" | grep -q helloagainfromhost4
|
||||||
|
|
||||||
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 = "ubuntu/jammy64"
|
config.vm.box = "bento/ubuntu-24.04"
|
||||||
|
|
||||||
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 = "generic/netbsd9"
|
config.vm.box = "DefinedNet/netbsd10"
|
||||||
|
|
||||||
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 = "generic/openbsd7"
|
config.vm.box = "DefinedNet/openbsd78"
|
||||||
|
|
||||||
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
config.vm.synced_folder "../build", "/nebula", type: "rsync"
|
||||||
end
|
end
|
||||||
|
|||||||
+104
-58
@@ -13,92 +13,138 @@ on:
|
|||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
test-linux:
|
static:
|
||||||
name: Build all and test on ubuntu-linux
|
name: Static checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build
|
- name: Install goimports
|
||||||
run: make all
|
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
|
||||||
|
- name: gofmt
|
||||||
|
run: |
|
||||||
|
if [ "$(find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -l)" ]
|
||||||
|
then
|
||||||
|
find . -iname '*.go' | grep -v '\.pb\.go$' | xargs goimports -d
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Vet
|
- name: Vet
|
||||||
run: make vet
|
run: make vet
|
||||||
|
|
||||||
- name: Test
|
- name: golangci-lint
|
||||||
run: make test
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
|
||||||
- name: End 2 end
|
|
||||||
run: make e2evv
|
|
||||||
|
|
||||||
- name: Build test mobile
|
|
||||||
run: make build-test-mobile
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: e2e packet flow linux-latest
|
version: v2.12
|
||||||
path: e2e/mermaid/linux-latest
|
|
||||||
if-no-files-found: warn
|
|
||||||
|
|
||||||
test-linux-boringcrypto:
|
|
||||||
name: Build and test on linux with boringcrypto
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
check-latest: true
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: make bin-boringcrypto
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: make test-boringcrypto
|
|
||||||
|
|
||||||
- name: End 2 end
|
|
||||||
run: make e2evv GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Build and test on ${{ matrix.os }}
|
name: Test ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, macos-latest]
|
include:
|
||||||
|
- name: linux
|
||||||
|
os: ubuntu-latest
|
||||||
|
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
||||||
|
test-cmd: make test
|
||||||
|
e2e-cmd: make e2evv
|
||||||
|
- name: linux-boringcrypto
|
||||||
|
os: ubuntu-latest
|
||||||
|
build-cmd: make bin-boringcrypto
|
||||||
|
test-cmd: make test-boringcrypto
|
||||||
|
e2e-cmd: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
||||||
|
- name: linux-pkcs11
|
||||||
|
os: ubuntu-latest
|
||||||
|
build-cmd: make bin-pkcs11
|
||||||
|
test-cmd: make test-pkcs11
|
||||||
|
e2e-cmd: ''
|
||||||
|
- name: macos
|
||||||
|
os: macos-latest
|
||||||
|
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
||||||
|
test-cmd: make test
|
||||||
|
e2e-cmd: make e2evv
|
||||||
|
- name: windows
|
||||||
|
os: windows-latest
|
||||||
|
build-cmd: go build ./cmd/nebula ./cmd/nebula-cert
|
||||||
|
test-cmd: make test
|
||||||
|
e2e-cmd: make e2evv
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: '1.22'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Build nebula
|
- name: Build
|
||||||
run: go build ./cmd/nebula
|
run: ${{ matrix.build-cmd }}
|
||||||
|
|
||||||
- name: Build nebula-cert
|
- name: Cross-build darwin-amd64
|
||||||
run: go build ./cmd/nebula-cert
|
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: Vet
|
|
||||||
run: make vet
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: make test
|
run: ${{ matrix.test-cmd }}
|
||||||
|
|
||||||
- name: End 2 end
|
- name: End 2 end
|
||||||
run: make e2evv
|
if: matrix.e2e-cmd != ''
|
||||||
|
run: ${{ matrix.e2e-cmd }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
|
if: matrix.e2e-cmd != '' && always()
|
||||||
with:
|
with:
|
||||||
name: e2e packet flow ${{ matrix.os }}
|
name: e2e packet flow ${{ matrix.name }}
|
||||||
path: e2e/mermaid/${{ matrix.os }}
|
path: e2e/mermaid/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
cross-build:
|
||||||
|
name: Cross-build ${{ matrix.name }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- {name: linux-arm, make-target: all-cross-linux-arm}
|
||||||
|
- {name: linux-mips, make-target: all-cross-linux-mips}
|
||||||
|
- {name: linux-other, make-target: all-cross-linux-other}
|
||||||
|
- {name: freebsd, make-target: all-freebsd}
|
||||||
|
- {name: openbsd, make-target: all-openbsd}
|
||||||
|
- {name: netbsd, make-target: all-netbsd}
|
||||||
|
- {name: windows, make-target: all-cross-windows}
|
||||||
|
- {name: mobile, make-target: build-test-mobile}
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.name }}
|
||||||
|
run: make -j"$(nproc)" ${{ matrix.make-target }}
|
||||||
|
|
||||||
|
finish:
|
||||||
|
name: CI status
|
||||||
|
if: always()
|
||||||
|
needs: [static, test, cross-build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Fail if any upstream job failed
|
||||||
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: |
|
||||||
|
echo "upstream results: ${{ toJSON(needs) }}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: All upstream jobs passed
|
||||||
|
run: echo "ok"
|
||||||
|
|||||||
+3
-1
@@ -5,7 +5,8 @@
|
|||||||
/nebula-darwin
|
/nebula-darwin
|
||||||
/nebula.exe
|
/nebula.exe
|
||||||
/nebula-cert.exe
|
/nebula-cert.exe
|
||||||
/coverage.out
|
**/coverage.out
|
||||||
|
**/cover.out
|
||||||
/cpu.pprof
|
/cpu.pprof
|
||||||
/build
|
/build
|
||||||
/*.tar.gz
|
/*.tar.gz
|
||||||
@@ -13,5 +14,6 @@
|
|||||||
**.crt
|
**.crt
|
||||||
**.key
|
**.key
|
||||||
**.pem
|
**.pem
|
||||||
|
**.pub
|
||||||
!/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.key
|
!/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.key
|
||||||
!/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.crt
|
!/examples/quickstart-vagrant/ansible/roles/nebula/files/vagrant-test-ca.crt
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
version: "2"
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- sloglint
|
||||||
|
- testifylint
|
||||||
|
settings:
|
||||||
|
sloglint:
|
||||||
|
# Enforce key-value pair form for Info/Debug/Warn/Error/Log/With and
|
||||||
|
# the package-level slog equivalents. Use l.Log(ctx, level, ...) for
|
||||||
|
# custom levels instead of LogAttrs when you can.
|
||||||
|
#
|
||||||
|
# LogAttrs is also flagged by this rule because it takes ...slog.Attr;
|
||||||
|
# the few legitimate sites (where attrs is built up as a []slog.Attr)
|
||||||
|
# carry a //nolint:sloglint with rationale.
|
||||||
|
kv-only: true
|
||||||
|
# no-mixed-args is on by default: forbids mixing kv and attrs in one call.
|
||||||
|
# discard-handler is on by default (since Go 1.24): suggests
|
||||||
|
# slog.DiscardHandler over slog.NewTextHandler(io.Discard, nil).
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
+189
-1
@@ -7,6 +7,189 @@ 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
|
||||||
|
|
||||||
|
- Reload the firewall when the unsafe networks in the certificate change. (#1719)
|
||||||
|
- Reconfigure, start, and stop the stats listener on a config reload instead of requiring a restart. (#1670)
|
||||||
|
- Update a static host's addresses when they change on reload. (#1713)
|
||||||
|
- Don't require a port on ICMP firewall rules. (#1609)
|
||||||
|
- Connection track ICMP traffic. (#1602)
|
||||||
|
- Return `NODATA` instead of `NXDOMAIN` from the DNS server for a name that exists but has no record of the
|
||||||
|
requested type, so clients that query `AAAA` first (busybox/Alpine) fall through to `A`. (#1668)
|
||||||
|
- Record the local host's details in the DNS server. (#1716)
|
||||||
|
- Install Windows unsafe routes as link routes. (#1709)
|
||||||
|
- Reduce relay handshake log spam, and only log a handshake send error at error level when the remote list
|
||||||
|
changes. (#1733, #1765, #1810)
|
||||||
|
- Start, stop, and reload subsystems (DNS, stats, conntrack, ssh, punchy) cleanly without leaking goroutines. (#1640, #1654, #1661, #1667, #1669, #1708, #1806, #1815)
|
||||||
|
- `Control` is now safe to stop and wait on from any lifecycle state, and a new `Control.Wait` blocks until nebula
|
||||||
|
has fully stopped and returns the first fatal reader error. Failed starts release the udp sockets and tun fd
|
||||||
|
instead of leaking them. (#1794)
|
||||||
|
- Trigger an immediate lighthouse update when reconnecting to or adding a lighthouse instead of waiting for the next update tick. (#1645)
|
||||||
|
- Bring the Darwin and OpenBSD tun implementations in line with the other BSDs. (#1703)
|
||||||
|
- Update to build against go v1.26. (#1818)
|
||||||
|
- Various dependency updates. (#1586, #1587, #1604, #1617, #1618, #1627, #1628, #1629, #1652, #1664, #1665, #1697, #1721, #1732, #1742, #1743, #1750, #1763, #1771, #1782, #1800, #1807)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix a data race on a host's remote address that could send packets to the wrong address during a roam. (#1773)
|
||||||
|
- Fix tunnels that could permanently escape connection manager monitoring. (#1752)
|
||||||
|
- Fix a crash when reloading the SSH server's trusted keys. (#1787)
|
||||||
|
- Fix hostmap corruption when a host has multiple overlay addresses. Each address now gets its own list instead of
|
||||||
|
a single shared chain, which also fixes two latent bugs on the add and makePrimary paths. (#1788, #1790)
|
||||||
|
- Apply `remote_allow_list` IPv4 rules to 4-in-6 mapped addresses. (#1786)
|
||||||
|
- Don't panic in the DNS server on a short or empty query name. (#1635)
|
||||||
|
- Advance the replay window on relayed packets so a relay drops replayed frames instead of re-forwarding them. (#1751)
|
||||||
|
- Fix a race in relay state handling. (#1753)
|
||||||
|
- Lock replay window updates so concurrent readers can't corrupt it. (#1802)
|
||||||
|
- Reject malformed handshakes more reliably, including invalid ed25519 key lengths. (#1601, #1756)
|
||||||
|
- Properly handle `closetunnel` packets. (#1638)
|
||||||
|
- Fix an IPv6 extension-header length overflow that could make the firewall parse the wrong protocol and ports. (#1789)
|
||||||
|
- Fix relay re-establishment when a handshake arrives over a relay entry that a one-sided teardown left
|
||||||
|
`Disestablished`, which silently dropped every send until dead tunnel detection forced a re-handshake. (#1805)
|
||||||
|
- Don't build new relay state on a tunnel that was just discarded. (#1796)
|
||||||
|
- Don't delete the wrong pending hostinfo in the handshake manager. (#1811)
|
||||||
|
- Don't call the packet reader after a UDP error on Darwin. (#1755)
|
||||||
|
- Open the FreeBSD tun device non blocking. (#1666)
|
||||||
|
|
||||||
|
## [1.10.3] - 2026-02-06
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix an issue where blocklist bypass is possible when using curve P256 since the signature can have 2 valid representations.
|
||||||
|
Both fingerprint representations will be tested against the blocklist.
|
||||||
|
Any newly issued P256 based certificates will have their signature clamped to the low-s form.
|
||||||
|
Nebula will assert the low-s signature form when validating certificates in a future version. [GHSA-69x3-g4r3-p962](https://github.com/slackhq/nebula/security/advisories/GHSA-69x3-g4r3-p962)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improve error reporting if nebula fails to start due to a tun device naming issue. (#1588)
|
||||||
|
|
||||||
|
## [1.10.2] - 2026-01-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix panic when using `use_system_route_table` that was introduced in v1.10.1. (#1580)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fix some typos in comments. (#1582)
|
||||||
|
- Dependency updates. (#1581)
|
||||||
|
|
||||||
|
## [1.10.1] - 2026-01-16
|
||||||
|
|
||||||
|
See the [v1.10.1](https://github.com/slackhq/nebula/milestone/26?closed=1) milestone for a complete list of changes.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix a bug where an unsafe route derived from the system route table could be lost on a config reload. (#1573)
|
||||||
|
- Fix the PEM banner for ECDSA P256 public keys. (#1552)
|
||||||
|
- Fix a regression on Windows from 1.9.x where nebula could fall back to a less performant UDP listener if
|
||||||
|
non-critical ioctls failed. (#1568)
|
||||||
|
- Fix a bug in handshake processing when a peer sends an unexpected public key. (#1566)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add a config option to control accepting `recv_error` packets which defaults to `always`. (#1569)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Various dependency updates. (#1541, #1549, #1550, #1557, #1558, #1560, #1561, #1570, #1571)
|
||||||
|
|
||||||
|
## [1.10.0] - 2025-12-04
|
||||||
|
|
||||||
|
See the [v1.10.0](https://github.com/slackhq/nebula/milestone/16?closed=1) milestone for a complete list of changes.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for ipv6 and multiple ipv4/6 addresses in the overlay.
|
||||||
|
A new v2 ASN.1 based certificate format.
|
||||||
|
Certificates now have a unified interface for external implementations.
|
||||||
|
(#1212, #1216, #1345, #1359, #1381, #1419, #1464, #1466, #1451, #1476, #1467, #1481, #1399, #1488, #1492, #1495, #1468, #1521, #1535, #1538)
|
||||||
|
- Add the ability to mark packets on linux to better target nebula packets in iptables/nftables. (#1331)
|
||||||
|
- Add ECMP support for `unsafe_routes`. (#1332)
|
||||||
|
- PKCS11 support for P256 keys when built with `pkcs11` tag (#1153, #1482)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **NOTE**: `default_local_cidr_any` now defaults to false, meaning that any firewall rule
|
||||||
|
intended to target an `unsafe_routes` entry must explicitly declare it via the
|
||||||
|
`local_cidr` field. This is almost always the intended behavior. This flag is
|
||||||
|
deprecated and will be removed in a future release. (#1373)
|
||||||
|
- Improve logging when a relay is in use on an inbound packet. (#1533)
|
||||||
|
- Avoid fatal errors if `rountines` is > 1 on systems that don't support more than 1 routine. (#1531)
|
||||||
|
- Log a warning if a firewall rule contains an `any` that negates a more restrictive filter. (#1513)
|
||||||
|
- Accept encrypted CA passphrase from an environment variable. (#1421)
|
||||||
|
- Allow handshaking with any trusted remote. (#1509)
|
||||||
|
- Log only the count of blocklisted certificate fingerprints instead of the entire list. (#1525)
|
||||||
|
- Don't fatal when the ssh server is unable to be configured successfully. (#1520)
|
||||||
|
- Update to build against go v1.25. (#1483)
|
||||||
|
- Allow projects using `nebula` as a library with userspace networking to configure the `logger` and build version. (#1239)
|
||||||
|
- Upgrade to `yaml.v3`. (#1148, #1371, #1438, #1478)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix a potential bug with udp ipv4 only on darwin. (#1532)
|
||||||
|
- Improve lost packet statistics. (#1441, #1537)
|
||||||
|
- Honor `remote_allow_list` in hole punch response. (#1186)
|
||||||
|
- Fix a panic when `tun.use_system_route_table` is `true` and a route lacks a destination. (#1437)
|
||||||
|
- Fix an issue when `tun.use_system_route_table: true` could result in heavy CPU utilization when many thousands of routes
|
||||||
|
are present. (#1326)
|
||||||
|
- Fix tests for 32 bit machines. (#1394)
|
||||||
|
- Fix a possible 32bit integer underflow in config handling. (#1353)
|
||||||
|
- Fix moving a udp address from one vpn address to another in the `static_host_map`
|
||||||
|
which could cause rapid re-handshaking with an incorrect remote. (#1259)
|
||||||
|
- Improve smoke tests in environments where the docker network is not the default. (#1347)
|
||||||
|
|
||||||
|
## [1.9.7] - 2025-10-10
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix an issue where Nebula could incorrectly accept and process a packet from an erroneous source IP when the sender's
|
||||||
|
certificate is configured with unsafe_routes (cert v1/v2) or multiple IPs (cert v2). (#1494)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Disable sending `recv_error` messages when a packet is received outside the allowable counter window. (#1459)
|
||||||
|
- Improve error messages and remove some unnecessary fatal conditions in the Windows and generic udp listener. (#1453)
|
||||||
|
|
||||||
## [1.9.6] - 2025-7-15
|
## [1.9.6] - 2025-7-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -687,7 +870,12 @@ created.)
|
|||||||
|
|
||||||
- Initial public release.
|
- Initial public release.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.9.6...HEAD
|
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.10.3...HEAD
|
||||||
|
[1.10.3]: https://github.com/slackhq/nebula/releases/tag/v1.10.3
|
||||||
|
[1.10.2]: https://github.com/slackhq/nebula/releases/tag/v1.10.2
|
||||||
|
[1.10.1]: https://github.com/slackhq/nebula/releases/tag/v1.10.1
|
||||||
|
[1.10.0]: https://github.com/slackhq/nebula/releases/tag/v1.10.0
|
||||||
|
[1.9.7]: https://github.com/slackhq/nebula/releases/tag/v1.9.7
|
||||||
[1.9.6]: https://github.com/slackhq/nebula/releases/tag/v1.9.6
|
[1.9.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.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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
#ECCN:Open Source
|
||||||
@@ -40,7 +40,7 @@ ALL_LINUX = linux-amd64 \
|
|||||||
linux-mips64le \
|
linux-mips64le \
|
||||||
linux-mips-softfloat \
|
linux-mips-softfloat \
|
||||||
linux-riscv64 \
|
linux-riscv64 \
|
||||||
linux-loong64
|
linux-loong64
|
||||||
|
|
||||||
ALL_FREEBSD = freebsd-amd64 \
|
ALL_FREEBSD = freebsd-amd64 \
|
||||||
freebsd-arm64
|
freebsd-arm64
|
||||||
@@ -60,10 +60,22 @@ 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
|
||||||
|
|
||||||
e2ev: TEST_FLAGS = -v
|
e2ev: TEST_FLAGS += -v
|
||||||
e2ev: e2e
|
e2ev: e2e
|
||||||
|
|
||||||
e2evv: TEST_ENV += TEST_LOGS=1
|
e2evv: TEST_ENV += TEST_LOGS=1
|
||||||
@@ -82,6 +94,35 @@ 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)
|
||||||
@@ -96,7 +137,7 @@ release-netbsd: $(ALL_NETBSD:%=build/nebula-%.tar.gz)
|
|||||||
|
|
||||||
release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz
|
release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz
|
||||||
|
|
||||||
BUILD_ARGS = -trimpath
|
BUILD_ARGS += -trimpath
|
||||||
|
|
||||||
bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe
|
bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe
|
||||||
mv $? .
|
mv $? .
|
||||||
@@ -116,6 +157,10 @@ bin-freebsd-arm64: build/freebsd-arm64/nebula build/freebsd-arm64/nebula-cert
|
|||||||
bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert
|
bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert
|
||||||
mv $? .
|
mv $? .
|
||||||
|
|
||||||
|
bin-pkcs11: BUILD_ARGS += -tags pkcs11
|
||||||
|
bin-pkcs11: CGO_ENABLED = 1
|
||||||
|
bin-pkcs11: bin
|
||||||
|
|
||||||
bin:
|
bin:
|
||||||
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
|
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
|
||||||
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
|
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
|
||||||
@@ -133,6 +178,8 @@ build/linux-mips-softfloat/%: LDFLAGS += -s -w
|
|||||||
# boringcrypto
|
# boringcrypto
|
||||||
build/linux-amd64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
build/linux-amd64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
||||||
build/linux-arm64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
build/linux-arm64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
||||||
|
build/linux-amd64-boringcrypto/%: LDFLAGS += -checklinkname=0
|
||||||
|
build/linux-arm64-boringcrypto/%: LDFLAGS += -checklinkname=0
|
||||||
|
|
||||||
build/%/nebula: .FORCE
|
build/%/nebula: .FORCE
|
||||||
GOOS=$(firstword $(subst -, , $*)) \
|
GOOS=$(firstword $(subst -, , $*)) \
|
||||||
@@ -166,7 +213,10 @@ test:
|
|||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
test-boringcrypto:
|
test-boringcrypto:
|
||||||
GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go test -v ./...
|
GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go test -ldflags "-checklinkname=0" -v ./...
|
||||||
|
|
||||||
|
test-pkcs11:
|
||||||
|
CGO_ENABLED=1 go test -v -tags pkcs11 ./...
|
||||||
|
|
||||||
test-cov-html:
|
test-cov-html:
|
||||||
go test -coverprofile=coverage.out
|
go test -coverprofile=coverage.out
|
||||||
@@ -189,7 +239,7 @@ bench-cpu-long:
|
|||||||
go test -bench=. -benchtime=60s -cpuprofile=cpu.pprof
|
go test -bench=. -benchtime=60s -cpuprofile=cpu.pprof
|
||||||
go tool pprof go-audit.test cpu.pprof
|
go tool pprof go-audit.test cpu.pprof
|
||||||
|
|
||||||
proto: nebula.pb.go cert/cert.pb.go
|
proto: nebula.pb.go cert/cert_v1.pb.go
|
||||||
|
|
||||||
nebula.pb.go: nebula.proto .FORCE
|
nebula.pb.go: nebula.proto .FORCE
|
||||||
go build github.com/gogo/protobuf/protoc-gen-gogofaster
|
go build github.com/gogo/protobuf/protoc-gen-gogofaster
|
||||||
@@ -218,6 +268,9 @@ 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
|
||||||
@@ -227,5 +280,5 @@ smoke-vagrant/%: bin-docker build/%/nebula
|
|||||||
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
||||||
|
|
||||||
.FORCE:
|
.FORCE:
|
||||||
.PHONY: bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
|
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
|
||||||
.DEFAULT_GOAL := bin
|
.DEFAULT_GOAL := bin
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ It lets you seamlessly connect computers anywhere in the world. Nebula is portab
|
|||||||
It can be used to connect a small number of computers, but is also able to connect tens of thousands of computers.
|
It can be used to connect a small number of computers, but is also able to connect tens of thousands of computers.
|
||||||
|
|
||||||
Nebula incorporates a number of existing concepts like encryption, security groups, certificates,
|
Nebula incorporates a number of existing concepts like encryption, security groups, certificates,
|
||||||
and tunneling, and each of those individual pieces existed before Nebula in various forms.
|
and tunneling.
|
||||||
What makes Nebula different to existing offerings is that it brings all of these ideas together,
|
What makes Nebula different to existing offerings is that it brings all of these ideas together,
|
||||||
resulting in a sum that is greater than its individual parts.
|
resulting in a sum that is greater than its individual parts.
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Further documentation can be found [here](https://nebula.defined.net/docs/).
|
|||||||
|
|
||||||
You can read more about Nebula [here](https://medium.com/p/884110a5579).
|
You can read more about Nebula [here](https://medium.com/p/884110a5579).
|
||||||
|
|
||||||
You can also join the NebulaOSS Slack group [here](https://join.slack.com/t/nebulaoss/shared_invite/enQtOTA5MDI4NDg3MTg4LTkwY2EwNTI4NzQyMzc0M2ZlODBjNWI3NTY1MzhiOThiMmZlZjVkMTI0NGY4YTMyNjUwMWEyNzNkZTJmYzQxOGU).
|
You can also join the NebulaOSS Slack group [here](https://join.slack.com/t/nebulaoss/shared_invite/zt-39pk4xopc-CUKlGcb5Z39dQ0cK1v7ehA).
|
||||||
|
|
||||||
## Supported Platforms
|
## Supported Platforms
|
||||||
|
|
||||||
@@ -28,46 +28,46 @@ Check the [releases](https://github.com/slackhq/nebula/releases/latest) page for
|
|||||||
#### Distribution Packages
|
#### Distribution Packages
|
||||||
|
|
||||||
- [Arch Linux](https://archlinux.org/packages/extra/x86_64/nebula/)
|
- [Arch Linux](https://archlinux.org/packages/extra/x86_64/nebula/)
|
||||||
```
|
```sh
|
||||||
$ sudo pacman -S nebula
|
sudo pacman -S nebula
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Fedora Linux](https://src.fedoraproject.org/rpms/nebula)
|
- [Fedora Linux](https://src.fedoraproject.org/rpms/nebula)
|
||||||
```
|
```sh
|
||||||
$ sudo dnf install nebula
|
sudo dnf install nebula
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Debian Linux](https://packages.debian.org/source/stable/nebula)
|
- [Debian Linux](https://packages.debian.org/source/stable/nebula)
|
||||||
```
|
```sh
|
||||||
$ sudo apt install nebula
|
sudo apt install nebula
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=nebula)
|
- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=nebula)
|
||||||
```
|
```sh
|
||||||
$ sudo apk add nebula
|
sudo apk add nebula
|
||||||
```
|
```
|
||||||
|
|
||||||
- [macOS Homebrew](https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/nebula.rb)
|
- [macOS Homebrew](https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/n/nebula.rb)
|
||||||
```
|
```sh
|
||||||
$ brew install nebula
|
brew install nebula
|
||||||
```
|
```
|
||||||
|
|
||||||
- [Docker](https://hub.docker.com/r/nebulaoss/nebula)
|
- [Docker](https://hub.docker.com/r/nebulaoss/nebula)
|
||||||
```
|
```sh
|
||||||
$ docker pull nebulaoss/nebula
|
docker pull nebulaoss/nebula
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Mobile
|
#### Mobile ([source code](https://github.com/DefinedNet/mobile_nebula))
|
||||||
|
|
||||||
- [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)
|
||||||
|
|
||||||
## Technical Overview
|
## Technical Overview
|
||||||
|
|
||||||
Nebula is a mutually authenticated peer-to-peer software defined network based on the [Noise Protocol Framework](https://noiseprotocol.org/).
|
Nebula is a mutually authenticated peer-to-peer software-defined network based on the [Noise Protocol Framework](https://noiseprotocol.org/).
|
||||||
Nebula uses certificates to assert a node's IP address, name, and membership within user-defined groups.
|
Nebula uses certificates to assert a node's IP address, name, and membership within user-defined groups.
|
||||||
Nebula's user-defined groups allow for provider agnostic traffic filtering between nodes.
|
Nebula's user-defined groups allow for provider agnostic traffic filtering between nodes.
|
||||||
Discovery nodes allow individual peers to find each other and optionally use UDP hole punching to establish connections from behind most firewalls or NATs.
|
Discovery nodes (aka lighthouses) allow individual peers to find each other and optionally use UDP hole punching to establish connections from behind most firewalls or NATs.
|
||||||
Users can move data between nodes in any number of cloud service providers, datacenters, and endpoints, without needing to maintain a particular addressing scheme.
|
Users can move data between nodes in any number of cloud service providers, datacenters, and endpoints, without needing to maintain a particular addressing scheme.
|
||||||
|
|
||||||
Nebula uses Elliptic-curve Diffie-Hellman (`ECDH`) key exchange and `AES-256-GCM` in its default configuration.
|
Nebula uses Elliptic-curve Diffie-Hellman (`ECDH`) key exchange and `AES-256-GCM` in its default configuration.
|
||||||
@@ -76,34 +76,42 @@ 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.
|
||||||
|
|
||||||
#### 2. (Optional, but you really should..) At least one discovery node with a routable IP address, which we call a lighthouse.
|
#### 2. (Optional, but you really should..) At least one discovery node with a routable IP address, which we call a lighthouse.
|
||||||
|
|
||||||
Nebula lighthouses allow nodes to find each other, anywhere in the world. A lighthouse is the only node in a Nebula network whose IP should not change. Running a lighthouse requires very few compute resources, and you can easily use the least expensive option from a cloud hosting provider. If you're not sure which provider to use, a number of us have used $5/mo [DigitalOcean](https://digitalocean.com) droplets as lighthouses.
|
Nebula lighthouses allow nodes to find each other, anywhere in the world. A lighthouse is the only node in a Nebula network whose IP should not change. Running a lighthouse requires very few compute resources, and you can easily use the least expensive option from a cloud hosting provider. If you're not sure which provider to use, a number of us have used $6/mo [DigitalOcean](https://digitalocean.com) droplets as lighthouses.
|
||||||
|
|
||||||
Once you have launched an instance, ensure that Nebula udp traffic (default port udp/4242) can reach it over the internet.
|
|
||||||
|
|
||||||
|
Once you have launched an instance, ensure that Nebula udp traffic (default port udp/4242) can reach it over the internet.
|
||||||
|
|
||||||
#### 3. A Nebula certificate authority, which will be the root of trust for a particular Nebula network.
|
#### 3. A Nebula certificate authority, which will be the root of trust for a particular Nebula network.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
./nebula-cert ca -name "Myorganization, Inc"
|
./nebula-cert ca -name "Myorganization, Inc"
|
||||||
```
|
```
|
||||||
This will create files named `ca.key` and `ca.cert` in the current directory. The `ca.key` file is the most sensitive file you'll create, because it is the key used to sign the certificates for individual nebula nodes/hosts. Please store this file somewhere safe, preferably with strong encryption.
|
|
||||||
|
This will create files named `ca.key` and `ca.cert` in the current directory. The `ca.key` file is the most sensitive file you'll create, because it is the key used to sign the certificates for individual nebula nodes/hosts. Please store this file somewhere safe, preferably with strong encryption.
|
||||||
|
|
||||||
|
**Be aware!** By default, certificate authorities have a 1-year lifetime before expiration. See [this guide](https://nebula.defined.net/docs/guides/rotating-certificate-authority/) for details on rotating a CA.
|
||||||
|
|
||||||
#### 4. Nebula host keys and certificates generated from that certificate authority
|
#### 4. Nebula host keys and certificates generated from that certificate authority
|
||||||
|
|
||||||
This assumes you have four nodes, named lighthouse1, laptop, server1, host3. You can name the nodes any way you'd like, including FQDN. You'll also need to choose IP addresses and the associated subnet. In this example, we are creating a nebula network that will use 192.168.100.x/24 as its network range. This example also demonstrates nebula groups, which can later be used to define traffic rules in a nebula network.
|
This assumes you have four nodes, named lighthouse1, laptop, server1, host3. You can name the nodes any way you'd like, including FQDN. You'll also need to choose IP addresses and the associated subnet. In this example, we are creating a nebula network that will use 192.168.100.x/24 as its network range. This example also demonstrates nebula groups, which can later be used to define traffic rules in a nebula network.
|
||||||
```
|
```sh
|
||||||
./nebula-cert sign -name "lighthouse1" -ip "192.168.100.1/24"
|
./nebula-cert sign -name "lighthouse1" -ip "192.168.100.1/24"
|
||||||
./nebula-cert sign -name "laptop" -ip "192.168.100.2/24" -groups "laptop,home,ssh"
|
./nebula-cert sign -name "laptop" -ip "192.168.100.2/24" -groups "laptop,home,ssh"
|
||||||
./nebula-cert sign -name "server1" -ip "192.168.100.9/24" -groups "servers"
|
./nebula-cert sign -name "server1" -ip "192.168.100.9/24" -groups "servers"
|
||||||
./nebula-cert sign -name "host3" -ip "192.168.100.10/24"
|
./nebula-cert sign -name "host3" -ip "192.168.100.10/24"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
By default, host certificates will expire 1 second before the CA expires. Use the `-duration` flag to specify a shorter lifetime.
|
||||||
|
|
||||||
#### 5. Configuration files for each host
|
#### 5. Configuration files for each host
|
||||||
|
|
||||||
Download a copy of the nebula [example configuration](https://github.com/slackhq/nebula/blob/master/examples/config.yml).
|
Download a copy of the nebula [example configuration](https://github.com/slackhq/nebula/blob/master/examples/config.yml).
|
||||||
|
|
||||||
* On the lighthouse node, you'll need to ensure `am_lighthouse: true` is set.
|
* On the lighthouse node, you'll need to ensure `am_lighthouse: true` is set.
|
||||||
@@ -118,10 +126,13 @@ For each host, copy the nebula binary to the host, along with `config.yml` from
|
|||||||
**DO NOT COPY `ca.key` TO INDIVIDUAL NODES.**
|
**DO NOT COPY `ca.key` TO INDIVIDUAL NODES.**
|
||||||
|
|
||||||
#### 7. Run nebula on each host
|
#### 7. Run nebula on each host
|
||||||
```
|
|
||||||
|
```sh
|
||||||
./nebula -config /path/to/config.yml
|
./nebula -config /path/to/config.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For more detailed instructions, [find the full documentation here](https://nebula.defined.net/docs/).
|
||||||
|
|
||||||
## Building Nebula from source
|
## Building Nebula from source
|
||||||
|
|
||||||
Make sure you have [go](https://go.dev/doc/install) installed and clone this repo. Change to the nebula directory.
|
Make sure you have [go](https://go.dev/doc/install) installed and clone this repo. Change to the nebula directory.
|
||||||
@@ -140,8 +151,10 @@ The default curve used for cryptographic handshakes and signatures is Curve25519
|
|||||||
|
|
||||||
In addition, Nebula can be built using the [BoringCrypto GOEXPERIMENT](https://github.com/golang/go/blob/go1.20/src/crypto/internal/boring/README.md) by running either of the following make targets:
|
In addition, Nebula can be built using the [BoringCrypto GOEXPERIMENT](https://github.com/golang/go/blob/go1.20/src/crypto/internal/boring/README.md) by running either of the following make targets:
|
||||||
|
|
||||||
make bin-boringcrypto
|
```sh
|
||||||
make release-boringcrypto
|
make bin-boringcrypto
|
||||||
|
make release-boringcrypto
|
||||||
|
```
|
||||||
|
|
||||||
This is not the recommended default deployment, but may be useful based on your compliance requirements.
|
This is not the recommended default deployment, but may be useful based on your compliance requirements.
|
||||||
|
|
||||||
@@ -149,5 +162,3 @@ This is not the recommended default deployment, but may be useful based on your
|
|||||||
|
|
||||||
Nebula was created at Slack Technologies, Inc by Nate Brown and Ryan Huber, with contributions from Oliver Fross, Alan Lam, Wade Simmons, and Lining Wang.
|
Nebula was created at Slack Technologies, Inc by Nate Brown and Ryan Huber, with contributions from Oliver Fross, Alan Lam, Wade Simmons, and Lining Wang.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+37
-38
@@ -36,7 +36,7 @@ type AllowListNameRule struct {
|
|||||||
|
|
||||||
func NewLocalAllowListFromConfig(c *config.C, k string) (*LocalAllowList, error) {
|
func NewLocalAllowListFromConfig(c *config.C, k string) (*LocalAllowList, error) {
|
||||||
var nameRules []AllowListNameRule
|
var nameRules []AllowListNameRule
|
||||||
handleKey := func(key string, value interface{}) (bool, error) {
|
handleKey := func(key string, value any) (bool, error) {
|
||||||
if key == "interfaces" {
|
if key == "interfaces" {
|
||||||
var err error
|
var err error
|
||||||
nameRules, err = getAllowListInterfaces(k, value)
|
nameRules, err = getAllowListInterfaces(k, value)
|
||||||
@@ -70,7 +70,7 @@ func NewRemoteAllowListFromConfig(c *config.C, k, rangesKey string) (*RemoteAllo
|
|||||||
|
|
||||||
// If the handleKey func returns true, the rest of the parsing is skipped
|
// If the handleKey func returns true, the rest of the parsing is skipped
|
||||||
// for this key. This allows parsing of special values like `interfaces`.
|
// for this key. This allows parsing of special values like `interfaces`.
|
||||||
func newAllowListFromConfig(c *config.C, k string, handleKey func(key string, value interface{}) (bool, error)) (*AllowList, error) {
|
func newAllowListFromConfig(c *config.C, k string, handleKey func(key string, value any) (bool, error)) (*AllowList, error) {
|
||||||
r := c.Get(k)
|
r := c.Get(k)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -81,8 +81,8 @@ func newAllowListFromConfig(c *config.C, k string, handleKey func(key string, va
|
|||||||
|
|
||||||
// If the handleKey func returns true, the rest of the parsing is skipped
|
// If the handleKey func returns true, the rest of the parsing is skipped
|
||||||
// for this key. This allows parsing of special values like `interfaces`.
|
// for this key. This allows parsing of special values like `interfaces`.
|
||||||
func newAllowList(k string, raw interface{}, handleKey func(key string, value interface{}) (bool, error)) (*AllowList, error) {
|
func newAllowList(k string, raw any, handleKey func(key string, value any) (bool, error)) (*AllowList, error) {
|
||||||
rawMap, ok := raw.(map[interface{}]interface{})
|
rawMap, ok := raw.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("config `%s` has invalid type: %T", k, raw)
|
return nil, fmt.Errorf("config `%s` has invalid type: %T", k, raw)
|
||||||
}
|
}
|
||||||
@@ -100,12 +100,7 @@ func newAllowList(k string, raw interface{}, handleKey func(key string, value in
|
|||||||
rules4 := allowListRules{firstValue: true, allValuesMatch: true, defaultSet: false}
|
rules4 := allowListRules{firstValue: true, allValuesMatch: true, defaultSet: false}
|
||||||
rules6 := allowListRules{firstValue: true, allValuesMatch: true, defaultSet: false}
|
rules6 := allowListRules{firstValue: true, allValuesMatch: true, defaultSet: false}
|
||||||
|
|
||||||
for rawKey, rawValue := range rawMap {
|
for rawCIDR, rawValue := range rawMap {
|
||||||
rawCIDR, ok := rawKey.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("config `%s` has invalid key (type %T): %v", k, rawKey, rawKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if handleKey != nil {
|
if handleKey != nil {
|
||||||
handled, err := handleKey(rawCIDR, rawValue)
|
handled, err := handleKey(rawCIDR, rawValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,7 +111,7 @@ func newAllowList(k string, raw interface{}, handleKey func(key string, value in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
value, ok := rawValue.(bool)
|
value, ok := config.AsBool(rawValue)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("config `%s` has invalid value (type %T): %v", k, rawValue, rawValue)
|
return nil, fmt.Errorf("config `%s` has invalid value (type %T): %v", k, rawValue, rawValue)
|
||||||
}
|
}
|
||||||
@@ -128,7 +123,6 @@ func newAllowList(k string, raw interface{}, handleKey func(key string, value in
|
|||||||
|
|
||||||
ipNet = netip.PrefixFrom(ipNet.Addr().Unmap(), ipNet.Bits())
|
ipNet = netip.PrefixFrom(ipNet.Addr().Unmap(), ipNet.Bits())
|
||||||
|
|
||||||
// TODO: should we error on duplicate CIDRs in the config?
|
|
||||||
tree.Insert(ipNet, value)
|
tree.Insert(ipNet, value)
|
||||||
|
|
||||||
maskBits := ipNet.Bits()
|
maskBits := ipNet.Bits()
|
||||||
@@ -174,22 +168,18 @@ func newAllowList(k string, raw interface{}, handleKey func(key string, value in
|
|||||||
return &AllowList{cidrTree: tree}, nil
|
return &AllowList{cidrTree: tree}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllowListInterfaces(k string, v interface{}) ([]AllowListNameRule, error) {
|
func getAllowListInterfaces(k string, v any) ([]AllowListNameRule, error) {
|
||||||
var nameRules []AllowListNameRule
|
var nameRules []AllowListNameRule
|
||||||
|
|
||||||
rawRules, ok := v.(map[interface{}]interface{})
|
rawRules, ok := v.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("config `%s.interfaces` is invalid (type %T): %v", k, v, v)
|
return nil, fmt.Errorf("config `%s.interfaces` is invalid (type %T): %v", k, v, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
firstEntry := true
|
firstEntry := true
|
||||||
var allValues bool
|
var allValues bool
|
||||||
for rawName, rawAllow := range rawRules {
|
for name, rawAllow := range rawRules {
|
||||||
name, ok := rawName.(string)
|
allow, ok := config.AsBool(rawAllow)
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("config `%s.interfaces` has invalid key (type %T): %v", k, rawName, rawName)
|
|
||||||
}
|
|
||||||
allow, ok := rawAllow.(bool)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("config `%s.interfaces` has invalid value (type %T): %v", k, rawAllow, rawAllow)
|
return nil, fmt.Errorf("config `%s.interfaces` has invalid value (type %T): %v", k, rawAllow, rawAllow)
|
||||||
}
|
}
|
||||||
@@ -225,16 +215,11 @@ func getRemoteAllowRanges(c *config.C, k string) (*bart.Table[*AllowList], error
|
|||||||
|
|
||||||
remoteAllowRanges := new(bart.Table[*AllowList])
|
remoteAllowRanges := new(bart.Table[*AllowList])
|
||||||
|
|
||||||
rawMap, ok := value.(map[interface{}]interface{})
|
rawMap, ok := value.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("config `%s` has invalid type: %T", k, value)
|
return nil, fmt.Errorf("config `%s` has invalid type: %T", k, value)
|
||||||
}
|
}
|
||||||
for rawKey, rawValue := range rawMap {
|
for rawCIDR, rawValue := range rawMap {
|
||||||
rawCIDR, ok := rawKey.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("config `%s` has invalid key (type %T): %v", k, rawKey, rawKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
allowList, err := newAllowList(fmt.Sprintf("%s.%s", k, rawCIDR), rawValue, nil)
|
allowList, err := newAllowList(fmt.Sprintf("%s.%s", k, rawCIDR), rawValue, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -251,20 +236,20 @@ func getRemoteAllowRanges(c *config.C, k string) (*bart.Table[*AllowList], error
|
|||||||
return remoteAllowRanges, nil
|
return remoteAllowRanges, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *AllowList) Allow(ip netip.Addr) bool {
|
func (al *AllowList) Allow(addr netip.Addr) bool {
|
||||||
if al == nil {
|
if al == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
result, _ := al.cidrTree.Lookup(ip)
|
result, _ := al.cidrTree.Lookup(addr)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *LocalAllowList) Allow(ip netip.Addr) bool {
|
func (al *LocalAllowList) Allow(udpAddr netip.Addr) bool {
|
||||||
if al == nil {
|
if al == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return al.AllowList.Allow(ip)
|
return al.AllowList.Allow(udpAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *LocalAllowList) AllowName(name string) bool {
|
func (al *LocalAllowList) AllowName(name string) bool {
|
||||||
@@ -282,23 +267,37 @@ func (al *LocalAllowList) AllowName(name string) bool {
|
|||||||
return !al.nameRules[0].Allow
|
return !al.nameRules[0].Allow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *RemoteAllowList) AllowUnknownVpnIp(ip netip.Addr) bool {
|
func (al *RemoteAllowList) AllowUnknownVpnAddr(vpnAddr netip.Addr) bool {
|
||||||
if al == nil {
|
if al == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return al.AllowList.Allow(ip)
|
return al.AllowList.Allow(vpnAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *RemoteAllowList) Allow(vpnIp netip.Addr, ip netip.Addr) bool {
|
func (al *RemoteAllowList) Allow(vpnAddr netip.Addr, udpAddr netip.Addr) bool {
|
||||||
if !al.getInsideAllowList(vpnIp).Allow(ip) {
|
if !al.getInsideAllowList(vpnAddr).Allow(udpAddr) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return al.AllowList.Allow(ip)
|
return al.AllowList.Allow(udpAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *RemoteAllowList) getInsideAllowList(vpnIp netip.Addr) *AllowList {
|
func (al *RemoteAllowList) AllowAll(vpnAddrs []netip.Addr, udpAddr netip.Addr) bool {
|
||||||
|
if !al.AllowList.Allow(udpAddr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vpnAddr := range vpnAddrs {
|
||||||
|
if !al.getInsideAllowList(vpnAddr).Allow(udpAddr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (al *RemoteAllowList) getInsideAllowList(vpnAddr netip.Addr) *AllowList {
|
||||||
if al.insideAllowLists != nil {
|
if al.insideAllowLists != nil {
|
||||||
inside, ok := al.insideAllowLists.Lookup(vpnIp)
|
inside, ok := al.insideAllowLists.Lookup(vpnAddr)
|
||||||
if ok {
|
if ok {
|
||||||
return inside
|
return inside
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-33
@@ -9,32 +9,33 @@ import (
|
|||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
"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 TestNewAllowListFromConfig(t *testing.T) {
|
func TestNewAllowListFromConfig(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
c := config.NewC(l)
|
c := config.NewC(l)
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"192.168.0.0": true,
|
"192.168.0.0": true,
|
||||||
}
|
}
|
||||||
r, err := newAllowListFromConfig(c, "allowlist", nil)
|
r, err := newAllowListFromConfig(c, "allowlist", nil)
|
||||||
assert.EqualError(t, err, "config `allowlist` has invalid CIDR: 192.168.0.0. netip.ParsePrefix(\"192.168.0.0\"): no '/'")
|
require.EqualError(t, err, "config `allowlist` has invalid CIDR: 192.168.0.0. netip.ParsePrefix(\"192.168.0.0\"): no '/'")
|
||||||
assert.Nil(t, r)
|
assert.Nil(t, r)
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"192.168.0.0/16": "abc",
|
"192.168.0.0/16": "abc",
|
||||||
}
|
}
|
||||||
r, err = newAllowListFromConfig(c, "allowlist", nil)
|
r, err = newAllowListFromConfig(c, "allowlist", nil)
|
||||||
assert.EqualError(t, err, "config `allowlist` has invalid value (type string): abc")
|
require.EqualError(t, err, "config `allowlist` has invalid value (type string): abc")
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"192.168.0.0/16": true,
|
"192.168.0.0/16": true,
|
||||||
"10.0.0.0/8": false,
|
"10.0.0.0/8": false,
|
||||||
}
|
}
|
||||||
r, err = newAllowListFromConfig(c, "allowlist", nil)
|
r, err = newAllowListFromConfig(c, "allowlist", nil)
|
||||||
assert.EqualError(t, err, "config `allowlist` contains both true and false rules, but no default set for 0.0.0.0/0")
|
require.EqualError(t, err, "config `allowlist` contains both true and false rules, but no default set for 0.0.0.0/0")
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"0.0.0.0/0": true,
|
"0.0.0.0/0": true,
|
||||||
"10.0.0.0/8": false,
|
"10.0.0.0/8": false,
|
||||||
"10.42.42.0/24": true,
|
"10.42.42.0/24": true,
|
||||||
@@ -42,9 +43,9 @@ func TestNewAllowListFromConfig(t *testing.T) {
|
|||||||
"fd00:fd00::/16": false,
|
"fd00:fd00::/16": false,
|
||||||
}
|
}
|
||||||
r, err = newAllowListFromConfig(c, "allowlist", nil)
|
r, err = newAllowListFromConfig(c, "allowlist", nil)
|
||||||
assert.EqualError(t, err, "config `allowlist` contains both true and false rules, but no default set for ::/0")
|
require.EqualError(t, err, "config `allowlist` contains both true and false rules, but no default set for ::/0")
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"0.0.0.0/0": true,
|
"0.0.0.0/0": true,
|
||||||
"10.0.0.0/8": false,
|
"10.0.0.0/8": false,
|
||||||
"10.42.42.0/24": true,
|
"10.42.42.0/24": true,
|
||||||
@@ -54,7 +55,7 @@ func TestNewAllowListFromConfig(t *testing.T) {
|
|||||||
assert.NotNil(t, r)
|
assert.NotNil(t, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"0.0.0.0/0": true,
|
"0.0.0.0/0": true,
|
||||||
"10.0.0.0/8": false,
|
"10.0.0.0/8": false,
|
||||||
"10.42.42.0/24": true,
|
"10.42.42.0/24": true,
|
||||||
@@ -69,25 +70,25 @@ func TestNewAllowListFromConfig(t *testing.T) {
|
|||||||
|
|
||||||
// Test interface names
|
// Test interface names
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"interfaces": map[interface{}]interface{}{
|
"interfaces": map[string]any{
|
||||||
`docker.*`: "foo",
|
`docker.*`: "foo",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
lr, err := NewLocalAllowListFromConfig(c, "allowlist")
|
lr, err := NewLocalAllowListFromConfig(c, "allowlist")
|
||||||
assert.EqualError(t, err, "config `allowlist.interfaces` has invalid value (type string): foo")
|
require.EqualError(t, err, "config `allowlist.interfaces` has invalid value (type string): foo")
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"interfaces": map[interface{}]interface{}{
|
"interfaces": map[string]any{
|
||||||
`docker.*`: false,
|
`docker.*`: false,
|
||||||
`eth.*`: true,
|
`eth.*`: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
lr, err = NewLocalAllowListFromConfig(c, "allowlist")
|
lr, err = NewLocalAllowListFromConfig(c, "allowlist")
|
||||||
assert.EqualError(t, err, "config `allowlist.interfaces` values must all be the same true/false value")
|
require.EqualError(t, err, "config `allowlist.interfaces` values must all be the same true/false value")
|
||||||
|
|
||||||
c.Settings["allowlist"] = map[interface{}]interface{}{
|
c.Settings["allowlist"] = map[string]any{
|
||||||
"interfaces": map[interface{}]interface{}{
|
"interfaces": map[string]any{
|
||||||
`docker.*`: false,
|
`docker.*`: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ func TestNewAllowListFromConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAllowList_Allow(t *testing.T) {
|
func TestAllowList_Allow(t *testing.T) {
|
||||||
assert.Equal(t, true, ((*AllowList)(nil)).Allow(netip.MustParseAddr("1.1.1.1")))
|
assert.True(t, ((*AllowList)(nil)).Allow(netip.MustParseAddr("1.1.1.1")))
|
||||||
|
|
||||||
tree := new(bart.Table[bool])
|
tree := new(bart.Table[bool])
|
||||||
tree.Insert(netip.MustParsePrefix("0.0.0.0/0"), true)
|
tree.Insert(netip.MustParsePrefix("0.0.0.0/0"), true)
|
||||||
@@ -111,17 +112,17 @@ func TestAllowList_Allow(t *testing.T) {
|
|||||||
tree.Insert(netip.MustParsePrefix("::2/128"), false)
|
tree.Insert(netip.MustParsePrefix("::2/128"), false)
|
||||||
al := &AllowList{cidrTree: tree}
|
al := &AllowList{cidrTree: tree}
|
||||||
|
|
||||||
assert.Equal(t, true, al.Allow(netip.MustParseAddr("1.1.1.1")))
|
assert.True(t, al.Allow(netip.MustParseAddr("1.1.1.1")))
|
||||||
assert.Equal(t, false, al.Allow(netip.MustParseAddr("10.0.0.4")))
|
assert.False(t, al.Allow(netip.MustParseAddr("10.0.0.4")))
|
||||||
assert.Equal(t, true, al.Allow(netip.MustParseAddr("10.42.42.42")))
|
assert.True(t, al.Allow(netip.MustParseAddr("10.42.42.42")))
|
||||||
assert.Equal(t, false, al.Allow(netip.MustParseAddr("10.42.42.41")))
|
assert.False(t, al.Allow(netip.MustParseAddr("10.42.42.41")))
|
||||||
assert.Equal(t, true, al.Allow(netip.MustParseAddr("10.42.0.1")))
|
assert.True(t, al.Allow(netip.MustParseAddr("10.42.0.1")))
|
||||||
assert.Equal(t, true, al.Allow(netip.MustParseAddr("::1")))
|
assert.True(t, al.Allow(netip.MustParseAddr("::1")))
|
||||||
assert.Equal(t, false, al.Allow(netip.MustParseAddr("::2")))
|
assert.False(t, al.Allow(netip.MustParseAddr("::2")))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalAllowList_AllowName(t *testing.T) {
|
func TestLocalAllowList_AllowName(t *testing.T) {
|
||||||
assert.Equal(t, true, ((*LocalAllowList)(nil)).AllowName("docker0"))
|
assert.True(t, ((*LocalAllowList)(nil)).AllowName("docker0"))
|
||||||
|
|
||||||
rules := []AllowListNameRule{
|
rules := []AllowListNameRule{
|
||||||
{Name: regexp.MustCompile("^docker.*$"), Allow: false},
|
{Name: regexp.MustCompile("^docker.*$"), Allow: false},
|
||||||
@@ -129,9 +130,9 @@ func TestLocalAllowList_AllowName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
al := &LocalAllowList{nameRules: rules}
|
al := &LocalAllowList{nameRules: rules}
|
||||||
|
|
||||||
assert.Equal(t, false, al.AllowName("docker0"))
|
assert.False(t, al.AllowName("docker0"))
|
||||||
assert.Equal(t, false, al.AllowName("tun0"))
|
assert.False(t, al.AllowName("tun0"))
|
||||||
assert.Equal(t, true, al.AllowName("eth0"))
|
assert.True(t, al.AllowName("eth0"))
|
||||||
|
|
||||||
rules = []AllowListNameRule{
|
rules = []AllowListNameRule{
|
||||||
{Name: regexp.MustCompile("^eth.*$"), Allow: true},
|
{Name: regexp.MustCompile("^eth.*$"), Allow: true},
|
||||||
@@ -139,7 +140,7 @@ func TestLocalAllowList_AllowName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
al = &LocalAllowList{nameRules: rules}
|
al = &LocalAllowList{nameRules: rules}
|
||||||
|
|
||||||
assert.Equal(t, false, al.AllowName("docker0"))
|
assert.False(t, al.AllowName("docker0"))
|
||||||
assert.Equal(t, true, al.AllowName("eth0"))
|
assert.True(t, al.AllowName("eth0"))
|
||||||
assert.Equal(t, true, al.AllowName("ens5"))
|
assert.True(t, al.AllowName("ens5"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,157 +1,263 @@
|
|||||||
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 []bool
|
bits []uint64
|
||||||
firstSeen bool
|
|
||||||
lostCounter metrics.Counter
|
lostCounter metrics.Counter
|
||||||
dupeCounter metrics.Counter
|
dupeCounter metrics.Counter
|
||||||
outOfWindowCounter metrics.Counter
|
outOfWindowCounter metrics.Counter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBits(bits uint64) *Bits {
|
func NewBits(length uint64) *Bits {
|
||||||
return &Bits{
|
if length == 0 || length&(length-1) != 0 {
|
||||||
length: bits,
|
panic(fmt.Sprintf("Bits length must be a power of two, got %d", length))
|
||||||
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) Check(l logrus.FieldLogger, i uint64) bool {
|
func (b *Bits) get(i uint64) bool {
|
||||||
// If i is the next number, return true.
|
pos := i & b.lengthMask
|
||||||
if i > b.current || (i == 0 && b.firstSeen == false && b.current < b.length) {
|
//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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If i is within the window, check if it's been set already. The first window will fail this check
|
// Next, if the packet is in-window, see if we've seen it before
|
||||||
if i > b.current-b.length {
|
if i > b.current-b.length {
|
||||||
return !b.bits[i%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 > b.current {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If i is within the first window
|
if b.strictlyWithinWindow(i) {
|
||||||
if i < b.length {
|
return !b.get(i)
|
||||||
return !b.bits[i%b.length]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not within the window
|
// Not within the window
|
||||||
l.Debugf("rejected a packet (top) %d %d\n", b.current, i)
|
if l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
|
l.Debug("rejected a packet (top)", "current", b.current, "incoming", i)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bits) Update(l *logrus.Logger, i uint64) bool {
|
// Update has three branches:
|
||||||
// If i is the next number, return true and update current.
|
// - i == b.current+1: fast path; advance the cursor by one and lose-count
|
||||||
|
// the slot we just stomped (only past warmup; see the i > b.length guard
|
||||||
|
// below).
|
||||||
|
// - i > b.current+1: jump path; clear all slots between current and i
|
||||||
|
// (or up to a full window's worth, whichever is smaller) via clearRange,
|
||||||
|
// then mark i. Two arms here: a warmup arm that handles the very first
|
||||||
|
// window before the cursor has slid, and a steady-state arm that treats
|
||||||
|
// every cleared empty slot as a lost packet.
|
||||||
|
// - i <= b.current: in-window check for duplicates; out-of-window otherwise.
|
||||||
|
//
|
||||||
|
// NewBits seeds bits[0]=1 so counter 0 looks "received" — Update never
|
||||||
|
// clears that marker during warmup (clearRange skips position 0 when
|
||||||
|
// startPos=1), and once b.current >= b.length the marker is no longer
|
||||||
|
// consulted. The marker prevents a fictitious "lost" hit on the first real
|
||||||
|
// counter.
|
||||||
|
func (b *Bits) Update(l *slog.Logger, i uint64) bool {
|
||||||
|
// Fast path: i is the next expected counter. Split out so the function
|
||||||
|
// stays small and avoids paying for the slow paths' slog argument-build
|
||||||
|
// stack frame on every call. The bit read/test/write is inlined to
|
||||||
|
// touch the backing word once.
|
||||||
if i == b.current+1 {
|
if i == b.current+1 {
|
||||||
// Report missed packets, we can only understand what was missed after the first window has been gone through
|
pos := i & b.lengthMask
|
||||||
if i > b.length && b.bits[i%b.length] == false {
|
word := pos >> 6
|
||||||
|
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[i%b.length] = true
|
b.bits[word] = w | mask
|
||||||
b.current = i
|
b.current = i
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
return b.updateSlow(l, i)
|
||||||
|
}
|
||||||
|
|
||||||
// If i packet is greater than current but less than the maximum length of our bitmap,
|
// updateSlow handles jumps, in-window backfill, dupes, and out-of-window.
|
||||||
// flip everything in between to false and move ahead.
|
func (b *Bits) updateSlow(l *slog.Logger, i uint64) bool {
|
||||||
if i > b.current && i < b.current+b.length {
|
// If i is a jump, adjust the window, record lost, update current, and return true
|
||||||
// In between current and i need to be zero'd to allow those packets to come in later
|
if i > b.current {
|
||||||
for n := b.current + 1; n < i; n++ {
|
end := i
|
||||||
b.bits[n%b.length] = false
|
if end > b.current+b.length {
|
||||||
|
end = b.current + b.length
|
||||||
|
}
|
||||||
|
count := end - b.current
|
||||||
|
startPos := (b.current + 1) & b.lengthMask
|
||||||
|
|
||||||
|
var lost int64
|
||||||
|
if b.current >= b.length {
|
||||||
|
// Steady state: every cleared slot is past warmup, so any unset
|
||||||
|
// bit we evict is a lost packet from the previous cycle.
|
||||||
|
wasSet := b.clearRange(startPos, count)
|
||||||
|
lost = int64(count) - int64(wasSet)
|
||||||
|
} else {
|
||||||
|
// Warmup (the very first window). Some cleared slots represent
|
||||||
|
// packets <= length where eviction is not "lost" in the usual
|
||||||
|
// sense. This branch is taken at most once per connection so we
|
||||||
|
// don't bother optimizing it.
|
||||||
|
for n := b.current + 1; n <= end; n++ {
|
||||||
|
if !b.get(n) && n > b.length {
|
||||||
|
lost++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.clearRange(startPos, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.bits[i%b.length] = true
|
// Anything past the new window can never be backfilled, so it's lost.
|
||||||
b.current = i
|
if i > b.current+b.length {
|
||||||
//l.Debugf("missed %d packets between %d and %d\n", i-b.current, i, b.current)
|
lost += int64(i - b.current - b.length)
|
||||||
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)
|
||||||
|
|
||||||
if l.Level >= logrus.DebugLevel {
|
b.set(i)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow for the 0 packet to come in within the first window
|
// If i is within the current window but below the current counter, check to see if it's a duplicate
|
||||||
if i == 0 && b.firstSeen == false && b.current < b.length {
|
if b.strictlyWithinWindow(i) {
|
||||||
b.firstSeen = true
|
pos := i & b.lengthMask
|
||||||
b.bits[i%b.length] = true
|
word := pos >> 6
|
||||||
return true
|
mask := uint64(1) << (pos & 63)
|
||||||
}
|
w := b.bits[word]
|
||||||
|
if b.current == i || w&mask != 0 {
|
||||||
// If i is within the window of current minus length (the total pat window size),
|
if l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
// allow it and flip to true but to NOT change current. We also have to account for the first window
|
l.Debug("Receive window",
|
||||||
if ((b.current >= b.length && i > b.current-b.length) || (b.current < b.length && i < b.length)) && i <= b.current {
|
"accepted", false,
|
||||||
if b.current == i {
|
"currentCounter", b.current,
|
||||||
if l.Level >= logrus.DebugLevel {
|
"incomingCounter", i,
|
||||||
l.WithField("receiveWindow", m{"accepted": false, "currentCounter": b.current, "incomingCounter": i, "reason": "duplicate"}).
|
"reason", "duplicate",
|
||||||
Debug("Receive window")
|
)
|
||||||
}
|
}
|
||||||
b.dupeCounter.Inc(1)
|
b.dupeCounter.Inc(1)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.bits[i%b.length] == true {
|
b.bits[word] = w | mask
|
||||||
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.Level >= logrus.DebugLevel {
|
if l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
l.WithField("accepted", false).
|
l.Debug("Receive window",
|
||||||
WithField("currentCounter", b.current).
|
"accepted", false,
|
||||||
WithField("incomingCounter", i).
|
"currentCounter", b.current,
|
||||||
WithField("reason", "nonsense").
|
"incomingCounter", i,
|
||||||
Debug("Receive window")
|
"reason", "nonsense",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func maxInt64(a, b int64) int64 {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|||||||
+330
-120
@@ -7,77 +7,114 @@ 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(10)
|
b := NewBits(16)
|
||||||
|
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))
|
||||||
u := b.Update(l, 1)
|
assert.True(t, b.Update(l, 1))
|
||||||
assert.True(t, u)
|
|
||||||
assert.EqualValues(t, 1, b.current)
|
assert.EqualValues(t, 1, b.current)
|
||||||
g := []bool{false, true, false, false, false, false, false, false, false, false}
|
g := []bool{true, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}
|
||||||
assert.Equal(t, g, b.bits)
|
assert.Equal(t, g, b.snapshot())
|
||||||
|
|
||||||
// Receive two
|
// Receive two
|
||||||
assert.True(t, b.Check(l, 2))
|
assert.True(t, b.Check(l, 2))
|
||||||
u = b.Update(l, 2)
|
assert.True(t, b.Update(l, 2))
|
||||||
assert.True(t, u)
|
|
||||||
assert.EqualValues(t, 2, b.current)
|
assert.EqualValues(t, 2, b.current)
|
||||||
g = []bool{false, true, true, false, false, false, false, false, false, false}
|
g = []bool{true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false}
|
||||||
assert.Equal(t, g, b.bits)
|
assert.Equal(t, g, b.snapshot())
|
||||||
|
|
||||||
// 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))
|
||||||
u = b.Update(l, 2)
|
assert.False(t, b.Update(l, 2))
|
||||||
assert.False(t, u)
|
|
||||||
assert.EqualValues(t, 2, b.current)
|
assert.EqualValues(t, 2, b.current)
|
||||||
|
|
||||||
// Jump ahead to 15, which should clear everything and set the 6th element
|
// Jump ahead to 25, which clears the window and sets slot 25%16 = 9.
|
||||||
assert.True(t, b.Check(l, 15))
|
assert.True(t, b.Check(l, 25))
|
||||||
u = b.Update(l, 15)
|
assert.True(t, b.Update(l, 25))
|
||||||
assert.True(t, u)
|
assert.EqualValues(t, 25, b.current)
|
||||||
assert.EqualValues(t, 15, b.current)
|
g = []bool{false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false}
|
||||||
g = []bool{false, false, false, false, false, true, false, false, false, false}
|
assert.Equal(t, g, b.snapshot())
|
||||||
assert.Equal(t, g, b.bits)
|
|
||||||
|
|
||||||
// Mark 14, which is allowed because it is in the window
|
// Mark 24, which is in window (current 25, length 16, window covers [10,25]).
|
||||||
assert.True(t, b.Check(l, 14))
|
assert.True(t, b.Check(l, 24))
|
||||||
u = b.Update(l, 14)
|
assert.True(t, b.Update(l, 24))
|
||||||
assert.True(t, u)
|
assert.EqualValues(t, 25, b.current)
|
||||||
assert.EqualValues(t, 15, b.current)
|
g = []bool{false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false}
|
||||||
g = []bool{false, false, false, false, true, true, false, false, false, false}
|
assert.Equal(t, g, b.snapshot())
|
||||||
assert.Equal(t, g, b.bits)
|
|
||||||
|
|
||||||
// Mark 5, which is not allowed because it is not in the window
|
// Mark 5, not allowed because 5 <= current-length (25-16=9).
|
||||||
assert.False(t, b.Check(l, 5))
|
assert.False(t, b.Check(l, 5))
|
||||||
u = b.Update(l, 5)
|
assert.False(t, b.Update(l, 5))
|
||||||
assert.False(t, u)
|
assert.EqualValues(t, 25, b.current)
|
||||||
assert.EqualValues(t, 15, b.current)
|
g = []bool{false, false, false, false, false, false, false, false, true, true, false, false, false, false, false, false}
|
||||||
g = []bool{false, false, false, false, true, true, false, false, false, false}
|
assert.Equal(t, g, b.snapshot())
|
||||||
assert.Equal(t, g, b.bits)
|
|
||||||
|
|
||||||
// make sure we handle wrapping around once to the current position
|
// Make sure we handle wrapping around once to the same slot. With
|
||||||
b = NewBits(10)
|
// length=16, packets 1 and 17 share slot 1.
|
||||||
|
b = NewBits(16)
|
||||||
assert.True(t, b.Update(l, 1))
|
assert.True(t, b.Update(l, 1))
|
||||||
assert.True(t, b.Update(l, 11))
|
assert.True(t, b.Update(l, 17))
|
||||||
assert.Equal(t, []bool{false, true, false, false, false, false, false, false, false, false}, b.bits)
|
assert.Equal(t, []bool{false, true, false, false, false, false, false, false, false, false, false, false, false, false, false, false}, b.snapshot())
|
||||||
|
|
||||||
// Walk through a few windows in order
|
// Walk through a few windows in order
|
||||||
b = NewBits(10)
|
b = NewBits(16)
|
||||||
for i := uint64(0); i <= 100; i++ {
|
for i := uint64(1); 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(10)
|
b := NewBits(16)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
@@ -102,127 +139,300 @@ func TestBitsDupeCounter(t *testing.T) {
|
|||||||
|
|
||||||
func TestBitsOutOfWindowCounter(t *testing.T) {
|
func TestBitsOutOfWindowCounter(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
b := NewBits(10)
|
b := NewBits(16)
|
||||||
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())
|
||||||
|
|
||||||
assert.True(t, b.Update(l, 21))
|
// 9 single-step advances, each evicts a slot whose bit was cleared during
|
||||||
assert.True(t, b.Update(l, 22))
|
// the jump above and whose value was never seen, so each contributes 1
|
||||||
assert.True(t, b.Update(l, 23))
|
// to lostCounter.
|
||||||
assert.True(t, b.Update(l, 24))
|
for n := uint64(21); n <= 29; n++ {
|
||||||
assert.True(t, b.Update(l, 25))
|
assert.True(t, b.Update(l, n))
|
||||||
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())
|
||||||
|
|
||||||
//tODO: make sure lostcounter doesn't increase in orderly increment
|
// 4 from the Update(20) jump + 9 from 21..29.
|
||||||
assert.Equal(t, int64(20), b.lostCounter.Count())
|
assert.Equal(t, int64(13), 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(10)
|
b := NewBits(16)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
|
|
||||||
//assert.True(t, b.Update(0))
|
// Walk 20..29 like the original, just with a bigger window. Same
|
||||||
assert.True(t, b.Update(l, 0))
|
// reasoning as TestBitsOutOfWindowCounter: 4 past-window from Update(20),
|
||||||
assert.True(t, b.Update(l, 20))
|
// then 9 more from the unit advances.
|
||||||
assert.True(t, b.Update(l, 21))
|
for n := uint64(20); n <= 29; n++ {
|
||||||
assert.True(t, b.Update(l, 22))
|
assert.True(t, b.Update(l, n))
|
||||||
assert.True(t, b.Update(l, 23))
|
}
|
||||||
assert.True(t, b.Update(l, 24))
|
assert.Equal(t, int64(13), b.lostCounter.Count())
|
||||||
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(10)
|
b = NewBits(16)
|
||||||
b.lostCounter.Clear()
|
b.lostCounter.Clear()
|
||||||
b.dupeCounter.Clear()
|
b.dupeCounter.Clear()
|
||||||
b.outOfWindowCounter.Clear()
|
b.outOfWindowCounter.Clear()
|
||||||
|
|
||||||
assert.True(t, b.Update(l, 0))
|
// Update(15) clears the warmup window (no lost), sets slot 15.
|
||||||
|
assert.True(t, b.Update(l, 15))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Update(16): slot 0 was already set (NewBits seeded it), and 16 is not
|
||||||
|
// strictly > length, so nothing is recorded as lost.
|
||||||
|
assert.True(t, b.Update(l, 16))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Update(17): we jumped straight from 0 to 15, so slot 1 was cleared
|
||||||
|
// (and never re-set). 17 > 16 is past warmup, so packet 1 is recorded lost.
|
||||||
|
assert.True(t, b.Update(l, 17))
|
||||||
|
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Fill in 18..30 in single steps. Each i evicts slot i%16. Slots 2..14
|
||||||
|
// were all cleared during Update(15), and we never re-set any of them,
|
||||||
|
// so each i in 18..30 is a fresh lost packet — 13 more.
|
||||||
|
for n := uint64(18); n <= 30; n++ {
|
||||||
|
assert.True(t, b.Update(l, n))
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(14), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Jump ahead by exactly one window size.
|
||||||
|
assert.True(t, b.Update(l, 46))
|
||||||
|
// end = min(46, 30+16) = 46, count = 16, all slots cleared. Before the
|
||||||
|
// jump every slot 0..15 had been set (Update(15), (16), (17), 18..30),
|
||||||
|
// so wasSet=16 and 46 == current+length means no past-window slack:
|
||||||
|
// lost contribution = 0.
|
||||||
|
assert.Equal(t, int64(14), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Walk 47..55. The Update(46) jump cleared every slot, so only slot 14
|
||||||
|
// (for packet 46) is set when we start. Each subsequent unit step lands
|
||||||
|
// on a slot that was cleared and is past warmup, so it counts as lost.
|
||||||
|
// 9 more = 23.
|
||||||
|
for n := uint64(47); n <= 55; n++ {
|
||||||
|
assert.True(t, b.Update(l, n))
|
||||||
|
}
|
||||||
|
assert.Equal(t, int64(23), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Jump ahead by two windows: clears the window plus past-window loss.
|
||||||
|
assert.True(t, b.Update(l, 87))
|
||||||
|
// current=55, length=16. end = min(87, 71) = 71. count=16, all slots
|
||||||
|
// cleared. Slots set before the clear are slots 14,15,0..7 (10 total).
|
||||||
|
// Lost from clear = 16 - 10 = 6. Past window: 87 - 55 - 16 = 16. +22.
|
||||||
|
assert.Equal(t, int64(45), b.lostCounter.Count())
|
||||||
|
assert.Equal(t, int64(0), b.dupeCounter.Count())
|
||||||
|
assert.Equal(t, int64(0), b.outOfWindowCounter.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitsLostCounterIssue1(t *testing.T) {
|
||||||
|
l := test.NewLogger()
|
||||||
|
b := NewBits(16)
|
||||||
|
b.lostCounter.Clear()
|
||||||
|
b.dupeCounter.Clear()
|
||||||
|
b.outOfWindowCounter.Clear()
|
||||||
|
|
||||||
|
// Receive 4, backfill 1, then 9, 2, 3, 5, 6, 7 (skip 8), 10, 11, 14.
|
||||||
|
// Then jump to 25 — slot 25%16=9 is being evicted, but it had been set
|
||||||
|
// (we received packet 9), so no spurious lost increment. The original
|
||||||
|
// regression was about double-counting a missing packet when its slot
|
||||||
|
// got cleared on a jump. With the jump path now using clearRange's
|
||||||
|
// word-level wasSet count, the same semantics hold.
|
||||||
|
assert.True(t, b.Update(l, 4))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
assert.True(t, b.Update(l, 1))
|
||||||
assert.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())
|
||||||
// 10 will set 0 index, 0 was already set, no lost packets
|
assert.True(t, b.Update(l, 2))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
assert.True(t, b.Update(l, 3))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
assert.True(t, b.Update(l, 5))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
assert.True(t, b.Update(l, 6))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
assert.True(t, b.Update(l, 7))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
// Skip packet 8.
|
||||||
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(1), b.lostCounter.Count())
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
// Now let's fill in the window, should end up with 8 lost packets
|
|
||||||
assert.True(t, b.Update(l, 12))
|
|
||||||
assert.True(t, b.Update(l, 13))
|
|
||||||
assert.True(t, b.Update(l, 14))
|
assert.True(t, b.Update(l, 14))
|
||||||
|
assert.Equal(t, int64(0), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Jump to 25. With length=16, slot 25%16=9 corresponds to packet 9
|
||||||
|
// (which we DID receive), so its bit is set and no lost++ from that
|
||||||
|
// eviction. The trace below shows the only loss is packet 8.
|
||||||
|
assert.True(t, b.Update(l, 25))
|
||||||
|
// current was 14, i=25. end=min(25,30)=25. count=11. startPos=15.
|
||||||
|
// steady? current=14<16, so warmup branch: per-bit n=15..25, count those
|
||||||
|
// with !get(n) AND n>16. n=17..25 are >16. Among slots 17%16=1..25%16=9
|
||||||
|
// did we set slots 1..9 (packets 1..9)? Yes for all but slot 8 (packet 8
|
||||||
|
// was skipped). n=24 maps to slot 8 which is FALSE → lost++. All other
|
||||||
|
// n in 17..25 map to slots that are set. n=16 is not strictly > 16. So
|
||||||
|
// lost = 1.
|
||||||
|
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||||
|
|
||||||
|
// Fill in 12, 13, 15, 16. Each is below current=25 (in-window). 16 must
|
||||||
|
// recheck slot 0 — it was set by NewBits and then cleared by the
|
||||||
|
// Update(25) jump, so 16 backfills cleanly.
|
||||||
|
assert.True(t, b.Update(l, 12))
|
||||||
|
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||||
|
assert.True(t, b.Update(l, 13))
|
||||||
|
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||||
assert.True(t, b.Update(l, 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.True(t, b.Update(l, 17))
|
assert.Equal(t, int64(1), b.lostCounter.Count())
|
||||||
assert.True(t, b.Update(l, 18))
|
|
||||||
assert.True(t, b.Update(l, 19))
|
|
||||||
assert.Equal(t, int64(8), b.lostCounter.Count())
|
|
||||||
|
|
||||||
// Jump ahead by a window size
|
// We missed packet 8 above and that loss is still recorded once, never
|
||||||
assert.True(t, b.Update(l, 29))
|
// double-counted, never zeroed.
|
||||||
assert.Equal(t, int64(8), b.lostCounter.Count())
|
assert.Equal(t, int64(1), 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkBits(b *testing.B) {
|
// TestBitsWarmupOvershoot exercises the jump path's warmup arm with an
|
||||||
z := NewBits(10)
|
// overshoot past one full window. NewBits leaves current=0 with only slot 0
|
||||||
for n := 0; n < b.N; n++ {
|
// "set" by the marker. Jumping straight to length+k must (a) clear every
|
||||||
for i := range z.bits {
|
// slot the jump straddles, (b) count only past-window slack (not the
|
||||||
z.bits[i] = true
|
// in-window slots, which never had a "lost" tenant during warmup), and
|
||||||
}
|
// (c) leave the cursor at the new counter so subsequent unit advances
|
||||||
for i := range z.bits {
|
// count from steady state. The marker bit at slot 0 is irrelevant once
|
||||||
z.bits[i] = false
|
// 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++ {
|
||||||
|
z.Update(l, uint64(n)+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,5 +1,4 @@
|
|||||||
//go:build boringcrypto
|
//go:build boringcrypto
|
||||||
// +build boringcrypto
|
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
|
|||||||
+37
-33
@@ -21,7 +21,11 @@ type calculatedRemote struct {
|
|||||||
port uint32
|
port uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCalculatedRemote(maskCidr netip.Prefix, port int) (*calculatedRemote, error) {
|
func newCalculatedRemote(cidr, maskCidr netip.Prefix, port int) (*calculatedRemote, error) {
|
||||||
|
if maskCidr.Addr().BitLen() != cidr.Addr().BitLen() {
|
||||||
|
return nil, fmt.Errorf("invalid mask: %s for cidr: %s", maskCidr, cidr)
|
||||||
|
}
|
||||||
|
|
||||||
masked := maskCidr.Masked()
|
masked := maskCidr.Masked()
|
||||||
if port < 0 || port > math.MaxUint16 {
|
if port < 0 || port > math.MaxUint16 {
|
||||||
return nil, fmt.Errorf("invalid port: %d", port)
|
return nil, fmt.Errorf("invalid port: %d", port)
|
||||||
@@ -38,32 +42,38 @@ func (c *calculatedRemote) String() string {
|
|||||||
return fmt.Sprintf("CalculatedRemote(mask=%v port=%d)", c.ipNet, c.port)
|
return fmt.Sprintf("CalculatedRemote(mask=%v port=%d)", c.ipNet, c.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *calculatedRemote) Apply(ip netip.Addr) *Ip4AndPort {
|
func (c *calculatedRemote) ApplyV4(addr netip.Addr) *V4AddrPort {
|
||||||
// Combine the masked bytes of the "mask" IP with the unmasked bytes
|
// Combine the masked bytes of the "mask" IP with the unmasked bytes of the overlay IP
|
||||||
// of the overlay IP
|
|
||||||
if c.ipNet.Addr().Is4() {
|
|
||||||
return c.apply4(ip)
|
|
||||||
}
|
|
||||||
return c.apply6(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *calculatedRemote) apply4(ip netip.Addr) *Ip4AndPort {
|
|
||||||
//TODO: IPV6-WORK this can be less crappy
|
|
||||||
maskb := net.CIDRMask(c.mask.Bits(), c.mask.Addr().BitLen())
|
maskb := net.CIDRMask(c.mask.Bits(), c.mask.Addr().BitLen())
|
||||||
mask := binary.BigEndian.Uint32(maskb[:])
|
mask := binary.BigEndian.Uint32(maskb[:])
|
||||||
|
|
||||||
b := c.mask.Addr().As4()
|
b := c.mask.Addr().As4()
|
||||||
maskIp := binary.BigEndian.Uint32(b[:])
|
maskAddr := binary.BigEndian.Uint32(b[:])
|
||||||
|
|
||||||
b = ip.As4()
|
b = addr.As4()
|
||||||
intIp := binary.BigEndian.Uint32(b[:])
|
intAddr := binary.BigEndian.Uint32(b[:])
|
||||||
|
|
||||||
return &Ip4AndPort{(maskIp & mask) | (intIp & ^mask), c.port}
|
return &V4AddrPort{(maskAddr & mask) | (intAddr & ^mask), c.port}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *calculatedRemote) apply6(ip netip.Addr) *Ip4AndPort {
|
func (c *calculatedRemote) ApplyV6(addr netip.Addr) *V6AddrPort {
|
||||||
//TODO: IPV6-WORK
|
mask := net.CIDRMask(c.mask.Bits(), c.mask.Addr().BitLen())
|
||||||
panic("Can not calculate ipv6 remote addresses")
|
maskAddr := c.mask.Addr().As16()
|
||||||
|
calcAddr := addr.As16()
|
||||||
|
|
||||||
|
ap := V6AddrPort{Port: c.port}
|
||||||
|
|
||||||
|
maskb := binary.BigEndian.Uint64(mask[:8])
|
||||||
|
maskAddrb := binary.BigEndian.Uint64(maskAddr[:8])
|
||||||
|
calcAddrb := binary.BigEndian.Uint64(calcAddr[:8])
|
||||||
|
ap.Hi = (maskAddrb & maskb) | (calcAddrb & ^maskb)
|
||||||
|
|
||||||
|
maskb = binary.BigEndian.Uint64(mask[8:])
|
||||||
|
maskAddrb = binary.BigEndian.Uint64(maskAddr[8:])
|
||||||
|
calcAddrb = binary.BigEndian.Uint64(calcAddr[8:])
|
||||||
|
ap.Lo = (maskAddrb & maskb) | (calcAddrb & ^maskb)
|
||||||
|
|
||||||
|
return &ap
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCalculatedRemotesFromConfig(c *config.C, k string) (*bart.Table[[]*calculatedRemote], error) {
|
func NewCalculatedRemotesFromConfig(c *config.C, k string) (*bart.Table[[]*calculatedRemote], error) {
|
||||||
@@ -74,23 +84,17 @@ func NewCalculatedRemotesFromConfig(c *config.C, k string) (*bart.Table[[]*calcu
|
|||||||
|
|
||||||
calculatedRemotes := new(bart.Table[[]*calculatedRemote])
|
calculatedRemotes := new(bart.Table[[]*calculatedRemote])
|
||||||
|
|
||||||
rawMap, ok := value.(map[any]any)
|
rawMap, ok := value.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("config `%s` has invalid type: %T", k, value)
|
return nil, fmt.Errorf("config `%s` has invalid type: %T", k, value)
|
||||||
}
|
}
|
||||||
for rawKey, rawValue := range rawMap {
|
for rawCIDR, rawValue := range rawMap {
|
||||||
rawCIDR, ok := rawKey.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("config `%s` has invalid key (type %T): %v", k, rawKey, rawKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
cidr, err := netip.ParsePrefix(rawCIDR)
|
cidr, err := netip.ParsePrefix(rawCIDR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("config `%s` has invalid CIDR: %s", k, rawCIDR)
|
return nil, fmt.Errorf("config `%s` has invalid CIDR: %s", k, rawCIDR)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: IPV6-WORK this does not verify that rawValue contains the same bits as cidr here
|
entry, err := newCalculatedRemotesListFromConfig(cidr, rawValue)
|
||||||
entry, err := newCalculatedRemotesListFromConfig(rawValue)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("config '%s.%s': %w", k, rawCIDR, err)
|
return nil, fmt.Errorf("config '%s.%s': %w", k, rawCIDR, err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +105,7 @@ func NewCalculatedRemotesFromConfig(c *config.C, k string) (*bart.Table[[]*calcu
|
|||||||
return calculatedRemotes, nil
|
return calculatedRemotes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCalculatedRemotesListFromConfig(raw any) ([]*calculatedRemote, error) {
|
func newCalculatedRemotesListFromConfig(cidr netip.Prefix, raw any) ([]*calculatedRemote, error) {
|
||||||
rawList, ok := raw.([]any)
|
rawList, ok := raw.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("calculated_remotes entry has invalid type: %T", raw)
|
return nil, fmt.Errorf("calculated_remotes entry has invalid type: %T", raw)
|
||||||
@@ -109,7 +113,7 @@ func newCalculatedRemotesListFromConfig(raw any) ([]*calculatedRemote, error) {
|
|||||||
|
|
||||||
var l []*calculatedRemote
|
var l []*calculatedRemote
|
||||||
for _, e := range rawList {
|
for _, e := range rawList {
|
||||||
c, err := newCalculatedRemotesEntryFromConfig(e)
|
c, err := newCalculatedRemotesEntryFromConfig(cidr, e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("calculated_remotes entry: %w", err)
|
return nil, fmt.Errorf("calculated_remotes entry: %w", err)
|
||||||
}
|
}
|
||||||
@@ -119,8 +123,8 @@ func newCalculatedRemotesListFromConfig(raw any) ([]*calculatedRemote, error) {
|
|||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCalculatedRemotesEntryFromConfig(raw any) (*calculatedRemote, error) {
|
func newCalculatedRemotesEntryFromConfig(cidr netip.Prefix, raw any) (*calculatedRemote, error) {
|
||||||
rawMap, ok := raw.(map[any]any)
|
rawMap, ok := raw.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid type: %T", raw)
|
return nil, fmt.Errorf("invalid type: %T", raw)
|
||||||
}
|
}
|
||||||
@@ -155,5 +159,5 @@ func newCalculatedRemotesEntryFromConfig(raw any) (*calculatedRemote, error) {
|
|||||||
return nil, fmt.Errorf("invalid port (type %T): %v", rawValue, rawValue)
|
return nil, fmt.Errorf("invalid port (type %T): %v", rawValue, rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newCalculatedRemote(maskCidr, port)
|
return newCalculatedRemote(cidr, maskCidr, port)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,73 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCalculatedRemoteApply(t *testing.T) {
|
func TestCalculatedRemoteApply(t *testing.T) {
|
||||||
ipNet, err := netip.ParsePrefix("192.168.1.0/24")
|
// Test v4 addresses
|
||||||
require.NoError(t, err)
|
ipNet := netip.MustParsePrefix("192.168.1.0/24")
|
||||||
|
c, err := newCalculatedRemote(ipNet, ipNet, 4242)
|
||||||
c, err := newCalculatedRemote(ipNet, 4242)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
input, err := netip.ParseAddr("10.0.10.182")
|
input, err := netip.ParseAddr("10.0.10.182")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected, err := netip.ParseAddr("192.168.1.182")
|
expected, err := netip.ParseAddr("192.168.1.182")
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, NewIp4AndPortFromNetIP(expected, 4242), c.Apply(input))
|
assert.Equal(t, netAddrToProtoV4AddrPort(expected, 4242), c.ApplyV4(input))
|
||||||
|
|
||||||
|
// Test v6 addresses
|
||||||
|
ipNet = netip.MustParsePrefix("ffff:ffff:ffff:ffff::0/64")
|
||||||
|
c, err = newCalculatedRemote(ipNet, ipNet, 4242)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
input, err = netip.ParseAddr("beef:beef:beef:beef:beef:beef:beef:beef")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected, err = netip.ParseAddr("ffff:ffff:ffff:ffff:beef:beef:beef:beef")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, netAddrToProtoV6AddrPort(expected, 4242), c.ApplyV6(input))
|
||||||
|
|
||||||
|
// Test v6 addresses part 2
|
||||||
|
ipNet = netip.MustParsePrefix("ffff:ffff:ffff:ffff:ffff::0/80")
|
||||||
|
c, err = newCalculatedRemote(ipNet, ipNet, 4242)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
input, err = netip.ParseAddr("beef:beef:beef:beef:beef:beef:beef:beef")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected, err = netip.ParseAddr("ffff:ffff:ffff:ffff:ffff:beef:beef:beef")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, netAddrToProtoV6AddrPort(expected, 4242), c.ApplyV6(input))
|
||||||
|
|
||||||
|
// Test v6 addresses part 2
|
||||||
|
ipNet = netip.MustParsePrefix("ffff:ffff:ffff::0/48")
|
||||||
|
c, err = newCalculatedRemote(ipNet, ipNet, 4242)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
input, err = netip.ParseAddr("beef:beef:beef:beef:beef:beef:beef:beef")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected, err = netip.ParseAddr("ffff:ffff:ffff:beef:beef:beef:beef:beef")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, netAddrToProtoV6AddrPort(expected, 4242), c.ApplyV6(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newCalculatedRemote(t *testing.T) {
|
||||||
|
c, err := newCalculatedRemote(netip.MustParsePrefix("1::1/128"), netip.MustParsePrefix("1.0.0.0/32"), 4242)
|
||||||
|
require.EqualError(t, err, "invalid mask: 1.0.0.0/32 for cidr: 1::1/128")
|
||||||
|
require.Nil(t, c)
|
||||||
|
|
||||||
|
c, err = newCalculatedRemote(netip.MustParsePrefix("1.0.0.0/32"), netip.MustParsePrefix("1::1/128"), 4242)
|
||||||
|
require.EqualError(t, err, "invalid mask: 1::1/128 for cidr: 1.0.0.0/32")
|
||||||
|
require.Nil(t, c)
|
||||||
|
|
||||||
|
c, err = newCalculatedRemote(netip.MustParsePrefix("1.0.0.0/32"), netip.MustParsePrefix("1.0.0.0/32"), 4242)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, c)
|
||||||
|
|
||||||
|
c, err = newCalculatedRemote(netip.MustParsePrefix("1::1/128"), netip.MustParsePrefix("1::1/128"), 4242)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, c)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
GO111MODULE = on
|
GO111MODULE = on
|
||||||
export GO111MODULE
|
export GO111MODULE
|
||||||
|
|
||||||
cert.pb.go: cert.proto .FORCE
|
cert_v1.pb.go: cert_v1.proto .FORCE
|
||||||
go build google.golang.org/protobuf/cmd/protoc-gen-go
|
go build google.golang.org/protobuf/cmd/protoc-gen-go
|
||||||
PATH="$(CURDIR):$(PATH)" protoc --go_out=. --go_opt=paths=source_relative $<
|
PATH="$(CURDIR):$(PATH)" protoc --go_out=. --go_opt=paths=source_relative $<
|
||||||
rm protoc-gen-go
|
rm protoc-gen-go
|
||||||
|
|||||||
+15
-4
@@ -2,14 +2,25 @@
|
|||||||
|
|
||||||
This is a library for interacting with `nebula` style certificates and authorities.
|
This is a library for interacting with `nebula` style certificates and authorities.
|
||||||
|
|
||||||
A `protobuf` definition of the certificate format is also included
|
There are now 2 versions of `nebula` certificates:
|
||||||
|
|
||||||
### Compiling the protobuf definition
|
## v1
|
||||||
|
|
||||||
Make sure you have `protoc` installed.
|
This version is deprecated.
|
||||||
|
|
||||||
|
A `protobuf` definition of the certificate format is included at `cert_v1.proto`
|
||||||
|
|
||||||
|
To compile the definition you will need `protoc` installed.
|
||||||
|
|
||||||
To compile for `go` with the same version of protobuf specified in go.mod:
|
To compile for `go` with the same version of protobuf specified in go.mod:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make proto
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## v2
|
||||||
|
|
||||||
|
This is the latest version which uses asn.1 DER encoding. It can support ipv4 and ipv6 and tolerate
|
||||||
|
future certificate changes better than v1.
|
||||||
|
|
||||||
|
`cert_v2.asn1` defines the wire format and can be used to compile marshalers.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
"golang.org/x/crypto/cryptobyte/asn1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readOptionalASN1Boolean reads an asn.1 boolean with a specific tag instead of a asn.1 tag wrapping a boolean with a value
|
||||||
|
// https://github.com/golang/go/issues/64811#issuecomment-1944446920
|
||||||
|
func readOptionalASN1Boolean(b *cryptobyte.String, out *bool, tag asn1.Tag, defaultValue bool) bool {
|
||||||
|
var present bool
|
||||||
|
var child cryptobyte.String
|
||||||
|
if !b.ReadOptionalASN1(&child, &present, tag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !present {
|
||||||
|
*out = defaultValue
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have 1 byte
|
||||||
|
if len(child) == 1 {
|
||||||
|
*out = child[0] > 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// readOptionalASN1Byte reads an asn.1 uint8 with a specific tag instead of a asn.1 tag wrapping a uint8 with a value
|
||||||
|
// Similar issue as with readOptionalASN1Boolean
|
||||||
|
func readOptionalASN1Byte(b *cryptobyte.String, out *byte, tag asn1.Tag, defaultValue byte) bool {
|
||||||
|
var present bool
|
||||||
|
var child cryptobyte.String
|
||||||
|
if !b.ReadOptionalASN1(&child, &present, tag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !present {
|
||||||
|
*out = defaultValue
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have 1 byte
|
||||||
|
if len(child) == 1 {
|
||||||
|
*out = child[0]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
-148
@@ -1,148 +0,0 @@
|
|||||||
package cert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NebulaCAPool struct {
|
|
||||||
CAs map[string]*NebulaCertificate
|
|
||||||
certBlocklist map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCAPool creates a CAPool
|
|
||||||
func NewCAPool() *NebulaCAPool {
|
|
||||||
ca := NebulaCAPool{
|
|
||||||
CAs: make(map[string]*NebulaCertificate),
|
|
||||||
certBlocklist: make(map[string]struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ca
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCAPoolFromBytes will create a new CA pool from the provided
|
|
||||||
// input bytes, which must be a PEM-encoded set of nebula certificates.
|
|
||||||
// If the pool contains unsupported certificates, they will generate warnings
|
|
||||||
// in the []error return arg.
|
|
||||||
// If the pool contains any expired certificates, an ErrExpired will be
|
|
||||||
// returned along with the pool. The caller must handle any such errors.
|
|
||||||
func NewCAPoolFromBytes(caPEMs []byte) (*NebulaCAPool, []error, error) {
|
|
||||||
pool := NewCAPool()
|
|
||||||
var err error
|
|
||||||
var warnings []error
|
|
||||||
good := 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
caPEMs, err = pool.AddCACertificate(caPEMs)
|
|
||||||
if errors.Is(err, ErrExpired) {
|
|
||||||
warnings = append(warnings, err)
|
|
||||||
} else if errors.Is(err, ErrInvalidPEMCertificateUnsupported) {
|
|
||||||
warnings = append(warnings, err)
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, warnings, err
|
|
||||||
} else {
|
|
||||||
// Only consider a good certificate if there were no errors present
|
|
||||||
good++
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(caPEMs) == 0 || strings.TrimSpace(string(caPEMs)) == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if good == 0 {
|
|
||||||
return nil, warnings, errors.New("no valid CA certificates present")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool, warnings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCACertificate verifies a Nebula CA certificate and adds it to the pool
|
|
||||||
// Only the first pem encoded object will be consumed, any remaining bytes are returned.
|
|
||||||
// Parsed certificates will be verified and must be a CA
|
|
||||||
func (ncp *NebulaCAPool) AddCACertificate(pemBytes []byte) ([]byte, error) {
|
|
||||||
c, pemBytes, err := UnmarshalNebulaCertificateFromPEM(pemBytes)
|
|
||||||
if err != nil {
|
|
||||||
return pemBytes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.Details.IsCA {
|
|
||||||
return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrNotCA)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.CheckSignature(c.Details.PublicKey) {
|
|
||||||
return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrNotSelfSigned)
|
|
||||||
}
|
|
||||||
|
|
||||||
sum, err := c.Sha256Sum()
|
|
||||||
if err != nil {
|
|
||||||
return pemBytes, fmt.Errorf("could not calculate shasum for provided CA; error: %s; %s", err, c.Details.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
ncp.CAs[sum] = c
|
|
||||||
if c.Expired(time.Now()) {
|
|
||||||
return pemBytes, fmt.Errorf("%s: %w", c.Details.Name, ErrExpired)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pemBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlocklistFingerprint adds a cert fingerprint to the blocklist
|
|
||||||
func (ncp *NebulaCAPool) BlocklistFingerprint(f string) {
|
|
||||||
ncp.certBlocklist[f] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetCertBlocklist removes all previously blocklisted cert fingerprints
|
|
||||||
func (ncp *NebulaCAPool) ResetCertBlocklist() {
|
|
||||||
ncp.certBlocklist = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: This uses an internal cache for Sha256Sum() that will not be invalidated
|
|
||||||
// automatically if you manually change any fields in the NebulaCertificate.
|
|
||||||
func (ncp *NebulaCAPool) IsBlocklisted(c *NebulaCertificate) bool {
|
|
||||||
return ncp.isBlocklistedWithCache(c, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsBlocklisted returns true if the fingerprint fails to generate or has been explicitly blocklisted
|
|
||||||
func (ncp *NebulaCAPool) isBlocklistedWithCache(c *NebulaCertificate, useCache bool) bool {
|
|
||||||
h, err := c.sha256SumWithCache(useCache)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := ncp.certBlocklist[h]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCAForCert attempts to return the signing certificate for the provided certificate.
|
|
||||||
// No signature validation is performed
|
|
||||||
func (ncp *NebulaCAPool) GetCAForCert(c *NebulaCertificate) (*NebulaCertificate, error) {
|
|
||||||
if c.Details.Issuer == "" {
|
|
||||||
return nil, fmt.Errorf("no issuer in certificate")
|
|
||||||
}
|
|
||||||
|
|
||||||
signer, ok := ncp.CAs[c.Details.Issuer]
|
|
||||||
if ok {
|
|
||||||
return signer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("could not find ca for the certificate")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFingerprints returns an array of trusted CA fingerprints
|
|
||||||
func (ncp *NebulaCAPool) GetFingerprints() []string {
|
|
||||||
fp := make([]string, len(ncp.CAs))
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for k := range ncp.CAs {
|
|
||||||
fp[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return fp
|
|
||||||
}
|
|
||||||
+345
@@ -0,0 +1,345 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CAPool struct {
|
||||||
|
CAs map[string]*CachedCertificate
|
||||||
|
certBlocklist map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCAPool creates an empty CAPool
|
||||||
|
func NewCAPool() *CAPool {
|
||||||
|
ca := CAPool{
|
||||||
|
CAs: make(map[string]*CachedCertificate),
|
||||||
|
certBlocklist: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ca
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCAPoolFromPEM will create a new CA pool from the provided
|
||||||
|
// input bytes, which must be a PEM-encoded set of nebula certificates.
|
||||||
|
// If the pool contains any expired certificates, an ErrExpired will be
|
||||||
|
// returned along with the pool. The caller must handle any such errors.
|
||||||
|
func NewCAPoolFromPEM(caPEMs []byte) (*CAPool, error) {
|
||||||
|
return NewCAPoolFromPEMReader(bytes.NewReader(caPEMs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCAPoolFromPEMReader will create a new CA pool from the provided reader.
|
||||||
|
// The reader must contain a PEM-encoded set of nebula certificates.
|
||||||
|
func NewCAPoolFromPEMReader(r io.Reader) (*CAPool, error) {
|
||||||
|
pool := NewCAPool()
|
||||||
|
|
||||||
|
var expired bool
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Split(SplitPEM)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
pemBytes := scanner.Bytes()
|
||||||
|
|
||||||
|
block, rest := pem.Decode(pemBytes)
|
||||||
|
if len(bytes.TrimSpace(rest)) > 0 {
|
||||||
|
return nil, ErrInvalidPEMBlock
|
||||||
|
}
|
||||||
|
if block == nil {
|
||||||
|
return nil, ErrInvalidPEMBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := unmarshalCertificateBlock(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pool.AddCA(c)
|
||||||
|
if errors.Is(err, ErrExpired) {
|
||||||
|
expired = true
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, ErrInvalidPEMBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
if expired {
|
||||||
|
return pool, ErrExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCAFromPEM verifies a Nebula CA certificate and adds it to the pool.
|
||||||
|
// Only the first pem encoded object will be consumed, any remaining bytes are returned.
|
||||||
|
// Parsed certificates will be verified and must be a CA
|
||||||
|
func (ncp *CAPool) AddCAFromPEM(pemBytes []byte) ([]byte, error) {
|
||||||
|
c, pemBytes, err := UnmarshalCertificateFromPEM(pemBytes)
|
||||||
|
if err != nil {
|
||||||
|
return pemBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ncp.AddCA(c)
|
||||||
|
if err != nil {
|
||||||
|
return pemBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pemBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCA verifies a Nebula CA certificate and adds it to the pool.
|
||||||
|
func (ncp *CAPool) AddCA(c Certificate) error {
|
||||||
|
if !c.IsCA() {
|
||||||
|
return fmt.Errorf("%s: %w", c.Name(), ErrNotCA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.CheckSignature(c.PublicKey()) {
|
||||||
|
return fmt.Errorf("%s: %w", c.Name(), ErrNotSelfSigned)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum, err := c.Fingerprint()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not calculate fingerprint for provided CA; error: %w; %s", err, c.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := &CachedCertificate{
|
||||||
|
Certificate: c,
|
||||||
|
Fingerprint: sum,
|
||||||
|
InvertedGroups: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range c.Groups() {
|
||||||
|
cc.InvertedGroups[g] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ncp.CAs[sum] = cc
|
||||||
|
|
||||||
|
if c.Expired(time.Now()) {
|
||||||
|
return fmt.Errorf("%s: %w", c.Name(), ErrExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlocklistFingerprint adds a cert fingerprint to the blocklist
|
||||||
|
func (ncp *CAPool) BlocklistFingerprint(f string) {
|
||||||
|
ncp.certBlocklist[f] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetCertBlocklist removes all previously blocklisted cert fingerprints
|
||||||
|
func (ncp *CAPool) ResetCertBlocklist() {
|
||||||
|
ncp.certBlocklist = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBlocklisted tests the provided fingerprint against the pools blocklist.
|
||||||
|
// Returns true if the fingerprint is blocked.
|
||||||
|
func (ncp *CAPool) IsBlocklisted(fingerprint string) bool {
|
||||||
|
if _, ok := ncp.certBlocklist[fingerprint]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCertificate verifies the certificate is valid and is signed by a trusted CA in the pool.
|
||||||
|
// If the certificate is valid then the returned CachedCertificate can be used in subsequent verification attempts
|
||||||
|
// to increase performance.
|
||||||
|
func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCertificate, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("no certificate")
|
||||||
|
}
|
||||||
|
fp, err := c.Fingerprint()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not calculate fingerprint to verify: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ncp.verify(c, now, fp, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre nebula v1.10.3 could generate signatures in either high or low s form and validation
|
||||||
|
// of signatures allowed for either. Nebula v1.10.3 and beyond clamps signature generation to low-s form
|
||||||
|
// but validation still allows for either. Since a change in the signature bytes affects the fingerprint, we
|
||||||
|
// need to test both forms until such a time comes that we enforce low-s form on signature validation.
|
||||||
|
fp2, err := CalculateAlternateFingerprint(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not calculate alternate fingerprint to verify: %w", err)
|
||||||
|
}
|
||||||
|
if fp2 != "" && ncp.IsBlocklisted(fp2) {
|
||||||
|
return nil, ErrBlockListed
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := CachedCertificate{
|
||||||
|
Certificate: c,
|
||||||
|
InvertedGroups: make(map[string]struct{}),
|
||||||
|
Fingerprint: fp,
|
||||||
|
fingerprint2: fp2,
|
||||||
|
signerFingerprint: signer.Fingerprint,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range c.Groups() {
|
||||||
|
cc.InvertedGroups[g] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCachedCertificate is the same as VerifyCertificate other than it operates on a pre-verified structure and
|
||||||
|
// is a cheaper operation to perform as a result.
|
||||||
|
func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error {
|
||||||
|
// Check any available alternate fingerprint forms for this certificate, re P256 high-s/low-s
|
||||||
|
if c.fingerprint2 != "" && ncp.IsBlocklisted(c.fingerprint2) {
|
||||||
|
return ErrBlockListed
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncp *CAPool) verify(c Certificate, now time.Time, certFp string, signerFp string) (*CachedCertificate, error) {
|
||||||
|
if ncp.IsBlocklisted(certFp) {
|
||||||
|
return nil, ErrBlockListed
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ncp.GetCAForCert(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if signer.Certificate.Curve() != c.Curve() {
|
||||||
|
return nil, ErrCurveMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
if signer.Certificate.Expired(now) {
|
||||||
|
return nil, ErrRootExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Expired(now) {
|
||||||
|
return nil, ErrExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are checking a cached certificate then we can bail early here
|
||||||
|
// Either the root is no longer trusted or everything is fine
|
||||||
|
if len(signerFp) > 0 {
|
||||||
|
if signerFp != signer.Fingerprint {
|
||||||
|
return nil, ErrFingerprintMismatch
|
||||||
|
}
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
if !c.CheckSignature(signer.Certificate.PublicKey()) {
|
||||||
|
return nil, ErrSignatureMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CheckCAConstraints(signer.Certificate, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCAForCert attempts to return the signing certificate for the provided certificate.
|
||||||
|
// No signature validation is performed
|
||||||
|
func (ncp *CAPool) GetCAForCert(c Certificate) (*CachedCertificate, error) {
|
||||||
|
issuer := c.Issuer()
|
||||||
|
if issuer == "" {
|
||||||
|
return nil, fmt.Errorf("no issuer in certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, ok := ncp.CAs[issuer]
|
||||||
|
if ok {
|
||||||
|
return signer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrCaNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFingerprints returns an array of trusted CA fingerprints
|
||||||
|
func (ncp *CAPool) GetFingerprints() []string {
|
||||||
|
fp := make([]string, len(ncp.CAs))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for k := range ncp.CAs {
|
||||||
|
fp[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return fp
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckCAConstraints returns an error if the sub certificate violates constraints present in the signer certificate.
|
||||||
|
func CheckCAConstraints(signer Certificate, sub Certificate) error {
|
||||||
|
return checkCAConstraints(signer, sub.NotBefore(), sub.NotAfter(), sub.Groups(), sub.Networks(), sub.UnsafeNetworks())
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkCAConstraints is a very generic function allowing both Certificates and TBSCertificates to be tested.
|
||||||
|
func checkCAConstraints(signer Certificate, notBefore, notAfter time.Time, groups []string, networks, unsafeNetworks []netip.Prefix) error {
|
||||||
|
// Make sure this cert isn't valid after the root
|
||||||
|
if notAfter.After(signer.NotAfter()) {
|
||||||
|
return fmt.Errorf("certificate expires after signing certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure this cert wasn't valid before the root
|
||||||
|
if notBefore.Before(signer.NotBefore()) {
|
||||||
|
return fmt.Errorf("certificate is valid before the signing certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the signer has a limited set of groups make sure the cert only contains a subset
|
||||||
|
signerGroups := signer.Groups()
|
||||||
|
if len(signerGroups) > 0 {
|
||||||
|
for _, g := range groups {
|
||||||
|
if !slices.Contains(signerGroups, g) {
|
||||||
|
return fmt.Errorf("certificate contained a group not present on the signing ca: %s", g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the signer has a limited set of ip ranges to issue from make sure the cert only contains a subset
|
||||||
|
signingNetworks := signer.Networks()
|
||||||
|
if len(signingNetworks) > 0 {
|
||||||
|
for _, certNetwork := range networks {
|
||||||
|
found := false
|
||||||
|
for _, signingNetwork := range signingNetworks {
|
||||||
|
if signingNetwork.Contains(certNetwork.Addr()) && signingNetwork.Bits() <= certNetwork.Bits() {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("certificate contained a network assignment outside the limitations of the signing ca: %s", certNetwork.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the signer has a limited set of subnet ranges to issue from make sure the cert only contains a subset
|
||||||
|
signingUnsafeNetworks := signer.UnsafeNetworks()
|
||||||
|
if len(signingUnsafeNetworks) > 0 {
|
||||||
|
for _, certUnsafeNetwork := range unsafeNetworks {
|
||||||
|
found := false
|
||||||
|
for _, caNetwork := range signingUnsafeNetworks {
|
||||||
|
if caNetwork.Contains(certUnsafeNetwork.Addr()) && caNetwork.Bits() <= certUnsafeNetwork.Bits() {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("certificate contained an unsafe network assignment outside the limitations of the signing ca: %s", certUnsafeNetwork.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,684 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert/p256"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCAPoolFromBytes(t *testing.T) {
|
||||||
|
noNewLines := `
|
||||||
|
# Current provisional, Remove once everything moves over to the real root.
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
Cj4KDm5lYnVsYSByb290IGNhKM0cMM24zPCvBzogV24YEw5YiqeI/oYo8XXFsoo+
|
||||||
|
PBmiOafNJhLacf9rsspAARJAz9OAnh8TKAUKix1kKVMyQU4iM3LsFfZRf6ODWXIf
|
||||||
|
2qWMpB6fpd3PSoVYziPoOt2bIHIFLlgRLPJz3I3xBEdBCQ==
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
# root-ca01
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
CkEKEW5lYnVsYSByb290IGNhIDAxKM0cMM24zPCvBzogPzbWTxt8ZgXPQEwup7Br
|
||||||
|
BrtIt1O0q5AuTRT3+t2x1VJAARJAZ+2ib23qBXjdy49oU1YysrwuKkWWKrtJ7Jye
|
||||||
|
rFBQpDXikOukhQD/mfkloFwJ+Yjsfru7IpTN4ZfjXL+kN/2sCA==
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
withNewLines := `
|
||||||
|
# Current provisional, Remove once everything moves over to the real root.
|
||||||
|
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
Cj4KDm5lYnVsYSByb290IGNhKM0cMM24zPCvBzogV24YEw5YiqeI/oYo8XXFsoo+
|
||||||
|
PBmiOafNJhLacf9rsspAARJAz9OAnh8TKAUKix1kKVMyQU4iM3LsFfZRf6ODWXIf
|
||||||
|
2qWMpB6fpd3PSoVYziPoOt2bIHIFLlgRLPJz3I3xBEdBCQ==
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
|
||||||
|
# root-ca01
|
||||||
|
|
||||||
|
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
CkEKEW5lYnVsYSByb290IGNhIDAxKM0cMM24zPCvBzogPzbWTxt8ZgXPQEwup7Br
|
||||||
|
BrtIt1O0q5AuTRT3+t2x1VJAARJAZ+2ib23qBXjdy49oU1YysrwuKkWWKrtJ7Jye
|
||||||
|
rFBQpDXikOukhQD/mfkloFwJ+Yjsfru7IpTN4ZfjXL+kN/2sCA==
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
expired := `
|
||||||
|
# expired certificate
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
CjMKB2V4cGlyZWQozRwwzRw6ICJSG94CqX8wn5I65Pwn25V6HftVfWeIySVtp2DA
|
||||||
|
7TY/QAESQMaAk5iJT5EnQwK524ZaaHGEJLUqqbh5yyOHhboIGiVTWkFeH3HccTW8
|
||||||
|
Tq5a8AyWDQdfXbtEZ1FwabeHfH5Asw0=
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
p256 := `
|
||||||
|
# p256 certificate
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
CmQKEG5lYnVsYSBQMjU2IHRlc3QozRwwzbjM8K8HOkEEdrmmg40zQp44AkMq6DZp
|
||||||
|
k+coOv04r+zh33ISyhbsafnYduN17p2eD7CmHvHuerguXD9f32gcxo/KsFCKEjMe
|
||||||
|
+0ABoAYBEkcwRQIgVoTg38L7uWku9xQgsr06kxZ/viQLOO/w1Qj1vFUEnhcCIQCq
|
||||||
|
75SjTiV92kv/1GcbT3wWpAZQQDBiUHVMVmh1822szA==
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
rootCA := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: "nebula root ca",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCA01 := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: "nebula root ca 01",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCAP256 := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: "nebula P256 test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewCAPoolFromPEM([]byte(noNewLines))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, p.CAs["ce4e6c7a596996eb0d82a8875f0f0137a4b53ce22d2421c9fd7150e7a26f6300"].Certificate.Name(), rootCA.details.name)
|
||||||
|
assert.Equal(t, p.CAs["04c585fcd9a49b276df956a22b7ebea3bf23f1fca5a17c0b56ce2e626631969e"].Certificate.Name(), rootCA01.details.name)
|
||||||
|
|
||||||
|
pp, err := NewCAPoolFromPEM([]byte(withNewLines))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, pp.CAs["ce4e6c7a596996eb0d82a8875f0f0137a4b53ce22d2421c9fd7150e7a26f6300"].Certificate.Name(), rootCA.details.name)
|
||||||
|
assert.Equal(t, pp.CAs["04c585fcd9a49b276df956a22b7ebea3bf23f1fca5a17c0b56ce2e626631969e"].Certificate.Name(), rootCA01.details.name)
|
||||||
|
|
||||||
|
// expired cert, no valid certs
|
||||||
|
ppp, err := NewCAPoolFromPEM([]byte(expired))
|
||||||
|
assert.Equal(t, ErrExpired, err)
|
||||||
|
assert.Equal(t, "expired", ppp.CAs["c39b35a0e8f246203fe4f32b9aa8bfd155f1ae6a6be9d78370641e43397f48f5"].Certificate.Name())
|
||||||
|
|
||||||
|
// expired cert, with valid certs
|
||||||
|
pppp, err := NewCAPoolFromPEM(append([]byte(expired), noNewLines...))
|
||||||
|
assert.Equal(t, ErrExpired, err)
|
||||||
|
assert.Equal(t, pppp.CAs["ce4e6c7a596996eb0d82a8875f0f0137a4b53ce22d2421c9fd7150e7a26f6300"].Certificate.Name(), rootCA.details.name)
|
||||||
|
assert.Equal(t, pppp.CAs["04c585fcd9a49b276df956a22b7ebea3bf23f1fca5a17c0b56ce2e626631969e"].Certificate.Name(), rootCA01.details.name)
|
||||||
|
assert.Equal(t, "expired", pppp.CAs["c39b35a0e8f246203fe4f32b9aa8bfd155f1ae6a6be9d78370641e43397f48f5"].Certificate.Name())
|
||||||
|
assert.Len(t, pppp.CAs, 3)
|
||||||
|
|
||||||
|
ppppp, err := NewCAPoolFromPEM([]byte(p256))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, ppppp.CAs["552bf7d99bec1fc775a0e4c324bf6d8f789b3078f1919c7960d2e5e0c351ee97"].Certificate.Name(), rootCAP256.details.name)
|
||||||
|
assert.Len(t, ppppp.CAs, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// oneByteReader wraps a reader to return at most 1 byte per Read call,
|
||||||
|
// exercising the streaming accumulation logic in NewCAPoolFromPEMReader.
|
||||||
|
type oneByteReader struct {
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *oneByteReader) Read(p []byte) (int, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return o.r.Read(p[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCAPoolFromPEMReader_EmptyReader(t *testing.T) {
|
||||||
|
pool, err := NewCAPoolFromPEMReader(bytes.NewReader(nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pool.CAs)
|
||||||
|
|
||||||
|
pool, err = NewCAPoolFromPEMReader(strings.NewReader(" \n\t\n "))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, pool.CAs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCAPoolFromPEMReader_OneByteReads(t *testing.T) {
|
||||||
|
ca1, _, _, pem1 := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(time.Hour), nil, nil, nil)
|
||||||
|
ca2, _, _, pem2 := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(time.Hour), nil, nil, nil)
|
||||||
|
|
||||||
|
bundle := append(pem1, pem2...)
|
||||||
|
pool, err := NewCAPoolFromPEMReader(&oneByteReader{r: bytes.NewReader(bundle)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, pool.CAs, 2)
|
||||||
|
|
||||||
|
fp1, err := ca1.Fingerprint()
|
||||||
|
require.NoError(t, err)
|
||||||
|
fp2, err := ca2.Fingerprint()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, pool.CAs, fp1)
|
||||||
|
assert.Contains(t, pool.CAs, fp2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCAPoolFromPEMReader_TruncatedPEM(t *testing.T) {
|
||||||
|
_, err := NewCAPoolFromPEMReader(strings.NewReader("-----BEGIN NEBULA CERTIFICATE-----\npartialdata"))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidPEMBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCAPoolFromPEMReader_TrailingGarbage(t *testing.T) {
|
||||||
|
_, _, _, pem1 := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(time.Hour), nil, nil, nil)
|
||||||
|
|
||||||
|
bundle := append(pem1, []byte("some trailing garbage")...)
|
||||||
|
_, err := NewCAPoolFromPEMReader(bytes.NewReader(bundle))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidPEMBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_Verify(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||||
|
c, _, _, _ := NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test cert", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||||
|
|
||||||
|
caPool := NewCAPool()
|
||||||
|
require.NoError(t, caPool.AddCA(ca))
|
||||||
|
|
||||||
|
f, err := c.Fingerprint()
|
||||||
|
require.NoError(t, err)
|
||||||
|
caPool.BlocklistFingerprint(f)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now().Add(time.Hour*1000), c)
|
||||||
|
require.EqualError(t, err, "root certificate is expired")
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate is valid before the signing certificate", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test cert2", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test group assertion
|
||||||
|
ca, _, caKey, _ = NewTestCaCert(Version1, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{"test1", "test2"})
|
||||||
|
caPem, err := ca.MarshalPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caPool = NewCAPool()
|
||||||
|
b, err := caPool.AddCAFromPEM(caPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate contained a group not present on the signing ca: bad", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1", "bad"})
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test2", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_VerifyP256(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_P256, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||||
|
c, _, _, _ := NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||||
|
|
||||||
|
caPool := NewCAPool()
|
||||||
|
require.NoError(t, caPool.AddCA(ca))
|
||||||
|
|
||||||
|
f, err := c.Fingerprint()
|
||||||
|
require.NoError(t, err)
|
||||||
|
caPool.BlocklistFingerprint(f)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
// Create a copy of the cert and swap to the alternate form for the signature
|
||||||
|
nc := c.Copy()
|
||||||
|
b, err := p256.Swap(c.Signature())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, nc.(*certificateV1).setSignature(b))
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), nc)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now().Add(time.Hour*1000), c)
|
||||||
|
require.EqualError(t, err, "root certificate is expired")
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate is valid before the signing certificate", func() {
|
||||||
|
NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test group assertion
|
||||||
|
ca, _, caKey, _ = NewTestCaCert(Version1, Curve_P256, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{"test1", "test2"})
|
||||||
|
caPem, err := ca.MarshalPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caPool = NewCAPool()
|
||||||
|
b, err = caPool.AddCAFromPEM(caPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate contained a group not present on the signing ca: bad", func() {
|
||||||
|
NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1", "bad"})
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
|
||||||
|
cc, err := caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Reset the blocklist and block the alternate form fingerprint
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
caPool.BlocklistFingerprint(cc.fingerprint2)
|
||||||
|
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_Verify_IPs(t *testing.T) {
|
||||||
|
caIp1 := mustParsePrefixUnmapped("10.0.0.0/16")
|
||||||
|
caIp2 := mustParsePrefixUnmapped("192.168.0.0/24")
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, 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.1.0.0/24")
|
||||||
|
cIp2 := mustParsePrefixUnmapped("192.168.0.1/16")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is outside the network reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.1.0.0/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip and mask are within the network
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/16")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/25")
|
||||||
|
c, _, _, _ := NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp1, caIp2}, nil, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp2, caIp1}, nil, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed with just 1
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp1}, nil, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_Verify_Subnets(t *testing.T) {
|
||||||
|
caIp1 := mustParsePrefixUnmapped("10.0.0.0/16")
|
||||||
|
caIp2 := mustParsePrefixUnmapped("192.168.0.0/24")
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, []netip.Prefix{caIp1, caIp2}, []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.1.0.0/24")
|
||||||
|
cIp2 := mustParsePrefixUnmapped("192.168.0.1/16")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is outside the network reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.1.0.0/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip and mask are within the network
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/16")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/25")
|
||||||
|
c, _, _, _ := NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp1, caIp2}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp2, caIp1}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed with just 1
|
||||||
|
c, _, _, _ = NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp1}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_Verify(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||||
|
c, _, _, _ := NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test cert", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||||
|
|
||||||
|
caPool := NewCAPool()
|
||||||
|
require.NoError(t, caPool.AddCA(ca))
|
||||||
|
|
||||||
|
f, err := c.Fingerprint()
|
||||||
|
require.NoError(t, err)
|
||||||
|
caPool.BlocklistFingerprint(f)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now().Add(time.Hour*1000), c)
|
||||||
|
require.EqualError(t, err, "root certificate is expired")
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate is valid before the signing certificate", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test cert2", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test group assertion
|
||||||
|
ca, _, caKey, _ = NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{"test1", "test2"})
|
||||||
|
caPem, err := ca.MarshalPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caPool = NewCAPool()
|
||||||
|
b, err := caPool.AddCAFromPEM(caPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate contained a group not present on the signing ca: bad", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1", "bad"})
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test2", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_VerifyP256(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_P256, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||||
|
c, _, _, _ := NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||||
|
|
||||||
|
caPool := NewCAPool()
|
||||||
|
require.NoError(t, caPool.AddCA(ca))
|
||||||
|
|
||||||
|
f, err := c.Fingerprint()
|
||||||
|
require.NoError(t, err)
|
||||||
|
caPool.BlocklistFingerprint(f)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
// Create a copy of the cert and swap to the alternate form for the signature
|
||||||
|
nc := c.Copy()
|
||||||
|
b, err := p256.Swap(c.Signature())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, nc.(*certificateV2).setSignature(b))
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), nc)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now().Add(time.Hour*1000), c)
|
||||||
|
require.EqualError(t, err, "root certificate is expired")
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate is valid before the signing certificate", func() {
|
||||||
|
NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test group assertion
|
||||||
|
ca, _, caKey, _ = NewTestCaCert(Version2, Curve_P256, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{"test1", "test2"})
|
||||||
|
caPem, err := ca.MarshalPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caPool = NewCAPool()
|
||||||
|
b, err = caPool.AddCAFromPEM(caPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
|
||||||
|
assert.PanicsWithError(t, "certificate contained a group not present on the signing ca: bad", func() {
|
||||||
|
NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1", "bad"})
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
|
||||||
|
cc, err := caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Reset the blocklist and block the alternate form fingerprint
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
caPool.BlocklistFingerprint(cc.fingerprint2)
|
||||||
|
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||||
|
require.EqualError(t, err, "certificate is in the block list")
|
||||||
|
|
||||||
|
caPool.ResetCertBlocklist()
|
||||||
|
err = caPool.VerifyCachedCertificate(time.Now(), cc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_Verify_IPs(t *testing.T) {
|
||||||
|
caIp1 := mustParsePrefixUnmapped("10.0.0.0/16")
|
||||||
|
caIp2 := mustParsePrefixUnmapped("192.168.0.0/24")
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_CURVE25519, 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.1.0.0/24")
|
||||||
|
cIp2 := mustParsePrefixUnmapped("192.168.0.1/16")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is outside the network reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.1.0.0/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
assert.PanicsWithError(t, "certificate contained a network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip and mask are within the network
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/16")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/25")
|
||||||
|
c, _, _, _ := NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1, cIp2}, nil, []string{"test"})
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp1, caIp2}, nil, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp2, caIp1}, nil, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed with just 1
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{caIp1}, nil, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_Verify_Subnets(t *testing.T) {
|
||||||
|
caIp1 := mustParsePrefixUnmapped("10.0.0.0/16")
|
||||||
|
caIp2 := mustParsePrefixUnmapped("192.168.0.0/24")
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, []netip.Prefix{caIp1, caIp2}, []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.1.0.0/24")
|
||||||
|
cIp2 := mustParsePrefixUnmapped("192.168.0.1/16")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is outside the network reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.1.0.0/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.1.0.0/24", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip is within the network but mask is outside reversed order of above
|
||||||
|
cIp1 = mustParsePrefixUnmapped("192.168.0.1/24")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("10.0.1.0/15")
|
||||||
|
assert.PanicsWithError(t, "certificate contained an unsafe network assignment outside the limitations of the signing ca: 10.0.1.0/15", func() {
|
||||||
|
NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ip and mask are within the network
|
||||||
|
cIp1 = mustParsePrefixUnmapped("10.0.1.0/16")
|
||||||
|
cIp2 = mustParsePrefixUnmapped("192.168.0.1/25")
|
||||||
|
c, _, _, _ := NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{cIp1, cIp2}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp1, caIp2}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp2, caIp1}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Exact matches reversed with just 1
|
||||||
|
c, _, _, _ = NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, []netip.Prefix{caIp1}, []string{"test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_CurveMismatch(t *testing.T) {
|
||||||
|
caIp1 := mustParsePrefixUnmapped("10.0.0.0/16")
|
||||||
|
caIp2 := mustParsePrefixUnmapped("192.168.0.0/24")
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_P256, time.Now(), time.Now().Add(10*time.Minute), []netip.Prefix{caIp1, caIp2}, nil, []string{"test"})
|
||||||
|
|
||||||
|
caPem, err := ca.MarshalPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caPool := NewCAPool()
|
||||||
|
b, err := caPool.AddCAFromPEM(caPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
|
||||||
|
// ip is outside the network
|
||||||
|
cIp1 := mustParsePrefixUnmapped("10.0.0.1/24")
|
||||||
|
c, _, _, _ := NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), []netip.Prefix{cIp1}, nil, []string{"test"})
|
||||||
|
|
||||||
|
fp, _ := c.Fingerprint()
|
||||||
|
_, err = caPool.verify(c, time.Now(), fp, c.Issuer())
|
||||||
|
require.NoError(t, err)
|
||||||
|
//
|
||||||
|
c2 := c.(*certificateV2)
|
||||||
|
c2.curve = Curve_CURVE25519
|
||||||
|
fp, _ = c.Fingerprint()
|
||||||
|
_, err = caPool.verify(c, time.Now(), fp, c.Issuer())
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
+146
-992
File diff suppressed because it is too large
Load Diff
-1251
File diff suppressed because it is too large
Load Diff
+505
@@ -0,0 +1,505 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const publicKeyLen = 32
|
||||||
|
|
||||||
|
type certificateV1 struct {
|
||||||
|
details detailsV1
|
||||||
|
signature []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type detailsV1 struct {
|
||||||
|
name string
|
||||||
|
networks []netip.Prefix
|
||||||
|
unsafeNetworks []netip.Prefix
|
||||||
|
groups []string
|
||||||
|
notBefore time.Time
|
||||||
|
notAfter time.Time
|
||||||
|
publicKey []byte
|
||||||
|
isCA bool
|
||||||
|
issuer string
|
||||||
|
|
||||||
|
curve Curve
|
||||||
|
}
|
||||||
|
|
||||||
|
type m = map[string]any
|
||||||
|
|
||||||
|
func (c *certificateV1) Version() Version {
|
||||||
|
return Version1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Curve() Curve {
|
||||||
|
return c.details.curve
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Groups() []string {
|
||||||
|
return c.details.groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) IsCA() bool {
|
||||||
|
return c.details.isCA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Issuer() string {
|
||||||
|
return c.details.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Name() string {
|
||||||
|
return c.details.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Networks() []netip.Prefix {
|
||||||
|
return c.details.networks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) NotAfter() time.Time {
|
||||||
|
return c.details.notAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) NotBefore() time.Time {
|
||||||
|
return c.details.notBefore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) PublicKey() []byte {
|
||||||
|
return c.details.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) MarshalPublicKeyPEM() []byte {
|
||||||
|
return marshalCertPublicKeyToPEM(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Signature() []byte {
|
||||||
|
return c.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) UnsafeNetworks() []netip.Prefix {
|
||||||
|
return c.details.unsafeNetworks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Fingerprint() (string, error) {
|
||||||
|
b, err := c.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(b)
|
||||||
|
return hex.EncodeToString(sum[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) CheckSignature(key []byte) bool {
|
||||||
|
b, err := proto.Marshal(c.getRawDetails())
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch c.details.curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
if len(key) != ed25519.PublicKeySize {
|
||||||
|
return false //avoids a panic internal to ed25519
|
||||||
|
}
|
||||||
|
return ed25519.Verify(key, b, c.signature)
|
||||||
|
case Curve_P256:
|
||||||
|
pubKey, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), key)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hashed := sha256.Sum256(b)
|
||||||
|
return ecdsa.VerifyASN1(pubKey, hashed[:], c.signature)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Expired(t time.Time) bool {
|
||||||
|
return c.details.notBefore.After(t) || c.details.notAfter.Before(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) VerifyPrivateKey(curve Curve, key []byte) error {
|
||||||
|
if curve != c.details.curve {
|
||||||
|
return fmt.Errorf("curve in cert and private key supplied don't match")
|
||||||
|
}
|
||||||
|
if c.details.isCA {
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
// the call to PublicKey below will panic slice bounds out of range otherwise
|
||||||
|
if len(key) != ed25519.PrivateKeySize {
|
||||||
|
return fmt.Errorf("key was not 64 bytes, is invalid ed25519 private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ed25519.PublicKey(c.details.publicKey).Equal(ed25519.PrivateKey(key).Public()) {
|
||||||
|
return fmt.Errorf("public key in cert and private key supplied don't match")
|
||||||
|
}
|
||||||
|
case Curve_P256:
|
||||||
|
privkey, err := ecdh.P256().NewPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse private key as P256: %w", err)
|
||||||
|
}
|
||||||
|
pub := privkey.PublicKey().Bytes()
|
||||||
|
if !bytes.Equal(pub, c.details.publicKey) {
|
||||||
|
return fmt.Errorf("public key in cert and private key supplied don't match")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid curve: %s", curve)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub []byte
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
var err error
|
||||||
|
pub, err = curve25519.X25519(key, curve25519.Basepoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Curve_P256:
|
||||||
|
privkey, err := ecdh.P256().NewPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pub = privkey.PublicKey().Bytes()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid curve: %s", curve)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(pub, c.details.publicKey) {
|
||||||
|
return fmt.Errorf("public key in cert and private key supplied don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRawDetails marshals the raw details into protobuf ready struct
|
||||||
|
func (c *certificateV1) getRawDetails() *RawNebulaCertificateDetails {
|
||||||
|
rd := &RawNebulaCertificateDetails{
|
||||||
|
Name: c.details.name,
|
||||||
|
Groups: c.details.groups,
|
||||||
|
NotBefore: c.details.notBefore.Unix(),
|
||||||
|
NotAfter: c.details.notAfter.Unix(),
|
||||||
|
PublicKey: make([]byte, len(c.details.publicKey)),
|
||||||
|
IsCA: c.details.isCA,
|
||||||
|
Curve: c.details.curve,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipNet := range c.details.networks {
|
||||||
|
mask := net.CIDRMask(ipNet.Bits(), ipNet.Addr().BitLen())
|
||||||
|
rd.Ips = append(rd.Ips, addr2int(ipNet.Addr()), ip2int(mask))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipNet := range c.details.unsafeNetworks {
|
||||||
|
mask := net.CIDRMask(ipNet.Bits(), ipNet.Addr().BitLen())
|
||||||
|
rd.Subnets = append(rd.Subnets, addr2int(ipNet.Addr()), ip2int(mask))
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(rd.PublicKey, c.details.publicKey[:])
|
||||||
|
|
||||||
|
// I know, this is terrible
|
||||||
|
rd.Issuer, _ = hex.DecodeString(c.details.issuer)
|
||||||
|
|
||||||
|
return rd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) String() string {
|
||||||
|
b, err := json.MarshalIndent(c.marshalJSON(), "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<error marshalling certificate: %v>", err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) MarshalForHandshakes() ([]byte, error) {
|
||||||
|
pubKey := c.details.publicKey
|
||||||
|
c.details.publicKey = nil
|
||||||
|
rawCertNoKey, err := c.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.details.publicKey = pubKey
|
||||||
|
return rawCertNoKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Marshal() ([]byte, error) {
|
||||||
|
rc := RawNebulaCertificate{
|
||||||
|
Details: c.getRawDetails(),
|
||||||
|
Signature: c.signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto.Marshal(&rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) MarshalPEM() ([]byte, error) {
|
||||||
|
b, err := c.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: CertificateBanner, Bytes: b}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c.marshalJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) marshalJSON() m {
|
||||||
|
fp, _ := c.Fingerprint()
|
||||||
|
return m{
|
||||||
|
"version": Version1,
|
||||||
|
"details": m{
|
||||||
|
"name": c.details.name,
|
||||||
|
"networks": c.details.networks,
|
||||||
|
"unsafeNetworks": c.details.unsafeNetworks,
|
||||||
|
"groups": c.details.groups,
|
||||||
|
"notBefore": c.details.notBefore,
|
||||||
|
"notAfter": c.details.notAfter,
|
||||||
|
"publicKey": fmt.Sprintf("%x", c.details.publicKey),
|
||||||
|
"isCa": c.details.isCA,
|
||||||
|
"issuer": c.details.issuer,
|
||||||
|
"curve": c.details.curve.String(),
|
||||||
|
},
|
||||||
|
"fingerprint": fp,
|
||||||
|
"signature": fmt.Sprintf("%x", c.Signature()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) Copy() Certificate {
|
||||||
|
nc := &certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: c.details.name,
|
||||||
|
notBefore: c.details.notBefore,
|
||||||
|
notAfter: c.details.notAfter,
|
||||||
|
publicKey: make([]byte, len(c.details.publicKey)),
|
||||||
|
isCA: c.details.isCA,
|
||||||
|
issuer: c.details.issuer,
|
||||||
|
curve: c.details.curve,
|
||||||
|
},
|
||||||
|
signature: make([]byte, len(c.signature)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.details.groups != nil {
|
||||||
|
nc.details.groups = make([]string, len(c.details.groups))
|
||||||
|
copy(nc.details.groups, c.details.groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.details.networks != nil {
|
||||||
|
nc.details.networks = make([]netip.Prefix, len(c.details.networks))
|
||||||
|
copy(nc.details.networks, c.details.networks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.details.unsafeNetworks != nil {
|
||||||
|
nc.details.unsafeNetworks = make([]netip.Prefix, len(c.details.unsafeNetworks))
|
||||||
|
copy(nc.details.unsafeNetworks, c.details.unsafeNetworks)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(nc.signature, c.signature)
|
||||||
|
copy(nc.details.publicKey, c.details.publicKey)
|
||||||
|
|
||||||
|
return nc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) fromTBSCertificate(t *TBSCertificate) error {
|
||||||
|
c.details = detailsV1{
|
||||||
|
name: t.Name,
|
||||||
|
networks: t.Networks,
|
||||||
|
unsafeNetworks: t.UnsafeNetworks,
|
||||||
|
groups: t.Groups,
|
||||||
|
notBefore: t.NotBefore,
|
||||||
|
notAfter: t.NotAfter,
|
||||||
|
publicKey: t.PublicKey,
|
||||||
|
isCA: t.IsCA,
|
||||||
|
curve: t.Curve,
|
||||||
|
issuer: t.issuer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) validate() error {
|
||||||
|
// Empty names are allowed
|
||||||
|
|
||||||
|
if len(c.details.publicKey) == 0 {
|
||||||
|
return ErrInvalidPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original v1 rules allowed multiple networks to be present but ignored all but the first one.
|
||||||
|
// Continue to allow this behavior
|
||||||
|
if !c.details.isCA && len(c.details.networks) == 0 {
|
||||||
|
return NewErrInvalidCertificateProperties("non-CA certificates must contain exactly one network")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range c.details.networks {
|
||||||
|
if !network.IsValid() || !network.Addr().IsValid() {
|
||||||
|
return NewErrInvalidCertificateProperties("invalid network: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Is6() {
|
||||||
|
return NewErrInvalidCertificateProperties("certificate may not contain IPv6 networks: %v", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().IsUnspecified() {
|
||||||
|
return NewErrInvalidCertificateProperties("non-CA certificates must not use the zero address as a network: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Zone() != "" {
|
||||||
|
return NewErrInvalidCertificateProperties("networks may not contain zones: %s", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range c.details.unsafeNetworks {
|
||||||
|
if !network.IsValid() || !network.Addr().IsValid() {
|
||||||
|
return NewErrInvalidCertificateProperties("invalid unsafe network: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Is6() {
|
||||||
|
return NewErrInvalidCertificateProperties("certificate may not contain IPv6 unsafe networks: %v", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Zone() != "" {
|
||||||
|
return NewErrInvalidCertificateProperties("unsafe networks may not contain zones: %s", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1 doesn't bother with sort order or uniqueness of networks or unsafe networks.
|
||||||
|
// We can't modify the unmarshalled data because verification requires re-marshalling and a re-ordered
|
||||||
|
// unsafe networks would result in a different signature.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) marshalForSigning() ([]byte, error) {
|
||||||
|
b, err := proto.Marshal(c.getRawDetails())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV1) setSignature(b []byte) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return ErrEmptySignature
|
||||||
|
}
|
||||||
|
c.signature = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalCertificateV1 will unmarshal a protobuf byte representation of a nebula cert
|
||||||
|
// if the publicKey is provided here then it is not required to be present in `b`
|
||||||
|
func unmarshalCertificateV1(b []byte, publicKey []byte) (*certificateV1, error) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil, fmt.Errorf("nil byte array")
|
||||||
|
}
|
||||||
|
var rc RawNebulaCertificate
|
||||||
|
err := proto.Unmarshal(b, &rc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.Details == nil {
|
||||||
|
return nil, fmt.Errorf("encoded Details was nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rc.Details.Ips)%2 != 0 {
|
||||||
|
return nil, fmt.Errorf("encoded IPs should be in pairs, an odd number was found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rc.Details.Subnets)%2 != 0 {
|
||||||
|
return nil, fmt.Errorf("encoded Subnets should be in pairs, an odd number was found")
|
||||||
|
}
|
||||||
|
|
||||||
|
nc := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: rc.Details.Name,
|
||||||
|
groups: make([]string, len(rc.Details.Groups)),
|
||||||
|
networks: make([]netip.Prefix, len(rc.Details.Ips)/2),
|
||||||
|
unsafeNetworks: make([]netip.Prefix, len(rc.Details.Subnets)/2),
|
||||||
|
notBefore: time.Unix(rc.Details.NotBefore, 0),
|
||||||
|
notAfter: time.Unix(rc.Details.NotAfter, 0),
|
||||||
|
publicKey: nil,
|
||||||
|
isCA: rc.Details.IsCA,
|
||||||
|
curve: rc.Details.Curve,
|
||||||
|
},
|
||||||
|
signature: make([]byte, len(rc.Signature)),
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(nc.signature, rc.Signature)
|
||||||
|
copy(nc.details.groups, rc.Details.Groups)
|
||||||
|
nc.details.issuer = hex.EncodeToString(rc.Details.Issuer)
|
||||||
|
|
||||||
|
// If a public key is passed in as an argument, the certificate pubkey must be empty
|
||||||
|
// and the passed-in pubkey copied into the cert.
|
||||||
|
if len(publicKey) > 0 {
|
||||||
|
if len(rc.Details.PublicKey) != 0 {
|
||||||
|
return nil, ErrCertPubkeyPresent
|
||||||
|
}
|
||||||
|
nc.details.publicKey = make([]byte, len(publicKey))
|
||||||
|
copy(nc.details.publicKey, publicKey)
|
||||||
|
} else {
|
||||||
|
nc.details.publicKey = make([]byte, len(rc.Details.PublicKey))
|
||||||
|
copy(nc.details.publicKey, rc.Details.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip netip.Addr
|
||||||
|
for i, rawIp := range rc.Details.Ips {
|
||||||
|
if i%2 == 0 {
|
||||||
|
ip = int2addr(rawIp)
|
||||||
|
} else {
|
||||||
|
ones, _ := net.IPMask(int2ip(rawIp)).Size()
|
||||||
|
nc.details.networks[i/2] = netip.PrefixFrom(ip, ones)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, rawIp := range rc.Details.Subnets {
|
||||||
|
if i%2 == 0 {
|
||||||
|
ip = int2addr(rawIp)
|
||||||
|
} else {
|
||||||
|
ones, _ := net.IPMask(int2ip(rawIp)).Size()
|
||||||
|
nc.details.unsafeNetworks[i/2] = netip.PrefixFrom(ip, ones)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nc.validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &nc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ip2int(ip []byte) uint32 {
|
||||||
|
if len(ip) == 16 {
|
||||||
|
return binary.BigEndian.Uint32(ip[12:16])
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func int2ip(nn uint32) net.IP {
|
||||||
|
ip := make(net.IP, net.IPv4len)
|
||||||
|
binary.BigEndian.PutUint32(ip, nn)
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func addr2int(addr netip.Addr) uint32 {
|
||||||
|
b := addr.Unmap().As4()
|
||||||
|
return binary.BigEndian.Uint32(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func int2addr(nn uint32) netip.Addr {
|
||||||
|
ip := [4]byte{}
|
||||||
|
binary.BigEndian.PutUint32(ip[:], nn)
|
||||||
|
return netip.AddrFrom4(ip).Unmap()
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.30.0
|
// protoc-gen-go v1.34.2
|
||||||
// protoc v3.21.5
|
// protoc v3.21.5
|
||||||
// source: cert.proto
|
// source: cert_v1.proto
|
||||||
|
|
||||||
package cert
|
package cert
|
||||||
|
|
||||||
@@ -50,11 +50,11 @@ func (x Curve) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (Curve) Descriptor() protoreflect.EnumDescriptor {
|
func (Curve) Descriptor() protoreflect.EnumDescriptor {
|
||||||
return file_cert_proto_enumTypes[0].Descriptor()
|
return file_cert_v1_proto_enumTypes[0].Descriptor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Curve) Type() protoreflect.EnumType {
|
func (Curve) Type() protoreflect.EnumType {
|
||||||
return &file_cert_proto_enumTypes[0]
|
return &file_cert_v1_proto_enumTypes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x Curve) Number() protoreflect.EnumNumber {
|
func (x Curve) Number() protoreflect.EnumNumber {
|
||||||
@@ -63,7 +63,7 @@ func (x Curve) Number() protoreflect.EnumNumber {
|
|||||||
|
|
||||||
// Deprecated: Use Curve.Descriptor instead.
|
// Deprecated: Use Curve.Descriptor instead.
|
||||||
func (Curve) EnumDescriptor() ([]byte, []int) {
|
func (Curve) EnumDescriptor() ([]byte, []int) {
|
||||||
return file_cert_proto_rawDescGZIP(), []int{0}
|
return file_cert_v1_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawNebulaCertificate struct {
|
type RawNebulaCertificate struct {
|
||||||
@@ -78,7 +78,7 @@ type RawNebulaCertificate struct {
|
|||||||
func (x *RawNebulaCertificate) Reset() {
|
func (x *RawNebulaCertificate) Reset() {
|
||||||
*x = RawNebulaCertificate{}
|
*x = RawNebulaCertificate{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_cert_proto_msgTypes[0]
|
mi := &file_cert_v1_proto_msgTypes[0]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func (x *RawNebulaCertificate) String() string {
|
|||||||
func (*RawNebulaCertificate) ProtoMessage() {}
|
func (*RawNebulaCertificate) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RawNebulaCertificate) ProtoReflect() protoreflect.Message {
|
func (x *RawNebulaCertificate) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_cert_proto_msgTypes[0]
|
mi := &file_cert_v1_proto_msgTypes[0]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -104,7 +104,7 @@ func (x *RawNebulaCertificate) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RawNebulaCertificate.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RawNebulaCertificate.ProtoReflect.Descriptor instead.
|
||||||
func (*RawNebulaCertificate) Descriptor() ([]byte, []int) {
|
func (*RawNebulaCertificate) Descriptor() ([]byte, []int) {
|
||||||
return file_cert_proto_rawDescGZIP(), []int{0}
|
return file_cert_v1_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RawNebulaCertificate) GetDetails() *RawNebulaCertificateDetails {
|
func (x *RawNebulaCertificate) GetDetails() *RawNebulaCertificateDetails {
|
||||||
@@ -143,7 +143,7 @@ type RawNebulaCertificateDetails struct {
|
|||||||
func (x *RawNebulaCertificateDetails) Reset() {
|
func (x *RawNebulaCertificateDetails) Reset() {
|
||||||
*x = RawNebulaCertificateDetails{}
|
*x = RawNebulaCertificateDetails{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_cert_proto_msgTypes[1]
|
mi := &file_cert_v1_proto_msgTypes[1]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ func (x *RawNebulaCertificateDetails) String() string {
|
|||||||
func (*RawNebulaCertificateDetails) ProtoMessage() {}
|
func (*RawNebulaCertificateDetails) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RawNebulaCertificateDetails) ProtoReflect() protoreflect.Message {
|
func (x *RawNebulaCertificateDetails) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_cert_proto_msgTypes[1]
|
mi := &file_cert_v1_proto_msgTypes[1]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -169,7 +169,7 @@ func (x *RawNebulaCertificateDetails) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RawNebulaCertificateDetails.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RawNebulaCertificateDetails.ProtoReflect.Descriptor instead.
|
||||||
func (*RawNebulaCertificateDetails) Descriptor() ([]byte, []int) {
|
func (*RawNebulaCertificateDetails) Descriptor() ([]byte, []int) {
|
||||||
return file_cert_proto_rawDescGZIP(), []int{1}
|
return file_cert_v1_proto_rawDescGZIP(), []int{1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RawNebulaCertificateDetails) GetName() string {
|
func (x *RawNebulaCertificateDetails) GetName() string {
|
||||||
@@ -254,7 +254,7 @@ type RawNebulaEncryptedData struct {
|
|||||||
func (x *RawNebulaEncryptedData) Reset() {
|
func (x *RawNebulaEncryptedData) Reset() {
|
||||||
*x = RawNebulaEncryptedData{}
|
*x = RawNebulaEncryptedData{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_cert_proto_msgTypes[2]
|
mi := &file_cert_v1_proto_msgTypes[2]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func (x *RawNebulaEncryptedData) String() string {
|
|||||||
func (*RawNebulaEncryptedData) ProtoMessage() {}
|
func (*RawNebulaEncryptedData) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RawNebulaEncryptedData) ProtoReflect() protoreflect.Message {
|
func (x *RawNebulaEncryptedData) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_cert_proto_msgTypes[2]
|
mi := &file_cert_v1_proto_msgTypes[2]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -280,7 +280,7 @@ func (x *RawNebulaEncryptedData) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RawNebulaEncryptedData.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RawNebulaEncryptedData.ProtoReflect.Descriptor instead.
|
||||||
func (*RawNebulaEncryptedData) Descriptor() ([]byte, []int) {
|
func (*RawNebulaEncryptedData) Descriptor() ([]byte, []int) {
|
||||||
return file_cert_proto_rawDescGZIP(), []int{2}
|
return file_cert_v1_proto_rawDescGZIP(), []int{2}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RawNebulaEncryptedData) GetEncryptionMetadata() *RawNebulaEncryptionMetadata {
|
func (x *RawNebulaEncryptedData) GetEncryptionMetadata() *RawNebulaEncryptionMetadata {
|
||||||
@@ -309,7 +309,7 @@ type RawNebulaEncryptionMetadata struct {
|
|||||||
func (x *RawNebulaEncryptionMetadata) Reset() {
|
func (x *RawNebulaEncryptionMetadata) Reset() {
|
||||||
*x = RawNebulaEncryptionMetadata{}
|
*x = RawNebulaEncryptionMetadata{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_cert_proto_msgTypes[3]
|
mi := &file_cert_v1_proto_msgTypes[3]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ func (x *RawNebulaEncryptionMetadata) String() string {
|
|||||||
func (*RawNebulaEncryptionMetadata) ProtoMessage() {}
|
func (*RawNebulaEncryptionMetadata) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RawNebulaEncryptionMetadata) ProtoReflect() protoreflect.Message {
|
func (x *RawNebulaEncryptionMetadata) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_cert_proto_msgTypes[3]
|
mi := &file_cert_v1_proto_msgTypes[3]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -335,7 +335,7 @@ func (x *RawNebulaEncryptionMetadata) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RawNebulaEncryptionMetadata.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RawNebulaEncryptionMetadata.ProtoReflect.Descriptor instead.
|
||||||
func (*RawNebulaEncryptionMetadata) Descriptor() ([]byte, []int) {
|
func (*RawNebulaEncryptionMetadata) Descriptor() ([]byte, []int) {
|
||||||
return file_cert_proto_rawDescGZIP(), []int{3}
|
return file_cert_v1_proto_rawDescGZIP(), []int{3}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RawNebulaEncryptionMetadata) GetEncryptionAlgorithm() string {
|
func (x *RawNebulaEncryptionMetadata) GetEncryptionAlgorithm() string {
|
||||||
@@ -367,7 +367,7 @@ type RawNebulaArgon2Parameters struct {
|
|||||||
func (x *RawNebulaArgon2Parameters) Reset() {
|
func (x *RawNebulaArgon2Parameters) Reset() {
|
||||||
*x = RawNebulaArgon2Parameters{}
|
*x = RawNebulaArgon2Parameters{}
|
||||||
if protoimpl.UnsafeEnabled {
|
if protoimpl.UnsafeEnabled {
|
||||||
mi := &file_cert_proto_msgTypes[4]
|
mi := &file_cert_v1_proto_msgTypes[4]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -380,7 +380,7 @@ func (x *RawNebulaArgon2Parameters) String() string {
|
|||||||
func (*RawNebulaArgon2Parameters) ProtoMessage() {}
|
func (*RawNebulaArgon2Parameters) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RawNebulaArgon2Parameters) ProtoReflect() protoreflect.Message {
|
func (x *RawNebulaArgon2Parameters) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_cert_proto_msgTypes[4]
|
mi := &file_cert_v1_proto_msgTypes[4]
|
||||||
if protoimpl.UnsafeEnabled && x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -393,7 +393,7 @@ func (x *RawNebulaArgon2Parameters) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RawNebulaArgon2Parameters.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RawNebulaArgon2Parameters.ProtoReflect.Descriptor instead.
|
||||||
func (*RawNebulaArgon2Parameters) Descriptor() ([]byte, []int) {
|
func (*RawNebulaArgon2Parameters) Descriptor() ([]byte, []int) {
|
||||||
return file_cert_proto_rawDescGZIP(), []int{4}
|
return file_cert_v1_proto_rawDescGZIP(), []int{4}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RawNebulaArgon2Parameters) GetVersion() int32 {
|
func (x *RawNebulaArgon2Parameters) GetVersion() int32 {
|
||||||
@@ -431,87 +431,87 @@ func (x *RawNebulaArgon2Parameters) GetSalt() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var File_cert_proto protoreflect.FileDescriptor
|
var File_cert_v1_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
var file_cert_proto_rawDesc = []byte{
|
var file_cert_v1_proto_rawDesc = []byte{
|
||||||
0x0a, 0x0a, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x63, 0x65,
|
0x0a, 0x0d, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||||
0x72, 0x74, 0x22, 0x71, 0x0a, 0x14, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43,
|
0x04, 0x63, 0x65, 0x72, 0x74, 0x22, 0x71, 0x0a, 0x14, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
|
||||||
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x07, 0x44, 0x65,
|
0x6c, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a,
|
||||||
0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x65,
|
0x07, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21,
|
||||||
0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43, 0x65, 0x72, 0x74,
|
0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43,
|
||||||
0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07,
|
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c,
|
||||||
0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61,
|
0x73, 0x52, 0x07, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x69,
|
||||||
0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53, 0x69, 0x67, 0x6e,
|
0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53,
|
||||||
0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9c, 0x02, 0x0a, 0x1b, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62,
|
0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x9c, 0x02, 0x0a, 0x1b, 0x52, 0x61, 0x77,
|
||||||
0x75, 0x6c, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x44, 0x65,
|
0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
|
||||||
0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
|
0x65, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x49, 0x70, 0x73,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03,
|
||||||
0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x49, 0x70, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x53,
|
0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x49, 0x70, 0x73, 0x12, 0x18,
|
||||||
0x75, 0x62, 0x6e, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x07, 0x53, 0x75,
|
0x0a, 0x07, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52,
|
||||||
0x62, 0x6e, 0x65, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18,
|
0x07, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x47, 0x72, 0x6f, 0x75,
|
||||||
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x1c, 0x0a,
|
0x70, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73,
|
||||||
0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
|
0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20,
|
||||||
0x52, 0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x4e,
|
0x01, 0x28, 0x03, 0x52, 0x09, 0x4e, 0x6f, 0x74, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1a,
|
||||||
0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x4e,
|
0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03,
|
||||||
0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x75, 0x62, 0x6c, 0x69,
|
0x52, 0x08, 0x4e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x75,
|
||||||
0x63, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x50, 0x75, 0x62, 0x6c,
|
0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x50,
|
||||||
0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x73, 0x43, 0x41, 0x18, 0x08, 0x20,
|
0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x49, 0x73, 0x43, 0x41,
|
||||||
0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x73, 0x43, 0x41, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x73, 0x73,
|
0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x49, 0x73, 0x43, 0x41, 0x12, 0x16, 0x0a, 0x06,
|
||||||
0x75, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65,
|
0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x49, 0x73,
|
||||||
0x72, 0x12, 0x21, 0x0a, 0x05, 0x63, 0x75, 0x72, 0x76, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e,
|
0x73, 0x75, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x05, 0x63, 0x75, 0x72, 0x76, 0x65, 0x18, 0x64, 0x20,
|
||||||
0x32, 0x0b, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x52, 0x05, 0x63,
|
0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x43, 0x75, 0x72, 0x76, 0x65,
|
||||||
0x75, 0x72, 0x76, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x16, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
|
0x52, 0x05, 0x63, 0x75, 0x72, 0x76, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x16, 0x52, 0x61, 0x77, 0x4e,
|
||||||
0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x12,
|
0x65, 0x62, 0x75, 0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x44, 0x61,
|
||||||
0x51, 0x0a, 0x12, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
|
0x74, 0x61, 0x12, 0x51, 0x0a, 0x12, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x65,
|
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21,
|
||||||
0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72,
|
0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x45,
|
||||||
0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12,
|
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
|
||||||
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
0x61, 0x52, 0x12, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
|
||||||
0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65, 0x78, 0x74,
|
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74,
|
||||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x74, 0x65,
|
0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x43, 0x69, 0x70, 0x68, 0x65,
|
||||||
0x78, 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x1b, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61,
|
0x72, 0x74, 0x65, 0x78, 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x1b, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62,
|
||||||
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
0x75, 0x6c, 0x61, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
|
||||||
0x74, 0x61, 0x12, 0x30, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
|
||||||
0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x01, 0x20, 0x01,
|
||||||
0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x72,
|
0x28, 0x09, 0x52, 0x13, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x6c,
|
||||||
0x69, 0x74, 0x68, 0x6d, 0x12, 0x4b, 0x0a, 0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61,
|
0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x4b, 0x0a, 0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e,
|
||||||
0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f,
|
0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||||
0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x41,
|
0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x2e, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
|
||||||
0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52,
|
0x6c, 0x61, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65,
|
||||||
0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72,
|
0x72, 0x73, 0x52, 0x10, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65,
|
||||||
0x73, 0x22, 0xa3, 0x01, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x41,
|
0x74, 0x65, 0x72, 0x73, 0x22, 0xa3, 0x01, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x4e, 0x65, 0x62, 0x75,
|
||||||
0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12,
|
0x6c, 0x61, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65,
|
||||||
0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
|
0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20,
|
||||||
0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d,
|
0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06,
|
||||||
0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72,
|
0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6d, 0x65,
|
||||||
0x79, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c, 0x69, 0x73, 0x6d,
|
0x6d, 0x6f, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c,
|
||||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c, 0x6c, 0x65, 0x6c,
|
0x69, 0x73, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x61, 0x6c,
|
||||||
0x69, 0x73, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
0x6c, 0x65, 0x6c, 0x69, 0x73, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74,
|
||||||
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69,
|
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72,
|
||||||
0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28,
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x05,
|
||||||
0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x2a, 0x21, 0x0a, 0x05, 0x43, 0x75, 0x72, 0x76, 0x65,
|
0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x2a, 0x21, 0x0a, 0x05, 0x43, 0x75,
|
||||||
0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x55, 0x52, 0x56, 0x45, 0x32, 0x35, 0x35, 0x31, 0x39, 0x10, 0x00,
|
0x72, 0x76, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x55, 0x52, 0x56, 0x45, 0x32, 0x35, 0x35, 0x31,
|
||||||
0x12, 0x08, 0x0a, 0x04, 0x50, 0x32, 0x35, 0x36, 0x10, 0x01, 0x42, 0x20, 0x5a, 0x1e, 0x67, 0x69,
|
0x39, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x32, 0x35, 0x36, 0x10, 0x01, 0x42, 0x20, 0x5a,
|
||||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6c, 0x61, 0x63, 0x6b, 0x68, 0x71,
|
0x1e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6c, 0x61, 0x63,
|
||||||
0x2f, 0x6e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x62, 0x06, 0x70, 0x72,
|
0x6b, 0x68, 0x71, 0x2f, 0x6e, 0x65, 0x62, 0x75, 0x6c, 0x61, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x62,
|
||||||
0x6f, 0x74, 0x6f, 0x33,
|
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_cert_proto_rawDescOnce sync.Once
|
file_cert_v1_proto_rawDescOnce sync.Once
|
||||||
file_cert_proto_rawDescData = file_cert_proto_rawDesc
|
file_cert_v1_proto_rawDescData = file_cert_v1_proto_rawDesc
|
||||||
)
|
)
|
||||||
|
|
||||||
func file_cert_proto_rawDescGZIP() []byte {
|
func file_cert_v1_proto_rawDescGZIP() []byte {
|
||||||
file_cert_proto_rawDescOnce.Do(func() {
|
file_cert_v1_proto_rawDescOnce.Do(func() {
|
||||||
file_cert_proto_rawDescData = protoimpl.X.CompressGZIP(file_cert_proto_rawDescData)
|
file_cert_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_cert_v1_proto_rawDescData)
|
||||||
})
|
})
|
||||||
return file_cert_proto_rawDescData
|
return file_cert_v1_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_cert_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
var file_cert_v1_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||||
var file_cert_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
var file_cert_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||||
var file_cert_proto_goTypes = []interface{}{
|
var file_cert_v1_proto_goTypes = []any{
|
||||||
(Curve)(0), // 0: cert.Curve
|
(Curve)(0), // 0: cert.Curve
|
||||||
(*RawNebulaCertificate)(nil), // 1: cert.RawNebulaCertificate
|
(*RawNebulaCertificate)(nil), // 1: cert.RawNebulaCertificate
|
||||||
(*RawNebulaCertificateDetails)(nil), // 2: cert.RawNebulaCertificateDetails
|
(*RawNebulaCertificateDetails)(nil), // 2: cert.RawNebulaCertificateDetails
|
||||||
@@ -519,7 +519,7 @@ var file_cert_proto_goTypes = []interface{}{
|
|||||||
(*RawNebulaEncryptionMetadata)(nil), // 4: cert.RawNebulaEncryptionMetadata
|
(*RawNebulaEncryptionMetadata)(nil), // 4: cert.RawNebulaEncryptionMetadata
|
||||||
(*RawNebulaArgon2Parameters)(nil), // 5: cert.RawNebulaArgon2Parameters
|
(*RawNebulaArgon2Parameters)(nil), // 5: cert.RawNebulaArgon2Parameters
|
||||||
}
|
}
|
||||||
var file_cert_proto_depIdxs = []int32{
|
var file_cert_v1_proto_depIdxs = []int32{
|
||||||
2, // 0: cert.RawNebulaCertificate.Details:type_name -> cert.RawNebulaCertificateDetails
|
2, // 0: cert.RawNebulaCertificate.Details:type_name -> cert.RawNebulaCertificateDetails
|
||||||
0, // 1: cert.RawNebulaCertificateDetails.curve:type_name -> cert.Curve
|
0, // 1: cert.RawNebulaCertificateDetails.curve:type_name -> cert.Curve
|
||||||
4, // 2: cert.RawNebulaEncryptedData.EncryptionMetadata:type_name -> cert.RawNebulaEncryptionMetadata
|
4, // 2: cert.RawNebulaEncryptedData.EncryptionMetadata:type_name -> cert.RawNebulaEncryptionMetadata
|
||||||
@@ -531,13 +531,13 @@ var file_cert_proto_depIdxs = []int32{
|
|||||||
0, // [0:4] is the sub-list for field type_name
|
0, // [0:4] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_cert_proto_init() }
|
func init() { file_cert_v1_proto_init() }
|
||||||
func file_cert_proto_init() {
|
func file_cert_v1_proto_init() {
|
||||||
if File_cert_proto != nil {
|
if File_cert_v1_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !protoimpl.UnsafeEnabled {
|
if !protoimpl.UnsafeEnabled {
|
||||||
file_cert_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
file_cert_v1_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RawNebulaCertificate); i {
|
switch v := v.(*RawNebulaCertificate); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -549,7 +549,7 @@ func file_cert_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_cert_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
file_cert_v1_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RawNebulaCertificateDetails); i {
|
switch v := v.(*RawNebulaCertificateDetails); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -561,7 +561,7 @@ func file_cert_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_cert_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
file_cert_v1_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RawNebulaEncryptedData); i {
|
switch v := v.(*RawNebulaEncryptedData); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -573,7 +573,7 @@ func file_cert_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_cert_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
file_cert_v1_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RawNebulaEncryptionMetadata); i {
|
switch v := v.(*RawNebulaEncryptionMetadata); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -585,7 +585,7 @@ func file_cert_proto_init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file_cert_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
file_cert_v1_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
switch v := v.(*RawNebulaArgon2Parameters); i {
|
switch v := v.(*RawNebulaArgon2Parameters); i {
|
||||||
case 0:
|
case 0:
|
||||||
return &v.state
|
return &v.state
|
||||||
@@ -602,19 +602,19 @@ func file_cert_proto_init() {
|
|||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_cert_proto_rawDesc,
|
RawDescriptor: file_cert_v1_proto_rawDesc,
|
||||||
NumEnums: 1,
|
NumEnums: 1,
|
||||||
NumMessages: 5,
|
NumMessages: 5,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
GoTypes: file_cert_proto_goTypes,
|
GoTypes: file_cert_v1_proto_goTypes,
|
||||||
DependencyIndexes: file_cert_proto_depIdxs,
|
DependencyIndexes: file_cert_v1_proto_depIdxs,
|
||||||
EnumInfos: file_cert_proto_enumTypes,
|
EnumInfos: file_cert_v1_proto_enumTypes,
|
||||||
MessageInfos: file_cert_proto_msgTypes,
|
MessageInfos: file_cert_v1_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_cert_proto = out.File
|
File_cert_v1_proto = out.File
|
||||||
file_cert_proto_rawDesc = nil
|
file_cert_v1_proto_rawDesc = nil
|
||||||
file_cert_proto_goTypes = nil
|
file_cert_v1_proto_goTypes = nil
|
||||||
file_cert_proto_depIdxs = nil
|
file_cert_v1_proto_depIdxs = nil
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCertificateV1_Marshal(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 := 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"),
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := nc.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
//t.Log("Cert size:", len(b))
|
||||||
|
|
||||||
|
nc2, err := unmarshalCertificateV1(b, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, Version1, nc.Version())
|
||||||
|
assert.Equal(t, Curve_CURVE25519, nc.Curve())
|
||||||
|
assert.Equal(t, nc.Signature(), nc2.Signature())
|
||||||
|
assert.Equal(t, nc.Name(), nc2.Name())
|
||||||
|
assert.Equal(t, nc.NotBefore(), nc2.NotBefore())
|
||||||
|
assert.Equal(t, nc.NotAfter(), nc2.NotAfter())
|
||||||
|
assert.Equal(t, nc.PublicKey(), nc2.PublicKey())
|
||||||
|
assert.Equal(t, nc.IsCA(), nc2.IsCA())
|
||||||
|
|
||||||
|
assert.Equal(t, nc.Networks(), nc2.Networks())
|
||||||
|
assert.Equal(t, nc.UnsafeNetworks(), nc2.UnsafeNetworks())
|
||||||
|
|
||||||
|
assert.Equal(t, nc.Groups(), nc2.Groups())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_Unmarshal(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
|
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
|
pubKey := []byte("1234567890abcedfghij1234567890ab")
|
||||||
|
invalidPubkey := []byte("00000000000000000000000000000000")
|
||||||
|
|
||||||
|
nc := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: "testing",
|
||||||
|
networks: []netip.Prefix{
|
||||||
|
mustParsePrefixUnmapped("10.1.1.1/24"),
|
||||||
|
mustParsePrefixUnmapped("10.1.1.2/16"),
|
||||||
|
},
|
||||||
|
unsafeNetworks: []netip.Prefix{
|
||||||
|
mustParsePrefixUnmapped("9.1.1.2/24"),
|
||||||
|
mustParsePrefixUnmapped("9.1.1.3/16"),
|
||||||
|
},
|
||||||
|
groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||||
|
notBefore: before,
|
||||||
|
notAfter: after,
|
||||||
|
publicKey: pubKey,
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcedfghij1234567890ab",
|
||||||
|
},
|
||||||
|
signature: []byte("1234567890abcedfghij1234567890ab"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// This certificate has a pubkey included
|
||||||
|
certWithPubkey, err := nc.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// This certificate is missing the pubkey section
|
||||||
|
certWithoutPubkey, err := nc.MarshalForHandshakes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Cert has no pubkey and no pubkey passed in must fail to validate
|
||||||
|
isNil, err := unmarshalCertificateV1(certWithoutPubkey, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Cert has different pubkey than one passed in must fail
|
||||||
|
isNil, err = unmarshalCertificateV1(certWithPubkey, invalidPubkey)
|
||||||
|
require.Nil(t, isNil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Cert has pubkey and no pubkey argument works ok
|
||||||
|
_, err = unmarshalCertificateV1(certWithPubkey, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Cert has no pubkey and valid, correctly signed pubkey passed in
|
||||||
|
nc2, err := unmarshalCertificateV1(certWithoutPubkey, pubKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, pubKey, nc2.PublicKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_PublicKeyPem(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
|
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
|
pubKey := ed25519.PublicKey("1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
nc := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
name: "testing",
|
||||||
|
networks: []netip.Prefix{},
|
||||||
|
unsafeNetworks: []netip.Prefix{},
|
||||||
|
groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||||
|
notBefore: before,
|
||||||
|
notAfter: after,
|
||||||
|
publicKey: pubKey,
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcedfghij1234567890ab",
|
||||||
|
},
|
||||||
|
signature: []byte("1234567890abcedfghij1234567890ab"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, Version1, nc.Version())
|
||||||
|
assert.Equal(t, Curve_CURVE25519, nc.Curve())
|
||||||
|
pubPem := "-----BEGIN NEBULA X25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA X25519 PUBLIC KEY-----\n"
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
|
||||||
|
assert.False(t, nc.IsCA())
|
||||||
|
|
||||||
|
nc.details.isCA = true
|
||||||
|
assert.Equal(t, Curve_CURVE25519, nc.Curve())
|
||||||
|
pubPem = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA ED25519 PUBLIC KEY-----\n"
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
|
||||||
|
assert.True(t, nc.IsCA())
|
||||||
|
|
||||||
|
pubP256KeyPem := []byte(`-----BEGIN NEBULA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
|
||||||
|
pubP256KeyPemCA := []byte(`-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
nc.details.curve = Curve_P256
|
||||||
|
nc.details.publicKey = pubP256Key
|
||||||
|
assert.Equal(t, Curve_P256, nc.Curve())
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPemCA))
|
||||||
|
assert.True(t, nc.IsCA())
|
||||||
|
|
||||||
|
nc.details.isCA = false
|
||||||
|
assert.Equal(t, Curve_P256, nc.Curve())
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
|
||||||
|
assert.False(t, nc.IsCA())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_Expired(t *testing.T) {
|
||||||
|
nc := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
notBefore: time.Now().Add(time.Second * -60).Round(time.Second),
|
||||||
|
notAfter: time.Now().Add(time.Second * 60).Round(time.Second),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, nc.Expired(time.Now().Add(time.Hour)))
|
||||||
|
assert.True(t, nc.Expired(time.Now().Add(-time.Hour)))
|
||||||
|
assert.False(t, nc.Expired(time.Now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_MarshalJSON(t *testing.T) {
|
||||||
|
time.Local = time.UTC
|
||||||
|
pubKey := []byte("1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
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: time.Date(1, 0, 0, 1, 0, 0, 0, time.UTC),
|
||||||
|
notAfter: time.Date(1, 0, 0, 2, 0, 0, 0, time.UTC),
|
||||||
|
publicKey: pubKey,
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcedfghij1234567890ab",
|
||||||
|
},
|
||||||
|
signature: []byte("1234567890abcedfghij1234567890ab"),
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := nc.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.JSONEq(
|
||||||
|
t,
|
||||||
|
"{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"test-group1\",\"test-group2\",\"test-group3\"],\"isCa\":false,\"issuer\":\"1234567890abcedfghij1234567890ab\",\"name\":\"testing\",\"networks\":[\"10.1.1.1/24\",\"10.1.1.2/16\"],\"notAfter\":\"0000-11-30T02:00:00Z\",\"notBefore\":\"0000-11-30T01:00:00Z\",\"publicKey\":\"313233343536373839306162636564666768696a313233343536373839306162\",\"unsafeNetworks\":[\"9.1.1.2/24\",\"9.1.1.3/16\"]},\"fingerprint\":\"3944c53d4267a229295b56cb2d27d459164c010ac97d655063ba421e0670f4ba\",\"signature\":\"313233343536373839306162636564666768696a313233343536373839306162\",\"version\":1}",
|
||||||
|
string(b),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_VerifyPrivateKey(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
err := ca.VerifyPrivateKey(Curve_CURVE25519, caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, caKey2, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ca.VerifyPrivateKey(Curve_CURVE25519, caKey2)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
c, _, priv, _ := NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
rawPriv, b, curve, err := UnmarshalPrivateKeyFromPEM(priv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
err = c.VerifyPrivateKey(Curve_CURVE25519, rawPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, priv2 := X25519Keypair()
|
||||||
|
err = c.VerifyPrivateKey(Curve_CURVE25519, priv2)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_VerifyPrivateKeyP256(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_P256, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
err := ca.VerifyPrivateKey(Curve_P256, caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, caKey2, _ := NewTestCaCert(Version1, Curve_P256, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ca.VerifyPrivateKey(Curve_P256, caKey2)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
c, _, priv, _ := NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
rawPriv, b, curve, err := UnmarshalPrivateKeyFromPEM(priv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, rawPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, priv2 := P256Keypair()
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, priv2)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that upgrading the protobuf library does not change how certificates
|
||||||
|
// are marshalled, since this would break signature verification
|
||||||
|
func TestMarshalingCertificateV1Consistency(t *testing.T) {
|
||||||
|
before := time.Date(1970, time.January, 1, 1, 1, 1, 1, time.UTC)
|
||||||
|
after := time.Date(9999, time.January, 1, 1, 1, 1, 1, time.UTC)
|
||||||
|
pubKey := []byte("1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
nc := certificateV1{
|
||||||
|
details: detailsV1{
|
||||||
|
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,
|
||||||
|
publicKey: pubKey,
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcedfghij1234567890ab",
|
||||||
|
},
|
||||||
|
signature: []byte("1234567890abcedfghij1234567890ab"),
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := nc.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "0a8e010a0774657374696e671212828284508080fcff0f8182845080feffff0f1a12838284488080fcff0f8282844880feffff0f220b746573742d67726f757031220b746573742d67726f757032220b746573742d67726f75703328cd1c30cdb8ccf0af073a20313233343536373839306162636564666768696a3132333435363738393061624a081234567890abcedf1220313233343536373839306162636564666768696a313233343536373839306162", fmt.Sprintf("%x", b))
|
||||||
|
|
||||||
|
b, err = proto.Marshal(nc.getRawDetails())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "0a0774657374696e671212828284508080fcff0f8182845080feffff0f1a12838284488080fcff0f8282844880feffff0f220b746573742d67726f757031220b746573742d67726f757032220b746573742d67726f75703328cd1c30cdb8ccf0af073a20313233343536373839306162636564666768696a3132333435363738393061624a081234567890abcedf", fmt.Sprintf("%x", b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_Copy(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version1, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||||
|
c, _, _, _ := NewTestCert(Version1, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||||
|
cc := c.Copy()
|
||||||
|
test.AssertDeepCopyEqual(t, c, cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalCertificateV1(t *testing.T) {
|
||||||
|
// Test that we don't panic with an invalid certificate (#332)
|
||||||
|
data := []byte("\x98\x00\x00")
|
||||||
|
_, err := unmarshalCertificateV1(data, nil)
|
||||||
|
require.EqualError(t, err, "encoded Details was nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendByteSlices(b ...[]byte) []byte {
|
||||||
|
retSlice := []byte{}
|
||||||
|
for _, v := range b {
|
||||||
|
retSlice = append(retSlice, v...)
|
||||||
|
}
|
||||||
|
return retSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParsePrefixUnmapped(s string) netip.Prefix {
|
||||||
|
prefix := netip.MustParsePrefix(s)
|
||||||
|
return netip.PrefixFrom(prefix.Addr().Unmap(), prefix.Bits())
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
Nebula DEFINITIONS AUTOMATIC TAGS ::= BEGIN
|
||||||
|
|
||||||
|
Name ::= UTF8String (SIZE (1..253))
|
||||||
|
Time ::= INTEGER (0..18446744073709551615) -- Seconds since unix epoch, uint64 maximum
|
||||||
|
Network ::= OCTET STRING (SIZE (5,17)) -- IP addresses are 4 or 16 bytes + 1 byte for the prefix length
|
||||||
|
Curve ::= ENUMERATED {
|
||||||
|
curve25519 (0),
|
||||||
|
p256 (1)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- The maximum size of a certificate must not exceed 65536 bytes
|
||||||
|
Certificate ::= SEQUENCE {
|
||||||
|
details OCTET STRING,
|
||||||
|
curve Curve DEFAULT curve25519,
|
||||||
|
publicKey OCTET STRING,
|
||||||
|
-- signature(details + curve + publicKey) using the appropriate method for curve
|
||||||
|
signature OCTET STRING
|
||||||
|
}
|
||||||
|
|
||||||
|
Details ::= SEQUENCE {
|
||||||
|
name Name,
|
||||||
|
|
||||||
|
-- At least 1 ipv4 or ipv6 address must be present if isCA is false
|
||||||
|
networks SEQUENCE OF Network OPTIONAL,
|
||||||
|
unsafeNetworks SEQUENCE OF Network OPTIONAL,
|
||||||
|
groups SEQUENCE OF Name OPTIONAL,
|
||||||
|
isCA BOOLEAN DEFAULT false,
|
||||||
|
notBefore Time,
|
||||||
|
notAfter Time,
|
||||||
|
|
||||||
|
-- issuer is only required if isCA is false, if isCA is true then it must not be present
|
||||||
|
issuer OCTET STRING OPTIONAL,
|
||||||
|
...
|
||||||
|
-- New fields can be added below here
|
||||||
|
}
|
||||||
|
|
||||||
|
END
|
||||||
+745
@@ -0,0 +1,745 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
"golang.org/x/crypto/cryptobyte/asn1"
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
classConstructed = 0x20
|
||||||
|
classContextSpecific = 0x80
|
||||||
|
|
||||||
|
TagCertDetails = 0 | classConstructed | classContextSpecific
|
||||||
|
TagCertCurve = 1 | classContextSpecific
|
||||||
|
TagCertPublicKey = 2 | classContextSpecific
|
||||||
|
TagCertSignature = 3 | classContextSpecific
|
||||||
|
|
||||||
|
TagDetailsName = 0 | classContextSpecific
|
||||||
|
TagDetailsNetworks = 1 | classConstructed | classContextSpecific
|
||||||
|
TagDetailsUnsafeNetworks = 2 | classConstructed | classContextSpecific
|
||||||
|
TagDetailsGroups = 3 | classConstructed | classContextSpecific
|
||||||
|
TagDetailsIsCA = 4 | classContextSpecific
|
||||||
|
TagDetailsNotBefore = 5 | classContextSpecific
|
||||||
|
TagDetailsNotAfter = 6 | classContextSpecific
|
||||||
|
TagDetailsIssuer = 7 | classContextSpecific
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MaxCertificateSize is the maximum length a valid certificate can be
|
||||||
|
MaxCertificateSize = 65536
|
||||||
|
|
||||||
|
// MaxNameLength is limited to a maximum realistic DNS domain name to help facilitate DNS systems
|
||||||
|
MaxNameLength = 253
|
||||||
|
|
||||||
|
// MaxNetworkLength is the maximum length a network value can be.
|
||||||
|
// 16 bytes for an ipv6 address + 1 byte for the prefix length
|
||||||
|
MaxNetworkLength = 17
|
||||||
|
)
|
||||||
|
|
||||||
|
type certificateV2 struct {
|
||||||
|
details detailsV2
|
||||||
|
|
||||||
|
// RawDetails contains the entire asn.1 DER encoded Details struct
|
||||||
|
// This is to benefit forwards compatibility in signature checking.
|
||||||
|
// signature(RawDetails + Curve + PublicKey) == Signature
|
||||||
|
rawDetails []byte
|
||||||
|
curve Curve
|
||||||
|
publicKey []byte
|
||||||
|
signature []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type detailsV2 struct {
|
||||||
|
name string
|
||||||
|
networks []netip.Prefix // MUST BE SORTED
|
||||||
|
unsafeNetworks []netip.Prefix // MUST BE SORTED
|
||||||
|
groups []string
|
||||||
|
isCA bool
|
||||||
|
notBefore time.Time
|
||||||
|
notAfter time.Time
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Version() Version {
|
||||||
|
return Version2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Curve() Curve {
|
||||||
|
return c.curve
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Groups() []string {
|
||||||
|
return c.details.groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) IsCA() bool {
|
||||||
|
return c.details.isCA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Issuer() string {
|
||||||
|
return c.details.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Name() string {
|
||||||
|
return c.details.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Networks() []netip.Prefix {
|
||||||
|
return c.details.networks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) NotAfter() time.Time {
|
||||||
|
return c.details.notAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) NotBefore() time.Time {
|
||||||
|
return c.details.notBefore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) PublicKey() []byte {
|
||||||
|
return c.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) MarshalPublicKeyPEM() []byte {
|
||||||
|
return marshalCertPublicKeyToPEM(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Signature() []byte {
|
||||||
|
return c.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) UnsafeNetworks() []netip.Prefix {
|
||||||
|
return c.details.unsafeNetworks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Fingerprint() (string, error) {
|
||||||
|
if len(c.rawDetails) == 0 {
|
||||||
|
return "", ErrMissingDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, len(c.rawDetails)+1+len(c.publicKey)+len(c.signature))
|
||||||
|
copy(b, c.rawDetails)
|
||||||
|
b[len(c.rawDetails)] = byte(c.curve)
|
||||||
|
copy(b[len(c.rawDetails)+1:], c.publicKey)
|
||||||
|
copy(b[len(c.rawDetails)+1+len(c.publicKey):], c.signature)
|
||||||
|
sum := sha256.Sum256(b)
|
||||||
|
return hex.EncodeToString(sum[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) CheckSignature(key []byte) bool {
|
||||||
|
if len(c.rawDetails) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b := make([]byte, len(c.rawDetails)+1+len(c.publicKey))
|
||||||
|
copy(b, c.rawDetails)
|
||||||
|
b[len(c.rawDetails)] = byte(c.curve)
|
||||||
|
copy(b[len(c.rawDetails)+1:], c.publicKey)
|
||||||
|
|
||||||
|
switch c.curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
if len(key) != ed25519.PublicKeySize {
|
||||||
|
return false //avoids a panic internal to ed25519
|
||||||
|
}
|
||||||
|
return ed25519.Verify(key, b, c.signature)
|
||||||
|
case Curve_P256:
|
||||||
|
pubKey, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), key)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hashed := sha256.Sum256(b)
|
||||||
|
return ecdsa.VerifyASN1(pubKey, hashed[:], c.signature)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Expired(t time.Time) bool {
|
||||||
|
return c.details.notBefore.After(t) || c.details.notAfter.Before(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) VerifyPrivateKey(curve Curve, key []byte) error {
|
||||||
|
if curve != c.curve {
|
||||||
|
return ErrPublicPrivateCurveMismatch
|
||||||
|
}
|
||||||
|
if c.details.isCA {
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
// the call to PublicKey below will panic slice bounds out of range otherwise
|
||||||
|
if len(key) != ed25519.PrivateKeySize {
|
||||||
|
return ErrInvalidPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ed25519.PublicKey(c.publicKey).Equal(ed25519.PrivateKey(key).Public()) {
|
||||||
|
return ErrPublicPrivateKeyMismatch
|
||||||
|
}
|
||||||
|
case Curve_P256:
|
||||||
|
privkey, err := ecdh.P256().NewPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidPrivateKey
|
||||||
|
}
|
||||||
|
pub := privkey.PublicKey().Bytes()
|
||||||
|
if !bytes.Equal(pub, c.publicKey) {
|
||||||
|
return ErrPublicPrivateKeyMismatch
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid curve: %s", curve)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub []byte
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
var err error
|
||||||
|
pub, err = curve25519.X25519(key, curve25519.Basepoint)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidPrivateKey
|
||||||
|
}
|
||||||
|
case Curve_P256:
|
||||||
|
privkey, err := ecdh.P256().NewPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return ErrInvalidPrivateKey
|
||||||
|
}
|
||||||
|
pub = privkey.PublicKey().Bytes()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid curve: %s", curve)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(pub, c.publicKey) {
|
||||||
|
return ErrPublicPrivateKeyMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) String() string {
|
||||||
|
mb, err := c.marshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<error marshalling certificate: %v>", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.MarshalIndent(mb, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<error marshalling certificate: %v>", err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) MarshalForHandshakes() ([]byte, error) {
|
||||||
|
if c.rawDetails == nil {
|
||||||
|
return nil, ErrEmptyRawDetails
|
||||||
|
}
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
// Outermost certificate
|
||||||
|
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
|
||||||
|
|
||||||
|
// Add the cert details which is already marshalled
|
||||||
|
b.AddBytes(c.rawDetails)
|
||||||
|
|
||||||
|
// Skipping the curve and public key since those come across in a different part of the handshake
|
||||||
|
|
||||||
|
// Add the signature
|
||||||
|
b.AddASN1(TagCertSignature, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(c.signature)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Marshal() ([]byte, error) {
|
||||||
|
if c.rawDetails == nil {
|
||||||
|
return nil, ErrEmptyRawDetails
|
||||||
|
}
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
// Outermost certificate
|
||||||
|
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
|
||||||
|
|
||||||
|
// Add the cert details which is already marshalled
|
||||||
|
b.AddBytes(c.rawDetails)
|
||||||
|
|
||||||
|
// Add the curve only if its not the default value
|
||||||
|
if c.curve != Curve_CURVE25519 {
|
||||||
|
b.AddASN1(TagCertCurve, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes([]byte{byte(c.curve)})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the public key if it is not empty
|
||||||
|
if c.publicKey != nil {
|
||||||
|
b.AddASN1(TagCertPublicKey, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(c.publicKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the signature
|
||||||
|
b.AddASN1(TagCertSignature, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(c.signature)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) MarshalPEM() ([]byte, error) {
|
||||||
|
b, err := c.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: CertificateV2Banner, Bytes: b}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) MarshalJSON() ([]byte, error) {
|
||||||
|
b, err := c.marshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.Marshal(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) marshalJSON() (m, error) {
|
||||||
|
fp, err := c.Fingerprint()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m{
|
||||||
|
"details": m{
|
||||||
|
"name": c.details.name,
|
||||||
|
"networks": c.details.networks,
|
||||||
|
"unsafeNetworks": c.details.unsafeNetworks,
|
||||||
|
"groups": c.details.groups,
|
||||||
|
"notBefore": c.details.notBefore,
|
||||||
|
"notAfter": c.details.notAfter,
|
||||||
|
"isCa": c.details.isCA,
|
||||||
|
"issuer": c.details.issuer,
|
||||||
|
},
|
||||||
|
"version": Version2,
|
||||||
|
"publicKey": fmt.Sprintf("%x", c.publicKey),
|
||||||
|
"curve": c.curve.String(),
|
||||||
|
"fingerprint": fp,
|
||||||
|
"signature": fmt.Sprintf("%x", c.Signature()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) Copy() Certificate {
|
||||||
|
nc := &certificateV2{
|
||||||
|
details: detailsV2{
|
||||||
|
name: c.details.name,
|
||||||
|
notBefore: c.details.notBefore,
|
||||||
|
notAfter: c.details.notAfter,
|
||||||
|
isCA: c.details.isCA,
|
||||||
|
issuer: c.details.issuer,
|
||||||
|
},
|
||||||
|
curve: c.curve,
|
||||||
|
publicKey: make([]byte, len(c.publicKey)),
|
||||||
|
signature: make([]byte, len(c.signature)),
|
||||||
|
rawDetails: make([]byte, len(c.rawDetails)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.details.groups != nil {
|
||||||
|
nc.details.groups = make([]string, len(c.details.groups))
|
||||||
|
copy(nc.details.groups, c.details.groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.details.networks != nil {
|
||||||
|
nc.details.networks = make([]netip.Prefix, len(c.details.networks))
|
||||||
|
copy(nc.details.networks, c.details.networks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.details.unsafeNetworks != nil {
|
||||||
|
nc.details.unsafeNetworks = make([]netip.Prefix, len(c.details.unsafeNetworks))
|
||||||
|
copy(nc.details.unsafeNetworks, c.details.unsafeNetworks)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(nc.rawDetails, c.rawDetails)
|
||||||
|
copy(nc.signature, c.signature)
|
||||||
|
copy(nc.publicKey, c.publicKey)
|
||||||
|
|
||||||
|
return nc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) fromTBSCertificate(t *TBSCertificate) error {
|
||||||
|
c.details = detailsV2{
|
||||||
|
name: t.Name,
|
||||||
|
networks: t.Networks,
|
||||||
|
unsafeNetworks: t.UnsafeNetworks,
|
||||||
|
groups: t.Groups,
|
||||||
|
isCA: t.IsCA,
|
||||||
|
notBefore: t.NotBefore,
|
||||||
|
notAfter: t.NotAfter,
|
||||||
|
issuer: t.issuer,
|
||||||
|
}
|
||||||
|
c.curve = t.Curve
|
||||||
|
c.publicKey = t.PublicKey
|
||||||
|
return c.validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) validate() error {
|
||||||
|
// Empty names are allowed
|
||||||
|
|
||||||
|
if len(c.publicKey) == 0 {
|
||||||
|
return ErrInvalidPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.details.isCA && len(c.details.networks) == 0 {
|
||||||
|
return NewErrInvalidCertificateProperties("non-CA certificate must contain at least 1 network")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasV4Networks := false
|
||||||
|
hasV6Networks := false
|
||||||
|
for _, network := range c.details.networks {
|
||||||
|
if !network.IsValid() || !network.Addr().IsValid() {
|
||||||
|
return NewErrInvalidCertificateProperties("invalid network: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().IsUnspecified() {
|
||||||
|
return NewErrInvalidCertificateProperties("non-CA certificates must not use the zero address as a network: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Zone() != "" {
|
||||||
|
return NewErrInvalidCertificateProperties("networks may not contain zones: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Is4In6() {
|
||||||
|
return NewErrInvalidCertificateProperties("4in6 networks are not allowed: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasV4Networks = hasV4Networks || network.Addr().Is4()
|
||||||
|
hasV6Networks = hasV6Networks || network.Addr().Is6()
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(c.details.networks, comparePrefix)
|
||||||
|
err := findDuplicatePrefix(c.details.networks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range c.details.unsafeNetworks {
|
||||||
|
if !network.IsValid() || !network.Addr().IsValid() {
|
||||||
|
return NewErrInvalidCertificateProperties("invalid unsafe network: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.Addr().Zone() != "" {
|
||||||
|
return NewErrInvalidCertificateProperties("unsafe networks may not contain zones: %s", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.details.isCA {
|
||||||
|
if network.Addr().Is6() {
|
||||||
|
if !hasV6Networks {
|
||||||
|
return NewErrInvalidCertificateProperties("IPv6 unsafe networks require an IPv6 address assignment: %s", network)
|
||||||
|
}
|
||||||
|
} else if network.Addr().Is4() {
|
||||||
|
if !hasV4Networks {
|
||||||
|
return NewErrInvalidCertificateProperties("IPv4 unsafe networks require an IPv4 address assignment: %s", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(c.details.unsafeNetworks, comparePrefix)
|
||||||
|
err = findDuplicatePrefix(c.details.unsafeNetworks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) marshalForSigning() ([]byte, error) {
|
||||||
|
d, err := c.details.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshalling certificate details failed: %w", err)
|
||||||
|
}
|
||||||
|
c.rawDetails = d
|
||||||
|
|
||||||
|
b := make([]byte, len(c.rawDetails)+1+len(c.publicKey))
|
||||||
|
copy(b, c.rawDetails)
|
||||||
|
b[len(c.rawDetails)] = byte(c.curve)
|
||||||
|
copy(b[len(c.rawDetails)+1:], c.publicKey)
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *certificateV2) setSignature(b []byte) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return ErrEmptySignature
|
||||||
|
}
|
||||||
|
c.signature = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *detailsV2) Marshal() ([]byte, error) {
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Details are a structure
|
||||||
|
b.AddASN1(TagCertDetails, func(b *cryptobyte.Builder) {
|
||||||
|
|
||||||
|
// Add the name
|
||||||
|
b.AddASN1(TagDetailsName, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes([]byte(d.name))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add the networks if any exist
|
||||||
|
if len(d.networks) > 0 {
|
||||||
|
b.AddASN1(TagDetailsNetworks, func(b *cryptobyte.Builder) {
|
||||||
|
for _, n := range d.networks {
|
||||||
|
sb, innerErr := n.MarshalBinary()
|
||||||
|
if innerErr != nil {
|
||||||
|
// MarshalBinary never returns an error
|
||||||
|
err = fmt.Errorf("unable to marshal network: %w", innerErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.AddASN1OctetString(sb)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the unsafe networks if any exist
|
||||||
|
if len(d.unsafeNetworks) > 0 {
|
||||||
|
b.AddASN1(TagDetailsUnsafeNetworks, func(b *cryptobyte.Builder) {
|
||||||
|
for _, n := range d.unsafeNetworks {
|
||||||
|
sb, innerErr := n.MarshalBinary()
|
||||||
|
if innerErr != nil {
|
||||||
|
// MarshalBinary never returns an error
|
||||||
|
err = fmt.Errorf("unable to marshal unsafe network: %w", innerErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.AddASN1OctetString(sb)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add groups if any exist
|
||||||
|
if len(d.groups) > 0 {
|
||||||
|
b.AddASN1(TagDetailsGroups, func(b *cryptobyte.Builder) {
|
||||||
|
for _, group := range d.groups {
|
||||||
|
b.AddASN1(asn1.UTF8String, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes([]byte(group))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IsCA only if true
|
||||||
|
if d.isCA {
|
||||||
|
b.AddASN1(TagDetailsIsCA, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddUint8(0xff)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add not before
|
||||||
|
b.AddASN1Int64WithTag(d.notBefore.Unix(), TagDetailsNotBefore)
|
||||||
|
|
||||||
|
// Add not after
|
||||||
|
b.AddASN1Int64WithTag(d.notAfter.Unix(), TagDetailsNotAfter)
|
||||||
|
|
||||||
|
// Add the issuer if present
|
||||||
|
if d.issuer != "" {
|
||||||
|
issuerBytes, innerErr := hex.DecodeString(d.issuer)
|
||||||
|
if innerErr != nil {
|
||||||
|
err = fmt.Errorf("failed to decode issuer: %w", innerErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.AddASN1(TagDetailsIssuer, func(b *cryptobyte.Builder) {
|
||||||
|
b.AddBytes(issuerBytes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalCertificateV2(b []byte, publicKey []byte, curve Curve) (*certificateV2, error) {
|
||||||
|
l := len(b)
|
||||||
|
if l == 0 || l > MaxCertificateSize {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
input := cryptobyte.String(b)
|
||||||
|
// Open the envelope
|
||||||
|
if !input.ReadASN1(&input, asn1.SEQUENCE) || input.Empty() {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the cert details, we need to preserve the tag and length
|
||||||
|
var rawDetails cryptobyte.String
|
||||||
|
if !input.ReadASN1Element(&rawDetails, TagCertDetails) || rawDetails.Empty() {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
//Maybe grab the curve
|
||||||
|
var rawCurve byte
|
||||||
|
if !readOptionalASN1Byte(&input, &rawCurve, TagCertCurve, byte(curve)) {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
curve = Curve(rawCurve)
|
||||||
|
|
||||||
|
// Maybe grab the public key
|
||||||
|
var rawPublicKey cryptobyte.String
|
||||||
|
if len(publicKey) > 0 {
|
||||||
|
// If a public key is passed in, then the handshake certificate must
|
||||||
|
// not have a public key present
|
||||||
|
if input.PeekASN1Tag(TagCertPublicKey) {
|
||||||
|
return nil, ErrCertPubkeyPresent
|
||||||
|
}
|
||||||
|
rawPublicKey = make(cryptobyte.String, len(publicKey))
|
||||||
|
copy(rawPublicKey, publicKey)
|
||||||
|
} else if !input.ReadOptionalASN1(&rawPublicKey, nil, TagCertPublicKey) {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rawPublicKey) == 0 {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the signature
|
||||||
|
var rawSignature cryptobyte.String
|
||||||
|
if !input.ReadASN1(&rawSignature, TagCertSignature) || rawSignature.Empty() {
|
||||||
|
return nil, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally unmarshal the details
|
||||||
|
details, err := unmarshalDetails(rawDetails)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &certificateV2{
|
||||||
|
details: details,
|
||||||
|
rawDetails: rawDetails,
|
||||||
|
curve: curve,
|
||||||
|
publicKey: rawPublicKey,
|
||||||
|
signature: rawSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalDetails(b cryptobyte.String) (detailsV2, error) {
|
||||||
|
// Open the envelope
|
||||||
|
if !b.ReadASN1(&b, TagCertDetails) || b.Empty() {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the name
|
||||||
|
var name cryptobyte.String
|
||||||
|
if !b.ReadASN1(&name, TagDetailsName) || name.Empty() || len(name) > MaxNameLength {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the network addresses
|
||||||
|
var subString cryptobyte.String
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
if !b.ReadOptionalASN1(&subString, &found, TagDetailsNetworks) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var networks []netip.Prefix
|
||||||
|
var val cryptobyte.String
|
||||||
|
if found {
|
||||||
|
for !subString.Empty() {
|
||||||
|
if !subString.ReadASN1(&val, asn1.OCTET_STRING) || val.Empty() || len(val) > MaxNetworkLength {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var n netip.Prefix
|
||||||
|
if err := n.UnmarshalBinary(val); err != nil {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
networks = append(networks, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read out any unsafe networks
|
||||||
|
if !b.ReadOptionalASN1(&subString, &found, TagDetailsUnsafeNetworks) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var unsafeNetworks []netip.Prefix
|
||||||
|
if found {
|
||||||
|
for !subString.Empty() {
|
||||||
|
if !subString.ReadASN1(&val, asn1.OCTET_STRING) || val.Empty() || len(val) > MaxNetworkLength {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var n netip.Prefix
|
||||||
|
if err := n.UnmarshalBinary(val); err != nil {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
unsafeNetworks = append(unsafeNetworks, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read out any groups
|
||||||
|
if !b.ReadOptionalASN1(&subString, &found, TagDetailsGroups) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []string
|
||||||
|
if found {
|
||||||
|
for !subString.Empty() {
|
||||||
|
if !subString.ReadASN1(&val, asn1.UTF8String) || val.Empty() {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
groups = append(groups, string(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read out IsCA
|
||||||
|
var isCa bool
|
||||||
|
if !readOptionalASN1Boolean(&b, &isCa, TagDetailsIsCA, false) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read not before and not after
|
||||||
|
var notBefore int64
|
||||||
|
if !b.ReadASN1Int64WithTag(¬Before, TagDetailsNotBefore) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var notAfter int64
|
||||||
|
if !b.ReadASN1Int64WithTag(¬After, TagDetailsNotAfter) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read issuer
|
||||||
|
var issuer cryptobyte.String
|
||||||
|
if !b.ReadOptionalASN1(&issuer, nil, TagDetailsIssuer) {
|
||||||
|
return detailsV2{}, ErrBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return detailsV2{
|
||||||
|
name: string(name),
|
||||||
|
networks: networks,
|
||||||
|
unsafeNetworks: unsafeNetworks,
|
||||||
|
groups: groups,
|
||||||
|
isCA: isCa,
|
||||||
|
notBefore: time.Unix(notBefore, 0),
|
||||||
|
notAfter: time.Unix(notAfter, 0),
|
||||||
|
issuer: hex.EncodeToString(issuer),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCertificateV2_Marshal(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
|
||||||
|
|
||||||
|
b, err := nc.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
//t.Log("Cert size:", len(b))
|
||||||
|
|
||||||
|
nc2, err := unmarshalCertificateV2(b, nil, Curve_CURVE25519)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, Version2, nc.Version())
|
||||||
|
assert.Equal(t, Curve_CURVE25519, nc.Curve())
|
||||||
|
assert.Equal(t, nc.Signature(), nc2.Signature())
|
||||||
|
assert.Equal(t, nc.Name(), nc2.Name())
|
||||||
|
assert.Equal(t, nc.NotBefore(), nc2.NotBefore())
|
||||||
|
assert.Equal(t, nc.NotAfter(), nc2.NotAfter())
|
||||||
|
assert.Equal(t, nc.PublicKey(), nc2.PublicKey())
|
||||||
|
assert.Equal(t, nc.IsCA(), nc2.IsCA())
|
||||||
|
assert.Equal(t, nc.Issuer(), nc2.Issuer())
|
||||||
|
|
||||||
|
// unmarshalling will sort networks and unsafeNetworks, we need to do the same
|
||||||
|
// but first make sure it fails
|
||||||
|
assert.NotEqual(t, nc.Networks(), nc2.Networks())
|
||||||
|
assert.NotEqual(t, nc.UnsafeNetworks(), nc2.UnsafeNetworks())
|
||||||
|
|
||||||
|
slices.SortFunc(nc.details.networks, comparePrefix)
|
||||||
|
slices.SortFunc(nc.details.unsafeNetworks, comparePrefix)
|
||||||
|
|
||||||
|
assert.Equal(t, nc.Networks(), nc2.Networks())
|
||||||
|
assert.Equal(t, nc.UnsafeNetworks(), nc2.UnsafeNetworks())
|
||||||
|
|
||||||
|
assert.Equal(t, nc.Groups(), nc2.Groups())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_Unmarshal(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
|
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
|
pubKey := []byte("1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
nc := certificateV2{
|
||||||
|
details: detailsV2{
|
||||||
|
name: "testing",
|
||||||
|
networks: []netip.Prefix{
|
||||||
|
mustParsePrefixUnmapped("10.1.1.2/16"),
|
||||||
|
mustParsePrefixUnmapped("10.1.1.1/24"),
|
||||||
|
},
|
||||||
|
unsafeNetworks: []netip.Prefix{
|
||||||
|
mustParsePrefixUnmapped("9.1.1.3/16"),
|
||||||
|
mustParsePrefixUnmapped("9.1.1.2/24"),
|
||||||
|
},
|
||||||
|
groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||||
|
notBefore: before,
|
||||||
|
notAfter: after,
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcdef1234567890abcdef",
|
||||||
|
},
|
||||||
|
signature: []byte("1234567890abcdef1234567890abcdef"),
|
||||||
|
publicKey: pubKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := nc.details.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
nc.rawDetails = db
|
||||||
|
|
||||||
|
certWithPubkey, err := nc.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
//t.Log("Cert size:", len(b))
|
||||||
|
certWithoutPubkey, err := nc.MarshalForHandshakes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Cert must not have a pubkey if one is passed in as an argument
|
||||||
|
_, err = unmarshalCertificateV2(certWithPubkey, pubKey, Curve_CURVE25519)
|
||||||
|
require.ErrorIs(t, err, ErrCertPubkeyPresent)
|
||||||
|
|
||||||
|
// Certs must have pubkeys
|
||||||
|
_, err = unmarshalCertificateV2(certWithoutPubkey, nil, Curve_CURVE25519)
|
||||||
|
require.ErrorIs(t, err, ErrBadFormat)
|
||||||
|
|
||||||
|
// Ensure proper unmarshal if a pubkey is passed in
|
||||||
|
nc2, err := unmarshalCertificateV2(certWithoutPubkey, pubKey, Curve_CURVE25519)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, nc.PublicKey(), nc2.PublicKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_PublicKeyPem(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
|
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
|
pubKey := ed25519.PublicKey("1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
nc := certificateV2{
|
||||||
|
details: detailsV2{
|
||||||
|
name: "testing",
|
||||||
|
networks: []netip.Prefix{},
|
||||||
|
unsafeNetworks: []netip.Prefix{},
|
||||||
|
groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||||
|
notBefore: before,
|
||||||
|
notAfter: after,
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcedfghij1234567890ab",
|
||||||
|
},
|
||||||
|
publicKey: pubKey,
|
||||||
|
signature: []byte("1234567890abcedfghij1234567890ab"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, Version2, nc.Version())
|
||||||
|
assert.Equal(t, Curve_CURVE25519, nc.Curve())
|
||||||
|
pubPem := "-----BEGIN NEBULA X25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA X25519 PUBLIC KEY-----\n"
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
|
||||||
|
assert.False(t, nc.IsCA())
|
||||||
|
|
||||||
|
nc.details.isCA = true
|
||||||
|
assert.Equal(t, Curve_CURVE25519, nc.Curve())
|
||||||
|
pubPem = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nMTIzNDU2Nzg5MGFiY2VkZmdoaWoxMjM0NTY3ODkwYWI=\n-----END NEBULA ED25519 PUBLIC KEY-----\n"
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), pubPem)
|
||||||
|
assert.True(t, nc.IsCA())
|
||||||
|
|
||||||
|
pubP256KeyPem := []byte(`-----BEGIN NEBULA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
|
||||||
|
pubP256KeyPemCA := []byte(`-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
|
||||||
|
pubP256Key, _, _, err := UnmarshalPublicKeyFromPEM(pubP256KeyPem)
|
||||||
|
require.NoError(t, err)
|
||||||
|
nc.curve = Curve_P256
|
||||||
|
nc.publicKey = pubP256Key
|
||||||
|
assert.Equal(t, Curve_P256, nc.Curve())
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPemCA))
|
||||||
|
assert.True(t, nc.IsCA())
|
||||||
|
|
||||||
|
nc.details.isCA = false
|
||||||
|
assert.Equal(t, Curve_P256, nc.Curve())
|
||||||
|
assert.Equal(t, string(nc.MarshalPublicKeyPEM()), string(pubP256KeyPem))
|
||||||
|
assert.False(t, nc.IsCA())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_Expired(t *testing.T) {
|
||||||
|
nc := certificateV2{
|
||||||
|
details: detailsV2{
|
||||||
|
notBefore: time.Now().Add(time.Second * -60).Round(time.Second),
|
||||||
|
notAfter: time.Now().Add(time.Second * 60).Round(time.Second),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, nc.Expired(time.Now().Add(time.Hour)))
|
||||||
|
assert.True(t, nc.Expired(time.Now().Add(-time.Hour)))
|
||||||
|
assert.False(t, nc.Expired(time.Now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_MarshalJSON(t *testing.T) {
|
||||||
|
time.Local = time.UTC
|
||||||
|
pubKey := []byte("1234567890abcedf1234567890abcedf")
|
||||||
|
|
||||||
|
nc := certificateV2{
|
||||||
|
details: detailsV2{
|
||||||
|
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: time.Date(1, 0, 0, 1, 0, 0, 0, time.UTC),
|
||||||
|
notAfter: time.Date(1, 0, 0, 2, 0, 0, 0, time.UTC),
|
||||||
|
isCA: false,
|
||||||
|
issuer: "1234567890abcedf1234567890abcedf",
|
||||||
|
},
|
||||||
|
publicKey: pubKey,
|
||||||
|
signature: []byte("1234567890abcedf1234567890abcedf1234567890abcedf1234567890abcedf"),
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := nc.MarshalJSON()
|
||||||
|
require.ErrorIs(t, err, ErrMissingDetails)
|
||||||
|
|
||||||
|
rd, err := nc.details.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nc.rawDetails = rd
|
||||||
|
b, err = nc.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.JSONEq(
|
||||||
|
t,
|
||||||
|
"{\"curve\":\"CURVE25519\",\"details\":{\"groups\":[\"test-group1\",\"test-group2\",\"test-group3\"],\"isCa\":false,\"issuer\":\"1234567890abcedf1234567890abcedf\",\"name\":\"testing\",\"networks\":[\"10.1.1.1/24\",\"10.1.1.2/16\"],\"notAfter\":\"0000-11-30T02:00:00Z\",\"notBefore\":\"0000-11-30T01:00:00Z\",\"unsafeNetworks\":[\"9.1.1.2/24\",\"9.1.1.3/16\"]},\"fingerprint\":\"152d9a7400c1e001cb76cffd035215ebb351f69eeb797f7f847dd086e15e56dd\",\"publicKey\":\"3132333435363738393061626365646631323334353637383930616263656466\",\"signature\":\"31323334353637383930616263656466313233343536373839306162636564663132333435363738393061626365646631323334353637383930616263656466\",\"version\":2}",
|
||||||
|
string(b),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_VerifyPrivateKey(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
err := ca.VerifyPrivateKey(Curve_CURVE25519, caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ca.VerifyPrivateKey(Curve_CURVE25519, caKey[:16])
|
||||||
|
require.ErrorIs(t, err, ErrInvalidPrivateKey)
|
||||||
|
|
||||||
|
_, caKey2, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ca.VerifyPrivateKey(Curve_CURVE25519, caKey2)
|
||||||
|
require.ErrorIs(t, err, ErrPublicPrivateKeyMismatch)
|
||||||
|
|
||||||
|
c, _, priv, _ := NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
rawPriv, b, curve, err := UnmarshalPrivateKeyFromPEM(priv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
err = c.VerifyPrivateKey(Curve_CURVE25519, rawPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, priv2 := X25519Keypair()
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, priv2)
|
||||||
|
require.ErrorIs(t, err, ErrPublicPrivateCurveMismatch)
|
||||||
|
|
||||||
|
err = c.VerifyPrivateKey(Curve_CURVE25519, priv2)
|
||||||
|
require.ErrorIs(t, err, ErrPublicPrivateKeyMismatch)
|
||||||
|
|
||||||
|
err = c.VerifyPrivateKey(Curve_CURVE25519, priv2[:16])
|
||||||
|
require.ErrorIs(t, err, ErrInvalidPrivateKey)
|
||||||
|
|
||||||
|
ac, ok := c.(*certificateV2)
|
||||||
|
require.True(t, ok)
|
||||||
|
ac.curve = Curve(99)
|
||||||
|
err = c.VerifyPrivateKey(Curve(99), priv2)
|
||||||
|
require.EqualError(t, err, "invalid curve: 99")
|
||||||
|
|
||||||
|
ca2, _, caKey2, _ := NewTestCaCert(Version2, Curve_P256, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
err = ca.VerifyPrivateKey(Curve_CURVE25519, caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ca2.VerifyPrivateKey(Curve_P256, caKey2[:16])
|
||||||
|
require.ErrorIs(t, err, ErrInvalidPrivateKey)
|
||||||
|
|
||||||
|
c, _, priv, _ = NewTestCert(Version2, Curve_P256, ca2, caKey2, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
rawPriv, b, curve, err = UnmarshalPrivateKeyFromPEM(priv)
|
||||||
|
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, priv[:16])
|
||||||
|
require.ErrorIs(t, err, ErrInvalidPrivateKey)
|
||||||
|
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, priv)
|
||||||
|
require.ErrorIs(t, err, ErrInvalidPrivateKey)
|
||||||
|
|
||||||
|
aCa, ok := ca2.(*certificateV2)
|
||||||
|
require.True(t, ok)
|
||||||
|
aCa.curve = Curve(99)
|
||||||
|
err = aCa.VerifyPrivateKey(Curve(99), priv2)
|
||||||
|
require.EqualError(t, err, "invalid curve: 99")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_VerifyPrivateKeyP256(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_P256, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
err := ca.VerifyPrivateKey(Curve_P256, caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, caKey2, _ := NewTestCaCert(Version2, Curve_P256, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = ca.VerifyPrivateKey(Curve_P256, caKey2)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
c, _, priv, _ := NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
|
rawPriv, b, curve, err := UnmarshalPrivateKeyFromPEM(priv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, b)
|
||||||
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, rawPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, priv2 := P256Keypair()
|
||||||
|
err = c.VerifyPrivateKey(Curve_P256, priv2)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_Copy(t *testing.T) {
|
||||||
|
ca, _, caKey, _ := NewTestCaCert(Version2, Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, nil)
|
||||||
|
c, _, _, _ := NewTestCert(Version2, Curve_CURVE25519, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, nil)
|
||||||
|
cc := c.Copy()
|
||||||
|
test.AssertDeepCopyEqual(t, c, cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalCertificateV2(t *testing.T) {
|
||||||
|
data := []byte("\x98\x00\x00")
|
||||||
|
_, err := unmarshalCertificateV2(data, nil, Curve_CURVE25519)
|
||||||
|
require.EqualError(t, err, "bad wire format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV2_marshalForSigningStability(t *testing.T) {
|
||||||
|
before := time.Date(1996, time.May, 5, 0, 0, 0, 0, time.UTC)
|
||||||
|
after := before.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedRawDetailsStr = "a070800774657374696e67a10e04050a0101021004050a01010118a20e0405090101031004050901010218a3270c0b746573742d67726f7570310c0b746573742d67726f7570320c0b746573742d67726f7570338504318bef808604318befbc87101234567890abcdef1234567890abcdef"
|
||||||
|
expectedRawDetails, err := hex.DecodeString(expectedRawDetailsStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
db, err := nc.details.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedRawDetails, db)
|
||||||
|
|
||||||
|
expectedForSigning, err := hex.DecodeString(expectedRawDetailsStr + "00313233343536373839306162636564666768696a313233343536373839306162")
|
||||||
|
b, err := nc.marshalForSigning()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedForSigning, b)
|
||||||
|
}
|
||||||
+159
-2
@@ -3,14 +3,28 @@ package cert
|
|||||||
import (
|
import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KDF factors
|
type NebulaEncryptedData struct {
|
||||||
|
EncryptionMetadata NebulaEncryptionMetadata
|
||||||
|
Ciphertext []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type NebulaEncryptionMetadata struct {
|
||||||
|
EncryptionAlgorithm string
|
||||||
|
Argon2Parameters Argon2Parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Argon2Parameters KDF factors
|
||||||
type Argon2Parameters struct {
|
type Argon2Parameters struct {
|
||||||
version rune
|
version rune
|
||||||
Memory uint32 // KiB
|
Memory uint32 // KiB
|
||||||
@@ -19,7 +33,7 @@ type Argon2Parameters struct {
|
|||||||
salt []byte
|
salt []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a new Argon2Parameters object with current version set
|
// NewArgon2Parameters Returns a new Argon2Parameters object with current version set
|
||||||
func NewArgon2Parameters(memory uint32, parallelism uint8, iterations uint32) *Argon2Parameters {
|
func NewArgon2Parameters(memory uint32, parallelism uint8, iterations uint32) *Argon2Parameters {
|
||||||
return &Argon2Parameters{
|
return &Argon2Parameters{
|
||||||
version: argon2.Version,
|
version: argon2.Version,
|
||||||
@@ -141,3 +155,146 @@ func splitNonceCiphertext(blob []byte, nonceSize int) ([]byte, []byte, error) {
|
|||||||
|
|
||||||
return blob[:nonceSize], blob[nonceSize:], nil
|
return blob[:nonceSize], blob[nonceSize:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncryptAndMarshalSigningPrivateKey is a simple helper to encrypt and PEM encode a private key
|
||||||
|
func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
|
||||||
|
ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = proto.Marshal(&RawNebulaEncryptedData{
|
||||||
|
EncryptionMetadata: &RawNebulaEncryptionMetadata{
|
||||||
|
EncryptionAlgorithm: "AES-256-GCM",
|
||||||
|
Argon2Parameters: &RawNebulaArgon2Parameters{
|
||||||
|
Version: kdfParams.version,
|
||||||
|
Memory: kdfParams.Memory,
|
||||||
|
Parallelism: uint32(kdfParams.Parallelism),
|
||||||
|
Iterations: kdfParams.Iterations,
|
||||||
|
Salt: kdfParams.salt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Ciphertext: ciphertext,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519PrivateKeyBanner, Bytes: b}), nil
|
||||||
|
case Curve_P256:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: EncryptedECDSAP256PrivateKeyBanner, Bytes: b}), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid curve: %v", curve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalNebulaEncryptedData will unmarshal a protobuf byte representation of a nebula cert into its
|
||||||
|
// protobuf-generated struct.
|
||||||
|
func UnmarshalNebulaEncryptedData(b []byte) (*NebulaEncryptedData, error) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil, fmt.Errorf("nil byte array")
|
||||||
|
}
|
||||||
|
var rned RawNebulaEncryptedData
|
||||||
|
err := proto.Unmarshal(b, &rned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rned.EncryptionMetadata == nil {
|
||||||
|
return nil, fmt.Errorf("encoded EncryptionMetadata was nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rned.EncryptionMetadata.Argon2Parameters == nil {
|
||||||
|
return nil, fmt.Errorf("encoded Argon2Parameters was nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := unmarshalArgon2Parameters(rned.EncryptionMetadata.Argon2Parameters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ned := NebulaEncryptedData{
|
||||||
|
EncryptionMetadata: NebulaEncryptionMetadata{
|
||||||
|
EncryptionAlgorithm: rned.EncryptionMetadata.EncryptionAlgorithm,
|
||||||
|
Argon2Parameters: *params,
|
||||||
|
},
|
||||||
|
Ciphertext: rned.Ciphertext,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalArgon2Parameters(params *RawNebulaArgon2Parameters) (*Argon2Parameters, error) {
|
||||||
|
if params.Version < math.MinInt32 || params.Version > math.MaxInt32 {
|
||||||
|
return nil, fmt.Errorf("Argon2Parameters Version must be at least %d and no more than %d", math.MinInt32, math.MaxInt32)
|
||||||
|
}
|
||||||
|
if params.Memory <= 0 || params.Memory > math.MaxUint32 {
|
||||||
|
return nil, fmt.Errorf("Argon2Parameters Memory must be be greater than 0 and no more than %d KiB", uint32(math.MaxUint32))
|
||||||
|
}
|
||||||
|
if params.Parallelism <= 0 || params.Parallelism > math.MaxUint8 {
|
||||||
|
return nil, fmt.Errorf("Argon2Parameters Parallelism must be be greater than 0 and no more than %d", math.MaxUint8)
|
||||||
|
}
|
||||||
|
if params.Iterations <= 0 || params.Iterations > math.MaxUint32 {
|
||||||
|
return nil, fmt.Errorf("-argon-iterations must be be greater than 0 and no more than %d", uint32(math.MaxUint32))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Argon2Parameters{
|
||||||
|
version: params.Version,
|
||||||
|
Memory: params.Memory,
|
||||||
|
Parallelism: uint8(params.Parallelism),
|
||||||
|
Iterations: params.Iterations,
|
||||||
|
salt: params.Salt,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptAndUnmarshalSigningPrivateKey will try to pem decode and decrypt an Ed25519/ECDSA private key with
|
||||||
|
// the given passphrase, returning any other bytes b or an error on failure
|
||||||
|
func DecryptAndUnmarshalSigningPrivateKey(passphrase, b []byte) (Curve, []byte, []byte, error) {
|
||||||
|
var curve Curve
|
||||||
|
|
||||||
|
k, r := pem.Decode(b)
|
||||||
|
if k == nil {
|
||||||
|
return curve, nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k.Type {
|
||||||
|
case EncryptedEd25519PrivateKeyBanner:
|
||||||
|
curve = Curve_CURVE25519
|
||||||
|
case EncryptedECDSAP256PrivateKeyBanner:
|
||||||
|
curve = Curve_P256
|
||||||
|
default:
|
||||||
|
return curve, nil, r, fmt.Errorf("bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
|
||||||
|
}
|
||||||
|
|
||||||
|
ned, err := UnmarshalNebulaEncryptedData(k.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return curve, nil, r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes []byte
|
||||||
|
switch ned.EncryptionMetadata.EncryptionAlgorithm {
|
||||||
|
case "AES-256-GCM":
|
||||||
|
bytes, err = aes256Decrypt(passphrase, &ned.EncryptionMetadata.Argon2Parameters, ned.Ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return curve, nil, r, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return curve, nil, r, fmt.Errorf("unsupported encryption algorithm: %s", ned.EncryptionMetadata.EncryptionAlgorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
if len(bytes) != ed25519.PrivateKeySize {
|
||||||
|
return curve, nil, r, fmt.Errorf("key was not %d bytes, is invalid ed25519 private key", ed25519.PrivateKeySize)
|
||||||
|
}
|
||||||
|
case Curve_P256:
|
||||||
|
if len(bytes) != 32 {
|
||||||
|
return curve, nil, r, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return curve, bytes, r, nil
|
||||||
|
}
|
||||||
|
|||||||
+90
-2
@@ -4,22 +4,110 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewArgon2Parameters(t *testing.T) {
|
func TestNewArgon2Parameters(t *testing.T) {
|
||||||
p := NewArgon2Parameters(64*1024, 4, 3)
|
p := NewArgon2Parameters(64*1024, 4, 3)
|
||||||
assert.EqualValues(t, &Argon2Parameters{
|
assert.Equal(t, &Argon2Parameters{
|
||||||
version: argon2.Version,
|
version: argon2.Version,
|
||||||
Memory: 64 * 1024,
|
Memory: 64 * 1024,
|
||||||
Parallelism: 4,
|
Parallelism: 4,
|
||||||
Iterations: 3,
|
Iterations: 3,
|
||||||
}, p)
|
}, p)
|
||||||
p = NewArgon2Parameters(2*1024*1024, 2, 1)
|
p = NewArgon2Parameters(2*1024*1024, 2, 1)
|
||||||
assert.EqualValues(t, &Argon2Parameters{
|
assert.Equal(t, &Argon2Parameters{
|
||||||
version: argon2.Version,
|
version: argon2.Version,
|
||||||
Memory: 2 * 1024 * 1024,
|
Memory: 2 * 1024 * 1024,
|
||||||
Parallelism: 2,
|
Parallelism: 2,
|
||||||
Iterations: 1,
|
Iterations: 1,
|
||||||
}, p)
|
}, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecryptAndUnmarshalSigningPrivateKey(t *testing.T) {
|
||||||
|
passphrase := []byte("DO NOT USE")
|
||||||
|
privKey := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
|
||||||
|
CjsKC0FFUy0yNTYtR0NNEiwIExCAgAQYAyAEKiCPoDfGQiosxNPTbPn5EsMlc2MI
|
||||||
|
c0Bt4oz6gTrFQhX3aBJcimhHKeAuhyTGvllD0Z19fe+DFPcLH3h5VrdjVfIAajg0
|
||||||
|
KrbV3n9UHif/Au5skWmquNJzoW1E4MTdRbvpti6o+WdQ49DxjBFhx0YH8LBqrbPU
|
||||||
|
0BGkUHmIO7daP24=
|
||||||
|
-----END NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
shortKey := []byte(`# A key which, once decrypted, is too short
|
||||||
|
-----BEGIN NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
|
||||||
|
CjsKC0FFUy0yNTYtR0NNEiwIExCAgAQYAyAEKiAVJwdfl3r+eqi/vF6S7OMdpjfo
|
||||||
|
hAzmTCRnr58Su4AqmBJbCv3zleYCEKYJP6UI3S8ekLMGISsgO4hm5leukCCyqT0Z
|
||||||
|
cQ76yrberpzkJKoPLGisX8f+xdy4aXSZl7oEYWQte1+vqbtl/eY9PGZhxUQdcyq7
|
||||||
|
hqzIyrRqfUgVuA==
|
||||||
|
-----END NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
invalidBanner := []byte(`# Invalid banner (not encrypted)
|
||||||
|
-----BEGIN NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
bWRp2CTVFhW9HD/qCd28ltDgK3w8VXSeaEYczDWos8sMUBqDb9jP3+NYwcS4lURG
|
||||||
|
XgLvodMXZJuaFPssp+WwtA==
|
||||||
|
-----END NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
invalidPem := []byte(`# Not a valid PEM format
|
||||||
|
-BEGIN NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
|
||||||
|
CjwKC0FFUy0yNTYtR0NNEi0IExCAgIABGAEgBCognnjujd67Vsv99p22wfAjQaDT
|
||||||
|
oCMW1mdjkU3gACKNW4MSXOWR9Sts4C81yk1RUku2gvGKs3TB9LYoklLsIizSYOLl
|
||||||
|
+Vs//O1T0I1Xbml2XBAROsb/VSoDln/6LMqR4B6fn6B3GOsLBBqRI8daDl9lRMPB
|
||||||
|
qrlJ69wer3ZUHFXA
|
||||||
|
-END NEBULA ED25519 ENCRYPTED PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
|
||||||
|
keyBundle := appendByteSlices(privKey, shortKey, invalidBanner, invalidPem)
|
||||||
|
|
||||||
|
// Success test case
|
||||||
|
curve, k, rest, err := DecryptAndUnmarshalSigningPrivateKey(passphrase, keyBundle)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
assert.Len(t, k, 64)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||||
|
|
||||||
|
// Fail due to short key
|
||||||
|
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
|
||||||
|
require.EqualError(t, err, "key was not 64 bytes, is invalid ed25519 private key")
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||||
|
|
||||||
|
// Fail due to invalid banner
|
||||||
|
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
|
||||||
|
// Fail due to invalid PEM format, because
|
||||||
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
|
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey(passphrase, rest)
|
||||||
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
|
||||||
|
// Fail due to invalid passphrase
|
||||||
|
curve, k, rest, err = DecryptAndUnmarshalSigningPrivateKey([]byte("invalid passphrase"), privKey)
|
||||||
|
require.EqualError(t, err, "invalid passphrase or corrupt private key")
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, []byte{}, rest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptAndMarshalSigningPrivateKey(t *testing.T) {
|
||||||
|
// Having proved that decryption works correctly above, we can test the
|
||||||
|
// encryption function produces a value which can be decrypted
|
||||||
|
passphrase := []byte("passphrase")
|
||||||
|
bytes := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||||
|
kdfParams := NewArgon2Parameters(64*1024, 4, 3)
|
||||||
|
key, err := EncryptAndMarshalSigningPrivateKey(Curve_CURVE25519, bytes, passphrase, kdfParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the "key" can be decrypted successfully
|
||||||
|
curve, k, rest, err := DecryptAndUnmarshalSigningPrivateKey(passphrase, key)
|
||||||
|
assert.Len(t, k, 64)
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
assert.Equal(t, []byte{}, rest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// EncryptAndMarshalEd25519PrivateKey does not create any errors itself
|
||||||
|
}
|
||||||
|
|||||||
+44
-7
@@ -2,14 +2,51 @@ package cert
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrRootExpired = errors.New("root certificate is expired")
|
ErrBadFormat = errors.New("bad wire format")
|
||||||
ErrExpired = errors.New("certificate is expired")
|
ErrRootExpired = errors.New("root certificate is expired")
|
||||||
ErrNotCA = errors.New("certificate is not a CA")
|
ErrExpired = errors.New("certificate is expired")
|
||||||
ErrNotSelfSigned = errors.New("certificate is not self-signed")
|
ErrNotCA = errors.New("certificate is not a CA")
|
||||||
ErrBlockListed = errors.New("certificate is in the block list")
|
ErrNotSelfSigned = errors.New("certificate is not self-signed")
|
||||||
ErrSignatureMismatch = errors.New("certificate signature did not match")
|
ErrBlockListed = errors.New("certificate is in the block list")
|
||||||
ErrInvalidPEMCertificateUnsupported = errors.New("bytes contain an unsupported certificate format")
|
ErrFingerprintMismatch = errors.New("certificate fingerprint did not match")
|
||||||
|
ErrSignatureMismatch = errors.New("certificate signature did not match")
|
||||||
|
ErrInvalidPublicKey = errors.New("invalid public key")
|
||||||
|
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||||
|
ErrPublicPrivateCurveMismatch = errors.New("public key does not match private key curve")
|
||||||
|
ErrPublicPrivateKeyMismatch = errors.New("public key and private key are not a pair")
|
||||||
|
ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
|
||||||
|
ErrCaNotFound = errors.New("could not find ca for the certificate")
|
||||||
|
ErrUnknownVersion = errors.New("certificate version unrecognized")
|
||||||
|
ErrCertPubkeyPresent = errors.New("certificate has unexpected pubkey present")
|
||||||
|
ErrCurveMismatch = errors.New("certificate curve does not match CA")
|
||||||
|
|
||||||
|
ErrInvalidPEMBlock = errors.New("input did not contain a valid PEM encoded block")
|
||||||
|
ErrInvalidPEMCertificateBanner = errors.New("bytes did not contain a proper certificate banner")
|
||||||
|
ErrInvalidPEMX25519PublicKeyBanner = errors.New("bytes did not contain a proper X25519 public key banner")
|
||||||
|
ErrInvalidPEMX25519PrivateKeyBanner = errors.New("bytes did not contain a proper X25519 private key banner")
|
||||||
|
ErrInvalidPEMEd25519PublicKeyBanner = errors.New("bytes did not contain a proper Ed25519 public key banner")
|
||||||
|
ErrInvalidPEMEd25519PrivateKeyBanner = errors.New("bytes did not contain a proper Ed25519 private key banner")
|
||||||
|
|
||||||
|
ErrNoPeerStaticKey = errors.New("no peer static key was present")
|
||||||
|
ErrNoPayload = errors.New("provided payload was empty")
|
||||||
|
|
||||||
|
ErrMissingDetails = errors.New("certificate did not contain details")
|
||||||
|
ErrEmptySignature = errors.New("empty signature")
|
||||||
|
ErrEmptyRawDetails = errors.New("empty rawDetails not allowed")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ErrInvalidCertificateProperties struct {
|
||||||
|
str string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrInvalidCertificateProperties(format string, a ...any) error {
|
||||||
|
return &ErrInvalidCertificateProperties{fmt.Sprintf(format, a...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrInvalidCertificateProperties) Error() string {
|
||||||
|
return e.str
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testCertNow is the reference "now" used to derive default before/after times
|
||||||
|
// in NewTestCaCert and NewTestCert. Holding it fixed for the lifetime of the
|
||||||
|
// test binary keeps CA and leaf defaults aligned at the same second, so a leaf
|
||||||
|
// signed with default times can never expire after its CA on a rounding race.
|
||||||
|
var testCertNow = time.Now().Round(time.Second)
|
||||||
|
|
||||||
|
// NewTestCaCert will create a new ca certificate
|
||||||
|
func NewTestCaCert(version Version, curve Curve, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (Certificate, []byte, []byte, []byte) {
|
||||||
|
var err error
|
||||||
|
var pub, priv []byte
|
||||||
|
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
pub, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
case Curve_P256:
|
||||||
|
privk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub = elliptic.Marshal(elliptic.P256(), privk.PublicKey.X, privk.PublicKey.Y)
|
||||||
|
priv = privk.D.FillBytes(make([]byte, 32))
|
||||||
|
default:
|
||||||
|
// There is no default to allow the underlying lib to respond with an error
|
||||||
|
}
|
||||||
|
|
||||||
|
if before.IsZero() {
|
||||||
|
before = testCertNow.Add(time.Second * -60)
|
||||||
|
}
|
||||||
|
if after.IsZero() {
|
||||||
|
after = testCertNow.Add(time.Second * 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &TBSCertificate{
|
||||||
|
Curve: curve,
|
||||||
|
Version: version,
|
||||||
|
Name: "test ca",
|
||||||
|
NotBefore: time.Unix(before.Unix(), 0),
|
||||||
|
NotAfter: time.Unix(after.Unix(), 0),
|
||||||
|
PublicKey: pub,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
Groups: groups,
|
||||||
|
IsCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := t.Sign(nil, curve, priv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := c.MarshalPEM()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, pub, priv, pem
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestCert will generate a signed certificate with the provided details.
|
||||||
|
// Expiry times are defaulted if you do not pass them in
|
||||||
|
func NewTestCert(v Version, curve Curve, ca Certificate, key []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (Certificate, []byte, []byte, []byte) {
|
||||||
|
if before.IsZero() {
|
||||||
|
before = testCertNow.Add(time.Second * -60)
|
||||||
|
}
|
||||||
|
|
||||||
|
if after.IsZero() {
|
||||||
|
after = testCertNow.Add(time.Second * 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(networks) == 0 {
|
||||||
|
networks = []netip.Prefix{netip.MustParsePrefix("10.0.0.123/8")}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub, priv []byte
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
pub, priv = X25519Keypair()
|
||||||
|
case Curve_P256:
|
||||||
|
pub, priv = P256Keypair()
|
||||||
|
default:
|
||||||
|
panic("unknown curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
nc := &TBSCertificate{
|
||||||
|
Version: v,
|
||||||
|
Curve: curve,
|
||||||
|
Name: name,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
Groups: groups,
|
||||||
|
NotBefore: time.Unix(before.Unix(), 0),
|
||||||
|
NotAfter: time.Unix(after.Unix(), 0),
|
||||||
|
PublicKey: pub,
|
||||||
|
IsCA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := c.MarshalPEM()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, pub, MarshalPrivateKeyToPEM(curve, priv), pem
|
||||||
|
}
|
||||||
|
|
||||||
|
func X25519Keypair() ([]byte, []byte) {
|
||||||
|
privkey := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := curve25519.X25519(privkey, curve25519.Basepoint)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubkey, privkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func P256Keypair() ([]byte, []byte) {
|
||||||
|
privkey, err := ecdh.P256().GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pubkey := privkey.PublicKey()
|
||||||
|
return pubkey.Bytes(), privkey.Bytes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
+277
@@ -0,0 +1,277 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrTruncatedPEMBlock = errors.New("truncated PEM block")
|
||||||
|
|
||||||
|
// SplitPEM is a split function for bufio.Scanner that returns each PEM block.
|
||||||
|
func SplitPEM(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
// Look for the start of a PEM block
|
||||||
|
start := bytes.Index(data, []byte("-----BEGIN "))
|
||||||
|
if start == -1 {
|
||||||
|
if atEOF && len(bytes.TrimSpace(data)) > 0 {
|
||||||
|
// Non-whitespace content with no PEM block
|
||||||
|
return 0, nil, ErrTruncatedPEMBlock
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return len(data), nil, nil
|
||||||
|
}
|
||||||
|
// Request more data
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the end marker
|
||||||
|
endMarkerStart := bytes.Index(data[start:], []byte("-----END "))
|
||||||
|
if endMarkerStart == -1 {
|
||||||
|
if atEOF {
|
||||||
|
// Incomplete PEM block at EOF
|
||||||
|
return 0, nil, ErrTruncatedPEMBlock
|
||||||
|
}
|
||||||
|
// Need more data to find the end
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the actual end of the END line (after the newline)
|
||||||
|
endMarkerStart += start
|
||||||
|
endLineEnd := bytes.IndexByte(data[endMarkerStart:], '\n')
|
||||||
|
var end int
|
||||||
|
if endLineEnd == -1 {
|
||||||
|
if atEOF {
|
||||||
|
// END marker without newline at EOF - take it anyway
|
||||||
|
end = len(data)
|
||||||
|
} else {
|
||||||
|
// Need more data
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
end = endMarkerStart + endLineEnd + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the PEM block
|
||||||
|
pemBlock := data[start:end]
|
||||||
|
|
||||||
|
// Return the valid PEM block
|
||||||
|
return end, pemBlock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ( //cert banners
|
||||||
|
CertificateBanner = "NEBULA CERTIFICATE"
|
||||||
|
CertificateV2Banner = "NEBULA CERTIFICATE V2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ( //key-agreement-key banners
|
||||||
|
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
|
||||||
|
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
|
||||||
|
P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY"
|
||||||
|
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */
|
||||||
|
const ( //signing key banners
|
||||||
|
EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
|
||||||
|
ECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 PRIVATE KEY"
|
||||||
|
ECDSAP256PublicKeyBanner = "NEBULA ECDSA P256 PUBLIC KEY"
|
||||||
|
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
|
||||||
|
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
|
||||||
|
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnmarshalCertificateFromPEM will try to unmarshal the first pem block in a byte array, returning any non consumed
|
||||||
|
// data or an error on failure
|
||||||
|
func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) {
|
||||||
|
p, r := pem.Decode(b)
|
||||||
|
if p == nil {
|
||||||
|
return nil, r, ErrInvalidPEMBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := unmarshalCertificateBlock(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, r, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalCertificateBlock decodes a single PEM block into a certificate.
|
||||||
|
// It expects a Nebula certificate banner and returns ErrInvalidPEMCertificateBanner otherwise.
|
||||||
|
func unmarshalCertificateBlock(block *pem.Block) (Certificate, error) {
|
||||||
|
switch block.Type {
|
||||||
|
// Implementations must validate the resulting certificate contains valid information
|
||||||
|
case CertificateBanner:
|
||||||
|
return unmarshalCertificateV1(block.Bytes, nil)
|
||||||
|
case CertificateV2Banner:
|
||||||
|
return unmarshalCertificateV2(block.Bytes, nil, Curve_CURVE25519)
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidPEMCertificateBanner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalCertPublicKeyToPEM(c Certificate) []byte {
|
||||||
|
if c.IsCA() {
|
||||||
|
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
|
||||||
|
} else {
|
||||||
|
return MarshalPublicKeyToPEM(c.Curve(), c.PublicKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPublicKeyToPEM returns a PEM representation of a public key used for ECDH.
|
||||||
|
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
|
||||||
|
func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
|
||||||
|
case Curve_P256:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalSigningPublicKeyToPEM returns a PEM representation of a public key used for signing.
|
||||||
|
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
|
||||||
|
func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
|
||||||
|
case Curve_P256:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PublicKeyBanner, Bytes: b})
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||||
|
// consumed data or an error on failure. Only key-agreement (ECDH) public key banners are accepted.
|
||||||
|
// Use UnmarshalSigningPublicKeyFromPEM for Ed25519/ECDSA banners.
|
||||||
|
func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||||
|
k, r := pem.Decode(b)
|
||||||
|
if k == nil {
|
||||||
|
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 X25519PublicKeyBanner:
|
||||||
|
expectedLen = 32
|
||||||
|
curve = Curve_CURVE25519
|
||||||
|
case P256PublicKeyBanner:
|
||||||
|
// Uncompressed
|
||||||
|
expectedLen = 65
|
||||||
|
curve = Curve_P256
|
||||||
|
default:
|
||||||
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalSigningPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||||
|
// consumed data or an error on failure. Only Ed25519/ECDSA public key banners are accepted.
|
||||||
|
// Use UnmarshalPublicKeyFromPEM for X25519/P256 (ECDH) banners.
|
||||||
|
func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
||||||
|
k, r := pem.Decode(b)
|
||||||
|
if k == nil {
|
||||||
|
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
|
var expectedLen int
|
||||||
|
var curve Curve
|
||||||
|
switch k.Type {
|
||||||
|
case Ed25519PublicKeyBanner:
|
||||||
|
expectedLen = 32
|
||||||
|
curve = Curve_CURVE25519
|
||||||
|
case ECDSAP256PublicKeyBanner:
|
||||||
|
// Uncompressed
|
||||||
|
expectedLen = 65
|
||||||
|
curve = Curve_P256
|
||||||
|
default:
|
||||||
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||||
|
}
|
||||||
|
if len(k.Bytes) != expectedLen {
|
||||||
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
|
||||||
|
}
|
||||||
|
return k.Bytes, r, curve, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: X25519PrivateKeyBanner, Bytes: b})
|
||||||
|
case Curve_P256:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: P256PrivateKeyBanner, Bytes: b})
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalSigningPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
||||||
|
switch curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: b})
|
||||||
|
case Curve_P256:
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PrivateKeyBanner, Bytes: b})
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPrivateKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
||||||
|
// consumed data or an error on failure
|
||||||
|
func UnmarshalPrivateKeyFromPEM(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 X25519PrivateKeyBanner:
|
||||||
|
expectedLen = 32
|
||||||
|
curve = Curve_CURVE25519
|
||||||
|
case P256PrivateKeyBanner:
|
||||||
|
expectedLen = 32
|
||||||
|
curve = Curve_P256
|
||||||
|
default:
|
||||||
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper private key banner")
|
||||||
|
}
|
||||||
|
if len(k.Bytes) != expectedLen {
|
||||||
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s private key", expectedLen, curve)
|
||||||
|
}
|
||||||
|
return k.Bytes, r, curve, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmarshalSigningPrivateKeyFromPEM(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 curve Curve
|
||||||
|
switch k.Type {
|
||||||
|
case EncryptedEd25519PrivateKeyBanner:
|
||||||
|
return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
|
||||||
|
case EncryptedECDSAP256PrivateKeyBanner:
|
||||||
|
return nil, nil, Curve_P256, ErrPrivateKeyEncrypted
|
||||||
|
case Ed25519PrivateKeyBanner:
|
||||||
|
curve = Curve_CURVE25519
|
||||||
|
if len(k.Bytes) != ed25519.PrivateKeySize {
|
||||||
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid Ed25519 private key", ed25519.PrivateKeySize)
|
||||||
|
}
|
||||||
|
case ECDSAP256PrivateKeyBanner:
|
||||||
|
curve = Curve_P256
|
||||||
|
if len(k.Bytes) != 32 {
|
||||||
|
return nil, r, 0, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA private key banner")
|
||||||
|
}
|
||||||
|
return k.Bytes, r, curve, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func scanAll(t *testing.T, input string) ([]string, error) {
|
||||||
|
t.Helper()
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||||
|
scanner.Split(SplitPEM)
|
||||||
|
var blocks []string
|
||||||
|
for scanner.Scan() {
|
||||||
|
blocks = append(blocks, scanner.Text())
|
||||||
|
}
|
||||||
|
return blocks, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_Single(t *testing.T) {
|
||||||
|
input := "-----BEGIN TEST-----\ndata\n-----END TEST-----\n"
|
||||||
|
blocks, err := scanAll(t, input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, blocks, 1)
|
||||||
|
require.Equal(t, input, blocks[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_Multiple(t *testing.T) {
|
||||||
|
block1 := "-----BEGIN TEST-----\naaa\n-----END TEST-----\n"
|
||||||
|
block2 := "-----BEGIN TEST-----\nbbb\n-----END TEST-----\n"
|
||||||
|
blocks, err := scanAll(t, block1+block2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, blocks, 2)
|
||||||
|
require.Equal(t, block1, blocks[0])
|
||||||
|
require.Equal(t, block2, blocks[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_CommentsAndWhitespaceBetweenBlocks(t *testing.T) {
|
||||||
|
input := "# comment\n\n-----BEGIN TEST-----\naaa\n-----END TEST-----\n\n# another comment\n\n-----BEGIN TEST-----\nbbb\n-----END TEST-----\n"
|
||||||
|
blocks, err := scanAll(t, input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, blocks, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_Empty(t *testing.T) {
|
||||||
|
blocks, err := scanAll(t, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_WhitespaceOnly(t *testing.T) {
|
||||||
|
blocks, err := scanAll(t, " \n\t\n ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_TrailingGarbage(t *testing.T) {
|
||||||
|
input := "-----BEGIN TEST-----\ndata\n-----END TEST-----\ngarbage"
|
||||||
|
blocks, err := scanAll(t, input)
|
||||||
|
require.ErrorIs(t, err, ErrTruncatedPEMBlock)
|
||||||
|
require.Len(t, blocks, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_TruncatedBlock(t *testing.T) {
|
||||||
|
input := "-----BEGIN TEST-----\npartial data with no end"
|
||||||
|
_, err := scanAll(t, input)
|
||||||
|
require.ErrorIs(t, err, ErrTruncatedPEMBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_NoEndNewline(t *testing.T) {
|
||||||
|
input := "-----BEGIN TEST-----\ndata\n-----END TEST-----"
|
||||||
|
blocks, err := scanAll(t, input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, blocks, 1)
|
||||||
|
require.Equal(t, input, blocks[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitPEM_GarbageOnly(t *testing.T) {
|
||||||
|
_, err := scanAll(t, "this is not PEM data")
|
||||||
|
require.ErrorIs(t, err, ErrTruncatedPEMBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalCertificateFromPEM(t *testing.T) {
|
||||||
|
goodCert := []byte(`
|
||||||
|
# A good cert
|
||||||
|
-----BEGIN NEBULA CERTIFICATE-----
|
||||||
|
CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
|
||||||
|
vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
|
||||||
|
bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
|
||||||
|
-----END NEBULA CERTIFICATE-----
|
||||||
|
`)
|
||||||
|
badBanner := []byte(`# A bad banner
|
||||||
|
-----BEGIN NOT A NEBULA CERTIFICATE-----
|
||||||
|
CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
|
||||||
|
vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
|
||||||
|
bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
|
||||||
|
-----END NOT A NEBULA CERTIFICATE-----
|
||||||
|
`)
|
||||||
|
invalidPem := []byte(`# Not a valid PEM format
|
||||||
|
-BEGIN NEBULA CERTIFICATE-----
|
||||||
|
CkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL
|
||||||
|
vcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv
|
||||||
|
bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
|
||||||
|
-END NEBULA CERTIFICATE----`)
|
||||||
|
|
||||||
|
certBundle := appendByteSlices(goodCert, badBanner, invalidPem)
|
||||||
|
|
||||||
|
// Success test case
|
||||||
|
cert, rest, err := UnmarshalCertificateFromPEM(certBundle)
|
||||||
|
assert.NotNil(t, cert)
|
||||||
|
assert.Equal(t, rest, append(badBanner, invalidPem...))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Fail due to invalid banner.
|
||||||
|
cert, rest, err = UnmarshalCertificateFromPEM(rest)
|
||||||
|
assert.Nil(t, cert)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper certificate banner")
|
||||||
|
|
||||||
|
// Fail due to invalid PEM format, because
|
||||||
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
|
cert, rest, err = UnmarshalCertificateFromPEM(rest)
|
||||||
|
assert.Nil(t, cert)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalSigningPrivateKeyFromPEM(t *testing.T) {
|
||||||
|
privKey := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
|
-----END NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
privP256Key := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA ECDSA P256 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA ECDSA P256 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
shortKey := []byte(`# A short key
|
||||||
|
-----BEGIN NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
-----END NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
invalidBanner := []byte(`# Invalid banner
|
||||||
|
-----BEGIN NOT A NEBULA PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
|
-----END NOT A NEBULA PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
invalidPem := []byte(`# Not a valid PEM format
|
||||||
|
-BEGIN NEBULA ED25519 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
|
-END NEBULA ED25519 PRIVATE KEY-----`)
|
||||||
|
|
||||||
|
keyBundle := appendByteSlices(privKey, privP256Key, shortKey, invalidBanner, invalidPem)
|
||||||
|
|
||||||
|
// Success test case
|
||||||
|
k, rest, curve, err := UnmarshalSigningPrivateKeyFromPEM(keyBundle)
|
||||||
|
assert.Len(t, k, 64)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(privP256Key, shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Success test case
|
||||||
|
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
||||||
|
assert.Len(t, k, 32)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Fail due to short key
|
||||||
|
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||||
|
require.EqualError(t, err, "key was not 64 bytes, is invalid Ed25519 private key")
|
||||||
|
|
||||||
|
// Fail due to invalid banner
|
||||||
|
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA private key banner")
|
||||||
|
|
||||||
|
// Fail due to invalid PEM format, because
|
||||||
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
|
k, rest, curve, err = UnmarshalSigningPrivateKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalPrivateKeyFromPEM(t *testing.T) {
|
||||||
|
privKey := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA X25519 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA X25519 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
privP256Key := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA P256 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA P256 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
shortKey := []byte(`# A short key
|
||||||
|
-----BEGIN NEBULA X25519 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
|
-----END NEBULA X25519 PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
invalidBanner := []byte(`# Invalid banner
|
||||||
|
-----BEGIN NOT A NEBULA PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NOT A NEBULA PRIVATE KEY-----
|
||||||
|
`)
|
||||||
|
invalidPem := []byte(`# Not a valid PEM format
|
||||||
|
-BEGIN NEBULA X25519 PRIVATE KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-END NEBULA X25519 PRIVATE KEY-----`)
|
||||||
|
|
||||||
|
keyBundle := appendByteSlices(privKey, privP256Key, shortKey, invalidBanner, invalidPem)
|
||||||
|
|
||||||
|
// Success test case
|
||||||
|
k, rest, curve, err := UnmarshalPrivateKeyFromPEM(keyBundle)
|
||||||
|
assert.Len(t, k, 32)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(privP256Key, shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Success test case
|
||||||
|
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
||||||
|
assert.Len(t, k, 32)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Fail due to short key
|
||||||
|
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||||
|
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 private key")
|
||||||
|
|
||||||
|
// Fail due to invalid banner
|
||||||
|
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper private key banner")
|
||||||
|
|
||||||
|
// Fail due to invalid PEM format, because
|
||||||
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
|
k, rest, curve, err = UnmarshalPrivateKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalPublicKeyFromPEM(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
pubKey := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA X25519 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
pubP256Key := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
signingKey := []byte(`# A signing key has the wrong scope for this function
|
||||||
|
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
shortKey := []byte(`# A short key
|
||||||
|
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
|
-----END NEBULA X25519 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 X25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-END NEBULA X25519 PUBLIC KEY-----`)
|
||||||
|
|
||||||
|
keyBundle := appendByteSlices(pubKey, pubP256Key, signingKey, shortKey, invalidBanner, invalidPem)
|
||||||
|
|
||||||
|
// X25519 key
|
||||||
|
k, rest, curve, err := UnmarshalPublicKeyFromPEM(keyBundle)
|
||||||
|
assert.Len(t, k, 32)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(pubP256Key, signingKey, shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
|
||||||
|
// P256 key
|
||||||
|
k, rest, curve, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
|
assert.Len(t, k, 65)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(signingKey, shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
|
||||||
|
// Reject a signing public key (Ed25519/ECDSA banner)
|
||||||
|
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||||
|
|
||||||
|
// Fail due to short key
|
||||||
|
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||||
|
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||||
|
|
||||||
|
// Fail due to invalid banner
|
||||||
|
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper public key banner")
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
|
||||||
|
// Fail due to invalid PEM format, because
|
||||||
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
|
k, rest, _, err = UnmarshalPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalSigningPublicKeyFromPEM(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
pubKey := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
pubP256Key := []byte(`# A good key
|
||||||
|
-----BEGIN NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA ECDSA P256 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
ecdhKey := []byte(`# A key-agreement key has the wrong scope for this function
|
||||||
|
-----BEGIN NEBULA X25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NEBULA X25519 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
shortKey := []byte(`# A short key
|
||||||
|
-----BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||||
|
-----END NEBULA ED25519 PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
invalidBanner := []byte(`# Invalid banner
|
||||||
|
-----BEGIN NOT A NEBULA PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-----END NOT A NEBULA PUBLIC KEY-----
|
||||||
|
`)
|
||||||
|
invalidPem := []byte(`# Not a valid PEM format
|
||||||
|
-BEGIN NEBULA ED25519 PUBLIC KEY-----
|
||||||
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
-END NEBULA ED25519 PUBLIC KEY-----`)
|
||||||
|
|
||||||
|
keyBundle := appendByteSlices(pubKey, pubP256Key, ecdhKey, shortKey, invalidBanner, invalidPem)
|
||||||
|
|
||||||
|
// Ed25519 key
|
||||||
|
k, rest, curve, err := UnmarshalSigningPublicKeyFromPEM(keyBundle)
|
||||||
|
assert.Len(t, k, 32)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(pubP256Key, ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_CURVE25519, curve)
|
||||||
|
|
||||||
|
// ECDSA P256 key
|
||||||
|
k, rest, curve, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||||
|
assert.Len(t, k, 65)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(ecdhKey, shortKey, invalidBanner, invalidPem))
|
||||||
|
assert.Equal(t, Curve_P256, curve)
|
||||||
|
|
||||||
|
// Reject a key-agreement public key (X25519/P256 banner)
|
||||||
|
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(shortKey, invalidBanner, invalidPem))
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||||
|
|
||||||
|
// Fail due to short key
|
||||||
|
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, appendByteSlices(invalidBanner, invalidPem))
|
||||||
|
require.EqualError(t, err, "key was not 32 bytes, is invalid CURVE25519 public key")
|
||||||
|
|
||||||
|
// Fail due to invalid banner
|
||||||
|
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
require.EqualError(t, err, "bytes did not contain a proper Ed25519/ECDSA public key banner")
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
|
||||||
|
// Fail due to invalid PEM format, because
|
||||||
|
// it's missing the requisite pre-encapsulation boundary.
|
||||||
|
k, rest, _, err = UnmarshalSigningPublicKeyFromPEM(rest)
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Equal(t, rest, invalidPem)
|
||||||
|
require.EqualError(t, err, "input did not contain a valid PEM encoded block")
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert/p256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TBSCertificate represents a certificate intended to be signed.
|
||||||
|
// It is invalid to use this structure as a Certificate.
|
||||||
|
type TBSCertificate struct {
|
||||||
|
Version Version
|
||||||
|
Name string
|
||||||
|
Networks []netip.Prefix
|
||||||
|
UnsafeNetworks []netip.Prefix
|
||||||
|
Groups []string
|
||||||
|
IsCA bool
|
||||||
|
NotBefore time.Time
|
||||||
|
NotAfter time.Time
|
||||||
|
PublicKey []byte
|
||||||
|
Curve Curve
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
|
||||||
|
type beingSignedCertificate interface {
|
||||||
|
// fromTBSCertificate copies the values from the TBSCertificate to this versions internal representation
|
||||||
|
// Implementations must validate the resulting certificate contains valid information
|
||||||
|
fromTBSCertificate(*TBSCertificate) error
|
||||||
|
|
||||||
|
// marshalForSigning returns the bytes that should be signed
|
||||||
|
marshalForSigning() ([]byte, error)
|
||||||
|
|
||||||
|
// setSignature sets the signature for the certificate that has just been signed. The signature must not be blank.
|
||||||
|
setSignature([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignerLambda func(certBytes []byte) ([]byte, error)
|
||||||
|
|
||||||
|
// Sign will create a sealed certificate using details provided by the TBSCertificate as long as those
|
||||||
|
// details do not violate constraints of the signing certificate.
|
||||||
|
// If the TBSCertificate is a CA then signer must be nil.
|
||||||
|
func (t *TBSCertificate) Sign(signer Certificate, curve Curve, key []byte) (Certificate, error) {
|
||||||
|
switch t.Curve {
|
||||||
|
case Curve_CURVE25519:
|
||||||
|
pk := ed25519.PrivateKey(key)
|
||||||
|
sp := func(certBytes []byte) ([]byte, error) {
|
||||||
|
sig := ed25519.Sign(pk, certBytes)
|
||||||
|
return sig, nil
|
||||||
|
}
|
||||||
|
return t.SignWith(signer, curve, sp)
|
||||||
|
case Curve_P256:
|
||||||
|
pk, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sp := func(certBytes []byte) ([]byte, error) {
|
||||||
|
// We need to hash first for ECDSA
|
||||||
|
// - https://pkg.go.dev/crypto/ecdsa#SignASN1
|
||||||
|
hashed := sha256.Sum256(certBytes)
|
||||||
|
return ecdsa.SignASN1(rand.Reader, pk, hashed[:])
|
||||||
|
}
|
||||||
|
return t.SignWith(signer, curve, sp)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid curve: %s", t.Curve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignWith does the same thing as sign, but uses the function in `sp` to calculate the signature.
|
||||||
|
// You should only use SignWith if you do not have direct access to your private key.
|
||||||
|
func (t *TBSCertificate) SignWith(signer Certificate, curve Curve, sp SignerLambda) (Certificate, error) {
|
||||||
|
if curve != t.Curve {
|
||||||
|
return nil, fmt.Errorf("curve in cert and private key supplied don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
if signer != nil {
|
||||||
|
if t.IsCA {
|
||||||
|
return nil, fmt.Errorf("can not sign a CA certificate with another")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := checkCAConstraints(signer, t.NotBefore, t.NotAfter, t.Groups, t.Networks, t.UnsafeNetworks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer, err := signer.Fingerprint()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error computing issuer: %v", err)
|
||||||
|
}
|
||||||
|
t.issuer = issuer
|
||||||
|
} else {
|
||||||
|
if !t.IsCA {
|
||||||
|
return nil, fmt.Errorf("self signed certificates must have IsCA set to true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var c beingSignedCertificate
|
||||||
|
switch t.Version {
|
||||||
|
case Version1:
|
||||||
|
c = &certificateV1{}
|
||||||
|
err := c.fromTBSCertificate(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case Version2:
|
||||||
|
c = &certificateV2{}
|
||||||
|
err := c.fromTBSCertificate(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown cert version %d", t.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes, err := c.marshalForSigning()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := sp(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if curve == Curve_P256 {
|
||||||
|
sig, err = p256.Normalize(sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.setSignature(sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sc, ok := c.(Certificate)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func comparePrefix(a, b netip.Prefix) int {
|
||||||
|
addr := a.Addr().Compare(b.Addr())
|
||||||
|
if addr == 0 {
|
||||||
|
return a.Bits() - b.Bits()
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// findDuplicatePrefix returns an error if there is a duplicate prefix in the pre-sorted input slice sortedPrefixes
|
||||||
|
func findDuplicatePrefix(sortedPrefixes []netip.Prefix) error {
|
||||||
|
if len(sortedPrefixes) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := 1; i < len(sortedPrefixes); i++ {
|
||||||
|
if comparePrefix(sortedPrefixes[i], sortedPrefixes[i-1]) == 0 {
|
||||||
|
return NewErrInvalidCertificateProperties("duplicate network detected: %v", sortedPrefixes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert/p256"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCertificateV1_Sign(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("1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
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/24"),
|
||||||
|
},
|
||||||
|
Groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||||
|
NotBefore: before,
|
||||||
|
NotAfter: after,
|
||||||
|
PublicKey: pubKey,
|
||||||
|
IsCA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
c, err := tbs.Sign(&certificateV1{details: detailsV1{notBefore: before, notAfter: after}}, Curve_CURVE25519, priv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
assert.True(t, c.CheckSignature(pub))
|
||||||
|
|
||||||
|
b, err := c.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
uc, err := unmarshalCertificateV1(b, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, uc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateV1_SignP256(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: false,
|
||||||
|
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))
|
||||||
|
|
||||||
|
c, err := tbs.Sign(&certificateV1{details: detailsV1{notBefore: before, notAfter: after}}, Curve_P256, rawPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
assert.True(t, c.CheckSignature(pub))
|
||||||
|
|
||||||
|
b, err := c.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
uc, err := unmarshalCertificateV1(b, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, uc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificate_SignP256_AlwaysNormalized(t *testing.T) {
|
||||||
|
before := time.Now().Add(time.Second * -60).Round(time.Second)
|
||||||
|
after := time.Now().Add(time.Second * 60).Round(time.Second)
|
||||||
|
pubKey := []byte("01234567890abcedfghij1234567890ab1234567890abcedfghij1234567890ab")
|
||||||
|
|
||||||
|
tbs := TBSCertificate{
|
||||||
|
Version: Version1,
|
||||||
|
Name: "testing",
|
||||||
|
Networks: []netip.Prefix{
|
||||||
|
mustParsePrefixUnmapped("10.1.1.1/24"),
|
||||||
|
mustParsePrefixUnmapped("10.1.1.2/16"),
|
||||||
|
},
|
||||||
|
UnsafeNetworks: []netip.Prefix{
|
||||||
|
mustParsePrefixUnmapped("9.1.1.2/24"),
|
||||||
|
mustParsePrefixUnmapped("9.1.1.3/16"),
|
||||||
|
},
|
||||||
|
Groups: []string{"test-group1", "test-group2", "test-group3"},
|
||||||
|
NotBefore: before,
|
||||||
|
NotAfter: after,
|
||||||
|
PublicKey: pubKey,
|
||||||
|
IsCA: true,
|
||||||
|
Curve: Curve_P256,
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y)
|
||||||
|
rawPriv := priv.D.FillBytes(make([]byte, 32))
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
if i&1 == 1 {
|
||||||
|
tbs.Version = Version1
|
||||||
|
} else {
|
||||||
|
tbs.Version = Version2
|
||||||
|
}
|
||||||
|
c, err := tbs.Sign(nil, Curve_P256, rawPriv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
assert.True(t, c.CheckSignature(pub))
|
||||||
|
normie, err := p256.IsNormalized(c.Signature())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, normie)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package cert_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"golang.org/x/crypto/curve25519"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testCertNow is the reference "now" used to derive default before/after times
|
||||||
|
// in NewTestCaCert and NewTestCert. Holding it fixed for the lifetime of the
|
||||||
|
// test binary keeps CA and leaf defaults aligned at the same second, so a leaf
|
||||||
|
// signed with default times can never expire after its CA on a rounding race.
|
||||||
|
var testCertNow = time.Now().Round(time.Second)
|
||||||
|
|
||||||
|
// NewTestCaCert will create a new ca certificate
|
||||||
|
func NewTestCaCert(version cert.Version, curve cert.Curve, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte, []byte, []byte) {
|
||||||
|
var err error
|
||||||
|
var pub, priv []byte
|
||||||
|
|
||||||
|
switch curve {
|
||||||
|
case cert.Curve_CURVE25519:
|
||||||
|
pub, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
case cert.Curve_P256:
|
||||||
|
privk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub = elliptic.Marshal(elliptic.P256(), privk.PublicKey.X, privk.PublicKey.Y)
|
||||||
|
priv = privk.D.FillBytes(make([]byte, 32))
|
||||||
|
default:
|
||||||
|
// There is no default to allow the underlying lib to respond with an error
|
||||||
|
}
|
||||||
|
|
||||||
|
if before.IsZero() {
|
||||||
|
before = testCertNow.Add(time.Second * -60)
|
||||||
|
}
|
||||||
|
if after.IsZero() {
|
||||||
|
after = testCertNow.Add(time.Second * 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &cert.TBSCertificate{
|
||||||
|
Curve: curve,
|
||||||
|
Version: version,
|
||||||
|
Name: "test ca",
|
||||||
|
NotBefore: time.Unix(before.Unix(), 0),
|
||||||
|
NotAfter: time.Unix(after.Unix(), 0),
|
||||||
|
PublicKey: pub,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
Groups: groups,
|
||||||
|
IsCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := t.Sign(nil, curve, priv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := c.MarshalPEM()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, pub, priv, pem
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestCert will generate a signed certificate with the provided details.
|
||||||
|
// Expiry times are defaulted if you do not pass them in
|
||||||
|
func NewTestCert(v cert.Version, curve cert.Curve, ca cert.Certificate, key []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte, []byte, []byte) {
|
||||||
|
if before.IsZero() {
|
||||||
|
before = testCertNow.Add(time.Second * -60)
|
||||||
|
}
|
||||||
|
|
||||||
|
if after.IsZero() {
|
||||||
|
after = testCertNow.Add(time.Second * 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub, priv []byte
|
||||||
|
switch curve {
|
||||||
|
case cert.Curve_CURVE25519:
|
||||||
|
pub, priv = X25519Keypair()
|
||||||
|
case cert.Curve_P256:
|
||||||
|
pub, priv = P256Keypair()
|
||||||
|
default:
|
||||||
|
panic("unknown curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
nc := &cert.TBSCertificate{
|
||||||
|
Version: v,
|
||||||
|
Curve: curve,
|
||||||
|
Name: name,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
Groups: groups,
|
||||||
|
NotBefore: time.Unix(before.Unix(), 0),
|
||||||
|
NotAfter: time.Unix(after.Unix(), 0),
|
||||||
|
PublicKey: pub,
|
||||||
|
IsCA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := c.MarshalPEM()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, pub, cert.MarshalPrivateKeyToPEM(curve, priv), pem
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestCertDifferentVersion(c cert.Certificate, v cert.Version, ca cert.Certificate, key []byte) (cert.Certificate, []byte) {
|
||||||
|
nc := &cert.TBSCertificate{
|
||||||
|
Version: v,
|
||||||
|
Curve: c.Curve(),
|
||||||
|
Name: c.Name(),
|
||||||
|
Networks: c.Networks(),
|
||||||
|
UnsafeNetworks: c.UnsafeNetworks(),
|
||||||
|
Groups: c.Groups(),
|
||||||
|
NotBefore: time.Unix(c.NotBefore().Unix(), 0),
|
||||||
|
NotAfter: time.Unix(c.NotAfter().Unix(), 0),
|
||||||
|
PublicKey: c.PublicKey(),
|
||||||
|
IsCA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := nc.Sign(ca, ca.Curve(), key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pem, err := c.MarshalPEM()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, pem
|
||||||
|
}
|
||||||
|
|
||||||
|
func X25519Keypair() ([]byte, []byte) {
|
||||||
|
privkey := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := curve25519.X25519(privkey, curve25519.Basepoint)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubkey, privkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func P256Keypair() ([]byte, []byte) {
|
||||||
|
privkey, err := ecdh.P256().GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pubkey := privkey.PublicKey()
|
||||||
|
return pubkey.Bytes(), privkey.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DummyCert is a minimal cert.Certificate implementation for testing error paths.
|
||||||
|
type DummyCert struct {
|
||||||
|
Version_ cert.Version
|
||||||
|
Curve_ cert.Curve
|
||||||
|
Groups_ []string
|
||||||
|
IsCA_ bool
|
||||||
|
Issuer_ string
|
||||||
|
Name_ string
|
||||||
|
Networks_ []netip.Prefix
|
||||||
|
NotAfter_ time.Time
|
||||||
|
NotBefore_ time.Time
|
||||||
|
PublicKey_ []byte
|
||||||
|
Signature_ []byte
|
||||||
|
UnsafeNetworks_ []netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DummyCert) Version() cert.Version { return d.Version_ }
|
||||||
|
func (d *DummyCert) Curve() cert.Curve { return d.Curve_ }
|
||||||
|
func (d *DummyCert) Groups() []string { return d.Groups_ }
|
||||||
|
func (d *DummyCert) IsCA() bool { return d.IsCA_ }
|
||||||
|
func (d *DummyCert) Issuer() string { return d.Issuer_ }
|
||||||
|
func (d *DummyCert) Name() string { return d.Name_ }
|
||||||
|
func (d *DummyCert) Networks() []netip.Prefix { return d.Networks_ }
|
||||||
|
func (d *DummyCert) NotAfter() time.Time { return d.NotAfter_ }
|
||||||
|
func (d *DummyCert) NotBefore() time.Time { return d.NotBefore_ }
|
||||||
|
func (d *DummyCert) PublicKey() []byte { return d.PublicKey_ }
|
||||||
|
func (d *DummyCert) Signature() []byte { return d.Signature_ }
|
||||||
|
func (d *DummyCert) UnsafeNetworks() []netip.Prefix { return d.UnsafeNetworks_ }
|
||||||
|
func (d *DummyCert) Fingerprint() (string, error) { return "", nil }
|
||||||
|
func (d *DummyCert) CheckSignature(key []byte) bool { return false }
|
||||||
|
func (d *DummyCert) MarshalForHandshakes() ([]byte, error) { return nil, nil }
|
||||||
|
func (d *DummyCert) MarshalPEM() ([]byte, error) { return nil, nil }
|
||||||
|
func (d *DummyCert) MarshalJSON() ([]byte, error) { return nil, nil }
|
||||||
|
func (d *DummyCert) Marshal() ([]byte, error) { return nil, nil }
|
||||||
|
func (d *DummyCert) String() string { return "dummy" }
|
||||||
|
func (d *DummyCert) Copy() cert.Certificate { return d }
|
||||||
|
func (d *DummyCert) VerifyPrivateKey(c cert.Curve, k []byte) error { return nil }
|
||||||
|
func (d *DummyCert) Expired(time.Time) bool { return false }
|
||||||
|
func (d *DummyCert) MarshalPublicKeyPEM() []byte { return nil }
|
||||||
|
func (d *DummyCert) PublicKeyPEM() []byte { return nil }
|
||||||
|
|
||||||
|
// NewTestCAPool creates a CAPool from the given CA certificates, panicking on error.
|
||||||
|
func NewTestCAPool(cas ...cert.Certificate) *cert.CAPool {
|
||||||
|
pool := cert.NewCAPool()
|
||||||
|
for _, ca := range cas {
|
||||||
|
if err := pool.AddCA(ca); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool
|
||||||
|
}
|
||||||
+195
-103
@@ -8,13 +8,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"github.com/slackhq/nebula/pkclient"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,32 +27,43 @@ type caFlags struct {
|
|||||||
outCertPath *string
|
outCertPath *string
|
||||||
outQRPath *string
|
outQRPath *string
|
||||||
groups *string
|
groups *string
|
||||||
ips *string
|
networks *string
|
||||||
subnets *string
|
unsafeNetworks *string
|
||||||
argonMemory *uint
|
argonMemory *uint
|
||||||
argonIterations *uint
|
argonIterations *uint
|
||||||
argonParallelism *uint
|
argonParallelism *uint
|
||||||
encryption *bool
|
encryption *bool
|
||||||
|
version *uint
|
||||||
|
|
||||||
curve *string
|
curve *string
|
||||||
|
p11url *string
|
||||||
|
|
||||||
|
// Deprecated options
|
||||||
|
ips *string
|
||||||
|
subnets *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCaFlags() *caFlags {
|
func newCaFlags() *caFlags {
|
||||||
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
|
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
|
||||||
cf.set.Usage = func() {}
|
cf.set.Usage = func() {}
|
||||||
cf.name = cf.set.String("name", "", "Required: name of the certificate authority")
|
cf.name = cf.set.String("name", "", "Required: name of the certificate authority")
|
||||||
|
cf.version = cf.set.Uint("version", uint(cert.Version2), "Optional: version of the certificate format to use")
|
||||||
cf.duration = cf.set.Duration("duration", time.Duration(time.Hour*8760), "Optional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
|
cf.duration = cf.set.Duration("duration", time.Duration(time.Hour*8760), "Optional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
|
||||||
cf.outKeyPath = cf.set.String("out-key", "ca.key", "Optional: path to write the private key to")
|
cf.outKeyPath = cf.set.String("out-key", "ca.key", "Optional: path to write the private key to")
|
||||||
cf.outCertPath = cf.set.String("out-crt", "ca.crt", "Optional: path to write the certificate to")
|
cf.outCertPath = cf.set.String("out-crt", "ca.crt", "Optional: path to write the certificate to")
|
||||||
cf.outQRPath = cf.set.String("out-qr", "", "Optional: output a qr code image (png) of the certificate")
|
cf.outQRPath = cf.set.String("out-qr", "", "Optional: output a qr code image (png) of the certificate")
|
||||||
cf.groups = cf.set.String("groups", "", "Optional: comma separated list of groups. This will limit which groups subordinate certs can use")
|
cf.groups = cf.set.String("groups", "", "Optional: comma separated list of groups. This will limit which groups subordinate certs can use")
|
||||||
cf.ips = cf.set.String("ips", "", "Optional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use for ip addresses")
|
cf.networks = cf.set.String("networks", "", "Optional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in networks")
|
||||||
cf.subnets = cf.set.String("subnets", "", "Optional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use in subnets")
|
cf.unsafeNetworks = cf.set.String("unsafe-networks", "", "Optional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in unsafe networks")
|
||||||
cf.argonMemory = cf.set.Uint("argon-memory", 2*1024*1024, "Optional: Argon2 memory parameter (in KiB) used for encrypted private key passphrase")
|
cf.argonMemory = cf.set.Uint("argon-memory", 2*1024*1024, "Optional: Argon2 memory parameter (in KiB) used for encrypted private key passphrase")
|
||||||
cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
|
cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
|
||||||
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
|
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
|
||||||
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
|
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
|
||||||
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
|
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
|
||||||
|
cf.p11url = p11Flag(cf.set)
|
||||||
|
|
||||||
|
cf.ips = cf.set.String("ips", "", "Deprecated, see -networks")
|
||||||
|
cf.subnets = cf.set.String("subnets", "", "Deprecated, see -unsafe-networks")
|
||||||
return &cf
|
return &cf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,17 +88,34 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isP11 := len(*cf.p11url) > 0
|
||||||
|
|
||||||
if err := mustFlagString("name", cf.name); err != nil {
|
if err := mustFlagString("name", cf.name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := mustFlagString("out-key", cf.outKeyPath); err != nil {
|
if !isP11 {
|
||||||
return err
|
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// out-key is meaningless under PKCS#11 because the private key never
|
||||||
|
// leaves the HSM; reject it so we never silently accept or claim a
|
||||||
|
// stdout slot for it.
|
||||||
|
outKeySet := false
|
||||||
|
cf.set.Visit(func(f *flag.Flag) {
|
||||||
|
if f.Name == "out-key" {
|
||||||
|
outKeySet = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if outKeySet {
|
||||||
|
return newHelpErrorf("cannot set -out-key with -pkcs11")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := mustFlagString("out-crt", cf.outCertPath); err != nil {
|
if err := mustFlagString("out-crt", cf.outCertPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var kdfParams *cert.Argon2Parameters
|
var kdfParams *cert.Argon2Parameters
|
||||||
if *cf.encryption {
|
if !isP11 && *cf.encryption {
|
||||||
if kdfParams, err = parseArgonParameters(*cf.argonMemory, *cf.argonParallelism, *cf.argonIterations); err != nil {
|
if kdfParams, err = parseArgonParameters(*cf.argonMemory, *cf.argonParallelism, *cf.argonIterations); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -106,139 +135,201 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ips []*net.IPNet
|
version := cert.Version(*cf.version)
|
||||||
if *cf.ips != "" {
|
if version != cert.Version1 && version != cert.Version2 {
|
||||||
for _, rs := range strings.Split(*cf.ips, ",") {
|
return newHelpErrorf("-version must be either %v or %v", cert.Version1, cert.Version2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var networks []netip.Prefix
|
||||||
|
if *cf.networks == "" && *cf.ips != "" {
|
||||||
|
// Pull up deprecated -ips flag if needed
|
||||||
|
*cf.networks = *cf.ips
|
||||||
|
}
|
||||||
|
|
||||||
|
if *cf.networks != "" {
|
||||||
|
for _, rs := range strings.Split(*cf.networks, ",") {
|
||||||
rs := strings.Trim(rs, " ")
|
rs := strings.Trim(rs, " ")
|
||||||
if rs != "" {
|
if rs != "" {
|
||||||
ip, ipNet, err := net.ParseCIDR(rs)
|
n, err := netip.ParsePrefix(rs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newHelpErrorf("invalid ip definition: %s", err)
|
return newHelpErrorf("invalid -networks definition: %s", rs)
|
||||||
}
|
}
|
||||||
if ip.To4() == nil {
|
if version == cert.Version1 && !n.Addr().Is4() {
|
||||||
return newHelpErrorf("invalid ip definition: can only be ipv4, have %s", rs)
|
return newHelpErrorf("invalid -networks definition: v1 certificates can only be ipv4, have %s", rs)
|
||||||
}
|
}
|
||||||
|
networks = append(networks, n)
|
||||||
ipNet.IP = ip
|
|
||||||
ips = append(ips, ipNet)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var subnets []*net.IPNet
|
var unsafeNetworks []netip.Prefix
|
||||||
if *cf.subnets != "" {
|
if *cf.unsafeNetworks == "" && *cf.subnets != "" {
|
||||||
for _, rs := range strings.Split(*cf.subnets, ",") {
|
// Pull up deprecated -subnets flag if needed
|
||||||
|
*cf.unsafeNetworks = *cf.subnets
|
||||||
|
}
|
||||||
|
|
||||||
|
if *cf.unsafeNetworks != "" {
|
||||||
|
for _, rs := range strings.Split(*cf.unsafeNetworks, ",") {
|
||||||
rs := strings.Trim(rs, " ")
|
rs := strings.Trim(rs, " ")
|
||||||
if rs != "" {
|
if rs != "" {
|
||||||
_, s, err := net.ParseCIDR(rs)
|
n, err := netip.ParsePrefix(rs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newHelpErrorf("invalid subnet definition: %s", err)
|
return newHelpErrorf("invalid -unsafe-networks definition: %s", rs)
|
||||||
}
|
}
|
||||||
if s.IP.To4() == nil {
|
if version == cert.Version1 && !n.Addr().Is4() {
|
||||||
return newHelpErrorf("invalid subnet definition: can only be ipv4, have %s", rs)
|
return newHelpErrorf("invalid -unsafe-networks definition: v1 certificates can only be ipv4, have %s", rs)
|
||||||
}
|
}
|
||||||
subnets = append(subnets, s)
|
unsafeNetworks = append(unsafeNetworks, n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 *cf.encryption {
|
if !isP11 && *cf.encryption {
|
||||||
for i := 0; i < 5; i++ {
|
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
||||||
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 {
|
||||||
return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext")
|
for i := 0; i < 5; i++ {
|
||||||
|
errOut.Write([]byte("Enter passphrase: "))
|
||||||
|
passphrase, err = pr.ReadPassword()
|
||||||
|
|
||||||
|
if err == ErrNoTerminal {
|
||||||
|
return fmt.Errorf("out-key must be encrypted interactively")
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("error reading passphrase: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(passphrase) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
return fmt.Errorf("no passphrase specified, remove -encrypt flag to write out-key in plaintext")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var curve cert.Curve
|
var curve cert.Curve
|
||||||
var pub, rawPriv []byte
|
var pub, rawPriv []byte
|
||||||
switch *cf.curve {
|
var p11Client *pkclient.PKClient
|
||||||
case "25519", "X25519", "Curve25519", "CURVE25519":
|
|
||||||
curve = cert.Curve_CURVE25519
|
if isP11 {
|
||||||
pub, rawPriv, err = ed25519.GenerateKey(rand.Reader)
|
switch *cf.curve {
|
||||||
if err != nil {
|
case "P256":
|
||||||
return fmt.Errorf("error while generating ed25519 keys: %s", err)
|
curve = cert.Curve_P256
|
||||||
}
|
default:
|
||||||
case "P256":
|
return fmt.Errorf("invalid curve for PKCS#11: %s", *cf.curve)
|
||||||
var key *ecdsa.PrivateKey
|
|
||||||
curve = cert.Curve_P256
|
|
||||||
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while generating ecdsa keys: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ecdh.PrivateKey lets us get at the encoded bytes, even though
|
p11Client, err = pkclient.FromUrl(*cf.p11url)
|
||||||
// we aren't using ECDH here.
|
|
||||||
eKey, err := key.ECDH()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while converting ecdsa key: %s", err)
|
return fmt.Errorf("error while creating PKCS#11 client: %w", err)
|
||||||
}
|
}
|
||||||
rawPriv = eKey.Bytes()
|
defer func(client *pkclient.PKClient) {
|
||||||
pub = eKey.PublicKey().Bytes()
|
_ = client.Close()
|
||||||
}
|
}(p11Client)
|
||||||
|
pub, err = p11Client.GetPubKey()
|
||||||
nc := cert.NebulaCertificate{
|
|
||||||
Details: cert.NebulaCertificateDetails{
|
|
||||||
Name: *cf.name,
|
|
||||||
Groups: groups,
|
|
||||||
Ips: ips,
|
|
||||||
Subnets: subnets,
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(*cf.duration),
|
|
||||||
PublicKey: pub,
|
|
||||||
IsCA: true,
|
|
||||||
Curve: curve,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
|
||||||
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
|
||||||
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nc.Sign(curve, rawPriv)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while signing: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var b []byte
|
|
||||||
if *cf.encryption {
|
|
||||||
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while encrypting out-key: %s", err)
|
return fmt.Errorf("error while getting public key with PKCS#11: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b = cert.MarshalSigningPrivateKey(curve, rawPriv)
|
switch *cf.curve {
|
||||||
|
case "25519", "X25519", "Curve25519", "CURVE25519":
|
||||||
|
curve = cert.Curve_CURVE25519
|
||||||
|
pub, rawPriv, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while generating ed25519 keys: %s", err)
|
||||||
|
}
|
||||||
|
case "P256":
|
||||||
|
var key *ecdsa.PrivateKey
|
||||||
|
curve = cert.Curve_P256
|
||||||
|
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while generating ecdsa keys: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ecdh.PrivateKey lets us get at the encoded bytes, even though
|
||||||
|
// we aren't using ECDH here.
|
||||||
|
eKey, err := key.ECDH()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while converting ecdsa key: %s", err)
|
||||||
|
}
|
||||||
|
rawPriv = eKey.Bytes()
|
||||||
|
pub = eKey.PublicKey().Bytes()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid curve: %s", *cf.curve)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*cf.outKeyPath, b, 0600)
|
t := &cert.TBSCertificate{
|
||||||
if err != nil {
|
Version: version,
|
||||||
return fmt.Errorf("error while writing out-key: %s", err)
|
Name: *cf.name,
|
||||||
|
Groups: groups,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(*cf.duration),
|
||||||
|
PublicKey: pub,
|
||||||
|
IsCA: true,
|
||||||
|
Curve: curve,
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err = nc.MarshalToPEM()
|
if !isP11 && !isStdio(*cf.outKeyPath) {
|
||||||
|
if _, err := os.Stat(*cf.outKeyPath); err == nil {
|
||||||
|
return fmt.Errorf("refusing to overwrite existing CA key: %s", *cf.outKeyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isStdio(*cf.outCertPath) {
|
||||||
|
if _, err := os.Stat(*cf.outCertPath); err == nil {
|
||||||
|
return fmt.Errorf("refusing to overwrite existing CA cert: %s", *cf.outCertPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var c cert.Certificate
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
if isP11 {
|
||||||
|
c, err = t.SignWith(nil, curve, p11Client.SignASN1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while signing with PKCS#11: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c, err = t.Sign(nil, curve, rawPriv)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while signing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *cf.encryption {
|
||||||
|
b, err = cert.EncryptAndMarshalSigningPrivateKey(curve, rawPriv, passphrase, kdfParams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while encrypting out-key: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b = cert.MarshalSigningPrivateKeyToPEM(curve, rawPriv)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeOutput(*cf.outKeyPath, b, 0600, out)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while writing out-key: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = c.MarshalPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while marshalling certificate: %s", err)
|
return fmt.Errorf("error while marshalling certificate: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*cf.outCertPath, b, 0600)
|
err = writeOutput(*cf.outCertPath, b, 0600, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-crt: %s", err)
|
return fmt.Errorf("error while writing out-crt: %s", err)
|
||||||
}
|
}
|
||||||
@@ -249,7 +340,7 @@ func ca(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error
|
|||||||
return fmt.Errorf("error while generating qr code: %s", err)
|
return fmt.Errorf("error while generating qr code: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*cf.outQRPath, b, 0600)
|
err = writeOutput(*cf.outQRPath, b, 0600, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||||
}
|
}
|
||||||
@@ -265,6 +356,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-72
@@ -14,10 +14,9 @@ import (
|
|||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
//TODO: test file permissions
|
|
||||||
|
|
||||||
func Test_caSummary(t *testing.T) {
|
func Test_caSummary(t *testing.T) {
|
||||||
assert.Equal(t, "ca <flags>: create a self signed certificate authority", caSummary())
|
assert.Equal(t, "ca <flags>: create a self signed certificate authority", caSummary())
|
||||||
}
|
}
|
||||||
@@ -28,6 +27,7 @@ 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"+
|
||||||
@@ -43,17 +43,24 @@ func Test_caHelp(t *testing.T) {
|
|||||||
" -groups string\n"+
|
" -groups string\n"+
|
||||||
" \tOptional: comma separated list of groups. This will limit which groups subordinate certs can use\n"+
|
" \tOptional: comma separated list of groups. This will limit which groups subordinate certs can use\n"+
|
||||||
" -ips string\n"+
|
" -ips string\n"+
|
||||||
" \tOptional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use for ip addresses\n"+
|
" Deprecated, see -networks\n"+
|
||||||
" -name string\n"+
|
" -name string\n"+
|
||||||
" \tRequired: name of the certificate authority\n"+
|
" \tRequired: name of the certificate authority\n"+
|
||||||
|
" -networks string\n"+
|
||||||
|
" \tOptional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in networks\n"+
|
||||||
" -out-crt string\n"+
|
" -out-crt string\n"+
|
||||||
" \tOptional: path to write the certificate to (default \"ca.crt\")\n"+
|
" \tOptional: path to write the certificate to (default \"ca.crt\")\n"+
|
||||||
" -out-key string\n"+
|
" -out-key string\n"+
|
||||||
" \tOptional: path to write the private key to (default \"ca.key\")\n"+
|
" \tOptional: path to write the private key to (default \"ca.key\")\n"+
|
||||||
" -out-qr string\n"+
|
" -out-qr string\n"+
|
||||||
" \tOptional: output a qr code image (png) of the certificate\n"+
|
" \tOptional: output a qr code image (png) of the certificate\n"+
|
||||||
|
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||||
" -subnets string\n"+
|
" -subnets string\n"+
|
||||||
" \tOptional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use in subnets\n",
|
" \tDeprecated, see -unsafe-networks\n"+
|
||||||
|
" -unsafe-networks string\n"+
|
||||||
|
" \tOptional: comma separated list of ip address and network in CIDR notation. This will limit which ip addresses and networks subordinate certs can use in unsafe networks\n"+
|
||||||
|
" -version uint\n"+
|
||||||
|
" \tOptional: version of the certificate format to use (default 2)\n",
|
||||||
ob.String(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -78,97 +85,109 @@ func Test_ca(t *testing.T) {
|
|||||||
err: nil,
|
err: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
pwPromptOb := "Enter passphrase: "
|
pwPromptEB := "Enter passphrase: "
|
||||||
|
|
||||||
// required args
|
// required args
|
||||||
assertHelpError(t, ca(
|
assertHelpError(t, ca(
|
||||||
[]string{"-out-key", "nope", "-out-crt", "nope", "duration", "100m"}, ob, eb, nopw,
|
[]string{"-version", "1", "-out-key", "nope", "-out-crt", "nope", "duration", "100m"}, ob, eb, nopw,
|
||||||
), "-name is required")
|
), "-name is required")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// ipv4 only ips
|
// ipv4 only ips
|
||||||
assertHelpError(t, ca([]string{"-name", "ipv6", "-ips", "100::100/100"}, ob, eb, nopw), "invalid ip definition: can only be ipv4, have 100::100/100")
|
assertHelpError(t, ca([]string{"-version", "1", "-name", "ipv6", "-ips", "100::100/100"}, ob, eb, nopw), "invalid -networks definition: v1 certificates can only be ipv4, have 100::100/100")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// ipv4 only subnets
|
// ipv4 only subnets
|
||||||
assertHelpError(t, ca([]string{"-name", "ipv6", "-subnets", "100::100/100"}, ob, eb, nopw), "invalid subnet definition: can only be ipv4, have 100::100/100")
|
assertHelpError(t, ca([]string{"-version", "1", "-name", "ipv6", "-subnets", "100::100/100"}, ob, eb, nopw), "invalid -unsafe-networks definition: v1 certificates can only be ipv4, have 100::100/100")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// failed key write
|
// failed key write
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args := []string{"-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey"}
|
args := []string{"-version", "1", "-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey"}
|
||||||
assert.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
require.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// create temp key file
|
// create temp key file
|
||||||
keyF, err := os.CreateTemp("", "test.key")
|
keyF, err := os.CreateTemp("", "test.key")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
os.Remove(keyF.Name())
|
require.NoError(t, os.Remove(keyF.Name()))
|
||||||
|
|
||||||
// failed cert write
|
// failed cert write
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name()}
|
args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name()}
|
||||||
assert.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
|
require.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// create temp cert file
|
// create temp cert file
|
||||||
crtF, err := os.CreateTemp("", "test.crt")
|
crtF, err := os.CreateTemp("", "test.crt")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
os.Remove(crtF.Name())
|
require.NoError(t, os.Remove(crtF.Name()))
|
||||||
os.Remove(keyF.Name())
|
require.NoError(t, os.Remove(keyF.Name()))
|
||||||
|
|
||||||
// test proper cert with removed empty groups and subnets
|
// test proper cert with removed empty groups and subnets
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||||
assert.Nil(t, ca(args, ob, eb, nopw))
|
require.NoError(t, ca(args, ob, eb, nopw))
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// read cert and key files
|
// read cert and key files
|
||||||
rb, _ := os.ReadFile(keyF.Name())
|
rb, _ := os.ReadFile(keyF.Name())
|
||||||
lKey, b, err := cert.UnmarshalEd25519PrivateKey(rb)
|
lKey, b, c, err := cert.UnmarshalSigningPrivateKeyFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Equal(t, cert.Curve_CURVE25519, c)
|
||||||
assert.Nil(t, err)
|
assert.Empty(t, b)
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Len(t, lKey, 64)
|
assert.Len(t, lKey, 64)
|
||||||
|
|
||||||
rb, _ = os.ReadFile(crtF.Name())
|
rb, _ = os.ReadFile(crtF.Name())
|
||||||
lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb)
|
lCrt, b, err := cert.UnmarshalCertificateFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Empty(t, b)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "test", lCrt.Details.Name)
|
assert.Equal(t, "test", lCrt.Name())
|
||||||
assert.Len(t, lCrt.Details.Ips, 0)
|
assert.Empty(t, lCrt.Networks())
|
||||||
assert.True(t, lCrt.Details.IsCA)
|
assert.True(t, lCrt.IsCA())
|
||||||
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups)
|
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Groups())
|
||||||
assert.Len(t, lCrt.Details.Subnets, 0)
|
assert.Empty(t, lCrt.UnsafeNetworks())
|
||||||
assert.Len(t, lCrt.Details.PublicKey, 32)
|
assert.Len(t, lCrt.PublicKey(), 32)
|
||||||
assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore))
|
assert.Equal(t, time.Duration(time.Minute*100), lCrt.NotAfter().Sub(lCrt.NotBefore()))
|
||||||
assert.Equal(t, "", lCrt.Details.Issuer)
|
assert.Empty(t, lCrt.Issuer())
|
||||||
assert.True(t, lCrt.CheckSignature(lCrt.Details.PublicKey))
|
assert.True(t, lCrt.CheckSignature(lCrt.PublicKey()))
|
||||||
|
|
||||||
// test encrypted key
|
// test encrypted key
|
||||||
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{"-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()}
|
||||||
assert.Nil(t, ca(args, ob, eb, testpw))
|
require.NoError(t, ca(args, ob, eb, testpw))
|
||||||
assert.Equal(t, pwPromptOb, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.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())
|
||||||
|
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())
|
||||||
k, _ := pem.Decode(rb)
|
k, _ := pem.Decode(rb)
|
||||||
ned, err := cert.UnmarshalNebulaEncryptedData(k.Bytes)
|
ned, err := cert.UnmarshalNebulaEncryptedData(k.Bytes)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
// we won't know salt in advance, so just check start of string
|
// we won't know salt in advance, so just check start of string
|
||||||
assert.Equal(t, uint32(2*1024*1024), ned.EncryptionMetadata.Argon2Parameters.Memory)
|
assert.Equal(t, uint32(2*1024*1024), ned.EncryptionMetadata.Argon2Parameters.Memory)
|
||||||
assert.Equal(t, uint8(4), ned.EncryptionMetadata.Argon2Parameters.Parallelism)
|
assert.Equal(t, uint8(4), ned.EncryptionMetadata.Argon2Parameters.Parallelism)
|
||||||
@@ -178,54 +197,118 @@ func Test_ca(t *testing.T) {
|
|||||||
var curve cert.Curve
|
var curve cert.Curve
|
||||||
curve, lKey, b, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rb)
|
curve, lKey, b, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rb)
|
||||||
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, b, 0)
|
assert.Empty(t, b)
|
||||||
assert.Len(t, lKey, 64)
|
assert.Len(t, lKey, 64)
|
||||||
|
|
||||||
// test when reading passsword results in an error
|
// test when reading password 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{"-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()}
|
||||||
assert.Error(t, ca(args, ob, eb, errpw))
|
require.Error(t, ca(args, ob, eb, errpw))
|
||||||
assert.Equal(t, pwPromptOb, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Equal(t, pwPromptEB, 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())
|
||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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()}
|
||||||
assert.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.Equal(t, strings.Repeat(pwPromptOb, 5), ob.String()) // prompts 5 times before giving up
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Equal(t, strings.Repeat(pwPromptEB, 5), eb.String()) // prompts 5 times before giving up
|
||||||
|
|
||||||
// create valid cert/key for overwrite tests
|
// create valid cert/key for overwrite tests
|
||||||
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{"-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||||
assert.Nil(t, ca(args, ob, eb, nopw))
|
require.NoError(t, ca(args, ob, eb, nopw))
|
||||||
|
|
||||||
// test that we won't overwrite existing certificate file
|
// test that we won't overwrite existing certificate file
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||||
assert.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA key: "+keyF.Name())
|
require.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA key: "+keyF.Name())
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// test that we won't overwrite existing key file
|
// test that we won't overwrite existing key file
|
||||||
os.Remove(keyF.Name())
|
os.Remove(keyF.Name())
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
args = []string{"-version", "1", "-name", "test", "-duration", "100m", "-groups", "1,, 2 , ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
|
||||||
assert.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA cert: "+crtF.Name())
|
require.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA cert: "+crtF.Name())
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
+59
-19
@@ -6,6 +6,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/pkclient"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,8 +15,8 @@ type keygenFlags struct {
|
|||||||
set *flag.FlagSet
|
set *flag.FlagSet
|
||||||
outKeyPath *string
|
outKeyPath *string
|
||||||
outPubPath *string
|
outPubPath *string
|
||||||
|
curve *string
|
||||||
curve *string
|
p11url *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeygenFlags() *keygenFlags {
|
func newKeygenFlags() *keygenFlags {
|
||||||
@@ -23,6 +25,7 @@ func newKeygenFlags() *keygenFlags {
|
|||||||
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
|
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
|
||||||
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
|
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
|
||||||
cf.curve = cf.set.String("curve", "25519", "ECDH Curve (25519, P256)")
|
cf.curve = cf.set.String("curve", "25519", "ECDH Curve (25519, P256)")
|
||||||
|
cf.p11url = p11Flag(cf.set)
|
||||||
return &cf
|
return &cf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,32 +36,68 @@ func keygen(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mustFlagString("out-key", cf.outKeyPath); err != nil {
|
isP11 := len(*cf.p11url) > 0
|
||||||
return err
|
|
||||||
|
if !isP11 {
|
||||||
|
if err = mustFlagString("out-key", cf.outKeyPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if *cf.outKeyPath != "" {
|
||||||
|
return newHelpErrorf("cannot set -out-key with -pkcs11")
|
||||||
}
|
}
|
||||||
if err := mustFlagString("out-pub", cf.outPubPath); err != nil {
|
if err = mustFlagString("out-pub", cf.outPubPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pub, rawPriv []byte
|
var pub, rawPriv []byte
|
||||||
var curve cert.Curve
|
var curve cert.Curve
|
||||||
switch *cf.curve {
|
if isP11 {
|
||||||
case "25519", "X25519", "Curve25519", "CURVE25519":
|
switch *cf.curve {
|
||||||
pub, rawPriv = x25519Keypair()
|
case "P256":
|
||||||
curve = cert.Curve_CURVE25519
|
curve = cert.Curve_P256
|
||||||
case "P256":
|
default:
|
||||||
pub, rawPriv = p256Keypair()
|
return fmt.Errorf("invalid curve for PKCS#11: %s", *cf.curve)
|
||||||
curve = cert.Curve_P256
|
}
|
||||||
default:
|
} else {
|
||||||
return fmt.Errorf("invalid curve: %s", *cf.curve)
|
switch *cf.curve {
|
||||||
|
case "25519", "X25519", "Curve25519", "CURVE25519":
|
||||||
|
pub, rawPriv = x25519Keypair()
|
||||||
|
curve = cert.Curve_CURVE25519
|
||||||
|
case "P256":
|
||||||
|
pub, rawPriv = p256Keypair()
|
||||||
|
curve = cert.Curve_P256
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid curve: %s", *cf.curve)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*cf.outKeyPath, cert.MarshalPrivateKey(curve, rawPriv), 0600)
|
var claims ioClaims
|
||||||
if err != nil {
|
if err := reserveOutputs(&claims,
|
||||||
return fmt.Errorf("error while writing out-key: %s", err)
|
"out-key", *cf.outKeyPath,
|
||||||
|
"out-pub", *cf.outPubPath,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*cf.outPubPath, cert.MarshalPublicKey(curve, pub), 0600)
|
if isP11 {
|
||||||
|
p11Client, err := pkclient.FromUrl(*cf.p11url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while creating PKCS#11 client: %w", err)
|
||||||
|
}
|
||||||
|
defer func(client *pkclient.PKClient) {
|
||||||
|
_ = client.Close()
|
||||||
|
}(p11Client)
|
||||||
|
pub, err = p11Client.GetPubKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while getting public key: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = writeOutput(*cf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while writing out-key: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = writeOutput(*cf.outPubPath, cert.MarshalPublicKeyToPEM(curve, pub), 0600, out)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -72,7 +111,8 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
//TODO: test file permissions
|
|
||||||
|
|
||||||
func Test_keygenSummary(t *testing.T) {
|
func Test_keygenSummary(t *testing.T) {
|
||||||
assert.Equal(t, "keygen <flags>: create a public/private key pair. the public key can be passed to `nebula-cert sign`", keygenSummary())
|
assert.Equal(t, "keygen <flags>: create a public/private key pair. the public key can be passed to `nebula-cert sign`", keygenSummary())
|
||||||
}
|
}
|
||||||
@@ -21,12 +20,14 @@ 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"+
|
||||||
" \tRequired: path to write the private key to\n"+
|
" \tRequired: path to write the private key to\n"+
|
||||||
" -out-pub string\n"+
|
" -out-pub string\n"+
|
||||||
" \tRequired: path to write the public key to\n",
|
" \tRequired: path to write the public key to\n"+
|
||||||
|
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n"),
|
||||||
ob.String(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -37,57 +38,99 @@ func Test_keygen(t *testing.T) {
|
|||||||
|
|
||||||
// required args
|
// required args
|
||||||
assertHelpError(t, keygen([]string{"-out-pub", "nope"}, ob, eb), "-out-key is required")
|
assertHelpError(t, keygen([]string{"-out-pub", "nope"}, ob, eb), "-out-key is required")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
assertHelpError(t, keygen([]string{"-out-key", "nope"}, ob, eb), "-out-pub is required")
|
assertHelpError(t, keygen([]string{"-out-key", "nope"}, ob, eb), "-out-pub is required")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// failed key write
|
// failed key write
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args := []string{"-out-pub", "/do/not/write/pleasepub", "-out-key", "/do/not/write/pleasekey"}
|
args := []string{"-out-pub", "/do/not/write/pleasepub", "-out-key", "/do/not/write/pleasekey"}
|
||||||
assert.EqualError(t, keygen(args, ob, eb), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
require.EqualError(t, keygen(args, ob, eb), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// create temp key file
|
// create temp key file
|
||||||
keyF, err := os.CreateTemp("", "test.key")
|
keyF, err := os.CreateTemp("", "test.key")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(keyF.Name())
|
defer os.Remove(keyF.Name())
|
||||||
|
|
||||||
// failed pub write
|
// failed pub write
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-out-pub", "/do/not/write/pleasepub", "-out-key", keyF.Name()}
|
args = []string{"-out-pub", "/do/not/write/pleasepub", "-out-key", keyF.Name()}
|
||||||
assert.EqualError(t, keygen(args, ob, eb), "error while writing out-pub: open /do/not/write/pleasepub: "+NoSuchDirError)
|
require.EqualError(t, keygen(args, ob, eb), "error while writing out-pub: open /do/not/write/pleasepub: "+NoSuchDirError)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// create temp pub file
|
// create temp pub file
|
||||||
pubF, err := os.CreateTemp("", "test.pub")
|
pubF, err := os.CreateTemp("", "test.pub")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(pubF.Name())
|
defer os.Remove(pubF.Name())
|
||||||
|
|
||||||
// test proper keygen
|
// test proper keygen
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-out-pub", pubF.Name(), "-out-key", keyF.Name()}
|
args = []string{"-out-pub", pubF.Name(), "-out-key", keyF.Name()}
|
||||||
assert.Nil(t, keygen(args, ob, eb))
|
require.NoError(t, keygen(args, ob, eb))
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// read cert and key files
|
// read cert and key files
|
||||||
rb, _ := os.ReadFile(keyF.Name())
|
rb, _ := os.ReadFile(keyF.Name())
|
||||||
lKey, b, err := cert.UnmarshalX25519PrivateKey(rb)
|
lKey, b, curve, err := cert.UnmarshalPrivateKeyFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||||
assert.Nil(t, err)
|
assert.Empty(t, b)
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Len(t, lKey, 32)
|
assert.Len(t, lKey, 32)
|
||||||
|
|
||||||
rb, _ = os.ReadFile(pubF.Name())
|
rb, _ = os.ReadFile(pubF.Name())
|
||||||
lPub, b, err := cert.UnmarshalX25519PublicKey(rb)
|
lPub, b, curve, err := cert.UnmarshalPublicKeyFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||||
assert.Nil(t, err)
|
assert.Empty(t, b)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|||||||
+19
-1
@@ -5,10 +5,28 @@ 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
|
||||||
}
|
}
|
||||||
@@ -17,7 +35,7 @@ func (he *helpError) Error() string {
|
|||||||
return he.s
|
return he.s
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHelpErrorf(s string, v ...interface{}) error {
|
func newHelpErrorf(s string, v ...any) error {
|
||||||
return &helpError{s: fmt.Sprintf(s, v...)}
|
return &helpError{s: fmt.Sprintf(s, v...)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
//TODO: all flag parsing continueOnError will print to stderr on its own currently
|
|
||||||
|
|
||||||
func Test_help(t *testing.T) {
|
func Test_help(t *testing.T) {
|
||||||
expected := "Usage of " + os.Args[0] + " <global flags> <mode>:\n" +
|
expected := "Usage of " + os.Args[0] + " <global flags> <mode>:\n" +
|
||||||
" Global flags:\n" +
|
" Global flags:\n" +
|
||||||
@@ -77,8 +77,16 @@ func assertHelpError(t *testing.T, err error, msg string) {
|
|||||||
case *helpError:
|
case *helpError:
|
||||||
// good
|
// good
|
||||||
default:
|
default:
|
||||||
t.Fatal("err was not a helpError")
|
t.Fatal(fmt.Sprintf("err was not a helpError: %q, expected %q", err, msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.EqualError(t, err, msg)
|
require.EqualError(t, err, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalPkcs11String(msg string) string {
|
||||||
|
if p11Supported() {
|
||||||
|
return msg
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build cgo && pkcs11
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func p11Supported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func p11Flag(set *flag.FlagSet) *string {
|
||||||
|
return set.String("pkcs11", "", "Optional: PKCS#11 URI to an existing private key")
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build !cgo || !pkcs11
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func p11Supported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func p11Flag(set *flag.FlagSet) *string {
|
||||||
|
var ret = ""
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
@@ -22,7 +22,9 @@ func (pr StdinPasswordReader) ReadPassword() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
fmt.Println()
|
// Terminal echo is off while reading, so the user's Enter key does not
|
||||||
|
// produce a visible newline. Emit one on stderr to match the prompt.
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
|
||||||
return password, err
|
return password, err
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-13
@@ -40,33 +40,46 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCert, err := os.ReadFile(*pf.path)
|
var claims ioClaims
|
||||||
|
if err := reserveInputs(&claims, "path", *pf.path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := reserveOutputs(&claims, "out-qr", *pf.outQRPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawCert, err := readInput("path", *pf.path, &claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read cert; %s", err)
|
return fmt.Errorf("unable to read cert; %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var c *cert.NebulaCertificate
|
// When the QR is going to stdout, suppress the human-readable text/json
|
||||||
|
// output so the binary stream is not contaminated.
|
||||||
|
qrToStdout := isStdio(*pf.outQRPath)
|
||||||
|
|
||||||
|
var c cert.Certificate
|
||||||
var qrBytes []byte
|
var qrBytes []byte
|
||||||
part := 0
|
part := 0
|
||||||
|
|
||||||
|
var jsonCerts []cert.Certificate
|
||||||
|
|
||||||
for {
|
for {
|
||||||
c, rawCert, err = cert.UnmarshalNebulaCertificateFromPEM(rawCert)
|
c, rawCert, err = cert.UnmarshalCertificateFromPEM(rawCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while unmarshaling cert: %s", err)
|
return fmt.Errorf("error while unmarshaling cert: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *pf.json {
|
if !qrToStdout {
|
||||||
b, _ := json.Marshal(c)
|
if *pf.json {
|
||||||
out.Write(b)
|
jsonCerts = append(jsonCerts, c)
|
||||||
out.Write([]byte("\n"))
|
} else {
|
||||||
|
_, _ = out.Write([]byte(c.String()))
|
||||||
} else {
|
_, _ = out.Write([]byte("\n"))
|
||||||
out.Write([]byte(c.String()))
|
}
|
||||||
out.Write([]byte("\n"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if *pf.outQRPath != "" {
|
if *pf.outQRPath != "" {
|
||||||
b, err := c.MarshalToPEM()
|
b, err := c.MarshalPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while marshalling cert to PEM: %s", err)
|
return fmt.Errorf("error while marshalling cert to PEM: %s", err)
|
||||||
}
|
}
|
||||||
@@ -80,13 +93,19 @@ func printCert(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
part++
|
part++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *pf.json && !qrToStdout {
|
||||||
|
b, _ := json.Marshal(jsonCerts)
|
||||||
|
_, _ = out.Write(b)
|
||||||
|
_, _ = out.Write([]byte("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
if *pf.outQRPath != "" {
|
if *pf.outQRPath != "" {
|
||||||
b, err := qrcode.Encode(string(qrBytes), qrcode.Medium, -5)
|
b, err := qrcode.Encode(string(qrBytes), qrcode.Medium, -5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while generating qr code: %s", err)
|
return fmt.Errorf("error while generating qr code: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*pf.outQRPath, b, 0600)
|
err = writeOutput(*pf.outQRPath, b, 0600, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||||
}
|
}
|
||||||
@@ -102,6 +121,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
+197
-34
@@ -2,12 +2,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_printSummary(t *testing.T) {
|
func Test_printSummary(t *testing.T) {
|
||||||
@@ -20,6 +25,7 @@ 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"+
|
||||||
@@ -38,84 +44,241 @@ func Test_printCert(t *testing.T) {
|
|||||||
|
|
||||||
// no path
|
// no path
|
||||||
err := printCert([]string{}, ob, eb)
|
err := printCert([]string{}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assertHelpError(t, err, "-path is required")
|
assertHelpError(t, err, "-path is required")
|
||||||
|
|
||||||
// no cert at path
|
// no cert at path
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
err = printCert([]string{"-path", "does_not_exist"}, ob, eb)
|
err = printCert([]string{"-path", "does_not_exist"}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "unable to read cert; open does_not_exist: "+NoSuchFileError)
|
require.EqualError(t, err, "unable to read cert; open does_not_exist: "+NoSuchFileError)
|
||||||
|
|
||||||
// invalid cert at path
|
// invalid cert at path
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
tf, err := os.CreateTemp("", "print-cert")
|
tf, err := os.CreateTemp("", "print-cert")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(tf.Name())
|
defer os.Remove(tf.Name())
|
||||||
|
|
||||||
tf.WriteString("-----BEGIN NOPE-----")
|
tf.WriteString("-----BEGIN NOPE-----")
|
||||||
err = printCert([]string{"-path", tf.Name()}, ob, eb)
|
err = printCert([]string{"-path", tf.Name()}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "error while unmarshaling cert: input did not contain a valid PEM encoded block")
|
require.EqualError(t, err, "error while unmarshaling cert: input did not contain a valid PEM encoded block")
|
||||||
|
|
||||||
// test multiple certs
|
// test multiple certs
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
tf.Truncate(0)
|
tf.Truncate(0)
|
||||||
tf.Seek(0, 0)
|
tf.Seek(0, 0)
|
||||||
c := cert.NebulaCertificate{
|
ca, caKey := NewTestCaCert("test ca", nil, nil, time.Time{}, time.Time{}, nil, nil, nil)
|
||||||
Details: cert.NebulaCertificateDetails{
|
c, _ := NewTestCert(ca, caKey, "test", time.Time{}, time.Time{}, []netip.Prefix{netip.MustParsePrefix("10.0.0.123/8")}, nil, []string{"hi"})
|
||||||
Name: "test",
|
|
||||||
Groups: []string{"hi"},
|
|
||||||
PublicKey: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
|
|
||||||
},
|
|
||||||
Signature: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
|
|
||||||
}
|
|
||||||
|
|
||||||
p, _ := c.MarshalToPEM()
|
p, _ := c.MarshalPEM()
|
||||||
tf.Write(p)
|
tf.Write(p)
|
||||||
tf.Write(p)
|
tf.Write(p)
|
||||||
tf.Write(p)
|
tf.Write(p)
|
||||||
|
|
||||||
err = printCert([]string{"-path", tf.Name()}, ob, eb)
|
err = printCert([]string{"-path", tf.Name()}, ob, eb)
|
||||||
assert.Nil(t, err)
|
fp, _ := c.Fingerprint()
|
||||||
|
pk := hex.EncodeToString(c.PublicKey())
|
||||||
|
sig := hex.EncodeToString(c.Signature())
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"NebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: \n\t\tPublic key: 0102030405060708090001020304050607080900010203040506070809000102\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\n\tSignature: 0102030405060708090001020304050607080900010203040506070809000102\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: \n\t\tPublic key: 0102030405060708090001020304050607080900010203040506070809000102\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\n\tSignature: 0102030405060708090001020304050607080900010203040506070809000102\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: \n\t\tPublic key: 0102030405060708090001020304050607080900010203040506070809000102\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\n\tSignature: 0102030405060708090001020304050607080900010203040506070809000102\n}\n",
|
//"NebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: "+c.Issuer()+"\n\t\tPublic key: "+pk+"\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: "+fp+"\n\tSignature: "+sig+"\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: "+c.Issuer()+"\n\t\tPublic key: "+pk+"\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: "+fp+"\n\tSignature: "+sig+"\n}\nNebulaCertificate {\n\tDetails {\n\t\tName: test\n\t\tIps: []\n\t\tSubnets: []\n\t\tGroups: [\n\t\t\t\"hi\"\n\t\t]\n\t\tNot before: 0001-01-01 00:00:00 +0000 UTC\n\t\tNot After: 0001-01-01 00:00:00 +0000 UTC\n\t\tIs CA: false\n\t\tIssuer: "+c.Issuer()+"\n\t\tPublic key: "+pk+"\n\t\tCurve: CURVE25519\n\t}\n\tFingerprint: "+fp+"\n\tSignature: "+sig+"\n}\n",
|
||||||
|
`{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"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(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// test json
|
// test json
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
tf.Truncate(0)
|
tf.Truncate(0)
|
||||||
tf.Seek(0, 0)
|
tf.Seek(0, 0)
|
||||||
c = cert.NebulaCertificate{
|
|
||||||
Details: cert.NebulaCertificateDetails{
|
|
||||||
Name: "test",
|
|
||||||
Groups: []string{"hi"},
|
|
||||||
PublicKey: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
|
|
||||||
},
|
|
||||||
Signature: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2},
|
|
||||||
}
|
|
||||||
|
|
||||||
p, _ = c.MarshalToPEM()
|
|
||||||
tf.Write(p)
|
tf.Write(p)
|
||||||
tf.Write(p)
|
tf.Write(p)
|
||||||
tf.Write(p)
|
tf.Write(p)
|
||||||
|
|
||||||
err = printCert([]string{"-json", "-path", tf.Name()}, ob, eb)
|
err = printCert([]string{"-json", "-path", tf.Name()}, ob, eb)
|
||||||
assert.Nil(t, err)
|
fp, _ = c.Fingerprint()
|
||||||
|
pk = hex.EncodeToString(c.PublicKey())
|
||||||
|
sig = hex.EncodeToString(c.Signature())
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\"0102030405060708090001020304050607080900010203040506070809000102\",\"subnets\":[]},\"fingerprint\":\"cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\",\"signature\":\"0102030405060708090001020304050607080900010203040506070809000102\"}\n{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\"0102030405060708090001020304050607080900010203040506070809000102\",\"subnets\":[]},\"fingerprint\":\"cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\",\"signature\":\"0102030405060708090001020304050607080900010203040506070809000102\"}\n{\"details\":{\"curve\":\"CURVE25519\",\"groups\":[\"hi\"],\"ips\":[],\"isCa\":false,\"issuer\":\"\",\"name\":\"test\",\"notAfter\":\"0001-01-01T00:00:00Z\",\"notBefore\":\"0001-01-01T00:00:00Z\",\"publicKey\":\"0102030405060708090001020304050607080900010203040506070809000102\",\"subnets\":[]},\"fingerprint\":\"cc3492c0e9c48f17547f5987ea807462ebb3451e622590a10bb3763c344c82bd\",\"signature\":\"0102030405060708090001020304050607080900010203040506070809000102\"}\n",
|
`[{"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},{"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},{"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(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
assert.Equal(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
|
||||||
|
func NewTestCaCert(name string, pubKey, privKey []byte, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte) {
|
||||||
|
var err error
|
||||||
|
if pubKey == nil || privKey == nil {
|
||||||
|
pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &cert.TBSCertificate{
|
||||||
|
Version: cert.Version1,
|
||||||
|
Name: name,
|
||||||
|
NotBefore: time.Unix(before.Unix(), 0),
|
||||||
|
NotAfter: time.Unix(after.Unix(), 0),
|
||||||
|
PublicKey: pubKey,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
Groups: groups,
|
||||||
|
IsCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := t.Sign(nil, cert.Curve_CURVE25519, privKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestCert(ca cert.Certificate, signerKey []byte, name string, before, after time.Time, networks, unsafeNetworks []netip.Prefix, groups []string) (cert.Certificate, []byte) {
|
||||||
|
if before.IsZero() {
|
||||||
|
before = ca.NotBefore()
|
||||||
|
}
|
||||||
|
|
||||||
|
if after.IsZero() {
|
||||||
|
after = ca.NotAfter()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(networks) == 0 {
|
||||||
|
networks = []netip.Prefix{netip.MustParsePrefix("10.0.0.123/8")}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, rawPriv := x25519Keypair()
|
||||||
|
nc := &cert.TBSCertificate{
|
||||||
|
Version: cert.Version1,
|
||||||
|
Name: name,
|
||||||
|
Networks: networks,
|
||||||
|
UnsafeNetworks: unsafeNetworks,
|
||||||
|
Groups: groups,
|
||||||
|
NotBefore: time.Unix(before.Unix(), 0),
|
||||||
|
NotAfter: time.Unix(after.Unix(), 0),
|
||||||
|
PublicKey: pub,
|
||||||
|
IsCA: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := nc.Sign(ca, ca.Curve(), signerKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, rawPriv
|
||||||
}
|
}
|
||||||
|
|||||||
+286
-130
@@ -3,50 +3,63 @@ package main
|
|||||||
import (
|
import (
|
||||||
"crypto/ecdh"
|
"crypto/ecdh"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/skip2/go-qrcode"
|
"github.com/skip2/go-qrcode"
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"github.com/slackhq/nebula/pkclient"
|
||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
type signFlags struct {
|
type signFlags struct {
|
||||||
set *flag.FlagSet
|
set *flag.FlagSet
|
||||||
caKeyPath *string
|
version *uint
|
||||||
caCertPath *string
|
caKeyPath *string
|
||||||
name *string
|
caCertPath *string
|
||||||
ip *string
|
name *string
|
||||||
duration *time.Duration
|
networks *string
|
||||||
inPubPath *string
|
unsafeNetworks *string
|
||||||
outKeyPath *string
|
duration *time.Duration
|
||||||
outCertPath *string
|
inPubPath *string
|
||||||
outQRPath *string
|
outKeyPath *string
|
||||||
groups *string
|
outCertPath *string
|
||||||
subnets *string
|
outQRPath *string
|
||||||
|
groups *string
|
||||||
|
|
||||||
|
p11url *string
|
||||||
|
|
||||||
|
// Deprecated options
|
||||||
|
ip *string
|
||||||
|
subnets *string
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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")
|
||||||
sf.ip = sf.set.String("ip", "", "Required: ipv4 address and network in CIDR notation to assign the cert")
|
sf.networks = sf.set.String("networks", "", "Required: comma separated list of ip address and network in CIDR notation to assign to this cert")
|
||||||
|
sf.unsafeNetworks = sf.set.String("unsafe-networks", "", "Optional: comma separated list of ip address and network in CIDR notation. Unsafe networks this cert can route for")
|
||||||
sf.duration = sf.set.Duration("duration", 0, "Optional: how long the cert should be valid for. The default is 1 second before the signing cert expires. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
|
sf.duration = sf.set.Duration("duration", 0, "Optional: how long the cert should be valid for. The default is 1 second before the signing cert expires. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"")
|
||||||
sf.inPubPath = sf.set.String("in-pub", "", "Optional (if out-key not set): path to read a previously generated public key")
|
sf.inPubPath = sf.set.String("in-pub", "", "Optional (if out-key not set): path to read a previously generated public key")
|
||||||
sf.outKeyPath = sf.set.String("out-key", "", "Optional (if in-pub not set): path to write the private key to")
|
sf.outKeyPath = sf.set.String("out-key", "", "Optional (if in-pub not set): path to write the private key to")
|
||||||
sf.outCertPath = sf.set.String("out-crt", "", "Optional: path to write the certificate to")
|
sf.outCertPath = sf.set.String("out-crt", "", "Optional: path to write the certificate to")
|
||||||
sf.outQRPath = sf.set.String("out-qr", "", "Optional: output a qr code image (png) of the certificate")
|
sf.outQRPath = sf.set.String("out-qr", "", "Optional: output a qr code image (png) of the certificate")
|
||||||
sf.groups = sf.set.String("groups", "", "Optional: comma separated list of groups")
|
sf.groups = sf.set.String("groups", "", "Optional: comma separated list of groups")
|
||||||
sf.subnets = sf.set.String("subnets", "", "Optional: comma separated list of ipv4 address and network in CIDR notation. Subnets this cert can serve for")
|
sf.p11url = p11Flag(sf.set)
|
||||||
return &sf
|
|
||||||
|
|
||||||
|
sf.ip = sf.set.String("ip", "", "Deprecated, see -networks")
|
||||||
|
sf.subnets = sf.set.String("subnets", "", "Deprecated, see -unsafe-networks")
|
||||||
|
return &sf
|
||||||
}
|
}
|
||||||
|
|
||||||
func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error {
|
func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader) error {
|
||||||
@@ -56,8 +69,12 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mustFlagString("ca-key", sf.caKeyPath); err != nil {
|
isP11 := len(*sf.p11url) > 0
|
||||||
return err
|
|
||||||
|
if !isP11 {
|
||||||
|
if err := mustFlagString("ca-key", sf.caKeyPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := mustFlagString("ca-crt", sf.caCertPath); err != nil {
|
if err := mustFlagString("ca-crt", sf.caCertPath); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -65,90 +82,169 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
if err := mustFlagString("name", sf.name); err != nil {
|
if err := mustFlagString("name", sf.name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := mustFlagString("ip", sf.ip); err != nil {
|
if !isP11 && *sf.inPubPath != "" && *sf.outKeyPath != "" {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *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")
|
||||||
|
}
|
||||||
|
|
||||||
rawCAKey, err := os.ReadFile(*sf.caKeyPath)
|
var v4Networks []netip.Prefix
|
||||||
if err != nil {
|
var v6Networks []netip.Prefix
|
||||||
return fmt.Errorf("error while reading ca-key: %s", err)
|
if *sf.networks == "" && *sf.ip != "" {
|
||||||
|
// Pull up deprecated -ip flag if needed
|
||||||
|
*sf.networks = *sf.ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*sf.networks) == 0 {
|
||||||
|
return newHelpErrorf("-networks is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
version := cert.Version(*sf.version)
|
||||||
|
if version != 0 && version != cert.Version1 && version != 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
|
||||||
|
|
||||||
// naively attempt to decode the private key as though it is not encrypted
|
if !isP11 {
|
||||||
caKey, _, curve, err = cert.UnmarshalSigningPrivateKey(rawCAKey)
|
var rawCAKey []byte
|
||||||
if err == cert.ErrPrivateKeyEncrypted {
|
rawCAKey, err = readInput("ca-key", *sf.caKeyPath, &claims)
|
||||||
// ask for a passphrase until we get one
|
|
||||||
var passphrase []byte
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
out.Write([]byte("Enter passphrase: "))
|
|
||||||
passphrase, err = pr.ReadPassword()
|
|
||||||
|
|
||||||
if err == ErrNoTerminal {
|
|
||||||
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("error reading password: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(passphrase) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(passphrase) == 0 {
|
|
||||||
return fmt.Errorf("cannot open encrypted ca-key without passphrase")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 reading ca-key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// naively attempt to decode the private key as though it is not encrypted
|
||||||
|
caKey, _, curve, err = cert.UnmarshalSigningPrivateKeyFromPEM(rawCAKey)
|
||||||
|
if errors.Is(err, cert.ErrPrivateKeyEncrypted) {
|
||||||
|
var passphrase []byte
|
||||||
|
passphrase = []byte(os.Getenv("NEBULA_CA_PASSPHRASE"))
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
// ask for a passphrase until we get one
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
errOut.Write([]byte("Enter passphrase: "))
|
||||||
|
passphrase, err = pr.ReadPassword()
|
||||||
|
|
||||||
|
if errors.Is(err, ErrNoTerminal) {
|
||||||
|
return fmt.Errorf("ca-key is encrypted and must be decrypted interactively")
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("error reading password: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(passphrase) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(passphrase) == 0 {
|
||||||
|
return fmt.Errorf("cannot open encrypted ca-key without passphrase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
curve, caKey, _, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rawCAKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while parsing encrypted ca-key: %s", err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("error while parsing ca-key: %s", err)
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("error while parsing ca-key: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCACert, err := os.ReadFile(*sf.caCertPath)
|
rawCACert, err := readInput("ca-crt", *sf.caCertPath, &claims)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCACert)
|
caCert, _, err := cert.UnmarshalCertificateFromPEM(rawCACert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while parsing ca-crt: %s", err)
|
return fmt.Errorf("error while parsing ca-crt: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := caCert.VerifyPrivateKey(curve, caKey); err != nil {
|
if !isP11 {
|
||||||
return fmt.Errorf("refusing to sign, root certificate does not match private key")
|
if err := caCert.VerifyPrivateKey(curve, caKey); err != nil {
|
||||||
}
|
return fmt.Errorf("refusing to sign, root certificate does not match private key")
|
||||||
|
}
|
||||||
issuer, err := caCert.Sha256Sum()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while getting -ca-crt fingerprint: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if caCert.Expired(time.Now()) {
|
if caCert.Expired(time.Now()) {
|
||||||
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.Details.NotAfter) - time.Second*1
|
*sf.duration = time.Until(caCert.NotAfter()) - time.Second*1
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, ipNet, err := net.ParseCIDR(*sf.ip)
|
if *sf.networks != "" {
|
||||||
if err != nil {
|
for _, rs := range strings.Split(*sf.networks, ",") {
|
||||||
return newHelpErrorf("invalid ip definition: %s", err)
|
rs := strings.Trim(rs, " ")
|
||||||
}
|
if rs != "" {
|
||||||
if ip.To4() == nil {
|
n, err := netip.ParsePrefix(rs)
|
||||||
return newHelpErrorf("invalid ip definition: can only be ipv4, have %s", *sf.ip)
|
if err != nil {
|
||||||
}
|
return newHelpErrorf("invalid -networks definition: %s", rs)
|
||||||
ipNet.IP = ip
|
}
|
||||||
|
|
||||||
groups := []string{}
|
if n.Addr().Is4() {
|
||||||
|
v4Networks = append(v4Networks, n)
|
||||||
|
} else {
|
||||||
|
v6Networks = append(v6Networks, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var v4UnsafeNetworks []netip.Prefix
|
||||||
|
var v6UnsafeNetworks []netip.Prefix
|
||||||
|
if *sf.unsafeNetworks == "" && *sf.subnets != "" {
|
||||||
|
// Pull up deprecated -subnets flag if needed
|
||||||
|
*sf.unsafeNetworks = *sf.subnets
|
||||||
|
}
|
||||||
|
|
||||||
|
if *sf.unsafeNetworks != "" {
|
||||||
|
for _, rs := range strings.Split(*sf.unsafeNetworks, ",") {
|
||||||
|
rs := strings.Trim(rs, " ")
|
||||||
|
if rs != "" {
|
||||||
|
n, err := netip.ParsePrefix(rs)
|
||||||
|
if err != nil {
|
||||||
|
return newHelpErrorf("invalid -unsafe-networks definition: %s", rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Addr().Is4() {
|
||||||
|
v4UnsafeNetworks = append(v4UnsafeNetworks, n)
|
||||||
|
} else {
|
||||||
|
v6UnsafeNetworks = append(v6UnsafeNetworks, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []string
|
||||||
if *sf.groups != "" {
|
if *sf.groups != "" {
|
||||||
for _, rg := range strings.Split(*sf.groups, ",") {
|
for _, rg := range strings.Split(*sf.groups, ",") {
|
||||||
g := strings.TrimSpace(rg)
|
g := strings.TrimSpace(rg)
|
||||||
@@ -158,94 +254,153 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subnets := []*net.IPNet{}
|
var pub, rawPriv []byte
|
||||||
if *sf.subnets != "" {
|
var p11Client *pkclient.PKClient
|
||||||
for _, rs := range strings.Split(*sf.subnets, ",") {
|
|
||||||
rs := strings.Trim(rs, " ")
|
if isP11 {
|
||||||
if rs != "" {
|
curve = cert.Curve_P256
|
||||||
_, s, err := net.ParseCIDR(rs)
|
p11Client, err = pkclient.FromUrl(*sf.p11url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return newHelpErrorf("invalid subnet definition: %s", err)
|
return fmt.Errorf("error while creating PKCS#11 client: %w", err)
|
||||||
}
|
|
||||||
if s.IP.To4() == nil {
|
|
||||||
return newHelpErrorf("invalid subnet definition: can only be ipv4, have %s", rs)
|
|
||||||
}
|
|
||||||
subnets = append(subnets, s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
defer func(client *pkclient.PKClient) {
|
||||||
|
_ = client.Close()
|
||||||
|
}(p11Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pub, rawPriv []byte
|
|
||||||
if *sf.inPubPath != "" {
|
if *sf.inPubPath != "" {
|
||||||
rawPub, err := os.ReadFile(*sf.inPubPath)
|
var pubCurve cert.Curve
|
||||||
|
rawPub, err := readInput("in-pub", *sf.inPubPath, &claims)
|
||||||
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)
|
||||||
}
|
}
|
||||||
var pubCurve cert.Curve
|
|
||||||
pub, _, pubCurve, err = cert.UnmarshalPublicKey(rawPub)
|
pub, _, pubCurve, err = cert.UnmarshalPublicKeyFromPEM(rawPub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while parsing in-pub: %s", err)
|
return fmt.Errorf("error while parsing in-pub: %s", err)
|
||||||
}
|
}
|
||||||
if pubCurve != curve {
|
if pubCurve != curve {
|
||||||
return fmt.Errorf("curve of in-pub does not match ca")
|
return fmt.Errorf("curve of in-pub does not match ca")
|
||||||
}
|
}
|
||||||
|
} else if isP11 {
|
||||||
|
pub, err = p11Client.GetPubKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while getting public key with PKCS#11: %w", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
pub, rawPriv = newKeypair(curve)
|
pub, rawPriv = newKeypair(curve)
|
||||||
}
|
}
|
||||||
|
|
||||||
nc := cert.NebulaCertificate{
|
if !isStdio(*sf.outCertPath) {
|
||||||
Details: cert.NebulaCertificateDetails{
|
if _, err := os.Stat(*sf.outCertPath); err == nil {
|
||||||
Name: *sf.name,
|
return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
|
||||||
Ips: []*net.IPNet{ipNet},
|
}
|
||||||
Groups: groups,
|
|
||||||
Subnets: subnets,
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(*sf.duration),
|
|
||||||
PublicKey: pub,
|
|
||||||
IsCA: false,
|
|
||||||
Issuer: issuer,
|
|
||||||
Curve: curve,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := nc.CheckRootConstrains(caCert); err != nil {
|
var crts []cert.Certificate
|
||||||
return fmt.Errorf("refusing to sign, root certificate constraints violated: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *sf.outKeyPath == "" {
|
notBefore := time.Now()
|
||||||
*sf.outKeyPath = *sf.name + ".key"
|
notAfter := notBefore.Add(*sf.duration)
|
||||||
}
|
|
||||||
|
|
||||||
if *sf.outCertPath == "" {
|
switch version {
|
||||||
*sf.outCertPath = *sf.name + ".crt"
|
case cert.Version1:
|
||||||
}
|
// Make sure we have only one ipv4 address
|
||||||
|
if len(v4Networks) != 1 {
|
||||||
if _, err := os.Stat(*sf.outCertPath); err == nil {
|
return newHelpErrorf("invalid -networks definition: v1 certificates can only have a single ipv4 address")
|
||||||
return fmt.Errorf("refusing to overwrite existing cert: %s", *sf.outCertPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nc.Sign(curve, caKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while signing: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *sf.inPubPath == "" {
|
|
||||||
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
|
||||||
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*sf.outKeyPath, cert.MarshalPrivateKey(curve, rawPriv), 0600)
|
if len(v6Networks) > 0 {
|
||||||
|
return newHelpErrorf("invalid -networks definition: v1 certificates can only contain ipv4 addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v6UnsafeNetworks) > 0 {
|
||||||
|
return newHelpErrorf("invalid -unsafe-networks definition: v1 certificates can only contain ipv4 addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &cert.TBSCertificate{
|
||||||
|
Version: cert.Version1,
|
||||||
|
Name: *sf.name,
|
||||||
|
Networks: []netip.Prefix{v4Networks[0]},
|
||||||
|
Groups: groups,
|
||||||
|
UnsafeNetworks: v4UnsafeNetworks,
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: notAfter,
|
||||||
|
PublicKey: pub,
|
||||||
|
IsCA: false,
|
||||||
|
Curve: curve,
|
||||||
|
}
|
||||||
|
|
||||||
|
var nc cert.Certificate
|
||||||
|
if p11Client == nil {
|
||||||
|
nc, err = t.Sign(caCert, curve, caKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while signing: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nc, err = t.SignWith(caCert, curve, p11Client.SignASN1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while signing with PKCS#11: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crts = append(crts, nc)
|
||||||
|
|
||||||
|
case cert.Version2:
|
||||||
|
t := &cert.TBSCertificate{
|
||||||
|
Version: cert.Version2,
|
||||||
|
Name: *sf.name,
|
||||||
|
Networks: append(v4Networks, v6Networks...),
|
||||||
|
Groups: groups,
|
||||||
|
UnsafeNetworks: append(v4UnsafeNetworks, v6UnsafeNetworks...),
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: notAfter,
|
||||||
|
PublicKey: pub,
|
||||||
|
IsCA: false,
|
||||||
|
Curve: curve,
|
||||||
|
}
|
||||||
|
|
||||||
|
var nc cert.Certificate
|
||||||
|
if p11Client == nil {
|
||||||
|
nc, err = t.Sign(caCert, curve, caKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while signing: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nc, err = t.SignWith(caCert, curve, p11Client.SignASN1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while signing with PKCS#11: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crts = append(crts, nc)
|
||||||
|
default:
|
||||||
|
// this should be unreachable
|
||||||
|
return fmt.Errorf("invalid version: %d", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isP11 && *sf.inPubPath == "" {
|
||||||
|
if !isStdio(*sf.outKeyPath) {
|
||||||
|
if _, err := os.Stat(*sf.outKeyPath); err == nil {
|
||||||
|
return fmt.Errorf("refusing to overwrite existing key: %s", *sf.outKeyPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeOutput(*sf.outKeyPath, cert.MarshalPrivateKeyToPEM(curve, rawPriv), 0600, out)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := nc.MarshalToPEM()
|
var b []byte
|
||||||
if err != nil {
|
for _, c := range crts {
|
||||||
return fmt.Errorf("error while marshalling certificate: %s", err)
|
sb, err := c.MarshalPEM()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while marshalling certificate: %s", err)
|
||||||
|
}
|
||||||
|
b = append(b, sb...)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*sf.outCertPath, b, 0600)
|
err = writeOutput(*sf.outCertPath, b, 0600, out)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -256,7 +411,7 @@ func signCert(args []string, out io.Writer, errOut io.Writer, pr PasswordReader)
|
|||||||
return fmt.Errorf("error while generating qr code: %s", err)
|
return fmt.Errorf("error while generating qr code: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(*sf.outQRPath, b, 0600)
|
err = writeOutput(*sf.outQRPath, b, 0600, out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error while writing out-qr: %s", err)
|
return fmt.Errorf("error while writing out-qr: %s", err)
|
||||||
}
|
}
|
||||||
@@ -306,6 +461,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
+238
-114
@@ -13,11 +13,10 @@ import (
|
|||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
//TODO: test file permissions
|
|
||||||
|
|
||||||
func Test_signSummary(t *testing.T) {
|
func Test_signSummary(t *testing.T) {
|
||||||
assert.Equal(t, "sign <flags>: create and sign a certificate", signSummary())
|
assert.Equal(t, "sign <flags>: create and sign a certificate", signSummary())
|
||||||
}
|
}
|
||||||
@@ -28,6 +27,7 @@ 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"+
|
||||||
@@ -39,17 +39,24 @@ func Test_signHelp(t *testing.T) {
|
|||||||
" -in-pub string\n"+
|
" -in-pub string\n"+
|
||||||
" \tOptional (if out-key not set): path to read a previously generated public key\n"+
|
" \tOptional (if out-key not set): path to read a previously generated public key\n"+
|
||||||
" -ip string\n"+
|
" -ip string\n"+
|
||||||
" \tRequired: ipv4 address and network in CIDR notation to assign the cert\n"+
|
" \tDeprecated, see -networks\n"+
|
||||||
" -name string\n"+
|
" -name string\n"+
|
||||||
" \tRequired: name of the cert, usually a hostname\n"+
|
" \tRequired: name of the cert, usually a hostname\n"+
|
||||||
|
" -networks string\n"+
|
||||||
|
" \tRequired: comma separated list of ip address and network in CIDR notation to assign to this cert\n"+
|
||||||
" -out-crt string\n"+
|
" -out-crt string\n"+
|
||||||
" \tOptional: path to write the certificate to\n"+
|
" \tOptional: path to write the certificate to\n"+
|
||||||
" -out-key string\n"+
|
" -out-key string\n"+
|
||||||
" \tOptional (if in-pub not set): path to write the private key to\n"+
|
" \tOptional (if in-pub not set): path to write the private key to\n"+
|
||||||
" -out-qr string\n"+
|
" -out-qr string\n"+
|
||||||
" \tOptional: output a qr code image (png) of the certificate\n"+
|
" \tOptional: output a qr code image (png) of the certificate\n"+
|
||||||
|
optionalPkcs11String(" -pkcs11 string\n \tOptional: PKCS#11 URI to an existing private key\n")+
|
||||||
" -subnets string\n"+
|
" -subnets string\n"+
|
||||||
" \tOptional: comma separated list of ipv4 address and network in CIDR notation. Subnets this cert can serve for\n",
|
" \tDeprecated, see -unsafe-networks\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"+
|
||||||
|
" -version uint\n"+
|
||||||
|
" \tOptional: version of the certificate format to use. The default is to match the version of the signing CA\n",
|
||||||
ob.String(),
|
ob.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -76,20 +83,20 @@ func Test_signCert(t *testing.T) {
|
|||||||
|
|
||||||
// required args
|
// required args
|
||||||
assertHelpError(t, signCert(
|
assertHelpError(t, signCert(
|
||||||
[]string{"-ca-crt", "./nope", "-ca-key", "./nope", "-ip", "1.1.1.1/24", "-out-key", "nope", "-out-crt", "nope"}, ob, eb, nopw,
|
[]string{"-version", "1", "-ca-crt", "./nope", "-ca-key", "./nope", "-ip", "1.1.1.1/24", "-out-key", "nope", "-out-crt", "nope"}, ob, eb, nopw,
|
||||||
), "-name is required")
|
), "-name is required")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
assertHelpError(t, signCert(
|
assertHelpError(t, signCert(
|
||||||
[]string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-out-key", "nope", "-out-crt", "nope"}, ob, eb, nopw,
|
[]string{"-version", "1", "-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-out-key", "nope", "-out-crt", "nope"}, ob, eb, nopw,
|
||||||
), "-ip is required")
|
), "-networks is required")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// cannot set -in-pub and -out-key
|
// cannot set -in-pub and -out-key
|
||||||
assertHelpError(t, signCert(
|
assertHelpError(t, signCert(
|
||||||
[]string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-in-pub", "nope", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope"}, ob, eb, nopw,
|
[]string{"-version", "1", "-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-in-pub", "nope", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope"}, ob, eb, nopw,
|
||||||
), "cannot set both -in-pub and -out-key")
|
), "cannot set both -in-pub and -out-key")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
@@ -97,18 +104,18 @@ func Test_signCert(t *testing.T) {
|
|||||||
// failed to read key
|
// failed to read key
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args := []string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
args := []string{"-version", "1", "-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while reading ca-key: open ./nope: "+NoSuchFileError)
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while reading ca-key: open ./nope: "+NoSuchFileError)
|
||||||
|
|
||||||
// failed to unmarshal key
|
// failed to unmarshal key
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
caKeyF, err := os.CreateTemp("", "sign-cert.key")
|
caKeyF, err := os.CreateTemp("", "sign-cert.key")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(caKeyF.Name())
|
defer os.Remove(caKeyF.Name())
|
||||||
|
|
||||||
args = []string{"-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
args = []string{"-version", "1", "-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing ca-key: input did not contain a valid PEM encoded block")
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing ca-key: input did not contain a valid PEM encoded block")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
@@ -116,11 +123,11 @@ func Test_signCert(t *testing.T) {
|
|||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
caKeyF.Write(cert.MarshalEd25519PrivateKey(caPriv))
|
caKeyF.Write(cert.MarshalSigningPrivateKeyToPEM(cert.Curve_CURVE25519, caPriv))
|
||||||
|
|
||||||
// failed to read cert
|
// failed to read cert
|
||||||
args = []string{"-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
args = []string{"-version", "1", "-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while reading ca-crt: open ./nope: "+NoSuchFileError)
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while reading ca-crt: open ./nope: "+NoSuchFileError)
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
@@ -128,30 +135,22 @@ func Test_signCert(t *testing.T) {
|
|||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
caCrtF, err := os.CreateTemp("", "sign-cert.crt")
|
caCrtF, err := os.CreateTemp("", "sign-cert.crt")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(caCrtF.Name())
|
defer os.Remove(caCrtF.Name())
|
||||||
|
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
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"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing ca-crt: input did not contain a valid PEM encoded block")
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing ca-crt: input did not contain a valid PEM encoded block")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// write a proper ca cert for later
|
// write a proper ca cert for later
|
||||||
ca := cert.NebulaCertificate{
|
ca, _ := NewTestCaCert("ca", caPub, caPriv, time.Now(), time.Now().Add(time.Minute*200), nil, nil, nil)
|
||||||
Details: cert.NebulaCertificateDetails{
|
b, _ := ca.MarshalPEM()
|
||||||
Name: "ca",
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(time.Minute * 200),
|
|
||||||
PublicKey: caPub,
|
|
||||||
IsCA: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b, _ := ca.MarshalToPEM()
|
|
||||||
caCrtF.Write(b)
|
caCrtF.Write(b)
|
||||||
|
|
||||||
// failed to read pub
|
// failed to read pub
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-in-pub", "./nope", "-duration", "100m"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-in-pub", "./nope", "-duration", "100m"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while reading in-pub: open ./nope: "+NoSuchFileError)
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while reading in-pub: open ./nope: "+NoSuchFileError)
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
@@ -159,11 +158,11 @@ func Test_signCert(t *testing.T) {
|
|||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
inPubF, err := os.CreateTemp("", "in.pub")
|
inPubF, err := os.CreateTemp("", "in.pub")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(inPubF.Name())
|
defer os.Remove(inPubF.Name())
|
||||||
|
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-in-pub", inPubF.Name(), "-duration", "100m"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-in-pub", inPubF.Name(), "-duration", "100m"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing in-pub: input did not contain a valid PEM encoded block")
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing in-pub: input did not contain a valid PEM encoded block")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
@@ -171,116 +170,124 @@ func Test_signCert(t *testing.T) {
|
|||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
inPub, _ := x25519Keypair()
|
inPub, _ := x25519Keypair()
|
||||||
inPubF.Write(cert.MarshalX25519PublicKey(inPub))
|
inPubF.Write(cert.MarshalPublicKeyToPEM(cert.Curve_CURVE25519, inPub))
|
||||||
|
|
||||||
// bad ip cidr
|
// bad ip cidr
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "a1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "a1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid ip definition: invalid CIDR address: a1.1.1.1/24")
|
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -networks definition: a1.1.1.1/24")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "100::100/100", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "100::100/100", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid ip definition: can only be ipv4, have 100::100/100")
|
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -networks definition: v1 certificates can only have a single ipv4 address")
|
||||||
|
assert.Empty(t, ob.String())
|
||||||
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24,1.1.1.2/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
|
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -networks definition: v1 certificates can only have a single ipv4 address")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// bad subnet cidr
|
// bad subnet cidr
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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", "a"}
|
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", "a"}
|
||||||
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid subnet definition: invalid CIDR address: a")
|
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -unsafe-networks definition: a")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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 subnet definition: can only be ipv4, have 100::100/100")
|
assertHelpError(t, signCert(args, ob, eb, nopw), "invalid -unsafe-networks definition: v1 certificates can only contain ipv4 addresses")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// mismatched ca key
|
// mismatched ca key
|
||||||
_, caPriv2, _ := ed25519.GenerateKey(rand.Reader)
|
_, caPriv2, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
caKeyF2, err := os.CreateTemp("", "sign-cert-2.key")
|
caKeyF2, err := os.CreateTemp("", "sign-cert-2.key")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(caKeyF2.Name())
|
defer os.Remove(caKeyF2.Name())
|
||||||
caKeyF2.Write(cert.MarshalEd25519PrivateKey(caPriv2))
|
caKeyF2.Write(cert.MarshalSigningPrivateKeyToPEM(cert.Curve_CURVE25519, caPriv2))
|
||||||
|
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF2.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "a"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF2.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "a"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to sign, root certificate does not match private key")
|
require.EqualError(t, signCert(args, ob, eb, nopw), "refusing to sign, root certificate does not match private key")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// failed key write
|
// failed key write
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey", "-duration", "100m", "-subnets", "10.1.1.1/32"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey", "-duration", "100m", "-subnets", "10.1.1.1/32"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// create temp key file
|
// create temp key file
|
||||||
keyF, err := os.CreateTemp("", "test.key")
|
keyF, err := os.CreateTemp("", "test.key")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
os.Remove(keyF.Name())
|
os.Remove(keyF.Name())
|
||||||
|
|
||||||
// failed cert write
|
// failed cert write
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
os.Remove(keyF.Name())
|
os.Remove(keyF.Name())
|
||||||
|
|
||||||
// create temp cert file
|
// create temp cert file
|
||||||
crtF, err := os.CreateTemp("", "test.crt")
|
crtF, err := os.CreateTemp("", "test.crt")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
|
|
||||||
// test proper cert with removed empty groups and subnets
|
// test proper cert with removed empty groups and subnets
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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"}
|
||||||
assert.Nil(t, signCert(args, ob, eb, nopw))
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// read cert and key files
|
// read cert and key files
|
||||||
rb, _ := os.ReadFile(keyF.Name())
|
rb, _ := os.ReadFile(keyF.Name())
|
||||||
lKey, b, err := cert.UnmarshalX25519PrivateKey(rb)
|
lKey, b, curve, err := cert.UnmarshalPrivateKeyFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||||
assert.Nil(t, err)
|
assert.Empty(t, b)
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Len(t, lKey, 32)
|
assert.Len(t, lKey, 32)
|
||||||
|
|
||||||
rb, _ = os.ReadFile(crtF.Name())
|
rb, _ = os.ReadFile(crtF.Name())
|
||||||
lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb)
|
lCrt, b, err := cert.UnmarshalCertificateFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Empty(t, b)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "test", lCrt.Details.Name)
|
assert.Equal(t, "test", lCrt.Name())
|
||||||
assert.Equal(t, "1.1.1.1/24", lCrt.Details.Ips[0].String())
|
assert.Equal(t, "1.1.1.1/24", lCrt.Networks()[0].String())
|
||||||
assert.Len(t, lCrt.Details.Ips, 1)
|
assert.Len(t, lCrt.Networks(), 1)
|
||||||
assert.False(t, lCrt.Details.IsCA)
|
assert.False(t, lCrt.IsCA())
|
||||||
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups)
|
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Groups())
|
||||||
assert.Len(t, lCrt.Details.Subnets, 3)
|
assert.Len(t, lCrt.UnsafeNetworks(), 3)
|
||||||
assert.Len(t, lCrt.Details.PublicKey, 32)
|
assert.Len(t, lCrt.PublicKey(), 32)
|
||||||
assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore))
|
assert.Equal(t, time.Duration(time.Minute*100), lCrt.NotAfter().Sub(lCrt.NotBefore()))
|
||||||
|
|
||||||
sns := []string{}
|
sns := []string{}
|
||||||
for _, sn := range lCrt.Details.Subnets {
|
for _, sn := range lCrt.UnsafeNetworks() {
|
||||||
sns = append(sns, sn.String())
|
sns = append(sns, sn.String())
|
||||||
}
|
}
|
||||||
assert.Equal(t, []string{"10.1.1.1/32", "10.2.2.2/32", "10.5.5.5/32"}, sns)
|
assert.Equal(t, []string{"10.1.1.1/32", "10.2.2.2/32", "10.5.5.5/32"}, sns)
|
||||||
|
|
||||||
issuer, _ := ca.Sha256Sum()
|
issuer, _ := ca.Fingerprint()
|
||||||
assert.Equal(t, issuer, lCrt.Details.Issuer)
|
assert.Equal(t, issuer, lCrt.Issuer())
|
||||||
|
|
||||||
assert.True(t, lCrt.CheckSignature(caPub))
|
assert.True(t, lCrt.CheckSignature(caPub))
|
||||||
|
|
||||||
@@ -289,53 +296,55 @@ func Test_signCert(t *testing.T) {
|
|||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-in-pub", inPubF.Name(), "-duration", "100m", "-groups", "1"}
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-in-pub", inPubF.Name(), "-duration", "100m", "-groups", "1"}
|
||||||
assert.Nil(t, signCert(args, ob, eb, nopw))
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// read cert file and check pub key matches in-pub
|
// read cert file and check pub key matches in-pub
|
||||||
rb, _ = os.ReadFile(crtF.Name())
|
rb, _ = os.ReadFile(crtF.Name())
|
||||||
lCrt, b, err = cert.UnmarshalNebulaCertificateFromPEM(rb)
|
lCrt, b, err = cert.UnmarshalCertificateFromPEM(rb)
|
||||||
assert.Len(t, b, 0)
|
assert.Empty(t, b)
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, lCrt.Details.PublicKey, inPub)
|
assert.Equal(t, lCrt.PublicKey(), inPub)
|
||||||
|
|
||||||
// test refuse to sign cert with duration beyond root
|
// test refuse to sign cert with duration beyond root
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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", "1000m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
os.Remove(keyF.Name())
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to sign, root certificate constraints violated: certificate expires after signing certificate")
|
os.Remove(crtF.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", "1000m", "-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 signing: certificate expires after signing certificate")
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
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())
|
||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
args = []string{"-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"}
|
||||||
assert.Nil(t, signCert(args, ob, eb, nopw))
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
|
|
||||||
// test that we won't overwrite existing key file
|
// test that we won't overwrite existing key file
|
||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to overwrite existing key: "+keyF.Name())
|
require.EqualError(t, signCert(args, ob, eb, nopw), "refusing to overwrite existing key: "+keyF.Name())
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
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())
|
||||||
os.Remove(crtF.Name())
|
os.Remove(crtF.Name())
|
||||||
args = []string{"-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"}
|
||||||
assert.Nil(t, signCert(args, ob, eb, nopw))
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
|
|
||||||
// test that we won't overwrite existing certificate file
|
// test that we won't overwrite existing certificate file
|
||||||
os.Remove(keyF.Name())
|
os.Remove(keyF.Name())
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
args = []string{"-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"}
|
||||||
assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to overwrite existing cert: "+crtF.Name())
|
require.EqualError(t, signCert(args, ob, eb, nopw), "refusing to overwrite existing cert: "+crtF.Name())
|
||||||
assert.Empty(t, ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
@@ -348,11 +357,11 @@ func Test_signCert(t *testing.T) {
|
|||||||
eb.Reset()
|
eb.Reset()
|
||||||
|
|
||||||
caKeyF, err = os.CreateTemp("", "sign-cert.key")
|
caKeyF, err = os.CreateTemp("", "sign-cert.key")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(caKeyF.Name())
|
defer os.Remove(caKeyF.Name())
|
||||||
|
|
||||||
caCrtF, err = os.CreateTemp("", "sign-cert.crt")
|
caCrtF, err = os.CreateTemp("", "sign-cert.crt")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(caCrtF.Name())
|
defer os.Remove(caCrtF.Name())
|
||||||
|
|
||||||
// generate the encrypted key
|
// generate the encrypted key
|
||||||
@@ -361,50 +370,165 @@ func Test_signCert(t *testing.T) {
|
|||||||
b, _ = cert.EncryptAndMarshalSigningPrivateKey(cert.Curve_CURVE25519, caPriv, passphrase, kdfParams)
|
b, _ = cert.EncryptAndMarshalSigningPrivateKey(cert.Curve_CURVE25519, caPriv, passphrase, kdfParams)
|
||||||
caKeyF.Write(b)
|
caKeyF.Write(b)
|
||||||
|
|
||||||
ca = cert.NebulaCertificate{
|
ca, _ = NewTestCaCert("ca", caPub, caPriv, time.Now(), time.Now().Add(time.Minute*200), nil, nil, nil)
|
||||||
Details: cert.NebulaCertificateDetails{
|
b, _ = ca.MarshalPEM()
|
||||||
Name: "ca",
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(time.Minute * 200),
|
|
||||||
PublicKey: caPub,
|
|
||||||
IsCA: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
b, _ = ca.MarshalToPEM()
|
|
||||||
caCrtF.Write(b)
|
caCrtF.Write(b)
|
||||||
|
|
||||||
// test with the proper password
|
// test with the proper password
|
||||||
args = []string{"-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"}
|
||||||
assert.Nil(t, signCert(args, ob, eb, testpw))
|
require.NoError(t, signCert(args, ob, eb, testpw))
|
||||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
|
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||||
|
|
||||||
|
// test with the proper password in the environment
|
||||||
|
os.Remove(crtF.Name())
|
||||||
|
os.Remove(keyF.Name())
|
||||||
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||||
|
os.Setenv("NEBULA_CA_PASSPHRASE", string(passphrase))
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
require.NoError(t, signCert(args, ob, eb, testpw))
|
||||||
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
os.Setenv("NEBULA_CA_PASSPHRASE", "")
|
||||||
|
|
||||||
// test with the wrong password
|
// test with the wrong password
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
|
|
||||||
testpw.password = []byte("invalid password")
|
testpw.password = []byte("invalid password")
|
||||||
args = []string{"-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"}
|
||||||
assert.Error(t, signCert(args, ob, eb, testpw))
|
require.Error(t, signCert(args, ob, eb, testpw))
|
||||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
|
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||||
|
|
||||||
|
// test with the wrong password in environment
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
|
||||||
|
os.Setenv("NEBULA_CA_PASSPHRASE", "invalid password")
|
||||||
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"}
|
||||||
|
require.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing encrypted ca-key: invalid passphrase or corrupt private key")
|
||||||
|
assert.Empty(t, ob.String())
|
||||||
assert.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()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
|
|
||||||
args = []string{"-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"}
|
||||||
assert.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.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", eb.String())
|
||||||
|
|
||||||
// test an error condition
|
// test an error condition
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
|
|
||||||
args = []string{"-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"}
|
||||||
assert.Error(t, signCert(args, ob, eb, errpw))
|
require.Error(t, signCert(args, ob, eb, errpw))
|
||||||
assert.Equal(t, "Enter passphrase: ", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Empty(t, eb.String())
|
assert.Equal(t, "Enter passphrase: ", eb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_signCert_stdio(t *testing.T) {
|
||||||
|
nopw := &StubPasswordReader{
|
||||||
|
password: []byte(""),
|
||||||
|
err: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
|
rawCAKey := cert.MarshalSigningPrivateKeyToPEM(cert.Curve_CURVE25519, caPriv)
|
||||||
|
|
||||||
|
ca, _ := NewTestCaCert("ca", caPub, caPriv, time.Now(), time.Now().Add(time.Minute*200), nil, nil, nil)
|
||||||
|
rawCACrt, _ := ca.MarshalPEM()
|
||||||
|
|
||||||
|
caCrtF, err := os.CreateTemp("", "sign-cert.crt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(caCrtF.Name())
|
||||||
|
caCrtF.Write(rawCACrt)
|
||||||
|
|
||||||
|
caKeyF, err := os.CreateTemp("", "sign-cert.key")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(caKeyF.Name())
|
||||||
|
caKeyF.Write(rawCAKey)
|
||||||
|
|
||||||
|
keyF, err := os.CreateTemp("", "sign.key")
|
||||||
|
require.NoError(t, err)
|
||||||
|
os.Remove(keyF.Name())
|
||||||
|
defer os.Remove(keyF.Name())
|
||||||
|
|
||||||
|
// ca-key on stdin, cert to stdout
|
||||||
|
withStdin(t, bytes.NewReader(rawCAKey))
|
||||||
|
ob := &bytes.Buffer{}
|
||||||
|
eb := &bytes.Buffer{}
|
||||||
|
args := []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", "-", "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", "-", "-out-key", keyF.Name(), "-duration", "100m"}
|
||||||
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
|
lCrt, _, err := cert.UnmarshalCertificateFromPEM(ob.Bytes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "stdin-test", lCrt.Name())
|
||||||
|
assert.True(t, lCrt.CheckSignature(caPub))
|
||||||
|
|
||||||
|
// two flags reading from stdin should error before any read attempt;
|
||||||
|
// otherwise an interactive shell would hang on io.ReadAll
|
||||||
|
stdinIn := bytes.NewReader(rawCAKey)
|
||||||
|
withStdin(t, stdinIn)
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
args = []string{"-version", "1", "-ca-crt", "-", "-ca-key", "-", "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
||||||
|
require.EqualError(t, signCert(args, ob, eb, nopw),
|
||||||
|
`-ca-key and -ca-crt both set to "-", only one input may read from stdin`)
|
||||||
|
assert.Equal(t, len(rawCAKey), stdinIn.Len(), "stdin should be untouched when conflict is caught up front")
|
||||||
|
|
||||||
|
// two flags writing to stdout should error before any output is written
|
||||||
|
// AND before stdin is consumed
|
||||||
|
stdinR := bytes.NewReader(rawCAKey)
|
||||||
|
withStdin(t, stdinR)
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", "-", "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", "-", "-out-key", "-", "-duration", "100m"}
|
||||||
|
require.EqualError(t, signCert(args, ob, eb, nopw),
|
||||||
|
`-out-key and -out-crt both set to "-", only one output may write to stdout`)
|
||||||
|
assert.Empty(t, ob.String())
|
||||||
|
// stdin should be untouched because the conflict was caught up front
|
||||||
|
assert.Equal(t, len(rawCAKey), stdinR.Len())
|
||||||
|
|
||||||
|
// out-key on stdout, cert on disk
|
||||||
|
keyF2, err := os.CreateTemp("", "sign.key")
|
||||||
|
require.NoError(t, err)
|
||||||
|
os.Remove(keyF2.Name())
|
||||||
|
defer os.Remove(keyF2.Name())
|
||||||
|
crtF, err := os.CreateTemp("", "sign.crt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
os.Remove(crtF.Name())
|
||||||
|
defer os.Remove(crtF.Name())
|
||||||
|
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "stdin-test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", "-", "-duration", "100m"}
|
||||||
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
|
assert.Empty(t, eb.String())
|
||||||
|
_, _, curve, err := cert.UnmarshalPrivateKeyFromPEM(ob.Bytes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, cert.Curve_CURVE25519, curve)
|
||||||
|
|
||||||
|
// in-pub on stdin (caller already has a keypair, only the cert is generated)
|
||||||
|
inPub, _ := x25519Keypair()
|
||||||
|
rawInPub := cert.MarshalPublicKeyToPEM(cert.Curve_CURVE25519, inPub)
|
||||||
|
|
||||||
|
withStdin(t, bytes.NewReader(rawInPub))
|
||||||
|
os.Remove(crtF.Name())
|
||||||
|
ob.Reset()
|
||||||
|
eb.Reset()
|
||||||
|
args = []string{"-version", "1", "-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "in-pub-test", "-ip", "1.1.1.1/24", "-in-pub", "-", "-out-crt", "-", "-duration", "100m"}
|
||||||
|
require.NoError(t, signCert(args, ob, eb, nopw))
|
||||||
|
assert.Empty(t, eb.String())
|
||||||
|
stdinCrt, _, err := cert.UnmarshalCertificateFromPEM(ob.Bytes())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "in-pub-test", stdinCrt.Name())
|
||||||
|
assert.Equal(t, inPub, stdinCrt.PublicKey())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stdioPath is the special path value that selects stdin (for inputs) or
|
||||||
|
// stdout (for outputs) instead of a file on disk.
|
||||||
|
const stdioPath = "-"
|
||||||
|
|
||||||
|
// stdioHelpText is rendered just under the Usage line of each subcommand
|
||||||
|
// help so the - convention is documented once instead of on every flag.
|
||||||
|
const stdioHelpText = " Pass \"-\" to any path flag to read from stdin or write to stdout.\n"
|
||||||
|
|
||||||
|
// stdinReader is the source used when an input flag is set to "-".
|
||||||
|
// It is a package level var so tests can swap in a deterministic reader.
|
||||||
|
// Tests that mutate stdinReader cannot run with t.Parallel().
|
||||||
|
var stdinReader io.Reader = os.Stdin
|
||||||
|
|
||||||
|
// ioClaims tracks which flags have claimed stdin and stdout during a single
|
||||||
|
// command invocation so we can refuse a second flag asking for the same
|
||||||
|
// stream.
|
||||||
|
type ioClaims struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ioClaims) claimIn(flagName string) error {
|
||||||
|
if c.in != "" && c.in != flagName {
|
||||||
|
return fmt.Errorf("-%s and -%s both set to %q, only one input may read from stdin", c.in, flagName, stdioPath)
|
||||||
|
}
|
||||||
|
c.in = flagName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ioClaims) claimOut(flagName string) error {
|
||||||
|
if c.out != "" && c.out != flagName {
|
||||||
|
return fmt.Errorf("-%s and -%s both set to %q, only one output may write to stdout", c.out, flagName, stdioPath)
|
||||||
|
}
|
||||||
|
c.out = flagName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserveInputs walks alternating (flagName, path) pairs and claims stdin
|
||||||
|
// for any path equal to stdioPath. It must be called before any input is
|
||||||
|
// read so a conflict can be reported immediately instead of blocking on
|
||||||
|
// io.ReadAll while waiting for input that will never arrive.
|
||||||
|
func reserveInputs(claims *ioClaims, pairs ...string) error {
|
||||||
|
return reserveStdio(claims, "reserveInputs", (*ioClaims).claimIn, pairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserveOutputs walks alternating (flagName, path) pairs and claims stdout
|
||||||
|
// for any path equal to stdioPath. It must be called before any output is
|
||||||
|
// written so a conflict cannot leave one stream half written before the
|
||||||
|
// second flag fails.
|
||||||
|
func reserveOutputs(claims *ioClaims, pairs ...string) error {
|
||||||
|
return reserveStdio(claims, "reserveOutputs", (*ioClaims).claimOut, pairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reserveStdio(claims *ioClaims, who string, claim func(*ioClaims, string) error, pairs []string) error {
|
||||||
|
if len(pairs)%2 != 0 {
|
||||||
|
panic(who + " requires alternating name, path pairs")
|
||||||
|
}
|
||||||
|
for i := 0; i < len(pairs); i += 2 {
|
||||||
|
name, path := pairs[i], pairs[i+1]
|
||||||
|
if path != stdioPath {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := claim(claims, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readInput returns the bytes referenced by path, reading from stdin when
|
||||||
|
// path is stdioPath.
|
||||||
|
func readInput(flagName, path string, claims *ioClaims) ([]byte, error) {
|
||||||
|
if path == stdioPath {
|
||||||
|
if err := claims.claimIn(flagName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return io.ReadAll(stdinReader)
|
||||||
|
}
|
||||||
|
return os.ReadFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// openInput returns a reader for path. When path is stdioPath the returned
|
||||||
|
// reader wraps stdin and Close is a no-op.
|
||||||
|
func openInput(flagName, path string, claims *ioClaims) (io.ReadCloser, error) {
|
||||||
|
if path == stdioPath {
|
||||||
|
if err := claims.claimIn(flagName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return io.NopCloser(stdinReader), nil
|
||||||
|
}
|
||||||
|
return os.Open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeOutput writes data to path, or to stdout when path is stdioPath. perm
|
||||||
|
// is only used for file output. The caller must have already claimed stdout
|
||||||
|
// via reserveOutputs before invoking with stdioPath.
|
||||||
|
func writeOutput(path string, data []byte, perm os.FileMode, stdout io.Writer) error {
|
||||||
|
if path == stdioPath {
|
||||||
|
_, err := stdout.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isStdio reports whether path is the stdio sentinel and so should skip
|
||||||
|
// existence checks like "refuse to overwrite".
|
||||||
|
func isStdio(path string) bool {
|
||||||
|
return path == stdioPath
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// withStdin temporarily replaces stdinReader for the duration of t.
|
||||||
|
func withStdin(t *testing.T, r io.Reader) {
|
||||||
|
t.Helper()
|
||||||
|
prev := stdinReader
|
||||||
|
stdinReader = r
|
||||||
|
t.Cleanup(func() { stdinReader = prev })
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_readInput_stdin(t *testing.T) {
|
||||||
|
withStdin(t, bytes.NewBufferString("hello"))
|
||||||
|
var claims ioClaims
|
||||||
|
|
||||||
|
got, err := readInput("path", "-", &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("hello"), got)
|
||||||
|
assert.Equal(t, "path", claims.in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_readInput_file(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "f")
|
||||||
|
require.NoError(t, os.WriteFile(p, []byte("file"), 0600))
|
||||||
|
var claims ioClaims
|
||||||
|
|
||||||
|
got, err := readInput("path", p, &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("file"), got)
|
||||||
|
assert.Empty(t, claims.in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_readInput_doubleStdinErrors(t *testing.T) {
|
||||||
|
withStdin(t, bytes.NewBufferString("hello"))
|
||||||
|
var claims ioClaims
|
||||||
|
|
||||||
|
_, err := readInput("ca-key", "-", &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = readInput("ca-crt", "-", &claims)
|
||||||
|
require.EqualError(t, err, `-ca-key and -ca-crt both set to "-", only one input may read from stdin`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_openInput_stdin(t *testing.T) {
|
||||||
|
withStdin(t, bytes.NewBufferString("hi"))
|
||||||
|
var claims ioClaims
|
||||||
|
|
||||||
|
r, err := openInput("ca", "-", &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer r.Close()
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("hi"), b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_openInput_doubleStdinErrors(t *testing.T) {
|
||||||
|
withStdin(t, bytes.NewBufferString("hi"))
|
||||||
|
var claims ioClaims
|
||||||
|
|
||||||
|
r, err := openInput("ca", "-", &claims)
|
||||||
|
require.NoError(t, err)
|
||||||
|
r.Close()
|
||||||
|
|
||||||
|
_, err = openInput("crt", "-", &claims)
|
||||||
|
require.EqualError(t, err, `-ca and -crt both set to "-", only one input may read from stdin`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_writeOutput_stdout(t *testing.T) {
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
|
||||||
|
err := writeOutput("-", []byte("payload"), 0600, out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "payload", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_writeOutput_file(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
p := filepath.Join(dir, "f")
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
|
||||||
|
err := writeOutput(p, []byte("payload"), 0600, out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, out.String())
|
||||||
|
got, err := os.ReadFile(p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []byte("payload"), got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_reserveOutputs_noConflict(t *testing.T) {
|
||||||
|
var claims ioClaims
|
||||||
|
require.NoError(t, reserveOutputs(&claims,
|
||||||
|
"out-key", "/tmp/key",
|
||||||
|
"out-crt", "-",
|
||||||
|
"out-qr", "",
|
||||||
|
))
|
||||||
|
assert.Equal(t, "out-crt", claims.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_reserveOutputs_conflict(t *testing.T) {
|
||||||
|
var claims ioClaims
|
||||||
|
err := reserveOutputs(&claims,
|
||||||
|
"out-key", "-",
|
||||||
|
"out-crt", "-",
|
||||||
|
)
|
||||||
|
require.EqualError(t, err, `-out-key and -out-crt both set to "-", only one output may write to stdout`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_reserveOutputs_panicsOnOddPairs(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
require.NotNil(t, r)
|
||||||
|
}()
|
||||||
|
var claims ioClaims
|
||||||
|
_ = reserveOutputs(&claims, "out-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_reserveInputs_noConflict(t *testing.T) {
|
||||||
|
var claims ioClaims
|
||||||
|
require.NoError(t, reserveInputs(&claims,
|
||||||
|
"ca-key", "/tmp/ca.key",
|
||||||
|
"ca-crt", "-",
|
||||||
|
"in-pub", "",
|
||||||
|
))
|
||||||
|
assert.Equal(t, "ca-crt", claims.in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_reserveInputs_conflict(t *testing.T) {
|
||||||
|
var claims ioClaims
|
||||||
|
err := reserveInputs(&claims,
|
||||||
|
"ca-key", "-",
|
||||||
|
"ca-crt", "-",
|
||||||
|
)
|
||||||
|
require.EqualError(t, err, `-ca-key and -ca-crt both set to "-", only one input may read from stdin`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_claimIn_idempotent(t *testing.T) {
|
||||||
|
// pre-claim then a lazy re-claim of the same flag should be a no-op
|
||||||
|
var claims ioClaims
|
||||||
|
require.NoError(t, claims.claimIn("ca-key"))
|
||||||
|
require.NoError(t, claims.claimIn("ca-key"))
|
||||||
|
assert.Equal(t, "ca-key", claims.in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_claimOut_idempotent(t *testing.T) {
|
||||||
|
var claims ioClaims
|
||||||
|
require.NoError(t, claims.claimOut("out-crt"))
|
||||||
|
require.NoError(t, claims.claimOut("out-crt"))
|
||||||
|
assert.Equal(t, "out-crt", claims.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isStdio(t *testing.T) {
|
||||||
|
assert.True(t, isStdio("-"))
|
||||||
|
assert.False(t, isStdio(""))
|
||||||
|
assert.False(t, isStdio("./-"))
|
||||||
|
assert.False(t, isStdio("foo"))
|
||||||
|
}
|
||||||
+45
-32
@@ -1,11 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
@@ -39,39 +39,51 @@ func verify(args []string, out io.Writer, errOut io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCACert, err := os.ReadFile(*vf.caPath)
|
var claims ioClaims
|
||||||
if err != nil {
|
if err := reserveInputs(&claims,
|
||||||
return fmt.Errorf("error while reading ca: %s", err)
|
"ca", *vf.caPath,
|
||||||
}
|
"crt", *vf.certPath,
|
||||||
|
); err != nil {
|
||||||
caPool := cert.NewCAPool()
|
|
||||||
for {
|
|
||||||
rawCACert, err = caPool.AddCACertificate(rawCACert)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while adding ca cert to pool: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rawCACert == nil || len(rawCACert) == 0 || strings.TrimSpace(string(rawCACert)) == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rawCert, err := os.ReadFile(*vf.certPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to read crt; %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, _, err := cert.UnmarshalNebulaCertificateFromPEM(rawCert)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while parsing crt: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
good, err := c.Verify(time.Now(), caPool)
|
|
||||||
if !good {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
caReader, err := openInput("ca", *vf.caPath, &claims)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while reading ca: %w", err)
|
||||||
|
}
|
||||||
|
defer caReader.Close()
|
||||||
|
|
||||||
|
caPool, err := cert.NewCAPoolFromPEMReader(caReader)
|
||||||
|
if err != nil && !errors.Is(err, cert.ErrExpired) {
|
||||||
|
return fmt.Errorf("error while adding ca cert to pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawCert, err := readInput("crt", *vf.certPath, &claims)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read crt: %w", err)
|
||||||
|
}
|
||||||
|
var errs []error
|
||||||
|
for {
|
||||||
|
if len(rawCert) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c, extra, err := cert.UnmarshalCertificateFromPEM(rawCert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while parsing crt: %w", err)
|
||||||
|
}
|
||||||
|
rawCert = extra
|
||||||
|
_, err = caPool.VerifyCertificate(time.Now(), c)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, cert.ErrCaNotFound):
|
||||||
|
errs = append(errs, fmt.Errorf("error while verifying certificate v%d %s with issuer %s: %w", c.Version(), c.Name(), c.Issuer(), err))
|
||||||
|
default:
|
||||||
|
errs = append(errs, fmt.Errorf("error while verifying certificate %+v: %w", c, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifySummary() string {
|
func verifySummary() string {
|
||||||
@@ -80,7 +92,8 @@ 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
"github.com/slackhq/nebula/cert"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ 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"+
|
||||||
@@ -37,105 +39,130 @@ func Test_verify(t *testing.T) {
|
|||||||
|
|
||||||
// required args
|
// required args
|
||||||
assertHelpError(t, verify([]string{"-ca", "derp"}, ob, eb), "-crt is required")
|
assertHelpError(t, verify([]string{"-ca", "derp"}, ob, eb), "-crt is required")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
assertHelpError(t, verify([]string{"-crt", "derp"}, ob, eb), "-ca is required")
|
assertHelpError(t, verify([]string{"-crt", "derp"}, ob, eb), "-ca is required")
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
|
|
||||||
// no ca at path
|
// no ca at path
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
err := verify([]string{"-ca", "does_not_exist", "-crt", "does_not_exist"}, ob, eb)
|
err := verify([]string{"-ca", "does_not_exist", "-crt", "does_not_exist"}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "error while reading ca: open does_not_exist: "+NoSuchFileError)
|
require.EqualError(t, err, "error while reading ca: open does_not_exist: "+NoSuchFileError)
|
||||||
|
|
||||||
// invalid ca at path
|
// invalid ca at path
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
caFile, err := os.CreateTemp("", "verify-ca")
|
caFile, err := os.CreateTemp("", "verify-ca")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(caFile.Name())
|
defer os.Remove(caFile.Name())
|
||||||
|
|
||||||
caFile.WriteString("-----BEGIN NOPE-----")
|
caFile.WriteString("-----BEGIN NOPE-----")
|
||||||
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.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "error while adding ca cert to pool: input did not contain a valid PEM encoded block")
|
require.ErrorIs(t, err, cert.ErrInvalidPEMBlock)
|
||||||
|
|
||||||
// make a ca for later
|
// make a ca for later
|
||||||
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
ca := cert.NebulaCertificate{
|
ca, _ := NewTestCaCert("test-ca", caPub, caPriv, time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour*2), nil, nil, nil)
|
||||||
Details: cert.NebulaCertificateDetails{
|
b, _ := ca.MarshalPEM()
|
||||||
Name: "test-ca",
|
|
||||||
NotBefore: time.Now().Add(time.Hour * -1),
|
|
||||||
NotAfter: time.Now().Add(time.Hour * 2),
|
|
||||||
PublicKey: caPub,
|
|
||||||
IsCA: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ca.Sign(cert.Curve_CURVE25519, caPriv)
|
|
||||||
b, _ := ca.MarshalToPEM()
|
|
||||||
caFile.Truncate(0)
|
caFile.Truncate(0)
|
||||||
caFile.Seek(0, 0)
|
caFile.Seek(0, 0)
|
||||||
caFile.Write(b)
|
caFile.Write(b)
|
||||||
|
|
||||||
// no crt at path
|
// no crt at path
|
||||||
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.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "unable to read crt; open does_not_exist: "+NoSuchFileError)
|
require.EqualError(t, err, "unable to read crt: open does_not_exist: "+NoSuchFileError)
|
||||||
|
|
||||||
// invalid crt at path
|
// invalid crt at path
|
||||||
ob.Reset()
|
ob.Reset()
|
||||||
eb.Reset()
|
eb.Reset()
|
||||||
certFile, err := os.CreateTemp("", "verify-cert")
|
certFile, err := os.CreateTemp("", "verify-cert")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(certFile.Name())
|
defer os.Remove(certFile.Name())
|
||||||
|
|
||||||
certFile.WriteString("-----BEGIN NOPE-----")
|
certFile.WriteString("-----BEGIN NOPE-----")
|
||||||
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
|
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "error while parsing crt: input did not contain a valid PEM encoded block")
|
require.EqualError(t, err, "error while parsing crt: input did not contain a valid PEM encoded block")
|
||||||
|
|
||||||
// unverifiable cert at path
|
// unverifiable cert at path
|
||||||
_, badPriv, _ := ed25519.GenerateKey(rand.Reader)
|
crt, _ := NewTestCert(ca, caPriv, "test-cert", time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour), nil, nil, nil)
|
||||||
certPub, _ := x25519Keypair()
|
// Slightly evil hack to modify the certificate after it was sealed to generate an invalid signature
|
||||||
signer, _ := ca.Sha256Sum()
|
pub := crt.PublicKey()
|
||||||
crt := cert.NebulaCertificate{
|
for i, _ := range pub {
|
||||||
Details: cert.NebulaCertificateDetails{
|
pub[i] = 0
|
||||||
Name: "test-cert",
|
|
||||||
NotBefore: time.Now().Add(time.Hour * -1),
|
|
||||||
NotAfter: time.Now().Add(time.Hour),
|
|
||||||
PublicKey: certPub,
|
|
||||||
IsCA: false,
|
|
||||||
Issuer: signer,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
b, _ = crt.MarshalPEM()
|
||||||
crt.Sign(cert.Curve_CURVE25519, badPriv)
|
|
||||||
b, _ = crt.MarshalToPEM()
|
|
||||||
certFile.Truncate(0)
|
certFile.Truncate(0)
|
||||||
certFile.Seek(0, 0)
|
certFile.Seek(0, 0)
|
||||||
certFile.Write(b)
|
certFile.Write(b)
|
||||||
|
|
||||||
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
|
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.EqualError(t, err, "certificate signature did not match")
|
require.ErrorIs(t, err, cert.ErrSignatureMismatch)
|
||||||
|
|
||||||
// verified cert at path
|
// verified cert at path
|
||||||
crt.Sign(cert.Curve_CURVE25519, caPriv)
|
crt, _ = NewTestCert(ca, caPriv, "test-cert", time.Now().Add(time.Hour*-1), time.Now().Add(time.Hour), nil, nil, nil)
|
||||||
b, _ = crt.MarshalToPEM()
|
b, _ = crt.MarshalPEM()
|
||||||
certFile.Truncate(0)
|
certFile.Truncate(0)
|
||||||
certFile.Seek(0, 0)
|
certFile.Seek(0, 0)
|
||||||
certFile.Write(b)
|
certFile.Write(b)
|
||||||
|
|
||||||
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
|
err = verify([]string{"-ca", caFile.Name(), "-crt", certFile.Name()}, ob, eb)
|
||||||
assert.Equal(t, "", ob.String())
|
assert.Empty(t, ob.String())
|
||||||
assert.Equal(t, "", eb.String())
|
assert.Empty(t, eb.String())
|
||||||
assert.Nil(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,8 +3,15 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/sirupsen/logrus"
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
func HookLogger(l *logrus.Logger) {
|
"github.com/slackhq/nebula/logging"
|
||||||
// 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,54 +1,86 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"io/ioutil"
|
"log/slog"
|
||||||
"os"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/slackhq/nebula/logging"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookLogger routes the logrus logs through the service logger so that they end up in the Windows Event Viewer
|
// newPlatformLogger returns a *slog.Logger that routes every log record
|
||||||
// logrus output will be discarded
|
// through the Windows service logger so records end up in the Windows
|
||||||
func HookLogger(l *logrus.Logger) {
|
// Event Log. All the heavy lifting (level management, format swap,
|
||||||
l.AddHook(newLogHook(logger))
|
// timestamp toggle, WithAttrs/WithGroup) comes from logging.NewHandler;
|
||||||
l.SetOutput(ioutil.Discard)
|
// this file only contributes:
|
||||||
|
//
|
||||||
|
// - an io.Writer that forwards each formatted line to the service
|
||||||
|
// logger at the current record's Event Log severity, and
|
||||||
|
// - a thin severityTag that embeds *logging.Handler and overrides
|
||||||
|
// only Handle / WithAttrs / WithGroup, so Event Viewer's severity
|
||||||
|
// column and severity-based filters keep working the way they did
|
||||||
|
// before the slog migration.
|
||||||
|
//
|
||||||
|
// Format (text vs json) is carried by the embedded *logging.Handler, so
|
||||||
|
// logging.format: json in config still produces JSON lines in Event
|
||||||
|
// Viewer, same as the pre-slog logrus setup.
|
||||||
|
func newPlatformLogger() *slog.Logger {
|
||||||
|
w := &eventLogWriter{}
|
||||||
|
return slog.New(&severityTag{Handler: logging.NewHandler(w), w: w})
|
||||||
}
|
}
|
||||||
|
|
||||||
type logHook struct {
|
// eventLogWriter forwards slog-formatted lines to the Windows service
|
||||||
sl service.Logger
|
// logger at the severity most recently stashed by severityTag.Handle.
|
||||||
|
// The mutex serializes the stash + inner.Handle + Write cycle per record
|
||||||
|
// across all concurrent goroutines; slog's builtin text/json handlers
|
||||||
|
// each hold their own mutex around Write, but that only protects the
|
||||||
|
// Write call itself, not our stash-then-handle sequence.
|
||||||
|
type eventLogWriter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
level slog.Level
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLogHook(sl service.Logger) *logHook {
|
func (w *eventLogWriter) Write(p []byte) (int, error) {
|
||||||
return &logHook{sl: sl}
|
line := strings.TrimRight(string(p), "\n")
|
||||||
}
|
switch {
|
||||||
|
case w.level >= slog.LevelError:
|
||||||
func (h *logHook) Fire(entry *logrus.Entry) error {
|
return len(p), logger.Error(line)
|
||||||
line, err := entry.String()
|
case w.level >= slog.LevelWarn:
|
||||||
if err != nil {
|
return len(p), logger.Warning(line)
|
||||||
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 nil
|
return len(p), logger.Info(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *logHook) Levels() []logrus.Level {
|
// severityTag embeds *logging.Handler to pick up everything it does for
|
||||||
return logrus.AllLevels
|
// free (Enabled, SetLevel, GetLevel, SetFormat, GetFormat,
|
||||||
|
// SetDisableTimestamp) and overrides only Handle / WithAttrs / WithGroup
|
||||||
|
// so each record's slog.Level is stashed on the writer before formatting
|
||||||
|
// and so derived handlers stay wrapped as severityTag rather than
|
||||||
|
// downgrading to bare *logging.Handler.
|
||||||
|
type severityTag struct {
|
||||||
|
*logging.Handler
|
||||||
|
w *eventLogWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *severityTag) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
s.w.mu.Lock()
|
||||||
|
defer s.w.mu.Unlock()
|
||||||
|
s.w.level = r.Level
|
||||||
|
return s.Handler.Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *severityTag) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return &severityTag{Handler: s.Handler.WithAttrs(attrs).(*logging.Handler), w: s.w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *severityTag) WithGroup(name string) slog.Handler {
|
||||||
|
if name == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return &severityTag{Handler: s.Handler.WithGroup(name).(*logging.Handler), w: s.w}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-11
@@ -4,10 +4,12 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +20,17 @@ 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")
|
||||||
@@ -37,20 +50,30 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := logging.NewLogger(os.Stdout)
|
||||||
|
|
||||||
if *serviceFlag != "" {
|
if *serviceFlag != "" {
|
||||||
doService(configPath, configTest, Build, serviceFlag)
|
if *configTest {
|
||||||
os.Exit(1)
|
fmt.Println("-test is not supported with -service, run the config test without -service")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := doService(configPath, Build, serviceFlag); err != nil {
|
||||||
|
l.Error("Service command failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if *configPath == "" {
|
if *configPath == "" {
|
||||||
fmt.Println("-config flag must be set")
|
p, err := config.DefaultPath()
|
||||||
flag.Usage()
|
if err != nil {
|
||||||
os.Exit(1)
|
fmt.Println(err)
|
||||||
|
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 {
|
||||||
@@ -58,6 +81,16 @@ 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)
|
||||||
@@ -65,8 +98,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !*configTest {
|
if !*configTest {
|
||||||
ctrl.Start()
|
if err := ctrl.Start(); err != nil {
|
||||||
ctrl.ShutdownBlock()
|
util.LogWithContextIfNeeded("Error while running", err, l)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go ctrl.ShutdownBlock()
|
||||||
|
|
||||||
|
if err := ctrl.Wait(); err != nil {
|
||||||
|
l.Error("Nebula stopped due to fatal error", "error", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info("Goodbye")
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ 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
|
||||||
}
|
}
|
||||||
@@ -25,8 +23,7 @@ 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 := logrus.New()
|
l := newPlatformLogger()
|
||||||
HookLogger(l)
|
|
||||||
|
|
||||||
c := config.NewC(l)
|
c := config.NewC(l)
|
||||||
err := c.Load(*p.configPath)
|
err := c.Load(*p.configPath)
|
||||||
@@ -34,39 +31,56 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.control, err = nebula.Main(c, *p.configTest, Build, l, nil)
|
if err := logging.ApplyConfig(l, c); err != nil {
|
||||||
|
return fmt.Errorf("failed to apply logging config: %s", err)
|
||||||
|
}
|
||||||
|
c.RegisterReloadCallback(func(c *config.C) {
|
||||||
|
if err := logging.ApplyConfig(l, c); err != nil {
|
||||||
|
l.Error("Failed to reconfigure logger on reload", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
p.control, err = nebula.Main(c, false, Build, l, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.control.Start()
|
if err := p.control.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nebula can stop itself on a fatal packet reader error, make sure to log it if it happens.
|
||||||
|
go func() {
|
||||||
|
if err := p.control.Wait(); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("Nebula stopped due to fatal error: %v", err))
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
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 fileExists(filename string) bool {
|
func doService(configPath *string, build string, serviceFlag *string) error {
|
||||||
_, 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 == "" {
|
||||||
ex, err := os.Executable()
|
p, err := config.DefaultPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
|
||||||
*configPath = filepath.Dir(ex) + "/config.yaml"
|
|
||||||
if !fileExists(*configPath) {
|
|
||||||
*configPath = filepath.Dir(ex) + "/config.yml"
|
|
||||||
}
|
}
|
||||||
|
*configPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
svcConfig := &service.Config{
|
svcConfig := &service.Config{
|
||||||
@@ -78,23 +92,22 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
|
|||||||
|
|
||||||
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)
|
||||||
// - above, in `Run` we create a `logrus.Logger` which is what nebula expects to use
|
// - 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
|
||||||
s, err := service.New(prg, svcConfig)
|
s, err := service.New(prg, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return 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 {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -109,18 +122,17 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
|
|||||||
|
|
||||||
switch *serviceFlag {
|
switch *serviceFlag {
|
||||||
case "run":
|
case "run":
|
||||||
err = s.Run()
|
if err := s.Run(); err != nil {
|
||||||
if err != nil {
|
// Route any errors to the system logger and report the failure
|
||||||
// Route any errors to the system logger
|
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err := service.Control(s, *serviceFlag)
|
if err := service.Control(s, *serviceFlag); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Printf("Valid actions: %q\n", service.ControlAction)
|
log.Printf("Valid actions: %q\n", service.ControlAction)
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
//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])
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-8
@@ -4,10 +4,12 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +20,17 @@ 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")
|
||||||
@@ -37,13 +50,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *configPath == "" {
|
if *configPath == "" {
|
||||||
fmt.Println("-config flag must be set")
|
p, err := config.DefaultPath()
|
||||||
flag.Usage()
|
if err != nil {
|
||||||
os.Exit(1)
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
*configPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
l := logrus.New()
|
l := logging.NewLogger(os.Stdout)
|
||||||
l.Out = os.Stdout
|
|
||||||
|
|
||||||
c := config.NewC(l)
|
c := config.NewC(l)
|
||||||
err := c.Load(*configPath)
|
err := c.Load(*configPath)
|
||||||
@@ -52,6 +67,16 @@ 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)
|
||||||
@@ -59,9 +84,20 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !*configTest {
|
if !*configTest {
|
||||||
ctrl.Start()
|
if err := ctrl.Start(); err != nil {
|
||||||
|
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,11 +1,10 @@
|
|||||||
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
|
||||||
@@ -13,30 +12,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 *logrus.Logger) {
|
func notifyReady(l *slog.Logger) {
|
||||||
sockName := os.Getenv("NOTIFY_SOCKET")
|
sockName := os.Getenv("NOTIFY_SOCKET")
|
||||||
if sockName == "" {
|
if sockName == "" {
|
||||||
l.Debugln("NOTIFY_SOCKET systemd env var not set, not sending ready signal")
|
l.Debug("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.WithError(err).Error("failed to connect to systemd notification socket")
|
l.Error("failed to connect to systemd notification socket", "error", err)
|
||||||
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.WithError(err).Error("failed to set the write deadline for the systemd notification socket")
|
l.Error("failed to set the write deadline for the systemd notification socket", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = conn.Write([]byte(SdNotifyReady)); err != nil {
|
if _, err = conn.Write([]byte(SdNotifyReady)); err != nil {
|
||||||
l.WithError(err).Error("failed to signal the systemd notification socket")
|
l.Error("failed to signal the systemd notification socket", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debugln("notified systemd the service is ready")
|
l.Debug("notified systemd the service is ready")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/sirupsen/logrus"
|
import "log/slog"
|
||||||
|
|
||||||
func notifyReady(_ *logrus.Logger) {
|
func notifyReady(_ *slog.Logger) {
|
||||||
// No init service to notify
|
// No init service to notify
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-24
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -16,23 +17,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/sirupsen/logrus"
|
"go.yaml.in/yaml/v3"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type C struct {
|
type C struct {
|
||||||
path string
|
path string
|
||||||
files []string
|
files []string
|
||||||
Settings map[interface{}]interface{}
|
Settings map[string]any
|
||||||
oldSettings map[interface{}]interface{}
|
oldSettings map[string]any
|
||||||
callbacks []func(*C)
|
callbacks []func(*C)
|
||||||
l *logrus.Logger
|
l *slog.Logger
|
||||||
reloadLock sync.Mutex
|
reloadLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewC(l *logrus.Logger) *C {
|
func NewC(l *slog.Logger) *C {
|
||||||
return &C{
|
return &C{
|
||||||
Settings: make(map[interface{}]interface{}),
|
Settings: make(map[string]any),
|
||||||
l: l,
|
l: l,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +92,8 @@ func (c *C) HasChanged(k string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
nv interface{}
|
nv any
|
||||||
ov interface{}
|
ov any
|
||||||
)
|
)
|
||||||
|
|
||||||
if k == "" {
|
if k == "" {
|
||||||
@@ -107,12 +107,18 @@ 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.WithField("config_path", k).WithError(err).Error("Error while marshaling new config")
|
c.l.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.WithField("config_path", k).WithError(err).Error("Error while marshaling old config")
|
c.l.Error("Error while marshaling old config",
|
||||||
|
"config_path", k,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(newVals) != string(oldVals)
|
return string(newVals) != string(oldVals)
|
||||||
@@ -147,14 +153,17 @@ func (c *C) ReloadConfig() {
|
|||||||
c.reloadLock.Lock()
|
c.reloadLock.Lock()
|
||||||
defer c.reloadLock.Unlock()
|
defer c.reloadLock.Unlock()
|
||||||
|
|
||||||
c.oldSettings = make(map[interface{}]interface{})
|
c.oldSettings = make(map[string]any)
|
||||||
for k, v := range c.Settings {
|
for k, v := range c.Settings {
|
||||||
c.oldSettings[k] = v
|
c.oldSettings[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.Load(c.path)
|
err := c.Load(c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.l.WithField("config_path", c.path).WithError(err).Error("Error occurred while reloading config")
|
c.l.Error("Error occurred while reloading config",
|
||||||
|
"config_path", c.path,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +176,7 @@ func (c *C) ReloadConfigString(raw string) error {
|
|||||||
c.reloadLock.Lock()
|
c.reloadLock.Lock()
|
||||||
defer c.reloadLock.Unlock()
|
defer c.reloadLock.Unlock()
|
||||||
|
|
||||||
c.oldSettings = make(map[interface{}]interface{})
|
c.oldSettings = make(map[string]any)
|
||||||
for k, v := range c.Settings {
|
for k, v := range c.Settings {
|
||||||
c.oldSettings[k] = v
|
c.oldSettings[k] = v
|
||||||
}
|
}
|
||||||
@@ -201,7 +210,7 @@ func (c *C) GetStringSlice(k string, d []string) []string {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
rv, ok := r.([]interface{})
|
rv, ok := r.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
@@ -215,13 +224,13 @@ func (c *C) GetStringSlice(k string, d []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMap will get the map for k or return the default d if not found or invalid
|
// GetMap will get the map for k or return the default d if not found or invalid
|
||||||
func (c *C) GetMap(k string, d map[interface{}]interface{}) map[interface{}]interface{} {
|
func (c *C) GetMap(k string, d map[string]any) map[string]any {
|
||||||
r := c.Get(k)
|
r := c.Get(k)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
v, ok := r.(map[interface{}]interface{})
|
v, ok := r.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
@@ -243,7 +252,7 @@ func (c *C) GetInt(k string, d int) int {
|
|||||||
// GetUint32 will get the uint32 for k or return the default d if not found or invalid
|
// GetUint32 will get the uint32 for k or return the default d if not found or invalid
|
||||||
func (c *C) GetUint32(k string, d uint32) uint32 {
|
func (c *C) GetUint32(k string, d uint32) uint32 {
|
||||||
r := c.GetInt(k, int(d))
|
r := c.GetInt(k, int(d))
|
||||||
if uint64(r) > uint64(math.MaxUint32) {
|
if r < 0 || uint64(r) > uint64(math.MaxUint32) {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
return uint32(r)
|
return uint32(r)
|
||||||
@@ -266,6 +275,22 @@ func (c *C) GetBool(k string, d bool) bool {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AsBool(v any) (value bool, ok bool) {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return x, true
|
||||||
|
case string:
|
||||||
|
switch x {
|
||||||
|
case "y", "yes":
|
||||||
|
return true, true
|
||||||
|
case "n", "no":
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
// GetDuration will get the duration for k or return the default d if not found or invalid
|
// GetDuration will get the duration for k or return the default d if not found or invalid
|
||||||
func (c *C) GetDuration(k string, d time.Duration) time.Duration {
|
func (c *C) GetDuration(k string, d time.Duration) time.Duration {
|
||||||
r := c.GetString(k, "")
|
r := c.GetString(k, "")
|
||||||
@@ -276,7 +301,7 @@ func (c *C) GetDuration(k string, d time.Duration) time.Duration {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *C) Get(k string) interface{} {
|
func (c *C) Get(k string) any {
|
||||||
return c.get(k, c.Settings)
|
return c.get(k, c.Settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,10 +309,10 @@ func (c *C) IsSet(k string) bool {
|
|||||||
return c.get(k, c.Settings) != nil
|
return c.get(k, c.Settings) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *C) get(k string, v interface{}) interface{} {
|
func (c *C) get(k string, v any) any {
|
||||||
parts := strings.Split(k, ".")
|
parts := strings.Split(k, ".")
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
m, ok := v.(map[interface{}]interface{})
|
m, ok := v.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -346,7 +371,7 @@ func (c *C) addFile(path string, direct bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *C) parseRaw(b []byte) error {
|
func (c *C) parseRaw(b []byte) error {
|
||||||
var m map[interface{}]interface{}
|
var m map[string]any
|
||||||
|
|
||||||
err := yaml.Unmarshal(b, &m)
|
err := yaml.Unmarshal(b, &m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -358,7 +383,7 @@ func (c *C) parseRaw(b []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *C) parse() error {
|
func (c *C) parse() error {
|
||||||
var m map[interface{}]interface{}
|
var m map[string]any
|
||||||
|
|
||||||
for _, path := range c.files {
|
for _, path := range c.files {
|
||||||
b, err := os.ReadFile(path)
|
b, err := os.ReadFile(path)
|
||||||
@@ -366,7 +391,7 @@ func (c *C) parse() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var nm map[interface{}]interface{}
|
var nm map[string]any
|
||||||
err = yaml.Unmarshal(b, &nm)
|
err = yaml.Unmarshal(b, &nm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+31
-34
@@ -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"
|
||||||
"gopkg.in/yaml.v2"
|
"go.yaml.in/yaml/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Load(t *testing.T) {
|
func TestConfig_Load(t *testing.T) {
|
||||||
@@ -19,40 +19,37 @@ func TestConfig_Load(t *testing.T) {
|
|||||||
// invalid yaml
|
// invalid yaml
|
||||||
c := NewC(l)
|
c := NewC(l)
|
||||||
os.WriteFile(filepath.Join(dir, "01.yaml"), []byte(" invalid yaml"), 0644)
|
os.WriteFile(filepath.Join(dir, "01.yaml"), []byte(" invalid yaml"), 0644)
|
||||||
assert.EqualError(t, c.Load(dir), "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `invalid...` into map[interface {}]interface {}")
|
require.EqualError(t, c.Load(dir), "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `invalid...` into map[string]interface {}")
|
||||||
|
|
||||||
// simple multi config merge
|
// simple multi config merge
|
||||||
c = NewC(l)
|
c = NewC(l)
|
||||||
os.RemoveAll(dir)
|
os.RemoveAll(dir)
|
||||||
os.Mkdir(dir, 0755)
|
os.Mkdir(dir, 0755)
|
||||||
|
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644)
|
os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644)
|
||||||
os.WriteFile(filepath.Join(dir, "02.yml"), []byte("outer:\n inner: override\nnew: hi"), 0644)
|
os.WriteFile(filepath.Join(dir, "02.yml"), []byte("outer:\n inner: override\nnew: hi"), 0644)
|
||||||
assert.Nil(t, c.Load(dir))
|
require.NoError(t, c.Load(dir))
|
||||||
expected := map[interface{}]interface{}{
|
expected := map[string]any{
|
||||||
"outer": map[interface{}]interface{}{
|
"outer": map[string]any{
|
||||||
"inner": "override",
|
"inner": "override",
|
||||||
},
|
},
|
||||||
"new": "hi",
|
"new": "hi",
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, c.Settings)
|
assert.Equal(t, expected, c.Settings)
|
||||||
|
|
||||||
//TODO: test symlinked file
|
|
||||||
//TODO: test symlinked directory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_Get(t *testing.T) {
|
func TestConfig_Get(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
// test simple type
|
// test simple type
|
||||||
c := NewC(l)
|
c := NewC(l)
|
||||||
c.Settings["firewall"] = map[interface{}]interface{}{"outbound": "hi"}
|
c.Settings["firewall"] = map[string]any{"outbound": "hi"}
|
||||||
assert.Equal(t, "hi", c.Get("firewall.outbound"))
|
assert.Equal(t, "hi", c.Get("firewall.outbound"))
|
||||||
|
|
||||||
// test complex type
|
// test complex type
|
||||||
inner := []map[interface{}]interface{}{{"port": "1", "code": "2"}}
|
inner := []map[string]any{{"port": "1", "code": "2"}}
|
||||||
c.Settings["firewall"] = map[interface{}]interface{}{"outbound": inner}
|
c.Settings["firewall"] = map[string]any{"outbound": inner}
|
||||||
assert.EqualValues(t, inner, c.Get("firewall.outbound"))
|
assert.EqualValues(t, inner, c.Get("firewall.outbound"))
|
||||||
|
|
||||||
// test missing
|
// test missing
|
||||||
@@ -62,7 +59,7 @@ func TestConfig_Get(t *testing.T) {
|
|||||||
func TestConfig_GetStringSlice(t *testing.T) {
|
func TestConfig_GetStringSlice(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
c := NewC(l)
|
c := NewC(l)
|
||||||
c.Settings["slice"] = []interface{}{"one", "two"}
|
c.Settings["slice"] = []any{"one", "two"}
|
||||||
assert.Equal(t, []string{"one", "two"}, c.GetStringSlice("slice", []string{}))
|
assert.Equal(t, []string{"one", "two"}, c.GetStringSlice("slice", []string{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,28 +67,28 @@ func TestConfig_GetBool(t *testing.T) {
|
|||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
c := NewC(l)
|
c := NewC(l)
|
||||||
c.Settings["bool"] = true
|
c.Settings["bool"] = true
|
||||||
assert.Equal(t, true, c.GetBool("bool", false))
|
assert.True(t, c.GetBool("bool", false))
|
||||||
|
|
||||||
c.Settings["bool"] = "true"
|
c.Settings["bool"] = "true"
|
||||||
assert.Equal(t, true, c.GetBool("bool", false))
|
assert.True(t, c.GetBool("bool", false))
|
||||||
|
|
||||||
c.Settings["bool"] = false
|
c.Settings["bool"] = false
|
||||||
assert.Equal(t, false, c.GetBool("bool", true))
|
assert.False(t, c.GetBool("bool", true))
|
||||||
|
|
||||||
c.Settings["bool"] = "false"
|
c.Settings["bool"] = "false"
|
||||||
assert.Equal(t, false, c.GetBool("bool", true))
|
assert.False(t, c.GetBool("bool", true))
|
||||||
|
|
||||||
c.Settings["bool"] = "Y"
|
c.Settings["bool"] = "Y"
|
||||||
assert.Equal(t, true, c.GetBool("bool", false))
|
assert.True(t, c.GetBool("bool", false))
|
||||||
|
|
||||||
c.Settings["bool"] = "yEs"
|
c.Settings["bool"] = "yEs"
|
||||||
assert.Equal(t, true, c.GetBool("bool", false))
|
assert.True(t, c.GetBool("bool", false))
|
||||||
|
|
||||||
c.Settings["bool"] = "N"
|
c.Settings["bool"] = "N"
|
||||||
assert.Equal(t, false, c.GetBool("bool", true))
|
assert.False(t, c.GetBool("bool", true))
|
||||||
|
|
||||||
c.Settings["bool"] = "nO"
|
c.Settings["bool"] = "nO"
|
||||||
assert.Equal(t, false, c.GetBool("bool", true))
|
assert.False(t, c.GetBool("bool", true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_HasChanged(t *testing.T) {
|
func TestConfig_HasChanged(t *testing.T) {
|
||||||
@@ -104,14 +101,14 @@ func TestConfig_HasChanged(t *testing.T) {
|
|||||||
// Test key change
|
// Test key change
|
||||||
c = NewC(l)
|
c = NewC(l)
|
||||||
c.Settings["test"] = "hi"
|
c.Settings["test"] = "hi"
|
||||||
c.oldSettings = map[interface{}]interface{}{"test": "no"}
|
c.oldSettings = map[string]any{"test": "no"}
|
||||||
assert.True(t, c.HasChanged("test"))
|
assert.True(t, c.HasChanged("test"))
|
||||||
assert.True(t, c.HasChanged(""))
|
assert.True(t, c.HasChanged(""))
|
||||||
|
|
||||||
// No key change
|
// No key change
|
||||||
c = NewC(l)
|
c = NewC(l)
|
||||||
c.Settings["test"] = "hi"
|
c.Settings["test"] = "hi"
|
||||||
c.oldSettings = map[interface{}]interface{}{"test": "hi"}
|
c.oldSettings = map[string]any{"test": "hi"}
|
||||||
assert.False(t, c.HasChanged("test"))
|
assert.False(t, c.HasChanged("test"))
|
||||||
assert.False(t, c.HasChanged(""))
|
assert.False(t, c.HasChanged(""))
|
||||||
}
|
}
|
||||||
@@ -120,11 +117,11 @@ func TestConfig_ReloadConfig(t *testing.T) {
|
|||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
done := make(chan bool, 1)
|
done := make(chan bool, 1)
|
||||||
dir, err := os.MkdirTemp("", "config-test")
|
dir, err := os.MkdirTemp("", "config-test")
|
||||||
assert.Nil(t, err)
|
require.NoError(t, err)
|
||||||
os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644)
|
os.WriteFile(filepath.Join(dir, "01.yaml"), []byte("outer:\n inner: hi"), 0644)
|
||||||
|
|
||||||
c := NewC(l)
|
c := NewC(l)
|
||||||
assert.Nil(t, c.Load(dir))
|
require.NoError(t, c.Load(dir))
|
||||||
|
|
||||||
assert.False(t, c.HasChanged("outer.inner"))
|
assert.False(t, c.HasChanged("outer.inner"))
|
||||||
assert.False(t, c.HasChanged("outer"))
|
assert.False(t, c.HasChanged("outer"))
|
||||||
@@ -187,11 +184,11 @@ firewall:
|
|||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
var m map[any]any
|
var m map[string]any
|
||||||
|
|
||||||
// merge the same way config.parse() merges
|
// merge the same way config.parse() merges
|
||||||
for _, b := range configs {
|
for _, b := range configs {
|
||||||
var nm map[any]any
|
var nm map[string]any
|
||||||
err := yaml.Unmarshal(b, &nm)
|
err := yaml.Unmarshal(b, &nm)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -208,15 +205,15 @@ firewall:
|
|||||||
t.Logf("Merged Config as YAML:\n%s", mYaml)
|
t.Logf("Merged Config as YAML:\n%s", mYaml)
|
||||||
|
|
||||||
// If a bug is present, some items might be replaced instead of merged like we expect
|
// If a bug is present, some items might be replaced instead of merged like we expect
|
||||||
expected := map[any]any{
|
expected := map[string]any{
|
||||||
"firewall": map[any]any{
|
"firewall": map[string]any{
|
||||||
"inbound": []any{
|
"inbound": []any{
|
||||||
map[any]any{"host": "any", "port": "any", "proto": "icmp"},
|
map[string]any{"host": "any", "port": "any", "proto": "icmp"},
|
||||||
map[any]any{"groups": []any{"server"}, "port": 443, "proto": "tcp"},
|
map[string]any{"groups": []any{"server"}, "port": 443, "proto": "tcp"},
|
||||||
map[any]any{"groups": []any{"webapp"}, "port": 443, "proto": "tcp"}},
|
map[string]any{"groups": []any{"webapp"}, "port": 443, "proto": "tcp"}},
|
||||||
"outbound": []any{
|
"outbound": []any{
|
||||||
map[any]any{"host": "any", "port": "any", "proto": "any"}}},
|
map[string]any{"host": "any", "port": "any", "proto": "any"}}},
|
||||||
"listen": map[any]any{
|
"listen": map[string]any{
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 4242,
|
"port": 4242,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultPath returns a path to a config file alongside the running executable, preferring config.yaml over config.yml.
|
||||||
|
// If neither file exists an error is returned that names both paths checked.
|
||||||
|
func DefaultPath() (string, error) {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return defaultPathInDir(filepath.Dir(ex))
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultPathInDir(dir string) (string, error) {
|
||||||
|
yamlPath := filepath.Join(dir, "config.yaml")
|
||||||
|
if _, err := os.Stat(yamlPath); err == nil {
|
||||||
|
return yamlPath, nil
|
||||||
|
}
|
||||||
|
ymlPath := filepath.Join(dir, "config.yml")
|
||||||
|
if _, err := os.Stat(ymlPath); err == nil {
|
||||||
|
return ymlPath, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no default config found at %s or %s", yamlPath, ymlPath)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultPathInDir(t *testing.T) {
|
||||||
|
t.Run("prefers config.yaml when both exist", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
want := filepath.Join(dir, "config.yaml")
|
||||||
|
other := filepath.Join(dir, "config.yml")
|
||||||
|
require.NoError(t, os.WriteFile(want, []byte("a: 1"), 0644))
|
||||||
|
require.NoError(t, os.WriteFile(other, []byte("a: 2"), 0644))
|
||||||
|
|
||||||
|
got, err := defaultPathInDir(dir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns config.yaml when only it exists", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
want := filepath.Join(dir, "config.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(want, []byte("a: 1"), 0644))
|
||||||
|
|
||||||
|
got, err := defaultPathInDir(dir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls back to config.yml when only it exists", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
want := filepath.Join(dir, "config.yml")
|
||||||
|
require.NoError(t, os.WriteFile(want, []byte("a: 1"), 0644))
|
||||||
|
|
||||||
|
got, err := defaultPathInDir(dir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("errors when neither exists and names both paths", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
got, err := defaultPathInDir(dir)
|
||||||
|
assert.Empty(t, got)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), filepath.Join(dir, "config.yaml"))
|
||||||
|
assert.Contains(t, err.Error(), filepath.Join(dir, "config.yml"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultPath(t *testing.T) {
|
||||||
|
got, err := DefaultPath()
|
||||||
|
if err != nil {
|
||||||
|
ex, exErr := os.Executable()
|
||||||
|
require.NoError(t, exErr)
|
||||||
|
assert.Contains(t, err.Error(), filepath.Dir(ex))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ex, err := os.Executable()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, filepath.Dir(ex), filepath.Dir(got))
|
||||||
|
assert.Contains(t, []string{"config.yaml", "config.yml"}, filepath.Base(got))
|
||||||
|
}
|
||||||
+169
-138
@@ -5,13 +5,12 @@ 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"
|
||||||
@@ -45,19 +44,16 @@ type connectionManager struct {
|
|||||||
inactivityTimeout atomic.Int64
|
inactivityTimeout atomic.Int64
|
||||||
dropInactive atomic.Bool
|
dropInactive atomic.Bool
|
||||||
|
|
||||||
metricsTxPunchy metrics.Counter
|
l *slog.Logger
|
||||||
|
|
||||||
l *logrus.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConnectionManagerFromConfig(l *logrus.Logger, c *config.C, hm *HostMap, p *Punchy) *connectionManager {
|
func newConnectionManagerFromConfig(l *slog.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)
|
||||||
@@ -85,9 +81,10 @@ 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.WithField("oldDuration", old).
|
cm.l.Info("Inactivity timeout has changed",
|
||||||
WithField("newDuration", cm.getInactivityTimeout()).
|
"oldDuration", old,
|
||||||
Info("Inactivity timeout has changed")
|
"newDuration", cm.getInactivityTimeout(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +92,10 @@ 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.WithField("oldBool", old).
|
cm.l.Info("Drop inactive setting has changed",
|
||||||
WithField("newBool", cm.dropInactive.Load()).
|
"oldBool", old,
|
||||||
Info("Drop inactive setting has changed")
|
"newBool", cm.dropInactive.Load(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,14 +136,6 @@ func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time)
|
|||||||
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()
|
||||||
@@ -180,7 +170,7 @@ func (cm *connectionManager) doTrafficCheck(localIndex uint32, p, nb, out []byte
|
|||||||
case deleteTunnel:
|
case deleteTunnel:
|
||||||
if cm.hostMap.DeleteHostInfo(hostinfo) {
|
if cm.hostMap.DeleteHostInfo(hostinfo) {
|
||||||
// Only clearing the lighthouse cache if this is the last hostinfo for this vpn ip in the hostmap
|
// Only clearing the lighthouse cache if this is the last hostinfo for this vpn ip in the hostmap
|
||||||
cm.intf.lightHouse.DeleteVpnIp(hostinfo.vpnIp)
|
cm.intf.lightHouse.DeleteVpnAddrs(hostinfo.vpnAddrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
case closeTunnel:
|
case closeTunnel:
|
||||||
@@ -218,7 +208,7 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
|||||||
relayFor := oldhostinfo.relayState.CopyAllRelayFor()
|
relayFor := oldhostinfo.relayState.CopyAllRelayFor()
|
||||||
|
|
||||||
for _, r := range relayFor {
|
for _, r := range relayFor {
|
||||||
existing, ok := newhostinfo.relayState.QueryRelayForByIp(r.PeerIp)
|
existing, ok := newhostinfo.relayState.QueryRelayForByIp(r.PeerAddr)
|
||||||
|
|
||||||
var index uint32
|
var index uint32
|
||||||
var relayFrom netip.Addr
|
var relayFrom netip.Addr
|
||||||
@@ -230,15 +220,15 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
|||||||
// This relay already exists in newhostinfo, then do nothing.
|
// This relay already exists in newhostinfo, then do nothing.
|
||||||
continue
|
continue
|
||||||
case Requested:
|
case Requested:
|
||||||
// The relayed connection exists in a Requested state; re-send the request
|
// The relay exists in a Requested state; re-send the request
|
||||||
index = existing.LocalIndex
|
index = existing.LocalIndex
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case TerminalType:
|
case TerminalType:
|
||||||
relayFrom = cm.intf.myVpnNet.Addr()
|
relayFrom = cm.intf.myVpnAddrs[0]
|
||||||
relayTo = existing.PeerIp
|
relayTo = existing.PeerAddr
|
||||||
case ForwardingType:
|
case ForwardingType:
|
||||||
relayFrom = existing.PeerIp
|
relayFrom = existing.PeerAddr
|
||||||
relayTo = newhostinfo.vpnIp
|
relayTo = newhostinfo.vpnAddrs[0]
|
||||||
default:
|
default:
|
||||||
// should never happen
|
// should never happen
|
||||||
panic(fmt.Sprintf("Migrating unknown relay type: %v", r.Type))
|
panic(fmt.Sprintf("Migrating unknown relay type: %v", r.Type))
|
||||||
@@ -254,47 +244,66 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
|
|||||||
cm.relayUsedLock.RUnlock()
|
cm.relayUsedLock.RUnlock()
|
||||||
// The relay doesn't exist at all; create some relay state and send the request.
|
// The relay doesn't exist at all; create some relay state and send the request.
|
||||||
var err error
|
var err error
|
||||||
index, err = AddRelay(cm.l, newhostinfo, cm.hostMap, r.PeerIp, 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.WithError(err).Error("failed to migrate relay to new hostinfo")
|
cm.l.Error("failed to migrate relay to new hostinfo", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch r.Type {
|
switch r.Type {
|
||||||
case TerminalType:
|
case TerminalType:
|
||||||
relayFrom = cm.intf.myVpnNet.Addr()
|
relayFrom = cm.intf.myVpnAddrs[0]
|
||||||
relayTo = r.PeerIp
|
relayTo = r.PeerAddr
|
||||||
case ForwardingType:
|
case ForwardingType:
|
||||||
relayFrom = r.PeerIp
|
relayFrom = r.PeerAddr
|
||||||
relayTo = newhostinfo.vpnIp
|
relayTo = newhostinfo.vpnAddrs[0]
|
||||||
default:
|
default:
|
||||||
// should never happen
|
// should never happen
|
||||||
panic(fmt.Sprintf("Migrating unknown relay type: %v", r.Type))
|
panic(fmt.Sprintf("Migrating unknown relay type: %v", r.Type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: IPV6-WORK
|
|
||||||
relayFromB := relayFrom.As4()
|
|
||||||
relayToB := relayTo.As4()
|
|
||||||
|
|
||||||
// Send a CreateRelayRequest to the peer.
|
// Send a CreateRelayRequest to the peer.
|
||||||
req := NebulaControl{
|
req := NebulaControl{
|
||||||
Type: NebulaControl_CreateRelayRequest,
|
Type: NebulaControl_CreateRelayRequest,
|
||||||
InitiatorRelayIndex: index,
|
InitiatorRelayIndex: index,
|
||||||
RelayFromIp: binary.BigEndian.Uint32(relayFromB[:]),
|
|
||||||
RelayToIp: binary.BigEndian.Uint32(relayToB[:]),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch newhostinfo.GetCert().Certificate.Version() {
|
||||||
|
case cert.Version1:
|
||||||
|
if !relayFrom.Is4() {
|
||||||
|
cm.l.Error("can not migrate v1 relay with a v6 network because the relay is not running a current nebula version")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !relayTo.Is4() {
|
||||||
|
cm.l.Error("can not migrate v1 relay with a v6 remote network because the relay is not running a current nebula version")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
b := relayFrom.As4()
|
||||||
|
req.OldRelayFromAddr = binary.BigEndian.Uint32(b[:])
|
||||||
|
b = relayTo.As4()
|
||||||
|
req.OldRelayToAddr = binary.BigEndian.Uint32(b[:])
|
||||||
|
case cert.Version2:
|
||||||
|
req.RelayFromAddr = netAddrToProtoAddr(relayFrom)
|
||||||
|
req.RelayToAddr = netAddrToProtoAddr(relayTo)
|
||||||
|
default:
|
||||||
|
newhostinfo.logger(cm.l).Error("Unknown certificate version found while attempting to migrate relay")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
msg, err := req.Marshal()
|
msg, err := req.Marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cm.l.WithError(err).Error("failed to marshal Control message to migrate relay")
|
cm.l.Error("failed to marshal Control message to migrate relay", "error", err)
|
||||||
} 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.WithFields(logrus.Fields{
|
cm.l.Info("send CreateRelayRequest",
|
||||||
"relayFrom": req.RelayFromIp,
|
"relayFrom", relayFrom,
|
||||||
"relayTo": req.RelayToIp,
|
"relayTo", relayTo,
|
||||||
"initiatorRelayIndex": req.InitiatorRelayIndex,
|
"initiatorRelayIndex", req.InitiatorRelayIndex,
|
||||||
"responderRelayIndex": req.ResponderRelayIndex,
|
"responderRelayIndex", req.ResponderRelayIndex,
|
||||||
"vpnIp": newhostinfo.vpnIp}).
|
"vpnAddrs", newhostinfo.vpnAddrs,
|
||||||
Info("send CreateRelayRequest")
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +315,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.WithField("localIndex", localIndex).Debugln("Not found in hostmap")
|
cm.l.Debug("Not found in hostmap", "localIndex", localIndex)
|
||||||
return doNothing, nil, nil
|
return doNothing, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +323,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
return closeTunnel, hostinfo, nil
|
return closeTunnel, hostinfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
primary := cm.hostMap.Hosts[hostinfo.vpnIp]
|
primary := cm.hostMap.Hosts[hostinfo.vpnAddrs[0]]
|
||||||
mainHostInfo := true
|
mainHostInfo := true
|
||||||
if primary != nil && primary != hostinfo {
|
if primary != nil && primary != hostinfo {
|
||||||
mainHostInfo = false
|
mainHostInfo = false
|
||||||
@@ -326,18 +335,17 @@ 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.Level >= logrus.DebugLevel {
|
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
hostinfo.logger(cm.l).
|
hostinfo.logger(cm.l).Debug("Tunnel status",
|
||||||
WithField("tunnelCheck", m{"state": "alive", "method": "passive"}).
|
"tunnelCheck", m{"state": "alive", "method": "passive"},
|
||||||
Debug("Tunnel status")
|
)
|
||||||
}
|
}
|
||||||
hostinfo.pendingDeletion.Store(false)
|
hostinfo.pendingDeletion.Store(false)
|
||||||
|
|
||||||
if mainHostInfo {
|
if mainHostInfo {
|
||||||
decision = tryRehandshake
|
decision = tryRehandshake
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if cm.shouldSwapPrimary(hostinfo, primary) {
|
if cm.shouldSwapPrimary(hostinfo) {
|
||||||
decision = swapPrimary
|
decision = swapPrimary
|
||||||
} else {
|
} else {
|
||||||
// migrate the relays to the primary, if in use.
|
// migrate the relays to the primary, if in use.
|
||||||
@@ -349,7 +357,7 @@ 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.sendPunch(hostinfo)
|
cm.punchy.SendPunch(hostinfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return decision, hostinfo, primary
|
return decision, hostinfo, primary
|
||||||
@@ -357,9 +365,9 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
|
|
||||||
if hostinfo.pendingDeletion.Load() {
|
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).
|
hostinfo.logger(cm.l).Info("Tunnel status",
|
||||||
WithField("tunnelCheck", m{"state": "dead", "method": "active"}).
|
"tunnelCheck", m{"state": "dead", "method": "active"},
|
||||||
Info("Tunnel status")
|
)
|
||||||
|
|
||||||
return deleteTunnel, hostinfo, nil
|
return deleteTunnel, hostinfo, nil
|
||||||
}
|
}
|
||||||
@@ -370,40 +378,39 @@ 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).
|
hostinfo.logger(cm.l).Info("Dropping tunnel due to inactivity",
|
||||||
WithField("inactiveDuration", inactiveFor).
|
"inactiveDuration", inactiveFor,
|
||||||
WithField("primary", mainHostInfo).
|
"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.sendPunch(hostinfo)
|
cm.punchy.SendPunch(hostinfo)
|
||||||
cm.trafficTimer.Add(hostinfo.localIndexId, cm.checkInterval)
|
cm.trafficTimer.Add(hostinfo.localIndexId, cm.checkInterval)
|
||||||
return doNothing, nil, nil
|
return doNothing, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cm.punchy.GetTargetEverything() {
|
// We aren't receiving traffic but we are sending it. The outbound
|
||||||
// This is similar to the old punchy behavior with a slight optimization.
|
// traffic itself refreshes the primary remote's NAT state; this
|
||||||
// We aren't receiving traffic but we are sending it, punch on all known
|
// fans out to non-primary remotes, but only if target_all_remotes
|
||||||
// ips in case we need to re-prime NAT state
|
// is configured.
|
||||||
cm.sendPunch(hostinfo)
|
cm.punchy.SendPunchToAll(hostinfo)
|
||||||
}
|
|
||||||
|
|
||||||
if cm.l.Level >= logrus.DebugLevel {
|
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
hostinfo.logger(cm.l).
|
hostinfo.logger(cm.l).Debug("Tunnel status",
|
||||||
WithField("tunnelCheck", m{"state": "testing", "method": "active"}).
|
"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.Level >= logrus.DebugLevel {
|
if cm.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
hostinfo.logger(cm.l).Debugf("Hostinfo sadness")
|
hostinfo.logger(cm.l).Debug("Hostinfo sadness")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,92 +435,116 @@ func (cm *connectionManager) isInactive(hostinfo *HostInfo, now time.Time) (time
|
|||||||
return inactiveDuration, true
|
return inactiveDuration, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) shouldSwapPrimary(current, primary *HostInfo) bool {
|
func (cm *connectionManager) shouldSwapPrimary(current *HostInfo) bool {
|
||||||
// The primary tunnel is the most recent handshake to complete locally and should work entirely fine.
|
// The primary tunnel is the most recent handshake to complete locally and should work entirely fine.
|
||||||
// If we are here then we have multiple tunnels for a host pair and neither side believes the same tunnel is primary.
|
// If we are here then we have multiple tunnels for a host pair and neither side believes the same tunnel is primary.
|
||||||
// Let's sort this out.
|
// Let's sort this out.
|
||||||
|
|
||||||
if current.vpnIp.Compare(cm.intf.myVpnNet.Addr()) < 0 {
|
// Only one side should swap because if both swap then we may never resolve to a single tunnel.
|
||||||
// Only one side should flip primary because if both flip then we may never resolve to a single tunnel.
|
// vpn addr is static across all tunnels for this host pair so lets
|
||||||
// vpn ip is static across all tunnels for this host pair so lets use that to determine who is flipping.
|
// use that to determine if we should consider swapping.
|
||||||
// The remotes vpn ip is lower than mine. I will not flip.
|
if current.vpnAddrs[0].Compare(cm.intf.myVpnAddrs[0]) < 0 {
|
||||||
|
// Their primary vpn addr is less than mine. Do not swap.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
certState := cm.intf.pki.GetCertState()
|
crt := cm.intf.pki.getCertState().getCertificate(current.ConnectionState.myCert.Version())
|
||||||
return bytes.Equal(current.ConnectionState.myCert.Signature, certState.Certificate.Signature)
|
if crt == nil {
|
||||||
|
//my cert was reloaded away. We should definitely swap from this tunnel
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// If this tunnel is using the latest certificate then we should swap it to primary for a bit and see if things
|
||||||
|
// settle down.
|
||||||
|
return bytes.Equal(current.ConnectionState.myCert.Signature(), crt.Signature())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) swapPrimary(current, primary *HostInfo) {
|
func (cm *connectionManager) swapPrimary(current, primary *HostInfo) {
|
||||||
cm.hostMap.Lock()
|
cm.hostMap.Lock()
|
||||||
// Make sure the primary is still the same after the write lock. This avoids a race with a rehandshake.
|
// Make sure the primary is still the same after the write lock. This avoids a race with a rehandshake.
|
||||||
if cm.hostMap.Hosts[current.vpnIp] == primary {
|
if cm.hostMap.Hosts[current.vpnAddrs[0]] == primary {
|
||||||
cm.hostMap.unlockedMakePrimary(current)
|
cm.hostMap.unlockedMakePrimary(current)
|
||||||
}
|
}
|
||||||
cm.hostMap.Unlock()
|
cm.hostMap.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// isInvalidCertificate will check if we should destroy a tunnel if pki.disconnect_invalid is true and
|
// isInvalidCertificate decides if we should destroy a tunnel.
|
||||||
// the certificate is no longer valid. Block listed certificates will skip the pki.disconnect_invalid
|
// returns true if pki.disconnect_invalid is true and the certificate is no longer valid.
|
||||||
// check and return true.
|
// Blocklisted certificates will skip the pki.disconnect_invalid check and return true.
|
||||||
func (cm *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostInfo) bool {
|
func (cm *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostInfo) bool {
|
||||||
remoteCert := hostinfo.GetCert()
|
remoteCert := hostinfo.GetCert()
|
||||||
if remoteCert == nil {
|
if remoteCert == nil {
|
||||||
return false
|
return false //don't tear down tunnels for handshakes in progress
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := remoteCert.VerifyWithCache(now, cm.intf.pki.GetCAPool())
|
caPool := cm.intf.pki.GetCAPool()
|
||||||
if valid {
|
err := caPool.VerifyCachedCertificate(now, remoteCert)
|
||||||
return false
|
if err == nil {
|
||||||
}
|
return false //cert is still valid! yay!
|
||||||
|
} else if err == cert.ErrBlockListed { //avoiding errors.Is for speed
|
||||||
if !cm.intf.disconnectInvalid.Load() && err != cert.ErrBlockListed {
|
|
||||||
// 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",
|
||||||
|
"error", err,
|
||||||
|
"fingerprint", remoteCert.Fingerprint,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} else if cm.intf.disconnectInvalid.Load() {
|
||||||
|
hostinfo.logger(cm.l).Info("Remote certificate is no longer valid, tearing down the tunnel",
|
||||||
|
"error", err,
|
||||||
|
"fingerprint", remoteCert.Fingerprint,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
//if we reach here, the cert is no longer valid, but we're configured to keep tunnels from now-invalid certs open
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprint, _ := remoteCert.Sha256Sum()
|
|
||||||
hostinfo.logger(cm.l).WithError(err).
|
|
||||||
WithField("fingerprint", fingerprint).
|
|
||||||
Info("Remote certificate is no longer valid, tearing down the tunnel")
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *connectionManager) sendPunch(hostinfo *HostInfo) {
|
|
||||||
if !cm.punchy.GetPunch() {
|
|
||||||
// Punching is disabled
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cm.intf.lightHouse.IsLighthouseIP(hostinfo.vpnIp) {
|
|
||||||
// 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) {
|
||||||
certState := cm.intf.pki.GetCertState()
|
cs := cm.intf.pki.getCertState()
|
||||||
if bytes.Equal(hostinfo.ConnectionState.myCert.Signature, certState.Certificate.Signature) {
|
curCrt := hostinfo.ConnectionState.myCert
|
||||||
|
curCrtVersion := curCrt.Version()
|
||||||
|
myCrt := cs.getCertificate(curCrtVersion)
|
||||||
|
if myCrt == nil {
|
||||||
|
cm.l.Info("Re-handshaking with remote",
|
||||||
|
"vpnAddrs", hostinfo.vpnAddrs,
|
||||||
|
"version", curCrtVersion,
|
||||||
|
"reason", "local certificate removed",
|
||||||
|
)
|
||||||
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
peerCrt := hostinfo.ConnectionState.peerCert
|
||||||
|
if peerCrt != nil && curCrtVersion < peerCrt.Certificate.Version() {
|
||||||
|
// if our certificate version is less than theirs, and we have a matching version available, rehandshake?
|
||||||
|
if cs.getCertificate(peerCrt.Certificate.Version()) != nil {
|
||||||
|
cm.l.Info("Re-handshaking with remote",
|
||||||
|
"vpnAddrs", hostinfo.vpnAddrs,
|
||||||
|
"version", curCrtVersion,
|
||||||
|
"peerVersion", peerCrt.Certificate.Version(),
|
||||||
|
"reason", "local certificate version lower than peer, attempting to correct",
|
||||||
|
)
|
||||||
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], func(hh *HandshakeHostInfo) {
|
||||||
|
hh.initiatingVersionOverride = peerCrt.Certificate.Version()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !bytes.Equal(curCrt.Signature(), myCrt.Signature()) {
|
||||||
|
cm.l.Info("Re-handshaking with remote",
|
||||||
|
"vpnAddrs", hostinfo.vpnAddrs,
|
||||||
|
"reason", "local certificate is not current",
|
||||||
|
)
|
||||||
|
|
||||||
cm.l.WithField("vpnIp", hostinfo.vpnIp).
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||||
WithField("reason", "local certificate is not current").
|
return
|
||||||
Info("Re-handshaking with remote")
|
}
|
||||||
|
if curCrtVersion < cs.initiatingVersion {
|
||||||
|
cm.l.Info("Re-handshaking with remote",
|
||||||
|
"vpnAddrs", hostinfo.vpnAddrs,
|
||||||
|
"reason", "current cert version < pki.initiatingVersion",
|
||||||
|
)
|
||||||
|
|
||||||
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnIp, nil)
|
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+196
-94
@@ -3,17 +3,17 @@ package nebula
|
|||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"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"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestLighthouse() *LightHouse {
|
func newTestLighthouse() *LightHouse {
|
||||||
@@ -22,9 +22,10 @@ func newTestLighthouse() *LightHouse {
|
|||||||
addrMap: map[netip.Addr]*RemoteList{},
|
addrMap: map[netip.Addr]*RemoteList{},
|
||||||
queryChan: make(chan netip.Addr, 10),
|
queryChan: make(chan netip.Addr, 10),
|
||||||
}
|
}
|
||||||
lighthouses := map[netip.Addr]struct{}{}
|
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)
|
||||||
|
|
||||||
@@ -34,26 +35,25 @@ func newTestLighthouse() *LightHouse {
|
|||||||
func Test_NewConnectionManagerTest(t *testing.T) {
|
func Test_NewConnectionManagerTest(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
//_, tuncidr, _ := net.ParseCIDR("1.1.1.1/24")
|
//_, tuncidr, _ := net.ParseCIDR("1.1.1.1/24")
|
||||||
vpncidr := netip.MustParsePrefix("172.1.1.1/24")
|
|
||||||
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
||||||
vpnIp := netip.MustParseAddr("172.1.1.2")
|
vpnIp := netip.MustParseAddr("172.1.1.2")
|
||||||
preferredRanges := []netip.Prefix{localrange}
|
preferredRanges := []netip.Prefix{localrange}
|
||||||
|
|
||||||
// Very incomplete mock objects
|
// Very incomplete mock objects
|
||||||
hostMap := newHostMap(l, vpncidr)
|
hostMap := newHostMap(l)
|
||||||
hostMap.preferredRanges.Store(&preferredRanges)
|
hostMap.preferredRanges.Store(&preferredRanges)
|
||||||
|
|
||||||
cs := &CertState{
|
cs := &CertState{
|
||||||
RawCertificate: []byte{},
|
initiatingVersion: cert.Version1,
|
||||||
PrivateKey: []byte{},
|
privateKey: []byte{},
|
||||||
Certificate: &cert.NebulaCertificate{},
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
RawCertificateNoKey: []byte{},
|
v1Credential: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &test.NoopTun{},
|
inside: &overlaytest.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -64,9 +64,9 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
ifce.pki.cs.Store(cs)
|
ifce.pki.cs.Store(cs)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(l)
|
conf := config.NewC(test.NewLogger())
|
||||||
punchy := NewPunchyFromConfig(l, conf)
|
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
p := []byte("")
|
p := []byte("")
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
@@ -74,13 +74,12 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
|
|
||||||
// Add an ip we have established a connection w/ to hostmap
|
// Add an ip we have established a connection w/ to hostmap
|
||||||
hostinfo := &HostInfo{
|
hostinfo := &HostInfo{
|
||||||
vpnIp: vpnIp,
|
vpnAddrs: []netip.Addr{vpnIp},
|
||||||
localIndexId: 1099,
|
localIndexId: 1099,
|
||||||
remoteIndexId: 9901,
|
remoteIndexId: 9901,
|
||||||
}
|
}
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
hostinfo.ConnectionState = &ConnectionState{
|
||||||
myCert: &cert.NebulaCertificate{},
|
myCert: &dummyCert{version: cert.Version1},
|
||||||
H: &noise.HandshakeState{},
|
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnIp)
|
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.out.Load())
|
assert.True(t, hostinfo.out.Load())
|
||||||
assert.True(t, hostinfo.in.Load())
|
assert.True(t, hostinfo.in.Load())
|
||||||
@@ -107,37 +106,36 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.in.Load())
|
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.vpnIp)
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
// Do a final traffic check tick, the host should now be removed
|
// Do a final traffic check tick, the host should now be removed
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.NotContains(t, nc.hostMap.Hosts, hostinfo.vpnIp)
|
assert.NotContains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs)
|
||||||
assert.NotContains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.NotContains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_NewConnectionManagerTest2(t *testing.T) {
|
func Test_NewConnectionManagerTest2(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
//_, tuncidr, _ := net.ParseCIDR("1.1.1.1/24")
|
//_, tuncidr, _ := net.ParseCIDR("1.1.1.1/24")
|
||||||
vpncidr := netip.MustParsePrefix("172.1.1.1/24")
|
|
||||||
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
||||||
vpnIp := netip.MustParseAddr("172.1.1.2")
|
vpnIp := netip.MustParseAddr("172.1.1.2")
|
||||||
preferredRanges := []netip.Prefix{localrange}
|
preferredRanges := []netip.Prefix{localrange}
|
||||||
|
|
||||||
// Very incomplete mock objects
|
// Very incomplete mock objects
|
||||||
hostMap := newHostMap(l, vpncidr)
|
hostMap := newHostMap(l)
|
||||||
hostMap.preferredRanges.Store(&preferredRanges)
|
hostMap.preferredRanges.Store(&preferredRanges)
|
||||||
|
|
||||||
cs := &CertState{
|
cs := &CertState{
|
||||||
RawCertificate: []byte{},
|
initiatingVersion: cert.Version1,
|
||||||
PrivateKey: []byte{},
|
privateKey: []byte{},
|
||||||
Certificate: &cert.NebulaCertificate{},
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
RawCertificateNoKey: []byte{},
|
v1Credential: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &test.NoopTun{},
|
inside: &overlaytest.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -148,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(l)
|
conf := config.NewC(test.NewLogger())
|
||||||
punchy := NewPunchyFromConfig(l, conf)
|
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
p := []byte("")
|
p := []byte("")
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
@@ -158,13 +156,12 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
|
|
||||||
// Add an ip we have established a connection w/ to hostmap
|
// Add an ip we have established a connection w/ to hostmap
|
||||||
hostinfo := &HostInfo{
|
hostinfo := &HostInfo{
|
||||||
vpnIp: vpnIp,
|
vpnAddrs: []netip.Addr{vpnIp},
|
||||||
localIndexId: 1099,
|
localIndexId: 1099,
|
||||||
remoteIndexId: 9901,
|
remoteIndexId: 9901,
|
||||||
}
|
}
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
hostinfo.ConnectionState = &ConnectionState{
|
||||||
myCert: &cert.NebulaCertificate{},
|
myCert: &dummyCert{version: cert.Version1},
|
||||||
H: &noise.HandshakeState{},
|
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|
||||||
@@ -174,7 +171,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
assert.True(t, hostinfo.in.Load())
|
assert.True(t, hostinfo.in.Load())
|
||||||
assert.True(t, hostinfo.out.Load())
|
assert.True(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.pendingDeletion.Load())
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnIp)
|
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
|
||||||
@@ -190,7 +187,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.in.Load())
|
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.vpnIp)
|
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)
|
||||||
@@ -199,31 +196,30 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.in.Load())
|
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.vpnIp)
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
vpncidr := netip.MustParsePrefix("172.1.1.1/24")
|
|
||||||
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
||||||
vpnIp := netip.MustParseAddr("172.1.1.2")
|
vpnAddrs := []netip.Addr{netip.MustParseAddr("172.1.1.2")}
|
||||||
preferredRanges := []netip.Prefix{localrange}
|
preferredRanges := []netip.Prefix{localrange}
|
||||||
|
|
||||||
// Very incomplete mock objects
|
// Very incomplete mock objects
|
||||||
hostMap := newHostMap(l, vpncidr)
|
hostMap := newHostMap(l)
|
||||||
hostMap.preferredRanges.Store(&preferredRanges)
|
hostMap.preferredRanges.Store(&preferredRanges)
|
||||||
|
|
||||||
cs := &CertState{
|
cs := &CertState{
|
||||||
RawCertificate: []byte{},
|
initiatingVersion: cert.Version1,
|
||||||
PrivateKey: []byte{},
|
privateKey: []byte{},
|
||||||
Certificate: &cert.NebulaCertificate{},
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
RawCertificateNoKey: []byte{},
|
v1Credential: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &test.NoopTun{},
|
inside: &overlaytest.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -234,24 +230,23 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
ifce.pki.cs.Store(cs)
|
ifce.pki.cs.Store(cs)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(l)
|
conf := config.NewC(test.NewLogger())
|
||||||
conf.Settings["tunnels"] = map[interface{}]interface{}{
|
conf.Settings["tunnels"] = map[string]any{
|
||||||
"drop_inactive": true,
|
"drop_inactive": true,
|
||||||
}
|
}
|
||||||
punchy := NewPunchyFromConfig(l, conf)
|
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||||
assert.True(t, nc.dropInactive.Load())
|
assert.True(t, nc.dropInactive.Load())
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
|
|
||||||
// Add an ip we have established a connection w/ to hostmap
|
// Add an ip we have established a connection w/ to hostmap
|
||||||
hostinfo := &HostInfo{
|
hostinfo := &HostInfo{
|
||||||
vpnIp: vpnIp,
|
vpnAddrs: vpnAddrs,
|
||||||
localIndexId: 1099,
|
localIndexId: 1099,
|
||||||
remoteIndexId: 9901,
|
remoteIndexId: 9901,
|
||||||
}
|
}
|
||||||
hostinfo.ConnectionState = &ConnectionState{
|
hostinfo.ConnectionState = &ConnectionState{
|
||||||
myCert: &cert.NebulaCertificate{},
|
myCert: &dummyCert{version: cert.Version1},
|
||||||
H: &noise.HandshakeState{},
|
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
|
|
||||||
@@ -284,7 +279,7 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.in.Load())
|
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.vpnIp)
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
// Finally advance beyond the inactivity timeout
|
// Finally advance beyond the inactivity timeout
|
||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Minute*10))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Minute*10))
|
||||||
@@ -294,7 +289,7 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.out.Load())
|
||||||
assert.False(t, hostinfo.in.Load())
|
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.vpnIp)
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we can disconnect the peer.
|
// Check if we can disconnect the peer.
|
||||||
@@ -303,61 +298,54 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
ipNet := net.IPNet{
|
|
||||||
IP: net.IPv4(172, 1, 1, 2),
|
|
||||||
Mask: net.IPMask{255, 255, 255, 0},
|
|
||||||
}
|
|
||||||
vpncidr := netip.MustParsePrefix("172.1.1.1/24")
|
vpncidr := netip.MustParsePrefix("172.1.1.1/24")
|
||||||
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
||||||
vpnIp := netip.MustParseAddr("172.1.1.2")
|
vpnIp := netip.MustParseAddr("172.1.1.2")
|
||||||
preferredRanges := []netip.Prefix{localrange}
|
preferredRanges := []netip.Prefix{localrange}
|
||||||
hostMap := newHostMap(l, vpncidr)
|
hostMap := newHostMap(l)
|
||||||
hostMap.preferredRanges.Store(&preferredRanges)
|
hostMap.preferredRanges.Store(&preferredRanges)
|
||||||
|
|
||||||
// Generate keys for CA and peer's cert.
|
// Generate keys for CA and peer's cert.
|
||||||
pubCA, privCA, _ := ed25519.GenerateKey(rand.Reader)
|
pubCA, privCA, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
caCert := cert.NebulaCertificate{
|
tbs := &cert.TBSCertificate{
|
||||||
Details: cert.NebulaCertificateDetails{
|
Version: 1,
|
||||||
Name: "ca",
|
Name: "ca",
|
||||||
NotBefore: now,
|
IsCA: true,
|
||||||
NotAfter: now.Add(1 * time.Hour),
|
NotBefore: now,
|
||||||
IsCA: true,
|
NotAfter: now.Add(1 * time.Hour),
|
||||||
PublicKey: pubCA,
|
PublicKey: pubCA,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, caCert.Sign(cert.Curve_CURVE25519, privCA))
|
caCert, err := tbs.Sign(nil, cert.Curve_CURVE25519, privCA)
|
||||||
ncp := &cert.NebulaCAPool{
|
require.NoError(t, err)
|
||||||
CAs: cert.NewCAPool().CAs,
|
ncp := cert.NewCAPool()
|
||||||
}
|
require.NoError(t, ncp.AddCA(caCert))
|
||||||
ncp.CAs["ca"] = &caCert
|
|
||||||
|
|
||||||
pubCrt, _, _ := ed25519.GenerateKey(rand.Reader)
|
pubCrt, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||||
peerCert := cert.NebulaCertificate{
|
tbs = &cert.TBSCertificate{
|
||||||
Details: cert.NebulaCertificateDetails{
|
Version: 1,
|
||||||
Name: "host",
|
Name: "host",
|
||||||
Ips: []*net.IPNet{&ipNet},
|
Networks: []netip.Prefix{vpncidr},
|
||||||
Subnets: []*net.IPNet{},
|
NotBefore: now,
|
||||||
NotBefore: now,
|
NotAfter: now.Add(60 * time.Second),
|
||||||
NotAfter: now.Add(60 * time.Second),
|
PublicKey: pubCrt,
|
||||||
PublicKey: pubCrt,
|
|
||||||
IsCA: false,
|
|
||||||
Issuer: "ca",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
assert.NoError(t, peerCert.Sign(cert.Curve_CURVE25519, privCA))
|
peerCert, err := tbs.Sign(caCert, cert.Curve_CURVE25519, privCA)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cachedPeerCert, err := ncp.VerifyCertificate(now.Add(time.Second), peerCert)
|
||||||
|
|
||||||
cs := &CertState{
|
cs := &CertState{
|
||||||
RawCertificate: []byte{},
|
privateKey: []byte{},
|
||||||
PrivateKey: []byte{},
|
v1Cert: &dummyCert{},
|
||||||
Certificate: &cert.NebulaCertificate{},
|
v1Credential: nil,
|
||||||
RawCertificateNoKey: []byte{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lh := newTestLighthouse()
|
lh := newTestLighthouse()
|
||||||
ifce := &Interface{
|
ifce := &Interface{
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
inside: &test.NoopTun{},
|
inside: &overlaytest.NoopTun{},
|
||||||
outside: &udp.NoopConn{},
|
outside: &udp.NoopConn{},
|
||||||
firewall: &Firewall{},
|
firewall: &Firewall{},
|
||||||
lightHouse: lh,
|
lightHouse: lh,
|
||||||
@@ -370,18 +358,17 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
|||||||
ifce.disconnectInvalid.Store(true)
|
ifce.disconnectInvalid.Store(true)
|
||||||
|
|
||||||
// Create manager
|
// Create manager
|
||||||
conf := config.NewC(l)
|
conf := config.NewC(test.NewLogger())
|
||||||
punchy := NewPunchyFromConfig(l, conf)
|
punchy := NewPunchyFromConfig(test.NewLogger(), conf, nil)
|
||||||
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
|
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
|
||||||
nc.intf = ifce
|
nc.intf = ifce
|
||||||
ifce.connectionManager = nc
|
ifce.connectionManager = nc
|
||||||
|
|
||||||
hostinfo := &HostInfo{
|
hostinfo := &HostInfo{
|
||||||
vpnIp: vpnIp,
|
vpnAddrs: []netip.Addr{vpnIp},
|
||||||
ConnectionState: &ConnectionState{
|
ConnectionState: &ConnectionState{
|
||||||
myCert: &cert.NebulaCertificate{},
|
myCert: &dummyCert{},
|
||||||
peerCert: &peerCert,
|
peerCert: cachedPeerCert,
|
||||||
H: &noise.HandshakeState{},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
nc.hostMap.unlockedAddHostInfo(hostinfo, ifce)
|
||||||
@@ -400,3 +387,118 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
|
|||||||
invalid = nc.isInvalidCertificate(nextTick, hostinfo)
|
invalid = nc.isInvalidCertificate(nextTick, hostinfo)
|
||||||
assert.True(t, invalid)
|
assert.True(t, invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) MarshalPublicKeyPEM() []byte {
|
||||||
|
return cert.MarshalPublicKeyToPEM(d.curve, d.publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) Signature() []byte {
|
||||||
|
return d.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) UnsafeNetworks() []netip.Prefix {
|
||||||
|
return d.unsafeNetworks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) MarshalForHandshakes() ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) Sign(curve cert.Curve, key []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) CheckSignature(key []byte) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) Expired(t time.Time) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) CheckRootConstraints(signer cert.Certificate) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) VerifyPrivateKey(curve cert.Curve, key []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) Marshal() ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) MarshalPEM() ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) Fingerprint() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyCert) Copy() cert.Certificate {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|||||||
+76
-55
@@ -1,80 +1,48 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
"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 *NebulaCipherState
|
eKey noiseutil.CipherState
|
||||||
dKey *NebulaCipherState
|
dKey noiseutil.CipherState
|
||||||
H *noise.HandshakeState
|
myCert cert.Certificate
|
||||||
myCert *cert.NebulaCertificate
|
peerCert *cert.CachedCertificate
|
||||||
peerCert *cert.NebulaCertificate
|
|
||||||
initiator bool
|
initiator bool
|
||||||
messageCounter atomic.Uint64
|
messageCounter atomic.Uint64
|
||||||
window *Bits
|
window *Bits
|
||||||
|
decryptLock sync.Mutex
|
||||||
writeLock sync.Mutex
|
writeLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectionState(l *logrus.Logger, cipher string, certState *CertState, initiator bool, pattern noise.HandshakePattern, psk []byte, pskStage int) *ConnectionState {
|
// newConnectionStateFromResult builds a fully-populated ConnectionState from a
|
||||||
var dhFunc noise.DHFunc
|
// completed handshake.Result. It seeds messageCounter and the replay window so
|
||||||
switch certState.Certificate.Details.Curve {
|
// that the post-handshake message indices already used on the wire don't count
|
||||||
case cert.Curve_CURVE25519:
|
// as missed traffic in the data plane.
|
||||||
dhFunc = noise.DH25519
|
func newConnectionStateFromResult(r *handshake.Result) *ConnectionState {
|
||||||
case cert.Curve_P256:
|
|
||||||
dhFunc = noiseutil.DHP256
|
|
||||||
default:
|
|
||||||
l.Errorf("invalid curve: %s", certState.Certificate.Details.Curve)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var cs noise.CipherSuite
|
|
||||||
if cipher == "chachapoly" {
|
|
||||||
cs = noise.NewCipherSuite(dhFunc, noise.CipherChaChaPoly, noise.HashSHA256)
|
|
||||||
} else {
|
|
||||||
cs = noise.NewCipherSuite(dhFunc, noiseutil.CipherAESGCM, noise.HashSHA256)
|
|
||||||
}
|
|
||||||
|
|
||||||
static := noise.DHKey{Private: certState.PrivateKey, Public: certState.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: cs,
|
|
||||||
Random: rand.Reader,
|
|
||||||
Pattern: pattern,
|
|
||||||
Initiator: initiator,
|
|
||||||
StaticKeypair: static,
|
|
||||||
PresharedKey: psk,
|
|
||||||
PresharedKeyPlacement: pskStage,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{
|
||||||
H: hs,
|
myCert: r.MyCert,
|
||||||
initiator: initiator,
|
initiator: r.Initiator,
|
||||||
window: b,
|
peerCert: r.RemoteCert,
|
||||||
myCert: certState.Certificate,
|
eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
|
||||||
|
dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
|
||||||
|
window: NewBits(ReplayWindow),
|
||||||
|
}
|
||||||
|
ci.messageCounter.Add(r.MessageIndex)
|
||||||
|
for i := uint64(1); i <= r.MessageIndex; i++ {
|
||||||
|
ci.window.Update(nil, i)
|
||||||
}
|
}
|
||||||
// always start the counter from 2, as packet 1 and packet 2 are handshake packets.
|
|
||||||
ci.messageCounter.Add(2)
|
|
||||||
|
|
||||||
return ci
|
return ci
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,3 +53,56 @@ func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
|
|||||||
"message_counter": cs.messageCounter.Load(),
|
"message_counter": cs.messageCounter.Load(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs *ConnectionState) Curve() cert.Curve {
|
||||||
|
return cs.myCert.Curve()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, out []byte, packet []byte, nb []byte) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
cs.decryptLock.Lock()
|
||||||
|
result := cs.window.Check(l, messageCounter)
|
||||||
|
cs.decryptLock.Unlock()
|
||||||
|
if !result {
|
||||||
|
return nil, ErrAlreadySeen
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err = cs.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.decryptLock.Lock()
|
||||||
|
result = cs.window.Update(l, messageCounter)
|
||||||
|
cs.decryptLock.Unlock()
|
||||||
|
if !result {
|
||||||
|
return nil, ErrAlreadySeen
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyRelay verifies AEAD protected (but not encrypted) relay frames. packet must be length-checked by the caller.
|
||||||
|
func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
|
||||||
|
cs.decryptLock.Lock()
|
||||||
|
result := cs.window.Check(l, messageCounter)
|
||||||
|
cs.decryptLock.Unlock()
|
||||||
|
if !result {
|
||||||
|
return ErrAlreadySeen
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
|
||||||
|
signatureValue := packet[len(packet)-cs.dKey.Overhead():]
|
||||||
|
_, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.decryptLock.Lock()
|
||||||
|
result = cs.window.Update(l, messageCounter)
|
||||||
|
cs.decryptLock.Unlock()
|
||||||
|
if !result {
|
||||||
|
return ErrAlreadySeen
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
+156
-54
@@ -2,57 +2,98 @@ 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
|
||||||
|
|
||||||
type controlEach func(h *HostInfo)
|
type controlEach func(h *HostInfo)
|
||||||
|
|
||||||
type controlHostLister interface {
|
type controlHostLister interface {
|
||||||
QueryVpnIp(vpnIp netip.Addr) *HostInfo
|
QueryVpnAddr(vpnAddr netip.Addr) *HostInfo
|
||||||
ForEachIndex(each controlEach)
|
ForEachIndex(each controlEach)
|
||||||
ForEachVpnIp(each controlEach)
|
ForEachVpnAddr(each controlEach)
|
||||||
GetPreferredRanges() []netip.Prefix
|
GetPreferredRanges() []netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
type Control struct {
|
type Control struct {
|
||||||
|
stateLock sync.Mutex
|
||||||
|
state RunState
|
||||||
|
|
||||||
f *Interface
|
f *Interface
|
||||||
l *logrus.Logger
|
l *slog.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ControlHostInfo struct {
|
type ControlHostInfo struct {
|
||||||
VpnIp netip.Addr `json:"vpnIp"`
|
VpnAddrs []netip.Addr `json:"vpnAddrs"`
|
||||||
LocalIndex uint32 `json:"localIndex"`
|
LocalIndex uint32 `json:"localIndex"`
|
||||||
RemoteIndex uint32 `json:"remoteIndex"`
|
RemoteIndex uint32 `json:"remoteIndex"`
|
||||||
RemoteAddrs []netip.AddrPort `json:"remoteAddrs"`
|
RemoteAddrs []netip.AddrPort `json:"remoteAddrs"`
|
||||||
Cert *cert.NebulaCertificate `json:"cert"`
|
Cert cert.Certificate `json:"cert"`
|
||||||
MessageCounter uint64 `json:"messageCounter"`
|
MessageCounter uint64 `json:"messageCounter"`
|
||||||
CurrentRemote netip.AddrPort `json:"currentRemote"`
|
CurrentRemote netip.AddrPort `json:"currentRemote"`
|
||||||
CurrentRelaysToMe []netip.Addr `json:"currentRelaysToMe"`
|
CurrentRelaysToMe []netip.Addr `json:"currentRelaysToMe"`
|
||||||
CurrentRelaysThroughMe []netip.Addr `json:"currentRelaysThroughMe"`
|
CurrentRelaysThroughMe []netip.Addr `json:"currentRelaysThroughMe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start actually runs nebula, this is a nonblocking call. To block use Control.ShutdownBlock()
|
// Start actually runs nebula, this is a nonblocking call.
|
||||||
func (c *Control) Start() {
|
// Use Wait to block until nebula has fully stopped and to learn whether a fatal reader error caused the shutdown.
|
||||||
|
func (c *Control) Start() error {
|
||||||
|
c.stateLock.Lock()
|
||||||
|
defer c.stateLock.Unlock()
|
||||||
|
switch c.state {
|
||||||
|
case StateReady:
|
||||||
|
//yay!
|
||||||
|
case StateStopped, StateStopping:
|
||||||
|
return ErrAlreadyStopped
|
||||||
|
case StateStarted:
|
||||||
|
return ErrAlreadyStarted
|
||||||
|
default:
|
||||||
|
return ErrUnknownState
|
||||||
|
}
|
||||||
|
|
||||||
// Activate the interface
|
// Activate the interface
|
||||||
c.f.activate()
|
err := c.f.activate()
|
||||||
|
if err != nil {
|
||||||
|
// Cancel before Close so a caller returning from Wait always observes a dead Context
|
||||||
|
c.cancel()
|
||||||
|
_ = c.f.Close()
|
||||||
|
c.state = StateStopped
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -64,6 +105,9 @@ func (c *Control) Start() {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@@ -71,25 +115,70 @@ func (c *Control) Start() {
|
|||||||
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 signals nebula to shutdown and close all tunnels, returns after the shutdown is complete
|
// Stop tears nebula down, closing all tunnels and releasing everything it holds.
|
||||||
|
// Use Wait to block until the shutdown has completed.
|
||||||
|
// A Control that has been stopped cannot be started again, Start will return ErrAlreadyStopped.
|
||||||
func (c *Control) Stop() {
|
func (c *Control) Stop() {
|
||||||
// Stop the handshakeManager (and other services), to prevent new tunnels from
|
c.stateLock.Lock()
|
||||||
// being created while we're shutting them all down.
|
switch c.state {
|
||||||
c.cancel()
|
case StateStarted:
|
||||||
|
// Fall through to the full teardown below
|
||||||
|
|
||||||
c.CloseAllTunnels(false)
|
case StateReady:
|
||||||
if err := c.f.Close(); err != nil {
|
// Never started
|
||||||
c.l.WithError(err).Error("Close interface failed")
|
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.l.Info("Goodbye")
|
|
||||||
|
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.CloseAllTunnels(false)
|
||||||
|
|
||||||
|
c.stateLock.Lock()
|
||||||
|
c.state = StateStopped
|
||||||
|
if err := c.f.Close(); err != nil {
|
||||||
|
c.l.Error("Close interface failed", "error", err)
|
||||||
|
}
|
||||||
|
c.stateLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks until nebula has fully stopped, either via Stop or an internal fatal error,
|
||||||
|
// and returns the first fatal packet reader error if there was one.
|
||||||
|
// It is safe to call from multiple goroutines and at any point in the lifecycle,
|
||||||
|
// but a Wait on a Control that is never started and never stopped will block forever.
|
||||||
|
func (c *Control) Wait() error {
|
||||||
|
return c.f.wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
// ShutdownBlock will listen for and block on term and interrupt signals, calling Control.Stop() once signalled
|
||||||
@@ -100,13 +189,24 @@ func (c *Control) ShutdownBlock() {
|
|||||||
|
|
||||||
rawSig := <-sigChan
|
rawSig := <-sigChan
|
||||||
sig := rawSig.String()
|
sig := rawSig.String()
|
||||||
c.l.WithField("signal", sig).Info("Caught signal, shutting down")
|
c.l.Info("Caught signal, shutting down", "signal", sig)
|
||||||
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.f.outside.Rebind()
|
c.stateLock.Lock()
|
||||||
|
defer c.stateLock.Unlock()
|
||||||
|
|
||||||
|
if c.state != StateStarted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failure here means we are likely still pinned to the interface we came up on, so the rest of this is
|
||||||
|
// unlikely to help. Say so instead of silently carrying on as if we rebound.
|
||||||
|
if err := c.f.outside.Rebind(); err != nil {
|
||||||
|
c.l.Error("Failed to rebind udp socket", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
@@ -134,15 +234,17 @@ func (c *Control) ListHostmapIndexes(pendingMap bool) []ControlHostInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCertByVpnIp returns the authenticated certificate of the given vpn IP, or nil if not found
|
// GetCertByVpnIp returns the authenticated certificate of the given vpn IP, or nil if not found
|
||||||
func (c *Control) GetCertByVpnIp(vpnIp netip.Addr) *cert.NebulaCertificate {
|
func (c *Control) GetCertByVpnIp(vpnIp netip.Addr) cert.Certificate {
|
||||||
if c.f.myVpnNet.Addr() == vpnIp {
|
if c.f.myVpnAddrsTable.Contains(vpnIp) {
|
||||||
return c.f.pki.GetCertState().Certificate
|
// Only returning the default certificate since its impossible
|
||||||
|
// for any other host but ourselves to have more than 1
|
||||||
|
return c.f.pki.getCertState().GetDefaultCertificate().Copy()
|
||||||
}
|
}
|
||||||
hi := c.f.hostMap.QueryVpnIp(vpnIp)
|
hi := c.f.hostMap.QueryVpnAddr(vpnIp)
|
||||||
if hi == nil {
|
if hi == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return hi.GetCert()
|
return hi.GetCert().Certificate.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTunnel creates a new tunnel to the given vpn ip.
|
// CreateTunnel creates a new tunnel to the given vpn ip.
|
||||||
@@ -152,7 +254,7 @@ func (c *Control) CreateTunnel(vpnIp netip.Addr) {
|
|||||||
|
|
||||||
// PrintTunnel creates a new tunnel to the given vpn ip.
|
// PrintTunnel creates a new tunnel to the given vpn ip.
|
||||||
func (c *Control) PrintTunnel(vpnIp netip.Addr) *ControlHostInfo {
|
func (c *Control) PrintTunnel(vpnIp netip.Addr) *ControlHostInfo {
|
||||||
hi := c.f.hostMap.QueryVpnIp(vpnIp)
|
hi := c.f.hostMap.QueryVpnAddr(vpnIp)
|
||||||
if hi == nil {
|
if hi == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -169,9 +271,9 @@ func (c *Control) QueryLighthouse(vpnIp netip.Addr) *CacheMap {
|
|||||||
return hi.CopyCache()
|
return hi.CopyCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostInfoByVpnIp returns a single tunnels hostInfo, or nil if not found
|
// GetHostInfoByVpnAddr returns a single tunnels hostInfo, or nil if not found
|
||||||
// Caller should take care to Unmap() any 4in6 addresses prior to calling.
|
// Caller should take care to Unmap() any 4in6 addresses prior to calling.
|
||||||
func (c *Control) GetHostInfoByVpnIp(vpnIp netip.Addr, pending bool) *ControlHostInfo {
|
func (c *Control) GetHostInfoByVpnAddr(vpnAddr netip.Addr, pending bool) *ControlHostInfo {
|
||||||
var hl controlHostLister
|
var hl controlHostLister
|
||||||
if pending {
|
if pending {
|
||||||
hl = c.f.handshakeManager
|
hl = c.f.handshakeManager
|
||||||
@@ -179,7 +281,7 @@ func (c *Control) GetHostInfoByVpnIp(vpnIp netip.Addr, pending bool) *ControlHos
|
|||||||
hl = c.f.hostMap
|
hl = c.f.hostMap
|
||||||
}
|
}
|
||||||
|
|
||||||
h := hl.QueryVpnIp(vpnIp)
|
h := hl.QueryVpnAddr(vpnAddr)
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -191,7 +293,7 @@ func (c *Control) GetHostInfoByVpnIp(vpnIp netip.Addr, pending bool) *ControlHos
|
|||||||
// SetRemoteForTunnel forces a tunnel to use a specific remote
|
// SetRemoteForTunnel forces a tunnel to use a specific remote
|
||||||
// Caller should take care to Unmap() any 4in6 addresses prior to calling.
|
// Caller should take care to Unmap() any 4in6 addresses prior to calling.
|
||||||
func (c *Control) SetRemoteForTunnel(vpnIp netip.Addr, addr netip.AddrPort) *ControlHostInfo {
|
func (c *Control) SetRemoteForTunnel(vpnIp netip.Addr, addr netip.AddrPort) *ControlHostInfo {
|
||||||
hostInfo := c.f.hostMap.QueryVpnIp(vpnIp)
|
hostInfo := c.f.hostMap.QueryVpnAddr(vpnIp)
|
||||||
if hostInfo == nil {
|
if hostInfo == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -204,7 +306,7 @@ func (c *Control) SetRemoteForTunnel(vpnIp netip.Addr, addr netip.AddrPort) *Con
|
|||||||
// CloseTunnel closes a fully established tunnel. If localOnly is false it will notify the remote end as well.
|
// CloseTunnel closes a fully established tunnel. If localOnly is false it will notify the remote end as well.
|
||||||
// Caller should take care to Unmap() any 4in6 addresses prior to calling.
|
// Caller should take care to Unmap() any 4in6 addresses prior to calling.
|
||||||
func (c *Control) CloseTunnel(vpnIp netip.Addr, localOnly bool) bool {
|
func (c *Control) CloseTunnel(vpnIp netip.Addr, localOnly bool) bool {
|
||||||
hostInfo := c.f.hostMap.QueryVpnIp(vpnIp)
|
hostInfo := c.f.hostMap.QueryVpnAddr(vpnIp)
|
||||||
if hostInfo == nil {
|
if hostInfo == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -228,20 +330,17 @@ func (c *Control) CloseTunnel(vpnIp netip.Addr, localOnly bool) bool {
|
|||||||
// CloseAllTunnels is just like CloseTunnel except it goes through and shuts them all down, optionally you can avoid shutting down lighthouse tunnels
|
// CloseAllTunnels is just like CloseTunnel except it goes through and shuts them all down, optionally you can avoid shutting down lighthouse tunnels
|
||||||
// the int returned is a count of tunnels closed
|
// the int returned is a count of tunnels closed
|
||||||
func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
||||||
//TODO: this is probably better as a function in ConnectionManager or HostMap directly
|
|
||||||
lighthouses := c.f.lightHouse.GetLighthouses()
|
|
||||||
|
|
||||||
shutdown := func(h *HostInfo) {
|
shutdown := func(h *HostInfo) {
|
||||||
if excludeLighthouses {
|
if excludeLighthouses && c.f.lightHouse.IsAnyLighthouseAddr(h.vpnAddrs) {
|
||||||
if _, ok := lighthouses[h.vpnIp]; ok {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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.WithField("vpnIp", h.vpnIp).WithField("udpAddr", h.remote).
|
c.l.Debug("Sending close tunnel message",
|
||||||
Debug("Sending close tunnel message")
|
"vpnAddrs", h.vpnAddrs,
|
||||||
|
"udpAddr", h.GetRemote(),
|
||||||
|
)
|
||||||
closed++
|
closed++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +349,7 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
|||||||
// Grab the hostMap lock to access the Relays map
|
// Grab the hostMap lock to access the Relays map
|
||||||
c.f.hostMap.Lock()
|
c.f.hostMap.Lock()
|
||||||
for _, relayingHost := range c.f.hostMap.Relays {
|
for _, relayingHost := range c.f.hostMap.Relays {
|
||||||
relayingHosts[relayingHost.vpnIp] = relayingHost
|
relayingHosts[relayingHost.vpnAddrs[0]] = relayingHost
|
||||||
}
|
}
|
||||||
c.f.hostMap.Unlock()
|
c.f.hostMap.Unlock()
|
||||||
|
|
||||||
@@ -258,7 +357,7 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
|
|||||||
// Grab the hostMap lock to access the Hosts map
|
// Grab the hostMap lock to access the Hosts map
|
||||||
c.f.hostMap.Lock()
|
c.f.hostMap.Lock()
|
||||||
for _, relayHost := range c.f.hostMap.Indexes {
|
for _, relayHost := range c.f.hostMap.Indexes {
|
||||||
if _, ok := relayingHosts[relayHost.vpnIp]; !ok {
|
if _, ok := relayingHosts[relayHost.vpnAddrs[0]]; !ok {
|
||||||
hostInfos = append(hostInfos, relayHost)
|
hostInfos = append(hostInfos, relayHost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,15 +377,18 @@ func (c *Control) Device() overlay.Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
||||||
|
|
||||||
chi := ControlHostInfo{
|
chi := ControlHostInfo{
|
||||||
VpnIp: h.vpnIp,
|
VpnAddrs: make([]netip.Addr, len(h.vpnAddrs)),
|
||||||
LocalIndex: h.localIndexId,
|
LocalIndex: h.localIndexId,
|
||||||
RemoteIndex: h.remoteIndexId,
|
RemoteIndex: h.remoteIndexId,
|
||||||
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.remote,
|
CurrentRemote: h.GetRemote(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, a := range h.vpnAddrs {
|
||||||
|
chi.VpnAddrs[i] = a
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.ConnectionState != nil {
|
if h.ConnectionState != nil {
|
||||||
@@ -294,7 +396,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c := h.GetCert(); c != nil {
|
if c := h.GetCert(); c != nil {
|
||||||
chi.Cert = c.Copy()
|
chi.Cert = c.Certificate.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
return chi
|
return chi
|
||||||
@@ -303,7 +405,7 @@ func copyHostInfo(h *HostInfo, preferredRanges []netip.Prefix) ControlHostInfo {
|
|||||||
func listHostMapHosts(hl controlHostLister) []ControlHostInfo {
|
func listHostMapHosts(hl controlHostLister) []ControlHostInfo {
|
||||||
hosts := make([]ControlHostInfo, 0)
|
hosts := make([]ControlHostInfo, 0)
|
||||||
pr := hl.GetPreferredRanges()
|
pr := hl.GetPreferredRanges()
|
||||||
hl.ForEachVpnIp(func(hostinfo *HostInfo) {
|
hl.ForEachVpnAddr(func(hostinfo *HostInfo) {
|
||||||
hosts = append(hosts, copyHostInfo(hostinfo, pr))
|
hosts = append(hosts, copyHostInfo(hostinfo, pr))
|
||||||
})
|
})
|
||||||
return hosts
|
return hosts
|
||||||
|
|||||||
@@ -0,0 +1,292 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
+187
-46
@@ -1,23 +1,27 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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) {
|
||||||
|
//TODO: CERT-V2 with multiple certificate versions we have a problem with this test
|
||||||
|
// Some certs versions have different characteristics and each version implements their own Copy() func
|
||||||
|
// which means this is not a good place to test for exposing memory
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
// Special care must be taken to re-use all objects provided to the hostmap and certificate in the expectedInfo object
|
// Special care must be taken to re-use all objects provided to the hostmap and certificate in the expectedInfo object
|
||||||
// To properly ensure we are not exposing core memory to the caller
|
// To properly ensure we are not exposing core memory to the caller
|
||||||
hm := newHostMap(l, netip.Prefix{})
|
hm := newHostMap(l)
|
||||||
hm.preferredRanges.Store(&[]netip.Prefix{})
|
hm.preferredRanges.Store(&[]netip.Prefix{})
|
||||||
|
|
||||||
remote1 := netip.MustParseAddrPort("0.0.0.100:4444")
|
remote1 := netip.MustParseAddrPort("0.0.0.100:4444")
|
||||||
@@ -33,75 +37,63 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
|||||||
Mask: net.IPMask{255, 255, 255, 0},
|
Mask: net.IPMask{255, 255, 255, 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
crt := &cert.NebulaCertificate{
|
remotes := NewRemoteList([]netip.Addr{netip.IPv4Unspecified()}, nil)
|
||||||
Details: cert.NebulaCertificateDetails{
|
remotes.unlockedPrependV4(netip.IPv4Unspecified(), netAddrToProtoV4AddrPort(remote1.Addr(), remote1.Port()))
|
||||||
Name: "test",
|
remotes.unlockedPrependV6(netip.IPv4Unspecified(), netAddrToProtoV6AddrPort(remote2.Addr(), remote2.Port()))
|
||||||
Ips: []*net.IPNet{&ipNet},
|
|
||||||
Subnets: []*net.IPNet{},
|
|
||||||
Groups: []string{"default-group"},
|
|
||||||
NotBefore: time.Unix(1, 0),
|
|
||||||
NotAfter: time.Unix(2, 0),
|
|
||||||
PublicKey: []byte{5, 6, 7, 8},
|
|
||||||
IsCA: false,
|
|
||||||
Issuer: "the-issuer",
|
|
||||||
InvertedGroups: map[string]struct{}{"default-group": {}},
|
|
||||||
},
|
|
||||||
Signature: []byte{1, 2, 1, 2, 1, 3},
|
|
||||||
}
|
|
||||||
|
|
||||||
remotes := NewRemoteList(nil)
|
|
||||||
remotes.unlockedPrependV4(netip.IPv4Unspecified(), NewIp4AndPortFromNetIP(remote1.Addr(), remote1.Port()))
|
|
||||||
remotes.unlockedPrependV6(netip.IPv4Unspecified(), NewIp6AndPortFromNetIP(remote2.Addr(), remote2.Port()))
|
|
||||||
|
|
||||||
vpnIp, ok := netip.AddrFromSlice(ipNet.IP)
|
vpnIp, ok := netip.AddrFromSlice(ipNet.IP)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
hm.unlockedAddHostInfo(&HostInfo{
|
crt := &dummyCert{}
|
||||||
remote: remote1,
|
hi := &HostInfo{
|
||||||
remotes: remotes,
|
remotes: remotes,
|
||||||
ConnectionState: &ConnectionState{
|
ConnectionState: &ConnectionState{
|
||||||
peerCert: crt,
|
peerCert: &cert.CachedCertificate{Certificate: crt},
|
||||||
},
|
},
|
||||||
remoteIndexId: 200,
|
remoteIndexId: 200,
|
||||||
localIndexId: 201,
|
localIndexId: 201,
|
||||||
vpnIp: vpnIp,
|
vpnAddrs: []netip.Addr{vpnIp},
|
||||||
relayState: RelayState{
|
relayState: RelayState{
|
||||||
relays: nil,
|
relays: nil,
|
||||||
relayForByIp: 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)
|
||||||
|
|
||||||
hm.unlockedAddHostInfo(&HostInfo{
|
hi2 := &HostInfo{
|
||||||
remote: remote1,
|
|
||||||
remotes: remotes,
|
remotes: remotes,
|
||||||
ConnectionState: &ConnectionState{
|
ConnectionState: &ConnectionState{
|
||||||
peerCert: nil,
|
peerCert: nil,
|
||||||
},
|
},
|
||||||
remoteIndexId: 200,
|
remoteIndexId: 200,
|
||||||
localIndexId: 201,
|
localIndexId: 201,
|
||||||
vpnIp: vpnIp2,
|
vpnAddrs: []netip.Addr{vpnIp2},
|
||||||
relayState: RelayState{
|
relayState: RelayState{
|
||||||
relays: nil,
|
relays: nil,
|
||||||
relayForByIp: 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: logrus.New(),
|
l: test.NewLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
thi := c.GetHostInfoByVpnIp(vpnIp, false)
|
thi := c.GetHostInfoByVpnAddr(vpnIp, false)
|
||||||
|
|
||||||
expectedInfo := ControlHostInfo{
|
expectedInfo := ControlHostInfo{
|
||||||
VpnIp: vpnIp,
|
VpnAddrs: []netip.Addr{vpnIp},
|
||||||
LocalIndex: 201,
|
LocalIndex: 201,
|
||||||
RemoteIndex: 200,
|
RemoteIndex: 200,
|
||||||
RemoteAddrs: []netip.AddrPort{remote2, remote1},
|
RemoteAddrs: []netip.AddrPort{remote2, remote1},
|
||||||
@@ -113,18 +105,17 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't have any unexpected fields
|
// Make sure we don't have any unexpected fields
|
||||||
assertFields(t, []string{"VpnIp", "LocalIndex", "RemoteIndex", "RemoteAddrs", "Cert", "MessageCounter", "CurrentRemote", "CurrentRelaysToMe", "CurrentRelaysThroughMe"}, thi)
|
assertFields(t, []string{"VpnAddrs", "LocalIndex", "RemoteIndex", "RemoteAddrs", "Cert", "MessageCounter", "CurrentRemote", "CurrentRelaysToMe", "CurrentRelaysThroughMe"}, thi)
|
||||||
assert.EqualValues(t, &expectedInfo, thi)
|
assert.Equal(t, &expectedInfo, thi)
|
||||||
//TODO: netip.Addr reuses global memory for zone identifiers which breaks our "no reused memory check" here
|
test.AssertDeepCopyEqual(t, &expectedInfo, thi)
|
||||||
//test.AssertDeepCopyEqual(t, &expectedInfo, thi)
|
|
||||||
|
|
||||||
// Make sure we don't panic if the host info doesn't have a cert yet
|
// Make sure we don't panic if the host info doesn't have a cert yet
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
thi = c.GetHostInfoByVpnIp(vpnIp2, false)
|
thi = c.GetHostInfoByVpnAddr(vpnIp2, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertFields(t *testing.T, expected []string, actualStruct interface{}) {
|
func assertFields(t *testing.T, expected []string, actualStruct any) {
|
||||||
val := reflect.ValueOf(actualStruct).Elem()
|
val := reflect.ValueOf(actualStruct).Elem()
|
||||||
fields := make([]string, val.NumField())
|
fields := make([]string, val.NumField())
|
||||||
for i := 0; i < val.NumField(); i++ {
|
for i := 0; i < val.NumField(); i++ {
|
||||||
@@ -133,3 +124,153 @@ func assertFields(t *testing.T, expected []string, actualStruct interface{}) {
|
|||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+47
-51
@@ -1,15 +1,10 @@
|
|||||||
//go:build e2e_testing
|
//go:build e2e_testing
|
||||||
// +build e2e_testing
|
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
|
||||||
|
|
||||||
"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"
|
||||||
@@ -25,7 +20,9 @@ func (c *Control) WaitForType(msgType header.MessageType, subType header.Message
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
pipeTo.InjectUDPPacket(p)
|
pipeTo.InjectUDPPacket(p)
|
||||||
if h.Type == msgType && h.Subtype == subType {
|
match := h.Type == msgType && h.Subtype == subType
|
||||||
|
p.Release()
|
||||||
|
if match {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +38,9 @@ func (c *Control) WaitForTypeByIndex(toIndex uint32, msgType header.MessageType,
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
pipeTo.InjectUDPPacket(p)
|
pipeTo.InjectUDPPacket(p)
|
||||||
if h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType {
|
match := h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType
|
||||||
|
p.Release()
|
||||||
|
if match {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,15 +50,15 @@ func (c *Control) WaitForTypeByIndex(toIndex uint32, msgType header.MessageType,
|
|||||||
// This is necessary if you did not configure static hosts or are not running a lighthouse
|
// This is necessary if you did not configure static hosts or are not running a lighthouse
|
||||||
func (c *Control) InjectLightHouseAddr(vpnIp netip.Addr, toAddr netip.AddrPort) {
|
func (c *Control) InjectLightHouseAddr(vpnIp netip.Addr, toAddr netip.AddrPort) {
|
||||||
c.f.lightHouse.Lock()
|
c.f.lightHouse.Lock()
|
||||||
remoteList := c.f.lightHouse.unlockedGetRemoteList(vpnIp)
|
remoteList := c.f.lightHouse.unlockedGetRemoteList([]netip.Addr{vpnIp})
|
||||||
remoteList.Lock()
|
remoteList.Lock()
|
||||||
defer remoteList.Unlock()
|
defer remoteList.Unlock()
|
||||||
c.f.lightHouse.Unlock()
|
c.f.lightHouse.Unlock()
|
||||||
|
|
||||||
if toAddr.Addr().Is4() {
|
if toAddr.Addr().Is4() {
|
||||||
remoteList.unlockedPrependV4(vpnIp, NewIp4AndPortFromNetIP(toAddr.Addr(), toAddr.Port()))
|
remoteList.unlockedPrependV4(vpnIp, netAddrToProtoV4AddrPort(toAddr.Addr(), toAddr.Port()))
|
||||||
} else {
|
} else {
|
||||||
remoteList.unlockedPrependV6(vpnIp, NewIp6AndPortFromNetIP(toAddr.Addr(), toAddr.Port()))
|
remoteList.unlockedPrependV6(vpnIp, netAddrToProtoV6AddrPort(toAddr.Addr(), toAddr.Port()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +66,12 @@ func (c *Control) InjectLightHouseAddr(vpnIp netip.Addr, toAddr netip.AddrPort)
|
|||||||
// This is necessary to inform an initiator of possible relays for communicating with a responder
|
// This is necessary to inform an initiator of possible relays for communicating with a responder
|
||||||
func (c *Control) InjectRelays(vpnIp netip.Addr, relayVpnIps []netip.Addr) {
|
func (c *Control) InjectRelays(vpnIp netip.Addr, relayVpnIps []netip.Addr) {
|
||||||
c.f.lightHouse.Lock()
|
c.f.lightHouse.Lock()
|
||||||
remoteList := c.f.lightHouse.unlockedGetRemoteList(vpnIp)
|
remoteList := c.f.lightHouse.unlockedGetRemoteList([]netip.Addr{vpnIp})
|
||||||
remoteList.Lock()
|
remoteList.Lock()
|
||||||
defer remoteList.Unlock()
|
defer remoteList.Unlock()
|
||||||
c.f.lightHouse.Unlock()
|
c.f.lightHouse.Unlock()
|
||||||
|
|
||||||
remoteList.unlockedSetRelay(vpnIp, vpnIp, relayVpnIps)
|
remoteList.unlockedSetRelay(vpnIp, relayVpnIps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFromTun will pull a packet off the tun side of nebula
|
// GetFromTun will pull a packet off the tun side of nebula
|
||||||
@@ -93,54 +92,39 @@ func (c *Control) GetTunTxChan() <-chan []byte {
|
|||||||
return c.f.inside.(*overlay.TestTun).TxPackets
|
return c.f.inside.(*overlay.TestTun).TxPackets
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectUDPPacket will inject a packet into the udp side of nebula
|
// InjectUDPPacket injects a packet into the udp side. We copy internally so the caller keeps ownership of p.
|
||||||
|
// The copy comes from the freelist so steady-state alloc is zero.
|
||||||
func (c *Control) InjectUDPPacket(p *udp.Packet) {
|
func (c *Control) InjectUDPPacket(p *udp.Packet) {
|
||||||
c.f.outside.(*udp.TesterConn).Send(p)
|
c.f.outside.(*udp.TesterConn).Send(p.Copy())
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectTunUDPPacket puts a udp packet on the tun interface. Using UDP here because it's a simpler protocol
|
// InjectTunPacket pushes an IP packet onto the tun interface.
|
||||||
func (c *Control) InjectTunUDPPacket(toIp netip.Addr, toPort uint16, fromPort uint16, data []byte) {
|
func (c *Control) InjectTunPacket(packet []byte) {
|
||||||
//TODO: IPV6-WORK
|
c.f.inside.(*overlay.TestTun).Send(packet)
|
||||||
ip := layers.IPv4{
|
|
||||||
Version: 4,
|
|
||||||
TTL: 64,
|
|
||||||
Protocol: layers.IPProtocolUDP,
|
|
||||||
SrcIP: c.f.inside.Cidr().Addr().Unmap().AsSlice(),
|
|
||||||
DstIP: toIp.Unmap().AsSlice(),
|
|
||||||
}
|
|
||||||
|
|
||||||
udp := layers.UDP{
|
|
||||||
SrcPort: layers.UDPPort(fromPort),
|
|
||||||
DstPort: layers.UDPPort(toPort),
|
|
||||||
}
|
|
||||||
err := udp.SetNetworkLayerForChecksum(&ip)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := gopacket.NewSerializeBuffer()
|
|
||||||
opt := gopacket.SerializeOptions{
|
|
||||||
ComputeChecksums: true,
|
|
||||||
FixLengths: true,
|
|
||||||
}
|
|
||||||
err = gopacket.SerializeLayers(buffer, opt, &ip, &udp, gopacket.Payload(data))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.f.inside.(*overlay.TestTun).Send(buffer.Bytes())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) GetVpnIp() netip.Addr {
|
func (c *Control) GetVpnAddrs() []netip.Addr {
|
||||||
return c.f.myVpnNet.Addr()
|
return c.f.myVpnAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) GetUDPAddr() netip.AddrPort {
|
func (c *Control) GetUDPAddr() netip.AddrPort {
|
||||||
return c.f.outside.(*udp.TesterConn).Addr
|
return c.f.outside.(*udp.TesterConn).GetAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUDPAddr moves this node to a new underlay address, standing in for a laptop waking up on a different
|
||||||
|
// network. Register the new address with the router as well or nothing will route back.
|
||||||
|
func (c *Control) SetUDPAddr(addr netip.AddrPort) {
|
||||||
|
c.f.outside.(*udp.TesterConn).SetAddr(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocalAddrsFn replaces underlay address discovery so a test can advertise its simulated address instead of
|
||||||
|
// whatever this machine's NICs happen to be. Call it before Start, SendUpdate reads it from the update worker.
|
||||||
|
func (c *Control) SetLocalAddrsFn(fn func(*LocalAllowList) []netip.Addr) {
|
||||||
|
c.f.lightHouse.localAddrsFn = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
||||||
hostinfo := c.f.handshakeManager.QueryVpnIp(vpnIp)
|
hostinfo := c.f.handshakeManager.QueryVpnAddr(vpnIp)
|
||||||
if hostinfo == nil {
|
if hostinfo == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -153,8 +137,20 @@ func (c *Control) GetHostmap() *HostMap {
|
|||||||
return c.f.hostMap
|
return c.f.hostMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) GetCert() *cert.NebulaCertificate {
|
// GetHostmapIndexCount returns the number of entries in the main hostmap Indexes table, holding
|
||||||
return c.f.pki.GetCertState().Certificate
|
// the hostmap read lock so tests can poll it while connection manager churns tunnels.
|
||||||
|
func (c *Control) GetHostmapIndexCount() int {
|
||||||
|
c.f.hostMap.RLock()
|
||||||
|
defer c.f.hostMap.RUnlock()
|
||||||
|
return len(c.f.hostMap.Indexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Control) GetF() *Interface {
|
||||||
|
return c.f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Control) GetCertState() *CertState {
|
||||||
|
return c.f.pki.getCertState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Control) ReHandshake(vpnIp netip.Addr) {
|
func (c *Control) ReHandshake(vpnIp netip.Addr) {
|
||||||
|
|||||||
Vendored
+9
-15
@@ -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 = bit32.band(tvbuf:range(0,1):uint(), 0x0F)
|
local nebula_type = bit.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,30 +84,24 @@ 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 our old dissector
|
-- Remove all existing registrations
|
||||||
DissectorTable.get("udp.port"):remove_all(nebula)
|
DissectorTable.get("udp.port"):remove_all(nebula)
|
||||||
|
|
||||||
if nebula.prefs.all_ports and default_settings.all_ports ~= nebula.prefs.all_ports then
|
if nebula.prefs.all_ports then
|
||||||
default_settings.all_port = nebula.prefs.all_ports
|
-- Register on every port for hole punch capture
|
||||||
|
|
||||||
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
|
||||||
-- no need to establish again on specific ports
|
-- Register on the configured port only
|
||||||
return
|
DissectorTable.get("udp.port"):add(nebula.prefs.port, nebula)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
default_settings.all_ports = nebula.prefs.all_ports
|
||||||
if default_settings.all_ports ~= nebula.prefs.all_ports then
|
default_settings.port = nebula.prefs.port
|
||||||
-- 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)
|
||||||
|
|||||||
+369
-81
@@ -1,53 +1,278 @@
|
|||||||
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/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This whole thing should be rewritten to use context
|
type dnsServer struct {
|
||||||
|
|
||||||
var dnsR *dnsRecords
|
|
||||||
var dnsServer *dns.Server
|
|
||||||
var dnsAddr string
|
|
||||||
|
|
||||||
type dnsRecords struct {
|
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
dnsMap map[string]string
|
l *slog.Logger
|
||||||
|
ctx context.Context
|
||||||
|
dnsMap4 map[string]netip.Addr
|
||||||
|
dnsMap6 map[string]netip.Addr
|
||||||
hostMap *HostMap
|
hostMap *HostMap
|
||||||
|
pki *PKI
|
||||||
|
|
||||||
|
// selfHost is the cached FQDN we last seeded for ourselves
|
||||||
|
selfHost string
|
||||||
|
|
||||||
|
mux *dns.ServeMux
|
||||||
|
|
||||||
|
// enabled mirrors `lighthouse.serve_dns && lighthouse.am_lighthouse`.
|
||||||
|
// Start, Add, and reload consult it so callers don't need to know the
|
||||||
|
// gating rules. When it toggles off via reload, accumulated records are
|
||||||
|
// cleared so a later re-enable starts with a fresh map populated from
|
||||||
|
// new handshakes.
|
||||||
|
enabled atomic.Bool
|
||||||
|
|
||||||
|
serverMu sync.Mutex
|
||||||
|
server *dns.Server
|
||||||
|
// started is closed once `server` has finished binding (or after
|
||||||
|
// ListenAndServe returns on a bind failure). Stop waits on it before
|
||||||
|
// calling Shutdown to avoid the miekg/dns "server not started" race
|
||||||
|
// where a Shutdown that arrives before bind completes is silently
|
||||||
|
// ignored, leaving the listener running forever.
|
||||||
|
started chan struct{}
|
||||||
|
addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDnsRecords(hostMap *HostMap) *dnsRecords {
|
// newDnsServerFromConfig builds a dnsServer, applies the initial config, and
|
||||||
return &dnsRecords{
|
// registers a reload callback. The reload callback is registered before the
|
||||||
dnsMap: make(map[string]string),
|
// initial config is applied, so a SIGHUP can later enable, fix, or disable
|
||||||
|
// DNS even if the initial application failed.
|
||||||
|
//
|
||||||
|
// The dnsServer internally gates on `lighthouse.serve_dns &&
|
||||||
|
// lighthouse.am_lighthouse`. Start and Add are safe to call unconditionally,
|
||||||
|
// they no-op when DNS isn't enabled. Each Start invocation owns a ctx-cancel
|
||||||
|
// watcher that tears the listener down on nebula shutdown. The returned
|
||||||
|
// pointer is always non-nil, even on error.
|
||||||
|
func newDnsServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI, hostMap *HostMap, c *config.C) (*dnsServer, error) {
|
||||||
|
ds := &dnsServer{
|
||||||
|
l: l,
|
||||||
|
ctx: ctx,
|
||||||
|
dnsMap4: make(map[string]netip.Addr),
|
||||||
|
dnsMap6: make(map[string]netip.Addr),
|
||||||
hostMap: hostMap,
|
hostMap: hostMap,
|
||||||
|
pki: pki,
|
||||||
|
}
|
||||||
|
ds.mux = dns.NewServeMux()
|
||||||
|
ds.mux.HandleFunc(".", ds.handleDnsRequest)
|
||||||
|
|
||||||
|
c.RegisterReloadCallback(func(c *config.C) {
|
||||||
|
if err := ds.reload(c, false); err != nil {
|
||||||
|
ds.l.Error("Failed to reload DNS responder from config", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := ds.reload(c, true); err != nil {
|
||||||
|
return ds, err
|
||||||
|
}
|
||||||
|
ds.seedSelf()
|
||||||
|
return ds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload applies the latest config and reconciles the running state with it:
|
||||||
|
// - enabled toggled on -> spawn a runner
|
||||||
|
// - enabled toggled off -> stop the runner
|
||||||
|
// - listen address changed (while running) -> restart on the new address
|
||||||
|
// - everything else -> no-op
|
||||||
|
//
|
||||||
|
// On the initial call it only records configuration; Control.Start is what
|
||||||
|
// launches the first runner via dnsStart.
|
||||||
|
func (d *dnsServer) reload(c *config.C, initial bool) error {
|
||||||
|
wantsDns := c.GetBool("lighthouse.serve_dns", false)
|
||||||
|
amLighthouse := c.GetBool("lighthouse.am_lighthouse", false)
|
||||||
|
enabled := wantsDns && amLighthouse
|
||||||
|
newAddr := getDnsServerAddr(c)
|
||||||
|
|
||||||
|
d.serverMu.Lock()
|
||||||
|
running := d.server != nil
|
||||||
|
sameAddr := d.addr == newAddr
|
||||||
|
d.addr = newAddr
|
||||||
|
d.enabled.Store(enabled)
|
||||||
|
d.serverMu.Unlock()
|
||||||
|
|
||||||
|
if initial {
|
||||||
|
if wantsDns && !amLighthouse {
|
||||||
|
d.l.Warn("DNS server refusing to run because this host is not a lighthouse.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
if running {
|
||||||
|
d.Stop()
|
||||||
|
}
|
||||||
|
// Drop any records that accumulated while enabled; a later re-enable
|
||||||
|
// will repopulate from fresh handshakes and a fresh seedSelf.
|
||||||
|
d.clearRecords()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !running {
|
||||||
|
// Was disabled (or never started); bring it up now.
|
||||||
|
go d.Start()
|
||||||
|
} else if !sameAddr {
|
||||||
|
// Stop clears the slot before shutting down, otherwise the Start below can find the dying server and refuse
|
||||||
|
d.Stop()
|
||||||
|
go d.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the self entry every enabled reload so cert renewals that change our name or VPN addresses are picked up.
|
||||||
|
d.seedSelf()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdownServer waits for the server to finish binding (so Shutdown actually
|
||||||
|
// stops it rather than no-oping) and then shuts it down.
|
||||||
|
func (d *dnsServer) shutdownServer(srv *dns.Server, started chan struct{}, reason string) {
|
||||||
|
if srv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if started != nil {
|
||||||
|
<-started
|
||||||
|
}
|
||||||
|
if err := srv.Shutdown(); err != nil {
|
||||||
|
d.l.Warn("Failed to shut down the DNS responder", "reason", reason, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dnsRecords) Query(data string) string {
|
// Start binds and serves the DNS responder. Blocks until Stop is called or
|
||||||
|
// the listener errors. Safe to call when DNS is disabled (returns
|
||||||
|
// immediately). This is what Control.dnsStart points at.
|
||||||
|
//
|
||||||
|
// Must be invoked after the tun device is active so that lighthouse.dns.host
|
||||||
|
// may bind to a nebula IP.
|
||||||
|
func (d *dnsServer) Start() {
|
||||||
|
if !d.enabled.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
started := make(chan struct{})
|
||||||
|
d.serverMu.Lock()
|
||||||
|
// Re-check enabled under the lock, a disable that raced our check above snapshots the slot under it too.
|
||||||
|
// Two reloads in quick succession can both spawn a Start, the loser would orphan the live listener past Stop
|
||||||
|
if d.ctx.Err() != nil || d.server != nil || !d.enabled.Load() {
|
||||||
|
d.serverMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr := d.addr
|
||||||
|
server := &dns.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Net: "udp",
|
||||||
|
Handler: d.mux,
|
||||||
|
NotifyStartedFunc: func() { close(started) },
|
||||||
|
}
|
||||||
|
d.server = server
|
||||||
|
d.started = started
|
||||||
|
d.serverMu.Unlock()
|
||||||
|
|
||||||
|
// Per-invocation ctx watcher. Exits when Start does, so we don't leak a
|
||||||
|
// watcher per reload-driven restart.
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-d.ctx.Done():
|
||||||
|
d.shutdownServer(server, started, "shutdown")
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
d.l.Info("Starting DNS responder", "dnsListener", addr)
|
||||||
|
err := server.ListenAndServe()
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
// If the listener never bound (bind error) NotifyStartedFunc never fires,
|
||||||
|
// so close started here to release any Stop caller waiting on it.
|
||||||
|
select {
|
||||||
|
case <-started:
|
||||||
|
default:
|
||||||
|
close(started)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release our slot, unless a reload already replaced us, so a dead listener can't block a future Start
|
||||||
|
d.serverMu.Lock()
|
||||||
|
if d.server == server {
|
||||||
|
d.server = nil
|
||||||
|
d.started = nil
|
||||||
|
}
|
||||||
|
d.serverMu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
d.l.Warn("Failed to run the DNS responder", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop shuts down the active server, if any. Idempotent.
|
||||||
|
func (d *dnsServer) Stop() {
|
||||||
|
d.serverMu.Lock()
|
||||||
|
srv := d.server
|
||||||
|
started := d.started
|
||||||
|
d.server = nil
|
||||||
|
d.started = nil
|
||||||
|
d.serverMu.Unlock()
|
||||||
|
d.shutdownServer(srv, started, "stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns the address for the given name and query type. The second
|
||||||
|
// return value reports whether the name is known at all (in either A or AAAA),
|
||||||
|
// which lets callers distinguish NODATA from NXDOMAIN.
|
||||||
|
func (d *dnsServer) Query(q uint16, data string) (netip.Addr, bool) {
|
||||||
|
data = strings.ToLower(data)
|
||||||
d.RLock()
|
d.RLock()
|
||||||
defer d.RUnlock()
|
defer d.RUnlock()
|
||||||
if r, ok := d.dnsMap[strings.ToLower(data)]; ok {
|
addr4, haveV4 := d.dnsMap4[data]
|
||||||
return r
|
addr6, haveV6 := d.dnsMap6[data]
|
||||||
|
nameExists := haveV4 || haveV6
|
||||||
|
switch q {
|
||||||
|
case dns.TypeA:
|
||||||
|
if haveV4 {
|
||||||
|
return addr4, nameExists
|
||||||
|
}
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
if haveV6 {
|
||||||
|
return addr6, nameExists
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
return netip.Addr{}, nameExists
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dnsRecords) QueryCert(data string) string {
|
func (d *dnsServer) 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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
hostinfo := d.hostMap.QueryVpnIp(ip)
|
// The hostmap only ever contains peers we have handshaked with, so it never carries an entry for ourselves.
|
||||||
|
// Answer self lookups straight from the local cert state.
|
||||||
|
if cs := d.certState(); cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
|
||||||
|
c := cs.GetDefaultCertificate()
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b, err := c.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostinfo := d.hostMap.QueryVpnAddr(ip)
|
||||||
if hostinfo == nil {
|
if hostinfo == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -57,43 +282,145 @@ func (d *dnsRecords) QueryCert(data string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
cert := q.Details
|
b, err := q.Certificate.MarshalJSON()
|
||||||
c := fmt.Sprintf("\"Name: %s\" \"Ips: %s\" \"Subnets %s\" \"Groups %s\" \"NotBefore %s\" \"NotAfter %s\" \"PublicKey %x\" \"IsCA %t\" \"Issuer %s\"", cert.Name, cert.Ips, cert.Subnets, cert.Groups, cert.NotBefore, cert.NotAfter, cert.PublicKey, cert.IsCA, cert.Issuer)
|
if err != nil {
|
||||||
return c
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dnsRecords) Add(host, data string) {
|
// clearRecords drops all DNS records, including the self entry.
|
||||||
|
func (d *dnsServer) clearRecords() {
|
||||||
d.Lock()
|
d.Lock()
|
||||||
defer d.Unlock()
|
defer d.Unlock()
|
||||||
d.dnsMap[strings.ToLower(host)] = data
|
clear(d.dnsMap4)
|
||||||
|
clear(d.dnsMap6)
|
||||||
|
d.selfHost = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseQuery(l *logrus.Logger, m *dns.Msg, w dns.ResponseWriter) {
|
// seedSelf inserts (or refreshes) a record for our own cert name pointing at our VPN addresses,
|
||||||
|
// so a single-lighthouse network can resolve the lighthouse's own hostname without the two-process workaround.
|
||||||
|
func (d *dnsServer) seedSelf() {
|
||||||
|
if !d.enabled.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cs := d.certState()
|
||||||
|
if cs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := cs.GetDefaultCertificate()
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newHost := strings.ToLower(c.Name()) + "."
|
||||||
|
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
if d.selfHost != "" && d.selfHost != newHost {
|
||||||
|
delete(d.dnsMap4, d.selfHost)
|
||||||
|
delete(d.dnsMap6, d.selfHost)
|
||||||
|
}
|
||||||
|
d.selfHost = newHost
|
||||||
|
delete(d.dnsMap4, newHost)
|
||||||
|
delete(d.dnsMap6, newHost)
|
||||||
|
haveV4, haveV6 := false, false
|
||||||
|
for _, addr := range cs.myVpnAddrs {
|
||||||
|
if addr.Is4() && !haveV4 {
|
||||||
|
d.dnsMap4[newHost] = addr
|
||||||
|
haveV4 = true
|
||||||
|
} else if addr.Is6() && !haveV6 {
|
||||||
|
d.dnsMap6[newHost] = addr
|
||||||
|
haveV6 = true
|
||||||
|
}
|
||||||
|
if haveV4 && haveV6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dnsServer) certState() *CertState {
|
||||||
|
if d.pki == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.pki.getCertState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds the first IPv4 and IPv6 address that appears in `addresses` as the record for `host`
|
||||||
|
func (d *dnsServer) Add(host string, addresses []netip.Addr) {
|
||||||
|
if !d.enabled.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
haveV4 := false
|
||||||
|
haveV6 := false
|
||||||
|
for _, addr := range addresses {
|
||||||
|
if addr.Is4() && !haveV4 {
|
||||||
|
d.dnsMap4[host] = addr
|
||||||
|
haveV4 = true
|
||||||
|
} else if addr.Is6() && !haveV6 {
|
||||||
|
d.dnsMap6[host] = addr
|
||||||
|
haveV6 = true
|
||||||
|
}
|
||||||
|
if haveV4 && haveV6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool {
|
||||||
|
a, _, _ := net.SplitHostPort(addr)
|
||||||
|
b, err := netip.ParseAddr(a)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.IsLoopback() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := d.certState()
|
||||||
|
if cs == nil || cs.myVpnAddrsTable == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//if we found it in this table, it's good
|
||||||
|
return cs.myVpnAddrsTable.Contains(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
|
||||||
|
debugEnabled := d.l.Enabled(context.Background(), slog.LevelDebug)
|
||||||
|
// Per RFC 2308 §2.2, a name that exists but has no record of the requested
|
||||||
|
// type must be answered with NOERROR and an empty answer section (NODATA),
|
||||||
|
// not NXDOMAIN (RFC 2308 §2.1), which is reserved for names that do not
|
||||||
|
// exist at all.
|
||||||
|
anyNameExists := false
|
||||||
for _, q := range m.Question {
|
for _, q := range m.Question {
|
||||||
switch q.Qtype {
|
switch q.Qtype {
|
||||||
case dns.TypeA:
|
case dns.TypeA, dns.TypeAAAA:
|
||||||
l.Debugf("Query for A %s", q.Name)
|
qType := dns.TypeToString[q.Qtype]
|
||||||
ip := dnsR.Query(q.Name)
|
if debugEnabled {
|
||||||
if ip != "" {
|
d.l.Debug("DNS query", "type", qType, "name", q.Name)
|
||||||
rr, err := dns.NewRR(fmt.Sprintf("%s A %s", q.Name, ip))
|
}
|
||||||
|
ip, nameExists := d.Query(q.Qtype, q.Name)
|
||||||
|
if nameExists {
|
||||||
|
anyNameExists = true
|
||||||
|
}
|
||||||
|
if ip.IsValid() {
|
||||||
|
rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
m.Answer = append(m.Answer, rr)
|
m.Answer = append(m.Answer, rr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case dns.TypeTXT:
|
case dns.TypeTXT:
|
||||||
a, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
// We only answer these queries from nebula nodes or localhost
|
||||||
b, err := netip.ParseAddr(a)
|
if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if debugEnabled {
|
||||||
// We don't answer these queries from non nebula nodes or localhost
|
d.l.Debug("DNS query", "type", "TXT", "name", q.Name)
|
||||||
//l.Debugf("Does %s contain %s", b, dnsR.hostMap.vpnCIDR)
|
|
||||||
if !dnsR.hostMap.vpnCIDR.Contains(b) && a != "127.0.0.1" {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
l.Debugf("Query for TXT %s", q.Name)
|
ip := d.QueryCert(q.Name)
|
||||||
ip := dnsR.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))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -103,41 +430,24 @@ func parseQuery(l *logrus.Logger, m *dns.Msg, w dns.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.Answer) == 0 {
|
if len(m.Answer) == 0 && !anyNameExists {
|
||||||
m.Rcode = dns.RcodeNameError
|
m.Rcode = dns.RcodeNameError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDnsRequest(l *logrus.Logger, w dns.ResponseWriter, r *dns.Msg) {
|
func (d *dnsServer) 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
|
||||||
|
|
||||||
switch r.Opcode {
|
switch r.Opcode {
|
||||||
case dns.OpcodeQuery:
|
case dns.OpcodeQuery:
|
||||||
parseQuery(l, m, w)
|
d.parseQuery(m, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dnsMain(l *logrus.Logger, hostMap *HostMap, c *config.C) func() {
|
|
||||||
dnsR = newDnsRecords(hostMap)
|
|
||||||
|
|
||||||
// attach request handler func
|
|
||||||
dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
|
|
||||||
handleDnsRequest(l, w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -146,25 +456,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|||||||
+586
-13
@@ -1,46 +1,126 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"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/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) {
|
||||||
//TODO: This test is basically pointless
|
l := slog.New(slog.DiscardHandler)
|
||||||
hostMap := &HostMap{}
|
hostMap := &HostMap{}
|
||||||
ds := newDnsRecords(hostMap)
|
ds := &dnsServer{
|
||||||
ds.Add("test.com.com", "1.2.3.4")
|
l: l,
|
||||||
|
dnsMap4: make(map[string]netip.Addr),
|
||||||
|
dnsMap6: make(map[string]netip.Addr),
|
||||||
|
hostMap: hostMap,
|
||||||
|
}
|
||||||
|
ds.enabled.Store(true)
|
||||||
|
addrs := []netip.Addr{
|
||||||
|
netip.MustParseAddr("1.2.3.4"),
|
||||||
|
netip.MustParseAddr("1.2.3.5"),
|
||||||
|
netip.MustParseAddr("fd01::24"),
|
||||||
|
netip.MustParseAddr("fd01::25"),
|
||||||
|
}
|
||||||
|
ds.Add("test.com.com", addrs)
|
||||||
|
ds.Add("v4only.com.com", []netip.Addr{netip.MustParseAddr("1.2.3.6")})
|
||||||
|
ds.Add("v6only.com.com", []netip.Addr{netip.MustParseAddr("fd01::26")})
|
||||||
|
|
||||||
m := new(dns.Msg)
|
m := &dns.Msg{}
|
||||||
m.SetQuestion("test.com.com", dns.TypeA)
|
m.SetQuestion("test.com.com", dns.TypeA)
|
||||||
|
ds.parseQuery(m, nil)
|
||||||
|
assert.NotNil(t, m.Answer)
|
||||||
|
assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String())
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||||
|
|
||||||
//parseQuery(m)
|
m = &dns.Msg{}
|
||||||
|
m.SetQuestion("test.com.com", dns.TypeAAAA)
|
||||||
|
ds.parseQuery(m, nil)
|
||||||
|
assert.NotNil(t, m.Answer)
|
||||||
|
assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String())
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||||
|
|
||||||
|
// A known name with no record of the requested type should return NODATA
|
||||||
|
// (NOERROR with empty answer), not NXDOMAIN.
|
||||||
|
m = &dns.Msg{}
|
||||||
|
m.SetQuestion("v4only.com.com", dns.TypeAAAA)
|
||||||
|
ds.parseQuery(m, nil)
|
||||||
|
assert.Empty(t, m.Answer)
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||||
|
|
||||||
|
m = &dns.Msg{}
|
||||||
|
m.SetQuestion("v6only.com.com", dns.TypeA)
|
||||||
|
ds.parseQuery(m, nil)
|
||||||
|
assert.Empty(t, m.Answer)
|
||||||
|
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
|
||||||
|
|
||||||
|
// An unknown name should still return NXDOMAIN.
|
||||||
|
m = &dns.Msg{}
|
||||||
|
m.SetQuestion("unknown.com.com", dns.TypeA)
|
||||||
|
ds.parseQuery(m, nil)
|
||||||
|
assert.Empty(t, m.Answer)
|
||||||
|
assert.Equal(t, dns.RcodeNameError, m.Rcode)
|
||||||
|
|
||||||
|
// short lookups should not fail
|
||||||
|
m = &dns.Msg{}
|
||||||
|
m.Question = []dns.Question{{Name: "", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}}
|
||||||
|
ds.parseQuery(m, stubDNSWriter{})
|
||||||
|
assert.Empty(t, m.Answer)
|
||||||
|
assert.Equal(t, dns.RcodeNameError, m.Rcode)
|
||||||
|
|
||||||
|
m = &dns.Msg{}
|
||||||
|
m.Question = []dns.Question{{Name: ".", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}}
|
||||||
|
ds.parseQuery(m, stubDNSWriter{})
|
||||||
|
assert.Empty(t, m.Answer)
|
||||||
|
assert.Equal(t, dns.RcodeNameError, m.Rcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_getDnsServerAddr(t *testing.T) {
|
func Test_getDnsServerAddr(t *testing.T) {
|
||||||
c := config.NewC(nil)
|
c := config.NewC(nil)
|
||||||
|
|
||||||
c.Settings["lighthouse"] = map[interface{}]interface{}{
|
c.Settings["lighthouse"] = map[string]any{
|
||||||
"dns": map[interface{}]interface{}{
|
"dns": map[string]any{
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": "1",
|
"port": "1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, "0.0.0.0:1", getDnsServerAddr(c))
|
assert.Equal(t, "0.0.0.0:1", getDnsServerAddr(c))
|
||||||
|
|
||||||
c.Settings["lighthouse"] = map[interface{}]interface{}{
|
c.Settings["lighthouse"] = map[string]any{
|
||||||
"dns": map[interface{}]interface{}{
|
"dns": map[string]any{
|
||||||
"host": "::",
|
"host": "::",
|
||||||
"port": "1",
|
"port": "1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.Equal(t, "[::]:1", getDnsServerAddr(c))
|
assert.Equal(t, "[::]:1", getDnsServerAddr(c))
|
||||||
|
|
||||||
c.Settings["lighthouse"] = map[interface{}]interface{}{
|
c.Settings["lighthouse"] = map[string]any{
|
||||||
"dns": map[interface{}]interface{}{
|
"dns": map[string]any{
|
||||||
"host": "[::]",
|
"host": "[::]",
|
||||||
"port": "1",
|
"port": "1",
|
||||||
},
|
},
|
||||||
@@ -48,11 +128,504 @@ func Test_getDnsServerAddr(t *testing.T) {
|
|||||||
assert.Equal(t, "[::]:1", getDnsServerAddr(c))
|
assert.Equal(t, "[::]:1", getDnsServerAddr(c))
|
||||||
|
|
||||||
// Make sure whitespace doesn't mess us up
|
// Make sure whitespace doesn't mess us up
|
||||||
c.Settings["lighthouse"] = map[interface{}]interface{}{
|
c.Settings["lighthouse"] = map[string]any{
|
||||||
"dns": map[interface{}]interface{}{
|
"dns": map[string]any{
|
||||||
"host": "[::] ",
|
"host": "[::] ",
|
||||||
"port": "1",
|
"port": "1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
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,6 +1,16 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//go:build e2e_testing
|
||||||
|
// +build e2e_testing
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula"
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"github.com/slackhq/nebula/cert_test"
|
||||||
|
"github.com/slackhq/nebula/e2e/router"
|
||||||
|
"github.com/slackhq/nebula/header"
|
||||||
|
"github.com/slackhq/nebula/udp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertTestRequestEchoed(t *testing.T, cipher string) {
|
||||||
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
|
over := m{"cipher": cipher}
|
||||||
|
a, aNet, aUdp, _ := newSimpleServer(cert.Version1, ca, caKey, "a", "10.128.0.1/24", over)
|
||||||
|
b, bNet, bUdp, _ := newSimpleServer(cert.Version1, ca, caKey, "b", "10.128.0.2/24", over)
|
||||||
|
|
||||||
|
a.InjectLightHouseAddr(bNet[0].Addr(), bUdp)
|
||||||
|
b.InjectLightHouseAddr(aNet[0].Addr(), aUdp)
|
||||||
|
a.Start()
|
||||||
|
b.Start()
|
||||||
|
t.Cleanup(func() { a.Stop(); b.Stop() })
|
||||||
|
r := router.NewR(t, a, b)
|
||||||
|
defer r.RenderFlow()
|
||||||
|
|
||||||
|
assertTunnel(t, aNet[0].Addr(), bNet[0].Addr(), a, b, r)
|
||||||
|
drainUDPTx(a)
|
||||||
|
drainUDPTx(b)
|
||||||
|
|
||||||
|
payload := []byte("a test payload well over sixteen bytes long, wow it's so very long long long!")
|
||||||
|
require.Greater(t, len(payload), header.Len)
|
||||||
|
a.GetF().SendMessageToVpnAddr(header.Test, header.TestRequest, bNet[0].Addr(), payload, make([]byte, 12, 12), make([]byte, udp.MTU))
|
||||||
|
|
||||||
|
// Deliver A's request to B; B must echo a reply back
|
||||||
|
b.InjectUDPPacket(a.GetFromUDP(true))
|
||||||
|
reply := nextUDPTxOfType(t, b, header.Test, header.TestReply, 2*time.Second)
|
||||||
|
|
||||||
|
assert.Equal(t, aUdp, reply.To, "the reply must go back to the requester")
|
||||||
|
// header + echoed payload + 16-byte AEAD tag: proves the whole payload
|
||||||
|
// round-tripped rather than being dropped or truncated.
|
||||||
|
assert.Equal(t, header.Len+len(payload)+16, len(reply.Data), "the full payload must be echoed back")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTestRequestEchoesLongPayloadAES(t *testing.T) {
|
||||||
|
assertTestRequestEchoed(t, "aes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTestRequestEchoesLongPayloadChaChaPoly(t *testing.T) {
|
||||||
|
assertTestRequestEchoed(t, "chachapoly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// drainUDPTx empties a control's UDP tx queue without blocking.
|
||||||
|
func drainUDPTx(c *nebula.Control) {
|
||||||
|
for c.GetFromUDP(false) != nil {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextUDPTxOfType returns the next packet a control transmits whose nebula
|
||||||
|
// header matches (wantType, wantSub), skipping unrelated packets.
|
||||||
|
// It fails the test if none arrives within the timeout.
|
||||||
|
func nextUDPTxOfType(t *testing.T, c *nebula.Control, wantType header.MessageType, wantSub header.MessageSubType, within time.Duration) *udp.Packet {
|
||||||
|
t.Helper()
|
||||||
|
ch := c.GetUDPTxChan()
|
||||||
|
timeout := time.After(within)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case p := <-ch:
|
||||||
|
var h header.H
|
||||||
|
if err := h.Parse(p.Data); err == nil && h.Type == wantType && h.Subtype == wantSub {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("timed out waiting for a %v/%v packet on the udp tx queue", wantType, wantSub)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
//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.
|
||||||
+861
-289
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user