snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s

This commit is contained in:
upstream-mirror
2026-09-15 20:16:01 +05:00
commit 709385cbaa
455 changed files with 155508 additions and 0 deletions

1
.coveralls.yml Normal file
View File

@@ -0,0 +1 @@
service_name: travis-pro

5
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,5 @@
* @nats-io/server
*.go @nats-io/server-dev
doc/ @nats-io/server-dev
test/ @nats-io/server-dev
locksordering.txt @nats-io/server-dev

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
# NATS.io
custom: "https://crowdfunding.linuxfoundation.org/initiatives/nats-io"

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Discussion
url: https://github.com/nats-io/nats-server/discussions
about: Ideal for ideas, feedback, or longer form questions.
- name: Chat
url: https://slack.nats.io
about: Ideal for short, one-off questions, general conversation, and meeting other NATS users!

54
.github/ISSUE_TEMPLATE/defect.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
---
name: Defect
description: Report a defect, such as a bug or regression.
labels:
- defect
body:
- type: textarea
id: observed
attributes:
label: Observed behavior
description: Describe the unexpected behavior or performance regression you are observing.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Describe the expected behavior or performance characteristics.
validations:
required: true
- type: textarea
id: versions
attributes:
label: Server and client version
description: |-
**One of the first things we are likely to ask you is if the defect reproduces on the latest version, so please check that first!**
For the server, use `nats-server --version`, check the startup log output, or the image tag pulled from Docker.
For the CLI client, use `nats --version`.
For language-specific clients, check the version downloaded by the language dependency manager.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Host environment
description: |-
Specify any relevant details about the host environment the server and/or client was running in,
such as operating system, CPU architecture, container runtime, etc.
validations:
required: false
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Provide as many concrete steps to reproduce the defect. Please also include stream info and consumer info if this issue is JetStream-related.
validations:
required: false
- type: checkboxes
id: no-ai-meat-proxy
attributes:
label: Submission acknowledgment
options:
- label: I am a human being writing in my own words and not an AI agent. I will not use an AI agent to communicate on my behalf in this issue, either directly or via copy-paste.
required: true

35
.github/ISSUE_TEMPLATE/proposal.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Proposal
description: Propose an enhancement or new feature.
labels:
- proposal
body:
- type: textarea
id: change
attributes:
label: Proposed change
description: This could be a behavior change, enhanced API, or a new feature.
validations:
required: true
- type: textarea
id: usecase
attributes:
label: Use case
description: What is the use case or general motivation for this proposal?
validations:
required: true
- type: textarea
id: contribute
attributes:
label: Contribution
description: |-
Are you intending or interested in contributing code for this proposal if accepted?
validations:
required: false
- type: checkboxes
id: no-ai-meat-proxy
attributes:
label: Submission acknowledgment
options:
- label: I am a human being writing in my own words and not an AI agent. I will not use an AI agent to communicate on my behalf in this issue, either directly or via copy-paste.
required: true

1
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1 @@
<!-- Please make sure to read @CONTRIBUTING.md, then delete this notice and replace it with your PR description. Commit sign-offs certify that the contribution is your original work and that you license the work to the project under the Apache-2.0 license. We cannot accept contributions without it. We accept AI-assisted contributions but AI agents should not open pull requests directly. -->

View File

@@ -0,0 +1,47 @@
name: Nightly Docker Releaser
description: Builds nightly docker images
inputs:
hub_username:
description: Docker hub username
required: true
hub_password:
description: Docker hub password
required: true
name:
description: The name of the build
default: nightly
required: false
runs:
using: composite
steps:
- name: Log in to Docker Hub
shell: bash
run: docker login -u "${{ inputs.hub_username }}" -p "${{ inputs.hub_password }}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate build metadata
shell: sh
run: |
echo "BUILD_DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
echo "GIT_COMMIT=$(cd ${{ inputs.workdir }}/nats-server && git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "BUILD_NAME=$(echo ${{ inputs.name }} | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9.-]/-/g')" >> $GITHUB_ENV
- name: Build and push Docker images
uses: docker/build-push-action@v6
with:
context: "${{ github.workspace }}/src/github.com/nats-io"
file: "${{ github.workspace }}/src/github.com/nats-io/nats-server/docker/Dockerfile.nightly"
build-args: |
VERSION=nightly-${{ env.BUILD_DATE }}
GIT_COMMIT=${{ env.GIT_COMMIT }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
synadia/nats-server:${{ env.BUILD_NAME }}
synadia/nats-server:${{ env.BUILD_NAME }}-${{ env.BUILD_DATE }}

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

64
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Claude Code
# GITHUB_TOKEN needs contents:read and actions:read — required by
# claude-code-action for restoring trusted config files from the base branch.
# All other GitHub API access uses the App token.
permissions:
contents: read
actions: read
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_target:
types: [opened, reopened]
jobs:
claude:
name: Claude Review
uses: synadia-io/ai-workflows/.github/workflows/claude.yml@v2
if: contains(
fromJson('["OWNER","MEMBER","COLLABORATOR"]'),
github.event.comment.author_association || github.event.pull_request.author_association
)
with:
gh_app_id: ${{ vars.CLAUDE_GH_APP_ID }}
checkout_mode: "base"
# review_focus is used only by the auto-review job (pull_request_target), not the
# interactive @claude job — so the check below only needs github.event.pull_request,
# which pull_request_target populates. On issue_comment events that field is absent,
# so this evaluates to startsWith(null, 'backports/') -> false; harmless, since the
# interactive job ignores review_focus. Can't be fixed in-expression: issue_comment
# payloads carry no head ref at all.
review_focus: |
${{ startsWith(github.event.pull_request.head.ref, 'backports/') && '
This is a BACKPORT / RELEASE pull request. Every cherry-picked commit
originates from a PR that was already independently reviewed and approved
on the source branch. Do NOT re-review the individual code changes and do
NOT comment on the implementation of cherry-picked hunks — assume they are
correct.
Instead, focus exclusively on:
- Cherry-pick completeness. Inspect the PRs/commits included in this
backport. For each source PR, check its milestone, labels, and target
branch to decide whether it belongs in this release line. Flag any PR
that targets this release but is MISSING here, and any included PR that
does not belong.
- Non-clean picks. Call out commits that appear to have been modified
during the cherry-pick (conflict resolution, adapted context) — those
are the only changes that warrant a closer look.
- Release overview. Produce a concise summary of what this release ships,
grouped by area (JetStream, Raft/NRG, leafnodes, security, etc.), so
reviewers can sanity-check the scope at a glance.
' || '
Additionally focus on:
- Performance implications (hot paths, allocations, lock contention)
- Concurrency safety (goroutine leaks, race conditions, deadlocks)
- Raft consensus and JetStream clustering correctness
- Security boundaries (authentication, authorization, TLS handling)
' }}
secrets:
claude_oauth_token: ${{ secrets.CLAUDE_OAUTH_TOKEN }}
gh_app_private_key: ${{ secrets.CLAUDE_GH_APP_PRIVATE_KEY }}

53
.github/workflows/cov.yaml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: NATS Server Code Coverage
on:
workflow_dispatch: {}
schedule:
- cron: "40 4 * * *"
permissions:
contents: read
jobs:
nightly_coverage:
runs-on: ubuntu-latest
env:
GOPATH: /home/runner/work/nats-server
GO111MODULE: "on"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
path: src/github.com/nats-io/nats-server
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: src/github.com/nats-io/nats-server/go.sum
- name: Run code coverage
shell: bash --noprofile --norc -x -eo pipefail {0}
run: |
set -e
cd src/github.com/nats-io/nats-server
./scripts/cov.sh upload
set +e
- name: Convert coverage.out to coverage.lcov
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
# Commit e4612787670fc5b5f49026b8c29c5569921de1db = tag v1.2.0
uses: jandelgado/gcov2lcov-action@e4612787670fc5b5f49026b8c29c5569921de1db
with:
infile: acc.out
working-directory: src/github.com/nats-io/nats-server
- name: Coveralls
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
# Commit 5cbfd81b66ca5d10c19b062c04de0199c215fb6e = tag v2.3.7
uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e
with:
github-token: ${{ secrets.github_token }}
file: src/github.com/nats-io/nats-server/coverage.lcov

33
.github/workflows/long-tests.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: NATS Server Long Tests
on:
# Allow manual trigger (any branch)
workflow_dispatch:
# Run daily at 12:30 on default branch
schedule:
- cron: "30 12 * * *"
permissions:
contents: read
concurrency:
# At most one of these workflow per ref running
group: ${{ github.workflow }}-${{ github.ref }}
# New one cancels in-progress one
cancel-in-progress: true
jobs:
js-long:
name: Long JetStream tests
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run tests
run: go test -race -v -run='^TestLong.*' ./server -tags=include_js_long_tests -count=1 -vet=off -timeout=60m -shuffle on -p 1 -failfast

53
.github/workflows/mqtt-test.yaml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: MQTT External Tests
on: [pull_request]
permissions:
contents: read
jobs:
test:
env:
GOPATH: /home/runner/work/nats-server
GO111MODULE: "on"
runs-on: ${{ vars.GHA_WORKER_SMALL || 'ubuntu-latest' }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
path: src/github.com/nats-io/nats-server
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: src/github.com/nats-io/nats-server/go.sum
- name: Set up testing tools and environment
shell: bash --noprofile --norc -eo pipefail {0}
id: setup
run: |
wget https://github.com/hivemq/mqtt-cli/releases/download/v4.20.0/mqtt-cli-4.20.0.deb
sudo apt install ./mqtt-cli-4.20.0.deb
# Pinned to the mqtt-test commit that adds --pipeline (PR #5);
# bump to @v0.2.0 once it is tagged.
go install github.com/ConnectEverything/mqtt-test@6138352776422dfd16eeb3333d078358d8b85fea
- name: Run tests (3 times to detect flappers)
shell: bash --noprofile --norc -eo pipefail {0}
run: |
cd src/github.com/nats-io/nats-server
go test -v --count=3 --run='TestXMQTT' ./server
- name: Run tests with --race
shell: bash --noprofile --norc -eo pipefail {0}
run: |
cd src/github.com/nats-io/nats-server
go test -v --race --failfast --run='TestXMQTT' ./server
- name: Run benchmarks
shell: bash --noprofile --norc -eo pipefail {0}
run: |
cd src/github.com/nats-io/nats-server
go test --run='-' --count=3 --bench 'BenchmarkXMQTT' --benchtime=100x ./server
# TODO: compare benchmarks

51
.github/workflows/nightly.yaml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Docker Nightly
on:
workflow_dispatch:
inputs:
target:
description: "Override source branch (optional)"
type: string
required: false
schedule:
- cron: "40 4 * * *"
permissions:
contents: read
jobs:
run:
runs-on: ${{ vars.GHA_WORKER_RELEASE || 'ubuntu-latest' }}
permissions:
contents: write
steps:
- name: Checkout NATS
uses: actions/checkout@v6
with:
path: src/github.com/nats-io/nats-server
ref: ${{ inputs.target || 'main' }}
fetch-depth: 0
fetch-tags: true
- name: Checkout NSC
uses: actions/checkout@v6
with:
repository: nats-io/nsc
path: src/github.com/nats-io/nsc
fetch-depth: 1
fetch-tags: true
- name: Checkout NATS CLI
uses: actions/checkout@v6
with:
repository: nats-io/natscli
path: src/github.com/nats-io/natscli
fetch-depth: 1
fetch-tags: true
- name: Build Docker nightly
uses: ./src/github.com/nats-io/nats-server/.github/actions/nightly-release
with:
name: ${{ inputs.target || 'nightly' }}
hub_username: "${{ secrets.DOCKER_USERNAME }}"
hub_password: "${{ secrets.DOCKER_PASSWORD }}"

59
.github/workflows/pull-requests.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: NATS Server PRs
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
signoffs:
name: Sign-offs
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
steps:
- name: Checkout base repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch PR head
run: |
set -euo pipefail
git fetch --no-tags origin "$BASE_SHA" "pull/${PR_NUMBER}/head:pr-head"
- name: Check all PR commits are signed off
run: |
set -euo pipefail
range="${BASE_SHA}..pr-head"
missing=0
missing_list=""
while read -r c; do
msg="$(git log -1 --pretty=%B "$c")"
if ! printf '%s\n' "$msg" | grep -q '^Signed-off-by:'; then
echo "::error ::Commit $c has not been signed off"
missing=1
missing_list+=$'\n- '"$c"
fi
done < <(git rev-list --no-merges "$range")
if [ "$missing" -ne 0 ]; then
body="$(printf 'Please review the `CONTRIBUTING.md` guide as some commits are missing `Signed-off-by:` in their commit messages.\n\nTo correct, please amend the following commits and force-push:\n\n%s' "$missing_list")"
gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "$body"
exit 1
fi

54
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: NATS Server Releases
on:
push:
tags:
- v*
permissions:
contents: read
jobs:
run:
name: GitHub Release
runs-on: ${{ vars.GHA_WORKER_RELEASE || 'ubuntu-latest' }}
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "stable"
- name: Check version matches tag
env:
TRAVIS_TAG: ${{ github.ref_name }}
run: |
go test -race -v -run=TestVersionMatchesTag ./server -ldflags="-X=github.com/nats-io/nats-server/v2/server.serverVersion=$TRAVIS_TAG" -count=1 -vet=off
- name: Install cosign
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
# Commit 6f9f17788090df1f26f669e9d70d6ae9567deba6 = tag v4.1.2
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6
- name: Install syft
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
# Commit e22c389904149dbc22b58101806040fa8d37a610 = tag v0.24.0
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610
with:
syft-version: "v1.42.3"
- name: Create release
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
env:
GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

21
.github/workflows/stale-issues.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Stale Issues
on:
schedule:
- cron: "30 1 * * *"
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
stale-issue-label: stale
stale-pr-label: stale
days-before-stale: 56 # Mark stale after 8 weeks (56 days) of inactivity
days-before-close: -1 # Disable auto-closing
exempt-all-milestones: true # Any issue/PR within a milestone will be omitted

372
.github/workflows/tests.yaml vendored Normal file
View File

@@ -0,0 +1,372 @@
name: NATS Server Tests
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
RACE: ${{ (github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') && github.event_name != 'pull_request') && '-race' || '' }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
- name: Run golangci-lint
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
# Commit 82606bf257cbaff209d206a39f5134f0cfbfd2ee = tag v9.2.1
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee
with:
version: v2.13.0
skip-cache: true
skip-save-cache: true
args: --timeout=5m --config=.golangci.yml
build-latest:
name: Build (Latest Go)
needs: [lint]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Build NATS Server 64-bit
run: GOARCH=amd64 GOOS=linux go build
- name: Build NATS Server 32-bit
run: GOARCH=386 GOOS=linux go build
build-supported:
name: Build (Minimum Go)
needs: [lint]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
- name: Build NATS Server 64-bit
run: GOARCH=amd64 GOOS=linux go build
- name: Build NATS Server 32-bit
run: GOARCH=386 GOOS=linux go build
# Using GitHub-supplied workers for Windows for now.
# Note that the below testing steps depend on the Linux build
# only, as the Windows builds take a fair bit longer to set up.
build-windows:
name: Build (Minimum Go, ${{ matrix.os }})
needs: [lint]
strategy:
fail-fast: false
matrix:
os: [windows-2022, windows-2025]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
- name: Build NATS Server
run: go build
store:
name: Test Stores
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh store_tests
js-no-cluster:
name: Test JetStream
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_tests
raft:
name: Test Raft
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh raft_tests
js-consumers:
name: Test JetStream Consumers
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_consumer_tests
js-cluster-1:
name: Test JetStream Cluster 1
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_1
js-cluster-2:
name: Test JetStream Cluster 2
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_2
js-cluster-3:
name: Test JetStream Cluster 3
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_3
js-cluster-4:
name: Test JetStream Cluster 4
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_cluster_tests_4
js-supercluster:
name: Test JetStream Supercluster
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh js_super_cluster_tests
no-race-1:
name: Test No-Race 1
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh no_race_1_tests
no-race-2:
name: Test No-Race 2
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh no_race_2_tests
mqtt:
name: Test MQTT
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh mqtt_tests
msgtrace:
name: Test Message Tracing
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh msgtrace_tests
jwt:
name: Test JWT
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh jwt_tests
server-pkg-non-js:
name: Test Remaining Server Tests
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_LARGE || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh srv_pkg_non_js_tests
non-server-pkg:
name: Test Other Packages
needs: [build-latest, build-supported, lint]
runs-on: ${{ vars.GHA_WORKER_MEDIUM || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Go
uses: actions/setup-go@v6
with:
go-version: stable
- name: Run unit tests
run: ./scripts/runTestsOnTravis.sh non_srv_pkg_tests

33
.github/workflows/vuln.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Vulnerability Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * *"
permissions:
contents: read
security-events: write
jobs:
govulncheck:
runs-on: ${{ vars.GHA_WORKER_SMALL || 'ubuntu-latest' }}
steps:
- name: Run govulncheck
# Use commit hash here to avoid a re-tagging attack, as this is a third-party action
# Commit b625fbe08f3bccbe446d94fbf87fcc875a4f50ee = tag v1.0.4
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee
with:
go-package: ./...
output-format: sarif
output-file: govulncheck.sarif
- name: Upload SARIF results
if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: govulncheck.sarif

55
.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
dist
# Configuration Files
*.conf
*.cfg
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
# Eclipse
.project
# IntelliJ
.idea/
# Emacs
*~
\#*\#
.\#*
# Visual Studio Code
.vscode
# Mac
.DS_Store
# bin
nats-server
gnatsd
check
# coverage
coverage.out
# Cross compiled binaries
pkg

90
.golangci.yml Normal file
View File

@@ -0,0 +1,90 @@
version: "2"
run:
concurrency: 4
modules-download-mode: readonly
issues-exit-code: 1
tests: true
output:
formats:
text:
path: stdout
print-linter-name: true
print-issued-lines: true
linters:
default: none
enable:
- forbidigo
- govet
- ineffassign
- misspell
- staticcheck
- unused
settings:
errcheck:
check-type-assertions: false
check-blank: false
forbidigo:
forbid:
- pattern: ^fmt\.Print(f|ln)?$
govet:
settings:
printf:
funcs:
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
misspell:
locale: US
staticcheck:
checks:
- all
- -QF*
- -ST1003
- -ST1016
- -SA1019
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- forbidigo
path: main.go
- linters:
- forbidigo
source: "nats-server: v%s"
- linters:
- forbidigo
path: server/opts.go
- linters:
- forbidigo
path: _test.go
paths:
- .github
- doc
- docker
- logos
- scripts
- util
- third_party$
- builtin$
- examples$
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- .github
- doc
- docker
- logos
- scripts
- util
- third_party$
- builtin$
- examples$

118
.goreleaser.yml Normal file
View File

@@ -0,0 +1,118 @@
project_name: nats-server
version: 2
release:
github:
owner: '{{ envOrDefault "GITHUB_REPOSITORY_OWNER" "nats-io" }}'
name: '{{ envOrDefault "GITHUB_REPOSITORY_NAME" "nats-server" }}'
name_template: "Release {{.Tag}}"
draft: true
changelog:
disable: true
builds:
- main: .
binary: nats-server
flags:
- -trimpath
ldflags:
- -w -X 'github.com/nats-io/nats-server/v2/server.gitCommit={{.ShortCommit}}' -X 'github.com/nats-io/nats-server/v2/server.serverVersion={{.Tag}}'
env:
# This is the toolchain version we use for releases. To override, set the env var, e.g.:
# GORELEASER_TOOLCHAIN="go1.22.8" TARGET='linux_amd64' goreleaser build --snapshot --clean --single-target
- GOTOOLCHAIN={{ envOrDefault "GORELEASER_TOOLCHAIN" "go1.27.1" }}
- GO111MODULE=on
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
- 386
- loong64
- mips64le
- s390x
- ppc64le
# RISC-V currently only supported on Linux
- riscv64
goarm:
- 6
- 7
ignore:
- goos: windows
goarch: arm
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
- goos: freebsd
goarch: arm64
- goos: freebsd
goarch: 386
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64
- goos: freebsd
goarch: riscv64
mod_timestamp: "{{ .CommitTimestamp }}"
nfpms:
- file_name_template: "{{.ProjectName}}-{{.Tag}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}"
homepage: https://nats.io
description: High-Performance server for NATS, the cloud native messaging system.
maintainer: Ivan Kozlovic <ivan@synadia.com>
license: Apache 2.0
vendor: Synadia Inc.
formats:
- deb
- rpm
mtime: "{{ .CommitDate }}"
contents:
- src: /usr/bin/nats-server
dst: /usr/local/bin/nats-server
type: "symlink"
rpm:
buildhost: synadia.com
archives:
- name_template: "{{.ProjectName}}-{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}"
id: targz-archives
wrap_in_directory: true
formats: ["tar.gz"]
format_overrides:
- goos: windows
formats: ["zip"]
builds_info:
owner: root
group: root
mode: 0755
files:
- src: README.md
info:
owner: root
group: root
mode: 0644
mtime: "{{ .CommitDate }}"
- src: LICENSE
info:
owner: root
group: root
mode: 0644
mtime: "{{ .CommitDate }}"
checksum:
name_template: "SHA256SUMS"
algorithm: sha256
sboms:
- artifacts: binary
documents:
[
"{{.ProjectName}}-{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}.sbom.spdx.json",
]

5
AMBASSADORS.md Normal file
View File

@@ -0,0 +1,5 @@
# Ambassadors
The NATS ambassador program recognizes community members that go above and beyond in their contributions to the community and the ecosystem. Learn more [here](https://nats.io/community#nats-ambassador-program).
- [Maurice van Veen](https://nats.io/community#maurice-van-veen) <contact@mauricevanveen.com>

3
CODE-OF-CONDUCT.md Normal file
View File

@@ -0,0 +1,3 @@
## Community Code of Conduct
NATS follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).

52
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,52 @@
# Contributing
Thanks for your interest in contributing! This document contains `nats-io/nats-server` specific contributing details. If you are a first-time contributor, please refer to the general [NATS Contributor Guide](https://nats.io/contributing/) to get a comprehensive overview of contributing to the NATS project.
## Getting started
There are three general ways you can contribute to this repo:
- Proposing an enhancement or new feature
- Reporting a bug or regression
- Contributing changes to the source code
For the first two, refer to the [GitHub Issues](https://github.com/nats-io/nats-server/issues/new/choose) which guides you through the available options along with the needed information to collect.
## Contributing changes
_Prior to opening a pull request, it is recommended to open an issue first to ensure the maintainers can review intended changes. This saves time for both you and us. Exceptions to this rule include fixing non-functional source such as code comments, documentation or other supporting files._
Proposing source code changes is done through GitHub's standard pull request workflow.
If your branch is a work-in-progress then please start by creating your pull requests as draft, by clicking the down-arrow next to the `Create pull request` button and instead selecting `Create draft pull request`.
This will defer the automatic process of requesting a review from the NATS team and significantly reduces noise until you are ready. Once you are happy, you can click the `Ready for review` button.
### Guidelines
A good pull request includes:
- A succinct yet descriptive title describing, in a few words, what is fixed/improved/optimised by this change.
- A high-level description of the changes, including an overview of _why_ the changes are relevant or what problem you are trying to solve. Include links to any issues that are related by adding comments like `Resolves #NNN` to your description. See [Linking a Pull Request to an Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) for more information.
- An up-to-date parent commit. Please make sure you are pulling in the latest `main` branch and rebasing your work on top of it, i.e. `git rebase main`.
- Unit tests where appropriate. Bug fixes will benefit from the addition of regression tests and performance improvements will benefit from the addition of new benchmarks where possible. New features will **NOT** be accepted without suitable test coverage!
- No more commits than necessary. Sometimes having multiple commits is useful for telling a story or isolating changes from one another, but please squash down any unnecessary commits that may just be for clean-up, comments or small changes.
- No additional external dependencies that aren't absolutely essential. Please do everything you can to avoid pulling in additional libraries/dependencies into `go.mod` as we will be very critical of these.
### AI Policy
We are happy to accept AI-assisted contributions, but we are not happy to talk to your AI agent during the process. When submitting an issue or a pull request, communicate with us in your own words and [do not meat-proxy](https://nomeatproxy.com). If we wanted to talk to an AI, we can do it ourselves! If you are submitting code, we expect you to understand how the code works and that it has been appropriately tested, even if an AI has written or reviewed it.
We also use AI review tools for pull requests, but it is always a human that makes the final decision on whether or not to accept it, so please engage faithfully during the code review process.
### Sign-off
In order to accept a contribution, you will first need to certify that the contribution is your original work and that you license the work to the project under the [Apache-2.0 license](https://github.com/nats-io/nats-server/blob/main/LICENSE).
This is done by using `Signed-off-by: Your Name <your.email@example.com>` statement which should appear in all of your commit messages. Please note that we can only accept sign-offs under a legal name. Nicknames and aliases are not permitted.
To perform a sign-off when committing with `git`, use `git commit -s` (or `--signoff`) to add the `Signed-off-by:` trailer to your commit message.
## Get help
If you have questions about the contribution process, please start a [GitHub discussion](https://github.com/nats-io/nats-server/discussions), join the [NATS Slack](https://slack.nats.io/), or send your question to the [NATS Google Group](https://groups.google.com/forum/#!forum/natsio).

18
DEPENDENCIES.md Normal file
View File

@@ -0,0 +1,18 @@
# External Dependencies
This file lists the dependencies used in this repository.
| Dependency | License |
|-|-|
| Go | BSD 3-Clause "New" or "Revised" License |
| github.com/nats-io/nats-server/v2 | Apache License 2.0 |
| github.com/google/go-tpm | Apache License 2.0 |
| github.com/klauspost/compress | BSD 3-Clause "New" or "Revised" License |
| github.com/minio/highwayhash | Apache License 2.0 |
| github.com/nats-io/jwt/v2 | Apache License 2.0 |
| github.com/nats-io/nats.go | Apache License 2.0 |
| github.com/nats-io/nkeys | Apache License 2.0 |
| github.com/nats-io/nuid | Apache License 2.0 |
| golang.org/x/crypto | BSD 3-Clause "New" or "Revised" License |
| golang.org/x/sys | BSD 3-Clause "New" or "Revised" License |
| golang.org/x/time | BSD 3-Clause "New" or "Revised" License |

3
GOVERNANCE.md Normal file
View File

@@ -0,0 +1,3 @@
# NATS Server Governance
NATS Server is part of the NATS project and is subject to the [NATS Governance](https://github.com/nats-io/nats-general/blob/main/GOVERNANCE.md).

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

10
MAINTAINERS.md Normal file
View File

@@ -0,0 +1,10 @@
# Maintainers
Maintainership is on a per project basis. Reference [NATS Governance Model](https://github.com/nats-io/nats-general/blob/main/GOVERNANCE.md).
### Maintainers
- Derek Collison <derek@nats.io> [@derekcollison](https://github.com/derekcollison)
- Ivan Kozlovic <ivan@nats.io> [@kozlovic](https://github.com/kozlovic)
- Waldemar Quevedo <wally@nats.io> [@wallyqs](https://github.com/wallyqs)
- Oleg Shaldybin <olegsh@google.com> [@olegshaldybin](https://github.com/olegshaldybin)
- R.I. Pienaar <rip@devco.net> [@ripienaar](https://github.com/ripienaar)

76
README.md Normal file
View File

@@ -0,0 +1,76 @@
<p align="center">
<img src="logos/nats-horizontal-color.png" width="300" alt="NATS Logo">
</p>
[NATS](https://nats.io) is a simple, secure and performant communications system for digital systems, services and devices. NATS is part of the Cloud Native Computing Foundation ([CNCF](https://cncf.io)). NATS has over [40 client language implementations](https://nats.io/download/), and its server can run on-premise, in the cloud, at the edge, and even on a Raspberry Pi. NATS can secure and simplify design and operation of modern distributed systems.
[![License][License-Image]][License-Url] [![Build][Build-Status-Image]][Build-Status-Url] [![Release][Release-Image]][Release-Url] [![Slack][Slack-Image]][Slack-Url] [![Coverage][Coverage-Image]][Coverage-Url] [![Docker Downloads][Docker-Image]][Docker-Url] [![GitHub Downloads][GitHub-Image]][Somsubhra-URL] [![CII Best Practices][CIIBestPractices-Image]][CIIBestPractices-Url] [![Artifact Hub][ArtifactHub-Image]][ArtifactHub-Url]
## Documentation
- [Official Website](https://nats.io)
- [Official Documentation](https://docs.nats.io)
- [FAQ](https://docs.nats.io/reference/faq)
- Watch [a video overview](https://rethink.synadia.com/episodes/1/) of NATS.
- Watch [this video from SCALE 13x](https://www.youtube.com/watch?v=sm63oAVPqAM) to learn more about its origin story and design philosophy.
## Contact
- [Twitter](https://twitter.com/nats_io): Follow us on Twitter!
- [Google Groups](https://groups.google.com/forum/#!forum/natsio): Where you can ask questions
- [Slack](https://natsio.slack.com): Click [here](https://slack.nats.io) to join. You can ask questions to our maintainers and to the rich and active community.
## Contributing
If you are interested in contributing to NATS, read about our...
- [Contributing guide](./CONTRIBUTING.md)
- [Report issues or propose Pull Requests](https://github.com/nats-io)
[License-Url]: https://www.apache.org/licenses/LICENSE-2.0
[License-Image]: https://img.shields.io/badge/License-Apache2-blue.svg
[Docker-Image]: https://img.shields.io/docker/pulls/_/nats.svg
[Docker-Url]: https://hub.docker.com/_/nats
[Slack-Image]: https://img.shields.io/badge/chat-on%20slack-green
[Slack-Url]: https://slack.nats.io
[Fossa-Url]: https://app.fossa.io/projects/git%2Bgithub.com%2Fnats-io%2Fnats-server?ref=badge_shield
[Fossa-Image]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fnats-io%2Fnats-server.svg?type=shield
[Build-Status-Url]: https://github.com/nats-io/nats-server/actions/workflows/tests.yaml
[Build-Status-Image]: https://github.com/nats-io/nats-server/actions/workflows/tests.yaml/badge.svg?branch=main
[Release-Url]: https://github.com/nats-io/nats-server/releases/latest
[Release-Image]: https://img.shields.io/github/v/release/nats-io/nats-server
[Coverage-Url]: https://coveralls.io/r/nats-io/nats-server?branch=main
[Coverage-image]: https://coveralls.io/repos/github/nats-io/nats-server/badge.svg?branch=main
[ReportCard-Url]: https://goreportcard.com/report/nats-io/nats-server
[ReportCard-Image]: https://goreportcard.com/badge/github.com/nats-io/nats-server
[CIIBestPractices-Url]: https://bestpractices.coreinfrastructure.org/projects/1895
[CIIBestPractices-Image]: https://bestpractices.coreinfrastructure.org/projects/1895/badge
[ArtifactHub-Url]: https://artifacthub.io/packages/helm/nats/nats
[ArtifactHub-Image]: https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/nats
[GitHub-Release]: https://github.com/nats-io/nats-server/releases/
[GitHub-Image]: https://img.shields.io/github/downloads/nats-io/nats-server/total.svg?logo=github
[Somsubhra-url]: https://somsubhra.github.io/github-release-stats/?username=nats-io&repository=nats-server
## Roadmap
The NATS product roadmap can be found [here](https://nats.io/about/#roadmap).
## Adopters
Who uses NATS? See our [list of users](https://nats.io/#who-uses-nats) on [https://nats.io](https://nats.io).
## Security
### Security Audit
A third party security audit was performed by Trail of Bits following engagement by the Open Source Technology Improvement Fund (OSTIF). You can see the [full report from April 2025 here](https://github.com/trailofbits/publications/blob/master/reviews/2025-04-ostif-nats-securityreview.pdf).
### Reporting Security Vulnerabilities
If you've found a vulnerability or a potential vulnerability in the NATS server, please let us know at
[nats-security](mailto:security@nats.io).
## License
Unless otherwise noted, the NATS source files are distributed
under the Apache Version 2.0 license found in the LICENSE file.

9
RELEASES.md Normal file
View File

@@ -0,0 +1,9 @@
# Release Workflow
- As of NATS 2.11, the server follows a 6-month release cycle. 2.11 was released in March 2025.
- Version tagging follows semantic versioning with security fixes clearly marked.
- The `main` branch is for active development of features and bug fixes. Release branches are created for preparing patch releases. Releases are tagged with semantic versions
- The current and previous minor series are supported in terms of bug fixes and security patches.
- Binaries are released via [GitHub Releases](https://github.com/nats-io/nats-server/releases) and Docker images are available as an [official image](https://hub.docker.com/_/nats).
- Preview releases for the next minor version, e.g. 2.14.0, become available once feature development is complete.
- Tagged releases as well as nightly Docker images of `main` (for testing) are available in the [synadia/nats-server](https://hub.docker.com/r/synadia/nats-server/tags) Docker repository.

54
TODO.md Normal file
View File

@@ -0,0 +1,54 @@
# General
- [ ] Auth for queue groups?
- [ ] Blacklist or ERR escalation to close connection for auth/permissions
- [ ] Protocol updates, MAP, MPUB, etc
- [ ] Multiple listen endpoints
- [ ] Websocket / HTTP2 strategy
- [ ] T series reservations
- [ ] _SYS. server events?
- [ ] No downtime restart
- [ ] Signal based reload of configuration
- [ ] brew, apt-get, rpm, chocately (windows)
- [ ] IOVec pools and writev for high fanout?
- [ ] Modify cluster support for single message across routes between pub/sub and d-queue
- [ ] Memory limits/warnings?
- [ ] Limit number of subscriptions a client can have, total memory usage etc.
- [ ] Multi-tenant accounts with isolation of subject space
- [ ] Pedantic state
- [X] _SYS.> reserved for server events?
- [X] Listen configure key vs addr and port
- [X] Add ENV and variable support to dconf? ucl?
- [X] Buffer pools/sync pools?
- [X] Multiple Authorization / Access
- [X] Write dynamic socket buffer sizes
- [X] Read dynamic socket buffer sizes
- [X] Info updates contain other implicit route servers
- [X] Sublist better at high concurrency, cache uses writelock always currently
- [X] Switch to 1.4/1.5 and use maps vs hashmaps in sublist
- [X] NewSource on Rand to lower lock contention on QueueSubs, or redesign!
- [X] Default sort by cid on connz
- [X] Track last activity time per connection?
- [X] Add total connections to varz so we won't miss spikes, etc.
- [X] Add starttime and uptime to connz list.
- [X] Gossip Protocol for discovery for clustering
- [X] Add in HTTP requests to varz?
- [X] Add favico and help link for monitoring?
- [X] Better user/pass support using bcrypt etc.
- [X] SSL/TLS support
- [X] Add support for / to point to varz, connz, etc..
- [X] Support sort options for /connz via nats-top
- [X] Dropped message statistics (slow consumers)
- [X] Add current time to each monitoring endpoint
- [X] varz uptime do days and only integer secs
- [X] Place version in varz (same info sent to clients)
- [X] Place server ID/UUID in varz
- [X] nats-top equivalent, utils
- [X] Connz report routes (/routez)
- [X] Docker
- [X] Remove reliance on `ps`
- [X] Syslog support
- [X] Client support for language and version
- [X] Fix benchmarks on linux
- [X] Daemon mode? Won't fix

24
conf/fuzz.go Normal file
View File

@@ -0,0 +1,24 @@
// Copyright 2020-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build gofuzz
package conf
func Fuzz(data []byte) int {
_, err := Parse(string(data))
if err != nil {
return 0
}
return 1
}

1319
conf/lex.go Normal file

File diff suppressed because it is too large Load Diff

1645
conf/lex_test.go Normal file

File diff suppressed because it is too large Load Diff

514
conf/parse.go Normal file
View File

@@ -0,0 +1,514 @@
// Copyright 2013-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package conf supports a configuration file format used by gnatsd. It is
// a flexible format that combines the best of traditional
// configuration formats and newer styles such as JSON and YAML.
package conf
// The format supported is less restrictive than today's formats.
// Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //)
// Also supports key value assignments using '=' or ':' or whiteSpace()
// e.g. foo = 2, foo : 2, foo 2
// maps can be assigned with no key separator as well
// semicolons as value terminators in key/value assignments are optional
//
// see parse_test.go for more examples.
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode"
)
const _EMPTY_ = ""
type parser struct {
mapping map[string]any
lx *lexer
// The current scoped context, can be array or map
ctx any
// stack of contexts, either map or array/slice stack
ctxs []any
// Keys stack
keys []string
// Keys stack as items
ikeys []item
// The config file path, empty by default.
fp string
// pedantic reports error when configuration is not correct.
pedantic bool
// Tracks environment variable references, to avoid cycles
envVarReferences map[string]bool
}
// Parse will return a map of keys to any, although concrete types
// underly them. The values supported are string, bool, int64, float64, DateTime.
// Arrays and nested Maps are also supported.
func Parse(data string) (map[string]any, error) {
p, err := parse(data, "", false)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// ParseWithChecks is equivalent to Parse but runs in pedantic mode.
func ParseWithChecks(data string) (map[string]any, error) {
p, err := parse(data, "", true)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// ParseFile is a helper to open file, etc. and parse the contents.
func ParseFile(fp string) (map[string]any, error) {
data, err := os.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
p, err := parse(string(data), fp, false)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// ParseFileWithChecks is equivalent to ParseFile but runs in pedantic mode.
func ParseFileWithChecks(fp string) (map[string]any, error) {
data, err := os.ReadFile(fp)
if err != nil {
return nil, err
}
p, err := parse(string(data), fp, true)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// configDigest returns a digest for the parsed config.
func configDigest(m map[string]any) (string, error) {
digest := sha256.New()
e := json.NewEncoder(digest)
if err := e.Encode(m); err != nil {
return _EMPTY_, err
}
return fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
}
// ParseFileWithChecksDigest returns the processed config and a digest
// that represents the configuration.
func ParseFileWithChecksDigest(fp string) (map[string]any, string, error) {
m, err := ParseFileWithChecks(fp)
if err != nil {
return nil, _EMPTY_, err
}
digest, err := configDigest(m)
if err != nil {
return nil, _EMPTY_, err
}
return m, digest, nil
}
type token struct {
item item
value any
usedVariable bool
sourceFile string
}
func (t *token) MarshalJSON() ([]byte, error) {
return json.Marshal(t.value)
}
func (t *token) Value() any {
return t.value
}
func (t *token) Line() int {
return t.item.line
}
func (t *token) IsUsedVariable() bool {
return t.usedVariable
}
func (t *token) SourceFile() string {
return t.sourceFile
}
func (t *token) Position() int {
return t.item.pos
}
func newParser(data, fp string, pedantic bool) *parser {
return &parser{
mapping: make(map[string]any),
lx: lex(data),
ctxs: make([]any, 0, 4),
keys: make([]string, 0, 4),
ikeys: make([]item, 0, 4),
fp: filepath.Dir(fp),
pedantic: pedantic,
envVarReferences: make(map[string]bool),
}
}
func parse(data, fp string, pedantic bool) (*parser, error) {
p := newParser(data, fp, pedantic)
if err := p.parse(fp); err != nil {
return nil, err
}
return p, nil
}
func parseEnv(data string, parent *parser) (*parser, error) {
p := newParser(data, "", false)
p.envVarReferences = parent.envVarReferences
if err := p.parse(""); err != nil {
return nil, err
}
return p, nil
}
func (p *parser) parse(fp string) error {
p.pushContext(p.mapping)
var prevItem item
for {
it := p.next()
if it.typ == itemEOF {
// Here we allow the final character to be a bracket '}'
// in order to support JSON like configurations.
if prevItem.typ == itemKey && prevItem.val != mapEndString {
return fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
}
break
}
prevItem = it
if err := p.processItem(it, fp); err != nil {
return err
}
}
return nil
}
func (p *parser) next() item {
return p.lx.nextItem()
}
func (p *parser) pushContext(ctx any) {
p.ctxs = append(p.ctxs, ctx)
p.ctx = ctx
}
func (p *parser) popContext() any {
if len(p.ctxs) == 0 {
panic("BUG in parser, context stack empty")
}
li := len(p.ctxs) - 1
last := p.ctxs[li]
p.ctxs = p.ctxs[0:li]
p.ctx = p.ctxs[len(p.ctxs)-1]
return last
}
func (p *parser) pushKey(key string) {
p.keys = append(p.keys, key)
}
func (p *parser) popKey() string {
if len(p.keys) == 0 {
panic("BUG in parser, keys stack empty")
}
li := len(p.keys) - 1
last := p.keys[li]
p.keys = p.keys[0:li]
return last
}
func (p *parser) pushItemKey(key item) {
p.ikeys = append(p.ikeys, key)
}
func (p *parser) popItemKey() item {
if len(p.ikeys) == 0 {
panic("BUG in parser, item keys stack empty")
}
li := len(p.ikeys) - 1
last := p.ikeys[li]
p.ikeys = p.ikeys[0:li]
return last
}
func (p *parser) processItem(it item, fp string) error {
setValue := func(it item, v any) {
if p.pedantic {
p.setValue(&token{it, v, false, fp})
} else {
p.setValue(v)
}
}
switch it.typ {
case itemError:
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
case itemKey:
// Keep track of the keys as items and strings,
// we do this in order to be able to still support
// includes without many breaking changes.
p.pushKey(it.val)
if p.pedantic {
p.pushItemKey(it)
}
case itemMapStart:
newCtx := make(map[string]any)
p.pushContext(newCtx)
case itemMapEnd:
setValue(it, p.popContext())
case itemString:
// FIXME(dlc) sanitize string?
setValue(it, it.val)
case itemInteger:
lastDigit := 0
for _, r := range it.val {
if !unicode.IsDigit(r) && r != '-' {
break
}
lastDigit++
}
numStr := it.val[:lastDigit]
num, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
return fmt.Errorf("integer '%s' is out of the range", it.val)
}
return fmt.Errorf("expected integer, but got '%s'", it.val)
}
// Process a suffix
suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))
switch suffix {
case "":
setValue(it, num)
case "k":
setValue(it, num*1000)
case "kb", "ki", "kib":
setValue(it, num*1024)
case "m":
setValue(it, num*1000*1000)
case "mb", "mi", "mib":
setValue(it, num*1024*1024)
case "g":
setValue(it, num*1000*1000*1000)
case "gb", "gi", "gib":
setValue(it, num*1024*1024*1024)
case "t":
setValue(it, num*1000*1000*1000*1000)
case "tb", "ti", "tib":
setValue(it, num*1024*1024*1024*1024)
case "p":
setValue(it, num*1000*1000*1000*1000*1000)
case "pb", "pi", "pib":
setValue(it, num*1024*1024*1024*1024*1024)
case "e":
setValue(it, num*1000*1000*1000*1000*1000*1000)
case "eb", "ei", "eib":
setValue(it, num*1024*1024*1024*1024*1024*1024)
}
case itemFloat:
num, err := strconv.ParseFloat(it.val, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
return fmt.Errorf("float '%s' is out of the range", it.val)
}
return fmt.Errorf("expected float, but got '%s'", it.val)
}
setValue(it, num)
case itemBool:
switch strings.ToLower(it.val) {
case "true", "yes", "on":
setValue(it, true)
case "false", "no", "off":
setValue(it, false)
default:
return fmt.Errorf("expected boolean value, but got '%s'", it.val)
}
case itemDatetime:
dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
if err != nil {
return fmt.Errorf(
"expected Zulu formatted DateTime, but got '%s'", it.val)
}
setValue(it, dt)
case itemArrayStart:
var array = make([]any, 0)
p.pushContext(array)
case itemArrayEnd:
array := p.ctx
p.popContext()
setValue(it, array)
case itemVariable:
value, found, err := p.lookupVariable(it.val)
if err != nil {
return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
it.val, it.line, err)
}
if !found {
return fmt.Errorf("variable reference for '%s' on line %d can not be found",
it.val, it.line)
}
if p.pedantic {
switch tk := value.(type) {
case *token:
// Mark the looked up variable as used, and make
// the variable reference become handled as a token.
tk.usedVariable = true
p.setValue(&token{it, tk.Value(), false, fp})
default:
// Special case to add position context to bcrypt references.
p.setValue(&token{it, value, false, fp})
}
} else {
p.setValue(value)
}
case itemInclude:
var (
m map[string]any
err error
)
if p.pedantic {
m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
} else {
m, err = ParseFile(filepath.Join(p.fp, it.val))
}
if err != nil {
return fmt.Errorf("error parsing include file '%s', %v", it.val, err)
}
for k, v := range m {
p.pushKey(k)
if p.pedantic {
switch tk := v.(type) {
case *token:
p.pushItemKey(tk.item)
}
}
p.setValue(v)
}
}
return nil
}
// Used to map an environment value into a temporary map to pass to secondary Parse call.
const pkey = "pk"
// We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
const bcryptPrefix = "2a$"
// lookupVariable will lookup a variable reference. It will use block scoping on keys
// it has seen before, with the top level scoping being the environment variables. We
// ignore array contexts and only process the map contexts..
//
// Returns true for ok if it finds something, similar to map.
func (p *parser) lookupVariable(varReference string) (any, bool, error) {
// Do special check to see if it is a raw bcrypt string.
if strings.HasPrefix(varReference, bcryptPrefix) {
return "$" + varReference, true, nil
}
// Loop through contexts currently on the stack.
for i := len(p.ctxs) - 1; i >= 0; i-- {
ctx := p.ctxs[i]
// Process if it is a map context
if m, ok := ctx.(map[string]any); ok {
if v, ok := m[varReference]; ok {
return v, ok, nil
}
}
}
// If we are here, we have exhausted our context maps and still not found anything.
// Detect reference cycles
if p.envVarReferences[varReference] {
return nil, false, fmt.Errorf("variable reference cycle for '%s'", varReference)
}
p.envVarReferences[varReference] = true
defer delete(p.envVarReferences, varReference)
// Parse from the environment
if vStr, ok := os.LookupEnv(varReference); ok {
// Everything we get here will be a string value, so we need to process as a parser would.
if subp, err := parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p); err == nil {
v, ok := subp.mapping[pkey]
return v, ok, nil
} else {
return nil, false, err
}
}
return nil, false, nil
}
func (p *parser) setValue(val any) {
// Test to see if we are on an array or a map
// Array processing
if ctx, ok := p.ctx.([]any); ok {
p.ctx = append(ctx, val)
p.ctxs[len(p.ctxs)-1] = p.ctx
}
// Map processing
if ctx, ok := p.ctx.(map[string]any); ok {
key := p.popKey()
if p.pedantic {
// Change the position to the beginning of the key
// since more useful when reporting errors.
switch v := val.(type) {
case *token:
it := p.popItemKey()
v.item.pos = it.pos
v.item.line = it.line
ctx[key] = v
}
} else {
// FIXME(dlc), make sure to error if redefining same key?
ctx[key] = val
}
}
}

1034
conf/parse_test.go Normal file

File diff suppressed because it is too large Load Diff

3
doc/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Architecture Decision Records
The NATS ADR documents have moved to their [own repository](https://github.com/nats-io/nats-architecture-and-design/)

49
docker/Dockerfile.nightly Normal file
View File

@@ -0,0 +1,49 @@
FROM --platform=$BUILDPLATFORM golang:alpine AS builder
ARG VERSION="nightly"
ARG GIT_COMMIT
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=$TARGETOS \
GOARCH=$TARGETARCH \
GO111MODULE=on \
CGO_ENABLED=0
RUN apk add --no-cache ca-certificates
RUN update-ca-certificates
WORKDIR /src
RUN mkdir -p /src/out/$GOOS/$GOARCH
COPY ./nats-server/ /src/nats-server/
COPY ./nsc/ /src/nsc/
COPY ./natscli/ /src/natscli/
RUN cd /src/nats-server && go build -trimpath \
-ldflags "-w -X server.serverVersion=${VERSION},server.gitCommit=${GIT_COMMIT}" \
-o /src/out/$TARGETOS/$TARGETARCH/nats-server .
RUN cd /src/natscli && go build -trimpath \
-ldflags "-w -X main.version=${VERSION}" \
-o /src/out/$TARGETOS/$TARGETARCH/nats ./nats
RUN cd /src/nsc && go build -trimpath \
-o /src/out/$TARGETOS/$TARGETARCH/nsc .
FROM --platform=$TARGETPLATFORM alpine:latest
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache tzdata
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /src/out/$TARGETOS/$TARGETARCH/nats-server /bin/nats-server
COPY --from=builder /src/out/$TARGETOS/$TARGETARCH/nats /bin/nats
COPY --from=builder /src/out/$TARGETOS/$TARGETARCH/nsc /bin/nsc
EXPOSE 4222 8222 6222 5222
ENTRYPOINT ["/bin/nats-server"]

19
go.mod Normal file
View File

@@ -0,0 +1,19 @@
module github.com/nats-io/nats-server/v2
go 1.26.0
toolchain go1.26.8
require (
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op
github.com/google/go-tpm v0.9.8
github.com/klauspost/compress v1.20.0
github.com/minio/highwayhash v1.0.4
github.com/nats-io/jwt/v2 v2.8.2
github.com/nats-io/nats.go v1.51.0
github.com/nats-io/nkeys v0.4.16
github.com/nats-io/nuid v1.0.1
golang.org/x/crypto v0.57.0
golang.org/x/sys v0.48.0
golang.org/x/time v0.16.0
)

23
go.sum Normal file
View File

@@ -0,0 +1,23 @@
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4=
github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI=
github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=

View File

@@ -0,0 +1,27 @@
// Copyright 2022-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is used iff the `enable_antithesis_sdk` build tag is not present
//go:build !enable_antithesis_sdk
package antithesis
import (
"testing"
)
// AssertUnreachable this implementation is a NOOP
func AssertUnreachable(_ testing.TB, _ string, _ map[string]any) {}
// Assert this implementation is a NOOP
func Assert(_ testing.TB, _ bool, _ string, _ map[string]any) {}

View File

@@ -0,0 +1,111 @@
// Copyright 2022-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is used iff the `enable_antithesis_sdk` build tag is present
//go:build enable_antithesis_sdk
package antithesis
import (
"encoding/json"
"fmt"
"runtime/debug"
"testing"
"github.com/antithesishq/antithesis-sdk-go/assert"
)
// This file provides assertions utility functions suitable for use in tests.
// It is a thin wrapper around the Antithesis SDK.
//
// Notice that unlike other assertions libraries, a violation does not halt execution or fail the test.
// The effects of violating an assertion, are:
// 1. Print the violation message, prefixed by the test name in which it happened
// 2. Print the stack for the calling goroutine so each failed test has a clear trace of the failure
// 3. Invoke the underlying Antithesis assertion
//
// N.B. Enabling this module outside for tests running outside of Antithesis will enable 1 and 2 above, but not 3.
// therefore it can be useful to output additional test failure details when running tests locally or in CI.
// AssertUnreachable is used to flag code branches that should not get invoked ever.
// Example:
//
// pubAck, err := js.Publish(...)
//
// if err != nil {
// antithesis.AssertUnreachable(t, "Publish failed", map[string]any{"error": err.Error()})
// t.Fatalf("Publish failed with error: %s", err)
// }
func AssertUnreachable(t testing.TB, message string, details map[string]any) {
// Always print a message
fmt.Printf("{*} [%s] Assert Unreachable violation: %s\n", t.Name(), message)
if details != nil && len(details) > 0 {
fmt.Printf("{*} Details:\n")
jsonDetails, err := json.MarshalIndent(details, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(jsonDetails))
}
// Always print the stack trace
fmt.Printf("{*} Stack trace:\n")
debug.PrintStack()
// N.B. as of today, message (as-is) is the unique identifier of the event
// Therefore this will de-duplicate the same assertion failing in 2 different tests
// But not the same assertion failing at 2 different lines of the same test
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
// Fire assertion violation event (if Antithesis is enabled)
assert.Unreachable(messageWithTestName, details)
}
// Assert is used to check that some given condition is always true,
// Example:
//
// antithesis.Assert(t, sequence > lastSequence, "Non-monotonic stream sequence number", map[string]any{
// "stream": streamName,
// "connection_id": nc.Id(),
// "sequence": sequence,
// "lastSequence": lastSequence,
// })
func Assert(t testing.TB, condition bool, message string, details map[string]any) {
// Condition is true, nothing to do
if condition {
return
}
// Always print a message
fmt.Printf("{*} [%s] Assert violation: %s\n", t.Name(), message)
if details != nil && len(details) > 0 {
fmt.Printf("{*} Details:\n")
jsonDetails, err := json.MarshalIndent(details, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(jsonDetails))
}
// Always print the stack trace
fmt.Printf("{*} Stack trace:\n")
debug.PrintStack()
// N.B. as of today, message (as-is) is the unique identifier of the event
// Therefore this will de-duplicate the same assertion failing in 2 different tests
// But not the same assertion failing at 2 different lines of the same test
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
// Fire assertion violation event (if Antithesis is enabled)
assert.AlwaysOrUnreachable(false, messageWithTestName, details)
}

305
internal/ldap/dn.go Normal file
View File

@@ -0,0 +1,305 @@
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
// Portions copyright (c) 2015-2016 go-ldap Authors
package ldap
import (
"bytes"
"crypto/x509/pkix"
"encoding/asn1"
enchex "encoding/hex"
"errors"
"fmt"
"strings"
)
var attributeTypeNames = map[string]string{
"2.5.4.3": "CN",
"2.5.4.5": "SERIALNUMBER",
"2.5.4.6": "C",
"2.5.4.7": "L",
"2.5.4.8": "ST",
"2.5.4.9": "STREET",
"2.5.4.10": "O",
"2.5.4.11": "OU",
"2.5.4.17": "POSTALCODE",
// FIXME: Add others.
"0.9.2342.19200300.100.1.25": "DC",
}
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
type AttributeTypeAndValue struct {
// Type is the attribute type
Type string
// Value is the attribute value
Value string
}
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
type RelativeDN struct {
Attributes []*AttributeTypeAndValue
}
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
type DN struct {
RDNs []*RelativeDN
}
// FromCertSubject takes a pkix.Name from a cert and returns a DN
// that uses the same set. Does not support multi value RDNs.
func FromCertSubject(subject pkix.Name) (*DN, error) {
dn := &DN{
RDNs: make([]*RelativeDN, 0),
}
for i := len(subject.Names) - 1; i >= 0; i-- {
name := subject.Names[i]
oidString := name.Type.String()
typeName, ok := attributeTypeNames[oidString]
if !ok {
return nil, fmt.Errorf("invalid type name: %+v", name)
}
v, ok := name.Value.(string)
if !ok {
return nil, fmt.Errorf("invalid type value: %+v", v)
}
rdn := &RelativeDN{
Attributes: []*AttributeTypeAndValue{
{
Type: typeName,
Value: v,
},
},
}
dn.RDNs = append(dn.RDNs, rdn)
}
return dn, nil
}
// FromRawCertSubject takes a raw subject from a certificate
// and uses asn1.Unmarshal to get the individual RDNs in the
// original order, including multi-value RDNs.
func FromRawCertSubject(rawSubject []byte) (*DN, error) {
dn := &DN{
RDNs: make([]*RelativeDN, 0),
}
var rdns pkix.RDNSequence
_, err := asn1.Unmarshal(rawSubject, &rdns)
if err != nil {
return nil, err
}
for i := len(rdns) - 1; i >= 0; i-- {
rdn := rdns[i]
if len(rdn) == 0 {
continue
}
r := &RelativeDN{}
attrs := make([]*AttributeTypeAndValue, 0)
for j := len(rdn) - 1; j >= 0; j-- {
atv := rdn[j]
typeName := ""
name := atv.Type.String()
typeName, ok := attributeTypeNames[name]
if !ok {
return nil, fmt.Errorf("invalid type name: %+v", name)
}
value, ok := atv.Value.(string)
if !ok {
return nil, fmt.Errorf("invalid type value: %+v", atv.Value)
}
attr := &AttributeTypeAndValue{
Type: typeName,
Value: value,
}
attrs = append(attrs, attr)
}
r.Attributes = attrs
dn.RDNs = append(dn.RDNs, r)
}
return dn, nil
}
// ParseDN returns a distinguishedName or an error.
// The function respects https://tools.ietf.org/html/rfc4514
func ParseDN(str string) (*DN, error) {
dn := new(DN)
dn.RDNs = make([]*RelativeDN, 0)
rdn := new(RelativeDN)
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
buffer := bytes.Buffer{}
attribute := new(AttributeTypeAndValue)
escaping := false
unescapedTrailingSpaces := 0
stringFromBuffer := func() string {
s := buffer.String()
s = s[0 : len(s)-unescapedTrailingSpaces]
buffer.Reset()
unescapedTrailingSpaces = 0
return s
}
for i := 0; i < len(str); i++ {
char := str[i]
switch {
case escaping:
unescapedTrailingSpaces = 0
escaping = false
switch char {
case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
buffer.WriteByte(char)
continue
}
// Not a special character, assume hex encoded octet
if len(str) == i+1 {
return nil, errors.New("got corrupted escaped character")
}
dst := []byte{0}
n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
if err != nil {
return nil, fmt.Errorf("failed to decode escaped character: %s", err)
} else if n != 1 {
return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n)
}
buffer.WriteByte(dst[0])
i++
case char == '\\':
unescapedTrailingSpaces = 0
escaping = true
case char == '=':
attribute.Type = stringFromBuffer()
// Special case: If the first character in the value is # the following data
// is BER encoded. Throw an error since not supported right now.
if len(str) > i+1 && str[i+1] == '#' {
return nil, errors.New("unsupported BER encoding")
}
case char == ',' || char == '+':
// We're done with this RDN or value, push it
if len(attribute.Type) == 0 {
return nil, errors.New("incomplete type, value pair")
}
attribute.Value = stringFromBuffer()
rdn.Attributes = append(rdn.Attributes, attribute)
attribute = new(AttributeTypeAndValue)
if char == ',' {
dn.RDNs = append(dn.RDNs, rdn)
rdn = new(RelativeDN)
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
}
case char == ' ' && buffer.Len() == 0:
// ignore unescaped leading spaces
continue
default:
if char == ' ' {
// Track unescaped spaces in case they are trailing and we need to remove them
unescapedTrailingSpaces++
} else {
// Reset if we see a non-space char
unescapedTrailingSpaces = 0
}
buffer.WriteByte(char)
}
}
if buffer.Len() > 0 {
if len(attribute.Type) == 0 {
return nil, errors.New("DN ended with incomplete type, value pair")
}
attribute.Value = stringFromBuffer()
rdn.Attributes = append(rdn.Attributes, attribute)
dn.RDNs = append(dn.RDNs, rdn)
}
return dn, nil
}
// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// Returns true if they have the same number of relative distinguished names
// and corresponding relative distinguished names (by position) are the same.
func (d *DN) Equal(other *DN) bool {
if len(d.RDNs) != len(other.RDNs) {
return false
}
for i := range d.RDNs {
if !d.RDNs[i].Equal(other.RDNs[i]) {
return false
}
}
return true
}
// RDNsMatch returns true if the individual RDNs of the DNs
// are the same regardless of ordering.
func (d *DN) RDNsMatch(other *DN) bool {
if len(d.RDNs) != len(other.RDNs) {
return false
}
matched := make([]bool, len(other.RDNs))
for _, irdn := range d.RDNs {
found := false
for j, ordn := range other.RDNs {
if !matched[j] && irdn.Equal(ordn) {
matched[j] = true
found = true
break
}
}
if !found {
return false
}
}
return true
}
// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN.
// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com"
// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com"
// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com"
func (d *DN) AncestorOf(other *DN) bool {
if len(d.RDNs) >= len(other.RDNs) {
return false
}
// Take the last `len(d.RDNs)` RDNs from the other DN to compare against
otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):]
for i := range d.RDNs {
if !d.RDNs[i].Equal(otherRDNs[i]) {
return false
}
}
return true
}
// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues
// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type.
// The order of attributes is not significant.
// Case of attribute types is not significant.
func (r *RelativeDN) Equal(other *RelativeDN) bool {
if len(r.Attributes) != len(other.Attributes) {
return false
}
return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes)
}
func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
for _, attr := range attrs {
found := false
for _, myattr := range r.Attributes {
if myattr.Equal(attr) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue
// Case of the attribute type is not significant
func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
}

225
internal/ldap/dn_test.go Normal file
View File

@@ -0,0 +1,225 @@
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
// Portions copyright (c) 2015-2016 go-ldap Authors
// Static-Check Fixes Copyright 2024 The NATS Authors
package ldap
import (
"reflect"
"testing"
)
func TestSuccessfulDNParsing(t *testing.T) {
testcases := map[string]DN{
"": {[]*RelativeDN{}},
"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}},
{[]*AttributeTypeAndValue{{"dc", "dummy"}}},
{[]*AttributeTypeAndValue{{"dc", "com"}}}}},
"UID=jsmith,DC=example,DC=net": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"UID", "jsmith"}}},
{[]*AttributeTypeAndValue{{"DC", "example"}}},
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
"OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{
{[]*AttributeTypeAndValue{
{"OU", "Sales"},
{"CN", "J. Smith"}}},
{[]*AttributeTypeAndValue{{"DC", "example"}}},
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
//
// "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}}},
// "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}},
// {[]*AttributeTypeAndValue{{"DC", "net"}}}}},
"CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
" CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
` A = 1 , B = 2 `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"A", "1"}}},
{[]*AttributeTypeAndValue{{"B", "2"}}}}},
` A = 1 + B = 2 `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{
{"A", "1"},
{"B", "2"}}}}},
` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{{" A ", " 1 "}}},
{[]*AttributeTypeAndValue{{" B ", " 2 "}}}}},
` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{
{" A ", " 1 "},
{" B ", " 2 "}}}}},
}
for test, answer := range testcases {
dn, err := ParseDN(test)
if err != nil {
t.Error(err.Error())
continue
}
if !reflect.DeepEqual(dn, &answer) {
t.Errorf("Parsed DN %s is not equal to the expected structure", test)
t.Logf("Expected:")
for _, rdn := range answer.RDNs {
for _, attribs := range rdn.Attributes {
t.Logf("#%v\n", attribs)
}
}
t.Logf("Actual:")
for _, rdn := range dn.RDNs {
for _, attribs := range rdn.Attributes {
t.Logf("#%v\n", attribs)
}
}
}
}
}
func TestErrorDNParsing(t *testing.T) {
testcases := map[string]string{
"*": "DN ended with incomplete type, value pair",
"cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
"cn=Jim\\0": "got corrupted escaped character",
"DC=example,=net": "DN ended with incomplete type, value pair",
// "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string",
"test,DC=example,DC=com": "incomplete type, value pair",
"=test,DC=example,DC=com": "incomplete type, value pair",
}
for test, answer := range testcases {
_, err := ParseDN(test)
if err == nil {
t.Errorf("Expected %s to fail parsing but succeeded\n", test)
} else if err.Error() != answer {
t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error())
}
}
}
func TestDNEqual(t *testing.T) {
testcases := []struct {
A string
B string
Equal bool
}{
// Exact match
{"", "", true},
{"o=A", "o=A", true},
{"o=A", "o=B", false},
{"o=A,o=B", "o=A,o=B", true},
{"o=A,o=B", "o=A,o=C", false},
{"o=A+o=B", "o=A+o=B", true},
{"o=A+o=B", "o=A+o=C", false},
// Case mismatch in type is ignored
{"o=A", "O=A", true},
{"o=A,o=B", "o=A,O=B", true},
{"o=A+o=B", "o=A+O=B", true},
// Case mismatch in value is significant
{"o=a", "O=A", false},
{"o=a,o=B", "o=A,O=B", false},
{"o=a+o=B", "o=A+O=B", false},
// Multi-valued RDN order mismatch is ignored
{"o=A+o=B", "O=B+o=A", true},
// Number of RDN attributes is significant
{"o=A+o=B", "O=B+o=A+O=B", false},
// Missing values are significant
{"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter
{"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter
// Whitespace tests
// Matching
{
"cn=John Doe, ou=People, dc=sun.com",
"cn=John Doe, ou=People, dc=sun.com",
true,
},
// Difference in leading/trailing chars is ignored
{
"cn=John Doe, ou=People, dc=sun.com",
"cn=John Doe,ou=People,dc=sun.com",
true,
},
// Difference in values is significant
{
"cn=John Doe, ou=People, dc=sun.com",
"cn=John Doe, ou=People, dc=sun.com",
false,
},
}
for i, tc := range testcases {
a, err := ParseDN(tc.A)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
b, err := ParseDN(tc.B)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
if expected, actual := tc.Equal, a.Equal(b); expected != actual {
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
continue
}
if expected, actual := tc.Equal, b.Equal(a); expected != actual {
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
continue
}
}
}
func TestDNAncestor(t *testing.T) {
testcases := []struct {
A string
B string
Ancestor bool
}{
// Exact match returns false
{"", "", false},
{"o=A", "o=A", false},
{"o=A,o=B", "o=A,o=B", false},
{"o=A+o=B", "o=A+o=B", false},
// Mismatch
{"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false},
// Descendant
{"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true},
}
for i, tc := range testcases {
a, err := ParseDN(tc.A)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
b, err := ParseDN(tc.B)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual {
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
continue
}
}
}
func TestDNReverseMatchDuplicates(t *testing.T) {
// A has two CN=Alice RDNs and one O=NATS.
// B has one CN=Alice, one O=NATS, and one OU=Users.
// They have the same length (3).
dnA, _ := ParseDN("CN=Alice,CN=Alice,O=NATS")
dnB, _ := ParseDN("CN=Alice,O=NATS,OU=Users")
if dnA.RDNsMatch(dnB) {
t.Fatal("Expected RDNsMatch to return false for different RDN sets, but it returned true")
}
}

276
internal/ocsp/ocsp.go Normal file
View File

@@ -0,0 +1,276 @@
// Copyright 2019-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testhelper
import (
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/ocsp"
)
const (
defaultResponseTTL = 4 * time.Second
defaultAddress = "127.0.0.1:8888"
)
func NewOCSPResponderCustomAddress(t *testing.T, issuerCertPEM, issuerKeyPEM string, addr string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, addr, defaultResponseTTL, "")
}
func NewOCSPResponder(t *testing.T, issuerCertPEM, issuerKeyPEM string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, "")
}
func NewOCSPResponderDesignatedCustomAddress(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, addr string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, respCertPEM, respKeyPEM, true, addr, defaultResponseTTL, "")
}
func NewOCSPResponderPreferringHTTPMethod(t *testing.T, issuerCertPEM, issuerKeyPEM, method string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, method)
}
func NewOCSPResponderCustomTimeout(t *testing.T, issuerCertPEM, issuerKeyPEM string, responseTTL time.Duration) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, responseTTL, "")
}
func NewOCSPResponderBase(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, embed bool, addr string, responseTTL time.Duration, method string) *http.Server {
t.Helper()
var mu sync.Mutex
status := make(map[string]int)
issuerCert := parseCertPEM(t, issuerCertPEM)
respCert := parseCertPEM(t, respCertPEM)
respKey := parseKeyPEM(t, respKeyPEM)
mux := http.NewServeMux()
// The "/statuses/" endpoint is for directly setting a key-value pair in
// the CA's status database.
mux.HandleFunc("/statuses/", func(rw http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
key := r.URL.Path[len("/statuses/"):]
switch r.Method {
case "GET":
mu.Lock()
n, ok := status[key]
if !ok {
n = ocsp.Unknown
}
mu.Unlock()
fmt.Fprintf(rw, "%s %d", key, n)
case "POST":
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
n, err := strconv.Atoi(string(data))
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
status[key] = n
mu.Unlock()
fmt.Fprintf(rw, "%s %d", key, n)
default:
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
})
// The "/" endpoint is for normal OCSP requests. This actually parses an
// OCSP status request and signs a response with a CA. Lightly based off:
// https://www.ietf.org/rfc/rfc2560.txt
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
var reqData []byte
var err error
switch {
case r.Method == "GET":
if method != "" && r.Method != method {
http.Error(rw, "", http.StatusBadRequest)
return
}
reqData, err = base64.StdEncoding.DecodeString(r.URL.Path[1:])
case r.Method == "POST":
if method != "" && r.Method != method {
http.Error(rw, "", http.StatusBadRequest)
return
}
reqData, err = io.ReadAll(r.Body)
r.Body.Close()
default:
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
ocspReq, err := ocsp.ParseRequest(reqData)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
n, ok := status[ocspReq.SerialNumber.String()]
if !ok {
n = ocsp.Unknown
}
mu.Unlock()
tmpl := ocsp.Response{
Status: n,
SerialNumber: ocspReq.SerialNumber,
ThisUpdate: time.Now(),
}
if responseTTL != 0 {
tmpl.NextUpdate = tmpl.ThisUpdate.Add(responseTTL)
}
if embed {
tmpl.Certificate = respCert
}
respData, err := ocsp.CreateResponse(issuerCert, respCert, tmpl, respKey)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/ocsp-response")
rw.Header().Set("Content-Length", fmt.Sprint(len(respData)))
fmt.Fprint(rw, string(respData))
})
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: time.Second * 5,
}
go srv.ListenAndServe()
time.Sleep(1 * time.Second)
return srv
}
func parseCertPEM(t *testing.T, certPEM string) *x509.Certificate {
t.Helper()
block := parsePEM(t, certPEM)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse cert '%s': %s", certPEM, err)
}
return cert
}
func parseKeyPEM(t *testing.T, keyPEM string) crypto.Signer {
t.Helper()
block := parsePEM(t, keyPEM)
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("failed to parse ikey %s: %s", keyPEM, err)
}
}
keyc := key.(crypto.Signer)
return keyc
}
func parsePEM(t *testing.T, pemPath string) *pem.Block {
t.Helper()
data, err := os.ReadFile(pemPath)
if err != nil {
t.Fatal(err)
}
block, _ := pem.Decode(data)
if block == nil {
t.Fatalf("failed to decode PEM %s", pemPath)
}
return block
}
func GetOCSPStatus(s tls.ConnectionState) (*ocsp.Response, error) {
if len(s.VerifiedChains) == 0 {
return nil, fmt.Errorf("missing TLS verified chains")
}
chain := s.VerifiedChains[0]
if got, want := len(chain), 2; got < want {
return nil, fmt.Errorf("incomplete cert chain, got %d, want at least %d", got, want)
}
leaf, issuer := chain[0], chain[1]
resp, err := ocsp.ParseResponseForCert(s.OCSPResponse, leaf, issuer)
if err != nil {
return nil, fmt.Errorf("failed to parse OCSP response: %w", err)
}
if err := resp.CheckSignatureFrom(issuer); err != nil {
return resp, err
}
return resp, nil
}
func SetOCSPStatus(t *testing.T, ocspURL, certPEM string, status int) {
t.Helper()
cert := parseCertPEM(t, certPEM)
hc := &http.Client{Timeout: 10 * time.Second}
resp, err := hc.Post(
fmt.Sprintf("%s/statuses/%s", ocspURL, cert.SerialNumber),
"",
strings.NewReader(fmt.Sprint(status)),
)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read OCSP HTTP response body: %s", err)
}
if got, want := resp.Status, "200 OK"; got != want {
t.Error(strings.TrimSpace(string(data)))
t.Fatalf("unexpected OCSP HTTP set status, got %q, want %q", got, want)
}
}

View File

@@ -0,0 +1,127 @@
// Copyright 2019-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testhelper
// These routines need to be accessible in both the server and test
// directories, and tests importing a package don't get exported symbols from
// _test.go files in the imported package, so we put them here where they can
// be used freely.
import (
"fmt"
"strings"
"sync"
"testing"
)
type DummyLogger struct {
sync.Mutex
Msg string
AllMsgs []string
}
func (l *DummyLogger) CheckContent(t *testing.T, expectedStr string) {
t.Helper()
l.Lock()
defer l.Unlock()
if l.Msg != expectedStr {
t.Fatalf("Expected log to be: %v, got %v", expectedStr, l.Msg)
}
}
func (l *DummyLogger) aggregate() {
if l.AllMsgs != nil {
l.AllMsgs = append(l.AllMsgs, l.Msg)
}
}
func (l *DummyLogger) Noticef(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Errorf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Warnf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Fatalf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Debugf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Tracef(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
// NewDummyLogger creates a dummy logger and allows to ask for logs to be
// retained instead of just keeping the most recent. Use retain to provide an
// initial size estimate on messages (not to provide a max capacity).
func NewDummyLogger(retain uint) *DummyLogger {
l := &DummyLogger{}
if retain > 0 {
l.AllMsgs = make([]string, 0, retain)
}
return l
}
func (l *DummyLogger) Drain() {
l.Lock()
defer l.Unlock()
if l.AllMsgs == nil {
return
}
l.AllMsgs = make([]string, 0, len(l.AllMsgs))
}
func (l *DummyLogger) CheckForProhibited(t *testing.T, reason, needle string) {
t.Helper()
l.Lock()
defer l.Unlock()
if l.AllMsgs == nil {
t.Fatal("DummyLogger.CheckForProhibited called without AllMsgs being collected")
}
// Collect _all_ matches, rather than have to re-test repeatedly.
// This will particularly help with less deterministic tests with multiple matches.
shouldFail := false
for i := range l.AllMsgs {
if strings.Contains(l.AllMsgs[i], needle) {
t.Errorf("log contains %s: %v", reason, l.AllMsgs[i])
shouldFail = true
}
}
if shouldFail {
t.FailNow()
}
}

67
locksordering.txt Normal file
View File

@@ -0,0 +1,67 @@
Here is the list of some established lock ordering.
In this list, A -> B means that you can have A.Lock() then B.Lock(), not the opposite.
jetStream -> jsAccount -> Server -> client -> Account
jetStream -> jsAccount -> stream -> consumer
The "prMu" lock protects the peer reconcile signaling state on jetStream, so
signaling is cheap and never contends on the JS lock. It must not be held while
acquiring the JetStream lock.
jetStream -> prMu
A lock to protect jetstream account's usage has been introduced: jsAccount.usageMu.
This lock is independent and can be invoked under any other lock: jsAccount -> jsa.usageMu, stream -> jsa.usageMu, etc...
A lock to protect the account's leafnodes list was also introduced to
allow that lock to be held and the acquire a client lock which is not
possible with the normal account lock.
accountLeafList -> client
AccountResolver interface has various implementations, but assume: AccountResolver -> Server
A reloadMu lock was added to prevent newly connecting clients racing with the configuration reload.
This must be taken out as soon as a reload is about to happen before any other locks:
reloadMu -> Server
reloadMu -> optsMu
The "jscmMu" lock in the Account is used to serialise calls to checkJetStreamMigrate and
clearObserverState so that they cannot interleave which would leave Raft nodes in
inconsistent observer states.
jscmMu -> Account -> jsAccount
jscmMu -> stream.clsMu
jscmMu -> RaftNode
The "clsMu" lock protects the consumer list on a stream, used for signalling consumer activity.
stream -> clsMu
The "clMu" and "ddMu" locks protect clustered and dedupe state respectively.
The stream lock (`mset.mu`) is optional, but if holding "clMu" or "ddMu",
locking the stream lock afterward would violate locking order.
stream -> clMu
stream -> clMu -> ddMu
The "mset.batches.mu" lock protects the batching state without needing to hold the stream lock.
If "clMu" is used to commit a batch, it should only be acquired while already holding the batch lock.
stream -> mset.batches.mu -> clMu
The "isolateMu" lock isolates direct get and msg get reads from writes, so those reads do not
observe partially applied rollups or atomic batches. Other read paths (consumer delivery,
mirrors/sources, info requests, etc.) are sequentially consistent and may observe a prefix of
an inflight batch. It must be acquired before the stream lock, never while holding it.
isolateMu -> stream
The "eventIdsMu" lock protects the server's event ID generator. It may be acquired
while holding the Server lock, but the Server lock must not be acquired while
holding eventIdsMu.
Server -> eventIdsMu

405
logger/log.go Normal file
View File

@@ -0,0 +1,405 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package logger provides logging facilities for the NATS server
package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)
// Default file permissions for log files.
const defaultLogPerms = os.FileMode(0640)
// Logger is the server logger
type Logger struct {
sync.Mutex
logger *log.Logger
debug bool
trace bool
infoLabel string
warnLabel string
errorLabel string
fatalLabel string
debugLabel string
traceLabel string
fl *fileLogger
}
type LogOption interface {
isLoggerOption()
}
// LogUTC controls whether timestamps in the log output should be UTC or local time.
type LogUTC bool
func (l LogUTC) isLoggerOption() {}
func logFlags(time bool, opts ...LogOption) int {
flags := 0
if time {
flags = log.LstdFlags | log.Lmicroseconds
}
for _, opt := range opts {
switch v := opt.(type) {
case LogUTC:
if time && bool(v) {
flags |= log.LUTC
}
}
}
return flags
}
// NewStdLogger creates a logger with output directed to Stderr
func NewStdLogger(time, debug, trace, colors, pid bool, opts ...LogOption) *Logger {
flags := logFlags(time, opts...)
pre := ""
if pid {
pre = pidPrefix()
}
l := &Logger{
logger: log.New(os.Stderr, pre, flags),
debug: debug,
trace: trace,
}
if colors {
setColoredLabelFormats(l)
} else {
setPlainLabelFormats(l)
}
return l
}
// NewFileLogger creates a logger with output directed to a file
func NewFileLogger(filename string, time, debug, trace, pid bool, opts ...LogOption) *Logger {
flags := logFlags(time, opts...)
pre := ""
if pid {
pre = pidPrefix()
}
fl, err := newFileLogger(filename, pre, time)
if err != nil {
log.Fatalf("error opening file: %v", err)
return nil
}
l := &Logger{
logger: log.New(fl, pre, flags),
debug: debug,
trace: trace,
fl: fl,
}
fl.Lock()
fl.l = l
fl.Unlock()
setPlainLabelFormats(l)
return l
}
type writerAndCloser interface {
Write(b []byte) (int, error)
Close() error
Name() string
}
type fileLogger struct {
out int64
canRotate int32
sync.Mutex
l *Logger
f writerAndCloser
limit int64
olimit int64
pid string
time bool
closed bool
maxNumFiles int
}
func newFileLogger(filename, pidPrefix string, time bool) (*fileLogger, error) {
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
f, err := os.OpenFile(filename, fileflags, defaultLogPerms)
if err != nil {
return nil, err
}
stats, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
fl := &fileLogger{
canRotate: 0,
f: f,
out: stats.Size(),
pid: pidPrefix,
time: time,
}
return fl, nil
}
func (l *fileLogger) setLimit(limit int64) {
l.Lock()
l.olimit, l.limit = limit, limit
atomic.StoreInt32(&l.canRotate, 1)
rotateNow := l.out > l.limit
l.Unlock()
if rotateNow {
l.l.Noticef("Rotating logfile...")
}
}
func (l *fileLogger) setMaxNumFiles(max int) {
l.Lock()
l.maxNumFiles = max
l.Unlock()
}
func (l *fileLogger) logDirect(label, format string, v ...any) int {
var entrya = [256]byte{}
var entry = entrya[:0]
if l.pid != "" {
entry = append(entry, l.pid...)
}
if l.time {
now := time.Now()
year, month, day := now.Date()
hour, min, sec := now.Clock()
microsec := now.Nanosecond() / 1000
entry = append(entry, fmt.Sprintf("%04d/%02d/%02d %02d:%02d:%02d.%06d ",
year, month, day, hour, min, sec, microsec)...)
}
entry = append(entry, label...)
entry = append(entry, fmt.Sprintf(format, v...)...)
entry = append(entry, '\r', '\n')
l.f.Write(entry)
return len(entry)
}
func (l *fileLogger) logPurge(fname string) {
var backups []string
lDir := filepath.Dir(fname)
lBase := filepath.Base(fname)
entries, err := os.ReadDir(lDir)
if err != nil {
l.logDirect(l.l.errorLabel, "Unable to read directory %q for log purge (%v), will attempt next rotation", lDir, err)
return
}
for _, entry := range entries {
if entry.IsDir() || entry.Name() == lBase || !strings.HasPrefix(entry.Name(), lBase) {
continue
}
if stamp, found := strings.CutPrefix(entry.Name(), fmt.Sprintf("%s%s", lBase, ".")); found {
_, err := time.Parse("2006:01:02:15:04:05.999999999", strings.Replace(stamp, ".", ":", 5))
if err == nil {
backups = append(backups, entry.Name())
}
}
}
currBackups := len(backups)
maxBackups := l.maxNumFiles - 1
if currBackups > maxBackups {
// backups sorted oldest to latest based on timestamped lexical filename (ReadDir)
for i := 0; i < currBackups-maxBackups; i++ {
if err := os.Remove(filepath.Join(lDir, string(os.PathSeparator), backups[i])); err != nil {
l.logDirect(l.l.errorLabel, "Unable to remove backup log file %q (%v), will attempt next rotation", backups[i], err)
// Bail fast, we'll try again next rotation
return
}
l.logDirect(l.l.infoLabel, "Purged log file %q", backups[i])
}
}
}
func (l *fileLogger) Write(b []byte) (int, error) {
if atomic.LoadInt32(&l.canRotate) == 0 {
n, err := l.f.Write(b)
if err == nil {
atomic.AddInt64(&l.out, int64(n))
}
return n, err
}
l.Lock()
n, err := l.f.Write(b)
if err == nil {
l.out += int64(n)
if l.out > l.limit {
if err := l.f.Close(); err != nil {
l.limit *= 2
l.logDirect(l.l.errorLabel, "Unable to close logfile for rotation (%v), will attempt next rotation at size %v", err, l.limit)
l.Unlock()
return n, err
}
fname := l.f.Name()
now := time.Now()
bak := fmt.Sprintf("%s.%04d.%02d.%02d.%02d.%02d.%02d.%09d", fname,
now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(),
now.Second(), now.Nanosecond())
os.Rename(fname, bak)
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
f, err := os.OpenFile(fname, fileflags, defaultLogPerms)
if err != nil {
l.Unlock()
panic(fmt.Sprintf("Unable to re-open the logfile %q after rotation: %v", fname, err))
}
l.f = f
n := l.logDirect(l.l.infoLabel, "Rotated log, backup saved as %q", bak)
l.out = int64(n)
l.limit = l.olimit
if l.maxNumFiles > 0 {
l.logPurge(fname)
}
}
}
l.Unlock()
return n, err
}
func (l *fileLogger) close() error {
l.Lock()
if l.closed {
l.Unlock()
return nil
}
l.closed = true
l.Unlock()
return l.f.Close()
}
// SetSizeLimit sets the size of a logfile after which a backup
// is created with the file name + "year.month.day.hour.min.sec.nanosec"
// and the current log is truncated.
func (l *Logger) SetSizeLimit(limit int64) error {
l.Lock()
if l.fl == nil {
l.Unlock()
return fmt.Errorf("can set log size limit only for file logger")
}
fl := l.fl
l.Unlock()
fl.setLimit(limit)
return nil
}
// SetMaxNumFiles sets the number of archived log files that will be retained
func (l *Logger) SetMaxNumFiles(max int) error {
l.Lock()
if l.fl == nil {
l.Unlock()
return fmt.Errorf("can set log max number of files only for file logger")
}
fl := l.fl
l.Unlock()
fl.setMaxNumFiles(max)
return nil
}
// NewTestLogger creates a logger with output directed to Stderr with a prefix.
// Useful for tracing in tests when multiple servers are in the same pid
func NewTestLogger(prefix string, time bool) *Logger {
flags := 0
if time {
flags = log.LstdFlags | log.Lmicroseconds
}
l := &Logger{
logger: log.New(os.Stderr, prefix, flags),
debug: true,
trace: true,
}
setColoredLabelFormats(l)
return l
}
// Close implements the io.Closer interface to clean up
// resources in the server's logger implementation.
// Caller must ensure threadsafety.
func (l *Logger) Close() error {
if l.fl != nil {
return l.fl.close()
}
return nil
}
// Generate the pid prefix string
func pidPrefix() string {
return fmt.Sprintf("[%d] ", os.Getpid())
}
func setPlainLabelFormats(l *Logger) {
l.infoLabel = "[INF] "
l.debugLabel = "[DBG] "
l.warnLabel = "[WRN] "
l.errorLabel = "[ERR] "
l.fatalLabel = "[FTL] "
l.traceLabel = "[TRC] "
}
func setColoredLabelFormats(l *Logger) {
colorFormat := "[\x1b[%sm%s\x1b[0m] "
l.infoLabel = fmt.Sprintf(colorFormat, "32", "INF")
l.debugLabel = fmt.Sprintf(colorFormat, "36", "DBG")
l.warnLabel = fmt.Sprintf(colorFormat, "0;93", "WRN")
l.errorLabel = fmt.Sprintf(colorFormat, "31", "ERR")
l.fatalLabel = fmt.Sprintf(colorFormat, "31", "FTL")
l.traceLabel = fmt.Sprintf(colorFormat, "33", "TRC")
}
// Noticef logs a notice statement
func (l *Logger) Noticef(format string, v ...any) {
l.logger.Printf(l.infoLabel+format, v...)
}
// Warnf logs a notice statement
func (l *Logger) Warnf(format string, v ...any) {
l.logger.Printf(l.warnLabel+format, v...)
}
// Errorf logs an error statement
func (l *Logger) Errorf(format string, v ...any) {
l.logger.Printf(l.errorLabel+format, v...)
}
// Fatalf logs a fatal error
func (l *Logger) Fatalf(format string, v ...any) {
l.logger.Fatalf(l.fatalLabel+format, v...)
}
// Debugf logs a debug statement
func (l *Logger) Debugf(format string, v ...any) {
if l.debug {
l.logger.Printf(l.debugLabel+format, v...)
}
}
// Tracef logs a trace statement
func (l *Logger) Tracef(format string, v ...any) {
if l.trace {
l.logger.Printf(l.traceLabel+format, v...)
}
}

327
logger/log_test.go Normal file
View File

@@ -0,0 +1,327 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package logger
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
func TestStdLogger(t *testing.T) {
logger := NewStdLogger(false, false, false, false, false)
flags := logger.logger.Flags()
if flags != 0 {
t.Fatalf("Expected %v, received %v\n", 0, flags)
}
if logger.debug {
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
}
if logger.trace {
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
}
}
func TestStdLoggerWithDebugTraceAndTime(t *testing.T) {
logger := NewStdLogger(true, true, true, false, false)
flags := logger.logger.Flags()
if flags != log.LstdFlags|log.Lmicroseconds {
t.Fatalf("Expected %d, received %d\n", log.LstdFlags, flags)
}
if !logger.debug {
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
}
if !logger.trace {
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
}
}
func TestStdLoggerNotice(t *testing.T) {
expectOutput(t, func() {
logger := NewStdLogger(false, false, false, false, false)
logger.Noticef("foo")
}, "[INF] foo\n")
}
func TestStdLoggerNoticeWithColor(t *testing.T) {
expectOutput(t, func() {
logger := NewStdLogger(false, false, false, true, false)
logger.Noticef("foo")
}, "[\x1b[32mINF\x1b[0m] foo\n")
}
func TestStdLoggerDebug(t *testing.T) {
expectOutput(t, func() {
logger := NewStdLogger(false, true, false, false, false)
logger.Debugf("foo %s", "bar")
}, "[DBG] foo bar\n")
}
func TestStdLoggerDebugWithOutDebug(t *testing.T) {
expectOutput(t, func() {
logger := NewStdLogger(false, false, false, false, false)
logger.Debugf("foo")
}, "")
}
func TestStdLoggerTrace(t *testing.T) {
expectOutput(t, func() {
logger := NewStdLogger(false, false, true, false, false)
logger.Tracef("foo")
}, "[TRC] foo\n")
}
func TestStdLoggerTraceWithOutDebug(t *testing.T) {
expectOutput(t, func() {
logger := NewStdLogger(false, false, false, false, false)
logger.Tracef("foo")
}, "")
}
func TestFileLogger(t *testing.T) {
tmpDir := t.TempDir()
file := createFileAtDir(t, tmpDir, "nats-server:log_")
file.Close()
logger := NewFileLogger(file.Name(), false, false, false, false)
defer logger.Close()
logger.Noticef("foo")
buf, err := os.ReadFile(file.Name())
if err != nil {
t.Fatalf("Could not read logfile: %v", err)
}
if len(buf) <= 0 {
t.Fatal("Expected a non-zero length logfile")
}
if string(buf) != "[INF] foo\n" {
t.Fatalf("Expected '%s', received '%s'\n", "[INFO] foo", string(buf))
}
file = createFileAtDir(t, tmpDir, "nats-server:log_")
file.Close()
logger = NewFileLogger(file.Name(), true, false, true, true)
defer logger.Close()
logger.Errorf("foo")
buf, err = os.ReadFile(file.Name())
if err != nil {
t.Fatalf("Could not read logfile: %v", err)
}
if len(buf) <= 0 {
t.Fatal("Expected a non-zero length logfile")
}
str := string(buf)
errMsg := fmt.Sprintf("Expected '%s', received '%s'\n", "[pid] <date> [ERR] foo", str)
pidEnd := strings.Index(str, " ")
infoStart := strings.LastIndex(str, "[ERR]")
if pidEnd == -1 || infoStart == -1 {
t.Fatalf("%v", errMsg)
}
pid := str[0:pidEnd]
if pid[0] != '[' || pid[len(pid)-1] != ']' {
t.Fatalf("%v", errMsg)
}
date := str[pidEnd:infoStart]
dateRegExp := "[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}"
reg, err := regexp.Compile(dateRegExp)
if err != nil {
t.Fatalf("Compile date regexp error: %v", err)
}
if matched := reg.Match([]byte(date)); !matched {
t.Fatalf("Date string '%s' does not match '%s'", date, dateRegExp)
}
if !strings.HasSuffix(str, "[ERR] foo\n") {
t.Fatalf("%v", errMsg)
}
}
func TestFileLoggerSizeLimit(t *testing.T) {
// Create std logger
logger := NewStdLogger(true, false, false, false, true)
if err := logger.SetSizeLimit(1000); err == nil ||
!strings.Contains(err.Error(), "only for file logger") {
t.Fatalf("Expected error about being able to use only for file logger, got %v", err)
}
logger.Close()
tmpDir := t.TempDir()
file := createFileAtDir(t, tmpDir, "log_")
file.Close()
logger = NewFileLogger(file.Name(), true, false, false, true)
defer logger.Close()
logger.SetSizeLimit(1000)
for i := 0; i < 50; i++ {
logger.Noticef("This is a line in the log file")
}
files, err := os.ReadDir(tmpDir)
if err != nil {
t.Fatalf("Error reading logs dir: %v", err)
}
if len(files) == 1 {
t.Fatalf("Expected file to have been rotated")
}
lastBackup := files[len(files)-1]
if err := logger.Close(); err != nil {
t.Fatalf("Error closing log: %v", err)
}
content, err := os.ReadFile(file.Name())
if err != nil {
t.Fatalf("Error loading latest log: %v", err)
}
if !bytes.Contains(content, []byte("Rotated log")) ||
!bytes.Contains(content, []byte(lastBackup.Name())) {
t.Fatalf("Should be statement about rotated log and backup name, got %s", content)
}
tmpDir = t.TempDir()
// Recreate logger and don't set a limit
file = createFileAtDir(t, tmpDir, "log_")
file.Close()
logger = NewFileLogger(file.Name(), true, false, false, true)
defer logger.Close()
for i := 0; i < 50; i++ {
logger.Noticef("This is line %d in the log file", i+1)
}
files, err = os.ReadDir(tmpDir)
if err != nil {
t.Fatalf("Error reading logs dir: %v", err)
}
if len(files) != 1 {
t.Fatalf("Expected file to not be rotated")
}
// Now set a limit that is below current size
logger.SetSizeLimit(1000)
// Should have triggered rotation
files, err = os.ReadDir(tmpDir)
if err != nil {
t.Fatalf("Error reading logs dir: %v", err)
}
if len(files) <= 1 {
t.Fatalf("Expected file to have been rotated")
}
if err := logger.Close(); err != nil {
t.Fatalf("Error closing log: %v", err)
}
lastBackup = files[len(files)-1]
content, err = os.ReadFile(file.Name())
if err != nil {
t.Fatalf("Error loading latest log: %v", err)
}
if !bytes.Contains(content, []byte("Rotated log")) ||
!bytes.Contains(content, []byte(lastBackup.Name())) {
t.Fatalf("Should be statement about rotated log and backup name, got %s", content)
}
logger = NewFileLogger(file.Name(), true, false, false, true)
defer logger.Close()
logger.SetSizeLimit(1000)
// Check error on rotate.
logger.Lock()
logger.fl.Lock()
failClose := &fileLogFailClose{logger.fl.f, true}
logger.fl.f = failClose
logger.fl.Unlock()
logger.Unlock()
// Write a big line that will force rotation.
// Since we fail to close the log file, we should have bumped the limit to 2000
logger.Noticef("This is a big line: %v", make([]byte, 1000))
// Remove the failure
failClose.fail = false
// Write a big line that makes rotation happen
logger.Noticef("This is a big line: %v", make([]byte, 2000))
// Close
logger.Close()
files, err = os.ReadDir(tmpDir)
if err != nil {
t.Fatalf("Error reading logs dir: %v", err)
}
lastBackup = files[len(files)-1]
content, err = os.ReadFile(filepath.Join(tmpDir, lastBackup.Name()))
if err != nil {
t.Fatalf("Error reading backup file: %v", err)
}
if !bytes.Contains(content, []byte("on purpose")) || !bytes.Contains(content, []byte("size 2000")) {
t.Fatalf("Expected error that file could not rotated and max size bumped to 2000, got %s", content)
}
}
type fileLogFailClose struct {
writerAndCloser
fail bool
}
func (l *fileLogFailClose) Close() error {
if l.fail {
return fmt.Errorf("on purpose")
}
return l.writerAndCloser.Close()
}
func expectOutput(t *testing.T, f func(), expected string) {
old := os.Stderr // keep backup of the real stderr
r, w, _ := os.Pipe()
os.Stderr = w
f()
outC := make(chan string)
// copy the output in a separate goroutine so printing can't block indefinitely
go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
outC <- buf.String()
}()
os.Stderr.Close()
os.Stderr = old // restoring the real stdout
out := <-outC
if out != expected {
t.Fatalf("Expected '%s', received '%s'\n", expected, out)
}
}
func createFileAtDir(t *testing.T, dir, prefix string) *os.File {
t.Helper()
f, err := os.CreateTemp(dir, prefix)
if err != nil {
t.Fatal(err)
}
return f
}

132
logger/syslog.go Normal file
View File

@@ -0,0 +1,132 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package logger
import (
"fmt"
"log"
"log/syslog"
"net/url"
"os"
"strings"
)
// SysLogger provides a system logger facility
type SysLogger struct {
writer *syslog.Writer
debug bool
trace bool
}
// SetSyslogName sets the name to use for the syslog.
// Currently used only on Windows.
func SetSyslogName(name string) {}
// GetSysLoggerTag generates the tag name for use in syslog statements. If
// the executable is linked, the name of the link will be used as the tag,
// otherwise, the name of the executable is used. "nats-server" is the default
// for the NATS server.
func GetSysLoggerTag() string {
procName := os.Args[0]
if strings.ContainsRune(procName, os.PathSeparator) {
parts := strings.FieldsFunc(procName, func(c rune) bool {
return c == os.PathSeparator
})
procName = parts[len(parts)-1]
}
return procName
}
// NewSysLogger creates a new system logger
func NewSysLogger(debug, trace bool) *SysLogger {
w, err := syslog.New(syslog.LOG_DAEMON|syslog.LOG_NOTICE, GetSysLoggerTag())
if err != nil {
log.Fatalf("error connecting to syslog: %q", err.Error())
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
// NewRemoteSysLogger creates a new remote system logger
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
network, addr := getNetworkAndAddr(fqn)
w, err := syslog.Dial(network, addr, syslog.LOG_DEBUG, GetSysLoggerTag())
if err != nil {
log.Fatalf("error connecting to syslog: %q", err.Error())
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
func getNetworkAndAddr(fqn string) (network, addr string) {
u, err := url.Parse(fqn)
if err != nil {
log.Fatal(err)
}
network = u.Scheme
if network == "udp" || network == "tcp" {
addr = u.Host
} else if network == "unix" {
addr = u.Path
} else {
log.Fatalf("error invalid network type: %q", u.Scheme)
}
return
}
// Noticef logs a notice statement
func (l *SysLogger) Noticef(format string, v ...any) {
l.writer.Notice(fmt.Sprintf(format, v...))
}
// Warnf logs a warning statement
func (l *SysLogger) Warnf(format string, v ...any) {
l.writer.Warning(fmt.Sprintf(format, v...))
}
// Fatalf logs a fatal error
func (l *SysLogger) Fatalf(format string, v ...any) {
l.writer.Crit(fmt.Sprintf(format, v...))
}
// Errorf logs an error statement
func (l *SysLogger) Errorf(format string, v ...any) {
l.writer.Err(fmt.Sprintf(format, v...))
}
// Debugf logs a debug statement
func (l *SysLogger) Debugf(format string, v ...any) {
if l.debug {
l.writer.Debug(fmt.Sprintf(format, v...))
}
}
// Tracef logs a trace statement
func (l *SysLogger) Tracef(format string, v ...any) {
if l.trace {
l.writer.Notice(fmt.Sprintf(format, v...))
}
}

237
logger/syslog_test.go Normal file
View File

@@ -0,0 +1,237 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package logger
import (
"fmt"
"log"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
var serverFQN string
func TestSysLogger(t *testing.T) {
logger := NewSysLogger(false, false)
if logger.debug {
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
}
if logger.trace {
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
}
}
func TestSysLoggerWithDebugAndTrace(t *testing.T) {
logger := NewSysLogger(true, true)
if !logger.debug {
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
}
if !logger.trace {
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
}
}
func testTag(t *testing.T, exePath, expected string) {
os.Args[0] = exePath
if result := GetSysLoggerTag(); result != expected {
t.Fatalf("Expected %s, received %s", expected, result)
}
}
func restoreArg(orig string) {
os.Args[0] = orig
}
func TestSysLoggerTagGen(t *testing.T) {
origArg := os.Args[0]
defer restoreArg(origArg)
testTag(t, "nats-server", "nats-server")
testTag(t, filepath.Join(".", "nats-server"), "nats-server")
testTag(t, filepath.Join("home", "bin", "nats-server"), "nats-server")
testTag(t, filepath.Join("..", "..", "nats-server"), "nats-server")
testTag(t, "nats-server.service1", "nats-server.service1")
testTag(t, "nats-server_service1", "nats-server_service1")
testTag(t, "nats-server-service1", "nats-server-service1")
testTag(t, "nats-server service1", "nats-server service1")
}
func TestSysLoggerTag(t *testing.T) {
origArg := os.Args[0]
defer restoreArg(origArg)
os.Args[0] = "ServerLoggerTag"
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, true, true)
logger.Noticef("foo")
line := <-done
data := strings.Split(line, "[")
if len(data) != 2 {
t.Fatalf("Unexpected syslog line %s\n", line)
}
if !strings.Contains(data[0], os.Args[0]) {
t.Fatalf("Expected '%s', received '%s'\n", os.Args[0], data[0])
}
}
func TestRemoteSysLogger(t *testing.T) {
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, true, true)
if !logger.debug {
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
}
if !logger.trace {
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
}
logger.Noticef("foo")
<-done
}
func TestRemoteSysLoggerNotice(t *testing.T) {
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, true, true)
logger.Noticef("foo %s", "bar")
expectSyslogOutput(t, <-done, "foo bar\n")
}
func TestRemoteSysLoggerDebug(t *testing.T) {
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, true, true)
logger.Debugf("foo %s", "qux")
expectSyslogOutput(t, <-done, "foo qux\n")
}
func TestRemoteSysLoggerDebugDisabled(t *testing.T) {
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, false, false)
logger.Debugf("foo %s", "qux")
rcvd := <-done
if rcvd != "" {
t.Fatalf("Unexpected syslog response %s\n", rcvd)
}
}
func TestRemoteSysLoggerTrace(t *testing.T) {
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, true, true)
logger.Tracef("foo %s", "qux")
expectSyslogOutput(t, <-done, "foo qux\n")
}
func TestRemoteSysLoggerTraceDisabled(t *testing.T) {
done := make(chan string)
startServer(done)
logger := NewRemoteSysLogger(serverFQN, true, false)
logger.Tracef("foo %s", "qux")
rcvd := <-done
if rcvd != "" {
t.Fatalf("Unexpected syslog response %s\n", rcvd)
}
}
func TestGetNetworkAndAddrUDP(t *testing.T) {
n, a := getNetworkAndAddr("udp://foo.com:1000")
if n != "udp" {
t.Fatalf("Unexpected network %s\n", n)
}
if a != "foo.com:1000" {
t.Fatalf("Unexpected addr %s\n", a)
}
}
func TestGetNetworkAndAddrTCP(t *testing.T) {
n, a := getNetworkAndAddr("tcp://foo.com:1000")
if n != "tcp" {
t.Fatalf("Unexpected network %s\n", n)
}
if a != "foo.com:1000" {
t.Fatalf("Unexpected addr %s\n", a)
}
}
func TestGetNetworkAndAddrUnix(t *testing.T) {
n, a := getNetworkAndAddr("unix:///foo.sock")
if n != "unix" {
t.Fatalf("Unexpected network %s\n", n)
}
if a != "/foo.sock" {
t.Fatalf("Unexpected addr %s\n", a)
}
}
func expectSyslogOutput(t *testing.T, line string, expected string) {
data := strings.Split(line, "]: ")
if len(data) != 2 {
t.Fatalf("Unexpected syslog line %s\n", line)
}
if data[1] != expected {
t.Fatalf("Expected '%s', received '%s'\n", expected, data[1])
}
}
func runSyslog(c net.PacketConn, done chan<- string) {
var buf [4096]byte
var rcvd string
for {
n, _, err := c.ReadFrom(buf[:])
if err != nil || n == 0 {
break
}
rcvd += string(buf[:n])
}
done <- rcvd
}
func startServer(done chan<- string) {
c, e := net.ListenPacket("udp", "127.0.0.1:0")
if e != nil {
log.Fatalf("net.ListenPacket failed udp :0 %v", e)
}
serverFQN = fmt.Sprintf("udp://%s", c.LocalAddr().String())
c.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
go runSyslog(c, done)
}

112
logger/syslog_windows.go Normal file
View File

@@ -0,0 +1,112 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package logger logs to the windows event log
package logger
import (
"fmt"
"os"
"strings"
"golang.org/x/sys/windows/svc/eventlog"
)
var natsEventSource = "NATS-Server"
// SetSyslogName sets the name to use for the system log event source
func SetSyslogName(name string) {
natsEventSource = name
}
// SysLogger logs to the windows event logger
type SysLogger struct {
writer *eventlog.Log
debug bool
trace bool
}
// NewSysLogger creates a log using the windows event logger
func NewSysLogger(debug, trace bool) *SysLogger {
if err := eventlog.InstallAsEventCreate(natsEventSource, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
if !strings.Contains(err.Error(), "registry key already exists") {
panic(fmt.Sprintf("could not access event log: %v", err))
}
}
w, err := eventlog.Open(natsEventSource)
if err != nil {
panic(fmt.Sprintf("could not open event log: %v", err))
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
// NewRemoteSysLogger creates a remote event logger
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
w, err := eventlog.OpenRemote(fqn, natsEventSource)
if err != nil {
panic(fmt.Sprintf("could not open event log: %v", err))
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
func formatMsg(tag, format string, v ...any) string {
orig := fmt.Sprintf(format, v...)
return fmt.Sprintf("pid[%d][%s]: %s", os.Getpid(), tag, orig)
}
// Noticef logs a notice statement
func (l *SysLogger) Noticef(format string, v ...any) {
l.writer.Info(1, formatMsg("NOTICE", format, v...))
}
// Warnf logs a warning statement
func (l *SysLogger) Warnf(format string, v ...any) {
l.writer.Info(1, formatMsg("WARN", format, v...))
}
// Fatalf logs a fatal error
func (l *SysLogger) Fatalf(format string, v ...any) {
msg := formatMsg("FATAL", format, v...)
l.writer.Error(5, msg)
panic(msg)
}
// Errorf logs an error statement
func (l *SysLogger) Errorf(format string, v ...any) {
l.writer.Error(2, formatMsg("ERROR", format, v...))
}
// Debugf logs a debug statement
func (l *SysLogger) Debugf(format string, v ...any) {
if l.debug {
l.writer.Info(3, formatMsg("DEBUG", format, v...))
}
}
// Tracef logs a trace statement
func (l *SysLogger) Tracef(format string, v ...any) {
if l.trace {
l.writer.Info(4, formatMsg("TRACE", format, v...))
}
}

View File

@@ -0,0 +1,140 @@
// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package logger
import (
"os/exec"
"strings"
"testing"
"golang.org/x/sys/windows/svc/eventlog"
)
// Skips testing if we do not have privledges to run this test.
// This lets us skip the tests for general (non admin/system) users.
func checkPrivledges(t *testing.T) {
src := "NATS-eventlog-testsource"
defer eventlog.Remove(src)
if err := eventlog.InstallAsEventCreate(src, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
if strings.Contains(err.Error(), "Access is denied") {
// Skip this test because elevated privileges are required.
t.SkipNow()
}
// let the tests report other types of errors
}
}
// lastLogEntryContains reads the last entry (/c:1 /rd:true) written
// to the event log by the NATS-Server source, returning true if the
// passed text was found, false otherwise.
func lastLogEntryContains(t *testing.T, text string) bool {
var output []byte
var err error
cmd := exec.Command("wevtutil.exe", "qe", "Application", "/q:*[System[Provider[@Name='NATS-Server']]]",
"/rd:true", "/c:1")
if output, err = cmd.Output(); err != nil {
t.Fatalf("Unable to execute command: %v", err)
}
return strings.Contains(string(output), text)
}
// TestSysLogger tests event logging on windows
func TestSysLogger(t *testing.T) {
checkPrivledges(t)
logger := NewSysLogger(false, false)
if logger.debug {
t.Fatalf("Expected %t, received %t\n", false, logger.debug)
}
if logger.trace {
t.Fatalf("Expected %t, received %t\n", false, logger.trace)
}
logger.Noticef("%s", "Noticef")
if !lastLogEntryContains(t, "[NOTICE]: Noticef") {
t.Fatalf("missing log entry")
}
logger.Errorf("%s", "Errorf")
if !lastLogEntryContains(t, "[ERROR]: Errorf") {
t.Fatalf("missing log entry")
}
logger.Tracef("%s", "Tracef")
if lastLogEntryContains(t, "Tracef") {
t.Fatalf("should not contain log entry")
}
logger.Debugf("%s", "Debugf")
if lastLogEntryContains(t, "Debugf") {
t.Fatalf("should not contain log entry")
}
}
// TestSysLoggerWithDebugAndTrace tests event logging
func TestSysLoggerWithDebugAndTrace(t *testing.T) {
checkPrivledges(t)
logger := NewSysLogger(true, true)
if !logger.debug {
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
}
if !logger.trace {
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
}
logger.Tracef("%s", "Tracef")
if !lastLogEntryContains(t, "[TRACE]: Tracef") {
t.Fatalf("missing log entry")
}
logger.Debugf("%s", "Debugf")
if !lastLogEntryContains(t, "[DEBUG]: Debugf") {
t.Fatalf("missing log entry")
}
}
// TestRemoteSysLoggerWithDebugAndTrace tests remote event logging
func TestRemoteSysLoggerWithDebugAndTrace(t *testing.T) {
checkPrivledges(t)
logger := NewRemoteSysLogger("", true, true)
if !logger.debug {
t.Fatalf("Expected %t, received %t\n", true, logger.debug)
}
if !logger.trace {
t.Fatalf("Expected %t, received %t\n", true, logger.trace)
}
logger.Tracef("NATS %s", "[TRACE]: Remote Noticef")
if !lastLogEntryContains(t, "Remote Noticef") {
t.Fatalf("missing log entry")
}
}
func TestSysLoggerFatalf(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if !lastLogEntryContains(t, "[FATAL]: Fatalf") {
t.Fatalf("missing log entry")
}
}
}()
checkPrivledges(t)
logger := NewSysLogger(true, true)
logger.Fatalf("%s", "Fatalf")
t.Fatalf("did not panic when expected to")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
logos/nats-server.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

135
main.go Normal file
View File

@@ -0,0 +1,135 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
//go:generate go run server/errors_gen.go
import (
"flag"
"fmt"
"os"
"github.com/nats-io/nats-server/v2/server"
)
var usageStr = `
Usage: nats-server [options]
Server Options:
-a, --addr, --net <host> Bind to host address (default: 0.0.0.0)
-p, --port <port> Use port for clients (default: 4222)
-n, --name
--server_name <server_name> Server name (default: auto)
-P, --pid <file> File to store PID
-m, --http_port <port> Use port for http monitoring
-ms,--https_port <port> Use port for https monitoring
-c, --config <file> Configuration file
-t Test configuration and exit
-sl,--signal <signal>[=<pid>] Send signal to nats-server process (ldm, stop, quit, term, reopen, reload)
<pid> can be either a PID (e.g. 1) or the path to a PID file (e.g. /var/run/nats-server.pid)
--client_advertise <string> Client URL to advertise to other servers
--ports_file_dir <dir> Creates a ports file in the specified directory (<executable_name>_<pid>.ports).
Logging Options:
-l, --log <file> File to redirect log output
-T, --logtime Timestamp log entries (default: true)
--logtime_utc Timestamps in UTC instead of local timezone (default: false)
-s, --syslog Log to syslog or windows event log
-r, --remote_syslog <addr> Syslog server addr (udp://localhost:514)
-D, --debug Enable debugging output
-V, --trace Trace the raw protocol
-VV Verbose trace (traces system account as well)
-DV Debug and trace
-DVV Debug and verbose trace (traces system account as well)
--log_size_limit <limit> Logfile size limit (default: auto)
--max_traced_msg_len <len> Maximum printable length for traced messages (default: unlimited)
JetStream Options:
-js, --jetstream Enable JetStream functionality
-sd, --store_dir <dir> Set the storage directory
Authorization Options:
--user <user> User required for connections
--pass <password> Password required for connections
--auth <token> Authorization token required for connections
TLS Options:
--tls Enable TLS, do not verify clients (default: false)
--tlscert <file> Server certificate file
--tlskey <file> Private key for server certificate
--tlsverify Enable TLS, verify client certificates
--tlscacert <file> Client certificate CA for verification
Cluster Options:
--routes <rurl-1, rurl-2> Routes to solicit and connect
--cluster <cluster-url> Cluster URL for solicited routes
--cluster_name <string> Cluster Name, if not set one will be dynamically generated
--no_advertise <bool> Do not advertise known cluster information to clients
--cluster_advertise <string> Cluster URL to advertise to other servers
--connect_retries <number> For implicit routes, number of connect retries
--cluster_listen <url> Cluster url from which members can solicit routes
Profiling Options:
--profile <port> Profiling HTTP port
Common Options:
-h, --help Show this message
-v, --version Show version
--help_tls TLS help
`
// usage will print out the flag options for the server.
func usage() {
fmt.Printf("%s\n", usageStr)
os.Exit(0)
}
func main() {
exe := "nats-server"
// Create a FlagSet and sets the usage
fs := flag.NewFlagSet(exe, flag.ExitOnError)
fs.Usage = usage
// Configure the options from the flags/config file
opts, err := server.ConfigureOptions(fs, os.Args[1:],
server.PrintServerAndExit,
fs.Usage,
server.PrintTLSHelpAndDie)
if err != nil {
server.PrintAndDie(fmt.Sprintf("%s: %s", exe, err))
} else if opts.CheckConfig {
fmt.Fprintf(os.Stderr, "%s: configuration file %s is valid (%s)\n", exe, opts.ConfigFile, opts.ConfigDigest())
os.Exit(0)
}
// Redact secret arguments before expvar reads them.
server.RedactArgs(os.Args)
// Create the server with appropriate options.
s, err := server.NewServer(opts)
if err != nil {
server.PrintAndDie(fmt.Sprintf("%s: %s", exe, err))
}
// Configure the logger based on the flags.
s.ConfigureLogger()
// Start things up. Block here until done.
if err := server.Run(s); err != nil {
server.PrintAndDie(err.Error())
}
s.WaitForShutdown()
}

62
scripts/cov.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
# Run from directory above via ./scripts/cov.sh
check_file () {
# If the content of the file is simply "mode: atomic", then it means that the
# code coverage did not complete due to a panic in one of the tests.
if [[ $(cat ./cov/$2) == "mode: atomic" ]]; then
echo "#############################################"
echo "## Code coverage for $1 package failed ##"
echo "#############################################"
exit 1
fi
}
# Do not globally set the -e flag because we don't a flapper to prevent the push to coverall.
export GO111MODULE="on"
go install github.com/wadey/gocovmerge@latest
# Fail fast by checking if we can run gocovmerge
gocovmerge
if [[ $? != 0 ]]; then
echo "Unable to run gocovmerge"
exit 1
fi
rm -rf ./cov
mkdir cov
#
# Since it is difficult to get a full run without a flapper, do not use `-failfast`.
# It is better to have one flapper or two and still get the report than have
# to re-run the whole code coverage. One or two failed tests should not affect
# so much the code coverage.
#
# However, we need to take into account that if there is a panic in one test, all
# other tests in that package will not run, which then would cause the code coverage
# to drastically be lowered. In that case, we don't want the code coverage to be
# uploaded.
#
go test -v -covermode=atomic -coverprofile=./cov/conf.out ./conf -timeout=1h -tags=skip_no_race_tests
check_file "conf" "conf.out"
go test -v -covermode=atomic -coverprofile=./cov/internal.out ./internal/ldap -timeout=1h -tags=skip_no_race_tests
check_file "internal" "internal.out"
go test -v -covermode=atomic -coverprofile=./cov/log.out ./logger -timeout=1h -tags=skip_no_race_tests
check_file "logger" "log.out"
go test -v -covermode=atomic -coverprofile=./cov/server_avl.out ./server/avl -timeout=1h -tags=skip_no_race_tests
check_file "server_avl" "server_avl.out"
go test -v -covermode=atomic -coverprofile=./cov/server.out ./server -timeout=1h -tags=skip_no_race_tests
check_file "server" "server.out"
go test -v -covermode=atomic -coverprofile=./cov/test.out -coverpkg=./server ./test -timeout=1h -tags=skip_no_race_tests
check_file "test" "test.out"
# At this point, if that fails, we want the caller to know about the failure.
set -e
gocovmerge ./cov/*.out > acc.out
rm -rf ./cov
# If no argument passed, launch a browser to see the results.
if [[ $1 == "" ]]; then
go tool cover -html=acc.out
fi
set +e

138
scripts/runTestsOnTravis.sh Executable file
View File

@@ -0,0 +1,138 @@
#!/bin/sh
set -ex
if [ "$1" = "compile" ]; then
# First check that NATS builds.
go build -v;
# Now run the linters.
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.4;
golangci-lint run;
if [ "$TRAVIS_TAG" != "" ]; then
go test -v -run=TestVersionMatchesTag ./server -ldflags="-X=github.com/nats-io/nats-server/v2/server.serverVersion=$TRAVIS_TAG" -count=1 -vet=off
fi
elif [ "$1" = "build_only" ]; then
go build -v;
elif [ "$1" = "no_race_1_tests" ]; then
# Run tests without the `-race` flag. By convention, those tests start
# with `TestNoRace`. This will include all test files that have the
# "&& !skip_no_race_1_tests" in the go build directive.
go test -v -p=1 -run=TestNoRace ./... -tags=skip_no_race_2_tests -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "no_race_2_tests" ]; then
# Run tests without the `-race` flag. By convention, those tests start
# with `TestNoRace`. This will include all test files that have the
# "&& !skip_no_race_2_tests" in the go build directive.
go test -v -p=1 -run=TestNoRace ./... -tags=skip_no_race_1_tests -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "store_tests" ]; then
# Run store tests. By convention, all file store tests start with `TestFileStore`,
# and memory store tests start with `TestMemStore`.
go test $RACE -v -p=1 -run=Test\(Store\|FileStore\|MemStore\) ./server -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_tests" ]; then
# Run JetStream non-clustered tests. By convention, all JS tests start
# with `TestJetStream`. We exclude the clustered, super-clustered and
# consumer tests by using the appropriate tags.
go test $RACE -v -p=1 -run=TestJetStream ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_2,skip_js_cluster_tests_3,skip_js_cluster_tests_4,skip_js_super_cluster_tests,skip_js_consumer_tests -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_consumer_tests" ]; then
# Run JetStream consumer tests. All tests start with TestJetStreamConsumer prefix.
go test $RACE -v -p=1 -run=TestJetStreamConsumer ./server -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "raft_tests" ]; then
# Run the RAFT tests. All tests start with TestNRG prefix.
go test $RACE -v -p=1 ./server/... -run="^TestNRG" -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_cluster_tests_1" ]; then
# Run JetStream clustered tests. By convention, all JS cluster tests
# start with `TestJetStreamCluster`. Will run the first batch of tests,
# excluding others with use of proper tags.
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests_2,skip_js_cluster_tests_3,skip_js_cluster_tests_4 -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_cluster_tests_2" ]; then
# Run JetStream clustered tests. By convention, all JS cluster tests
# start with `TestJetStreamCluster`. Will run the second batch of tests,
# excluding others with use of proper tags.
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_3,skip_js_cluster_tests_4 -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_cluster_tests_3" ]; then
# Run JetStream clustered tests. By convention, all JS cluster tests
# start with `TestJetStreamCluster`. Will run the third batch of tests,
# excluding others with use of proper tags.
#
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_2,skip_js_cluster_tests_4 -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_cluster_tests_4" ]; then
# Run JetStream clustered tests. By convention, all JS cluster tests
# start with `TestJetStreamCluster`. Will run the third batch of tests,
# excluding others with use of proper tags.
#
go test $RACE -v -p=1 -run=TestJetStreamCluster ./server -tags=skip_js_cluster_tests,skip_js_cluster_tests_2,skip_js_cluster_tests_3 -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "js_super_cluster_tests" ]; then
# Run JetStream super clustered tests. By convention, all JS super cluster
# tests start with `TestJetStreamSuperCluster`.
go test $RACE -v -p=1 -run=TestJetStreamSuperCluster ./server -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "mqtt_tests" ]; then
# Run MQTT tests. By convention, all MQTT tests start with `TestMQTT`.
go test $RACE -v -p=1 -run=TestMQTT ./server -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "msgtrace_tests" ]; then
# Run Message Tracing tests. By convention, all message tracing tests start with `TestMsgTrace`.
go test $RACE -v -p=1 -run=TestMsgTrace ./server -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "srv_pkg_non_js_tests" ]; then
# Run all non JetStream tests in the server package. We exclude the
# store tests by using the `skip_store_tests` build tag, the JS tests
# by using `skip_js_tests`, MQTT tests by using `skip_mqtt_tests` and
# message tracing tests by using `skip_msgtrace_tests`.
# Ignore JWT and NRG tests here as they have their own matrix run.
# Also including the ldflag with the version since this includes the `TestVersionMatchesTag`.
go test $RACE -v -p=1 ./server/... -run="^Test(N[^R]|NR[^G]|J[^W]|JW[^T]|[^JN])" -ldflags="-X=github.com/nats-io/nats-server/v2/server.serverVersion=$TRAVIS_TAG" -tags=skip_store_tests,skip_js_tests,skip_mqtt_tests,skip_msgtrace_tests,skip_no_race_tests -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "jwt_tests" ]; then
# Run the JWT tests. All tests start with TestJWT.
go test $RACE -v -p=1 ./server/... -run="^TestJWT" -count=1 -vet=off -timeout=30m -failfast
elif [ "$1" = "non_srv_pkg_tests" ]; then
# Run all tests of all non server package.
go test $RACE -v -p=1 $(go list ./... | grep -v "/server") -count=1 -vet=off -timeout=30m -failfast
fi

39
scripts/updateCopyrights.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# Ensure script is run at the root of a git repository
git rev-parse --is-inside-work-tree &>/dev/null || { echo "Not inside a git repository"; exit 1; }
# Find all .go files tracked by git
git ls-files "*.go" | while read -r file; do
# Skip files that don't have a copyright belonging to "The NATS Authors"
current_copyright=$(grep -oE "^// Copyright [0-9]{4}(-[0-9]{4})? The NATS Authors" "$file" || echo "")
[[ -z "$current_copyright" ]] && continue
# Get the last commit year for the file, ignore commit messages containing the word "copyright"
last_year=$(git log --follow --format="%ad" --date=format:%Y --grep="(C|c)opyright" --invert-grep -n 1 -- "$file")
existing_years=$(echo "$current_copyright" | grep -oE "[0-9]{4}(-[0-9]{4})?")
# Determine the new copyright range
if [[ "$existing_years" =~ ^([0-9]{4})-([0-9]{4})$ ]]; then
first_year=${BASH_REMATCH[1]}
new_copyright="// Copyright $first_year-$last_year The NATS Authors"
elif [[ "$existing_years" =~ ^([0-9]{4})$ ]]; then
first_year=${BASH_REMATCH[1]}
if [[ "$first_year" == "$last_year" ]]; then
new_copyright="// Copyright $first_year The NATS Authors"
else
new_copyright="// Copyright $first_year-$last_year The NATS Authors"
fi
else
continue # If the format is somehow incorrect, skip the file
fi
# Update the first line
if sed --version &>/dev/null; then
# Linux sed
sed -i "1s|^// Copyright.*|$new_copyright|" "$file"
else
# BSD/macOS sed, needs -i ''
sed -i '' "1s|^// Copyright.*|$new_copyright|" "$file"
fi
done

595
server/README-MQTT.md Normal file
View File

@@ -0,0 +1,595 @@
**MQTT Implementation Overview**
Revision 1.1
Authors: Ivan Kozlovic, Lev Brouk
NATS Server currently supports most of MQTT 3.1.1. This document describes how
it is implemented.
It is strongly recommended to review the [MQTT v3.1.1
specifications](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
and get a detailed understanding before proceeding with this document.
# Contents
1. [Concepts](#1-concepts)
- [Server, client](#server-client)
- [Connection, client ID, session](#connection-client-id-session)
- [Packets, messages, and subscriptions](#packets-messages-and-subscriptions)
- [Quality of Service (QoS), publish identifier (PI)](#quality-of-service-qos-publish-identifier-pi)
- [Retained message](#retained-message)
- [Will message](#will-message)
2. [Use of JetStream](#2-use-of-jetstream)
- [JetStream API](#jetstream-api)
- [Streams](#streams)
- [Consumers and Internal NATS Subscriptions](#consumers-and-internal-nats-subscriptions)
3. [Lifecycles](#3-lifecycles)
- [Connection, Session](#connection-session)
- [Subscription](#subscription)
- [Message](#message)
- [Retained messages](#retained-messages)
4. [Implementation Notes](#4-implementation-notes)
- [Hooking into NATS I/O](#hooking-into-nats-io)
- [Session Management](#session-management)
- [Processing QoS acks: PUBACK, PUBREC, PUBCOMP](#processing-qos-acks-puback-pubrec-pubcomp)
- [Subject Wildcards](#subject-wildcards)
5. [Known issues](#5-known-issues)
# 1. Concepts
## Server, client
In the MQTT specification there are concepts of **Client** and **Server**, used
somewhat interchangeably with those of **Sender** and **Receiver**. A **Server**
acts as a **Receiver** when it gets `PUBLISH` messages from a **Sender**
**Client**, and acts as a **Sender** when it delivers them to subscribed
**Clients**.
In the NATS server implementation there are also concepts (types) `server` and
`client`. `client` is an internal representation of a (connected) client and
runs its own read and write loops. Both of these have an `mqtt` field that if
set makes them behave as MQTT-compliant.
The code and comments may sometimes be confusing as they refer to `server` and
`client` sometimes ambiguously between MQTT and NATS.
## Connection, client ID, session
When an MQTT client connects to a server, it must send a `CONNECT` packet to
create an **MQTT Connection**. The packet must include a **Client Identifier**.
The server will then create or load a previously saved **Session** for the (hash
of) the client ID.
## Packets, messages, and subscriptions
The low level unit of transmission in MQTT is a **Packet**. Examples of packets
are: `CONNECT`, `SUBSCRIBE`, `SUBACK`, `PUBLISH`, `PUBCOMP`, etc.
An **MQTT Message** starts with a `PUBLISH` packet that a client sends to the
server. It is then matched against the current **MQTT Subscriptions** and is
delivered to them as appropriate. During the message delivery the server acts as
an MQTT client, and the receiver acts as an MQTT server.
Internally we use **NATS Messages** and **NATS Subscriptions** to facilitate
message delivery. This may be somewhat confusing as the code refers to `msg` and
`sub`. What may be even more confusing is that some MQTT packets (specifically,
`PUBREL`) are represented as NATS messages, and that the original MQTT packet
"metadata" may be encoded as NATS message headers.
## Quality of Service (QoS), publish identifier (PI)
MQTT specifies 3 levels of quality of service (**QoS**):
- `0` for at most once. A single delivery attempt.
- `1` for at least once. Will try to redeliver until acknowledged by the
receiver.
- `2` for exactly once. See the [SPEC REF] for the acknowledgement flow.
QoS 1 and 2 messages need to be identified with publish identifiers (**PI**s). A
PI is a 16-bit integer that must uniquely identify a message for the duration of
the required exchange of acknowledgment packets.
Note that the QoS applies separately to the transmission of a message from a
sender client to the server, and from the server to the receiving client. There
is no protocol-level acknowledgements between the receiver and the original
sender. The sender passes the ownership of messages to the server, and the
server then delivers them at maximum possible QoS to the receivers
(subscribers). The PIs for in-flight outgoing messages are issued and stored per
session.
## Retained message
A **Retained Message** is not part of any MQTT session and is not removed when the
session that produced it goes away. Instead, the server needs to persist a
_single_ retained message per topic. When a subscription is started, the server
needs to send the “matching” retained messages, that is, messages that would
have been delivered to the new subscription should that subscription had been
running prior to the publication of this message.
Retained messages are removed when the server receives a retained message with
an empty body. Still, this retained message that serves as a “delete” of a
retained message will be processed as a normal published message.
Retained messages can have QoS.
## Will message
The `CONNECT` packet can contain information about a **Will Message** that needs to
be sent to any client subscribing on the Will topic/subject in the event that
the client is disconnected implicitly, that is, not as a result as the client
sending the `DISCONNECT` packet.
Will messages can have the retain flag and QoS.
# 2. Use of JetStream
The MQTT implementation relies heavily on JetStream. We use it to:
- Persist (and restore) the [Session](#connection-client-id-session) state.
- Store and retrieve [Retained messages](#retained-message).
- Persist incoming [QoS 1 and
2](#quality-of-service-qos-publish-identifier-pi) messages, and
re-deliver if needed.
- Store and de-duplicate incoming [QoS
2](#quality-of-service-qos-publish-identifier-pi) messages.
- Persist and re-deliver outgoing [QoS
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL` packets.
Here is the overview of how we set up and use JetStream **streams**,
**consumers**, and **internal NATS subscriptions**.
## JetStream API
All interactions with JetStream are performed via `mqttJSA` that sends NATS
requests to JetStream. Most are processed synchronously and await a response,
some (e.g. `jsa.sendAck()`) are sent asynchronously. JetStream API is usually
referred to as `jsa` in the code. No special locking is required to use `jsa`,
however the asynchronous use of JetStream may create race conditions with
delivery callbacks.
## Streams
We create the following streams unless they already exist. Failing to ensure the
streams would prevent the client from connecting.
Each stream is created with a replica value that is determined by the size of
the cluster but limited to 3. It can also be overwritten by the stream_replicas
option in the MQTT configuration block.
The streams are created the first time an Account Session Manager is initialized
and are used by all sessions in it. Note that to avoid race conditions, some
subscriptions are created first. The streams are never deleted. See
`mqttCreateAccountSessionManager()` for details.
1. `$MQTT_sess` stores persisted **Session** records. It filters on
`"$MQTT.sess.>` subject and has a “limits” policy with `MaxMsgsPer` setting
of 1.
2. `$MQTT_msgs` is used for **QoS 1 and 2 message delivery**.
It filters on `$MQTT.msgs.>` subject and has an “interest” policy.
3. `$MQTT_rmsgs` stores **Retained Messages**. They are all
stored (and filtered) on a single subject `$MQTT.rmsg`. This stream has a
limits policy.
4. `$MQTT_qos2in` stores and deduplicates **Incoming QoS 2 Messages**. It
filters on `$MQTT.qos2.in.>` and has a "limits" policy with `MaxMsgsPer` of
1.
5. `$MQTT_out` stores **Outgoing QoS 2** `PUBREL` packets. It filters on
`$MQTT.out.>` and has a "interest" retention policy.
## Consumers and Internal NATS Subscriptions
### Account Scope
- A durable consumer for [Retained Messages](#retained-message) -
`$MQTT_rmsgs_<server name hash>`
- A subscription to handle all [jsa](#jetstream-api) replies for the account.
- A subscription to replies to "session persist" requests, so that we can detect
the use of a session with the same client ID anywhere in the cluster.
- 2 subscriptions to support [retained messages](#retained-message):
`$MQTT.sub.<nuid>` for the messages themselves, and one to receive replies to
"delete retained message" JS API (on the JS reply subject var).
### Session Scope
When a new QoS 2 MQTT subscription is detected in a session, we ensure that
there is a durable consumer for [QoS
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL`s out for delivery -
`$MQTT_PUBREL_<session id hash>`
### Subscription Scope
For all MQTT subscriptions, regardless of their QoS, we create internal NATS subscriptions to
- `subject` (directly encoded from `topic`). This subscription is used to
deliver QoS 0 messages, and messages originating from NATS.
- if needed, `subject fwc` complements `subject` for topics like `topic.#` to
include `topic` itself, see [top-level wildcards](#subject-wildcards)
For QoS 1 or 2 MQTT subscriptions we ensure:
- A durable consumer for messages out for delivery - `<session ID hash>_<nuid>`
- An internal subscription to `$MQTT.sub.<nuid>` to deliver the messages to the
receiving client.
### (Old) Notes
As indicated before, for a QoS1 or QoS2 subscription, the server will create a
JetStream consumer with the appropriate subject filter. If the subscription
already existed, then only the NATS subscription is created for the JetStream
consumer’s delivery subject.
Note that JS consumers can be created with an “Replicas” override, which from
recent discussion is problematic with “Interest” policy streams, which
“$MQTT_msgs” is.
We do handle situations where a subscription on the same subject filter is sent
with a different QoS as per MQTT specifications. If the existing was on QoS 1 or
2, and the “new” is for QoS 0, then we delete the existing JS consumer.
Subscriptions that are QoS 0 have a NATS subscription with the callback function
being `mqttDeliverMsgCbQos0()`; while QoS 1 and 2 have a NATS subscription with
callback `mqttDeliverMsgCbQos12()`. Both those functions have comments that
describe the reason for their existence and what they are doing. For instance
the `mqttDeliverMsgCbQos0()` callback will reject any producing client that is
of type JETSTREAM, so that it handles only non JetStream (QoS 1 and 2) messages.
Both these functions end-up calling mqttDeliver() which will first enqueue the
possible retained messages buffer before delivering any new message. The message
itself being delivered is serialized in MQTT format and enqueued to the client’s
outbound buffer and call to addToPCD is made so that it is flushed out of the
readloop.
# 3. Lifecycles
## Connection, Session
An MQTT connection is created when a listening MQTT server receives a `CONNECT`
packet. See `mqttProcessConnect()`. A connection is associated with a session.
Steps:
1. Ensure that we have an `AccountSessionManager` so we can have an
`mqttSession`. Lazily initialize JetStream streams, and internal consumers
and subscriptions. See `getOrCreateMQTTAccountSessionManager()`.
2. Find and disconnect any previous session/client for the same ID. See
`mqttProcessConnect()`.
3. Ensure we have an `mqttSession` - create a new or load a previously persisted
one. If the clean flag is set in `CONNECT`, clean the session. see
`mqttSession.clear()`
4. Initialize session's subscriptions, if any.
5. Always send back a `CONNACK` packet. If there were errors in previous steps,
include the error.
An MQTT connection can be closed for a number of reasons, including receiving a
`DISCONNECT` from the client, explicit internal errors processing MQTT packets,
or the server receiving another `CONNECT` packet with the same client ID. See
`mqttHandleClosedClient()` and `mqttHandleWill()`. Steps:
1. Send out the Will Message if applicable (if not caused by a `DISCONNECT` packet)
2. Delete the JetStream consumers for to QoS 1 and 2 packet delivery through
JS API calls (if "clean" session flag is set)
3. Delete the session record from the “$MQTT_sess” stream, based on recorded
stream sequence. (if "clean" session flag is set)
4. Close the client connection.
On an explicit disconnect, that is, the client sends the DISCONNECT packet, the
server will NOT send the Will, as per specifications.
For sessions that had the “clean” flag, the JS consumers corresponding to QoS 1
subscriptions are deleted through JS API calls, the session record is then
deleted (based on recorded stream sequence) from the “$MQTT_sess” stream.
Finally, the client connection is closed
Sessions are persisted on disconnect, and on subscriptions changes.
## Subscription
Receiving an MQTT `SUBSCRIBE` packet creates new subscriptions, or updates
existing subscriptions in a session. Each `SUBSCRIBE` packet may contain several
specific subscriptions (`topic` + QoS in each). We always respond with a
`SUBACK`, which may indicate which subscriptions errored out.
For each subscription in the packet, we:
1. Ignore it if `topic` starts with `$MQTT.sub.`.
2. Set up QoS 0 message delivery - an internal NATS subscription on `topic`.
3. Replay any retained messages for `topic`, once as QoS 0.
4. If we already have a subscription on `topic`, update its QoS
5. If this is a QoS 2 subscription in the session, ensure we have the [PUBREL
consumer](#session-scope) for the session.
6. If this is a QoS 1 or 2 subscription, ensure we have the [Message
consumer](#subscription-scope) for this subscription (or delete one if it
exists and this is now a QoS 0 sub).
7. Add an extra subscription for the [top-level wildcard](#subject-wildcards) case.
8. Update the session, persist it if changed.
When a session is restored (no clean flag), we go through the same steps to
re-subscribe to its stored subscription, except step #8 which would have been
redundant.
When we get an `UNSUBSCRIBE` packet, it can contain multiple subscriptions to
unsubscribe. The parsing will generate a slice of mqttFilter objects that
contain the “filter” (the topic with possibly wildcard of the subscription) and
the QoS value. The server goes through the list and deletes the JS consumer (if
QoS 1 or 2) and unsubscribes the NATS subscription for the delivery subject (if
it was a QoS 1 or 2) or on the actual topic/subject. In case of the “#”
wildcard, the server will handle the “level up” subscriptions that NATS had to
create.
Again, we update the session and persist it as needed in the `$MQTT_sess`
stream.
## Message
1. Detect an incoming PUBLISH packet, parse and check the message QoS. Fill out
the session's `mqttPublish` struct that contains information about the
published message. (see `mqttParse()`, `mqttParsePub()`)
2. Process the message according to its QoS (see `mqttProcessPub()`)
- QoS 0:
- Initiate message delivery
- QoS 1:
- Initiate message delivery
- Send back a `PUBACK`
- QoS 2:
- Store the message in `$MQTT_qos2in` stream, using a PI-specific subject.
Since `MaxMsgsPer` is set to 1, we will ignore duplicates on the PI.
- Send back a `PUBREC`
- "Wait" for a `PUBREL`, then initiate message delivery
- Remove the previously stored QoS2 message
- Send back a `PUBCOMP`
3. Initiate message delivery (see `mqttInitiateMsgDelivery()`)
- Convert the MQTT `topic` into a NATS `subject` using
`mqttTopicToNATSPubSubject()` function. If there is a known subject
mapping, then we select the new subject using `selectMappedSubject()`
function and then convert back this subject into an MQTT topic using
`natsSubjectToMQTTTopic()` function.
- Re-serialize the `PUBLISH` packet received as a NATS message. Use NATS
headers for the metadata, and the deliverable MQTT `PUBLISH` packet as the
contents.
- Publish the messages as `subject` (and `subject fwc` if applicable, see
[subject wildcards](#subject-wildcards)). Use the "standard" NATS
`c.processInboundClientMsg()` to do that. `processInboundClientMsg()` will
distribute the message to any NATS subscriptions (including routes,
gateways, leafnodes) and the relevant MQTT subscriptions.
- Check for retained messages, process as needed. See
`c.processInboundClientMsg()` calling `c.mqttHandlePubRetain()` For MQTT
clients.
- If the message QoS is 1 or 2, store it in `$MQTT_msgs` stream as
`$MQTT.msgs.<subject>` for "at least once" delivery with retries.
4. Let NATS and JetStream deliver to the internal subscriptions, and to the
receiving clients. See `mqttDeliverMsgCb...()`
- The NATS message posted to `subject` (and `subject fwc`) will be delivered
to each relevant internal subscription by calling `mqttDeliverMsgCbQoS0()`.
The function has access to both the publishing and the receiving clients.
- Ignore all irrelevant invocations. Specifically, do nothing if the
message needs to be delivered with a higher QoS - that will be handled by
the other, `...QoS12` callback. Note that if the original message was
publuished with a QoS 1 or 2, but the subscription has its maximum QoS
set to 0, the message will be delivered by this callback.
- Ignore "reserved" subscriptions, as per MQTT spec.
- Decode delivery `topic` from the NATS `subject`.
- Write (enqueue) outgoing `PUBLISH` packet.
- **DONE for QoS 0**
- The NATS message posted to JetStream as `$MQTT.msgs.subject` will be
consumed by subscription-specific consumers. Note that MQTT subscriptions
with max QoS 0 do not have JetStream consumers. They are handled by the
QoS0 callback.
The consumers will deliver it to the `$MQTT.sub.<nuid>`
subject for their respective NATS subscriptions by calling
`mqttDeliverMsgCbQoS12()`. This callback too has access to both the
publishing and the receiving clients.
- Ignore "reserved" subscriptions, as per MQTT spec.
- See if this is a re-delivery from JetStream by checking `sess.cpending`
for the JS reply subject. If so, use the existing PI and treat this as a
duplicate redelivery.
- Otherwise, assign the message a new PI (see `trackPublish()` and
`bumpPI()`) and store it in `sess.cpending` and `sess.pendingPublish`,
along with the JS reply subject that can be used to remove this pending
message from the consumer once it's delivered to the receipient.
- Decode delivery `topic` from the NATS `subject`.
- Write (enqueue) outgoing `PUBLISH` packet.
5. QoS 1: "Wait" for a `PUBACK`. See `mqttProcessPubAck()`.
- When received, remove the PI from the tracking maps, send an ACK to
consumer to remove the message.
- **DONE for QoS 1**
6. QoS 2: "Wait" for a `PUBREC`. When received, we need to do all the same
things as in the QoS 1 `PUBACK` case, but we need to send out a `PUBREL`, and
continue using the same PI until the delivery flow is complete and we get
back a `PUBCOMP`. For that, we add the PI to `sess.pendingPubRel`, and to
`sess.cpending` with the PubRel consumer durable name.
We also compose and store a headers-only NATS message signifying a `PUBREL`
out for delivery, and store it in the `$MQTT_qos2out` stream, as
`$MQTT.qos2.out.<session-id>`.
7. QoS 2: Deliver `PUBREL`. The PubRel session-specific consumer will publish to
internal subscription on `$MQTT.qos2.delivery`, calling
`mqttDeliverPubRelCb()`. We store the ACK reply subject in `cpending` to
remove the JS message on `PUBCOMP`, compose and send out a `PUBREL` packet.
8. QoS 2: "Wait" for a `PUBCOMP`. See `mqttProcessPubComp()`.
- When received, remove the PI from the tracking maps, send an ACK to
consumer to remove the `PUBREL` message.
- **DONE for QoS 2**
## Retained messages
When we process an inbound `PUBLISH` and submit it to
`processInboundClientMsg()` function, for MQTT clients it will invoke
`mqttHandlePubRetain()` which checks if the published message is “retained” or
not.
If it is, then we construct a record representing the retained message and store
it in the `$MQTT_rmsg` stream, under the single `$MQTT.rmsg` subject. The stored
record (in JSON) contains information about the subject, topic, MQTT flags, user
that produced this message and the message content itself. It is stored and the
stream sequence is remembered in the memory structure that contains retained
messages.
Note that when creating an account session manager, the retained messages stream
is read from scratch to load all the messages through the use of a JS consumer.
The associated subscription will process the recovered retained messages or any
new that comes from the network.
A retained message is added to a map and a subscription is created and inserted
into a sublist that will be used to perform a ReverseMatch() when a subscription
is started and we want to find all retained messages that the subscription would
have received if it had been running prior to the message being published.
If a retained message on topic “foo” already exists, then the server has to
delete the old message at the stream sequence we saved when storing it.
This could have been done with having retained messages stored under
`$MQTT.rmsg.<subject>` as opposed to all under a single subject, and make use of
the `MaxMsgsPer` field set to 1. The `MaxMsgsPer` option was introduced well into
the availability of MQTT and changes to the sessions was made in [PR
#2501](https://github.com/nats-io/nats-server/pull/2501), with a conversion of
existing streams such as `$MQTT*sess*<sess ID>` into a single stream with unique
subjects, but the changes were not made to the retained messages stream.
There are also subscriptions for the handling of retained messages which are
messages that are asked by the publisher to be retained by the MQTT server to be
delivered to matching subscriptions when they start. There is a single message
per topic. Retained messages are deleted when the user sends a retained message
(there is a flag in the PUBLISH protocol) on a given topic with an empty body.
The difficulty with retained messages is to handle them in a cluster since all
servers need to be aware of their presence so that they can deliver them to
subscriptions that those servers may become the leader for.
- `$MQTT_rmsgs` which has a “limits” policy and holds retained messages, all
under `$MQTT.rmsg` single subject. Not sure why I did not use MaxMsgsPer for
this stream and not filter `$MQTT.rmsg.>`.
The first step when processing a new subscription is to gather the retained
messages that would be a match for this subscription. To do so, the server will
serialize into a buffer all messages for the account session manager’s sublist’s
ReverseMatch result. We use the returned subscriptions’ subject to find from a
map appropriate retained message (see `serializeRetainedMsgsForSub()` for
details).
# 4. Implementation Notes
## Hooking into NATS I/O
### Starting the accept loop
The MQTT accept loop is started when the server detects that an MQTT port has
been defined in the configuration file. It works similarly to all other accept
loops. Note that for MQTT over websocket, the websocket port has to be defined
and MQTT clients will connect to that port instead of the MQTT port and need to
provide `/mqtt` as part of the URL to redirect the creation of the client to an
MQTT client (with websocket support) instead of a regular NATS with websocket.
See the branching done in `startWebsocketServer()`. See `startMQTT()`.
### Starting the read/write loops
When a TCP connection is accepted, the internal go routine will invoke
`createMQTTClient()`. This function will set a `c.mqtt` object that will make it
become an MQTT client (through the `isMqtt()` helper function). The `readLoop()`
and `writeLoop()` are started similarly to other clients. However, the read loop
will branch out to `mqttParse()` instead when detecting that this is an MQTT
client.
## Session Management
### Account Session Manager
`mqttAccountSessionManager` is an object that holds the state of all sessions in
an account. It also manages the lifecycle of JetStream streams and internal
subscriptions for processing JS API replies, session updates, etc. See
`mqttCreateAccountSessionManager()`. It is lazily initialized upon the first
MQTT `CONNECT` packet received. Account session manager is referred to as `asm`
in the code.
Note that creating the account session manager (and attempting to create the
streams) is done only once per account on a given server, since once created the
account session manager for a given account would be found in the sessions map
of the mqttSessionManager object.
### Find and disconnect previous session/client
Once all that is done, we now go to the creation of the session object itself.
For that, we first need to make sure that it does not already exist, meaning
that it is registered on the server - or anywhere in the cluster. Note that MQTT
dictates that if a session with the same ID connects, the OLD session needs to
be closed, not the new one being created. NATS Server complies with this
requirement.
Once a session is detected to already exists, the old one (as described above)
is closed and the new one accepted, however, the session ID is maintained in a
flappers map so that we detect situations where sessions with the same ID are
started multiple times causing the previous one to be closed. When that
detection occurs, the newly created session is put in “jail” for a second to
avoid a very rapid succession of connect/disconnect. This has already been seen
by users since there was some issue there where we would schedule the connection
closed instead of waiting in place which was causing a panic.
We also protect from multiple clients on a given server trying to connect with
the same ID at the “same time” while the processing of a CONNECT of a session is
not yet finished. This is done with the use of a sessLocked map, keyed by the
session ID.
### Create or restore the session
If everything is good up to that point, the server will either create or restore
a session from the stream. This is done in the `createOrRestoreSession()`
function. The client/session ID is hashed and added to the session’s stream
subject along with the JS domain to prevent clients connecting from different
domains to “pollute” the session stream of a given domain.
Since each session constitutes a subject and the stream has a maximum of 1
message per subject, we attempt to load the last message on the formed subject.
If we don’t find it, then the session object is created “empty”, while if we
find a record, we create the session object based on the record persisted on the
stream.
If the session was restored from the JS stream, we keep track of the stream
sequence where the record was located. When we save the session (even if it
already exists) we will use this sequence number to set the
`JSExpectedLastSubjSeq` header so that we handle possibly different servers in a
(super)cluster to detect the race of clients trying to use the same session ID,
since only one of the write should succeed. On success, the session’s new
sequence is remembered by the server that did the write.
When created or restored, the CONNACK can now be sent back to the client, and if
there were any recovered subscriptions, they are now processed.
## Processing QoS acks: PUBACK, PUBREC, PUBCOMP
When the server delivers a message with QoS 1 or 2 (also a `PUBREL` for QoS 2) to a subscribed client, the client will send back an acknowledgement. See `mqttProcessPubAck()`, `mqttProcessPubRec()`, and `mqttProcessPubComp()`
While the specific logic for each packet differs, these handlers all update the
session's PI mappings (`cpending`, `pendingPublish`, `pendingPubRel`), and if
needed send an ACK to JetStream to remove the message from its consumer and stop
the re-delivery attempts.
## Subject Wildcards
Note that MQTT subscriptions have wildcards too, the `“+”` wildcard is equivalent
to NATS’s `“*”` wildcard, however, MQTT’s wildcard `“#”` is similar to `“>”`, except
that it also includes the level above. That is, a subscription on `“foo/#”` would
receive messages on `“foo/bar/baz”`, but also on `“foo”`.
So, for MQTT subscriptions enging with a `'#'` we are forced to create 2
internal NATS subscriptions, one on `“foo”` and one on `“foo.>”`.
# 5. Known issues
- "active" redelivery for QoS from JetStream (compliant, just a note)
- JetStream QoS redelivery happens out of (original) order
- finish delivery of in-flight messages after UNSUB
- finish delivery of in-flight messages after a reconnect
- consider replacing `$MQTT_msgs` with `$MQTT_out`.
- consider using unique `$MQTT.rmsg.>` and `MaxMsgsPer` for retained messages.
- add a cli command to list/clean old sessions

17
server/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Tests
Tests that run on Travis have been split into jobs that run in their own VM in parallel. This reduces the overall running time but also is allowing recycling of a job when we get a flapper as opposed to have to recycle the whole test suite.
## JetStream Tests
For JetStream tests, we need to observe a naming convention so that no tests are omitted when running on Travis.
The script `runTestsOnTravis.sh` will run a given job based on the definition found in "`.travis.yml`".
As for the naming convention:
- All JetStream test name should start with `TestJetStream`
- Cluster tests should go into `jetstream_cluster_test.go` and start with `TestJetStreamCluster`
- Super-cluster tests should go into `jetstream_super_cluster_test.go` and start with `TestJetStreamSuperCluster`
Not following this convention means that some tests may not be executed on Travis.

356
server/archive/archive.go Normal file
View File

@@ -0,0 +1,356 @@
// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package archive
import (
"bufio"
"encoding/binary"
"errors"
"io"
)
const MagicBytes = "NATSARC1"
// maxNameLen bounds the entry name length accepted when reading an archive,
// avoiding an unbounded allocation on corrupt or malicious input. The name is
// the only variable-length field read directly from the stream.
const maxNameLen = 1 << 20
var (
ErrClosed = errors.New("archive: closed")
ErrInvalidArchive = errors.New("archive: invalid archive stream")
ErrIncompleteEntry = errors.New("archive: entry not fully written")
ErrNoActiveEntry = errors.New("archive: no active entry")
ErrWriteTooLong = errors.New("archive: write exceeds declared entry size")
ErrNilHeader = errors.New("archive: nil header")
ErrNegativeEntrySize = errors.New("archive: negative entry size")
)
// Header describes one archive entry.
//
// On the wire each entry is the encoded header fields followed by the payload.
// HeaderSize and PayloadSize describe how that payload is split (e.g. message
// headers vs. body); the payload length is their sum and is not stored
// separately. Sequence is always encoded, with 0 meaning "unset".
type Header struct {
Name string
HeaderSize int64
PayloadSize int64
Timestamp int64
Sequence uint64
}
func (h *Header) clone() *Header {
if h == nil {
return nil
}
c := *h
return &c
}
// Writer writes a stream of archive entries.
type Writer struct {
w io.Writer
wroteMagic bool
header *Header
remaining int64
closed bool
}
// NewWriter returns a new stream writer.
func NewWriter(w io.Writer) *Writer {
return &Writer{w: w}
}
// WriteHeader starts a new archive entry.
func (a *Writer) WriteHeader(hdr *Header) error {
if a.closed {
return ErrClosed
}
if hdr == nil {
return ErrNilHeader
}
if hdr.HeaderSize < 0 || hdr.PayloadSize < 0 {
return ErrNegativeEntrySize
}
if a.header != nil && a.remaining > 0 {
return ErrIncompleteEntry
}
totalPayloadSize := hdr.HeaderSize + hdr.PayloadSize
if totalPayloadSize < 0 {
return ErrNegativeEntrySize
}
if !a.wroteMagic {
if _, err := io.WriteString(a.w, MagicBytes); err != nil {
return err
}
a.wroteMagic = true
}
if err := a.writeUvarint(uint64(len(hdr.Name))); err != nil {
return err
}
if err := a.writeVarint(hdr.Timestamp); err != nil {
return err
}
if err := a.writeUvarint(hdr.Sequence); err != nil {
return err
}
if err := a.writeUvarint(uint64(hdr.HeaderSize)); err != nil {
return err
}
if err := a.writeUvarint(uint64(hdr.PayloadSize)); err != nil {
return err
}
if _, err := io.WriteString(a.w, hdr.Name); err != nil {
return err
}
a.header = hdr.clone()
a.remaining = totalPayloadSize
if a.remaining == 0 {
a.header = nil
}
return nil
}
// Write writes payload bytes for the current entry.
func (a *Writer) Write(p []byte) (int, error) {
if a.closed {
return 0, ErrClosed
}
if len(p) == 0 {
return 0, nil
}
if a.header == nil {
return 0, ErrNoActiveEntry
}
wanted := len(p)
if int64(wanted) > a.remaining {
wanted = int(a.remaining)
}
n, err := a.w.Write(p[:wanted])
a.remaining -= int64(n)
if err != nil {
return n, err
}
if n < wanted {
return n, io.ErrShortWrite
}
if len(p) > wanted {
if a.remaining == 0 {
a.header = nil
}
return n, ErrWriteTooLong
}
if a.remaining == 0 {
a.header = nil
}
return n, nil
}
// Close finalizes the stream writer.
func (a *Writer) Close() error {
if a.closed {
return nil
}
a.closed = true
if a.header != nil && a.remaining > 0 {
return ErrIncompleteEntry
}
return nil
}
// Flush flushes the underlying writer if it supports flushing.
func (a *Writer) Flush() error {
if a.closed {
return ErrClosed
}
if f, ok := a.w.(interface{ Flush() error }); ok {
return f.Flush()
}
if f, ok := a.w.(interface{ Flush() }); ok {
f.Flush()
}
return nil
}
// Reader reads a stream of archive entries.
type Reader struct {
r *bufio.Reader
header *Header
remaining int64
seenMagic bool
err error
}
// NewReader returns a new stream reader.
func NewReader(r io.Reader) *Reader {
return &Reader{r: bufio.NewReader(r)}
}
func (u *Reader) discardCurrent() error {
if u.remaining == 0 {
u.header = nil
return nil
}
_, err := io.CopyN(io.Discard, u.r, u.remaining)
u.remaining = 0
u.header = nil
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return ErrInvalidArchive
}
return err
}
// Next advances to the next entry and returns its header.
func (u *Reader) Next() (*Header, error) {
if u.err != nil {
return nil, u.err
}
if err := u.discardCurrent(); err != nil {
u.err = err
return nil, err
}
if !u.seenMagic {
magic := make([]byte, len(MagicBytes))
if _, err := io.ReadFull(u.r, magic); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
u.err = io.EOF
return nil, io.EOF
}
u.err = err
return nil, err
}
if string(magic) != MagicBytes {
u.err = ErrInvalidArchive
return nil, ErrInvalidArchive
}
u.seenMagic = true
}
// A clean io.EOF on the first field is the normal end of the stream.
nameLen, err := u.readUvarint()
if err != nil {
u.err = err
return nil, err
}
timestamp, err := u.readVarint()
if err != nil {
u.err = noEOF(err)
return nil, u.err
}
sequence, err := u.readUvarint()
if err != nil {
u.err = noEOF(err)
return nil, u.err
}
headerSize, err := u.readUvarint()
if err != nil {
u.err = noEOF(err)
return nil, u.err
}
payloadSize, err := u.readUvarint()
if err != nil {
u.err = noEOF(err)
return nil, u.err
}
// HeaderSize/PayloadSize are stored as int64: a value above MaxInt64
// converts to a negative int64, and an overflowing sum lands negative too,
// so the sign checks below cover both cases.
hdrSize, plSize := int64(headerSize), int64(payloadSize)
total := hdrSize + plSize
if nameLen > maxNameLen || hdrSize < 0 || plSize < 0 || total < 0 {
u.err = ErrInvalidArchive
return nil, ErrInvalidArchive
}
name := make([]byte, nameLen)
if _, err := io.ReadFull(u.r, name); err != nil {
u.err = ErrInvalidArchive
return nil, ErrInvalidArchive
}
u.header = &Header{
Name: string(name),
Timestamp: timestamp,
Sequence: sequence,
HeaderSize: hdrSize,
PayloadSize: plSize,
}
u.remaining = total
return u.header.clone(), nil
}
// Read reads bytes from the current entry payload.
func (u *Reader) Read(p []byte) (int, error) {
if u.err != nil {
return 0, u.err
}
if len(p) == 0 {
return 0, nil
}
if u.header == nil || u.remaining == 0 {
return 0, io.EOF
}
if int64(len(p)) > u.remaining {
p = p[:int(u.remaining)]
}
n, err := io.ReadFull(u.r, p)
u.remaining -= int64(n)
if err != nil {
u.err = ErrInvalidArchive
return n, ErrInvalidArchive
}
if u.remaining == 0 {
u.header = nil
}
return n, nil
}
// noEOF converts a clean io.EOF into ErrInvalidArchive. It is used for header
// fields after the first: reaching EOF there means the entry was truncated
// rather than that the stream ended cleanly on an entry boundary.
func noEOF(err error) error {
if err != nil && errors.Is(err, io.EOF) {
return ErrInvalidArchive
}
return err
}
func (a *Writer) writeVarint(v int64) error {
var tmp [binary.MaxVarintLen64]byte
n := binary.PutVarint(tmp[:], v)
_, err := a.w.Write(tmp[:n])
return err
}
func (a *Writer) writeUvarint(v uint64) error {
var tmp [binary.MaxVarintLen64]byte
n := binary.PutUvarint(tmp[:], v)
_, err := a.w.Write(tmp[:n])
return err
}
func (u *Reader) readVarint() (int64, error) {
v, err := binary.ReadVarint(u.r)
if err != nil && !errors.Is(err, io.EOF) {
return v, ErrInvalidArchive
}
return v, err
}
func (u *Reader) readUvarint() (uint64, error) {
v, err := binary.ReadUvarint(u.r)
if err != nil && !errors.Is(err, io.EOF) {
return v, ErrInvalidArchive
}
return v, err
}

View File

@@ -0,0 +1,244 @@
// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package archive
import (
"bytes"
"errors"
"io"
"testing"
)
func TestRoundTrip(t *testing.T) {
var buf bytes.Buffer
w := NewWriter(&buf)
want := []struct {
hdr Header
data string
}{
{
hdr: Header{
Name: "alpha.txt",
Timestamp: 100,
HeaderSize: 4,
PayloadSize: 7,
},
data: "hello alpha",
},
{
hdr: Header{
Name: "nested/beta.txt",
Timestamp: 100,
Sequence: 42,
HeaderSize: 3,
PayloadSize: 9,
},
data: "beta payload",
},
}
for i := range want {
if err := w.WriteHeader(&want[i].hdr); err != nil {
t.Fatalf("write header %d: %v", i, err)
}
if _, err := io.WriteString(w, want[i].data); err != nil {
t.Fatalf("write payload %d: %v", i, err)
}
}
if err := w.Close(); err != nil {
t.Fatalf("close writer: %v", err)
}
r := NewReader(bytes.NewReader(buf.Bytes()))
for i := range want {
hdr, err := r.Next()
if err != nil {
t.Fatalf("next %d: %v", i, err)
}
if hdr.Name != want[i].hdr.Name {
t.Fatalf("header %d name mismatch: got %q want %q", i, hdr.Name, want[i].hdr.Name)
}
if hdr.Timestamp != want[i].hdr.Timestamp {
t.Fatalf("header %d timestamp mismatch: got %d want %d", i, hdr.Timestamp, want[i].hdr.Timestamp)
}
if hdr.Sequence != want[i].hdr.Sequence {
t.Fatalf("header %d sequence mismatch: got %d want %d", i, hdr.Sequence, want[i].hdr.Sequence)
}
if hdr.HeaderSize != want[i].hdr.HeaderSize {
t.Fatalf("header %d header size mismatch: got %d want %d", i, hdr.HeaderSize, want[i].hdr.HeaderSize)
}
if hdr.PayloadSize != want[i].hdr.PayloadSize {
t.Fatalf("header %d payload size mismatch: got %d want %d", i, hdr.PayloadSize, want[i].hdr.PayloadSize)
}
got, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read payload %d: %v", i, err)
}
if string(got) != want[i].data {
t.Fatalf("payload %d mismatch: got %q want %q", i, got, want[i].data)
}
}
if _, err := r.Next(); !errors.Is(err, io.EOF) {
t.Fatalf("expected EOF, got %v", err)
}
}
func TestNextSkipsUnreadPayload(t *testing.T) {
var buf bytes.Buffer
w := NewWriter(&buf)
first := Header{Name: "first", HeaderSize: 0, PayloadSize: int64(len("abcdef"))}
if err := w.WriteHeader(&first); err != nil {
t.Fatalf("write first header: %v", err)
}
if _, err := io.WriteString(w, "abcdef"); err != nil {
t.Fatalf("write first payload: %v", err)
}
second := Header{Name: "second", HeaderSize: 0, PayloadSize: int64(len("second-body"))}
if err := w.WriteHeader(&second); err != nil {
t.Fatalf("write second header: %v", err)
}
if _, err := io.WriteString(w, "second-body"); err != nil {
t.Fatalf("write second payload: %v", err)
}
r := NewReader(bytes.NewReader(buf.Bytes()))
hdr, err := r.Next()
if err != nil {
t.Fatalf("next first: %v", err)
}
if hdr.Name != "first" {
t.Fatalf("unexpected first header name %q", hdr.Name)
}
buf1 := make([]byte, 3)
if _, err := io.ReadFull(r, buf1); err != nil {
t.Fatalf("read partial first payload: %v", err)
}
if string(buf1) != "abc" {
t.Fatalf("unexpected partial payload %q", buf1)
}
hdr, err = r.Next()
if err != nil {
t.Fatalf("next second: %v", err)
}
if hdr.Name != "second" {
t.Fatalf("unexpected second header name %q", hdr.Name)
}
got, err := io.ReadAll(r)
if err != nil {
t.Fatalf("read second payload: %v", err)
}
if string(got) != "second-body" {
t.Fatalf("unexpected second payload %q", got)
}
}
// validArchive returns a well-formed archive holding a single entry whose
// payload is the 6-byte string "abcdef" split as HeaderSize=2, PayloadSize=4.
func validArchive(t *testing.T) []byte {
t.Helper()
var buf bytes.Buffer
w := NewWriter(&buf)
hdr := Header{Name: "first", Timestamp: 7, Sequence: 3, HeaderSize: 2, PayloadSize: 4}
if err := w.WriteHeader(&hdr); err != nil {
t.Fatalf("write header: %v", err)
}
if _, err := io.WriteString(w, "abcdef"); err != nil {
t.Fatalf("write payload: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("close: %v", err)
}
return buf.Bytes()
}
func TestEmptyArchiveReturnsEOF(t *testing.T) {
r := NewReader(bytes.NewReader(nil))
if _, err := r.Next(); !errors.Is(err, io.EOF) {
t.Fatalf("expected io.EOF on empty archive, got %v", err)
}
}
func TestInvalidMagicIsRejected(t *testing.T) {
r := NewReader(bytes.NewReader([]byte("not-a-valid-archive-stream")))
if _, err := r.Next(); !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("expected ErrInvalidArchive, got %v", err)
}
}
// A clean EOF on the first header field means end-of-stream, but an EOF
// partway through the header means the entry was truncated.
func TestTruncatedHeaderIsInvalid(t *testing.T) {
full := validArchive(t)
// Keep the magic plus the single name-length byte, dropping the rest.
r := NewReader(bytes.NewReader(full[:len(MagicBytes)+1]))
if _, err := r.Next(); !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("expected ErrInvalidArchive, got %v", err)
}
}
func TestTruncatedPayloadIsInvalid(t *testing.T) {
full := validArchive(t)
// Drop the last 3 payload bytes; the header still decodes cleanly.
r := NewReader(bytes.NewReader(full[:len(full)-3]))
hdr, err := r.Next()
if err != nil {
t.Fatalf("next: %v", err)
}
if hdr.HeaderSize+hdr.PayloadSize != 6 {
t.Fatalf("unexpected payload size %d", hdr.HeaderSize+hdr.PayloadSize)
}
if _, err := io.ReadAll(r); !errors.Is(err, ErrInvalidArchive) {
t.Fatalf("expected ErrInvalidArchive reading payload, got %v", err)
}
}
// The header round-trips when its fields are read directly from the stream,
// including a large Sequence (uvarint) and a negative Timestamp (varint).
func TestHeaderFieldEncoding(t *testing.T) {
var buf bytes.Buffer
w := NewWriter(&buf)
want := Header{Name: "nested/x.dat", Timestamp: -123, Sequence: 1 << 60, HeaderSize: 0, PayloadSize: 0}
if err := w.WriteHeader(&want); err != nil {
t.Fatalf("write header: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("close: %v", err)
}
r := NewReader(bytes.NewReader(buf.Bytes()))
got, err := r.Next()
if err != nil {
t.Fatalf("next: %v", err)
}
if *got != want {
t.Fatalf("header round-trip mismatch: got %+v want %+v", *got, want)
}
// A zero-length payload yields immediate EOF, then a clean end-of-stream.
if n, err := r.Read(make([]byte, 4)); n != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("expected (0, EOF) reading empty payload, got (%d, %v)", n, err)
}
if _, err := r.Next(); !errors.Is(err, io.EOF) {
t.Fatalf("expected io.EOF after last entry, got %v", err)
}
}

86
server/ats/ats.go Normal file
View File

@@ -0,0 +1,86 @@
// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ats controls the go routines for the access time service.
// This allows more efficient unixnano operations for cache access times.
// We will have one per binary (usually per server).
package ats
import (
"sync/atomic"
"time"
)
// Update every 100ms for gathering access time in unix nano.
const TickInterval = 100 * time.Millisecond
var (
// Our unix nano time.
utime atomic.Int64
// How may registered users do we have, controls lifetime of Go routine.
refs atomic.Int64
// To signal the shutdown of the Go routine.
done chan struct{}
)
func init() {
// Initialize our done chan.
done = make(chan struct{}, 1)
}
// Register usage. This will happen on filestore creation.
func Register() {
if v := refs.Add(1); v == 1 {
// This is the first to register (could also go up and down),
// so spin up Go routine and grab initial time.
utime.Store(time.Now().UnixNano())
go func() {
ticker := time.NewTicker(TickInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
utime.Store(time.Now().UnixNano())
case <-done:
return
}
}
}()
}
}
// Unregister usage. We will shutdown the go routine if no more registered users.
func Unregister() {
if v := refs.Add(-1); v == 0 {
done <- struct{}{}
} else if v < 0 {
refs.Store(0)
panic("unbalanced unregister for access time state")
}
}
// Will load the access time from an atomic.
// If no one has registered this will return 0 or stale data.
// It is the responsibility of the user to properly register and unregister.
func AccessTime() int64 {
// Return last updated time.
v := utime.Load()
if v == 0 {
// Always register a time, the worst case is a stale time.
// On startup, we can register in parallel and could previously panic.
v = time.Now().UnixNano()
utime.Store(v)
}
return v
}

98
server/ats/ats_test.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ats
import (
"runtime"
"testing"
"time"
)
func TestNotRunningValue(t *testing.T) {
// Set back to zero in case this test gets run multiple times via --count.
utime.Store(0)
at := AccessTime()
if at == 0 {
t.Fatal("Expected non-zero access time")
}
atn := AccessTime()
if atn != at {
t.Fatal("Did not expect updates to access time")
}
}
func TestRegisterAndUnregister(t *testing.T) {
ngrp := runtime.NumGoroutine()
Register()
if r := refs.Load(); r != 1 {
t.Fatalf("Expected refs to be 1, got %d", r)
}
// Make sure we have a non-zero time.
at := AccessTime()
if at == 0 {
t.Fatal("Expected non-zero access time")
}
// Make sure we are updating access time.
time.Sleep(2 * TickInterval)
atn := AccessTime()
if atn == at {
t.Fatal("Expected access time to be updated but it was not")
}
// Now unregister.
Unregister()
if r := refs.Load(); r != 0 {
t.Fatalf("Expected refs to be 0, got %d", r)
}
time.Sleep(TickInterval)
at = AccessTime()
time.Sleep(2 * TickInterval)
atn = AccessTime()
if atn != at {
t.Fatal("Did not expect updates to access time")
}
// Check that we have no additional go routines running.
ngra := runtime.NumGoroutine()
if ngra != ngrp {
t.Fatalf("Expected same number of go routines after removing all registered: %d vs %d", ngrp, ngra)
}
// Check that we spin back up on going from zero to one registered after spinning down.
Register()
defer Unregister()
at = AccessTime()
time.Sleep(2 * TickInterval)
atn = AccessTime()
if atn == at {
t.Fatal("Expected access time to be updated but it was not")
}
ngra = runtime.NumGoroutine()
if ngra != ngrp+1 {
t.Fatalf("Expected to see additional go routine: %d vs %d", ngrp, ngra)
}
}
func TestUnbalancedUnregister(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected function to panic, but it did not")
}
}()
Unregister()
}

1837
server/auth.go Normal file

File diff suppressed because it is too large Load Diff

503
server/auth_callout.go Normal file
View File

@@ -0,0 +1,503 @@
// Copyright 2022-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"bytes"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"time"
"unicode"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)
const (
AuthCalloutSubject = "$SYS.REQ.USER.AUTH"
AuthRequestSubject = "nats-authorization-request"
AuthRequestXKeyHeader = "Nats-Server-Xkey"
)
func titleCase(m string) string {
r := []rune(m)
if len(r) == 0 {
return _EMPTY_
}
return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...))
}
// Process a callout on this client's behalf.
func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequired, trustedProxy bool, ujwt string) (authorized bool, errStr string) {
isOperatorMode := len(opts.TrustedKeys) > 0
// this is the account the user connected in, or the one running the callout
var acc *Account
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.Account != _EMPTY_ {
aname := opts.AuthCallout.Account
var err error
acc, err = s.LookupAccount(aname)
if err != nil {
errStr = fmt.Sprintf("No valid account %q for auth callout request: %v", aname, err)
s.Warnf(errStr)
return false, errStr
}
} else {
acc = c.acc
}
if acc == nil {
// FIX for https://github.com/nats-io/nats-server/issues/7841
// hand rolled creds on leafnode became crasher here
errStr = fmt.Sprintf("%s not mapped to a callout account", c.kindString())
s.Warnf(errStr)
return false, errStr
}
// Check if we have been requested to encrypt.
var xkp nkeys.KeyPair
var xkey string
var pubAccXKey string
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.XKey != _EMPTY_ {
pubAccXKey = opts.AuthCallout.XKey
} else if isOperatorMode {
pubAccXKey = acc.externalAuthXKey()
}
// If set grab server's xkey keypair and public key.
if pubAccXKey != _EMPTY_ {
// These are only set on creation, so lock not needed.
xkp, xkey = s.xkp, s.info.XKey
}
// Create a keypair for the user. We will expect this public user to be in the signed response.
// This prevents replay attacks.
ukp, _ := nkeys.CreateUser()
pub, _ := ukp.PublicKey()
reply := s.newRespInbox()
respCh := make(chan string, 1)
decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
account := acc.Name
_, msg := rc.msgParts(rmsg)
// This signals not authorized.
// Since this is an account subscription will always have "\r\n".
if len(msg) <= LEN_CR_LF {
return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
}
// Strip trailing CRLF.
msg = msg[:len(msg)-LEN_CR_LF]
encrypted := false
// If we sent an encrypted request the response could be encrypted as well.
// we are expecting the input to be `eyJ` if it is a JWT
if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
var err error
msg, err = xkp.Open(msg, pubAccXKey)
if err != nil {
return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
}
encrypted = true
}
cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
if err != nil {
return nil, err
}
vr := jwt.CreateValidationResults()
cr.Validate(vr)
if len(vr.Issues) > 0 {
return nil, fmt.Errorf("authorization response had validation errors: %v", vr.Issues[0])
}
// the subject is the user id
if cr.Subject != pub {
return nil, errors.New("auth callout violation: auth callout response is not for expected user")
}
// check the audience to be the server ID
if cr.Audience != s.info.ID {
return nil, errors.New("auth callout violation: auth callout response is not for server")
}
// check if had an error message from the auth account
if cr.Error != _EMPTY_ {
return nil, fmt.Errorf("auth callout service returned an error: %v", cr.Error)
}
// if response is encrypted none of this is needed
if isOperatorMode && !encrypted {
pkStr := cr.Issuer
if cr.IssuerAccount != _EMPTY_ {
pkStr = cr.IssuerAccount
}
if pkStr != account {
if _, ok := acc.hasIssuer(pkStr); !ok {
return nil, errors.New("auth callout signing key is unknown")
}
}
}
return jwt.DecodeUserClaims(cr.Jwt)
}
// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
// only in operator mode we expect to receive `issuer_account`.
getIssuerAccount := func(arc *jwt.UserClaims, account string) (string, error) {
// Make sure correct issuer.
var issuer string
if opts.AuthCallout != nil {
issuer = opts.AuthCallout.Issuer
} else {
// Operator mode is who we send the request on unless switching accounts.
issuer = acc.Name
}
// the jwt issuer can be a signing key
jwtIssuer := arc.Issuer
if arc.IssuerAccount != _EMPTY_ {
if !isOperatorMode {
// this should be invalid - effectively it would allow the auth callout
// to issue on another account which may be allowed given the configuration
// where the auth callout account can handle multiple different ones..
return _EMPTY_, fmt.Errorf("error non operator mode account %q: attempted to use issuer_account", account)
}
jwtIssuer = arc.IssuerAccount
}
if jwtIssuer != issuer {
if !isOperatorMode {
return _EMPTY_, fmt.Errorf("wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
} else if !acc.isAllowedAcount(jwtIssuer) {
return _EMPTY_, fmt.Errorf("account %q not permitted as valid account option for auth callout for account %q",
arc.Issuer, account)
}
}
return jwtIssuer, nil
}
getExpirationAndAllowedConnections := func(arc *jwt.UserClaims, account string) (time.Duration, map[string]struct{}, error) {
allowNow, expiration := validateTimes(arc)
if !allowNow {
c.Errorf("Outside connect times")
return 0, nil, fmt.Errorf("authorized user on account %q outside of valid connect times", account)
}
allowedConnTypes, err := convertAllowedConnectionTypes(arc.User.AllowedConnectionTypes)
if err != nil {
c.Debugf("%v", err)
if len(allowedConnTypes) == 0 {
return 0, nil, fmt.Errorf("authorized user on account %q using invalid connection type", account)
}
}
return expiration, allowedConnTypes, nil
}
assignAccountAndPermissions := func(arc *jwt.UserClaims, account string) (*Account, error) {
// Apply to this client.
var err error
issuerAccount, err := getIssuerAccount(arc, account)
if err != nil {
return nil, err
}
// if we are not in operator mode, they can specify placement as a tag
var placement string
if !isOperatorMode {
// only allow placement if we are not in operator mode
placement = arc.Audience
} else {
placement = issuerAccount
}
targetAcc, err := s.LookupAccount(placement)
if err != nil {
return nil, fmt.Errorf("no valid account %q for auth callout response on account %q: %v", placement, account, err)
}
if isOperatorMode {
// this will validate the signing key that emitted the user, and if it is a signing
// key it assigns the permissions from the target account
if scope, ok := targetAcc.hasIssuer(arc.Issuer); !ok {
return nil, fmt.Errorf("user JWT issuer %q is not known", arc.Issuer)
} else if scope != nil {
// this possibly has to be different because it could just be a plain issued by a non-scoped signing key
if err := scope.ValidateScopedSigner(arc); err != nil {
return nil, fmt.Errorf("user JWT is not valid: %v", err)
} else if uSc, ok := scope.(*jwt.UserScope); !ok {
return nil, fmt.Errorf("user JWT is not a valid scoped user")
} else if arc.User.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, arc, targetAcc); err != nil {
return nil, fmt.Errorf("user JWT generated invalid permissions: %v", err)
}
}
}
return targetAcc, nil
}
processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
arc, err := decodeResponse(rc, rmsg, racc)
if err != nil {
c.authViolation()
respCh <- titleCase(err.Error())
return
}
// If the caller had established that the user should go through a proxy,
// or if the `arc` JWT requires it, and we don't have a trusted proxy,
// reject the connection.
if (proxyRequired || arc.ProxyRequired) && !trustedProxy {
err = ErrAuthProxyRequired
c.setAuthError(err)
c.authViolation()
respCh <- titleCase(err.Error())
return
}
vr := jwt.CreateValidationResults()
arc.Validate(vr)
if len(vr.Issues) > 0 {
c.authViolation()
respCh <- fmt.Sprintf("Error validating user JWT: %v", vr.Issues[0])
return
}
// Make sure that the user is what we requested.
if arc.Subject != pub {
c.authViolation()
respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, arc.Subject, racc.Name)
return
}
expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
if err != nil {
c.authViolation()
respCh <- titleCase(err.Error())
return
}
targetAcc, err := assignAccountAndPermissions(arc, racc.Name)
if err != nil {
c.authViolation()
respCh <- titleCase(err.Error())
return
}
// the JWT is cleared, because if in operator mode it may hold the JWT
// for the bearer token that connected to the callout if in operator mode
// the permissions are already set on the client, this prevents a decode
// on c.RegisterNKeyUser which would have wrong values
c.mu.Lock()
c.opts.JWT = _EMPTY_
c.mu.Unlock()
// Build internal user and bind to the targeted account.
nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
if err := c.RegisterNkeyUser(nkuser); err != nil {
c.authViolation()
respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
return
}
// See if the response wants to override the username.
if arc.Name != _EMPTY_ {
c.mu.Lock()
c.opts.Username = arc.Name
// Clear any others.
c.opts.Nkey = _EMPTY_
c.pubKey = _EMPTY_
c.opts.Token = _EMPTY_
c.mu.Unlock()
}
// Check if we need to set an auth timer if the user jwt expires.
c.setExpiration(arc.Claims(), expiration)
respCh <- _EMPTY_
}
// create a subscription to receive a response from the authcallout
sub, err := acc.subscribeInternal(reply, processReply)
if err != nil {
errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
s.Warnf(errStr)
return false, errStr
}
defer acc.unsubscribeInternal(sub)
// Build our request claims - jwt subject should be nkey
jwtSub := acc.Name
if opts.AuthCallout != nil {
jwtSub = opts.AuthCallout.Issuer
}
// The public key of the server, if set is available on Varz.Key
// This means that when a service connects, it can now peer
// authenticate if it wants to - but that also means that it needs to be
// listening to cluster changes
claim := jwt.NewAuthorizationRequestClaims(jwtSub)
claim.Audience = AuthRequestSubject
// Set expected public user nkey.
claim.UserNkey = pub
s.mu.RLock()
claim.Server = jwt.ServerID{
Name: s.info.Name,
Host: s.info.Host,
ID: s.info.ID,
Version: s.info.Version,
Cluster: s.info.Cluster,
}
s.mu.RUnlock()
// Tags
claim.Server.Tags = s.getOpts().Tags
// Check if we have been requested to encrypt.
// FIXME: possibly this public key also needs to be on the
// Varz, because then it can be peer verified?
if xkp != nil {
claim.Server.XKey = xkey
}
authTimeout := secondsToDuration(s.getOpts().AuthTimeout)
claim.Expires = time.Now().Add(time.Duration(authTimeout)).UTC().Unix()
// Grab client info for the request.
c.mu.Lock()
c.fillClientInfo(&claim.ClientInformation)
c.fillConnectOpts(&claim.ConnectOptions, ujwt)
// If we have a sig in the client opts, fill in nonce.
if claim.ConnectOptions.SignedNonce != _EMPTY_ {
claim.ClientInformation.Nonce = string(c.nonce)
}
// TLS
if c.flags.isSet(handshakeComplete) && c.nc != nil {
var ct jwt.ClientTLS
conn := c.nc.(*tls.Conn)
cs := conn.ConnectionState()
ct.Version = tlsVersion(cs.Version)
ct.Cipher = tls.CipherSuiteName(cs.CipherSuite)
// Check verified chains.
for _, vs := range cs.VerifiedChains {
var certs []string
for _, c := range vs {
blk := &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}
certs = append(certs, string(pem.EncodeToMemory(blk)))
}
ct.VerifiedChains = append(ct.VerifiedChains, certs)
}
// If we do not have verified chains put in peer certs.
if len(ct.VerifiedChains) == 0 {
for _, c := range cs.PeerCertificates {
blk := &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}
ct.Certs = append(ct.Certs, string(pem.EncodeToMemory(blk)))
}
}
claim.TLS = &ct
}
c.mu.Unlock()
b, err := claim.Encode(s.kp)
if err != nil {
errStr = fmt.Sprintf("Error encoding auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
req := []byte(b)
var hdr []byte
// Check if we have been asked to encrypt.
if xkp != nil {
req, err = xkp.Seal([]byte(req), pubAccXKey)
if err != nil {
errStr = fmt.Sprintf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
hdr = genHeader(hdr, AuthRequestXKeyHeader, xkey)
}
// Send out our request.
if err := s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false); err != nil {
errStr = fmt.Sprintf("Error sending authorization request: %v", err)
s.Debugf(errStr)
return false, errStr
}
select {
case errStr = <-respCh:
if authorized = errStr == _EMPTY_; !authorized {
s.Warnf(errStr)
}
case <-time.After(authTimeout):
s.Debugf(fmt.Sprintf("Authorization callout response not received in time on account %q", acc.Name))
}
return authorized, errStr
}
// Fill in client information for the request.
// Lock should be held.
func (c *client) fillClientInfo(ci *jwt.ClientInformation) {
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
return
}
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
*ci = jwt.ClientInformation{
Host: c.host,
ID: c.cid,
User: c.getRawAuthUser(),
Name: c.opts.Name,
Tags: c.tags,
NameTag: c.nameTag,
Kind: c.kindString(),
Type: c.clientTypeString(),
MQTT: c.getMQTTClientID(),
}
}
// Fill in client options.
// Lock should be held.
func (c *client) fillConnectOpts(opts *jwt.ConnectOptions, ujwt string) {
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
return
}
o := c.opts
if ujwt == _EMPTY_ {
// The caller may supply a reconstructed JWT that should be sent to auth
// callout without storing it in c.opts.JWT. If not, fall back to the client
// option as before.
ujwt = o.JWT
}
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
*opts = jwt.ConnectOptions{
JWT: ujwt,
Nkey: o.Nkey,
SignedNonce: o.Sig,
Token: o.Token,
Username: o.Username,
Password: o.Password,
Name: o.Name,
Lang: o.Lang,
Version: o.Version,
Protocol: o.Protocol,
}
}

3258
server/auth_callout_test.go Normal file

File diff suppressed because it is too large Load Diff

1246
server/auth_test.go Normal file

File diff suppressed because it is too large Load Diff

198
server/avl/norace_test.go Normal file
View File

@@ -0,0 +1,198 @@
// Copyright 2023-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !race && !skip_no_race_tests && !skip_no_race_1_tests
package avl
import (
"flag"
"fmt"
"math"
"math/rand/v2"
"runtime"
"runtime/debug"
"testing"
"time"
)
// Print Results: go test -v --args --results
var printResults = flag.Bool("results", false, "Enable Results Logging")
// SequenceSet memory tests vs dmaps.
func TestNoRaceSeqSetSizeComparison(t *testing.T) {
// Create 5M random entries (dupes possible but ok for this test) out of 8M range.
num := 5_000_000
max := 7_000_000
seqs := make([]uint64, 0, num)
for i := 0; i < num; i++ {
n := uint64(rand.Int64N(int64(max + 1)))
seqs = append(seqs, n)
}
runtime.GC()
// Disable to get stable results.
gcp := debug.SetGCPercent(-1)
defer debug.SetGCPercent(gcp)
mem := runtime.MemStats{}
runtime.ReadMemStats(&mem)
inUseBefore := mem.HeapInuse
dmap := make(map[uint64]struct{}, num)
for _, n := range seqs {
dmap[n] = struct{}{}
}
runtime.ReadMemStats(&mem)
dmapUse := mem.HeapInuse - inUseBefore
inUseBefore = mem.HeapInuse
// Now do SequenceSet on same dataset.
var sset SequenceSet
for _, n := range seqs {
sset.Insert(n)
}
runtime.ReadMemStats(&mem)
seqSetUse := mem.HeapInuse - inUseBefore
if seqSetUse > 2*1024*1024 {
t.Fatalf("Expected SequenceSet size to be < 2M, got %v", friendlyBytes(seqSetUse))
}
if seqSetUse*50 > dmapUse {
t.Fatalf("Expected SequenceSet to be at least 50x better then dmap approach: %v vs %v",
friendlyBytes(seqSetUse),
friendlyBytes(dmapUse),
)
}
}
func TestNoRaceSeqSetEncodeLarge(t *testing.T) {
num := 2_500_000
max := 5_000_000
dmap := make(map[uint64]struct{}, num)
var ss SequenceSet
for i := 0; i < num; i++ {
n := uint64(rand.Int64N(int64(max + 1)))
ss.Insert(n)
dmap[n] = struct{}{}
}
// Disable to get stable results.
gcp := debug.SetGCPercent(-1)
defer debug.SetGCPercent(gcp)
// In general should be about the same, but can see some variability.
expected := time.Millisecond
start := time.Now()
b := ss.Encode(nil)
if elapsed := time.Since(start); elapsed > expected {
t.Fatalf("Expected encode of %d items with encoded size %v to take less than %v, got %v",
num, friendlyBytes(len(b)), expected, elapsed)
} else {
logResults("Encode time for %d items was %v, encoded size is %v\n", num, elapsed, friendlyBytes(len(b)))
}
start = time.Now()
ss2, _, err := Decode(b)
require_NoError(t, err)
if elapsed := time.Since(start); elapsed > expected {
t.Fatalf("Expected decode to take less than %v, got %v", expected, elapsed)
} else {
logResults("Decode time is %v\n", elapsed)
}
require_True(t, ss.Nodes() == ss2.Nodes())
require_True(t, ss.Size() == ss2.Size())
}
func TestNoRaceSeqSetRelativeSpeed(t *testing.T) {
// Create 1M random entries (dupes possible but ok for this test) out of 3M range.
num := 1_000_000
max := 3_000_000
seqs := make([]uint64, 0, num)
for i := 0; i < num; i++ {
n := uint64(rand.Int64N(int64(max + 1)))
seqs = append(seqs, n)
}
start := time.Now()
// Now do SequenceSet on same dataset.
var sset SequenceSet
for _, n := range seqs {
sset.Insert(n)
}
ssInsertElapsed := time.Since(start)
logResults("Inserts SequenceSet: %v for %d items\n", ssInsertElapsed, num)
start = time.Now()
for _, n := range seqs {
if ok := sset.Exists(n); !ok {
t.Fatalf("Should exist")
}
}
ssLookupElapsed := time.Since(start)
logResults("Lookups: %v\n", ssLookupElapsed)
// Now do a map.
dmap := make(map[uint64]struct{})
start = time.Now()
for _, n := range seqs {
dmap[n] = struct{}{}
}
mapInsertElapsed := time.Since(start)
logResults("Inserts Map[uint64]: %v for %d items\n", mapInsertElapsed, num)
start = time.Now()
for _, n := range seqs {
if _, ok := dmap[n]; !ok {
t.Fatalf("Should exist")
}
}
mapLookupElapsed := time.Since(start)
logResults("Lookups: %v\n", mapLookupElapsed)
// In general we are between 1.5 and 1.75 times slower atm then a straight map.
// Let's test an upper bound of 2x for now.
if mapInsertElapsed*2 <= ssInsertElapsed {
t.Fatalf("Expected SequenceSet insert to be no more than 2x slower (%v vs %v)", mapInsertElapsed, ssInsertElapsed)
}
if mapLookupElapsed*3 <= ssLookupElapsed {
t.Fatalf("Expected SequenceSet lookups to be no more than 3x slower (%v vs %v)", mapLookupElapsed, ssLookupElapsed)
}
}
// friendlyBytes returns a string with the given bytes int64
// represented as a size, such as 1KB, 10MB, etc...
func friendlyBytes[T int | uint64 | int64](bytes T) string {
fbytes := float64(bytes)
base := 1024
pre := []string{"K", "M", "G", "T", "P", "E"}
if fbytes < float64(base) {
return fmt.Sprintf("%v B", fbytes)
}
exp := int(math.Log(fbytes) / math.Log(float64(base)))
index := exp - 1
return fmt.Sprintf("%.2f %sB", fbytes/math.Pow(float64(base), float64(exp)), pre[index])
}
func logResults(format string, args ...any) {
if *printResults {
fmt.Printf(format, args...)
}
}

715
server/avl/seqset.go Normal file
View File

@@ -0,0 +1,715 @@
// Copyright 2023-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package avl
import (
"cmp"
"encoding/binary"
"errors"
"math/bits"
"slices"
)
// SequenceSet is a memory and encoding optimized set for storing unsigned ints.
//
// SequenceSet is ~80-100 times more efficient memory wise than a map[uint64]struct{}.
// SequenceSet is ~1.75 times slower at inserts than the same map.
// SequenceSet is not thread safe.
//
// We use an AVL tree with nodes that hold bitmasks for set membership.
//
// Encoding will convert to a space optimized encoding using bitmasks.
type SequenceSet struct {
root *node // root node
size int // number of items
nodes int // number of nodes
// Having this here vs on the stack in Insert/Delete
// makes a difference in memory usage.
changed bool
}
// Insert will insert the sequence into the set.
// The tree will be balanced inline.
func (ss *SequenceSet) Insert(seq uint64) {
// If a node covering seq already exists, setting a bit can not change the
// tree shape, so skip the recursive descent and rebalance checks.
for n := ss.root; n != nil; {
if seq < n.base {
n = n.l
} else if seq >= n.base+numEntries {
n = n.r
} else {
n.set(seq, &ss.changed)
if ss.changed {
ss.changed = false
ss.size++
}
return
}
}
if ss.root = ss.root.insert(seq, &ss.changed, &ss.nodes); ss.changed {
ss.changed = false
ss.size++
}
}
// Exists will return true iff the sequence is a member of this set.
func (ss *SequenceSet) Exists(seq uint64) bool {
for n := ss.root; n != nil; {
if seq < n.base {
n = n.l
continue
} else if seq >= n.base+numEntries {
n = n.r
continue
}
return n.exists(seq)
}
return false
}
// SetInitialMin should be used to set the initial minimum sequence when known.
// This will more effectively utilize space versus self selecting.
// The set should be empty.
func (ss *SequenceSet) SetInitialMin(min uint64) error {
if !ss.IsEmpty() {
return ErrSetNotEmpty
}
ss.root, ss.nodes = &node{base: min, h: 1}, 1
return nil
}
// Delete will remove the sequence from the set.
// Will optionally remove nodes and rebalance.
// Returns where the sequence was set.
func (ss *SequenceSet) Delete(seq uint64) bool {
if ss == nil || ss.root == nil {
return false
}
ss.root = ss.root.delete(seq, &ss.changed, &ss.nodes)
if ss.changed {
ss.changed = false
ss.size--
if ss.size == 0 {
ss.Empty()
}
return true
}
return false
}
// Size returns the number of items in the set.
func (ss *SequenceSet) Size() int {
return ss.size
}
// Nodes returns the number of nodes in the tree.
func (ss *SequenceSet) Nodes() int {
return ss.nodes
}
// Empty will clear all items from a set.
func (ss *SequenceSet) Empty() {
ss.root = nil
ss.size = 0
ss.nodes = 0
}
// IsEmpty is a fast check of the set being empty.
func (ss *SequenceSet) IsEmpty() bool {
if ss == nil || ss.root == nil {
return true
}
return false
}
// Range will invoke the given function for each item in the set.
// They will range over the set in ascending order.
// If the callback returns false we terminate the iteration.
func (ss *SequenceSet) Range(f func(uint64) bool) {
ss.root.iter(f)
}
// Heights returns the left and right heights of the tree.
func (ss *SequenceSet) Heights() (l, r int) {
if ss.root == nil {
return 0, 0
}
if ss.root.l != nil {
l = ss.root.l.h
}
if ss.root.r != nil {
r = ss.root.r.h
}
return l, r
}
// Returns min, max and number of set items.
func (ss *SequenceSet) State() (min, max, num uint64) {
if ss == nil || ss.root == nil {
return 0, 0, 0
}
min, max = ss.MinMax()
return min, max, uint64(ss.Size())
}
// MinMax will return the minunum and maximum values in the set.
func (ss *SequenceSet) MinMax() (min, max uint64) {
if ss.root == nil {
return 0, 0
}
for l := ss.root; l != nil; l = l.l {
if l.l == nil {
min = l.min()
}
}
for r := ss.root; r != nil; r = r.r {
if r.r == nil {
max = r.max()
}
}
return min, max
}
func clone(src *node, target **node) {
if src == nil {
return
}
n := &node{base: src.base, bits: src.bits, h: src.h}
*target = n
clone(src.l, &n.l)
clone(src.r, &n.r)
}
// Clone will return a clone of the given SequenceSet.
func (ss *SequenceSet) Clone() *SequenceSet {
if ss == nil {
return nil
}
css := &SequenceSet{nodes: ss.nodes, size: ss.size}
clone(ss.root, &css.root)
return css
}
// Equal returns whether the two sets contain exactly the same sequences.
func (ss *SequenceSet) Equal(other *SequenceSet) bool {
if ss.IsEmpty() || other.IsEmpty() {
return ss.IsEmpty() && other.IsEmpty()
}
if ss.size != other.size {
return false
}
// Sizes are equal, so a one-way membership check suffices.
equal := true
ss.Range(func(seq uint64) bool {
equal = other.Exists(seq)
return equal
})
return equal
}
// Union will union this SequenceSet with ssa.
func (ss *SequenceSet) Union(ssa ...*SequenceSet) {
for _, sa := range ssa {
sa.root.nodeIter(func(n *node) {
for nb, b := range n.bits {
base := n.base + uint64(nb)*bitsPerBucket
for b != 0 {
ss.Insert(base + uint64(bits.TrailingZeros64(b)))
b &= b - 1
}
}
})
}
}
// Union will return a union of all sets.
func Union(ssa ...*SequenceSet) *SequenceSet {
if len(ssa) == 0 {
return nil
}
// Sort so we can clone largest.
slices.SortFunc(ssa, func(i, j *SequenceSet) int { return -cmp.Compare(i.Size(), j.Size()) }) // reverse order
ss := ssa[0].Clone()
// Insert the rest through range call.
for i := 1; i < len(ssa); i++ {
ssa[i].Range(func(n uint64) bool {
ss.Insert(n)
return true
})
}
return ss
}
const (
// Magic is used to identify the encode binary state..
magic = uint8(22)
// Version
version = uint8(2)
// hdrLen
hdrLen = 2
// minimum length of an encoded SequenceSet.
minLen = 2 + 8 // magic + version + num nodes + num entries.
)
// EncodeLen returns the bytes needed for encoding.
func (ss SequenceSet) EncodeLen() int {
return minLen + (ss.Nodes() * ((numBuckets+1)*8 + 2))
}
func (ss SequenceSet) Encode(buf []byte) []byte {
nn, encLen := ss.Nodes(), ss.EncodeLen()
if cap(buf) < encLen {
buf = make([]byte, encLen)
} else {
buf = buf[:encLen]
}
// TODO(dlc) - Go 1.19 introduced Append to not have to keep track.
// Once 1.20 is out we could change this over.
// Also binary.Write() is way slower, do not use.
var le = binary.LittleEndian
buf[0], buf[1] = magic, version
i := hdrLen
le.PutUint32(buf[i:], uint32(nn))
le.PutUint32(buf[i+4:], uint32(ss.size))
i += 8
ss.root.nodeIter(func(n *node) {
le.PutUint64(buf[i:], n.base)
i += 8
for _, b := range n.bits {
le.PutUint64(buf[i:], b)
i += 8
}
le.PutUint16(buf[i:], uint16(n.h))
i += 2
})
return buf[:i]
}
// ErrBadEncoding is returned when we can not decode properly.
var (
ErrBadEncoding = errors.New("ss: bad encoding")
ErrBadVersion = errors.New("ss: bad version")
ErrSetNotEmpty = errors.New("ss: set not empty")
)
// Decode returns the sequence set and number of bytes read from the buffer on success.
func Decode(buf []byte) (*SequenceSet, int, error) {
if len(buf) < minLen || buf[0] != magic {
return nil, -1, ErrBadEncoding
}
switch v := buf[1]; v {
case 1:
return decodev1(buf)
case 2:
return decodev2(buf)
default:
return nil, -1, ErrBadVersion
}
}
// Helper to decode v2.
func decodev2(buf []byte) (*SequenceSet, int, error) {
var le = binary.LittleEndian
index := 2
nn := int(le.Uint32(buf[index:]))
sz := int(le.Uint32(buf[index+4:]))
index += 8
// nn is decoded as a uint32 but held in an int. On 32-bit builds a value
// above MaxInt32 turns negative and nn*perNode below overflows, so the
// length check would pass for a short buffer and the following make/reads
// run off the end. Compare with division so the bound holds on every arch.
if nn < 0 || nn > (len(buf)-minLen)/((numBuckets+1)*8+2) {
return nil, -1, ErrBadEncoding
}
ss, nodes := SequenceSet{size: sz}, make([]node, nn)
for i := 0; i < nn; i++ {
n := &nodes[i]
n.base = le.Uint64(buf[index:])
index += 8
for bi := range n.bits {
n.bits[bi] = le.Uint64(buf[index:])
index += 8
}
n.h = int(le.Uint16(buf[index:]))
index += 2
ss.insertNode(n)
}
return &ss, index, nil
}
// Helper to decode v1 into v2 which has fixed buckets of 32 vs 64 originally.
func decodev1(buf []byte) (*SequenceSet, int, error) {
var le = binary.LittleEndian
index := 2
nn := int(le.Uint32(buf[index:]))
sz := int(le.Uint32(buf[index+4:]))
index += 8
const v1NumBuckets = 64
// See decodev2: guard the node count without overflowing the multiply so
// the bound stays correct on 32-bit builds too.
if nn < 0 || nn > (len(buf)-minLen)/((v1NumBuckets+1)*8+2) {
return nil, -1, ErrBadEncoding
}
var ss SequenceSet
for i := 0; i < nn; i++ {
base := le.Uint64(buf[index:])
index += 8
for nb := uint64(0); nb < v1NumBuckets; nb++ {
n := le.Uint64(buf[index:])
// Walk all set bits and insert sequences manually for this decode from v1.
for n != 0 {
ss.Insert(base + (nb * uint64(bitsPerBucket)) + uint64(bits.TrailingZeros64(n)))
n &= n - 1
}
index += 8
}
// Skip over encoded height.
index += 2
}
// Sanity check.
if ss.Size() != sz {
return nil, -1, ErrBadEncoding
}
return &ss, index, nil
}
// insertNode places a decoded node into the tree.
// These should be done in tree order as defined by Encode()
// This allows us to not have to calculate height or do rebalancing.
// So much better performance this way.
func (ss *SequenceSet) insertNode(n *node) {
ss.nodes++
if ss.root == nil {
ss.root = n
return
}
// Walk our way to the insertion point.
for p := ss.root; p != nil; {
if n.base < p.base {
if p.l == nil {
p.l = n
return
}
p = p.l
} else {
if p.r == nil {
p.r = n
return
}
p = p.r
}
}
}
const (
bitsPerBucket = 64 // bits in uint64
numBuckets = 32
numEntries = numBuckets * bitsPerBucket
)
type node struct {
//v dvalue
base uint64
bits [numBuckets]uint64
l *node
r *node
h int
}
// Set the proper bit.
// seq should have already been qualified and inserted should be non nil.
func (n *node) set(seq uint64, inserted *bool) {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
if (n.bits[i] & mask) == 0 {
n.bits[i] |= mask
*inserted = true
}
}
func (n *node) insert(seq uint64, inserted *bool, nodes *int) *node {
if n == nil {
base := (seq / numEntries) * numEntries
n := &node{base: base, h: 1}
n.set(seq, inserted)
*nodes++
return n
}
if seq < n.base {
n.l = n.l.insert(seq, inserted, nodes)
} else if seq >= n.base+numEntries {
n.r = n.r.insert(seq, inserted, nodes)
} else {
n.set(seq, inserted)
}
n.h = maxH(n) + 1
// Don't make a function, impacts performance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// Right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
func (n *node) rotateL() *node {
r := n.r
if r != nil {
n.r = r.l
r.l = n
n.h = maxH(n) + 1
r.h = maxH(r) + 1
} else {
n.r = nil
n.h = maxH(n) + 1
}
return r
}
func (n *node) rotateR() *node {
l := n.l
if l != nil {
n.l = l.r
l.r = n
n.h = maxH(n) + 1
l.h = maxH(l) + 1
} else {
n.l = nil
n.h = maxH(n) + 1
}
return l
}
func balanceF(n *node) int {
if n == nil {
return 0
}
var lh, rh int
if n.l != nil {
lh = n.l.h
}
if n.r != nil {
rh = n.r.h
}
return lh - rh
}
func maxH(n *node) int {
if n == nil {
return 0
}
var lh, rh int
if n.l != nil {
lh = n.l.h
}
if n.r != nil {
rh = n.r.h
}
if lh > rh {
return lh
}
return rh
}
// Clear the proper bit.
// seq should have already been qualified and deleted should be non nil.
// Will return true if this node is now empty.
func (n *node) clear(seq uint64, deleted *bool) bool {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
if (n.bits[i] & mask) == 0 {
// Nothing cleared, and nodes in the tree are never empty,
// so no need to scan the buckets.
return false
}
n.bits[i] &^= mask
*deleted = true
for _, b := range n.bits {
if b != 0 {
return false
}
}
return true
}
func (n *node) delete(seq uint64, deleted *bool, nodes *int) *node {
if n == nil {
return nil
}
if seq < n.base {
n.l = n.l.delete(seq, deleted, nodes)
} else if seq >= n.base+numEntries {
n.r = n.r.delete(seq, deleted, nodes)
} else if empty := n.clear(seq, deleted); empty {
*nodes--
if n.l == nil {
n = n.r
} else if n.r == nil {
n = n.l
} else {
// We have both children.
n.r = n.r.insertNodePrev(n.l)
n = n.r
}
}
if n != nil {
n.h = maxH(n) + 1
}
// Check balance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
// Will insert nn into the node assuming it is less than all other nodes in n.
// Will re-calculate height and balance.
func (n *node) insertNodePrev(nn *node) *node {
if n.l == nil {
n.l = nn
} else {
n.l = n.l.insertNodePrev(nn)
}
n.h = maxH(n) + 1
// Check balance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
func (n *node) exists(seq uint64) bool {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
return n.bits[i]&mask != 0
}
// Return minimum sequence in the set.
// This node can not be empty.
func (n *node) min() uint64 {
for i, b := range n.bits {
if b != 0 {
return n.base +
uint64(i*bitsPerBucket) +
uint64(bits.TrailingZeros64(b))
}
}
return 0
}
// Return maximum sequence in the set.
// This node can not be empty.
func (n *node) max() uint64 {
for i := numBuckets - 1; i >= 0; i-- {
if b := n.bits[i]; b != 0 {
return n.base +
uint64(i*bitsPerBucket) +
uint64(bitsPerBucket-bits.LeadingZeros64(b>>1))
}
}
return 0
}
// This is done in tree order.
func (n *node) nodeIter(f func(n *node)) {
if n == nil {
return
}
f(n)
n.l.nodeIter(f)
n.r.nodeIter(f)
}
// iter will iterate through the set's items in this node.
// If the supplied function returns false we terminate the iteration.
func (n *node) iter(f func(uint64) bool) bool {
if n == nil {
return true
}
if ok := n.l.iter(f); !ok {
return false
}
for i, b := range n.bits {
base := n.base + uint64(i)*bitsPerBucket
for b != 0 {
if ok := f(base + uint64(bits.TrailingZeros64(b))); !ok {
return false
}
b &= b - 1
}
}
if ok := n.r.iter(f); !ok {
return false
}
return true
}

556
server/avl/seqset_test.go Normal file
View File

@@ -0,0 +1,556 @@
// Copyright 2023-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package avl
import (
"encoding/base64"
"encoding/binary"
"math/rand/v2"
"testing"
)
func TestSeqSetBasics(t *testing.T) {
var ss SequenceSet
seqs := []uint64{22, 222, 2000, 2, 2, 4}
for _, seq := range seqs {
ss.Insert(seq)
require_True(t, ss.Exists(seq))
}
require_True(t, ss.Nodes() == 1)
require_True(t, ss.Size() == len(seqs)-1) // We have one dup in there.
lh, rh := ss.Heights()
require_True(t, lh == 0)
require_True(t, rh == 0)
}
func TestSeqSetLeftLean(t *testing.T) {
var ss SequenceSet
for i := uint64(4 * numEntries); i > 0; i-- {
ss.Insert(i)
}
require_True(t, ss.Nodes() == 5)
require_True(t, ss.Size() == 4*numEntries)
lh, rh := ss.Heights()
require_True(t, lh == 2)
require_True(t, rh == 1)
}
func TestSeqSetRightLean(t *testing.T) {
var ss SequenceSet
for i := uint64(0); i < uint64(4*numEntries); i++ {
ss.Insert(i)
}
require_True(t, ss.Nodes() == 4)
require_True(t, ss.Size() == 4*numEntries)
lh, rh := ss.Heights()
require_True(t, lh == 1)
require_True(t, rh == 2)
}
func TestSeqSetCorrectness(t *testing.T) {
// Generate 100k sequences across 500k range.
num := 100_000
max := 500_000
set := make(map[uint64]struct{}, num)
var ss SequenceSet
for i := 0; i < num; i++ {
n := uint64(rand.Int64N(int64(max + 1)))
ss.Insert(n)
set[n] = struct{}{}
}
for i := uint64(0); i <= uint64(max); i++ {
_, exists := set[i]
require_True(t, ss.Exists(i) == exists)
}
}
func TestSeqSetRange(t *testing.T) {
num := 2*numEntries + 22
nums := make([]uint64, 0, num)
for i := 0; i < num; i++ {
nums = append(nums, uint64(i))
}
rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })
var ss SequenceSet
for _, n := range nums {
ss.Insert(n)
}
nums = nums[:0]
ss.Range(func(n uint64) bool {
nums = append(nums, n)
return true
})
require_True(t, len(nums) == num)
for i := uint64(0); i < uint64(num); i++ {
require_True(t, nums[i] == i)
}
// Test truncating the range call.
nums = nums[:0]
ss.Range(func(n uint64) bool {
if n >= 10 {
return false
}
nums = append(nums, n)
return true
})
require_True(t, len(nums) == 10)
for i := uint64(0); i < 10; i++ {
require_True(t, nums[i] == i)
}
}
func TestSeqSetDelete(t *testing.T) {
var ss SequenceSet
// Simple single node.
seqs := []uint64{22, 222, 2222, 2, 2, 4}
for _, seq := range seqs {
ss.Insert(seq)
}
for _, seq := range seqs {
ss.Delete(seq)
require_True(t, !ss.Exists(seq))
}
require_True(t, ss.root == nil)
}
func TestSeqSetInsertAndDeletePedantic(t *testing.T) {
var ss SequenceSet
num := 50*numEntries + 22
nums := make([]uint64, 0, num)
for i := 0; i < num; i++ {
nums = append(nums, uint64(i))
}
rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })
// Make sure always balanced.
testBalanced := func() {
t.Helper()
// Check heights.
ss.root.nodeIter(func(n *node) {
if n != nil && n.h != maxH(n)+1 {
t.Fatalf("Node height is wrong: %+v", n)
}
})
// Check balance factor.
if bf := balanceF(ss.root); bf > 1 || bf < -1 {
t.Fatalf("Unbalanced tree")
}
}
for _, n := range nums {
ss.Insert(n)
testBalanced()
}
require_True(t, ss.root != nil)
for _, n := range nums {
ss.Delete(n)
testBalanced()
require_True(t, !ss.Exists(n))
if ss.Size() > 0 {
require_True(t, ss.root != nil)
}
}
require_True(t, ss.root == nil)
}
func TestSeqSetMinMax(t *testing.T) {
var ss SequenceSet
// Simple single node.
seqs := []uint64{22, 222, 2222, 2, 2, 4}
for _, seq := range seqs {
ss.Insert(seq)
}
min, max := ss.MinMax()
require_True(t, min == 2 && max == 2222)
// Multi-node
ss.Empty()
num := 22*numEntries + 22
nums := make([]uint64, 0, num)
for i := 0; i < num; i++ {
nums = append(nums, uint64(i))
}
rand.Shuffle(len(nums), func(i, j int) { nums[i], nums[j] = nums[j], nums[i] })
for _, n := range nums {
ss.Insert(n)
}
min, max = ss.MinMax()
require_True(t, min == 0 && max == uint64(num-1))
}
func TestSeqSetClone(t *testing.T) {
// Generate 100k sequences across 500k range.
num := 100_000
max := 500_000
var ss SequenceSet
for i := 0; i < num; i++ {
ss.Insert(uint64(rand.Int64N(int64(max + 1))))
}
ssc := ss.Clone()
require_True(t, ss.Size() == ssc.Size())
require_True(t, ss.Nodes() == ssc.Nodes())
}
func TestSeqSetEqual(t *testing.T) {
var ss1, ss2, ss3 SequenceSet
// Empty and nil sets are all equal.
var nilSet *SequenceSet
require_True(t, ss1.Equal(&ss2))
require_True(t, ss1.Equal(nilSet))
require_True(t, nilSet.Equal(&ss1))
// Same state (first, last, size) but different contents.
for _, seq := range []uint64{1, 3, 5} {
ss1.Insert(seq)
}
for _, seq := range []uint64{1, 4, 5} {
ss2.Insert(seq)
}
require_False(t, ss1.Equal(&ss2))
require_False(t, ss2.Equal(&ss1))
require_False(t, ss1.Equal(nilSet))
require_False(t, nilSet.Equal(&ss1))
// Identical contents, insertion order should not matter.
for _, seq := range []uint64{5, 1, 3} {
ss3.Insert(seq)
}
require_True(t, ss1.Equal(&ss3))
require_True(t, ss3.Equal(&ss1))
// Differing sizes.
ss3.Insert(7)
require_False(t, ss1.Equal(&ss3))
}
func TestSeqSetUnion(t *testing.T) {
var ss1, ss2 SequenceSet
seqs1 := []uint64{22, 222, 2222, 2, 2, 4}
for _, seq := range seqs1 {
ss1.Insert(seq)
}
seqs2 := []uint64{33, 333, 3333, 3, 33_333, 333_333}
for _, seq := range seqs2 {
ss2.Insert(seq)
}
ss := Union(&ss1, &ss2)
require_True(t, ss.Size() == 11)
seqs := append(seqs1, seqs2...)
for _, n := range seqs {
require_True(t, ss.Exists(n))
}
}
func TestSeqSetFirst(t *testing.T) {
var ss SequenceSet
seqs := []uint64{22, 222, 2222, 222_222}
for _, seq := range seqs {
// Normal case where we pick first/base.
ss.Insert(seq)
require_True(t, ss.root.base == (seq/numEntries)*numEntries)
ss.Empty()
// Where we set the minimum start value.
ss.SetInitialMin(seq)
ss.Insert(seq)
require_True(t, ss.root.base == seq)
ss.Empty()
}
}
// Test that we can union with nodes vs individual sequence insertion.
func TestSeqSetDistinctUnion(t *testing.T) {
// Distinct sets.
var ss1 SequenceSet
seqs1 := []uint64{1, 10, 100, 200}
for _, seq := range seqs1 {
ss1.Insert(seq)
}
var ss2 SequenceSet
seqs2 := []uint64{5000, 6100, 6200, 6222}
for _, seq := range seqs2 {
ss2.Insert(seq)
}
ss := ss1.Clone()
allSeqs := append(seqs1, seqs2...)
ss.Union(&ss2)
require_True(t, ss.Size() == len(allSeqs))
for _, seq := range allSeqs {
require_True(t, ss.Exists(seq))
}
}
func TestSeqSetDecodeV1(t *testing.T) {
// Encoding from v1 which was 64 buckets.
seqs := []uint64{22, 222, 2222, 222_222, 2_222_222}
encStr := `
FgEDAAAABQAAAABgAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAADgIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAA==
`
enc, err := base64.StdEncoding.DecodeString(encStr)
require_NoError(t, err)
ss, _, err := Decode(enc)
require_NoError(t, err)
require_True(t, ss.Size() == len(seqs))
for _, seq := range seqs {
require_True(t, ss.Exists(seq))
}
}
func TestSeqSetDecodeNodeCountOverflow(t *testing.T) {
// A replicated stream state carries an avl-encoded delete map decoded from
// a peer. The node count is read as a uint32 into an int; on 32-bit builds
// a value above MaxInt32 turns negative (and a smaller value can overflow
// the nn*perNode multiply), defeating the length check so the decode then
// panics in make([]node, nn) or reads past the buffer. Both encodings must
// reject such counts on every architecture instead.
le := binary.LittleEndian
for _, ver := range []uint8{1, 2} {
buf := make([]byte, minLen)
buf[0], buf[1] = magic, ver
le.PutUint32(buf[2:], 0xFFFFFFFF) // node count, negative as a 32-bit int
if _, _, err := Decode(buf); err != ErrBadEncoding {
t.Fatalf("v%d: expected ErrBadEncoding for oversized node count, got %v", ver, err)
}
// Positive on 32-bit but the multiply still overflows for a short buffer.
le.PutUint32(buf[2:], 0x00800000)
if _, _, err := Decode(buf); err != ErrBadEncoding {
t.Fatalf("v%d: expected ErrBadEncoding for overflowing node count, got %v", ver, err)
}
}
// A well-formed single-node v2 buffer must still decode.
valid := make([]byte, minLen+(numBuckets+1)*8+2)
valid[0], valid[1] = magic, version
le.PutUint32(valid[2:], 1)
if _, _, err := Decode(valid); err != nil {
t.Fatalf("valid single-node buffer should decode, got %v", err)
}
}
func TestSeqSetInsertExistingNodeFastPath(t *testing.T) {
var ss SequenceSet
// Build a multi-node tree with only even sequences set.
for i := uint64(0); i < 8*numEntries; i += 2 {
ss.Insert(i)
}
nodes, size := ss.Nodes(), ss.Size()
// Inserting into existing nodes must not change the tree structure.
for i := uint64(1); i < 8*numEntries; i += 2 {
ss.Insert(i)
require_True(t, ss.Exists(i))
}
require_True(t, ss.Nodes() == nodes)
require_True(t, ss.Size() == size+4*numEntries)
// Duplicate inserts must not change the size.
for i := uint64(0); i < 8*numEntries; i++ {
ss.Insert(i)
}
require_True(t, ss.Size() == 8*numEntries)
// Heights and balance must still be intact.
ss.root.nodeIter(func(n *node) {
if n.h != maxH(n)+1 {
t.Fatalf("Node height is wrong: %+v", n)
}
})
if bf := balanceF(ss.root); bf > 1 || bf < -1 {
t.Fatalf("Unbalanced tree")
}
}
func TestSeqSetInsertUnalignedBase(t *testing.T) {
var ss SequenceSet
// SetInitialMin can create a node whose base is not aligned to numEntries.
require_NoError(t, ss.SetInitialMin(1000))
for _, seq := range []uint64{1000, 1001, 1000 + numEntries - 1} {
ss.Insert(seq)
require_True(t, ss.Exists(seq))
}
require_True(t, ss.Nodes() == 1)
require_True(t, ss.Size() == 3)
// Outside the node's range still creates a new node.
ss.Insert(1000 + numEntries)
require_True(t, ss.Exists(1000+numEntries))
require_True(t, ss.Nodes() == 2)
require_True(t, ss.Size() == 4)
}
func TestSeqSetDeleteNotPresent(t *testing.T) {
var ss SequenceSet
seqs := []uint64{22, 222, 2222, 222_222}
for _, seq := range seqs {
ss.Insert(seq)
}
nodes, size := ss.Nodes(), ss.Size()
// Deleting sequences that are not set, both inside and outside
// existing node ranges, must be a no-op and report false.
for _, seq := range []uint64{0, 23, 2223, 5000, 100_000, 999_999} {
require_True(t, !ss.Delete(seq))
}
require_True(t, ss.Nodes() == nodes)
require_True(t, ss.Size() == size)
for _, seq := range seqs {
require_True(t, ss.Exists(seq))
}
// Real deletes still work and empty the set.
for _, seq := range seqs {
require_True(t, ss.Delete(seq))
}
require_True(t, ss.root == nil)
}
func TestSeqSetRangeSparse(t *testing.T) {
// Cover bucket boundaries, node boundaries and large gaps.
seqs := []uint64{0, 1, 63, 64, 127, 128, 2047, 2048, 4095, 4096, 100_000, 1_000_000}
var ss SequenceSet
for _, seq := range seqs {
ss.Insert(seq)
}
var got []uint64
ss.Range(func(n uint64) bool {
got = append(got, n)
return true
})
require_True(t, len(got) == len(seqs))
for i, seq := range seqs {
require_True(t, got[i] == seq)
}
// Terminating mid-bucket should stop the iteration immediately.
got = got[:0]
ss.Range(func(n uint64) bool {
got = append(got, n)
return n != 64
})
require_True(t, len(got) == 4)
require_True(t, got[3] == 64)
}
func BenchmarkSeqSetInsertDense(b *testing.B) {
for i := 0; i < b.N; i++ {
var ss SequenceSet
for s := uint64(0); s < 200_000; s++ {
ss.Insert(s)
}
}
}
func BenchmarkSeqSetInsertSparse(b *testing.B) {
for i := 0; i < b.N; i++ {
var ss SequenceSet
for s := uint64(0); s < 200_000; s++ {
ss.Insert(s * 7)
}
}
}
func BenchmarkSeqSetRangeDense(b *testing.B) {
var ss SequenceSet
for s := uint64(0); s < 200_000; s++ {
ss.Insert(s)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var sum uint64
ss.Range(func(n uint64) bool { sum += n; return true })
}
}
func BenchmarkSeqSetRangeSparse(b *testing.B) {
var ss SequenceSet
for s := uint64(0); s < 10_000; s++ {
ss.Insert(s * 211) // ~10 bits per 2048-entry node
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var sum uint64
ss.Range(func(n uint64) bool { sum += n; return true })
}
}
func BenchmarkSeqSetDeleteNotPresent(b *testing.B) {
var ss SequenceSet
for s := uint64(0); s < 200_000; s += 2 {
ss.Insert(s)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for s := uint64(1); s < 200_000; s += 2 {
ss.Delete(s)
}
}
}
func require_NoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("require no error, but got: %v", err)
}
}
func require_True(t *testing.T, b bool) {
t.Helper()
if !b {
t.Fatalf("require true")
}
}
func require_False(t *testing.T, b bool) {
t.Helper()
if b {
t.Fatalf("require false")
}
}

View File

@@ -0,0 +1,185 @@
// Copyright 2022-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"math/rand/v2"
"sync/atomic"
"testing"
"github.com/nats-io/nats.go"
)
func BenchmarkPublish(b *testing.B) {
const (
verbose = false
minMessages = 10_000
subject = "S"
queue = "Q"
)
const (
KB = 1024
MB = KB * KB
)
type SubscriberType string
const (
Async SubscriberType = "Async"
QueueAsync SubscriberType = "AsyncQueue"
None SubscriberType = "None"
)
benchmarksCases := []struct {
messageSize int
}{
{0},
{1},
{32},
{128},
{512},
{4 * KB},
{32 * KB},
{128 * KB},
{512 * KB},
{1 * MB},
}
// All the cases above are run for each of the subscriber cases below
subscribersCases := []struct {
numSubs int
subType SubscriberType
}{
{0, None},
{1, Async},
{1, QueueAsync},
{10, Async},
{10, QueueAsync},
}
for _, bc := range benchmarksCases {
bcName := fmt.Sprintf(
"MsgSz=%db",
bc.messageSize,
)
b.Run(
bcName,
func(b *testing.B) {
for _, sc := range subscribersCases {
scName := fmt.Sprintf("Subs=%dx%v", sc.numSubs, sc.subType)
if sc.subType == None {
scName = fmt.Sprintf("Subs=%v", sc.subType)
}
b.Run(
scName,
func(b *testing.B) {
// Skip short runs, benchmark gets re-executed with a larger N
if b.N < minMessages {
b.ResetTimer()
return
}
if verbose {
b.Logf("Running %s/%s with %d ops", bcName, scName, b.N)
}
subErrors := uint64(0)
handleSubError := func(_ *nats.Conn, _ *nats.Subscription, _ error) {
atomic.AddUint64(&subErrors, 1)
}
// Start single server (no JS)
opts := DefaultTestOptions
opts.Port = -1
s := RunServer(&opts)
defer s.Shutdown()
// Create subscribers
for i := 0; i < sc.numSubs; i++ {
subConn, connErr := nats.Connect(s.ClientURL(), nats.ErrorHandler(handleSubError))
if connErr != nil {
b.Fatalf("Failed to connect: %v", connErr)
}
defer subConn.Close()
var sub *nats.Subscription
var subErr error
switch sc.subType {
case None:
// No subscription
case Async:
sub, subErr = subConn.Subscribe(subject, func(*nats.Msg) {})
case QueueAsync:
sub, subErr = subConn.QueueSubscribe(subject, queue, func(*nats.Msg) {})
default:
b.Fatalf("Unknow subscribers type: %v", sc.subType)
}
if subErr != nil {
b.Fatalf("Failed to subscribe: %v", subErr)
}
defer sub.Unsubscribe()
// Do not drop messages due to slow subscribers:
sub.SetPendingLimits(-1, -1)
}
// Create publisher connection
nc, err := nats.Connect(s.ClientURL())
if err != nil {
b.Fatalf("Failed to connect: %v", err)
}
defer nc.Close()
rng := rand.NewChaCha8([32]byte{42})
message := make([]byte, bc.messageSize)
var published, errors int
// Benchmark starts here
b.ResetTimer()
for i := 0; i < b.N; i++ {
rng.Read(message)
pubErr := nc.Publish(subject, message)
if pubErr != nil {
errors++
} else {
published++
b.SetBytes(int64(bc.messageSize))
}
}
// Benchmark ends here
b.StopTimer()
if published+errors != b.N {
b.Fatalf("Something doesn't add up: %d + %d != %d", published, errors, b.N)
} else if subErrors > 0 {
b.Fatalf("Subscribers errors: %d", subErrors)
}
b.ReportMetric(float64(errors)*100/float64(b.N), "%error")
},
)
}
},
)
}
}

312
server/certidp/certidp.go Normal file
View File

@@ -0,0 +1,312 @@
// Copyright 2023-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certidp
import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
const (
DefaultAllowedClockSkew = 30 * time.Second
DefaultOCSPResponderTimeout = 2 * time.Second
DefaultTTLUnsetNextUpdate = 1 * time.Hour
)
type StatusAssertion int
var (
StatusAssertionStrToVal = map[string]StatusAssertion{
"good": ocsp.Good,
"revoked": ocsp.Revoked,
"unknown": ocsp.Unknown,
}
StatusAssertionValToStr = map[StatusAssertion]string{
ocsp.Good: "good",
ocsp.Revoked: "revoked",
ocsp.Unknown: "unknown",
}
StatusAssertionIntToVal = map[int]StatusAssertion{
0: ocsp.Good,
1: ocsp.Revoked,
2: ocsp.Unknown,
}
)
// GetStatusAssertionStr returns the corresponding string representation of the StatusAssertion.
func GetStatusAssertionStr(sa int) string {
// If the provided status assertion value is not found in the map (StatusAssertionIntToVal),
// the function defaults to "unknown" to avoid defaulting to "good," which is the default iota value
// for the ocsp.StatusAssertion enumeration (https://pkg.go.dev/golang.org/x/crypto/ocsp#pkg-constants).
// This ensures that we don't unintentionally default to "good" when there's no map entry.
v, ok := StatusAssertionIntToVal[sa]
if !ok {
// set unknown as fallback
v = ocsp.Unknown
}
return StatusAssertionValToStr[v]
}
func (sa StatusAssertion) MarshalJSON() ([]byte, error) {
// This ensures that we don't unintentionally default to "good" when there's no map entry.
// (see more details in the GetStatusAssertionStr() comment)
str, ok := StatusAssertionValToStr[sa]
if !ok {
// set unknown as fallback
str = StatusAssertionValToStr[ocsp.Unknown]
}
return json.Marshal(str)
}
func (sa *StatusAssertion) UnmarshalJSON(in []byte) error {
// This ensures that we don't unintentionally default to "good" when there's no map entry.
// (see more details in the GetStatusAssertionStr() comment)
v, ok := StatusAssertionStrToVal[strings.ReplaceAll(string(in), "\"", "")]
if !ok {
// set unknown as fallback
v = StatusAssertionStrToVal["unknown"]
}
*sa = v
return nil
}
type ChainLink struct {
Leaf *x509.Certificate
Issuer *x509.Certificate
OCSPWebEndpoints *[]*url.URL
}
// OCSPPeerConfig holds the parsed OCSP peer configuration section of TLS configuration
type OCSPPeerConfig struct {
Verify bool
Timeout float64
ClockSkew float64
WarnOnly bool
UnknownIsGood bool
AllowWhenCAUnreachable bool
TTLUnsetNextUpdate float64
}
func NewOCSPPeerConfig() *OCSPPeerConfig {
return &OCSPPeerConfig{
Verify: false,
Timeout: DefaultOCSPResponderTimeout.Seconds(),
ClockSkew: DefaultAllowedClockSkew.Seconds(),
WarnOnly: false,
UnknownIsGood: false,
AllowWhenCAUnreachable: false,
TTLUnsetNextUpdate: DefaultTTLUnsetNextUpdate.Seconds(),
}
}
// Log is a neutral method of passing server loggers to plugins
type Log struct {
Debugf func(format string, v ...any)
Noticef func(format string, v ...any)
Warnf func(format string, v ...any)
Errorf func(format string, v ...any)
Tracef func(format string, v ...any)
}
type CertInfo struct {
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Raw []byte `json:"raw,omitempty"`
}
var OCSPPeerUsage = `
For client, leaf spoke (remotes), and leaf hub connections, you may enable OCSP peer validation:
tls {
...
# mTLS must be enabled (with exception of Leaf remotes)
verify: true
...
# short form enables peer verify and takes option defaults
ocsp_peer: true
# long form includes settable options
ocsp_peer {
# Enable OCSP peer validation (default false)
verify: true
# OCSP responder timeout in seconds (may be fractional, default 2 seconds)
ca_timeout: 2
# Allowed skew between server and OCSP responder time in seconds (may be fractional, default 30 seconds)
allowed_clockskew: 30
# Warn-only and never reject connections (default false)
warn_only: false
# Treat response Unknown status as valid certificate (default false)
unknown_is_good: false
# Warn-only if no CA response can be obtained and no cached revocation exists (default false)
allow_when_ca_unreachable: false
# If response NextUpdate unset by CA, set a default cache TTL in seconds from ThisUpdate (default 1 hour)
cache_ttl_when_next_update_unset: 3600
}
...
}
Note: OCSP validation for route and gateway connections is enabled using the 'ocsp' configuration option.
`
// GenerateFingerprint returns a base64-encoded SHA256 hash of the raw certificate
func GenerateFingerprint(cert *x509.Certificate) string {
data := sha256.Sum256(cert.Raw)
return base64.StdEncoding.EncodeToString(data[:])
}
func getWebEndpoints(uris []string) []*url.URL {
var urls []*url.URL
for _, uri := range uris {
endpoint, err := url.ParseRequestURI(uri)
if err != nil {
// skip invalid URLs
continue
}
if endpoint.Scheme != "http" && endpoint.Scheme != "https" {
// skip non-web URLs
continue
}
urls = append(urls, endpoint)
}
return urls
}
// GetSubjectDNForm returns RDN sequence concatenation of the certificate's subject to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetSubjectDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Subject.ToRDNSequence()), "+")
}
// GetIssuerDNForm returns RDN sequence concatenation of the certificate's issuer to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetIssuerDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Issuer.ToRDNSequence()), "+")
}
// CertOCSPEligible checks if the certificate's issuer has populated AIA with OCSP responder endpoint(s)
// and is thus eligible for OCSP validation
func CertOCSPEligible(link *ChainLink) bool {
if link == nil || link.Leaf.Raw == nil || len(link.Leaf.Raw) == 0 {
return false
}
if len(link.Leaf.OCSPServer) == 0 {
return false
}
urls := getWebEndpoints(link.Leaf.OCSPServer)
if len(urls) == 0 {
return false
}
link.OCSPWebEndpoints = &urls
return true
}
// GetLeafIssuerCert returns the issuer certificate of the leaf (positional) certificate in the chain
func GetLeafIssuerCert(chain []*x509.Certificate, leafPos int) *x509.Certificate {
if len(chain) == 0 || leafPos < 0 {
return nil
}
// self-signed certificate or too-big leafPos
if leafPos >= len(chain)-1 {
return nil
}
// returns pointer to issuer cert or nil
return (chain)[leafPos+1]
}
// OCSPResponseCurrent checks if the OCSP response is current (i.e. not expired and not future effective)
func OCSPResponseCurrent(ocspr *ocsp.Response, opts *OCSPPeerConfig, log *Log) bool {
skew := time.Duration(opts.ClockSkew * float64(time.Second))
if skew < 0*time.Second {
skew = DefaultAllowedClockSkew
}
now := time.Now().UTC()
// Typical effectivity check based on CA response ThisUpdate and NextUpdate semantics
if !ocspr.NextUpdate.IsZero() && ocspr.NextUpdate.Before(now.Add(-1*skew)) {
t := ocspr.NextUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseExpired, t, nt, skew)
return false
}
// CA responder can assert NextUpdate unset, in which case use config option to set a default cache TTL
if ocspr.NextUpdate.IsZero() {
ttl := time.Duration(opts.TTLUnsetNextUpdate * float64(time.Second))
if ttl < 0*time.Second {
ttl = DefaultTTLUnsetNextUpdate
}
expiryTime := ocspr.ThisUpdate.Add(ttl)
if expiryTime.Before(now.Add(-1 * skew)) {
t := expiryTime.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseTTLExpired, t, nt, skew)
return false
}
}
if ocspr.ThisUpdate.After(now.Add(skew)) {
t := ocspr.ThisUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseFutureDated, t, nt, skew)
return false
}
return true
}
// ValidDelegationCheck checks if the CA OCSP Response was signed by a valid CA Issuer delegate as per (RFC 6960, section 4.2.2.2)
// If a valid delegate or direct-signed by CA Issuer, true returned.
func ValidDelegationCheck(iss *x509.Certificate, ocspr *ocsp.Response) bool {
// This call assumes prior successful parse and signature validation of the OCSP response
// The Go OCSP library (as of x/crypto/ocsp v0.9) will detect and perform a 1-level delegate signature check but does not
// implement the additional criteria for delegation specified in RFC 6960, section 4.2.2.2.
if iss == nil || ocspr == nil {
return false
}
// not a delegation, no-op
if ocspr.Certificate == nil {
return true
}
// delegate is self-same with CA Issuer, not a delegation although response issued in that form
if ocspr.Certificate.Equal(iss) {
return true
}
// we need to verify CA Issuer stamped id-kp-OCSPSigning on delegate
delegatedSigner := false
for _, keyUseExt := range ocspr.Certificate.ExtKeyUsage {
if keyUseExt == x509.ExtKeyUsageOCSPSigning {
delegatedSigner = true
break
}
}
return delegatedSigner
}

View File

@@ -0,0 +1,56 @@
// Copyright 2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certidp
import "testing"
// Checks the return values of the function GetStatusAssertionStr
func TestGetStatusAssertionStr(t *testing.T) {
tests := []struct {
name string
input int
expected string
}{
{
name: "GoodStatus",
input: 0,
expected: "good",
},
{
name: "RevokedStatus",
input: 1,
expected: "revoked",
},
{
name: "UnknownStatus",
input: 2,
expected: "unknown",
},
// Invalid status assertion value.
{
name: "InvalidStatus",
input: 42,
expected: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetStatusAssertionStr(tt.input)
if got != tt.expected {
t.Errorf("Expected GetStatusAssertionStr: %v, got %v", tt.expected, got)
}
})
}
}

106
server/certidp/messages.go Normal file
View File

@@ -0,0 +1,106 @@
// Copyright 2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certidp
var (
// Returned errors
ErrIllegalPeerOptsConfig = "expected map to define OCSP peer options, got [%T]"
ErrIllegalCacheOptsConfig = "expected map to define OCSP peer cache options, got [%T]"
ErrParsingPeerOptFieldGeneric = "error parsing tls peer config, unknown field [%q]"
ErrParsingPeerOptFieldTypeConversion = "error parsing tls peer config, conversion error: %s"
ErrParsingCacheOptFieldTypeConversion = "error parsing OCSP peer cache config, conversion error: %s"
ErrUnableToPlugTLSEmptyConfig = "unable to plug TLS verify connection, config is nil"
ErrMTLSRequired = "OCSP peer verification for client connections requires TLS verify (mTLS) to be enabled"
ErrUnableToPlugTLSClient = "unable to register client OCSP verification"
ErrUnableToPlugTLSServer = "unable to register server OCSP verification"
ErrCannotWriteCompressed = "error writing to compression writer: %w"
ErrCannotReadCompressed = "error reading compression reader: %w"
ErrTruncatedWrite = "short write on body (%d != %d)"
ErrCannotCloseWriter = "error closing compression writer: %w"
ErrParsingCacheOptFieldGeneric = "error parsing OCSP peer cache config, unknown field [%q]"
ErrUnknownCacheType = "error parsing OCSP peer cache config, unknown type [%s]"
ErrInvalidChainlink = "invalid chain link"
ErrBadResponderHTTPStatus = "bad OCSP responder http status: [%d]"
ErrNoAvailOCSPServers = "no available OCSP servers"
ErrFailedWithAllRequests = "exhausted OCSP responders: %w"
// Direct logged errors
ErrLoadCacheFail = "Unable to load OCSP peer cache: %s"
ErrSaveCacheFail = "Unable to save OCSP peer cache: %s"
ErrBadCacheTypeConfig = "Unimplemented OCSP peer cache type [%v]"
ErrResponseCompressFail = "Unable to compress OCSP response for key [%s]: %s"
ErrResponseDecompressFail = "Unable to decompress OCSP response for key [%s]: %s"
ErrPeerEmptyNoEvent = "Peer certificate is nil, cannot send OCSP peer reject event"
ErrPeerEmptyAutoReject = "Peer certificate is nil, rejecting OCSP peer"
// Debug information
DbgPlugTLSForKind = "Plugging TLS OCSP peer for [%s]"
DbgNumServerChains = "Peer OCSP enabled: %d TLS server chain(s) will be evaluated"
DbgNumClientChains = "Peer OCSP enabled: %d TLS client chain(s) will be evaluated"
DbgLinksInChain = "Chain [%d]: %d total link(s)"
DbgSelfSignedValid = "Chain [%d] is self-signed, thus peer is valid"
DbgValidNonOCSPChain = "Chain [%d] has no OCSP eligible links, thus peer is valid"
DbgChainIsOCSPEligible = "Chain [%d] has %d OCSP eligible link(s)"
DbgChainIsOCSPValid = "Chain [%d] is OCSP valid for all eligible links, thus peer is valid"
DbgNoOCSPValidChains = "No OCSP valid chains, thus peer is invalid"
DbgCheckingCacheForCert = "Checking OCSP peer cache for [%s], key [%s]"
DbgCurrentResponseCached = "Cached OCSP response is current, status [%s]"
DbgExpiredResponseCached = "Cached OCSP response is expired, status [%s]"
DbgOCSPValidPeerLink = "OCSP verify pass for [%s]"
DbgCachingResponse = "Caching OCSP response for [%s], key [%s]"
DbgAchievedCompression = "OCSP response compression ratio: [%f]"
DbgCacheHit = "OCSP peer cache hit for key [%s]"
DbgCacheMiss = "OCSP peer cache miss for key [%s]"
DbgPreservedRevocation = "Revoked OCSP response for key [%s] preserved by cache policy"
DbgDeletingCacheResponse = "Deleting OCSP peer cached response for key [%s]"
DbgStartingCache = "Starting OCSP peer cache"
DbgStoppingCache = "Stopping OCSP peer cache"
DbgLoadingCache = "Loading OCSP peer cache [%s]"
DbgNoCacheFound = "No OCSP peer cache found, starting with empty cache"
DbgSavingCache = "Saving OCSP peer cache [%s]"
DbgCacheSaved = "Saved OCSP peer cache successfully (%d bytes)"
DbgMakingCARequest = "Trying OCSP responder url [%s]"
DbgResponseExpired = "OCSP response NextUpdate [%s] is before now [%s] with clockskew [%s]"
DbgResponseTTLExpired = "OCSP response cache expiry [%s] is before now [%s] with clockskew [%s]"
DbgResponseFutureDated = "OCSP response ThisUpdate [%s] is before now [%s] with clockskew [%s]"
DbgCacheSaveTimerExpired = "OCSP peer cache save timer expired"
DbgCacheDirtySave = "OCSP peer cache is dirty, saving"
// Returned to peer as TLS reject reason
MsgTLSClientRejectConnection = "client not OCSP valid"
MsgTLSServerRejectConnection = "server not OCSP valid"
// Expected runtime errors (direct logged)
ErrCAResponderCalloutFail = "Attempt to obtain OCSP response from CA responder for [%s] failed: %s"
ErrNewCAResponseNotCurrent = "New OCSP CA response obtained for [%s] but not current"
ErrCAResponseParseFailed = "Could not parse OCSP CA response for [%s]: %s"
ErrOCSPInvalidPeerLink = "OCSP verify fail for [%s] with CA status [%s]"
// Policy override warnings (direct logged)
MsgAllowWhenCAUnreachableOccurred = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; no cached revocation so allowing"
MsgAllowWhenCAUnreachableOccurredCachedRevoke = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; cached revocation exists so rejecting"
MsgAllowWarnOnlyOccurred = "OCSP verify fail for [%s] but WarnOnly is true so allowing"
// Info (direct logged)
MsgCacheOnline = "OCSP peer cache online, type [%s]"
MsgCacheOffline = "OCSP peer cache offline, type [%s]"
// OCSP cert invalid reasons (debug and event reasons)
MsgFailedOCSPResponseFetch = "Failed OCSP response fetch"
MsgOCSPResponseNotEffective = "OCSP response not in effectivity window"
MsgFailedOCSPResponseParse = "Failed OCSP response parse"
MsgOCSPResponseInvalidStatus = "Invalid OCSP response status: %s"
MsgOCSPResponseDelegationInvalid = "Invalid OCSP response delegation: %s"
MsgCachedOCSPResponseInvalid = "Invalid cached OCSP response for [%s] with fingerprint [%s]"
)

View File

@@ -0,0 +1,92 @@
// Copyright 2023-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certidp
import (
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
func FetchOCSPResponse(link *ChainLink, opts *OCSPPeerConfig, log *Log) ([]byte, error) {
if link == nil || link.Leaf == nil || link.Issuer == nil || opts == nil || log == nil {
return nil, errors.New(ErrInvalidChainlink)
}
timeout := time.Duration(opts.Timeout * float64(time.Second))
if timeout <= 0*time.Second {
timeout = DefaultOCSPResponderTimeout
}
getRequestBytes := func(u string, hc *http.Client) ([]byte, error) {
resp, err := hc.Get(u)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(ErrBadResponderHTTPStatus, resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
// Request documentation:
// https://tools.ietf.org/html/rfc6960#appendix-A.1
reqDER, err := ocsp.CreateRequest(link.Leaf, link.Issuer, nil)
if err != nil {
return nil, err
}
reqEnc := encodeOCSPRequest(reqDER)
responders := *link.OCSPWebEndpoints
if len(responders) == 0 {
return nil, errors.New(ErrNoAvailOCSPServers)
}
var raw []byte
hc := &http.Client{
Timeout: timeout,
}
for _, u := range responders {
responderURL := u.String()
log.Debugf(DbgMakingCARequest, responderURL)
responderURL = strings.TrimSuffix(responderURL, "/")
raw, err = getRequestBytes(fmt.Sprintf("%s/%s", responderURL, reqEnc), hc)
if err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf(ErrFailedWithAllRequests, err)
}
return raw, nil
}
// encodeOCSPRequest encodes the OCSP request in base64 and URL-encodes it.
// This is needed to fulfill the OCSP responder's requirements for the request format. (X.690)
func encodeOCSPRequest(reqDER []byte) string {
reqEnc := base64.StdEncoding.EncodeToString(reqDER)
return url.QueryEscape(reqEnc)
}

View File

@@ -0,0 +1,29 @@
package certidp
import (
"encoding/base64"
"net/url"
"strings"
"testing"
)
func TestEncodeOCSPRequest(t *testing.T) {
data := []byte("test data for OCSP request")
encoded := encodeOCSPRequest(data)
if strings.ContainsAny(encoded, "+/=") {
t.Errorf("URL contains unescaped characters: %s", encoded)
}
decodedURL, err := url.QueryUnescape(encoded)
if err != nil {
t.Errorf("failed to url-decode request: %v", err)
}
decodedData, err := base64.StdEncoding.DecodeString(decodedURL)
if err != nil {
t.Errorf("failed to base64-decode request: %v", err)
}
if string(decodedData) != string(data) {
t.Errorf("Decoded data does not match original: got %s, want %s", decodedData, data)
}
}

View File

@@ -0,0 +1,104 @@
// Copyright 2022-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certstore
import (
"crypto"
"crypto/x509"
"io"
"runtime"
"strings"
)
type StoreType int
const MATCHBYEMPTY = 0
const STOREEMPTY = 0
const (
windowsCurrentUser StoreType = iota + 1
windowsLocalMachine
)
var StoreMap = map[string]StoreType{
"windowscurrentuser": windowsCurrentUser,
"windowslocalmachine": windowsLocalMachine,
}
var StoreOSMap = map[StoreType]string{
windowsCurrentUser: "windows",
windowsLocalMachine: "windows",
}
type MatchByType int
const (
matchByIssuer MatchByType = iota + 1
matchBySubject
matchByThumbprint
)
var MatchByMap = map[string]MatchByType{
"issuer": matchByIssuer,
"subject": matchBySubject,
"thumbprint": matchByThumbprint,
}
var Usage = `
In place of cert_file and key_file you may use the windows certificate store:
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Subject"
cert_match: "MyServer123"
}
`
func ParseCertStore(certStore string) (StoreType, error) {
certStoreType, exists := StoreMap[strings.ToLower(certStore)]
if !exists {
return 0, ErrBadCertStore
}
validOS, exists := StoreOSMap[certStoreType]
if !exists || validOS != runtime.GOOS {
return 0, ErrOSNotCompatCertStore
}
return certStoreType, nil
}
func ParseCertMatchBy(certMatchBy string) (MatchByType, error) {
certMatchByType, exists := MatchByMap[strings.ToLower(certMatchBy)]
if !exists {
return 0, ErrBadMatchByType
}
return certMatchByType, nil
}
func GetLeafIssuer(leaf *x509.Certificate, vOpts x509.VerifyOptions) (issuer *x509.Certificate) {
chains, err := leaf.Verify(vOpts)
if err != nil || len(chains) == 0 {
issuer = nil
} else {
issuer = chains[0][1]
}
return
}
// credential provides access to a public key and is a crypto.Signer.
type credential interface {
// Public returns the public key corresponding to the leaf certificate.
Public() crypto.PublicKey
// Sign signs digest with the private key.
Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
}

View File

@@ -0,0 +1,45 @@
// Copyright 2022-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package certstore
import (
"crypto"
"crypto/tls"
"io"
)
var _ = MATCHBYEMPTY
// otherKey implements crypto.Signer and crypto.Decrypter to satisfy linter on platforms that don't implement certstore
type otherKey struct{}
func TLSConfig(_ StoreType, _ MatchByType, _ string, _ []string, _ bool, _ *tls.Config) error {
return ErrOSNotCompatCertStore
}
// Public always returns nil public key since this is a stub on non-supported platform
func (k otherKey) Public() crypto.PublicKey {
return nil
}
// Sign always returns a nil signature since this is a stub on non-supported platform
func (k otherKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
_, _, _ = rand, digest, opts
return nil, nil
}
// Verify interface conformance.
var _ credential = &otherKey{}

View File

@@ -0,0 +1,965 @@
// Copyright 2022-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted, updated, and enhanced from CertToStore, https://github.com/google/certtostore/releases/tag/v1.0.2
// Apache License, Version 2.0, Copyright 2017 Google Inc.
package certstore
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"fmt"
"io"
"math/big"
"reflect"
"sync"
"syscall"
"unicode/utf16"
"unsafe"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
"golang.org/x/sys/windows"
)
const (
// wincrypt.h constants
winAcquireCached = windows.CRYPT_ACQUIRE_CACHE_FLAG
winAcquireSilent = windows.CRYPT_ACQUIRE_SILENT_FLAG
winAcquireOnlyNCryptKey = windows.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG
winEncodingX509ASN = windows.X509_ASN_ENCODING
winEncodingPKCS7 = windows.PKCS_7_ASN_ENCODING
winCertStoreProvSystem = windows.CERT_STORE_PROV_SYSTEM
winCertStoreCurrentUser = windows.CERT_SYSTEM_STORE_CURRENT_USER
winCertStoreLocalMachine = windows.CERT_SYSTEM_STORE_LOCAL_MACHINE
winCertStoreReadOnly = windows.CERT_STORE_READONLY_FLAG
winInfoIssuerFlag = windows.CERT_INFO_ISSUER_FLAG
winInfoSubjectFlag = windows.CERT_INFO_SUBJECT_FLAG
winCompareNameStrW = windows.CERT_COMPARE_NAME_STR_W
winCompareShift = windows.CERT_COMPARE_SHIFT
// Reference https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
winFindIssuerStr = windows.CERT_FIND_ISSUER_STR_W
winFindSubjectStr = windows.CERT_FIND_SUBJECT_STR_W
winFindHashStr = windows.CERT_FIND_HASH_STR
winNcryptKeySpec = windows.CERT_NCRYPT_KEY_SPEC
winBCryptPadPKCS1 uintptr = 0x2
winBCryptPadPSS uintptr = 0x8 // Modern TLS 1.2+
winBCryptPadPSSSalt uint32 = 32 // default 20, 32 optimal for typical SHA256 hash
winRSA1Magic = 0x31415352 // "RSA1" BCRYPT_RSAPUBLIC_MAGIC
winECS1Magic = 0x31534345 // "ECS1" BCRYPT_ECDSA_PUBLIC_P256_MAGIC
winECS3Magic = 0x33534345 // "ECS3" BCRYPT_ECDSA_PUBLIC_P384_MAGIC
winECS5Magic = 0x35534345 // "ECS5" BCRYPT_ECDSA_PUBLIC_P521_MAGIC
winECK1Magic = 0x314B4345 // "ECK1" BCRYPT_ECDH_PUBLIC_P256_MAGIC
winECK3Magic = 0x334B4345 // "ECK3" BCRYPT_ECDH_PUBLIC_P384_MAGIC
winECK5Magic = 0x354B4345 // "ECK5" BCRYPT_ECDH_PUBLIC_P521_MAGIC
winCryptENotFound = windows.CRYPT_E_NOT_FOUND
providerMSSoftware = "Microsoft Software Key Storage Provider"
)
var (
winBCryptRSAPublicBlob = winWide("RSAPUBLICBLOB")
winBCryptECCPublicBlob = winWide("ECCPUBLICBLOB")
winNCryptAlgorithmGroupProperty = winWide("Algorithm Group") // NCRYPT_ALGORITHM_GROUP_PROPERTY
winNCryptUniqueNameProperty = winWide("Unique Name") // NCRYPT_UNIQUE_NAME_PROPERTY
winNCryptECCCurveNameProperty = winWide("ECCCurveName") // NCRYPT_ECC_CURVE_NAME_PROPERTY
winCurveIDs = map[uint32]elliptic.Curve{
winECS1Magic: elliptic.P256(), // BCRYPT_ECDSA_PUBLIC_P256_MAGIC
winECS3Magic: elliptic.P384(), // BCRYPT_ECDSA_PUBLIC_P384_MAGIC
winECS5Magic: elliptic.P521(), // BCRYPT_ECDSA_PUBLIC_P521_MAGIC
winECK1Magic: elliptic.P256(), // BCRYPT_ECDH_PUBLIC_P256_MAGIC
winECK3Magic: elliptic.P384(), // BCRYPT_ECDH_PUBLIC_P384_MAGIC
winECK5Magic: elliptic.P521(), // BCRYPT_ECDH_PUBLIC_P521_MAGIC
}
winCurveNames = map[string]elliptic.Curve{
"nistP256": elliptic.P256(), // BCRYPT_ECC_CURVE_NISTP256
"nistP384": elliptic.P384(), // BCRYPT_ECC_CURVE_NISTP384
"nistP521": elliptic.P521(), // BCRYPT_ECC_CURVE_NISTP521
}
winAlgIDs = map[crypto.Hash]*uint16{
crypto.SHA1: winWide("SHA1"), // BCRYPT_SHA1_ALGORITHM
crypto.SHA256: winWide("SHA256"), // BCRYPT_SHA256_ALGORITHM
crypto.SHA384: winWide("SHA384"), // BCRYPT_SHA384_ALGORITHM
crypto.SHA512: winWide("SHA512"), // BCRYPT_SHA512_ALGORITHM
}
// MY is well-known system store on Windows that holds personal certificates. Read
// More about the CA locations here:
// https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/wcf/certificate-of-clientcertificate-element?redirectedfrom=MSDN
// https://superuser.com/questions/217719/what-are-the-windows-system-certificate-stores
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/certificate-stores
// https://learn.microsoft.com/en-us/windows/win32/seccrypto/system-store-locations
// https://stackoverflow.com/questions/63286085/which-x509-storename-refers-to-the-certificates-stored-beneath-trusted-root-cert#:~:text=4-,StoreName.,is%20%22Intermediate%20Certification%20Authorities%22.
winMyStore = winWide("MY")
winIntermediateCAStore = winWide("CA")
winRootStore = winWide("Root")
winAuthRootStore = winWide("AuthRoot")
// These DLLs must be available on all Windows hosts
winCrypt32 = windows.NewLazySystemDLL("crypt32.dll")
winNCrypt = windows.NewLazySystemDLL("ncrypt.dll")
winCertFindCertificateInStore = winCrypt32.NewProc("CertFindCertificateInStore")
winCertVerifyTimeValidity = winCrypt32.NewProc("CertVerifyTimeValidity")
winCryptAcquireCertificatePrivateKey = winCrypt32.NewProc("CryptAcquireCertificatePrivateKey")
winNCryptExportKey = winNCrypt.NewProc("NCryptExportKey")
winNCryptOpenStorageProvider = winNCrypt.NewProc("NCryptOpenStorageProvider")
winNCryptGetProperty = winNCrypt.NewProc("NCryptGetProperty")
winNCryptSignHash = winNCrypt.NewProc("NCryptSignHash")
winFnGetProperty = winGetProperty
)
func init() {
for _, d := range []*windows.LazyDLL{
winCrypt32, winNCrypt,
} {
if err := d.Load(); err != nil {
panic(err)
}
}
for _, p := range []*windows.LazyProc{
winCertFindCertificateInStore, winCryptAcquireCertificatePrivateKey,
winNCryptExportKey, winNCryptOpenStorageProvider,
winNCryptGetProperty, winNCryptSignHash,
} {
if err := p.Find(); err != nil {
panic(err)
}
}
}
type winPKCS1PaddingInfo struct {
pszAlgID *uint16
}
type winPSSPaddingInfo struct {
pszAlgID *uint16
cbSalt uint32
}
// createCACertsPool generates a CertPool from the Windows certificate store,
// adding all matching certificates from the caCertsMatch array to the pool.
// All matching certificates (vs first) are added to the pool based on a user
// request. If no certificates are found an error is returned.
func createCACertsPool(cs *winCertStore, storeType uint32, caCertsMatch []string, skipInvalid bool) (*x509.CertPool, error) {
var errs []error
caPool := x509.NewCertPool()
for _, s := range caCertsMatch {
lfs, err := cs.caCertsBySubjectMatch(s, storeType, skipInvalid)
if err != nil {
errs = append(errs, err)
} else {
for _, lf := range lfs {
caPool.AddCert(lf)
}
}
}
// If every lookup failed return the errors.
if len(errs) == len(caCertsMatch) {
return nil, fmt.Errorf("unable to match any CA certificate: %v", errs)
}
return caPool, nil
}
// TLSConfig fulfills the same function as reading cert and key pair from
// pem files but sources the Windows certificate store instead. The
// certMatchBy and certMatch fields search the "MY" certificate location
// for the first certificate that matches the certMatch field. The
// caCertsMatch field is used to search the Trusted Root, Third Party Root,
// and Intermediate Certificate Authority locations for certificates with
// Subjects matching the provided strings. If a match is found, the
// certificate is added to the pool that is used to verify the certificate
// chain.
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, caCertsMatch []string, skipInvalid bool, config *tls.Config) error {
var (
leaf *x509.Certificate
leafCtx *windows.CertContext
pk *winKey
vOpts = x509.VerifyOptions{}
chains [][]*x509.Certificate
chain []*x509.Certificate
rawChain [][]byte
)
// By StoreType, open a store
if certStore == windowsCurrentUser || certStore == windowsLocalMachine {
var scope uint32
cs, err := winOpenCertStore(providerMSSoftware)
if err != nil || cs == nil {
return err
}
if certStore == windowsCurrentUser {
scope = winCertStoreCurrentUser
}
if certStore == windowsLocalMachine {
scope = winCertStoreLocalMachine
}
// certByIssuer or certBySubject
if certMatchBy == matchBySubject || certMatchBy == MATCHBYEMPTY {
leaf, leafCtx, err = cs.certBySubject(certMatch, scope, skipInvalid)
} else if certMatchBy == matchByIssuer {
leaf, leafCtx, err = cs.certByIssuer(certMatch, scope, skipInvalid)
} else if certMatchBy == matchByThumbprint {
leaf, leafCtx, err = cs.certByThumbprint(certMatch, scope, skipInvalid)
} else {
return ErrBadMatchByType
}
if err != nil {
// pass through error from cert search
return err
}
if leaf == nil || leafCtx == nil {
return ErrFailedCertSearch
}
pk, err = cs.certKey(leafCtx)
if err != nil {
return err
}
if pk == nil {
return ErrNoPrivateKeyStoreRef
}
// Look for CA Certificates
if len(caCertsMatch) != 0 {
caPool, err := createCACertsPool(cs, scope, caCertsMatch, skipInvalid)
if err != nil {
return err
}
config.ClientCAs = caPool
}
} else {
return ErrBadCertStore
}
// Get intermediates in the cert store for the found leaf IFF there is a full chain of trust in the store
// otherwise just use leaf as the final chain.
//
// Using std lib Verify as a reliable way to get valid chains out of the win store for the leaf; however,
// using empty options since server TLS stanza could be TLS role as server identity or client identity.
chains, err := leaf.Verify(vOpts)
if err != nil || len(chains) == 0 {
chains = append(chains, []*x509.Certificate{leaf})
}
// We have at least one verified chain so pop the first chain and remove the self-signed CA cert (if present)
// from the end of the chain
chain = chains[0]
if len(chain) > 1 {
chain = chain[:len(chain)-1]
}
// For tls.Certificate.Certificate need a [][]byte from []*x509.Certificate
// Approximate capacity for efficiency
rawChain = make([][]byte, 0, len(chain))
for _, link := range chain {
rawChain = append(rawChain, link.Raw)
}
tlsCert := tls.Certificate{
Certificate: rawChain,
PrivateKey: pk,
Leaf: leaf,
}
config.Certificates = []tls.Certificate{tlsCert}
// note: pk is a windows pointer (not freed by Go) but needs to live the life of the server for Signing.
// The cert context (leafCtx) windows pointer must not be freed underneath the pk so also life of the server.
return nil
}
// winWide returns a pointer to uint16 representing the equivalent
// to a Windows LPCWSTR.
func winWide(s string) *uint16 {
w := utf16.Encode([]rune(s))
w = append(w, 0)
return &w[0]
}
// winOpenProvider gets a provider handle for subsequent calls
func winOpenProvider(provider string) (uintptr, error) {
var hProv uintptr
pname := winWide(provider)
// Open the provider, the last parameter is not used
r, _, err := winNCryptOpenStorageProvider.Call(uintptr(unsafe.Pointer(&hProv)), uintptr(unsafe.Pointer(pname)), 0)
if r == 0 {
return hProv, nil
}
return hProv, fmt.Errorf("NCryptOpenStorageProvider returned %X: %v", r, err)
}
// winFindCert wraps the CertFindCertificateInStore library call. Note that any cert context passed
// into prev will be freed. If no certificate was found, nil will be returned.
func winFindCert(store windows.Handle, enc, findFlags, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) {
h, _, err := winCertFindCertificateInStore.Call(
uintptr(store),
uintptr(enc),
uintptr(findFlags),
uintptr(findType),
uintptr(unsafe.Pointer(para)),
uintptr(unsafe.Pointer(prev)),
)
if h == 0 {
// Actual error, or simply not found?
if errno, ok := err.(syscall.Errno); ok && errno == syscall.Errno(winCryptENotFound) {
return nil, ErrFailedCertSearch
}
return nil, ErrFailedCertSearch
}
// nolint:govet
return (*windows.CertContext)(unsafe.Pointer(h)), nil
}
// winVerifyCertValid wraps the CertVerifyTimeValidity and simply returns true if the certificate is valid
func winVerifyCertValid(timeToVerify *windows.Filetime, certInfo *windows.CertInfo) bool {
// this function does not document returning errors / setting lasterror
r, _, _ := winCertVerifyTimeValidity.Call(
uintptr(unsafe.Pointer(timeToVerify)),
uintptr(unsafe.Pointer(certInfo)),
)
return r == 0
}
// winCertStore is a store implementation for the Windows Certificate Store
type winCertStore struct {
Prov uintptr
ProvName string
stores map[string]*winStoreHandle
mu sync.Mutex
}
// winOpenCertStore creates a winCertStore
func winOpenCertStore(provider string) (*winCertStore, error) {
cngProv, err := winOpenProvider(provider)
if err != nil {
// pass through error from winOpenProvider
return nil, err
}
wcs := &winCertStore{
Prov: cngProv,
ProvName: provider,
stores: make(map[string]*winStoreHandle),
}
return wcs, nil
}
// winCertContextToX509 creates an x509.Certificate from a Windows cert context.
func winCertContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) {
var der []byte
slice := (*reflect.SliceHeader)(unsafe.Pointer(&der))
slice.Data = uintptr(unsafe.Pointer(ctx.EncodedCert))
slice.Len = int(ctx.Length)
slice.Cap = int(ctx.Length)
return x509.ParseCertificate(der)
}
// certByIssuer matches and returns the first certificate found by passed issuer.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_ISSUER_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certByIssuer(issuer string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType, skipInvalid)
}
// certBySubject matches and returns the first certificate found by passed subject field.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certBySubject(subject string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType, skipInvalid)
}
// certByThumbprint matches and returns the first certificate found by passed SHA1 thumbprint.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certByThumbprint(hash string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindHashStr, hash, winMyStore, storeType, skipInvalid)
}
// caCertsBySubjectMatch matches and returns all matching certificates of the subject field.
//
// The following locations are searched:
// 1) Root (Trusted Root Certification Authorities)
// 2) AuthRoot (Third-Party Root Certification Authorities)
// 3) CA (Intermediate Certification Authorities)
//
// Caller specifies current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) caCertsBySubjectMatch(subject string, storeType uint32, skipInvalid bool) ([]*x509.Certificate, error) {
var (
leaf *x509.Certificate
searchLocations = [3]*uint16{winRootStore, winAuthRootStore, winIntermediateCAStore}
rv []*x509.Certificate
)
// surprisingly, an empty string returns a result. We'll treat this as an error.
if subject == "" {
return nil, ErrBadCaCertMatchField
}
for _, sr := range searchLocations {
var err error
if leaf, _, err = w.certSearch(winFindSubjectStr, subject, sr, storeType, skipInvalid); err == nil {
rv = append(rv, leaf)
} else {
// Ignore the failed search from a single location. Errors we catch include
// ErrFailedX509Extract (resulting from a malformed certificate) and errors
// around invalid attributes, unsupported algorithms, etc. These are corner
// cases as certificates with these errors shouldn't have been allowed
// to be added to the store in the first place.
if err != ErrFailedCertSearch {
return nil, err
}
}
}
// Not found anywhere
if len(rv) == 0 {
return nil, ErrFailedCertSearch
}
return rv, nil
}
// certSearch is a helper function to lookup certificates based on search type and match value.
// store is used to specify which store to perform the lookup in (system or user).
func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
// store handle to "MY" store
h, err := w.storeHandle(store, searchRoot)
if err != nil {
return nil, nil, err
}
var prev *windows.CertContext
var cert *x509.Certificate
i, err := windows.UTF16PtrFromString(matchValue)
if err != nil {
return nil, nil, ErrFailedCertSearch
}
// pass 0 as the third parameter because it is not used
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376064(v=vs.85).aspx
for {
nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev)
if err != nil {
return nil, nil, err
}
if nc != nil {
// certificate found
prev = nc
var now *windows.Filetime
if skipInvalid && !winVerifyCertValid(now, nc.CertInfo) {
continue
}
// Extract the DER-encoded certificate from the cert context
xc, err := winCertContextToX509(nc)
if err == nil {
cert = xc
break
} else {
return nil, nil, ErrFailedX509Extract
}
} else {
return nil, nil, ErrFailedCertSearch
}
}
if cert == nil {
return nil, nil, ErrFailedX509Extract
}
return cert, prev, nil
}
type winStoreHandle struct {
handle *windows.Handle
}
func winNewStoreHandle(provider uint32, store *uint16) (*winStoreHandle, error) {
var s winStoreHandle
if s.handle != nil {
return &s, nil
}
st, err := windows.CertOpenStore(
winCertStoreProvSystem,
0,
0,
provider|winCertStoreReadOnly,
uintptr(unsafe.Pointer(store)))
if err != nil {
return nil, ErrBadCryptoStoreProvider
}
s.handle = &st
return &s, nil
}
// winKey implements crypto.Signer and crypto.Decrypter for key based operations.
type winKey struct {
handle uintptr
pub crypto.PublicKey
Container string
AlgorithmGroup string
}
// Public exports a public key to implement crypto.Signer
func (k winKey) Public() crypto.PublicKey {
return k.pub
}
// Sign returns the signature of a hash to implement crypto.Signer
func (k winKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
switch k.AlgorithmGroup {
case "ECDSA", "ECDH":
return winSignECDSA(k.handle, digest)
case "RSA":
hf := opts.HashFunc()
algID, ok := winAlgIDs[hf]
if !ok {
return nil, ErrBadRSAHashAlgorithm
}
switch opts.(type) {
case *rsa.PSSOptions:
return winSignRSAPSSPadding(k.handle, digest, algID)
default:
return winSignRSAPKCS1Padding(k.handle, digest, algID)
}
default:
return nil, ErrBadSigningAlgorithm
}
}
func winSignECDSA(kh uintptr, digest []byte) ([]byte, error) {
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
0,
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrStoreECDSASigningError
}
// Obtain the signature data
buf := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
0,
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrStoreECDSASigningError
}
if len(buf) != int(size) {
return nil, ErrStoreECDSASigningError
}
return winPackECDSASigValue(bytes.NewReader(buf[:size]), int(size/2))
}
func winPackECDSASigValue(r io.Reader, digestLength int) ([]byte, error) {
sigR := make([]byte, digestLength)
if _, err := io.ReadFull(r, sigR); err != nil {
return nil, ErrStoreECDSASigningError
}
sigS := make([]byte, digestLength)
if _, err := io.ReadFull(r, sigS); err != nil {
return nil, ErrStoreECDSASigningError
}
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1BigInt(new(big.Int).SetBytes(sigR))
b.AddASN1BigInt(new(big.Int).SetBytes(sigS))
})
return b.Bytes()
}
func winSignRSAPKCS1Padding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
// PKCS#1 v1.5 padding for some TLS 1.2
padInfo := winPKCS1PaddingInfo{pszAlgID: algID}
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
winBCryptPadPKCS1)
if r != 0 {
return nil, ErrStoreRSASigningError
}
// Obtain the signature data
sig := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&sig[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
winBCryptPadPKCS1)
if r != 0 {
return nil, ErrStoreRSASigningError
}
return sig[:size], nil
}
func winSignRSAPSSPadding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
// PSS padding for TLS 1.3 and some TLS 1.2
padInfo := winPSSPaddingInfo{pszAlgID: algID, cbSalt: winBCryptPadPSSSalt}
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
winBCryptPadPSS)
if r != 0 {
return nil, ErrStoreRSASigningError
}
// Obtain the signature data
sig := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&sig[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
winBCryptPadPSS)
if r != 0 {
return nil, ErrStoreRSASigningError
}
return sig[:size], nil
}
// certKey wraps CryptAcquireCertificatePrivateKey. It obtains the CNG private
// key of a known certificate and returns a pointer to a winKey which implements
// both crypto.Signer. When a nil cert context is passed
// a nil key is intentionally returned, to model the expected behavior of a
// non-existent cert having no private key.
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecertificateprivatekey
func (w *winCertStore) certKey(cert *windows.CertContext) (*winKey, error) {
// Return early if a nil cert was passed.
if cert == nil {
return nil, nil
}
var (
kh uintptr
spec uint32
mustFree int
)
r, _, _ := winCryptAcquireCertificatePrivateKey.Call(
uintptr(unsafe.Pointer(cert)),
winAcquireCached|winAcquireSilent|winAcquireOnlyNCryptKey,
0, // Reserved, must be null.
uintptr(unsafe.Pointer(&kh)),
uintptr(unsafe.Pointer(&spec)),
uintptr(unsafe.Pointer(&mustFree)),
)
// If the function succeeds, the return value is nonzero (TRUE).
if r == 0 {
return nil, ErrNoPrivateKeyStoreRef
}
if mustFree != 0 {
return nil, ErrNoPrivateKeyStoreRef
}
if spec != winNcryptKeySpec {
return nil, ErrNoPrivateKeyStoreRef
}
return winKeyMetadata(kh)
}
func winKeyMetadata(kh uintptr) (*winKey, error) {
// uc is used to populate the unique container name attribute of the private key
uc, err := winGetPropertyStr(kh, winNCryptUniqueNameProperty)
if err != nil {
// unable to determine key unique name
return nil, ErrExtractingPrivateKeyMetadata
}
alg, err := winGetPropertyStr(kh, winNCryptAlgorithmGroupProperty)
if err != nil {
// unable to determine key algorithm
return nil, ErrExtractingPrivateKeyMetadata
}
var pub crypto.PublicKey
switch alg {
case "ECDSA", "ECDH":
buf, err := winExport(kh, winBCryptECCPublicBlob)
if err != nil {
// failed to export ECC public key
return nil, ErrExtractingECCPublicKey
}
pub, err = unmarshalECC(buf, kh)
if err != nil {
return nil, ErrExtractingECCPublicKey
}
case "RSA":
buf, err := winExport(kh, winBCryptRSAPublicBlob)
if err != nil {
return nil, ErrExtractingRSAPublicKey
}
pub, err = winUnmarshalRSA(buf)
if err != nil {
return nil, ErrExtractingRSAPublicKey
}
default:
return nil, ErrBadPublicKeyAlgorithm
}
return &winKey{handle: kh, pub: pub, Container: uc, AlgorithmGroup: alg}, nil
}
func winGetProperty(kh uintptr, property *uint16) ([]byte, error) {
var strSize uint32
r, _, _ := winNCryptGetProperty.Call(
kh,
uintptr(unsafe.Pointer(property)),
0,
0,
uintptr(unsafe.Pointer(&strSize)),
0,
0)
if r != 0 {
return nil, ErrExtractPropertyFromKey
}
buf := make([]byte, strSize)
r, _, _ = winNCryptGetProperty.Call(
kh,
uintptr(unsafe.Pointer(property)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(strSize),
uintptr(unsafe.Pointer(&strSize)),
0,
0)
if r != 0 {
return nil, ErrExtractPropertyFromKey
}
return buf, nil
}
func winGetPropertyStr(kh uintptr, property *uint16) (string, error) {
buf, err := winFnGetProperty(kh, property)
if err != nil {
return "", ErrExtractPropertyFromKey
}
uc := bytes.ReplaceAll(buf, []byte{0x00}, []byte(""))
return string(uc), nil
}
func winExport(kh uintptr, blobType *uint16) ([]byte, error) {
var size uint32
// When obtaining the size of a public key, most parameters are not required
r, _, _ := winNCryptExportKey.Call(
kh,
0,
uintptr(unsafe.Pointer(blobType)),
0,
0,
0,
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrExtractingPublicKey
}
// Place the exported key in buf now that we know the size required
buf := make([]byte, size)
r, _, _ = winNCryptExportKey.Call(
kh,
0,
uintptr(unsafe.Pointer(blobType)),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrExtractingPublicKey
}
return buf, nil
}
func unmarshalECC(buf []byte, kh uintptr) (*ecdsa.PublicKey, error) {
// BCRYPT_ECCKEY_BLOB from bcrypt.h
header := struct {
Magic uint32
Key uint32
}{}
r := bytes.NewReader(buf)
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return nil, ErrExtractingECCPublicKey
}
curve, ok := winCurveIDs[header.Magic]
if !ok {
// Fix for b/185945636, where despite specifying the curve, nCrypt returns
// an incorrect response with BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC.
var err error
curve, err = winCurveName(kh)
if err != nil {
// unsupported header magic or cannot match the curve by name
return nil, err
}
}
keyX := make([]byte, header.Key)
if n, err := r.Read(keyX); n != int(header.Key) || err != nil {
// failed to read key X
return nil, ErrExtractingECCPublicKey
}
keyY := make([]byte, header.Key)
if n, err := r.Read(keyY); n != int(header.Key) || err != nil {
// failed to read key Y
return nil, ErrExtractingECCPublicKey
}
pub := &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(keyX),
Y: new(big.Int).SetBytes(keyY),
}
return pub, nil
}
// winCurveName reads the curve name property and returns the corresponding curve.
func winCurveName(kh uintptr) (elliptic.Curve, error) {
cn, err := winGetPropertyStr(kh, winNCryptECCCurveNameProperty)
if err != nil {
// unable to determine the curve property name
return nil, ErrExtractPropertyFromKey
}
curve, ok := winCurveNames[cn]
if !ok {
// unknown curve name
return nil, ErrBadECCCurveName
}
return curve, nil
}
func winUnmarshalRSA(buf []byte) (*rsa.PublicKey, error) {
// BCRYPT_RSA_BLOB from bcrypt.h
header := struct {
Magic uint32
BitLength uint32
PublicExpSize uint32
ModulusSize uint32
UnusedPrime1 uint32
UnusedPrime2 uint32
}{}
r := bytes.NewReader(buf)
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return nil, ErrExtractingRSAPublicKey
}
if header.Magic != winRSA1Magic {
// invalid header magic
return nil, ErrExtractingRSAPublicKey
}
if header.PublicExpSize > 8 {
// unsupported public exponent size
return nil, ErrExtractingRSAPublicKey
}
exp := make([]byte, 8)
if n, err := r.Read(exp[8-header.PublicExpSize:]); n != int(header.PublicExpSize) || err != nil {
// failed to read public exponent
return nil, ErrExtractingRSAPublicKey
}
mod := make([]byte, header.ModulusSize)
if n, err := r.Read(mod); n != int(header.ModulusSize) || err != nil {
// failed to read modulus
return nil, ErrExtractingRSAPublicKey
}
pub := &rsa.PublicKey{
N: new(big.Int).SetBytes(mod),
E: int(binary.BigEndian.Uint64(exp)),
}
return pub, nil
}
// storeHandle returns a handle to a given cert store, opening the handle as needed.
func (w *winCertStore) storeHandle(provider uint32, store *uint16) (windows.Handle, error) {
w.mu.Lock()
defer w.mu.Unlock()
key := fmt.Sprintf("%d%s", provider, windows.UTF16PtrToString(store))
var err error
if w.stores[key] == nil {
w.stores[key], err = winNewStoreHandle(provider, store)
if err != nil {
return 0, ErrBadCryptoStoreProvider
}
}
return *w.stores[key].handle, nil
}
// Verify interface conformance.
var _ credential = &winKey{}

View File

@@ -0,0 +1,79 @@
package certstore
import (
"errors"
)
var (
// ErrBadCryptoStoreProvider represents inablity to establish link with a certificate store
ErrBadCryptoStoreProvider = errors.New("unable to open certificate store or store not available")
// ErrBadRSAHashAlgorithm represents a bad or unsupported RSA hash algorithm
ErrBadRSAHashAlgorithm = errors.New("unsupported RSA hash algorithm")
// ErrBadSigningAlgorithm represents a bad or unsupported signing algorithm
ErrBadSigningAlgorithm = errors.New("unsupported signing algorithm")
// ErrStoreRSASigningError represents an error returned from store during RSA signature
ErrStoreRSASigningError = errors.New("unable to obtain RSA signature from store")
// ErrStoreECDSASigningError represents an error returned from store during ECDSA signature
ErrStoreECDSASigningError = errors.New("unable to obtain ECDSA signature from store")
// ErrNoPrivateKeyStoreRef represents an error getting a handle to a private key in store
ErrNoPrivateKeyStoreRef = errors.New("unable to obtain private key handle from store")
// ErrExtractingPrivateKeyMetadata represents a family of errors extracting metadata about the private key in store
ErrExtractingPrivateKeyMetadata = errors.New("unable to extract private key metadata")
// ErrExtractingECCPublicKey represents an error exporting ECC-type public key from store
ErrExtractingECCPublicKey = errors.New("unable to extract ECC public key from store")
// ErrExtractingRSAPublicKey represents an error exporting RSA-type public key from store
ErrExtractingRSAPublicKey = errors.New("unable to extract RSA public key from store")
// ErrExtractingPublicKey represents a general error exporting public key from store
ErrExtractingPublicKey = errors.New("unable to extract public key from store")
// ErrBadPublicKeyAlgorithm represents a bad or unsupported public key algorithm
ErrBadPublicKeyAlgorithm = errors.New("unsupported public key algorithm")
// ErrExtractPropertyFromKey represents a general failure to extract a metadata property field
ErrExtractPropertyFromKey = errors.New("unable to extract property from key")
// ErrBadECCCurveName represents an ECC signature curve name that is bad or unsupported
ErrBadECCCurveName = errors.New("unsupported ECC curve name")
// ErrFailedCertSearch represents not able to find certificate in store
ErrFailedCertSearch = errors.New("unable to find certificate in store")
// ErrFailedX509Extract represents not being able to extract x509 certificate from found cert in store
ErrFailedX509Extract = errors.New("unable to extract x509 from certificate")
// ErrBadMatchByType represents unknown CERT_MATCH_BY passed
ErrBadMatchByType = errors.New("cert match by type not implemented")
// ErrBadCertStore represents unknown CERT_STORE passed
ErrBadCertStore = errors.New("cert store type not implemented")
// ErrConflictCertFileAndStore represents ambiguous configuration of both file and store
ErrConflictCertFileAndStore = errors.New("'cert_file' and 'cert_store' may not both be configured")
// ErrBadCertStoreField represents malformed cert_store option
ErrBadCertStoreField = errors.New("expected 'cert_store' to be a valid non-empty string")
// ErrBadCertMatchByField represents malformed cert_match_by option
ErrBadCertMatchByField = errors.New("expected 'cert_match_by' to be a valid non-empty string")
// ErrBadCertMatchField represents malformed cert_match option
ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string")
// ErrBadCaCertMatchField represents malformed cert_match option
ErrBadCaCertMatchField = errors.New("expected 'ca_certs_match' to be a valid non-empty string array")
// ErrBadCertMatchSkipInvalidField represents malformed cert_match_skip_invalid option
ErrBadCertMatchSkipInvalidField = errors.New("expected 'cert_match_skip_invalid' to be a boolean")
// ErrOSNotCompatCertStore represents cert_store passed that exists but is not valid on current OS
ErrOSNotCompatCertStore = errors.New("cert_store not compatible with current operating system")
)

View File

@@ -0,0 +1,350 @@
// Copyright 2022-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package server
import (
"crypto/tls"
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go"
)
func runPowershellScript(scriptFile string, args []string) error {
psExec, _ := exec.LookPath("powershell.exe")
execArgs := []string{psExec, "-command", fmt.Sprintf("& '%s'", scriptFile)}
if len(args) > 0 {
execArgs = append(execArgs, args...)
}
cmdImport := &exec.Cmd{
Path: psExec,
Args: execArgs,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
return cmdImport.Run()
}
func runConfiguredLeaf(t *testing.T, hubPort int, certStore string, matchBy string, match string, caMatch string, expectedLeafCount int) {
// Fire up the leaf
u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", hubPort))
if err != nil {
t.Fatalf("Error parsing url: %v", err)
}
configStr := fmt.Sprintf(`
port: -1
leaf {
remotes [
{
url: "%s"
tls {
cert_store: "%s"
cert_match_by: "%s"
cert_match: "%s"
ca_certs_match: %s
# Test settings that succeed should be equivalent to:
# cert_file: "../test/configs/certs/tlsauth/client.pem"
# key_file: "../test/configs/certs/tlsauth/client-key.pem"
# ca_file: "../test/configs/certs/tlsauth/ca.pem"
timeout: 5
}
}
]
}
`, u.String(), certStore, matchBy, match, caMatch)
leafConfig := createConfFile(t, []byte(configStr))
defer removeFile(t, leafConfig)
leafServer, _ := RunServerWithConfig(leafConfig)
defer leafServer.Shutdown()
// After client verify, hub will match by SAN email, SAN dns, and Subject (in that order)
// Our test client specifies Subject only so we should match on that...
// A little settle time
time.Sleep(1 * time.Second)
checkLeafNodeConnectedCount(t, leafServer, expectedLeafCount)
}
// TestLeafTLSWindowsCertStore tests the topology of two NATS Servers connected as leaf and hub with authentication of
// leaf to hub via mTLS with leaf's certificate and signing key provisioned in the Windows certificate store.
func TestLeafTLSWindowsCertStore(t *testing.T) {
// Client Identity (client.pem)
// Issuer: O = NATS CA, OU = NATS.io, CN = localhost
// Subject: OU = NATS.io, CN = example.com
// Make sure windows cert store is reset to avoid conflict with other tests
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
if err != nil {
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
}
// Provision Windows cert store with client cert and secret
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-client.ps1", nil)
if err != nil {
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
}
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-ca.ps1", nil)
if err != nil {
t.Fatalf("expected powershell provision CA to succeed: %s", err.Error())
}
// Fire up the hub
hubConfig := createConfFile(t, []byte(`
port: -1
leaf {
listen: "127.0.0.1:-1"
tls {
ca_file: "../test/configs/certs/tlsauth/ca.pem"
cert_file: "../test/configs/certs/tlsauth/server.pem"
key_file: "../test/configs/certs/tlsauth/server-key.pem"
timeout: 5
verify_and_map: true
}
}
accounts: {
AcctA: {
users: [ {user: "OU = NATS.io, CN = example.com"} ]
},
AcctB: {
users: [ {user: UserB1} ]
},
SYS: {
users: [ {user: System} ]
}
}
system_account: "SYS"
`))
defer removeFile(t, hubConfig)
hubServer, hubOptions := RunServerWithConfig(hubConfig)
defer hubServer.Shutdown()
testCases := []struct {
certStore string
certMatchBy string
certMatch string
caCertsMatch string
expectedLeafCount int
}{
// Test subject and issuer
{"WindowsCurrentUser", "Subject", "example.com", "\"NATS CA\"", 1},
{"WindowsCurrentUser", "Issuer", "NATS CA", "\"NATS CA\"", 1},
{"WindowsCurrentUser", "Issuer", "Frodo Baggins, Inc.", "\"NATS CA\"", 0},
{"WindowsCurrentUser", "Thumbprint", "7e44f478114a2e29b98b00beb1b3687d8dc0e481", "\"NATS CA\"", 0},
// Test CAs, NATS CA is valid, others are missing
{"WindowsCurrentUser", "Subject", "example.com", "[\"NATS CA\"]", 1},
{"WindowsCurrentUser", "Subject", "example.com", "[\"GlobalSign\"]", 0},
{"WindowsCurrentUser", "Subject", "example.com", "[\"Missing NATS Cert\"]", 0},
{"WindowsCurrentUser", "Subject", "example.com", "[\"NATS CA\", \"Missing NATS Cert1\"]", 1},
{"WindowsCurrentUser", "Subject", "example.com", "[\"Missing Cert2\",\"NATS CA\"]", 1},
{"WindowsCurrentUser", "Subject", "example.com", "[\"Missing, Cert3\",\"Missing NATS Cert4\"]", 0},
}
for _, tc := range testCases {
testName := fmt.Sprintf("%s by %s match %s", tc.certStore, tc.certMatchBy, tc.certMatch)
t.Run(fmt.Sprintf(testName, tc.certStore, tc.certMatchBy, tc.certMatch, tc.caCertsMatch), func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if tc.expectedLeafCount != 0 {
t.Fatalf("did not expect panic: %s", testName)
} else {
if !strings.Contains(fmt.Sprintf("%v", r), "Error processing configuration file") {
t.Fatalf("did not expect unknown panic: %s", testName)
}
}
}
}()
runConfiguredLeaf(t, hubOptions.LeafNode.Port,
tc.certStore, tc.certMatchBy, tc.certMatch,
tc.caCertsMatch, tc.expectedLeafCount)
})
}
}
// TestServerTLSWindowsCertStore tests the topology of a NATS server requiring TLS and gettings it own server
// cert identity (as used when accepting NATS client connections and negotiating TLS) from Windows certificate store.
func TestServerTLSWindowsCertStore(t *testing.T) {
// Server Identity (server.pem)
// Issuer: O = NATS CA, OU = NATS.io, CN = localhost
// Subject: OU = NATS.io Operators, CN = localhost
// Make sure windows cert store is reset to avoid conflict with other tests
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
if err != nil {
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
}
// Provision Windows cert store with server cert and secret
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", nil)
if err != nil {
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
}
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-ca.ps1", nil)
if err != nil {
t.Fatalf("expected powershell provision CA to succeed: %s", err.Error())
}
// Fire up the server
srvConfig := createConfFile(t, []byte(`
listen: "localhost:-1"
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Subject"
cert_match: "NATS.io Operators"
ca_certs_match: ["NATS CA"]
timeout: 5
}
`))
defer removeFile(t, srvConfig)
srvServer, _ := RunServerWithConfig(srvConfig)
if srvServer == nil {
t.Fatalf("expected to be able start server with cert store configuration")
}
defer srvServer.Shutdown()
testCases := []struct {
clientCA string
expect bool
}{
{"../test/configs/certs/tlsauth/ca.pem", true},
{"../test/configs/certs/tlsauth/client.pem", false},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Client CA: %s", tc.clientCA), func(t *testing.T) {
nc, _ := nats.Connect(srvServer.clientConnectURLs[0], nats.RootCAs(tc.clientCA))
err := nc.Publish("foo", []byte("hello TLS server-authenticated server"))
if (err != nil) == tc.expect {
t.Fatalf("expected publish result %v to TLS authenticated server", tc.expect)
}
nc.Close()
for i := 0; i < 5; i++ {
nc, _ = nats.Connect(srvServer.clientConnectURLs[0], nats.RootCAs(tc.clientCA))
err = nc.Publish("foo", []byte("hello TLS server-authenticated server"))
if (err != nil) == tc.expect {
t.Fatalf("expected repeated connection result %v to TLS authenticated server", tc.expect)
}
nc.Close()
}
})
}
}
// TestServerIgnoreExpiredCerts tests if the server skips expired certificates in configuration, and finds non-expired ones
func TestServerIgnoreExpiredCerts(t *testing.T) {
// Server Identities: expired.pem; not-expired.pem
// Issuer: OU = NATS.io, CN = localhost
// Subject: OU = NATS.io Operators, CN = localhost
testCases := []struct {
certFile string
expect bool
}{
{"expired.p12", false},
{"not-expired.p12", true},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Server certificate: %s", tc.certFile), func(t *testing.T) {
// Make sure windows cert store is reset to avoid conflict with other tests
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
if err != nil {
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
}
// Provision Windows cert store with server cert and secret
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", []string{tc.certFile})
if err != nil {
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
}
// Fire up the server
srvConfig := createConfFile(t, []byte(`
listen: "localhost:-1"
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Subject"
cert_match: "NATS.io Operators"
cert_match_skip_invalid: true
timeout: 5
}
`))
defer removeFile(t, srvConfig)
cfg, _ := ProcessConfigFile(srvConfig)
if (cfg != nil) == tc.expect {
return
}
if tc.expect == false {
t.Fatalf("expected server start to fail with expired certificate")
} else {
t.Fatalf("expected server to start with non expired certificate")
}
})
}
}
func TestWindowsTLS12ECDSA(t *testing.T) {
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", []string{"ecdsa_server.pfx"})
if err != nil {
t.Fatalf("expected powershell provision to succeed: %v", err)
}
config := createConfFile(t, []byte(`
listen: "localhost:-1"
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Thumbprint"
cert_match: "4F8AF21756E5DBBD54619BBB6F3CC5D455ED4468"
cert_match_skip_invalid: true
timeout: 5
}
`))
defer removeFile(t, config)
srv, _ := RunServerWithConfig(config)
if srv == nil {
t.Fatalf("expected to be able start server with cert store configuration")
}
defer srv.Shutdown()
for name, version := range map[string]uint16{
"TLS 1.3": tls.VersionTLS13,
"TLS 1.2": tls.VersionTLS12,
} {
t.Run(name, func(t *testing.T) {
tc := &tls.Config{MaxVersion: version, MinVersion: version, InsecureSkipVerify: true}
if _, err = nats.Connect(srv.clientConnectURLs[0], nats.Secure(tc)); err != nil {
t.Fatalf("connection with %s: %v", name, err)
}
t.Logf("successful connection with %s", name)
})
}
}

79
server/ciphersuites.go Normal file
View File

@@ -0,0 +1,79 @@
// Copyright 2016-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"crypto/fips140"
"crypto/tls"
)
func init() {
for _, cs := range tls.CipherSuites() {
cipherMap[cs.Name] = cs
cipherMapByID[cs.ID] = cs
}
for _, cs := range tls.InsecureCipherSuites() {
cipherMap[cs.Name] = cs
cipherMapByID[cs.ID] = cs
}
}
var cipherMap = map[string]*tls.CipherSuite{}
var cipherMapByID = map[uint16]*tls.CipherSuite{}
// Where we maintain available curve preferences
var curvePreferenceMap = map[string]tls.CurveID{
"SecP384r1MLKEM1024": tls.SecP384r1MLKEM1024,
"SecP256r1MLKEM768": tls.SecP256r1MLKEM768,
"X25519MLKEM768": tls.X25519MLKEM768,
"X25519": tls.X25519,
"CurveP256": tls.CurveP256,
"CurveP384": tls.CurveP384,
"CurveP521": tls.CurveP521,
}
func defaultCipherSuites() []uint16 {
ciphers := tls.CipherSuites()
defaults := make([]uint16, 0, len(ciphers))
for _, cs := range ciphers {
defaults = append(defaults, cs.ID)
}
return defaults
}
// reorder to default to the highest level of security. See:
// https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
func defaultCurvePreferences() []tls.CurveID {
if fips140.Enabled() {
// X25519 is not FIPS-approved by itself, but it is when combined with MLKEM768.
// SecP256r1MLKEM768 and SecP384r1MLKEM1024 are both FIPS-approved hybrids.
return []tls.CurveID{
tls.X25519MLKEM768, // post-quantum
tls.SecP384r1MLKEM1024, // post-quantum
tls.SecP256r1MLKEM768, // post-quantum
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
}
}
return []tls.CurveID{
tls.X25519MLKEM768, // post-quantum
tls.SecP384r1MLKEM1024, // post-quantum
tls.SecP256r1MLKEM768, // post-quantum
tls.X25519, // faster than P256, arguably more secure
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
}
}

415
server/client_proxyproto.go Normal file
View File

@@ -0,0 +1,415 @@
// Copyright 2025-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
)
// PROXY protocol v2 constants
const (
// Protocol signature (12 bytes)
proxyProtoV2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
// Version and command byte format: version(4 bits) | command(4 bits)
proxyProtoV2VerMask = 0xF0
proxyProtoV2Ver = 0x20 // Version 2
// Commands
proxyProtoCmdMask = 0x0F
proxyProtoCmdLocal = 0x00 // LOCAL command (health check, use original connection)
proxyProtoCmdProxy = 0x01 // PROXY command (proxied connection)
// Address family and protocol byte format: family(4 bits) | protocol(4 bits)
proxyProtoFamilyMask = 0xF0
proxyProtoFamilyUnspec = 0x00 // Unspecified
proxyProtoFamilyInet = 0x10 // IPv4
proxyProtoFamilyInet6 = 0x20 // IPv6
proxyProtoFamilyUnix = 0x30 // Unix socket
proxyProtoProtoMask = 0x0F
proxyProtoProtoUnspec = 0x00 // Unspecified
proxyProtoProtoStream = 0x01 // TCP/STREAM
proxyProtoProtoDatagram = 0x02 // UDP/DGRAM
// Address sizes
proxyProtoAddrSizeIPv4 = 12 // 4 (src IP) + 4 (dst IP) + 2 (src port) + 2 (dst port)
proxyProtoAddrSizeIPv6 = 36 // 16 (src IP) + 16 (dst IP) + 2 (src port) + 2 (dst port)
// Header sizes
proxyProtoV2HeaderSize = 16 // Fixed header: 12 (sig) + 1 (ver/cmd) + 1 (fam/proto) + 2 (addr len)
// Timeout for reading PROXY protocol header
proxyProtoReadTimeout = 5 * time.Second
)
// PROXY protocol v1 constants
const (
proxyProtoV1Prefix = "PROXY "
proxyProtoV1MaxLineLen = 107 // Maximum line length including CRLF
proxyProtoV1TCP4 = "TCP4"
proxyProtoV1TCP6 = "TCP6"
proxyProtoV1Unknown = "UNKNOWN"
)
var (
// Errors
errProxyProtoInvalid = errors.New("invalid PROXY protocol header")
errProxyProtoUnsupported = errors.New("unsupported PROXY protocol feature")
errProxyProtoTimeout = errors.New("timeout reading PROXY protocol header")
errProxyProtoUnrecognized = errors.New("unrecognized PROXY protocol format")
)
// proxyProtoAddr contains the address information extracted from PROXY protocol header
type proxyProtoAddr struct {
srcIP net.IP
srcPort uint16
dstIP net.IP
dstPort uint16
}
// String implements net.Addr interface
func (p *proxyProtoAddr) String() string {
return net.JoinHostPort(p.srcIP.String(), fmt.Sprintf("%d", p.srcPort))
}
// Network implements net.Addr interface
func (p *proxyProtoAddr) Network() string {
if p.srcIP.To4() != nil {
return "tcp4"
}
return "tcp6"
}
// proxyConn wraps a net.Conn to override RemoteAddr() with the address
// extracted from the PROXY protocol header
type proxyConn struct {
net.Conn
remoteAddr net.Addr
}
// RemoteAddr returns the original client address extracted from PROXY protocol
func (pc *proxyConn) RemoteAddr() net.Addr {
return pc.remoteAddr
}
// detectProxyProtoVersion reads the first bytes and determines protocol version.
// Returns 1 for v1, 2 for v2, or error.
// The first 6 bytes read are returned so they can be used by the parser.
func detectProxyProtoVersion(conn net.Conn) (version int, header []byte, err error) {
// Read first 6 bytes to check for "PROXY " or v2 signature
header = make([]byte, 6)
if _, err = io.ReadFull(conn, header); err != nil {
return 0, nil, fmt.Errorf("failed to read protocol version: %w", err)
}
switch bytesToString(header) {
case proxyProtoV1Prefix:
return 1, header, nil
case proxyProtoV2Sig[:6]:
return 2, header, nil
default:
// Return the consumed bytes so the caller can replay them into the
// next protocol layer instead of discarding them.
return 0, header, errProxyProtoUnrecognized
}
}
// readProxyProtoV1Header parses PROXY protocol v1 text format.
// Expects the "PROXY " prefix (6 bytes) to have already been consumed.
// Returns any bytes that were read past the trailing CRLF so the caller can
// replay them into the next protocol layer.
func readProxyProtoV1Header(conn net.Conn) (*proxyProtoAddr, []byte, error) {
// Read rest of line (max 107 bytes total, already read 6)
maxRemaining := proxyProtoV1MaxLineLen - 6
// Read up to maxRemaining bytes at once (more efficient than byte-by-byte)
buf := make([]byte, maxRemaining)
var line []byte
var remaining []byte
for len(line) < maxRemaining {
// Read available data
n, err := conn.Read(buf[len(line):])
if err != nil {
return nil, nil, fmt.Errorf("failed to read v1 line: %w", err)
}
line = buf[:len(line)+n]
// Look for CRLF in what we've read so far
for i := 0; i < len(line)-1; i++ {
if line[i] == '\r' && line[i+1] == '\n' {
// Found CRLF - keep any over-read bytes for the client parser.
remaining = append(remaining, line[i+2:]...)
line = line[:i]
goto foundCRLF
}
}
}
// Exceeded max length without finding CRLF
return nil, nil, fmt.Errorf("%w: v1 line too long", errProxyProtoInvalid)
foundCRLF:
// Get parts from the protocol
parts := strings.Fields(string(line))
// Validate format
if len(parts) < 1 {
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
}
// Handle UNKNOWN (health check, like v2 LOCAL)
if parts[0] == proxyProtoV1Unknown {
return nil, remaining, nil
}
// Must have exactly 5 parts: protocol, src-ip, dst-ip, src-port, dst-port
if len(parts) != 5 {
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
}
protocol := parts[0]
srcIP := net.ParseIP(parts[1])
dstIP := net.ParseIP(parts[2])
if srcIP == nil || dstIP == nil {
return nil, nil, fmt.Errorf("%w: invalid address", errProxyProtoInvalid)
}
// Parse ports
srcPort, err := strconv.ParseUint(parts[3], 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("invalid source port: %w", err)
}
dstPort, err := strconv.ParseUint(parts[4], 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("invalid dest port: %w", err)
}
// Validate protocol matches IP version. The textual form determines the
// family: TCP4 requires dotted-quad addresses, TCP6 requires IPv6
// addresses. IPv4-mapped IPv6 addresses (e.g. "::ffff:192.0.2.1") are
// valid for TCP6 since dual-stack proxies can emit those for IPv4
// clients on IPv6 sockets, matching the v2 parser behavior.
srcIsV6 := strings.Contains(parts[1], ":")
dstIsV6 := strings.Contains(parts[2], ":")
if protocol == proxyProtoV1TCP4 && (srcIsV6 || dstIsV6) {
return nil, nil, fmt.Errorf("%w: TCP4 with IPv6 address", errProxyProtoInvalid)
}
if protocol == proxyProtoV1TCP6 && (!srcIsV6 || !dstIsV6) {
return nil, nil, fmt.Errorf("%w: TCP6 with IPv4 address", errProxyProtoInvalid)
}
if protocol != proxyProtoV1TCP4 && protocol != proxyProtoV1TCP6 {
return nil, nil, fmt.Errorf("%w: invalid protocol %s", errProxyProtoInvalid, protocol)
}
return &proxyProtoAddr{
srcIP: srcIP,
srcPort: uint16(srcPort),
dstIP: dstIP,
dstPort: uint16(dstPort),
}, remaining, nil
}
// readProxyProtoHeader reads and parses PROXY protocol (v1 or v2) from the connection.
// Automatically detects version and routes to appropriate parser.
// If the command is LOCAL/UNKNOWN (health check), it returns nil for addr and no error.
// If the command is PROXY, it returns the parsed address information.
// It also returns any bytes that were read past the v1 header terminator so the
// caller can replay them into the normal client parser.
// The connection must be fresh (no data read yet).
func readProxyProtoHeader(conn net.Conn) (*proxyProtoAddr, []byte, error) {
// Set read deadline to prevent hanging on slow/malicious clients
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
return nil, nil, err
}
defer conn.SetReadDeadline(time.Time{})
// Detect version.
// On errProxyProtoUnrecognized, firstBytes holds the bytes that were
// consumed so the caller can replay them.
version, firstBytes, err := detectProxyProtoVersion(conn)
if err != nil {
return nil, firstBytes, err
}
switch version {
case 1:
// v1 parser expects "PROXY " prefix already consumed
return readProxyProtoV1Header(conn)
case 2:
// Read rest of v2 signature (bytes 6-11, total 6 more bytes)
remaining := make([]byte, 6)
if _, err := io.ReadFull(conn, remaining); err != nil {
return nil, nil, fmt.Errorf("failed to read v2 signature: %w", err)
}
// Verify full signature
fullSig := string(firstBytes) + string(remaining)
if fullSig != proxyProtoV2Sig {
return nil, nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
}
// Read rest of header: ver/cmd, fam/proto, addr-len (4 bytes)
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, nil, fmt.Errorf("failed to read v2 header: %w", err)
}
// Continue with parsing
addr, err := parseProxyProtoV2Header(conn, header)
return addr, nil, err
default:
return nil, nil, fmt.Errorf("unsupported PROXY protocol version: %d", version)
}
}
// readProxyProtoV2Header is kept for backward compatibility and direct testing.
// It reads and parses a PROXY protocol v2 header from the connection.
// If the command is LOCAL (health check), it returns nil for addr and no error.
// If the command is PROXY, it returns the parsed address information.
// The connection must be fresh (no data read yet).
func readProxyProtoV2Header(conn net.Conn) (*proxyProtoAddr, error) {
// Set read deadline to prevent hanging on slow/malicious clients
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
return nil, err
}
defer conn.SetReadDeadline(time.Time{})
// Read fixed header (16 bytes)
header := make([]byte, proxyProtoV2HeaderSize)
if _, err := io.ReadFull(conn, header); err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return nil, errProxyProtoTimeout
}
return nil, fmt.Errorf("failed to read PROXY protocol header: %w", err)
}
// Validate signature (first 12 bytes)
if string(header[:12]) != proxyProtoV2Sig {
return nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
}
// Continue with parsing after signature
return parseProxyProtoV2Header(conn, header[12:16])
}
// parseProxyProtoV2Header parses v2 protocol after signature has been validated.
// header contains the 4 bytes: ver/cmd, fam/proto, addr-len (2 bytes).
func parseProxyProtoV2Header(conn net.Conn, header []byte) (*proxyProtoAddr, error) {
// Parse version and command
verCmd := header[0]
version := verCmd & proxyProtoV2VerMask
command := verCmd & proxyProtoCmdMask
if version != proxyProtoV2Ver {
return nil, fmt.Errorf("%w: invalid version 0x%02x", errProxyProtoInvalid, version)
}
// Parse address family and protocol
famProto := header[1]
family := famProto & proxyProtoFamilyMask
protocol := famProto & proxyProtoProtoMask
// Parse address length (big-endian uint16)
addrLen := binary.BigEndian.Uint16(header[2:4])
// Handle LOCAL command (health check)
if command == proxyProtoCmdLocal {
// For LOCAL, we should skip the address data if any
if addrLen > 0 {
// Discard the address data
if _, err := io.CopyN(io.Discard, conn, int64(addrLen)); err != nil {
return nil, fmt.Errorf("failed to discard LOCAL command address data: %w", err)
}
}
return nil, nil // nil addr indicates LOCAL command
}
// Handle PROXY command
if command != proxyProtoCmdProxy {
return nil, fmt.Errorf("unknown PROXY protocol command: 0x%02x", command)
}
// Validate protocol (we only support STREAM/TCP)
if protocol != proxyProtoProtoStream {
return nil, fmt.Errorf("%w: only STREAM protocol supported", errProxyProtoUnsupported)
}
// Parse address data based on family
var addr *proxyProtoAddr
var err error
switch family {
case proxyProtoFamilyInet:
addr, err = parseIPv4Addr(conn, addrLen)
case proxyProtoFamilyInet6:
addr, err = parseIPv6Addr(conn, addrLen)
case proxyProtoFamilyUnspec:
// UNSPEC family with PROXY command is valid but rare
// Just skip the address data
if addrLen > 0 {
if _, err := io.CopyN(io.Discard, conn, int64(addrLen)); err != nil {
return nil, fmt.Errorf("failed to discard UNSPEC address address data: %w", err)
}
}
return nil, nil
default:
return nil, fmt.Errorf("%w: unsupported address family 0x%02x", errProxyProtoUnsupported, family)
}
return addr, err
}
// parseIPv4Addr parses IPv4 address data from PROXY protocol header
func parseIPv4Addr(conn net.Conn, addrLen uint16) (*proxyProtoAddr, error) {
// IPv4: 4 (src IP) + 4 (dst IP) + 2 (src port) + 2 (dst port) = 12 bytes minimum
if addrLen < proxyProtoAddrSizeIPv4 {
return nil, fmt.Errorf("IPv4 address data too short: %d bytes", addrLen)
}
addrData := make([]byte, addrLen)
if _, err := io.ReadFull(conn, addrData); err != nil {
return nil, fmt.Errorf("failed to read IPv4 address data: %w", err)
}
return &proxyProtoAddr{
srcIP: net.IP(addrData[0:4]),
dstIP: net.IP(addrData[4:8]),
srcPort: binary.BigEndian.Uint16(addrData[8:10]),
dstPort: binary.BigEndian.Uint16(addrData[10:12]),
}, nil
}
// parseIPv6Addr parses IPv6 address data from PROXY protocol header
func parseIPv6Addr(conn net.Conn, addrLen uint16) (*proxyProtoAddr, error) {
// IPv6: 16 (src IP) + 16 (dst IP) + 2 (src port) + 2 (dst port) = 36 bytes minimum
if addrLen < proxyProtoAddrSizeIPv6 {
return nil, fmt.Errorf("IPv6 address data too short: %d bytes", addrLen)
}
addrData := make([]byte, addrLen)
if _, err := io.ReadFull(conn, addrData); err != nil {
return nil, fmt.Errorf("failed to read IPv6 address data: %w", err)
}
return &proxyProtoAddr{
srcIP: net.IP(addrData[0:16]),
dstIP: net.IP(addrData[16:32]),
srcPort: binary.BigEndian.Uint16(addrData[32:34]),
dstPort: binary.BigEndian.Uint16(addrData[34:36]),
}, nil
}

View File

@@ -0,0 +1,925 @@
// Copyright 2025-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"bufio"
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"strings"
"testing"
"time"
)
// mockConn is a mock net.Conn for testing
type mockConn struct {
net.Conn
readBuf *bytes.Buffer
writeBuf *bytes.Buffer
closed bool
deadline time.Time
}
func newMockConn(data []byte) *mockConn {
return &mockConn{
readBuf: bytes.NewBuffer(data),
writeBuf: &bytes.Buffer{},
}
}
func (m *mockConn) Read(b []byte) (int, error) {
if m.closed {
return 0, fmt.Errorf("connection closed")
}
if !m.deadline.IsZero() && time.Now().After(m.deadline) {
return 0, &net.OpError{Op: "read", Err: fmt.Errorf("timeout")}
}
return m.readBuf.Read(b)
}
func (m *mockConn) Write(b []byte) (int, error) {
if m.closed {
return 0, fmt.Errorf("connection closed")
}
return m.writeBuf.Write(b)
}
func (m *mockConn) Close() error {
m.closed = true
return nil
}
func (m *mockConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4222}
}
func (m *mockConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}
}
func (m *mockConn) SetDeadline(t time.Time) error {
m.deadline = t
return nil
}
func (m *mockConn) SetReadDeadline(t time.Time) error {
m.deadline = t
return nil
}
func (m *mockConn) SetWriteDeadline(t time.Time) error {
return nil
}
// buildProxyV2Header builds a valid PROXY protocol v2 header
func buildProxyV2Header(t *testing.T, srcIP, dstIP string, srcPort, dstPort uint16, family byte) []byte {
t.Helper()
buf := &bytes.Buffer{}
buf.WriteString(proxyProtoV2Sig) // Write signature
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy) // Write version and command (version 2, PROXY command)
buf.WriteByte(family | proxyProtoProtoStream) // Write family and protocol
src := net.ParseIP(srcIP)
dst := net.ParseIP(dstIP)
var addrData []byte
switch family {
case proxyProtoFamilyInet:
// IPv4: 12 bytes
addrData = make([]byte, proxyProtoAddrSizeIPv4)
copy(addrData[0:4], src.To4())
copy(addrData[4:8], dst.To4())
binary.BigEndian.PutUint16(addrData[8:10], srcPort)
binary.BigEndian.PutUint16(addrData[10:12], dstPort)
case proxyProtoFamilyInet6:
// IPv6: 36 bytes
addrData = make([]byte, proxyProtoAddrSizeIPv6)
copy(addrData[0:16], src.To16())
copy(addrData[16:32], dst.To16())
binary.BigEndian.PutUint16(addrData[32:34], srcPort)
binary.BigEndian.PutUint16(addrData[34:36], dstPort)
default:
t.Fatalf("unsupported address family: %d", family)
}
addrLen := make([]byte, 2)
binary.BigEndian.PutUint16(addrLen, uint16(len(addrData)))
buf.Write(addrLen)
buf.Write(addrData)
return buf.Bytes()
}
// buildProxyV2LocalHeader builds a PROXY protocol v2 LOCAL command header
func buildProxyV2LocalHeader() []byte {
buf := &bytes.Buffer{}
buf.WriteString(proxyProtoV2Sig) // // Write signature
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdLocal) // // Write version and command (version 2, LOCAL command)
buf.WriteByte(proxyProtoFamilyUnspec | proxyProtoProtoUnspec) // // Write family and protocol (UNSPEC)
buf.WriteByte(0)
buf.WriteByte(0)
return buf.Bytes()
}
// buildProxyV1Header builds a valid PROXY protocol v1 header (text format)
func buildProxyV1Header(t *testing.T, protocol, srcIP, dstIP string, srcPort, dstPort uint16) []byte {
t.Helper()
if protocol != "TCP4" && protocol != "TCP6" && protocol != "UNKNOWN" {
t.Fatalf("invalid protocol: %s", protocol)
}
var line string
if protocol == "UNKNOWN" {
line = "PROXY UNKNOWN\r\n"
} else {
line = fmt.Sprintf("PROXY %s %s %s %d %d\r\n", protocol, srcIP, dstIP, srcPort, dstPort)
}
return []byte(line)
}
func TestClientProxyProtoV2ParseIPv4(t *testing.T) {
header := buildProxyV2Header(t, "192.168.1.50", "10.0.0.1", 12345, 4222, proxyProtoFamilyInet)
conn := newMockConn(header)
addr, err := readProxyProtoV2Header(conn)
require_NoError(t, err)
require_NotNil(t, addr)
require_Equal(t, addr.srcIP.String(), "192.168.1.50")
require_Equal(t, addr.srcPort, 12345)
require_Equal(t, addr.dstIP.String(), "10.0.0.1")
require_Equal(t, addr.dstPort, 4222)
// Test String() and Network() methods
require_Equal(t, addr.String(), "192.168.1.50:12345")
require_Equal(t, addr.Network(), "tcp4")
}
func TestClientProxyProtoV2ParseIPv6(t *testing.T) {
header := buildProxyV2Header(t, "2001:db8::1", "2001:db8::2", 54321, 4222, proxyProtoFamilyInet6)
conn := newMockConn(header)
addr, err := readProxyProtoV2Header(conn)
require_NoError(t, err)
require_NotNil(t, addr)
require_Equal(t, addr.srcIP.String(), "2001:db8::1")
require_Equal(t, addr.srcPort, 54321)
require_Equal(t, addr.dstIP.String(), "2001:db8::2")
require_Equal(t, addr.dstPort, 4222)
// Test Network() method for IPv6
require_Equal(t, addr.String(), "[2001:db8::1]:54321")
require_Equal(t, addr.Network(), "tcp6")
}
func TestClientProxyProtoV2ParseLocalCommand(t *testing.T) {
header := buildProxyV2LocalHeader()
conn := newMockConn(header)
addr, err := readProxyProtoV2Header(conn)
require_NoError(t, err)
require_True(t, addr == nil)
}
func TestClientProxyProtoV2InvalidSignature(t *testing.T) {
// Create invalid signature
header := []byte("INVALID_SIG_")
header = append(header, []byte{0x20, 0x11, 0x00, 0x0C}...)
conn := newMockConn(header)
_, err := readProxyProtoV2Header(conn)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV2InvalidVersion(t *testing.T) {
buf := &bytes.Buffer{}
buf.WriteString(proxyProtoV2Sig)
buf.WriteByte(0x10 | proxyProtoCmdProxy) // Version 1 instead of 2
buf.WriteByte(proxyProtoFamilyInet | proxyProtoProtoStream)
buf.WriteByte(0)
buf.WriteByte(0)
conn := newMockConn(buf.Bytes())
_, err := readProxyProtoV2Header(conn)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV2UnsupportedFamily(t *testing.T) {
buf := &bytes.Buffer{}
buf.WriteString(proxyProtoV2Sig)
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy)
buf.WriteByte(proxyProtoFamilyUnix | proxyProtoProtoStream) // Unix socket family
buf.WriteByte(0)
buf.WriteByte(0)
conn := newMockConn(buf.Bytes())
_, err := readProxyProtoV2Header(conn)
require_Error(t, err, errProxyProtoUnsupported)
}
func TestClientProxyProtoV2UnsupportedProtocol(t *testing.T) {
buf := &bytes.Buffer{}
buf.WriteString(proxyProtoV2Sig)
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy)
buf.WriteByte(proxyProtoFamilyInet | proxyProtoProtoDatagram) // UDP instead of TCP
buf.WriteByte(0)
buf.WriteByte(12)
conn := newMockConn(buf.Bytes())
_, err := readProxyProtoV2Header(conn)
require_Error(t, err, errProxyProtoUnsupported)
}
func TestClientProxyProtoV2TruncatedHeader(t *testing.T) {
header := buildProxyV2Header(t, "192.168.1.50", "10.0.0.1", 12345, 4222, proxyProtoFamilyInet)
// Only send first 10 bytes (incomplete header)
conn := newMockConn(header[:10])
_, err := readProxyProtoV2Header(conn)
require_Error(t, err, io.ErrUnexpectedEOF)
}
func TestClientProxyProtoV2ShortAddressData(t *testing.T) {
buf := &bytes.Buffer{}
buf.WriteString(proxyProtoV2Sig)
buf.WriteByte(proxyProtoV2Ver | proxyProtoCmdProxy)
buf.WriteByte(proxyProtoFamilyInet | proxyProtoProtoStream)
// Set address length to 12 but don't provide data
buf.WriteByte(0)
buf.WriteByte(12)
// Only provide 5 bytes instead of 12
buf.Write([]byte{1, 2, 3, 4, 5})
conn := newMockConn(buf.Bytes())
_, err := readProxyProtoV2Header(conn)
require_Error(t, err, io.ErrUnexpectedEOF)
}
func TestProxyConnRemoteAddr(t *testing.T) {
// Create a real TCP connection for testing
originalAddr := &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}
// Create proxy address
proxyAddr := &proxyProtoAddr{
srcIP: net.ParseIP("10.0.0.50"),
srcPort: 12345,
dstIP: net.ParseIP("10.0.0.1"),
dstPort: 4222,
}
mockConn := newMockConn(nil)
wrapped := &proxyConn{
Conn: mockConn,
remoteAddr: proxyAddr,
}
// Verify RemoteAddr returns the proxied address
addr := wrapped.RemoteAddr()
require_Equal(t, addr.String(), "10.0.0.50:12345")
require_Equal(t, mockConn.RemoteAddr().String(), originalAddr.String())
}
func TestClientProxyProtoV2EndToEnd(t *testing.T) {
// Start a test server with PROXY protocol enabled
opts := DefaultOptions()
opts.Port = -1 // Random port
opts.ProxyProtocol = true
s := RunServer(opts)
defer s.Shutdown()
// Get the server's listening port
addr := s.Addr().String()
// Connect to the server
conn, err := net.Dial("tcp", addr)
require_NoError(t, err)
defer conn.Close()
// Send PROXY protocol header
clientIP := "203.0.113.50"
clientPort := uint16(54321)
header := buildProxyV2Header(t, clientIP, "127.0.0.1", clientPort, 4222, proxyProtoFamilyInet)
_, err = conn.Write(header)
require_NoError(t, err)
// Send CONNECT message
connectMsg := "CONNECT {\"verbose\":false,\"pedantic\":false,\"protocol\":1}\r\n"
_, err = conn.Write([]byte(connectMsg))
require_NoError(t, err)
// Read INFO and +OK
buf := make([]byte, 4096)
n, err := conn.Read(buf)
require_NoError(t, err)
response := string(buf[:n])
require_True(t, strings.Contains(response, "INFO"))
// Give server time to process
time.Sleep(100 * time.Millisecond)
// Check server's client list to verify the IP was extracted correctly
s.mu.Lock()
clients := s.clients
s.mu.Unlock()
require_True(t, len(clients) != 0)
// Find our client
var foundClient *client
for _, c := range clients {
c.mu.Lock()
if c.host == clientIP && c.port == clientPort {
foundClient = c
}
c.mu.Unlock()
if foundClient != nil {
break
}
}
require_NotNil(t, foundClient)
}
func TestClientProxyProtoV2LocalCommandEndToEnd(t *testing.T) {
// Start a test server with PROXY protocol enabled
opts := DefaultOptions()
opts.Port = -1 // Random port
opts.ProxyProtocol = true
s := RunServer(opts)
defer s.Shutdown()
// Get the server's listening port
addr := s.Addr().String()
// Connect to the server
conn, err := net.Dial("tcp", addr)
require_NoError(t, err)
defer conn.Close()
// Send PROXY protocol LOCAL header (health check)
header := buildProxyV2LocalHeader()
_, err = conn.Write(header)
require_NoError(t, err)
// Send CONNECT message
connectMsg := "CONNECT {\"verbose\":false,\"pedantic\":false,\"protocol\":1}\r\n"
_, err = conn.Write([]byte(connectMsg))
require_NoError(t, err)
// Read INFO and +OK
buf := make([]byte, 4096)
n, err := conn.Read(buf)
require_NoError(t, err)
response := string(buf[:n])
require_True(t, strings.Contains(response, "INFO"))
// Connection should work normally with LOCAL command
time.Sleep(100 * time.Millisecond)
// Verify at least one client is connected
s.mu.Lock()
numClients := len(s.clients)
s.mu.Unlock()
require_NotEqual(t, numClients, 0)
}
// ============================================================================
// PROXY Protocol v1 Tests
// ============================================================================
func TestClientProxyProtoV1ParseTCP4(t *testing.T) {
header := buildProxyV1Header(t, "TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222)
conn := newMockConn(header)
addr, _, err := readProxyProtoHeader(conn)
require_NoError(t, err)
require_NotNil(t, addr)
require_Equal(t, addr.srcIP.String(), "192.168.1.50")
require_Equal(t, addr.srcPort, 12345)
require_Equal(t, addr.dstIP.String(), "10.0.0.1")
require_Equal(t, addr.dstPort, 4222)
}
func TestClientProxyProtoV1ParseTCP6(t *testing.T) {
header := buildProxyV1Header(t, "TCP6", "2001:db8::1", "2001:db8::2", 54321, 4222)
conn := newMockConn(header)
addr, _, err := readProxyProtoHeader(conn)
require_NoError(t, err)
require_NotNil(t, addr)
require_Equal(t, addr.srcIP.String(), "2001:db8::1")
require_Equal(t, addr.srcPort, 54321)
require_Equal(t, addr.dstIP.String(), "2001:db8::2")
require_Equal(t, addr.dstPort, 4222)
}
func TestClientProxyProtoV1ParseTCP6IPv4Mapped(t *testing.T) {
// Dual-stack proxies can emit IPv4-mapped IPv6 addresses for TCP6
// (an IPv4 client connecting to an IPv6 socket). These are valid IPv6
// textual addresses and must be accepted, matching the v2 parser.
header := buildProxyV1Header(t, "TCP6", "::ffff:192.0.2.1", "::ffff:10.0.0.1", 1234, 4222)
conn := newMockConn(header)
addr, _, err := readProxyProtoHeader(conn)
require_NoError(t, err)
require_NotNil(t, addr)
require_Equal(t, addr.srcIP.String(), "192.0.2.1")
require_Equal(t, addr.srcPort, 1234)
require_Equal(t, addr.dstIP.String(), "10.0.0.1")
require_Equal(t, addr.dstPort, 4222)
}
func TestClientProxyProtoV1MismatchedDestProtocol(t *testing.T) {
// TCP4 with IPv6 destination address
header := buildProxyV1Header(t, "TCP4", "192.168.1.1", "2001:db8::2", 12345, 443)
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_Error(t, err, errProxyProtoInvalid)
// TCP6 with IPv4 destination address
header2 := buildProxyV1Header(t, "TCP6", "2001:db8::1", "10.0.0.1", 12345, 443)
conn2 := newMockConn(header2)
_, _, err = readProxyProtoHeader(conn2)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV1ParseUnknown(t *testing.T) {
header := buildProxyV1Header(t, "UNKNOWN", "", "", 0, 0)
conn := newMockConn(header)
addr, _, err := readProxyProtoHeader(conn)
require_NoError(t, err)
require_True(t, addr == nil)
}
func TestClientProxyProtoV1PreservesCoalescedClientBytes(t *testing.T) {
header := buildProxyV1Header(t, "TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222)
connect := []byte("CONNECT {\"verbose\":false,\"pedantic\":false,\"protocol\":1}\r\n")
conn := newMockConn(append(header, connect...))
addr, remaining, err := readProxyProtoHeader(conn)
require_NoError(t, err)
require_NotNil(t, addr)
require_Equal(t, string(remaining), string(connect))
rest, err := io.ReadAll(conn)
require_NoError(t, err)
require_Equal(t, len(rest), 0)
}
func TestClientProxyProtoV1InvalidFormat(t *testing.T) {
// Missing fields
header := []byte("PROXY TCP4 192.168.1.1\r\n")
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV1LineTooLong(t *testing.T) {
// Create a line longer than 107 bytes
longIP := strings.Repeat("1234567890", 12) // 120 chars
header := fmt.Appendf(nil, "PROXY TCP4 %s 10.0.0.1 12345 443\r\n", longIP)
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV1InvalidIP(t *testing.T) {
header := []byte("PROXY TCP4 not.an.ip.addr 10.0.0.1 12345 443\r\n")
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV1MismatchedProtocol(t *testing.T) {
// TCP4 with IPv6 address
header := buildProxyV1Header(t, "TCP4", "2001:db8::1", "2001:db8::2", 12345, 443)
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_Error(t, err, errProxyProtoInvalid)
// TCP6 with IPv4 address
header2 := buildProxyV1Header(t, "TCP6", "192.168.1.1", "10.0.0.1", 12345, 443)
conn2 := newMockConn(header2)
_, _, err = readProxyProtoHeader(conn2)
require_Error(t, err, errProxyProtoInvalid)
}
func TestClientProxyProtoV1InvalidPort(t *testing.T) {
header := []byte("PROXY TCP4 192.168.1.1 10.0.0.1 99999 443\r\n")
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_True(t, err != nil)
}
func TestClientProxyProtoV1EndToEnd(t *testing.T) {
// Start a test server with PROXY protocol enabled
opts := DefaultOptions()
opts.Port = -1 // Random port
opts.ProxyProtocol = true
s := RunServer(opts)
defer s.Shutdown()
// Get the server's listening port
addr := s.Addr().String()
// Connect to the server
conn, err := net.Dial("tcp", addr)
require_NoError(t, err)
defer conn.Close()
// Send the PROXY protocol header and first client commands in one write so
// the parser must preserve bytes read past the end of the v1 header.
clientIP, clientPort := "203.0.113.50", uint16(54321)
header := buildProxyV1Header(t, "TCP4", clientIP, "127.0.0.1", clientPort, 4222)
payload := append(header, []byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n")...)
_, err = conn.Write(payload)
require_NoError(t, err)
require_NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
cr := bufio.NewReader(conn)
line, err := cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "INFO "))
line, err = cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "+OK"))
expectPong(t, cr)
// Give server time to process
time.Sleep(100 * time.Millisecond)
// Check server's client list to verify the IP was extracted correctly
s.mu.Lock()
clients := s.clients
var foundClient *client
for _, c := range clients {
c.mu.Lock()
if c.host == clientIP && c.port == clientPort {
foundClient = c
}
c.mu.Unlock()
if foundClient != nil {
break
}
}
s.mu.Unlock()
require_NotNil(t, foundClient)
}
// ============================================================================
// Mixed Protocol Version Tests
// ============================================================================
func TestClientProxyProtoVersionDetection(t *testing.T) {
// Test v1 detection
v1Header := buildProxyV1Header(t, "TCP4", "192.168.1.1", "10.0.0.1", 12345, 443)
conn1 := newMockConn(v1Header)
addr1, _, err := readProxyProtoHeader(conn1)
require_NoError(t, err)
require_NotNil(t, addr1)
require_Equal(t, addr1.srcIP.String(), "192.168.1.1")
// Test v2 detection
v2Header := buildProxyV2Header(t, "192.168.1.2", "10.0.0.1", 54321, 443, proxyProtoFamilyInet)
conn2 := newMockConn(v2Header)
addr2, _, err := readProxyProtoHeader(conn2)
require_NoError(t, err)
require_NotNil(t, addr2)
require_Equal(t, addr2.srcIP.String(), "192.168.1.2")
}
func TestClientProxyProtoUnrecognizedVersion(t *testing.T) {
// Invalid header that doesn't match v1 or v2
header := []byte("HELLO WORLD\r\n")
conn := newMockConn(header)
_, _, err := readProxyProtoHeader(conn)
require_Error(t, err, errProxyProtoUnrecognized)
}
// ============================================================================
// PROXY Protocol + TLS Tests (regression for issue #7571)
// ============================================================================
// runProxyProtoTLSServer starts a server with proxy_protocol and a required
// TLS listener (no allow_non_tls, no tls_first).
func runProxyProtoTLSServer(t *testing.T) *Server {
t.Helper()
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-cert.pem",
KeyFile: "../test/configs/certs/server-key.pem",
CaFile: "../test/configs/certs/ca.pem",
}
tlsConfig, err := GenTLSConfig(tc)
require_NoError(t, err)
opts := DefaultOptions()
opts.Port = -1
opts.ProxyProtocol = true
opts.TLSConfig = tlsConfig
opts.TLSTimeout = 2
return RunServer(opts)
}
// proxyProtoTLSClientConfig builds a tls.Config trusting the test CA. The
// server cert is issued for "localhost" / 127.0.0.1, so ServerName must match.
func proxyProtoTLSClientConfig(t *testing.T) *tls.Config {
t.Helper()
caPEM, err := os.ReadFile("../test/configs/certs/ca.pem")
require_NoError(t, err)
pool := x509.NewCertPool()
require_True(t, pool.AppendCertsFromPEM(caPEM))
return &tls.Config{
RootCAs: pool,
ServerName: "localhost",
}
}
// findProxyProtoClient finds a connected client whose host/port matches what
// was advertised in the PROXY header.
func findProxyProtoClient(t *testing.T, s *Server, clientIP string, clientPort uint16) *client {
t.Helper()
s.mu.Lock()
clients := make([]*client, 0, len(s.clients))
for _, c := range s.clients {
clients = append(clients, c)
}
s.mu.Unlock()
for _, c := range clients {
c.mu.Lock()
host, port := c.host, c.port
c.mu.Unlock()
if host == clientIP && port == clientPort {
return c
}
}
return nil
}
// connectProxyProtoTLS mirrors what an L4 proxy and a TLS-enabled NATS client
// do over a single TCP connection: prepend the PROXY header, read the initial
// plaintext INFO, then upgrade to TLS. Returns the buffered reader and the
// TLS-wrapped connection for subsequent protocol writes.
func connectProxyProtoTLS(t *testing.T, addr string, header []byte) (*bufio.Reader, *tls.Conn) {
t.Helper()
rawConn, err := net.Dial("tcp", addr)
require_NoError(t, err)
require_NoError(t, rawConn.SetDeadline(time.Now().Add(5*time.Second)))
_, err = rawConn.Write(header)
require_NoError(t, err)
// Server emits INFO over plaintext before the TLS upgrade. Read it now
// so the bytes don't end up as input to the TLS handshake.
plainReader := bufio.NewReader(rawConn)
infoLine, err := plainReader.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(infoLine, "INFO "))
require_Equal(t, plainReader.Buffered(), 0)
tlsConn := tls.Client(rawConn, proxyProtoTLSClientConfig(t))
require_NoError(t, tlsConn.Handshake())
return bufio.NewReader(tlsConn), tlsConn
}
// TestClientProxyProtoV1WithRequiredTLS verifies that with proxy_protocol and
// required TLS, a client may send the PROXY v1 header followed by the TLS
// upgrade on a single TCP connection. Regression for issue #7571.
func TestClientProxyProtoV1WithRequiredTLS(t *testing.T) {
s := runProxyProtoTLSServer(t)
defer s.Shutdown()
clientIP, clientPort := "203.0.113.50", uint16(54321)
header := buildProxyV1Header(t, "TCP4", clientIP, "127.0.0.1", clientPort, 4222)
cr, tlsConn := connectProxyProtoTLS(t, s.Addr().String(), header)
defer tlsConn.Close()
_, err := tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
require_NoError(t, err)
line, err := cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "+OK"))
expectPong(t, cr)
checkFor(t, time.Second, 10*time.Millisecond, func() error {
if findProxyProtoClient(t, s, clientIP, clientPort) == nil {
return fmt.Errorf("no client with PROXY-supplied host/port yet")
}
return nil
})
}
// TestClientProxyProtoV2WithRequiredTLS is the v2 counterpart to
// TestClientProxyProtoV1WithRequiredTLS.
func TestClientProxyProtoV2WithRequiredTLS(t *testing.T) {
s := runProxyProtoTLSServer(t)
defer s.Shutdown()
clientIP, clientPort := "203.0.113.51", uint16(54322)
header := buildProxyV2Header(t, clientIP, "127.0.0.1", clientPort, 4222, proxyProtoFamilyInet)
cr, tlsConn := connectProxyProtoTLS(t, s.Addr().String(), header)
defer tlsConn.Close()
_, err := tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
require_NoError(t, err)
line, err := cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "+OK"))
expectPong(t, cr)
checkFor(t, time.Second, 10*time.Millisecond, func() error {
if findProxyProtoClient(t, s, clientIP, clientPort) == nil {
return fmt.Errorf("no client with PROXY-supplied host/port yet")
}
return nil
})
}
func TestClientProxyProtoNonProxiedTLSFirstFallback(t *testing.T) {
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-cert.pem",
KeyFile: "../test/configs/certs/server-key.pem",
CaFile: "../test/configs/certs/ca.pem",
}
tlsConfig, err := GenTLSConfig(tc)
require_NoError(t, err)
opts := DefaultOptions()
opts.Port = -1
opts.ProxyProtocol = true
opts.TLSConfig = tlsConfig
opts.TLSTimeout = 2
opts.TLSHandshakeFirst = true
opts.TLSHandshakeFirstFallback = 250 * time.Millisecond
s := RunServer(opts)
defer s.Shutdown()
rawConn, err := net.Dial("tcp", s.Addr().String())
require_NoError(t, err)
defer rawConn.Close()
require_NoError(t, rawConn.SetDeadline(time.Now().Add(5*time.Second)))
// Direct TLS-first client: start the handshake right away (within the
// fallback delay), no PROXY header.
tlsConn := tls.Client(rawConn, proxyProtoTLSClientConfig(t))
require_NoError(t, tlsConn.Handshake())
cr := bufio.NewReader(tlsConn)
infoLine, err := cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(infoLine, "INFO "))
_, err = tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
require_NoError(t, err)
line, err := cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "+OK"))
expectPong(t, cr)
}
// runProxyProtoAllowNonTLSServer starts a server with proxy_protocol and a
// TLS listener that also allows non-TLS clients (allow_non_tls).
func runProxyProtoAllowNonTLSServer(t *testing.T) *Server {
t.Helper()
tc := &TLSConfigOpts{
CertFile: "../test/configs/certs/server-cert.pem",
KeyFile: "../test/configs/certs/server-key.pem",
CaFile: "../test/configs/certs/ca.pem",
}
tlsConfig, err := GenTLSConfig(tc)
require_NoError(t, err)
opts := DefaultOptions()
opts.Port = -1
opts.ProxyProtocol = true
opts.TLSConfig = tlsConfig
opts.TLSTimeout = 2
opts.AllowNonTLS = true
return RunServer(opts)
}
// testClientProxyProtoAllowNonTLS verifies that with proxy_protocol and
// allow_non_tls, the TLS-vs-plaintext detection is performed on the first
// byte of the client's actual traffic, not on the first byte of the PROXY
// header. Both a TLS client and a plaintext client behind the proxy must
// be able to connect on the same listener.
func testClientProxyProtoAllowNonTLS(t *testing.T, header []byte, clientIP string, clientPort uint16) {
t.Helper()
s := runProxyProtoAllowNonTLSServer(t)
defer s.Shutdown()
// TLS client behind the proxy: the sniff must detect the TLS
// ClientHello that follows the PROXY header.
cr, tlsConn := connectProxyProtoTLS(t, s.Addr().String(), header)
defer tlsConn.Close()
_, err := tlsConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
require_NoError(t, err)
line, err := cr.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "+OK"))
expectPong(t, cr)
checkFor(t, time.Second, 10*time.Millisecond, func() error {
if findProxyProtoClient(t, s, clientIP, clientPort) == nil {
return fmt.Errorf("no client with PROXY-supplied host/port yet")
}
return nil
})
// Plaintext client behind the proxy must still be allowed.
rawConn, err := net.Dial("tcp", s.Addr().String())
require_NoError(t, err)
defer rawConn.Close()
require_NoError(t, rawConn.SetDeadline(time.Now().Add(5*time.Second)))
_, err = rawConn.Write(header)
require_NoError(t, err)
plainReader := bufio.NewReader(rawConn)
infoLine, err := plainReader.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(infoLine, "INFO "))
_, err = rawConn.Write([]byte("CONNECT {\"verbose\":true,\"pedantic\":false,\"protocol\":1}\r\nPING\r\n"))
require_NoError(t, err)
line, err = plainReader.ReadString('\n')
require_NoError(t, err)
require_True(t, strings.HasPrefix(line, "+OK"))
expectPong(t, plainReader)
}
// TestClientProxyProtoV1WithAllowNonTLS verifies that a proxied TLS client is
// detected as TLS (and a proxied plaintext client as non-TLS) when the
// listener has both tls and allow_non_tls configured, using a PROXY v1 header.
func TestClientProxyProtoV1WithAllowNonTLS(t *testing.T) {
clientIP, clientPort := "203.0.113.60", uint16(54331)
header := buildProxyV1Header(t, "TCP4", clientIP, "127.0.0.1", clientPort, 4222)
testClientProxyProtoAllowNonTLS(t, header, clientIP, clientPort)
}
// TestClientProxyProtoV2WithAllowNonTLS is the v2 counterpart to
// TestClientProxyProtoV1WithAllowNonTLS.
func TestClientProxyProtoV2WithAllowNonTLS(t *testing.T) {
clientIP, clientPort := "203.0.113.61", uint16(54332)
header := buildProxyV2Header(t, clientIP, "127.0.0.1", clientPort, 4222, proxyProtoFamilyInet)
testClientProxyProtoAllowNonTLS(t, header, clientIP, clientPort)
}

270
server/closed_conns_test.go Normal file
View File

@@ -0,0 +1,270 @@
// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go"
)
func checkClosedConns(t *testing.T, s *Server, num int, wait time.Duration) {
t.Helper()
checkFor(t, wait, 5*time.Millisecond, func() error {
if nc := s.closed.len(); nc != num {
return fmt.Errorf("Closed conns expected to be %v, got %v", num, nc)
}
return nil
})
}
func checkTotalClosedConns(t *testing.T, s *Server, num uint64, wait time.Duration) {
t.Helper()
checkFor(t, wait, 5*time.Millisecond, func() error {
if nc := s.closed.totalConns(); nc != num {
return fmt.Errorf("Total closed conns expected to be %v, got %v", num, nc)
}
return nil
})
}
func TestClosedConnsAccounting(t *testing.T) {
opts := DefaultOptions()
opts.MaxClosedClients = 10
opts.NoSystemAccount = true
s := RunServer(opts)
defer s.Shutdown()
wait := time.Second
nc, err := nats.Connect(fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port))
if err != nil {
t.Fatalf("Error on connect: %v", err)
}
id, _ := nc.GetClientID()
nc.Close()
checkClosedConns(t, s, 1, wait)
conns := s.closed.closedClients()
if lc := len(conns); lc != 1 {
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
}
if conns[0].Cid != id {
t.Fatalf("Expected CID to be %d, got %d\n", id, conns[0].Cid)
}
// Now create 21 more
for i := 0; i < 21; i++ {
nc, err = nats.Connect(fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port))
if err != nil {
t.Fatalf("Error on connect: %v", err)
}
nc.Close()
checkTotalClosedConns(t, s, uint64(i+2), wait)
}
checkClosedConns(t, s, opts.MaxClosedClients, wait)
checkTotalClosedConns(t, s, 22, wait)
conns = s.closed.closedClients()
if lc := len(conns); lc != opts.MaxClosedClients {
t.Fatalf("len(conns) expected to be %d, got %d\n",
opts.MaxClosedClients, lc)
}
// Set it to the start after overflow.
cid := uint64(22 - opts.MaxClosedClients)
for _, ci := range conns {
cid++
if ci.Cid != cid {
t.Fatalf("Expected cid of %d, got %d\n", cid, ci.Cid)
}
}
}
func TestClosedConnsSubsAccounting(t *testing.T) {
opts := DefaultOptions()
s := RunServer(opts)
defer s.Shutdown()
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
nc, err := nats.Connect(url)
if err != nil {
t.Fatalf("Error on subscribe: %v", err)
}
defer nc.Close()
// Now create some subscriptions
numSubs := 10
for i := 0; i < numSubs; i++ {
subj := fmt.Sprintf("foo.%d", i)
nc.Subscribe(subj, func(m *nats.Msg) {})
}
nc.Flush()
nc.Close()
checkClosedConns(t, s, 1, 20*time.Millisecond)
conns := s.closed.closedClients()
if lc := len(conns); lc != 1 {
t.Fatalf("len(conns) expected to be 1, got %d\n", lc)
}
ci := conns[0]
if len(ci.subs) != numSubs {
t.Fatalf("Expected number of Subs to be %d, got %d\n", numSubs, len(ci.subs))
}
}
func checkReason(t *testing.T, reason string, expected ClosedState) {
if !strings.Contains(reason, expected.String()) {
t.Fatalf("Expected closed connection with `%s` state, got `%s`\n",
expected, reason)
}
}
func TestClosedAuthorizationTimeout(t *testing.T) {
serverOptions := DefaultOptions()
serverOptions.Authorization = "my_token"
serverOptions.AuthTimeout = 0.4
s := RunServer(serverOptions)
defer s.Shutdown()
conn, err := net.Dial("tcp", net.JoinHostPort(serverOptions.Host, fmt.Sprintf("%d", serverOptions.Port)))
if err != nil {
t.Fatalf("Error dialing server: %v\n", err)
}
defer conn.Close()
checkClosedConns(t, s, 1, 2*time.Second)
conns := s.closed.closedClients()
if lc := len(conns); lc != 1 {
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
}
checkReason(t, conns[0].Reason, AuthenticationTimeout)
}
func TestClosedAuthorizationViolation(t *testing.T) {
serverOptions := DefaultOptions()
serverOptions.Authorization = "my_token"
s := RunServer(serverOptions)
defer s.Shutdown()
opts := s.getOpts()
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
nc, err := nats.Connect(url)
if err == nil {
nc.Close()
t.Fatal("Expected failure for connection")
}
checkClosedConns(t, s, 1, 2*time.Second)
conns := s.closed.closedClients()
if lc := len(conns); lc != 1 {
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
}
checkReason(t, conns[0].Reason, AuthenticationViolation)
}
func TestClosedUPAuthorizationViolation(t *testing.T) {
serverOptions := DefaultOptions()
serverOptions.Username = "my_user"
serverOptions.Password = "my_secret"
s := RunServer(serverOptions)
defer s.Shutdown()
opts := s.getOpts()
url := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port)
nc, err := nats.Connect(url)
if err == nil {
nc.Close()
t.Fatal("Expected failure for connection")
}
url2 := fmt.Sprintf("nats://my_user:wrong_pass@%s:%d", opts.Host, opts.Port)
nc, err = nats.Connect(url2)
if err == nil {
nc.Close()
t.Fatal("Expected failure for connection")
}
checkClosedConns(t, s, 2, 2*time.Second)
conns := s.closed.closedClients()
if lc := len(conns); lc != 2 {
t.Fatalf("len(conns) expected to be %d, got %d\n", 2, lc)
}
checkReason(t, conns[0].Reason, AuthenticationViolation)
checkReason(t, conns[1].Reason, AuthenticationViolation)
}
func TestClosedMaxPayload(t *testing.T) {
serverOptions := DefaultOptions()
serverOptions.MaxPayload = 100
s := RunServer(serverOptions)
defer s.Shutdown()
opts := s.getOpts()
endpoint := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port))
conn, err := net.DialTimeout("tcp", endpoint, time.Second)
if err != nil {
t.Fatalf("Could not make a raw connection to the server: %v", err)
}
defer conn.Close()
// This should trigger it.
pub := "PUB foo.bar 1024\r\n"
conn.Write([]byte(pub))
checkClosedConns(t, s, 1, 2*time.Second)
conns := s.closed.closedClients()
if lc := len(conns); lc != 1 {
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
}
checkReason(t, conns[0].Reason, MaxPayloadExceeded)
}
func TestClosedTLSHandshake(t *testing.T) {
opts, err := ProcessConfigFile("./configs/tls.conf")
if err != nil {
t.Fatalf("Error processing config file: %v", err)
}
opts.TLSVerify = true
opts.NoLog = true
opts.NoSigs = true
s := RunServer(opts)
defer s.Shutdown()
nc, err := nats.Connect(fmt.Sprintf("tls://%s:%d", opts.Host, opts.Port))
if err == nil {
nc.Close()
t.Fatal("Expected failure for connection")
}
checkClosedConns(t, s, 1, 2*time.Second)
conns := s.closed.closedClients()
if lc := len(conns); lc != 1 {
t.Fatalf("len(conns) expected to be %d, got %d\n", 1, lc)
}
checkReason(t, conns[0].Reason, TLSHandshakeError)
}

2636
server/config_check_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDnjCCAoagAwIBAgIJAM/HacKKaH7zMA0GCSqGSIb3DQEBCwUAMIGDMQswCQYD
VQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMSQwIgYD
VQQKDBtTeW5hZGlhIENvbW11bmljYXRpb25zIEluYy4xKzApBgNVBAMMIm5hdHMu
aW8vZW1haWxBZGRyZXNzPWRlcmVrQG5hdHMuaW8wHhcNMTkxMDE3MTIyODIxWhcN
MjkxMDE0MTIyODIxWjCBgzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYD
VQQHDAtMb3MgQW5nZWxlczEkMCIGA1UECgwbU3luYWRpYSBDb21tdW5pY2F0aW9u
cyBJbmMuMSswKQYDVQQDDCJuYXRzLmlvL2VtYWlsQWRkcmVzcz1kZXJla0BuYXRz
LmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBIAVUr4gCEBRVbA
HSweCLExOrMZmII4AdvgIqT+svjBkJd+vdkbd5b/SC1HQx1E14kiRJ/JrZIZoMqi
+7pK3kFM63Fkhkg8rWOxn0tQznSymKTpha5NdDWxnB0dXlXFCQG1e/cuDalR7UhF
LPHiuK42gAvhivBcymDPV0hTYt4rHb71SQ1DwfCYzcLkDvDFA/W7kronaEhRyWn6
uvZvHkdvScoubdzoW/kNBH4JYZw5svLzGz3z20rUGeLttF4ge5SCAz9unZk96HUO
EFmUDvFmxdnTXrYINjraNvgu7fPFmVzupWrWPA/7U+cxOvm3qBMdqxFxNr7bq+Md
UPKi3QIDAQABoxMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB
AQCCgFKP/bMi5cvasJOzXKLpwOneW+oItL2t/5NxPumIBMDo5NnShzsFLfGmujYw
fjcMharynkkbz/oeDAm8h1mySAJrnqtabiWgaW8zbNJfJOrAq4Jvs9COMAzjJclL
+h9GWtvVylDnsNtd18n1gA5OYv0A6YjuSrWINL8Sp5QvTF/5tT8jFrDOIjZl7m50
lX4R70N9GLt2jIlKro+qdsi6qUZccuJmoQxUpG1iQcRNFHtWfDPr5KFEXaO6IoYt
D1kYWmo/A3WQm7nbXeZw/zaSGSS0t6/hKZwm+gPCL6TEdDrjhjxpCZQTwaMd6jj2
bvT2OA0ZpzUWyxnaX6u+cDfM
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8EgBVSviAIQFF
VsAdLB4IsTE6sxmYgjgB2+AipP6y+MGQl3692Rt3lv9ILUdDHUTXiSJEn8mtkhmg
yqL7ukreQUzrcWSGSDytY7GfS1DOdLKYpOmFrk10NbGcHR1eVcUJAbV79y4NqVHt
SEUs8eK4rjaAC+GK8FzKYM9XSFNi3isdvvVJDUPB8JjNwuQO8MUD9buSuidoSFHJ
afq69m8eR29Jyi5t3Ohb+Q0EfglhnDmy8vMbPfPbStQZ4u20XiB7lIIDP26dmT3o
dQ4QWZQO8WbF2dNetgg2Oto2+C7t88WZXO6latY8D/tT5zE6+beoEx2rEXE2vtur
4x1Q8qLdAgMBAAECggEBAI0zJrTi2Le6D+4zN4Gvgc0c280qcmkiO1KJ9jmMjv1n
7kvUsf9vZUPgRkG2XO5ypyD7gJLtNMnwCvXBraQ5NcSwWkPampKG4ad8Vfs23LBk
xUH9bqZDOzuopHSFF2ugEZK1icBM0HLJUQ1JWUZpRMNLaPex8+AQnloDXSg20QRE
Z53CDYZVEVaLAYIkfgZ6QgBSbJ0ob/a+ZJkChWs3f6cpNHige4tR2RQ0aJFwvlwu
eC8G3RDNzUMhPf4TSf/XUlHXngyHKa7MS3feEtuKIQ4gb8hjESDcZznJUGG9qyMT
7445mapc60t8z46o9iVeVJQmQxmjFUDkLcZ9ckcWfzECgYEA4OiolNUiTnXbvP0H
3rxAnhx9GlyHiRaIZvgsAHH/6aMbJIXnH89cAR38PL79OaQpWZqK8Z1vrCHC8q7s
Wb1RehgnwPxtTl4RTZ0wj3Ri5BIq32nzgla0w/5RAmAw9HrYXjp4TR4Quc+bHLcr
ZM2gA8nZ4PG73rYvd/Mu3lCQiS8CgYEA1hGoaduGG7eHTMCct/kCupFfYXgEuUDh
V/nO1SYJbIkMuSONNaQy1oUc5o0BB/JTcslZWqtuGa9snKPVd3/rJz75YGt1QR6u
MukCYjbWGAWXfDh8G8aGSr3k1k3JmKNE7fI7L5DuvKCGoGjzzC+kclyoij8jwXk0
zghaYPvM+bMCgYEA3/fwgR3p5vZJF19mqfEP7CP0lP7V3bd5qAi1UNA1h4VsrydF
LRFCzr38hMWwx+jpYJicitU78s9AIon9RbRY4dwSIoV9mE/mrUK+q+y72eEZnpgU
7ZPIuXCVXWdK+PsoYlWZnTo2b8ME9UiWxvBZy8wD05UGgFcu2CVsY+kYtfMCgYAs
Vn/xXPyL5Rlq9kH/gN3l0pJU18zyqdOCq0UBtN0i08gE2K44vAejkvKHdhEOmkxa
bAXL19H4E/OFBhICrEYCXPK928PvdvFRrh1GRmFVnGLh4bki752FAYvSL05gBQET
36YOlhA8lWsM8m/8jKmc3kAyUh2PxxD+05AUolK0LQKBgHqPgMj5rfiR7MoVIaXr
K8h7g+HejppmCxzENc98hvIQIO2aWeC4Hi/EMe+XYSN/Gl6d5+aw80JYRgIv6dFV
hDEEt8lEI4sFhKD0oG1dQo+BdDNXOLbpHq+RQRPHa8J/pmSjI5Joqb2mCaAcm/KJ
nYE20ox06YMSb+ZrjP2p4qw3
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDc8YaxliKNsa8M
idwQsDXS0MI4z+ZP1UMW9U/Vt2qTienYs3L6KqezRfwEH2AoE7XyvQdzePV3aN1g
A0L3mAKiS2FN3DhxR6Y8D/JNb5YA6Pl0flaMKnWktma/SMOf7o7eS7Zt6ZmIvtBg
p1ZtRSZSH64KSqC467eGByHnXm25Z7Uf/2rO8K1jsmRwgDEJoJL5Z4va4pypX5Kq
7OxWk8Za50Kuk6DtdRfIcnUjoDadsYzGHzF7kp1/31elQ2aUPp9e6X3aWu++gJv5
brBMsWeQ+ETmjCMPqxoXDM2AJqwP73irELU7POUR/q3gooqy16BvTp7TaLvAtKdP
SK7kl1QVAgMBAAECggEAKVT1gZs4c5Fd0daoWGbeasWqz730s9KNbmoNuUlKorcW
C/fl9m6sWJkrAApujpDBZNt/3VYvdVskrqVtjaveMkYvucUMugRRUMMa7OmHAjyT
DfvkbOw0fc+xgO83yV9zUpqPUhh1JGOtz6b+QArDlHFguYQkdPODrYsACKxdkmU/
A+ZJkC2Sdn0L7ARmROxvoelxV3HNbC+p8lqrIVarpni8oeDF2JxQfyJETl3Df099
/KA/IQvXytEQBJpguh0j7sK81cI40TKO3kNWsCe46S2nkCuEeEums7BqAspmdNMS
gBW0rpFA9Ep5AyT4+A2XhWFQAwf/PI26JUVDhbClAQKBgQD/XlONHf5tNiOEtW88
Whd9ha7oLgoBhr4gFOcPM4MUStRDftRXY1ty0IGseV0Rj1xQRUp31Pl6Kep/udy1
r/6ML/IQNaht1VJOf2+2v7oZFoLRGLvR3qSRFNVtfLWp1Dg5OPgyi00OpFnxz/6o
iuWlh1EWwSm9vPl91HcWPPcwBQKBgQDdfWfDfy8q9jExj8tVNh5++haeemprkZxk
QuduIkahlbQ3V+SLaSWQrwzcB4PWL+45fnIWwK3Vmm2POBv+OWfKGjJniSd4Zrrf
H+lSg4+hpivvoaYf1TsCTHy/af/5pYIkEUmbPM0EqwubyoW6oMAgGIAMyH6XBLaY
a4vWZo6g0QKBgFXDXhgUrLAM8JzPOk5wi1cSoI1FeQLON+gaXQdT63/TKbqJS9MV
gU7sC8Da+ZC+LuiefMYF9ss6bJD84Mz8EGcQayFag/hvHjdSwTgE6AEo+EI1Jk7z
kR1Qe+VLbs9cgI1nPqPq+LQkKjj1+7aq/zk6WtdjhBs+7iN+SYhkbTfpAoGAeCbv
gz4beFLVnO1EgJU7Nea1HoOJ95CmJj2lDjnJ1x4/BUbI1FfV6QcNEs+A1VBrCwVt
HqqnopiDlo35oY/CngBYF5JvtwEDnsbQ69IyuJ5Md1JZrCsgN78GbVAzbFo8nxRB
udTh4wZm5byXjwZlMwQXctfQ1FvaMhUlZsl71gECgYA82osmLHxZZOiQrYJ7z302
CmPohwcTtUASqeA0kQOX/OdFJ1IxWIFpzq2ffMipLTfzk7OeFS2hqvNo/NE8NHlR
/79IycYwlO99T3e2yQhYKW5ZMT3Q8YtyLLPtBzPPtO/Tc8yKhjX8wc9w9sxq9kqg
i5Z+6xfO+l/NBT/JHF4yEw==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDnjCCAoagAwIBAgIJAOXpOWk41Pd7MA0GCSqGSIb3DQEBCwUAMIGDMQswCQYD
VQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMSQwIgYD
VQQKDBtTeW5hZGlhIENvbW11bmljYXRpb25zIEluYy4xKzApBgNVBAMMIm5hdHMu
aW8vZW1haWxBZGRyZXNzPWRlcmVrQG5hdHMuaW8wHhcNMTkxMDE3MTIzMjQ5WhcN
MjkxMDE0MTIzMjQ5WjCBgzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYD
VQQHDAtMb3MgQW5nZWxlczEkMCIGA1UECgwbU3luYWRpYSBDb21tdW5pY2F0aW9u
cyBJbmMuMSswKQYDVQQDDCJuYXRzLmlvL2VtYWlsQWRkcmVzcz1kZXJla0BuYXRz
LmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3PGGsZYijbGvDInc
ELA10tDCOM/mT9VDFvVP1bdqk4np2LNy+iqns0X8BB9gKBO18r0Hc3j1d2jdYANC
95gCokthTdw4cUemPA/yTW+WAOj5dH5WjCp1pLZmv0jDn+6O3ku2bemZiL7QYKdW
bUUmUh+uCkqguOu3hgch515tuWe1H/9qzvCtY7JkcIAxCaCS+WeL2uKcqV+Squzs
VpPGWudCrpOg7XUXyHJ1I6A2nbGMxh8xe5Kdf99XpUNmlD6fXul92lrvvoCb+W6w
TLFnkPhE5owjD6saFwzNgCasD+94qxC1OzzlEf6t4KKKstegb06e02i7wLSnT0iu
5JdUFQIDAQABoxMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IB
AQDIMNgMqgGdXwZTIDxz4iq76iHdSHxZK4YwJr/4xRSq1uEddxmUfAQzo+gBboA5
c0XxYxc0xViuueThnqdLhHmEyCs8uGFRLaQAI5Bq1PdMQP12m5fCWRAyKrCxdtli
zm8ByDq7mWpkfMTd/rJGR4wCR9qI9Y5Bp6p4FBKZ3pzEanFXMV9IHhkm1BGh9tbe
l6GQyBptEpfTiRwNCC/ympeiL3G8hfDCPkcLed5sQ+OhPe5iWMVPncZh/qehnUJK
B5CIXcagcROFutsDYPCurKcfQOsfqulu0q95h7FQUOsIeU7jIlcLxIii19qjlTZh
sFjsul5G7qqMEgIUsx3U985v
-----END CERTIFICATE-----

View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID2zCCAsOgAwIBAgIUZj0PngA93uUSShcRndTQju/J88YwDQYJKoZIhvcNAQEL
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
WhgPMjEyMzA3MjExNTI3NTdaMHwxEzARBgNVBAMMCm5hdHMuaW8uQ0ExIjAgBgNV
BAsMGVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWEx
EDAOBgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNB
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2cCyJL+DExUyZto2eFLm
MBRSkQLxM9pOWB9O8TecHlPcc/SPGq/x9lpguJ/IiaUj+VffVWy236KW2JL5Xj83
PZwhXi1yZzxlIBsKAgAUeNfWuTAc0K0Qm9pR5Wjv5eNcT0mw6JX0SPgUQAl9BSwU
WvtMOTxOt0hBjHmZaEamp7nLmwogpvgPsrubD6U4O/vUQm3JTsbp2rFQxXPpkG19
69PGsT37r0/w9Zv0xNAcB/zCWdNBXCTA2ACV2IpJedWm8Jrjcn3Kp4Fv3TKTsCZl
eWtfxCdljndk88+NFK7cEw7b9Bs5R5Zhu20C+Ne8vmMWhYbVBFYws5/jGzPBkVTD
7wIDAQABo1MwUTAdBgNVHQ4EFgQUEqfeAemfeIp4MM4C7H1bJS+mra4wHwYDVR0j
BBgwFoAUEqfeAemfeIp4MM4C7H1bJS+mra4wDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAQEAiamiPxOlZ0pwOJvv0ylDreHVk2kast67YlhAOcZoSMvi
e2jbKL98U3+ZznGj21AKqEaOkO7UmKoJ/3QlrjgElXzcMUrUrJ1WNowlbXPlAhyL
KhNthLKUr72Tv6wv5GZdAR6DaAwq3iYTbpnLq4oCnFHiXgDWgWyJDLsTGulWve/K
GGM2JMcnacNgNC18uki440Wcfp0vGj9HhO6I/u63oGewZnIK87GQMQCt3JLFyiUc
hrn9nWoixFWcJfCjBcMlwZXMIAlDdelU1/hWtSknKCs57GvZuACcicAYiYIkWCkd
p1pF4G0Ic6irAnLTqhdGwL4+5pjNd1Ih0Gezn9hJLg==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZwLIkv4MTFTJm
2jZ4UuYwFFKRAvEz2k5YH07xN5weU9xz9I8ar/H2WmC4n8iJpSP5V99VbLbfopbY
kvlePzc9nCFeLXJnPGUgGwoCABR419a5MBzQrRCb2lHlaO/l41xPSbDolfRI+BRA
CX0FLBRa+0w5PE63SEGMeZloRqanucubCiCm+A+yu5sPpTg7+9RCbclOxunasVDF
c+mQbX3r08axPfuvT/D1m/TE0BwH/MJZ00FcJMDYAJXYikl51abwmuNyfcqngW/d
MpOwJmV5a1/EJ2WOd2Tzz40UrtwTDtv0GzlHlmG7bQL417y+YxaFhtUEVjCzn+Mb
M8GRVMPvAgMBAAECggEASvFWfm5JMtqjP6HPiGbjoV2FMzJjiFiUiSCxXzSn7wom
v+PGEsXGTWa6jiAz+SeUc38KNtDVOa+wIfani4fPP82J8GtMyfoPxdZ4gcq8QQDr
/k1wRWOi6Tjg4cdVdXXkMcencs0VR73V3lpFpG+Qy+VcTQCUCOF96dZ59VkHh4a4
CHX6PegOWwr0TSaCUacwhua+rPmCar/btAYv7Wp7+c+Zf7Rn2WYTV7ol4sYXR4ZQ
Sy/ROijeFTMkYNpaW/KOO2/pn3OJA8ycYH8UWZpsenPfIajC0Eka7BoEQpw6M8HR
wRWrKwssBEs0psFiq9s8J+6resPgXfU/9pf+mTkTqQKBgQD8kktUqN+vYc6t22DE
tSkg8y8OsGh9VTYfp7x5hu9qEC9t4mAKjqA/rRLiTXze/wInntreRTjjMb/NlqMy
PvI0Z+dM1UuqcF1axgKrIYsgnLJWuunOhaj5K3LhiNcHznlCtN9601dbccwLlQhL
5jdjnOuJ0i+Nh9v5oiu37SfldwKBgQDctWdbF4hJrBPnS6CcojuQj6ha10wYYe46
ZVcxKe5hFBs1q975YCHnEntyCDvXGfOTRgbKeZbwNhMvAc7Pp6eGMR/9SpiRwTt4
567hUz56WXVmp4gSvxoNuYRlWiMI8rZkyKJ8KFipvHgRa8nuamh0QBB4ShEJiVk8
fhaUiZeTSQKBgF0hAD/OKPR9Jv06J9tARVMN+Cr9ZvnXwqY3biqNU5gTMbndv7YE
0xfHlG/3THTZKI09aMyOT6SOQn/m7HPpe9tQ+Jt/BnBpEDMZUgCR1MAIp0WNlAp/
hEej+q8oiskpG9M56DFc3hgsxKT8pdt+nqvPP5ZI9xnDn5vTbTVbb9uPAoGAFRvU
csXhZwpqLOjyx4hMohrbQzTsNjjHjBY9LJqSDf7aS1vQy5ECLRN7cwCOmJgGz8MW
yy6t3POPCiPmH74tK4xvPs5voSEWCw49j5dillkP/W1wejqEx2NC4l6okyaDg0gd
IjrJoBJCeYgRnBfZPaUS7i5HSt40BrEYf8RZFuECgYAjSnvY8nYFRudsylYOu3TL
AcGbAdpDfL2H4G9z7qEQC6t1LqqGNcfMan/qS4J/n5GCOedVWHcfCraROOMil52v
3ZDXyyjGEO08XgKnoa2ZL/z2a6s077+hAAnbcywyi2Qz8Yfi6GnwYfU1u8SN0APd
T71HPNsWkU4zkxmP4S0Olg==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDHTCCAgWgAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsYwDQYJKoZIhvcNAQEL
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMCow
BQYDK2VwAyEAyyc9y9iZgWWSsPRahbeGxF6XN3VOFPZBvD/HQps6jr6jgZEwgY4w
CwYDVR0PBAQDAgQwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCoGA1UdEQQjMCGCDnJl
dWJlbi5uYXRzLmlvgg9yZXViZW4ubmF0cy5jb20wHQYDVR0OBBYEFBwkwMU8xuQO
FN1Ck5o2qQ4Dz87ZMB8GA1UdIwQYMBaAFBKn3gHpn3iKeDDOAux9WyUvpq2uMA0G
CSqGSIb3DQEBCwUAA4IBAQALjCynuxEobk1MYQAFhkrfAD29H6yRpOcKigHCZjTJ
Dnpupip1xvaFPPvhi4nxtuWcXgKpWEfd1jOPaiNV6lrefahitZpzcflD7wNOxqvx
Hau2U3lFnjnGaC0ppp66x26cQznp6YcTdxrJ1QF4vkOejxqNvaTzmiwzSPIIYm7+
iKVWT+Z86WKof3vAdsX/f148YH1YSPk0ykiBzlbLScbyWebbaydrAIpU01IkSvMo
qDYu+Fba0tpONLe1BUklc608riwQjw9HiJJ2zJIAOBAUev5+48RP91/K111Ix1bl
fGPT8/1TJbyGG2jeJwyLoSIu72aDnnIBfqGkVunRTmeg
-----END CERTIFICATE-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDkzCCAnugAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsMwDQYJKoZIhvcNAQEL
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMIGf
MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyHHaVHinB3jBsicR4hp7uopz0u3+O
kUicIUSQXDcWiPzdvE+7YZ/s4+Ud4aw4g9q0wHzkZSaMg8nil4tCKmTrUKolVTVj
CCCBmtqq3LwzNLapyoDJRyXsWqHt5TWYSxaf/UQT6sWOgqHOLrbd4J8F0sjxEniB
GDHR1ZXpJCBaIQIDAQABo4GRMIGOMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggr
BgEFBQcDATAqBgNVHREEIzAhgg5yZXViZW4ubmF0cy5pb4IPcmV1YmVuLm5hdHMu
Y29tMB0GA1UdDgQWBBQk5kWOUcUNn7FppddLANe3droUlzAfBgNVHSMEGDAWgBQS
p94B6Z94ingwzgLsfVslL6atrjANBgkqhkiG9w0BAQsFAAOCAQEA2Njy2f1PUZRf
G1/oZ0El7J8L6Ql1HmEC7tOTzbORg7U9uMHKqIFL/IXXAdAlE/EjFEA2riPO8cu/
bvL2A4CapYzt2kDD9PPYfVtniRr7mv0EVntPwEvfiySMAEeZuW/M2liPfgPpQkhL
fzwPeCOfqM8AjpyDab8NEGX5Bbf421oQorlENpm4PKQCXoUN5cWpBwuwWxj7yndj
256MevLDKKe/ALSLQEo/2Jgpnmp7Qol0GtomCzsLgZ+ASuVtCsGTFmaRrsqVPspJ
oOl6qby5gYwN9TR8zfRYL1m1sbYROz+5+ofEoiTnaOoOSjiBIoYoMeSC/jvJQTPT
VdD8QeQ6Og==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEFzCCAv+gAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsQwDQYJKoZIhvcNAQEL
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyx3O+Z6u8Y1SiuHu3szWbLvL
WrZpSpEiZkll+wk5205S1FRcQLccfr4ubdtjOBdi+RzILCtkflUI01Dbqu6cV7/2
yfLthxBeNDiXMhjyOFkYLwwE4w7CdTwWWsmW31oUH1rYXIDPoeb7WPF7w3NwaUJu
ZXnqM98LRgWDTmh+nsqDDW/bz1fYIdxcO9az6iBOnJ2AGWI2ur5GzWc4+gNMOZiZ
Xj657g0MbyVM4Gzyc4Au22hShZ/YorLP8NAiwNJamlrCFzlnZN/ePjuQPcI6glnb
oO9IAGfPdAOJptfayuPAZgUngzewB38yY0Q/rKG1GJKSkQ8X6/lXiWaRPZJjYwID
AQABo4GRMIGOMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggrBgEFBQcDATAqBgNV
HREEIzAhgg5yZXViZW4ubmF0cy5pb4IPcmV1YmVuLm5hdHMuY29tMB0GA1UdDgQW
BBRtanJZScdSlsPsi58lBcpdj+bV/zAfBgNVHSMEGDAWgBQSp94B6Z94ingwzgLs
fVslL6atrjANBgkqhkiG9w0BAQsFAAOCAQEAV4TZ3b8cYO7ZeRyoCQtCBAab9gNe
kbQpWqICvkVQOk5Anq3opwAWk2FuIRs5KoT7ssckHpXwTwWLs+KuIVo+Fet19IH6
BQfck1jwhzM04MA6zLO/F2j548XlrJy3IzViPM/VxwMMTt5YSoogrz/3TzzJPIe0
eQomf5HbpVgrf08pMVkdaI7PCd7N/CxeWiD5zEWqBu9FqofO188Kb/umx0VwgBju
dX46MKO5TyUc91UrG3M35/r4Z7fd52SWWWFQiI7UBOl2L27samjHlJsKjyFoBF3Z
alvnoUVzo7zwAYmhEdPYDNVceF4KtAFpGipoQPRMg83G87LgYBA4Sa6uKw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFFzCCA/+gAwIBAgIUc31LCktokAIQGqLsSC2BlsLCTsUwDQYJKoZIhvcNAQEL
BQAwfDETMBEGA1UEAwwKbmF0cy5pby5DQTEiMCAGA1UECwwZUGVyZm9ybWFuY2VB
bmRSZWxpYWJpbGl0eTEQMA4GA1UECgwHU3luYWRpYTEQMA4GA1UEBwwHVG9yb250
bzEQMA4GA1UECAwHT250YXJpbzELMAkGA1UEBhMCQ0EwIBcNMjMwODE0MTUyNzU3
WhgPMjEyMzA3MjExNTI3NTdaMHkxEDAOBgNVBAMMB25hdHMuaW8xIjAgBgNVBAsM
GVBlcmZvcm1hbmNlQW5kUmVsaWFiaWxpdHkxEDAOBgNVBAoMB1N5bmFkaWExEDAO
BgNVBAcMB1Rvcm9udG8xEDAOBgNVBAgMB09udGFyaW8xCzAJBgNVBAYTAkNBMIIC
IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAj/rw0WpCnizUSs06NxSsjuYb
6sWtYQjLd6O6SCoQwrPSY2Zv0u28RywBJkSiIbv12kWxrhqv+kDqhmt7tfwEdHAE
gOl6E2P/CFDnyAJ4it5qgFRHcEItGp1Ap4ZrZ3OTl41DDe28giflHb7+VAzxUV6r
zkHmrqkaJd41YrJoKyZ3u+/FHs6CudO+jGXC2ubH1i71ARXHn6tkmOTwFf1N49wB
Hr144xBmPyvH/A/elgXBjR28/7x64MO/qBZsr/lbTaS4YwN1+rod/HRc2GIiJxpS
tB1bh/dXZCfa0QyhTzCNLG/j7IzetDrPBGzpw2WjufhkSuaxoMDWlDkkfqmQxtHJ
5L+PqIiPT69tzEbfuS9Ogz7DW10CcpSdXW13sWCdSNCGEvPqLFka36q9B82V0GHz
tmx8VqdfWSu4qMyVmTsxxzLTTxpQU4X1Q2RnT/1igbsOM620LuvrvnXlh5Rdg7tp
T++QJ/b4xCCg62tv4VwORe27xHYXMeDn4aoRdyoI45/+ZK6yqwNAOrKKpse/M8uz
mJK2i8pfEFmitIKoNYn3MR2dFrCqifZkFf9rX9A/1Ym+WKAPLmWdGp13fvTdzxQG
Y44f9tBL2RWsoGX++01XEwIiWz7kqObC0L8fz3EdIPaULX7MZiQrxzzhRCcJhyn/
aOrJfLYj0GAmIaHElHkCAwEAAaOBkTCBjjALBgNVHQ8EBAMCBDAwEwYDVR0lBAww
CgYIKwYBBQUHAwEwKgYDVR0RBCMwIYIOcmV1YmVuLm5hdHMuaW+CD3JldWJlbi5u
YXRzLmNvbTAdBgNVHQ4EFgQUbJSM8LNWmc9IgLe0X53yE3c3h1cwHwYDVR0jBBgw
FoAUEqfeAemfeIp4MM4C7H1bJS+mra4wDQYJKoZIhvcNAQELBQADggEBAEGmLvEE
+MTE1bHMbl/5QG+/xusmervIuxkfAfId0H+8TWB75y+yhVZpEdM7knfl+lexmtGQ
Gr4HNGTZhAZ3NYFaBw7nfeqO48He7gHUKfJA/zv7FREF3Fy+Qe/hydDJQzBzZfaU
64XqhY6jOurpZhTAoOXfjpYzZaLi7+rdpTAbfxHCCAC8SxZD3++Q97ZeoT6en47O
8SQQ7FIzWxs15k88oYalw51vZujxX7dz4l+LxsLXtlYW7ZM1163cgU7lF/jQqDcN
z8X8jk1AjQY7AuFPuOzQ1hLXcZySm8rUG5pPgHrZ1QKmkFFWRaeCiO2hU794wI5C
vIGR8lIhkNwEJnQ=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJRCtUNxUuutNs9j8OtcwFw1xkbs+zxjHhpAqVuqDNo5
-----END PRIVATE KEY-----

Some files were not shown because too many files have changed in this diff Show More