mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 15:57:01 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35ac12b0c0 | |||
| c07f28cd04 | |||
| 72bf111209 | |||
| 1617897043 |
+30
-10
@@ -25,19 +25,39 @@ jobs:
|
|||||||
go-version: '1.26'
|
go-version: '1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Smoke Docker
|
- name: build
|
||||||
run: make smoke-docker
|
run: make bin-docker CGO_ENABLED=1 BUILD_ARGS=-race
|
||||||
|
|
||||||
- name: Smoke Docker IPv6 overlay
|
- name: setup docker image
|
||||||
run: make smoke-docker-ipv6
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: ./build.sh
|
||||||
|
|
||||||
- name: Smoke Relay Docker
|
- name: run smoke
|
||||||
run: make smoke-relay-docker
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: ./smoke.sh
|
||||||
|
|
||||||
- name: Smoke Docker boringcrypto
|
- name: setup docker image ipv6
|
||||||
run: make boringcrypto smoke-docker
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: SMOKE_OVERLAY_IPV6=1 ./build.sh
|
||||||
|
|
||||||
- name: Smoke Docker fips140
|
- name: run smoke ipv6
|
||||||
run: make fips140-all GOALS=smoke-docker
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: SMOKE_OVERLAY_IPV6=1 ./smoke.sh
|
||||||
|
|
||||||
|
- name: setup relay docker image
|
||||||
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: ./build-relay.sh
|
||||||
|
|
||||||
|
- name: run smoke relay
|
||||||
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: ./smoke-relay.sh
|
||||||
|
|
||||||
|
- name: setup docker image for P256
|
||||||
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: NAME="smoke-p256" CURVE=P256 ./build.sh
|
||||||
|
|
||||||
|
- name: run smoke-p256
|
||||||
|
working-directory: ./.github/workflows/smoke
|
||||||
|
run: NAME="smoke-p256" ./smoke.sh
|
||||||
|
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|||||||
@@ -58,14 +58,9 @@ jobs:
|
|||||||
e2e-cmd: make e2evv
|
e2e-cmd: make e2evv
|
||||||
- name: linux-boringcrypto
|
- name: linux-boringcrypto
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
build-cmd: make boringcrypto
|
build-cmd: make bin-boringcrypto
|
||||||
test-cmd: make boringcrypto test
|
test-cmd: make test-boringcrypto
|
||||||
e2e-cmd: make boringcrypto e2evv
|
e2e-cmd: make e2e GOEXPERIMENT=boringcrypto CGO_ENABLED=1 TEST_ENV="TEST_LOGS=1" TEST_FLAGS="-v -ldflags -checklinkname=0"
|
||||||
- name: linux-fips140
|
|
||||||
os: ubuntu-latest
|
|
||||||
build-cmd: make fips140-all
|
|
||||||
test-cmd: make fips140-all GOALS=test
|
|
||||||
e2e-cmd: make fips140-all GOALS=e2evv
|
|
||||||
- name: linux-pkcs11
|
- name: linux-pkcs11
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
build-cmd: make bin-pkcs11
|
build-cmd: make bin-pkcs11
|
||||||
|
|||||||
@@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.11.0] - 2026-07-23
|
||||||
|
|
||||||
|
See the [v1.11.0](https://github.com/slackhq/nebula/milestone/25?closed=1) milestone for a complete list of changes.
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- Logging has switched from logrus to Go's structured `slog`. Log output changes: levels are upper case
|
||||||
|
(`level=INFO`), trace prints as `level=DEBUG-4`, timestamps are always RFC3339Nano and `logging.timestamp_format`
|
||||||
|
is ignored, and some messages were reworded. Review any log parsing before upgrading. This is also an API break
|
||||||
|
for embedders, as constructors now take a `*slog.Logger`. (#1672, #1734, #1621)
|
||||||
|
- `firewall.inbound_action` and `firewall.outbound_action` (used to set reject vs. drop policy) were each being
|
||||||
|
applied to the opposite direction, that is now corrected. This only affects how blocked packets are answered, not
|
||||||
|
which packets the firewall allows or denies. If you set either of these you are getting the behavior of the other
|
||||||
|
one today and likely want to swap them before upgrading. (#1798)
|
||||||
|
- On Windows, Nebula now installs WFP PERMIT filters for the nebula adapter and the listener port by default. WFP
|
||||||
|
sits below Windows Defender Firewall, so any WDF inbound rules you rely on for either will no longer apply. Set
|
||||||
|
`tun.windows_bypass_wdf` and `listen.windows_bypass_wdf` to false to leave WDF in charge. (#1710)
|
||||||
|
- On Windows, the nebula device is now set to the `private` network category instead of whatever Windows decided,
|
||||||
|
which is usually `Public`. This makes the host firewall less restrictive on the overlay. Set
|
||||||
|
`tun.network_category` to `unset` to keep the old behavior. (#1710)
|
||||||
|
- Reject packets for non-TCP now use ICMP code 13, communication administratively prohibited, instead of code 3,
|
||||||
|
port unreachable. Anything keying off the old code needs updating. (#1766, #1768)
|
||||||
|
- The SSH debug server's profiling commands are now confined to `sshd.sandbox_dir`, which defaults to
|
||||||
|
`$TMP/nebula-debug`. Relative paths resolve inside it and absolute paths outside it are rejected, so anything
|
||||||
|
scripting `start-cpu-profile`, `save-heap-profile`, or `save-mutex-profile` with a path elsewhere needs the
|
||||||
|
directory set. The directory is not created for you. (#1622)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Sign the Windows release binaries. (#1718)
|
||||||
|
- Generate IPv6 reject packets, matching the existing IPv4 behavior. (#1766, #1767, #1768)
|
||||||
|
- Accept `-` in `nebula-cert` to read from stdin or write to stdout. (#1714)
|
||||||
|
- Search for both `config.yml` and `config.yaml` in service and command line modes. (#1717)
|
||||||
|
- Add version labels to the Docker/OCI images. (#1772)
|
||||||
|
- Rebind the listener and re-query lighthouses on macOS when the underlay network changes, so devices moving
|
||||||
|
between wifi and wired or between networks recover without waiting for dead tunnel detection. Controlled by
|
||||||
|
`listen.rebind_on_network_change` (default `true`, not reloadable). (#1816)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reload the firewall when the unsafe networks in the certificate change. (#1719)
|
||||||
|
- Reconfigure, start, and stop the stats listener on a config reload instead of requiring a restart. (#1670)
|
||||||
|
- Update a static host's addresses when they change on reload. (#1713)
|
||||||
|
- Don't require a port on ICMP firewall rules. (#1609)
|
||||||
|
- Connection track ICMP traffic. (#1602)
|
||||||
|
- Return `NODATA` instead of `NXDOMAIN` from the DNS server for a name that exists but has no record of the
|
||||||
|
requested type, so clients that query `AAAA` first (busybox/Alpine) fall through to `A`. (#1668)
|
||||||
|
- Record the local host's details in the DNS server. (#1716)
|
||||||
|
- Install Windows unsafe routes as link routes. (#1709)
|
||||||
|
- Reduce relay handshake log spam, and only log a handshake send error at error level when the remote list
|
||||||
|
changes. (#1733, #1765, #1810)
|
||||||
|
- Start, stop, and reload subsystems (DNS, stats, conntrack, ssh, punchy) cleanly without leaking goroutines. (#1640, #1654, #1661, #1667, #1669, #1708, #1806, #1815)
|
||||||
|
- `Control` is now safe to stop and wait on from any lifecycle state, and a new `Control.Wait` blocks until nebula
|
||||||
|
has fully stopped and returns the first fatal reader error. Failed starts release the udp sockets and tun fd
|
||||||
|
instead of leaking them. (#1794)
|
||||||
|
- Trigger an immediate lighthouse update when reconnecting to or adding a lighthouse instead of waiting for the next update tick. (#1645)
|
||||||
|
- Bring the Darwin and OpenBSD tun implementations in line with the other BSDs. (#1703)
|
||||||
|
- Update to build against go v1.26. (#1818)
|
||||||
|
- Various dependency updates. (#1586, #1587, #1604, #1617, #1618, #1627, #1628, #1629, #1652, #1664, #1665, #1697, #1721, #1732, #1742, #1743, #1750, #1763, #1771, #1782, #1800, #1807)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix a data race on a host's remote address that could send packets to the wrong address during a roam. (#1773)
|
||||||
|
- Fix tunnels that could permanently escape connection manager monitoring. (#1752)
|
||||||
|
- Fix a crash when reloading the SSH server's trusted keys. (#1787)
|
||||||
|
- Fix hostmap corruption when a host has multiple overlay addresses. Each address now gets its own list instead of
|
||||||
|
a single shared chain, which also fixes two latent bugs on the add and makePrimary paths. (#1788, #1790)
|
||||||
|
- Apply `remote_allow_list` IPv4 rules to 4-in-6 mapped addresses. (#1786)
|
||||||
|
- Don't panic in the DNS server on a short or empty query name. (#1635)
|
||||||
|
- Advance the replay window on relayed packets so a relay drops replayed frames instead of re-forwarding them. (#1751)
|
||||||
|
- Fix a race in relay state handling. (#1753)
|
||||||
|
- Lock replay window updates so concurrent readers can't corrupt it. (#1802)
|
||||||
|
- Reject malformed handshakes more reliably, including invalid ed25519 key lengths. (#1601, #1756)
|
||||||
|
- Properly handle `closetunnel` packets. (#1638)
|
||||||
|
- Fix an IPv6 extension-header length overflow that could make the firewall parse the wrong protocol and ports. (#1789)
|
||||||
|
- Fix relay re-establishment when a handshake arrives over a relay entry that a one-sided teardown left
|
||||||
|
`Disestablished`, which silently dropped every send until dead tunnel detection forced a re-handshake. (#1805)
|
||||||
|
- Don't build new relay state on a tunnel that was just discarded. (#1796)
|
||||||
|
- Don't delete the wrong pending hostinfo in the handshake manager. (#1811)
|
||||||
|
- Don't call the packet reader after a UDP error on Darwin. (#1755)
|
||||||
|
- Open the FreeBSD tun device non blocking. (#1666)
|
||||||
|
|
||||||
## [1.10.3] - 2026-02-06
|
## [1.10.3] - 2026-02-06
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@@ -72,17 +72,6 @@ ALL_CROSS_LINUX_ARM = linux-arm-5 linux-arm-6 linux-arm-7 linux-arm64
|
|||||||
ALL_CROSS_LINUX_MIPS = linux-mips linux-mipsle linux-mips64 linux-mips64le linux-mips-softfloat
|
ALL_CROSS_LINUX_MIPS = linux-mips linux-mipsle linux-mips64 linux-mips64le linux-mips-softfloat
|
||||||
ALL_CROSS_LINUX_OTHER = linux-386 linux-ppc64le linux-riscv64 linux-loong64
|
ALL_CROSS_LINUX_OTHER = linux-386 linux-ppc64le linux-riscv64 linux-loong64
|
||||||
|
|
||||||
# Based on section 2.2 of the Go Cryptographic Module CVMP Security Policy #5247
|
|
||||||
ALL_FIPS140 = linux-amd64-fips140 \
|
|
||||||
linux-arm64-fips140 \
|
|
||||||
windows-amd64-fips140 \
|
|
||||||
windows-arm64-fips140 \
|
|
||||||
darwin-arm64-fips140 \
|
|
||||||
freebsd-amd64-fips140 \
|
|
||||||
linux-arm-7-fips140 \
|
|
||||||
linux-mips64-fips140 \
|
|
||||||
linux-ppc64le-fips140
|
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
$(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./e2e
|
$(TEST_ENV) go test -tags=e2e_testing -count=1 $(TEST_FLAGS) ./e2e
|
||||||
|
|
||||||
@@ -148,8 +137,6 @@ release-netbsd: $(ALL_NETBSD:%=build/nebula-%.tar.gz)
|
|||||||
|
|
||||||
release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz
|
release-boringcrypto: build/nebula-linux-$(shell go env GOARCH)-boringcrypto.tar.gz
|
||||||
|
|
||||||
release-fips140: $(ALL_FIPS140:%=build/nebula-%.tar.gz)
|
|
||||||
|
|
||||||
BUILD_ARGS += -trimpath
|
BUILD_ARGS += -trimpath
|
||||||
|
|
||||||
bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe
|
bin-windows: build/windows-amd64/nebula.exe build/windows-amd64/nebula-cert.exe
|
||||||
@@ -170,20 +157,17 @@ bin-freebsd-arm64: build/freebsd-arm64/nebula build/freebsd-arm64/nebula-cert
|
|||||||
bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert
|
bin-boringcrypto: build/linux-$(shell go env GOARCH)-boringcrypto/nebula build/linux-$(shell go env GOARCH)-boringcrypto/nebula-cert
|
||||||
mv $? .
|
mv $? .
|
||||||
|
|
||||||
bin-fips140: build/linux-$(shell go env GOARCH)-fips140/nebula build/linux-$(shell go env GOARCH)-fips140/nebula-cert
|
|
||||||
mv $? .
|
|
||||||
|
|
||||||
bin-pkcs11: BUILD_ARGS += -tags pkcs11
|
bin-pkcs11: BUILD_ARGS += -tags pkcs11
|
||||||
bin-pkcs11: CGO_ENABLED = 1
|
bin-pkcs11: CGO_ENABLED = 1
|
||||||
bin-pkcs11: bin
|
bin-pkcs11: bin
|
||||||
|
|
||||||
bin:
|
bin:
|
||||||
$(GOENV) go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
|
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula${NEBULA_CMD_SUFFIX} ${NEBULA_CMD_PATH}
|
||||||
$(GOENV) go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
|
go build $(BUILD_ARGS) -ldflags "$(LDFLAGS)" -o ./nebula-cert${NEBULA_CMD_SUFFIX} ./cmd/nebula-cert
|
||||||
|
|
||||||
install:
|
install:
|
||||||
$(GOENV) go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ${NEBULA_CMD_PATH}
|
go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ${NEBULA_CMD_PATH}
|
||||||
$(GOENV) go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ./cmd/nebula-cert
|
go install $(BUILD_ARGS) -ldflags "$(LDFLAGS)" ./cmd/nebula-cert
|
||||||
|
|
||||||
build/linux-arm-%: GOENV += GOARM=$(word 3, $(subst -, ,$*))
|
build/linux-arm-%: GOENV += GOARM=$(word 3, $(subst -, ,$*))
|
||||||
build/linux-mips-%: GOENV += GOMIPS=$(word 3, $(subst -, ,$*))
|
build/linux-mips-%: GOENV += GOMIPS=$(word 3, $(subst -, ,$*))
|
||||||
@@ -194,11 +178,8 @@ build/linux-mips-softfloat/%: LDFLAGS += -s -w
|
|||||||
# boringcrypto
|
# boringcrypto
|
||||||
build/linux-amd64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
build/linux-amd64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
||||||
build/linux-arm64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
build/linux-arm64-boringcrypto/%: GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1
|
||||||
|
build/linux-amd64-boringcrypto/%: LDFLAGS += -checklinkname=0
|
||||||
# fips140
|
build/linux-arm64-boringcrypto/%: LDFLAGS += -checklinkname=0
|
||||||
FIPSVERSION = v1.0.0
|
|
||||||
$(foreach _rule, $(ALL_FIPS140), build/$(_rule)/%): GOENV += GOFIPS140=$(FIPSVERSION)
|
|
||||||
$(foreach _rule, $(ALL_FIPS140), build/$(_rule)/%): BUILD_ARGS += -tags fips140enforce
|
|
||||||
|
|
||||||
build/%/nebula: .FORCE
|
build/%/nebula: .FORCE
|
||||||
GOOS=$(firstword $(subst -, , $*)) \
|
GOOS=$(firstword $(subst -, , $*)) \
|
||||||
@@ -229,7 +210,10 @@ vet:
|
|||||||
go vet $(VET_FLAGS) -v ./...
|
go vet $(VET_FLAGS) -v ./...
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(TEST_ENV) go test $(TEST_FLAGS) -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
|
test-boringcrypto:
|
||||||
|
GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go test -ldflags "-checklinkname=0" -v ./...
|
||||||
|
|
||||||
test-pkcs11:
|
test-pkcs11:
|
||||||
CGO_ENABLED=1 go test -v -tags pkcs11 ./...
|
CGO_ENABLED=1 go test -v -tags pkcs11 ./...
|
||||||
@@ -272,72 +256,29 @@ ifeq ($(words $(MAKECMDGOALS)),1)
|
|||||||
@$(MAKE) service ${.DEFAULT_GOAL} --no-print-directory
|
@$(MAKE) service ${.DEFAULT_GOAL} --no-print-directory
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Useful to chain together, like:
|
|
||||||
# - make fips140 e2evv
|
|
||||||
# - make fips140 smoke-docker
|
|
||||||
# Use `release-fips140` to build release binaries
|
|
||||||
fips140:
|
|
||||||
@echo > $(NULL_FILE)
|
|
||||||
ifeq ($(strip $(GOFIPS140)),)
|
|
||||||
$(eval GOFIPS140 = $(FIPSVERSION))
|
|
||||||
endif
|
|
||||||
$(eval GOENV += GOFIPS140=$(GOFIPS140))
|
|
||||||
$(eval BUILD_ARGS += -tags fips140enforce)
|
|
||||||
$(eval TEST_ENV += $(GOENV))
|
|
||||||
$(eval CURVE = P256)
|
|
||||||
ifeq ($(words $(MAKECMDGOALS)),1)
|
|
||||||
@$(MAKE) fips140 GOFIPS140=$(GOFIPS140) ${.DEFAULT_GOAL} --no-print-directory
|
|
||||||
endif
|
|
||||||
|
|
||||||
# To test the future pending module, use like `make fips140-latest test`
|
|
||||||
ALL_GOFIPS140 = v1.0.0 v1.26.0 latest
|
|
||||||
define FIPS140_rule
|
|
||||||
fips140-$(1): GOFIPS140 = $(1)
|
|
||||||
fips140-$(1): fips140
|
|
||||||
endef
|
|
||||||
$(foreach _rule, $(ALL_GOFIPS140), $(eval $(call FIPS140_rule,$(_rule))))
|
|
||||||
|
|
||||||
# Iterate and run the goals for all fips versions, like `make fips140-all GOALS=test`
|
|
||||||
fips140-all:
|
|
||||||
@$(foreach _v,$(ALL_GOFIPS140),$(MAKE) fips140-$(_v) $(GOALS) &&) true
|
|
||||||
|
|
||||||
# Useful to chain together, like:
|
|
||||||
# - make boringcrypto e2evv
|
|
||||||
# - make boringcrypto smoke-docker
|
|
||||||
# Use `release-boringcrypto` or `bin-boringcrypto` to build release binaries
|
|
||||||
boringcrypto:
|
|
||||||
@echo > $(NULL_FILE)
|
|
||||||
$(eval GOENV += GOEXPERIMENT=boringcrypto CGO_ENABLED=1)
|
|
||||||
$(eval TEST_ENV += $(GOENV))
|
|
||||||
$(eval CURVE = P256)
|
|
||||||
ifeq ($(words $(MAKECMDGOALS)),1)
|
|
||||||
@$(MAKE) boringcrypto ${.DEFAULT_GOAL} --no-print-directory
|
|
||||||
endif
|
|
||||||
|
|
||||||
bin-docker: bin build/linux-amd64/nebula build/linux-amd64/nebula-cert
|
bin-docker: bin build/linux-amd64/nebula build/linux-amd64/nebula-cert
|
||||||
|
|
||||||
smoke-docker: BUILD_ARGS += -race
|
|
||||||
smoke-docker: GOENV += CGO_ENABLED=1
|
|
||||||
smoke-docker: bin-docker
|
smoke-docker: bin-docker
|
||||||
# This is so we can limit `fips140` smoke test to just P256 curve.
|
cd .github/workflows/smoke/ && ./build.sh
|
||||||
if [ "$(CURVE)" != "P256" ]; then cd .github/workflows/smoke/ && $(GOENV) ./build.sh; fi
|
cd .github/workflows/smoke/ && ./smoke.sh
|
||||||
if [ "$(CURVE)" != "P256" ]; then cd .github/workflows/smoke/ && $(GOENV) ./smoke.sh; fi
|
cd .github/workflows/smoke/ && NAME="smoke-p256" CURVE="P256" ./build.sh
|
||||||
cd .github/workflows/smoke/ && $(GOENV) NAME="smoke-p256" CURVE="P256" ./build.sh
|
cd .github/workflows/smoke/ && NAME="smoke-p256" ./smoke.sh
|
||||||
cd .github/workflows/smoke/ && $(GOENV) NAME="smoke-p256" ./smoke.sh
|
|
||||||
|
|
||||||
smoke-relay-docker: BUILD_ARGS += -race
|
|
||||||
smoke-relay-docker: GOENV += CGO_ENABLED=1
|
|
||||||
smoke-relay-docker: bin-docker
|
smoke-relay-docker: bin-docker
|
||||||
cd .github/workflows/smoke/ && $(GOENV) ./build-relay.sh
|
cd .github/workflows/smoke/ && ./build-relay.sh
|
||||||
cd .github/workflows/smoke/ && $(GOENV) ./smoke-relay.sh
|
cd .github/workflows/smoke/ && ./smoke-relay.sh
|
||||||
|
|
||||||
smoke-docker-ipv6: export SMOKE_OVERLAY_IPV6 = 1
|
smoke-docker-ipv6: export SMOKE_OVERLAY_IPV6 = 1
|
||||||
smoke-docker-ipv6: smoke-docker
|
smoke-docker-ipv6: smoke-docker
|
||||||
|
|
||||||
|
smoke-docker-race: BUILD_ARGS = -race
|
||||||
|
smoke-docker-race: CGO_ENABLED = 1
|
||||||
|
smoke-docker-race: smoke-docker
|
||||||
|
|
||||||
smoke-vagrant/%: bin-docker build/%/nebula
|
smoke-vagrant/%: bin-docker build/%/nebula
|
||||||
cd .github/workflows/smoke/ && ./build.sh $*
|
cd .github/workflows/smoke/ && ./build.sh $*
|
||||||
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
cd .github/workflows/smoke/ && ./smoke-vagrant.sh $*
|
||||||
|
|
||||||
.FORCE:
|
.FORCE:
|
||||||
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin bin-windows bin-windows-arm64 bin-darwin bin-freebsd bin-freebsd-arm64 bin-boringcrypto bin-fips140 bin-pkcs11 bin-docker boringcrypto build-test-mobile docker e2e e2ev e2evv e2evvv e2evvvv e2e-bench fips140 fips140-all $(ALL_GOFIPS140:%=fips140-%) install proto release release-linux release-freebsd release-openbsd release-netbsd release-boringcrypto release-fips140 service smoke-docker smoke-relay-docker smoke-docker-ipv6 test test-pkcs11 test-cov-html vet smoke-vagrant/%
|
.PHONY: all all-linux all-freebsd all-openbsd all-netbsd all-darwin all-windows all-cross-linux all-cross-linux-arm all-cross-linux-mips all-cross-linux-other all-cross-darwin all-cross-windows bench bench-cpu bench-cpu-long bin build-test-mobile e2e e2ev e2evv e2evvv e2evvvv proto release service smoke-docker smoke-docker-race test test-cov-html smoke-vagrant/%
|
||||||
.DEFAULT_GOAL := bin
|
.DEFAULT_GOAL := bin
|
||||||
|
|||||||
@@ -145,27 +145,17 @@ To build nebula for a specific platform (ex, Windows):
|
|||||||
|
|
||||||
See the [Makefile](Makefile) for more details on build targets
|
See the [Makefile](Makefile) for more details on build targets
|
||||||
|
|
||||||
## Curve P256 and FIPS 140-3 mode
|
## Curve P256 and BoringCrypto
|
||||||
|
|
||||||
The default curve used for cryptographic handshakes and signatures is Curve25519. This is the recommended setting for most users. If your deployment has certain compliance requirements, you have the option of creating your CA using `nebula-cert ca -curve P256` to use NIST Curve P256. The CA will then sign certificates using ECDSA P256, and any hosts using these certificates will use P256 for ECDH handshakes.
|
The default curve used for cryptographic handshakes and signatures is Curve25519. This is the recommended setting for most users. If your deployment has certain compliance requirements, you have the option of creating your CA using `nebula-cert ca -curve P256` to use NIST Curve P256. The CA will then sign certificates using ECDSA P256, and any hosts using these certificates will use P256 for ECDH handshakes.
|
||||||
|
|
||||||
Nebula can be built to support the [FIPS 140-3](https://go.dev/doc/security/fips140) mode of Go by running either of the following make targets. (This sets GOFIPS140=v1.0.0, which must be done at compile time so that the correct AES-GCM can be used for FIPS 140-3 enforcement mode).
|
In addition, Nebula can be built using the [BoringCrypto GOEXPERIMENT](https://github.com/golang/go/blob/go1.20/src/crypto/internal/boring/README.md) by running either of the following make targets:
|
||||||
|
|
||||||
```sh
|
|
||||||
make fips140
|
|
||||||
make fips140 test
|
|
||||||
make release-fips140
|
|
||||||
```
|
|
||||||
|
|
||||||
Nebula can also be built using the [BoringCrypto GOEXPERIMENT](https://github.com/golang/go/blob/go1.20/src/crypto/internal/boring/README.md) by running either of the following make targets.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make bin-boringcrypto
|
make bin-boringcrypto
|
||||||
make release-boringcrypto
|
make release-boringcrypto
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: boringcrypto support is deprecated and will be removed in the next release. Users should migrate to the native FIPS 140-3 mode described above.
|
|
||||||
|
|
||||||
This is not the recommended default deployment, but may be useful based on your compliance requirements.
|
This is not the recommended default deployment, but may be useful based on your compliance requirements.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/fips140"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -44,13 +43,6 @@ type caFlags struct {
|
|||||||
subnets *string
|
subnets *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultCurve() string {
|
|
||||||
if fips140.Enforced() {
|
|
||||||
return "P256"
|
|
||||||
}
|
|
||||||
return "25519"
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCaFlags() *caFlags {
|
func newCaFlags() *caFlags {
|
||||||
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
|
cf := caFlags{set: flag.NewFlagSet("ca", flag.ContinueOnError)}
|
||||||
cf.set.Usage = func() {}
|
cf.set.Usage = func() {}
|
||||||
@@ -67,7 +59,7 @@ func newCaFlags() *caFlags {
|
|||||||
cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
|
cf.argonParallelism = cf.set.Uint("argon-parallelism", 4, "Optional: Argon2 parallelism parameter used for encrypted private key passphrase")
|
||||||
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
|
cf.argonIterations = cf.set.Uint("argon-iterations", 1, "Optional: Argon2 iterations parameter used for encrypted private key passphrase")
|
||||||
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
|
cf.encryption = cf.set.Bool("encrypt", false, "Optional: prompt for passphrase and write out-key in an encrypted format")
|
||||||
cf.curve = cf.set.String("curve", defaultCurve(), "EdDSA/ECDSA Curve (25519, P256)")
|
cf.curve = cf.set.String("curve", "25519", "EdDSA/ECDSA Curve (25519, P256)")
|
||||||
cf.p11url = p11Flag(cf.set)
|
cf.p11url = p11Flag(cf.set)
|
||||||
|
|
||||||
cf.ips = cf.set.String("ips", "", "Deprecated, see -networks")
|
cf.ips = cf.set.String("ips", "", "Deprecated, see -networks")
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build fips140enforce
|
|
||||||
|
|
||||||
//go:debug fips140=only
|
|
||||||
|
|
||||||
package main
|
|
||||||
@@ -24,7 +24,7 @@ func newKeygenFlags() *keygenFlags {
|
|||||||
cf.set.Usage = func() {}
|
cf.set.Usage = func() {}
|
||||||
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
|
cf.outPubPath = cf.set.String("out-pub", "", "Required: path to write the public key to")
|
||||||
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
|
cf.outKeyPath = cf.set.String("out-key", "", "Required: path to write the private key to")
|
||||||
cf.curve = cf.set.String("curve", defaultCurve(), "ECDH Curve (25519, P256)")
|
cf.curve = cf.set.String("curve", "25519", "ECDH Curve (25519, P256)")
|
||||||
cf.p11url = p11Flag(cf.set)
|
cf.p11url = p11Flag(cf.set)
|
||||||
return &cf
|
return &cf
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build fips140enforce
|
|
||||||
|
|
||||||
//go:debug fips140=only
|
|
||||||
|
|
||||||
package main
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build fips140enforce
|
|
||||||
|
|
||||||
//go:debug fips140=only
|
|
||||||
|
|
||||||
package main
|
|
||||||
+13
-8
@@ -105,11 +105,17 @@ func (cm *connectionManager) getInactivityTimeout() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) In(h *HostInfo) {
|
func (cm *connectionManager) In(h *HostInfo) {
|
||||||
h.in.Store(true)
|
h.markIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) Out(h *HostInfo) {
|
// OutRelay records relayed traffic, leaving the rebind epoch for the direct path to this host to consume
|
||||||
h.out.Store(true)
|
func (cm *connectionManager) OutRelay(h *HostInfo) {
|
||||||
|
h.markOutOnly()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out records outbound traffic and reports whether we rebound since this tunnel last sent
|
||||||
|
func (cm *connectionManager) Out(h *HostInfo) bool {
|
||||||
|
return h.markOut(cm.intf.rebindEpoch.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *connectionManager) RelayUsed(localIndex uint32) {
|
func (cm *connectionManager) RelayUsed(localIndex uint32) {
|
||||||
@@ -128,8 +134,7 @@ func (cm *connectionManager) RelayUsed(localIndex uint32) {
|
|||||||
// getAndResetTrafficCheck returns if there was any inbound or outbound traffic within the last tick and
|
// getAndResetTrafficCheck returns if there was any inbound or outbound traffic within the last tick and
|
||||||
// resets the state for this local index
|
// resets the state for this local index
|
||||||
func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time) (bool, bool) {
|
func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time) (bool, bool) {
|
||||||
in := h.in.Swap(false)
|
in, out := h.takeTraffic()
|
||||||
out := h.out.Swap(false)
|
|
||||||
if in || out {
|
if in || out {
|
||||||
h.lastUsed = now
|
h.lastUsed = now
|
||||||
}
|
}
|
||||||
@@ -340,7 +345,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
"tunnelCheck", m{"state": "alive", "method": "passive"},
|
"tunnelCheck", m{"state": "alive", "method": "passive"},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
hostinfo.pendingDeletion.Store(false)
|
hostinfo.setPendingDeletion(false)
|
||||||
|
|
||||||
if mainHostInfo {
|
if mainHostInfo {
|
||||||
decision = tryRehandshake
|
decision = tryRehandshake
|
||||||
@@ -363,7 +368,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
return decision, hostinfo, primary
|
return decision, hostinfo, primary
|
||||||
}
|
}
|
||||||
|
|
||||||
if hostinfo.pendingDeletion.Load() {
|
if hostinfo.isPendingDeletion() {
|
||||||
// We have already sent a test packet and nothing was returned, this hostinfo is dead
|
// We have already sent a test packet and nothing was returned, this hostinfo is dead
|
||||||
hostinfo.logger(cm.l).Info("Tunnel status",
|
hostinfo.logger(cm.l).Info("Tunnel status",
|
||||||
"tunnelCheck", m{"state": "dead", "method": "active"},
|
"tunnelCheck", m{"state": "dead", "method": "active"},
|
||||||
@@ -414,7 +419,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hostinfo.pendingDeletion.Store(true)
|
hostinfo.setPendingDeletion(true)
|
||||||
cm.trafficTimer.Add(hostinfo.localIndexId, cm.pendingDeletionInterval)
|
cm.trafficTimer.Add(hostinfo.localIndexId, cm.pendingDeletionInterval)
|
||||||
return decision, hostinfo, nil
|
return decision, hostinfo, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-36
@@ -86,25 +86,25 @@ func Test_NewConnectionManagerTest(t *testing.T) {
|
|||||||
// We saw traffic out to vpnIp
|
// We saw traffic out to vpnIp
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.True(t, hostinfo.out.Load())
|
assert.True(t, hostinfo.sentSinceCheck())
|
||||||
assert.True(t, hostinfo.in.Load())
|
assert.True(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
|
|
||||||
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
|
|
||||||
// Do another traffic check tick, this host should be pending deletion now
|
// Do another traffic check tick, this host should be pending deletion now
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
assert.True(t, hostinfo.out.Load())
|
assert.True(t, hostinfo.sentSinceCheck())
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.True(t, hostinfo.pendingDeletion.Load())
|
assert.True(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
@@ -168,33 +168,33 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
|
|||||||
// We saw traffic out to vpnIp
|
// We saw traffic out to vpnIp
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.True(t, hostinfo.in.Load())
|
assert.True(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
assert.True(t, hostinfo.out.Load())
|
assert.True(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
|
|
||||||
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
|
|
||||||
// Do another traffic check tick, this host should be pending deletion now
|
// Do another traffic check tick, this host should be pending deletion now
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.True(t, hostinfo.pendingDeletion.Load())
|
assert.True(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
// We saw traffic, should no longer be pending deletion
|
// We saw traffic, should no longer be pending deletion
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
}
|
}
|
||||||
@@ -253,31 +253,31 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
// Do a traffic check tick, in and out should be cleared but should not be pending deletion
|
// Do a traffic check tick, in and out should be cleared but should not be pending deletion
|
||||||
nc.Out(hostinfo)
|
nc.Out(hostinfo)
|
||||||
nc.In(hostinfo)
|
nc.In(hostinfo)
|
||||||
assert.True(t, hostinfo.out.Load())
|
assert.True(t, hostinfo.sentSinceCheck())
|
||||||
assert.True(t, hostinfo.in.Load())
|
assert.True(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
decision, _, _ := nc.makeTrafficDecision(hostinfo.localIndexId, now)
|
decision, _, _ := nc.makeTrafficDecision(hostinfo.localIndexId, now)
|
||||||
assert.Equal(t, tryRehandshake, decision)
|
assert.Equal(t, tryRehandshake, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
|
|
||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*5))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*5))
|
||||||
assert.Equal(t, doNothing, decision)
|
assert.Equal(t, doNothing, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
|
|
||||||
// Do another traffic check tick, should still not be pending deletion
|
// Do another traffic check tick, should still not be pending deletion
|
||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*10))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Second*10))
|
||||||
assert.Equal(t, doNothing, decision)
|
assert.Equal(t, doNothing, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
|
|
||||||
@@ -285,9 +285,9 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
|
|||||||
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Minute*10))
|
decision, _, _ = nc.makeTrafficDecision(hostinfo.localIndexId, now.Add(time.Minute*10))
|
||||||
assert.Equal(t, closeTunnel, decision)
|
assert.Equal(t, closeTunnel, decision)
|
||||||
assert.Equal(t, now, hostinfo.lastUsed)
|
assert.Equal(t, now, hostinfo.lastUsed)
|
||||||
assert.False(t, hostinfo.pendingDeletion.Load())
|
assert.False(t, hostinfo.isPendingDeletion())
|
||||||
assert.False(t, hostinfo.out.Load())
|
assert.False(t, hostinfo.sentSinceCheck())
|
||||||
assert.False(t, hostinfo.in.Load())
|
assert.False(t, (hostinfo.state.Load()&stateIn != 0))
|
||||||
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
|
||||||
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -212,7 +212,7 @@ func (c *Control) RebindUDPServer() {
|
|||||||
c.f.lightHouse.SendUpdate()
|
c.f.lightHouse.SendUpdate()
|
||||||
|
|
||||||
// Let the main interface know that we rebound so that underlying tunnels know to trigger punches from their remotes
|
// Let the main interface know that we rebound so that underlying tunnels know to trigger punches from their remotes
|
||||||
c.f.rebindCount++
|
c.f.rebindEpoch.Add(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListHostmapHosts returns details about the actual or pending (handshaking) hostmap by vpn ip
|
// ListHostmapHosts returns details about the actual or pending (handshaking) hostmap by vpn ip
|
||||||
|
|||||||
@@ -123,6 +123,16 @@ func (c *Control) SetLocalAddrsFn(fn func(*LocalAllowList) []netip.Addr) {
|
|||||||
c.f.lightHouse.localAddrsFn = fn
|
c.f.lightHouse.localAddrsFn = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRebindEpochFor returns the rebind epoch a tunnel last sent under, so a test can tell whether a send
|
||||||
|
// consumed the epoch edge without having to infer it from lighthouse traffic.
|
||||||
|
func (c *Control) GetRebindEpochFor(vpnAddr netip.Addr) (uint32, bool) {
|
||||||
|
h := c.f.hostMap.QueryVpnAddr(vpnAddr)
|
||||||
|
if h == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return h.state.Load() >> stateEpochShift, true
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
func (c *Control) KillPendingTunnel(vpnIp netip.Addr) bool {
|
||||||
hostinfo := c.f.handshakeManager.QueryVpnAddr(vpnIp)
|
hostinfo := c.f.handshakeManager.QueryVpnAddr(vpnIp)
|
||||||
if hostinfo == nil {
|
if hostinfo == nil {
|
||||||
|
|||||||
@@ -223,3 +223,57 @@ func TestRebindAdvertisesNewAddressAfterMove(t *testing.T) {
|
|||||||
lhControl.Stop()
|
lhControl.Stop()
|
||||||
myControl.Stop()
|
myControl.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A relayed send records traffic but must not consume the rebind epoch. If it does, the next direct send to the
|
||||||
|
// relay host sees the epoch already current and never requeries, so the far side is never told to punch at our
|
||||||
|
// new address. This pins the SendVia call site, which the unit tests cannot reach.
|
||||||
|
func TestRebindRequeriesAfterRelayedSend(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
|
|
||||||
|
// No lighthouse on purpose: it would hand out a direct address for them and nothing would relay.
|
||||||
|
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
|
||||||
|
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "relay", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
|
||||||
|
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
|
||||||
|
|
||||||
|
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
|
||||||
|
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
|
||||||
|
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||||
|
|
||||||
|
r := router.NewR(t, myControl, relayControl, theirControl)
|
||||||
|
defer r.RenderFlow()
|
||||||
|
|
||||||
|
myControl.Start()
|
||||||
|
relayControl.Start()
|
||||||
|
theirControl.Start()
|
||||||
|
|
||||||
|
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("establish")))
|
||||||
|
r.RouteForAllUntilTxTun(theirControl)
|
||||||
|
r.RouteFor(time.Millisecond * 500)
|
||||||
|
|
||||||
|
hi := myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false)
|
||||||
|
require.NotNil(t, hi, "expected a tunnel to them")
|
||||||
|
require.NotEmpty(t, hi.CurrentRelaysToMe, "them must be reachable only via the relay for this test to mean anything")
|
||||||
|
// sendNoMetrics only reaches SendVia when there is no direct remote, so pin that too. Without this the test
|
||||||
|
// keeps passing while quietly sending direct and never exercising the relay path.
|
||||||
|
require.False(t, hi.CurrentRemote.IsValid(), "them must have no direct remote, otherwise SendVia is never called")
|
||||||
|
|
||||||
|
before, ok := myControl.GetRebindEpochFor(relayVpnIpNet[0].Addr())
|
||||||
|
require.True(t, ok, "expected a tunnel to the relay")
|
||||||
|
|
||||||
|
myControl.RebindUDPServer()
|
||||||
|
|
||||||
|
// Traffic to them goes through SendVia on the relay tunnel. That must record traffic without consuming the
|
||||||
|
// relay tunnel's own epoch edge, which belongs to the direct path.
|
||||||
|
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("relayed")))
|
||||||
|
r.RouteForAllUntilTxTun(theirControl)
|
||||||
|
|
||||||
|
after, ok := myControl.GetRebindEpochFor(relayVpnIpNet[0].Addr())
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, before, after,
|
||||||
|
"a relayed send consumed the relay tunnel's rebind epoch, so the next direct send will not requery")
|
||||||
|
|
||||||
|
myControl.Stop()
|
||||||
|
relayControl.Stop()
|
||||||
|
theirControl.Stop()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
//go:build e2e_testing
|
||||||
|
// +build e2e_testing
|
||||||
|
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula"
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"github.com/slackhq/nebula/cert_test"
|
||||||
|
"github.com/slackhq/nebula/e2e/router"
|
||||||
|
"github.com/slackhq/nebula/udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRecoveryTiming measures how long a tunnel takes to come back after the peer stops accepting our traffic,
|
||||||
|
// which is what a laptop waking on a new network looks like from the peer's side: its NAT has no state for where
|
||||||
|
// we are now, so everything we send disappears.
|
||||||
|
//
|
||||||
|
// It is a measurement, not a pass/fail assertion. Recovery is timed to the moment the peer punches back at us,
|
||||||
|
// since that is when its NAT opens and the tunnel is usable again.
|
||||||
|
//
|
||||||
|
// go test -tags e2e_testing -v -run TestRecoveryTiming ./e2e/
|
||||||
|
func TestRecoveryTiming(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
rebind bool
|
||||||
|
}{
|
||||||
|
{"no trigger", false},
|
||||||
|
{"rebind counter", true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
d, lost := measureRecovery(t, tc.rebind)
|
||||||
|
t.Logf("RESULT %-16s recovered in %-9v (%d packets lost)", tc.name, d.Round(time.Millisecond), lost)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// measureRecovery returns how long until the peer punched back, and how many of our packets died meanwhile. When
|
||||||
|
// rebind is true we call RebindUDPServer once the tunnel goes dark, which is what the darwin network change
|
||||||
|
// monitor does and what iOS has always done. When false, nothing tells nebula anything is wrong.
|
||||||
|
func measureRecovery(t *testing.T, rebind bool) (time.Duration, int) {
|
||||||
|
t.Helper()
|
||||||
|
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
||||||
|
|
||||||
|
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
||||||
|
"lighthouse": m{"am_lighthouse": true},
|
||||||
|
})
|
||||||
|
|
||||||
|
peerCfg := m{
|
||||||
|
"lighthouse": m{
|
||||||
|
"hosts": []any{lhVpnIpNet[0].Addr().String()},
|
||||||
|
"interval": 600,
|
||||||
|
"local_allow_list": m{
|
||||||
|
"10.0.0.0/24": true,
|
||||||
|
"::/0": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"static_host_map": m{
|
||||||
|
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", peerCfg)
|
||||||
|
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24", peerCfg)
|
||||||
|
|
||||||
|
r := router.NewR(t, lhControl, myControl, theirControl)
|
||||||
|
defer r.RenderFlow()
|
||||||
|
defer func() {
|
||||||
|
lhControl.Stop()
|
||||||
|
myControl.Stop()
|
||||||
|
theirControl.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
lhControl.Start()
|
||||||
|
myControl.Start()
|
||||||
|
theirControl.Start()
|
||||||
|
r.RouteFor(time.Millisecond * 500)
|
||||||
|
|
||||||
|
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
||||||
|
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
||||||
|
|
||||||
|
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("establish")))
|
||||||
|
r.RouteFor(time.Second)
|
||||||
|
if myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false) == nil {
|
||||||
|
t.Fatal("failed to establish the tunnel we are measuring")
|
||||||
|
}
|
||||||
|
r.RouteFor(time.Millisecond * 500)
|
||||||
|
|
||||||
|
// From here the peer's NAT has no state for us, everything we send it disappears
|
||||||
|
start := time.Now()
|
||||||
|
blackholed := 0
|
||||||
|
var recovered time.Duration
|
||||||
|
|
||||||
|
if rebind {
|
||||||
|
myControl.RebindUDPServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the tun busy the way someone retrying a stalled connection would
|
||||||
|
stop := make(chan struct{})
|
||||||
|
defer close(stop)
|
||||||
|
go func() {
|
||||||
|
tick := time.NewTicker(time.Millisecond * 200)
|
||||||
|
defer tick.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-tick.C:
|
||||||
|
myControl.InjectTunPacket(BuildTunUDPPacket(
|
||||||
|
theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("retry")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
r.RouteForAllExitFuncOrTimeout(time.Second*30, func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
||||||
|
if c == theirControl && p.From == myControl.GetUDPAddr() {
|
||||||
|
blackholed++
|
||||||
|
return router.Drop
|
||||||
|
}
|
||||||
|
|
||||||
|
// The peer reaching us directly is the moment its NAT opened, whether that is a punch or a handshake
|
||||||
|
if c == myControl && p.From == theirUdpAddr {
|
||||||
|
recovered = time.Since(start)
|
||||||
|
return router.RouteAndExit
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.KeepRouting
|
||||||
|
})
|
||||||
|
|
||||||
|
if recovered == 0 {
|
||||||
|
t.Fatalf("no recovery within 30s (%d packets blackholed)", blackholed)
|
||||||
|
}
|
||||||
|
return recovered, blackholed
|
||||||
|
}
|
||||||
+19
-2
@@ -153,6 +153,9 @@ const (
|
|||||||
ExitNow ExitType = 1
|
ExitNow ExitType = 1
|
||||||
// RouteAndExit routes this packet and exits immediately afterwards
|
// RouteAndExit routes this packet and exits immediately afterwards
|
||||||
RouteAndExit ExitType = 2
|
RouteAndExit ExitType = 2
|
||||||
|
// Drop discards this packet without delivering it and keeps routing. Use it to simulate a blackhole, such as
|
||||||
|
// a restrictive NAT refusing traffic from an address it has not seen.
|
||||||
|
Drop ExitType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
||||||
@@ -163,7 +166,9 @@ type ExitFunc func(packet *udp.Packet, receiver *nebula.Control) ExitType
|
|||||||
func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
if err := os.MkdirAll("mermaid", 0755); err != nil {
|
// t.Name() contains a slash for subtests, so the flow log can land in a nested directory
|
||||||
|
fn := filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name()))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fn), 0755); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +179,7 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
|
|||||||
outNat: make(map[outNatKey]netip.AddrPort),
|
outNat: make(map[outNatKey]netip.AddrPort),
|
||||||
flow: []flowEntry{},
|
flow: []flowEntry{},
|
||||||
ignoreFlows: []ignoreFlow{},
|
ignoreFlows: []ignoreFlow{},
|
||||||
fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())),
|
fn: fn,
|
||||||
t: t,
|
t: t,
|
||||||
cancelRender: cancel,
|
cancelRender: cancel,
|
||||||
}
|
}
|
||||||
@@ -687,6 +692,10 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
|
|||||||
p.Release()
|
p.Release()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case Drop:
|
||||||
|
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||||
|
r.unlockedInjectFlow(sender, receiver, p, false)
|
||||||
|
|
||||||
case KeepRouting:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
fp := r.unlockedInjectFlow(sender, receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
@@ -779,6 +788,10 @@ func (r *R) RouteForAllExitFuncOrTimeout(timeout time.Duration, whatDo ExitFunc)
|
|||||||
p.Release()
|
p.Release()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
case Drop:
|
||||||
|
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||||
|
r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
|
|
||||||
case KeepRouting:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
@@ -884,6 +897,10 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
|
|||||||
p.Release()
|
p.Release()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case Drop:
|
||||||
|
// Record it so the flow log shows the attempt, but never hand it to the receiver
|
||||||
|
r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
|
|
||||||
case KeepRouting:
|
case KeepRouting:
|
||||||
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
fp := r.unlockedInjectFlow(cm[x], receiver, p, false)
|
||||||
receiver.InjectUDPPacket(p)
|
receiver.InjectUDPPacket(p)
|
||||||
|
|||||||
+73
-11
@@ -238,18 +238,26 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HostInfo struct {
|
type HostInfo struct {
|
||||||
|
// The first cache line is everything the packet paths touch.
|
||||||
|
|
||||||
remote atomic.Pointer[netip.AddrPort]
|
remote atomic.Pointer[netip.AddrPort]
|
||||||
remotes *RemoteList
|
|
||||||
promoteCounter atomic.Uint32
|
|
||||||
ConnectionState *ConnectionState
|
ConnectionState *ConnectionState
|
||||||
|
|
||||||
|
// Traffic bits, pendingDeletion, and the rebind epoch we last sent under
|
||||||
|
state atomic.Uint32
|
||||||
|
|
||||||
|
promoteCounter atomic.Uint32
|
||||||
remoteIndexId uint32
|
remoteIndexId uint32
|
||||||
localIndexId uint32
|
localIndexId uint32
|
||||||
|
remotes *RemoteList
|
||||||
|
|
||||||
// vpnAddrs is a list of vpn addresses assigned to this host that are within our own vpn networks
|
// vpnAddrs is a list of vpn addresses assigned to this host that are within our own vpn networks
|
||||||
// The host may have other vpn addresses that are outside our
|
// The host may have other vpn addresses that are outside our
|
||||||
// vpn networks but were removed because they are not usable
|
// vpn networks but were removed because they are not usable
|
||||||
vpnAddrs []netip.Addr
|
vpnAddrs []netip.Addr
|
||||||
|
|
||||||
|
// Everything below is off the packet path: handshakes, relays, roaming and the connection manager.
|
||||||
|
|
||||||
// networks is a combination of specific vpn addresses (not prefixes!) and full unsafe networks assigned to this host.
|
// networks is a combination of specific vpn addresses (not prefixes!) and full unsafe networks assigned to this host.
|
||||||
networks *bart.Table[NetworkType]
|
networks *bart.Table[NetworkType]
|
||||||
relayState RelayState
|
relayState RelayState
|
||||||
@@ -262,11 +270,6 @@ type HostInfo struct {
|
|||||||
// This is used to limit lighthouse re-queries in chatty clients
|
// This is used to limit lighthouse re-queries in chatty clients
|
||||||
nextLHQuery atomic.Int64
|
nextLHQuery atomic.Int64
|
||||||
|
|
||||||
// lastRebindCount is the other side of Interface.rebindCount, if these values don't match then we need to ask LH
|
|
||||||
// for a punch from the remote end of this tunnel. The goal being to prime their conntrack for our traffic just like
|
|
||||||
// with a handshake
|
|
||||||
lastRebindCount int8
|
|
||||||
|
|
||||||
// lastHandshakeTime records the time the remote side told us about at the stage when the handshake was completed locally
|
// lastHandshakeTime records the time the remote side told us about at the stage when the handshake was completed locally
|
||||||
// Stage 1 packet will contain it if I am a responder, stage 2 packet if I am an initiator
|
// Stage 1 packet will contain it if I am a responder, stage 2 packet if I am an initiator
|
||||||
// This is used to avoid an attack where a handshake packet is replayed after some time
|
// This is used to avoid an attack where a handshake packet is replayed after some time
|
||||||
@@ -275,9 +278,6 @@ type HostInfo struct {
|
|||||||
lastRoam time.Time
|
lastRoam time.Time
|
||||||
lastRoamRemote netip.AddrPort
|
lastRoamRemote netip.AddrPort
|
||||||
|
|
||||||
//TODO: in, out, and others might benefit from being an atomic.Int32. We could collapse connectionManager pendingDeletion, relayUsed, and in/out into this 1 thing
|
|
||||||
in, out, pendingDeletion atomic.Bool
|
|
||||||
|
|
||||||
// lastUsed tracks the last time ConnectionManager checked the tunnel and it was in use.
|
// lastUsed tracks the last time ConnectionManager checked the tunnel and it was in use.
|
||||||
// This value will be behind against actual tunnel utilization in the hot path.
|
// This value will be behind against actual tunnel utilization in the hot path.
|
||||||
// This should only be used by the ConnectionManagers ticker routine.
|
// This should only be used by the ConnectionManagers ticker routine.
|
||||||
@@ -658,7 +658,7 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
|
|||||||
hm.Indexes[hostinfo.localIndexId] = hostinfo
|
hm.Indexes[hostinfo.localIndexId] = hostinfo
|
||||||
hm.RemoteIndexes[hostinfo.remoteIndexId] = hostinfo
|
hm.RemoteIndexes[hostinfo.remoteIndexId] = hostinfo
|
||||||
|
|
||||||
hostinfo.out.Store(true)
|
hostinfo.markOut(f.rebindEpoch.Load())
|
||||||
if f.connectionManager != nil { // f.connectionManager is only nil in some unit tests
|
if f.connectionManager != nil { // f.connectionManager is only nil in some unit tests
|
||||||
f.connectionManager.trafficTimer.Add(hostinfo.localIndexId, f.connectionManager.checkInterval)
|
f.connectionManager.trafficTimer.Add(hostinfo.localIndexId, f.connectionManager.checkInterval)
|
||||||
}
|
}
|
||||||
@@ -759,6 +759,68 @@ func (i *HostInfo) TryPromoteBest(preferredRanges []netip.Prefix, ifce *Interfac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bits within HostInfo.state, everything above stateEpochShift is the epoch
|
||||||
|
const (
|
||||||
|
stateIn uint32 = 1 << iota
|
||||||
|
stateOut
|
||||||
|
statePendingDeletion
|
||||||
|
|
||||||
|
stateFlags = stateIn | stateOut | statePendingDeletion
|
||||||
|
stateEpochShift = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// markIn records inbound traffic
|
||||||
|
func (i *HostInfo) markIn() {
|
||||||
|
if i.state.Load()&stateIn == 0 {
|
||||||
|
i.state.Or(stateIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// markOut records a send and reports whether the epoch moved, meaning we want a punch from the far side
|
||||||
|
func (i *HostInfo) markOut(epoch uint32) bool {
|
||||||
|
e := epoch << stateEpochShift
|
||||||
|
for {
|
||||||
|
old := i.state.Load()
|
||||||
|
if old&stateOut != 0 && old&^stateFlags == e {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.state.CompareAndSwap(old, old&stateFlags|stateOut|e) {
|
||||||
|
return old&^stateFlags != e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// markOutOnly records a send without consuming the rebind epoch, for paths that cannot act on a requery
|
||||||
|
func (i *HostInfo) markOutOnly() {
|
||||||
|
if i.state.Load()&stateOut == 0 {
|
||||||
|
i.state.Or(stateOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sentSinceCheck reports whether anything has been sent since the connection manager last looked
|
||||||
|
func (i *HostInfo) sentSinceCheck() bool {
|
||||||
|
return i.state.Load()&stateOut != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// takeTraffic clears both traffic bits, leaving the epoch alone, and reports what they were
|
||||||
|
func (i *HostInfo) takeTraffic() (in bool, out bool) {
|
||||||
|
old := i.state.And(^(stateIn | stateOut))
|
||||||
|
return old&stateIn != 0, old&stateOut != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *HostInfo) setPendingDeletion(v bool) {
|
||||||
|
if v {
|
||||||
|
i.state.Or(statePendingDeletion)
|
||||||
|
} else {
|
||||||
|
i.state.And(^statePendingDeletion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *HostInfo) isPendingDeletion() bool {
|
||||||
|
return i.state.Load()&statePendingDeletion != 0
|
||||||
|
}
|
||||||
|
|
||||||
func (i *HostInfo) GetCert() *cert.CachedCertificate {
|
func (i *HostInfo) GetCert() *cert.CachedCertificate {
|
||||||
if i.ConnectionState != nil {
|
if i.ConnectionState != nil {
|
||||||
return i.ConnectionState.peerCert
|
return i.ConnectionState.peerCert
|
||||||
|
|||||||
@@ -401,3 +401,43 @@ func TestHostMap_RelayState(t *testing.T) {
|
|||||||
assert.Equal(t, []netip.Addr{}, h1.relayState.relays)
|
assert.Equal(t, []netip.Addr{}, h1.relayState.relays)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHostInfo_markOut(t *testing.T) {
|
||||||
|
h := &HostInfo{}
|
||||||
|
h.markOut(5) // stamped when the tunnel was added
|
||||||
|
|
||||||
|
// A tunnel already on the current epoch has nothing to report, which is what keeps a fresh tunnel from
|
||||||
|
// requerying on its first packet
|
||||||
|
assert.False(t, h.markOut(5), "an unchanged epoch should not report a move")
|
||||||
|
assert.True(t, h.sentSinceCheck(), "the send is still recorded as traffic")
|
||||||
|
|
||||||
|
// A rebind is observed exactly once, so we requery once per rebind
|
||||||
|
assert.True(t, h.markOut(6), "a bumped epoch should report a move")
|
||||||
|
assert.False(t, h.markOut(6), "the epoch move should only be reported once")
|
||||||
|
|
||||||
|
// Traffic and pendingDeletion live in the same word and must survive an epoch change
|
||||||
|
h.setPendingDeletion(true)
|
||||||
|
h.markIn()
|
||||||
|
assert.True(t, h.markOut(7))
|
||||||
|
assert.True(t, h.isPendingDeletion(), "pendingDeletion must survive an epoch change")
|
||||||
|
in, out := h.takeTraffic()
|
||||||
|
assert.True(t, in, "inbound traffic must survive an epoch change")
|
||||||
|
assert.True(t, out)
|
||||||
|
|
||||||
|
// Clearing the traffic bits leaves the epoch alone, otherwise an idle tunnel would requery forever
|
||||||
|
assert.False(t, h.markOut(7), "takeTraffic must not disturb the epoch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A relayed send records traffic but must leave the rebind epoch for the direct path to consume, otherwise
|
||||||
|
// relaying to a host swallows the requery that gets the far side punching at our new address.
|
||||||
|
func TestHostInfo_markOutOnly(t *testing.T) {
|
||||||
|
h := &HostInfo{}
|
||||||
|
h.markOut(5)
|
||||||
|
|
||||||
|
h.markOutOnly()
|
||||||
|
assert.True(t, h.sentSinceCheck(), "a relayed send is still outbound traffic")
|
||||||
|
assert.False(t, h.markOut(5), "a relayed send must not disturb the epoch")
|
||||||
|
|
||||||
|
assert.True(t, h.markOut(6), "a relayed send must not consume the epoch edge")
|
||||||
|
assert.False(t, h.markOut(6))
|
||||||
|
}
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ func (f *Interface) SendVia(via *HostInfo,
|
|||||||
c := via.ConnectionState.messageCounter.Add(1)
|
c := via.ConnectionState.messageCounter.Add(1)
|
||||||
|
|
||||||
out = header.Encode(out, header.Version, header.Message, header.MessageRelay, relay.RemoteIndex, c)
|
out = header.Encode(out, header.Version, header.Message, header.MessageRelay, relay.RemoteIndex, c)
|
||||||
f.connectionManager.Out(via)
|
f.connectionManager.OutRelay(via)
|
||||||
|
|
||||||
// Authenticate the header and payload, but do not encrypt for this message type.
|
// Authenticate the header and payload, but do not encrypt for this message type.
|
||||||
// The payload consists of the inner, unencrypted Nebula header, as well as the end-to-end encrypted payload.
|
// The payload consists of the inner, unencrypted Nebula header, as well as the end-to-end encrypted payload.
|
||||||
@@ -365,17 +365,11 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
|||||||
|
|
||||||
//l.WithField("trace", string(debug.Stack())).Error("out Header ", &Header{Version, t, st, 0, hostinfo.remoteIndexId, c}, p)
|
//l.WithField("trace", string(debug.Stack())).Error("out Header ", &Header{Version, t, st, 0, hostinfo.remoteIndexId, c}, p)
|
||||||
out = header.Encode(out, header.Version, t, st, hostinfo.remoteIndexId, c)
|
out = header.Encode(out, header.Version, t, st, hostinfo.remoteIndexId, c)
|
||||||
f.connectionManager.Out(hostinfo)
|
// We rebound since this tunnel last sent, ask the lighthouse to get the far side punching at us again
|
||||||
|
if f.connectionManager.Out(hostinfo) && t != header.CloseTunnel {
|
||||||
// Query our LH if we haven't since the last time we've been rebound, this will cause the remote to punch against
|
|
||||||
// all our addrs and enable a faster roaming.
|
|
||||||
if t != header.CloseTunnel && hostinfo.lastRebindCount != f.rebindCount {
|
|
||||||
//NOTE: there is an update hole if a tunnel isn't used and exactly 256 rebinds occur before the tunnel is
|
|
||||||
// finally used again. This tunnel would eventually be torn down and recreated if this action didn't help.
|
|
||||||
f.lightHouse.QueryServer(hostinfo.vpnAddrs[0])
|
f.lightHouse.QueryServer(hostinfo.vpnAddrs[0])
|
||||||
hostinfo.lastRebindCount = f.rebindCount
|
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
f.l.Debug("Lighthouse update triggered for punch due to rebind counter",
|
f.l.Debug("Lighthouse update triggered for punch due to rebind epoch",
|
||||||
"vpnAddrs", hostinfo.vpnAddrs,
|
"vpnAddrs", hostinfo.vpnAddrs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-6
@@ -2,7 +2,6 @@ package nebula
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/fips140"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -83,8 +82,8 @@ type Interface struct {
|
|||||||
sendRecvErrorConfig recvErrorConfig
|
sendRecvErrorConfig recvErrorConfig
|
||||||
acceptRecvErrorConfig recvErrorConfig
|
acceptRecvErrorConfig recvErrorConfig
|
||||||
|
|
||||||
// rebindCount is used to decide if an active tunnel should trigger a punch notification through a lighthouse
|
// Bumped on every udp rebind, tunnels compare it to decide they need a punch from the far side
|
||||||
rebindCount int8
|
rebindEpoch atomic.Uint32
|
||||||
version string
|
version string
|
||||||
|
|
||||||
conntrackCacheTimeout time.Duration
|
conntrackCacheTimeout time.Duration
|
||||||
@@ -239,9 +238,6 @@ func (f *Interface) activate() error {
|
|||||||
"build", f.version,
|
"build", f.version,
|
||||||
"udpAddr", addr,
|
"udpAddr", addr,
|
||||||
"boringcrypto", boringEnabled(),
|
"boringcrypto", boringEnabled(),
|
||||||
"fips140Version", fips140.Version(),
|
|
||||||
"fips140Enabled", fips140.Enabled(),
|
|
||||||
"fips140Enforced", fips140.Enforced(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if f.routines > 1 {
|
if f.routines > 1 {
|
||||||
|
|||||||
+65
-4
@@ -4,16 +4,77 @@
|
|||||||
package noiseutil
|
package noiseutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/boring"
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
// unsafe needed for go:linkname
|
||||||
|
_ "unsafe"
|
||||||
|
|
||||||
"github.com/flynn/noise"
|
"github.com/flynn/noise"
|
||||||
)
|
)
|
||||||
|
|
||||||
var CipherAESGCM noise.CipherFunc = CipherAESGCMFIPS140
|
|
||||||
|
|
||||||
// EncryptLockNeeded indicates if calls to Encrypt need a lock
|
// EncryptLockNeeded indicates if calls to Encrypt need a lock
|
||||||
// This is true for boringcrypto because the Seal function verifies that the
|
// This is true for boringcrypto because the Seal function verifies that the
|
||||||
// nonce is strictly increasing.
|
// nonce is strictly increasing.
|
||||||
const EncryptLockNeeded = true
|
const EncryptLockNeeded = true
|
||||||
|
|
||||||
var boringEnabled = boring.Enabled()
|
// NewGCMTLS is no longer exposed in go1.19+, so we need to link it in
|
||||||
|
// See: https://github.com/golang/go/issues/56326
|
||||||
|
//
|
||||||
|
// NewGCMTLS is the internal method used with boringcrypto that provides a
|
||||||
|
// validated mode of AES-GCM which enforces the nonce is strictly
|
||||||
|
// monotonically increasing. This is the TLS 1.2 specification for nonce
|
||||||
|
// generation (which also matches the method used by the Noise Protocol)
|
||||||
|
//
|
||||||
|
// - https://github.com/golang/go/blob/go1.19/src/crypto/tls/cipher_suites.go#L520-L522
|
||||||
|
// - https://github.com/golang/go/blob/go1.19/src/crypto/internal/boring/aes.go#L235-L237
|
||||||
|
// - https://github.com/golang/go/blob/go1.19/src/crypto/internal/boring/aes.go#L250
|
||||||
|
// - https://github.com/google/boringssl/blob/ae223d6138807a13006342edfeef32e813246b39/include/openssl/aead.h#L379-L381
|
||||||
|
// - https://github.com/google/boringssl/blob/ae223d6138807a13006342edfeef32e813246b39/crypto/fipsmodule/cipher/e_aes.c#L1082-L1093
|
||||||
|
//
|
||||||
|
//go:linkname newGCMTLS crypto/internal/boring.NewGCMTLS
|
||||||
|
func newGCMTLS(c cipher.Block) (cipher.AEAD, error)
|
||||||
|
|
||||||
|
type cipherFn struct {
|
||||||
|
fn func([32]byte) noise.Cipher
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cipherFn) Cipher(k [32]byte) noise.Cipher { return c.fn(k) }
|
||||||
|
func (c cipherFn) CipherName() string { return c.name }
|
||||||
|
|
||||||
|
// CipherAESGCM is the AES256-GCM AEAD cipher (using NewGCMTLS when GoBoring is present)
|
||||||
|
var CipherAESGCM noise.CipherFunc = cipherFn{cipherAESGCMBoring, "AESGCM"}
|
||||||
|
|
||||||
|
func cipherAESGCMBoring(k [32]byte) noise.Cipher {
|
||||||
|
c, err := aes.NewCipher(k[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gcm, err := newGCMTLS(c)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return aeadCipher{
|
||||||
|
gcm,
|
||||||
|
func(n uint64) []byte {
|
||||||
|
var nonce [12]byte
|
||||||
|
binary.BigEndian.PutUint64(nonce[4:], n)
|
||||||
|
return nonce[:]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type aeadCipher struct {
|
||||||
|
cipher.AEAD
|
||||||
|
nonce func(uint64) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c aeadCipher) Encrypt(out []byte, n uint64, ad, plaintext []byte) []byte {
|
||||||
|
return c.Seal(out, c.nonce(n), plaintext, ad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c aeadCipher) Decrypt(out []byte, n uint64, ad, ciphertext []byte) ([]byte, error) {
|
||||||
|
return c.Open(out, c.nonce(n), ciphertext, ad)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
package noiseutil
|
package noiseutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/boring"
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -12,3 +14,33 @@ import (
|
|||||||
func TestEncryptLockNeeded(t *testing.T) {
|
func TestEncryptLockNeeded(t *testing.T) {
|
||||||
assert.True(t, EncryptLockNeeded)
|
assert.True(t, EncryptLockNeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure NewGCMTLS validates the nonce is non-repeating
|
||||||
|
func TestNewGCMTLS(t *testing.T) {
|
||||||
|
assert.True(t, boring.Enabled())
|
||||||
|
|
||||||
|
// Test Case 16 from GCM Spec:
|
||||||
|
// - (now dead link): http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf
|
||||||
|
// - as listed in boringssl tests: https://github.com/google/boringssl/blob/fips-20220613/crypto/cipher_extra/test/cipher_tests.txt#L412-L418
|
||||||
|
key, _ := hex.DecodeString("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308")
|
||||||
|
iv, _ := hex.DecodeString("cafebabefacedbaddecaf888")
|
||||||
|
plaintext, _ := hex.DecodeString("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39")
|
||||||
|
aad, _ := hex.DecodeString("feedfacedeadbeeffeedfacedeadbeefabaddad2")
|
||||||
|
expected, _ := hex.DecodeString("522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662")
|
||||||
|
expectedTag, _ := hex.DecodeString("76fc6ece0f4e1768cddf8853bb2d551b")
|
||||||
|
|
||||||
|
expected = append(expected, expectedTag...)
|
||||||
|
|
||||||
|
var keyArray [32]byte
|
||||||
|
copy(keyArray[:], key)
|
||||||
|
c := CipherAESGCM.Cipher(keyArray)
|
||||||
|
aead := c.(aeadCipher).AEAD
|
||||||
|
|
||||||
|
dst := aead.Seal([]byte{}, iv, plaintext, aad)
|
||||||
|
assert.Equal(t, expected, dst)
|
||||||
|
|
||||||
|
// We expect this to fail since we are re-encrypting with a repeat IV
|
||||||
|
assert.PanicsWithError(t, "boringcrypto: EVP_AEAD_CTX_seal failed", func() {
|
||||||
|
dst = aead.Seal([]byte{}, iv, plaintext, aad)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,11 +29,8 @@ type CipherState interface {
|
|||||||
// NewCipherState wraps the post-handshake noise.CipherState in the per-cipher type that matches cipherFunc.
|
// NewCipherState wraps the post-handshake noise.CipherState in the per-cipher type that matches cipherFunc.
|
||||||
// cipherFunc must be the same cipher used to build the noise CipherSuite that produced s.
|
// cipherFunc must be the same cipher used to build the noise CipherSuite that produced s.
|
||||||
func NewCipherState(s *noise.CipherState, cipherFunc noise.CipherFunc) CipherState {
|
func NewCipherState(s *noise.CipherState, cipherFunc noise.CipherFunc) CipherState {
|
||||||
if cs, ok := s.Cipher().(CipherState); ok {
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
switch cipherFunc.CipherName() {
|
switch cipherFunc.CipherName() {
|
||||||
case noise.CipherAESGCM.CipherName():
|
case CipherAESGCM.CipherName():
|
||||||
return NewCipherStateAESGCM(s)
|
return NewCipherStateAESGCM(s)
|
||||||
case noise.CipherChaChaPoly.CipherName():
|
case noise.CipherChaChaPoly.CipherName():
|
||||||
return NewCipherStateChaChaPoly(s)
|
return NewCipherStateChaChaPoly(s)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package noiseutil
|
package noiseutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/fips140"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/flynn/noise"
|
"github.com/flynn/noise"
|
||||||
@@ -11,30 +10,24 @@ import (
|
|||||||
|
|
||||||
func TestCipherStateAESGCMRoundtrip(t *testing.T) {
|
func TestCipherStateAESGCMRoundtrip(t *testing.T) {
|
||||||
enc, dec := buildCipherStates(t, CipherAESGCM)
|
enc, dec := buildCipherStates(t, CipherAESGCM)
|
||||||
roundtrip(t, NewCipherState(enc, CipherAESGCM), NewCipherState(dec, CipherAESGCM))
|
roundtrip(t, NewCipherStateAESGCM(enc), NewCipherStateAESGCM(dec))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCipherStateChaChaPolyRoundtrip(t *testing.T) {
|
func TestCipherStateChaChaPolyRoundtrip(t *testing.T) {
|
||||||
enc, dec := buildCipherStates(t, noise.CipherChaChaPoly)
|
enc, dec := buildCipherStates(t, noise.CipherChaChaPoly)
|
||||||
roundtrip(t, NewCipherState(enc, noise.CipherChaChaPoly), NewCipherState(dec, noise.CipherChaChaPoly))
|
roundtrip(t, NewCipherStateChaChaPoly(enc), NewCipherStateChaChaPoly(dec))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCipherStateDispatch(t *testing.T) {
|
func TestNewCipherStateDispatch(t *testing.T) {
|
||||||
encA, _ := buildCipherStates(t, CipherAESGCM)
|
encA, _ := buildCipherStates(t, CipherAESGCM)
|
||||||
encC, _ := buildCipherStates(t, noise.CipherChaChaPoly)
|
encC, _ := buildCipherStates(t, noise.CipherChaChaPoly)
|
||||||
|
|
||||||
if !boringEnabled && !fips140.Enabled() {
|
|
||||||
assert.IsType(t, &CipherStateAESGCM{}, NewCipherState(encA, CipherAESGCM))
|
assert.IsType(t, &CipherStateAESGCM{}, NewCipherState(encA, CipherAESGCM))
|
||||||
} else {
|
|
||||||
// fips140
|
|
||||||
assert.IsType(t, encA.Cipher(), NewCipherState(encA, CipherAESGCM))
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.IsType(t, &CipherStateChaChaPoly{}, NewCipherState(encC, noise.CipherChaChaPoly))
|
assert.IsType(t, &CipherStateChaChaPoly{}, NewCipherState(encC, noise.CipherChaChaPoly))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCipherStateUnsupportedPanics(t *testing.T) {
|
func TestNewCipherStateUnsupportedPanics(t *testing.T) {
|
||||||
enc, _ := buildCipherStates(t, noise.CipherChaChaPoly)
|
enc, _ := buildCipherStates(t, CipherAESGCM)
|
||||||
assert.Panics(t, func() {
|
assert.Panics(t, func() {
|
||||||
NewCipherState(enc, fakeCipher{})
|
NewCipherState(enc, fakeCipher{})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
package noiseutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/fips140"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
// unsafe needed for go:linkname
|
|
||||||
_ "crypto/tls"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/flynn/noise"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Use NewGCMWithCounterNonce or NewGCMForQUIC once available:
|
|
||||||
// - https://github.com/golang/go/issues/73110
|
|
||||||
// - https://github.com/golang/go/issues/79219
|
|
||||||
// Using tls.aeadAESGCMTLS13 gives us the TLS 1.3 GCM, which also verifies
|
|
||||||
// that the nonce is strictly increasing. This works for both boringcrypto
|
|
||||||
// and fips140.
|
|
||||||
//
|
|
||||||
//go:linkname aeadAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
|
|
||||||
func aeadAESGCMTLS13(key, noncePrefix []byte) cipher.AEAD
|
|
||||||
|
|
||||||
type cipherFn struct {
|
|
||||||
fn func([32]byte) noise.Cipher
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cipherFn) Cipher(k [32]byte) noise.Cipher { return c.fn(k) }
|
|
||||||
func (c cipherFn) CipherName() string { return c.name }
|
|
||||||
|
|
||||||
// CipherAESGCMFIPS140 is the AES256-GCM AEAD cipher (using tls.aeadAESGCMTLS13, for both boringcrypto and fips140)
|
|
||||||
var CipherAESGCMFIPS140 noise.CipherFunc = cipherFn{cipherAESGCMFIPS140, "AESGCM"}
|
|
||||||
|
|
||||||
// tls.aeadAESGCMTLS13 uses a 4 byte static prefix and an 8 byte XOR mask
|
|
||||||
var emptyNonce = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
|
||||||
|
|
||||||
func cipherAESGCMFIPS140(k [32]byte) noise.Cipher {
|
|
||||||
gcm := aeadAESGCMTLS13(k[:], emptyNonce)
|
|
||||||
gcm = extractFIPSAEAD(gcm)
|
|
||||||
return &aeadGCMFIPS140Cipher{
|
|
||||||
AEAD: gcm,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type aeadGCMFIPS140Cipher struct {
|
|
||||||
cipher.AEAD
|
|
||||||
ready bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the internal FIPS GCM implementation from the tls wrapper. The TLS
|
|
||||||
// wrapper is not thread safe around Open, so instead of locking around it we
|
|
||||||
// can grab the internal implementation that is thread safe. This is the FIPS
|
|
||||||
// module implementation: `crypto/internal/fips140/aes/gcm.GCMWithXORCounterNonce`
|
|
||||||
//
|
|
||||||
// - https://github.com/golang/go/blob/go1.26.4/src/crypto/internal/fips140/aes/gcm/gcm_nonces.go#L212-L287
|
|
||||||
//
|
|
||||||
// The wrapper is struct `crypto/tls.xorNonceAEAD` , with field `aead`:
|
|
||||||
//
|
|
||||||
// - https://github.com/golang/go/blob/go1.26.4/src/crypto/tls/cipher_suites.go#L482-L487
|
|
||||||
//
|
|
||||||
// This can be cleaned up once these FIPS implementations are exposed directly:
|
|
||||||
//
|
|
||||||
// - https://github.com/golang/go/issues/73110
|
|
||||||
func extractFIPSAEAD(xorNonceAEAD cipher.AEAD) cipher.AEAD {
|
|
||||||
r := reflect.ValueOf(xorNonceAEAD)
|
|
||||||
v := r.Elem().FieldByName("aead")
|
|
||||||
if !v.IsValid() {
|
|
||||||
// The internal crypto/tls.xorNonceAEAD struct no longer has an `aead`
|
|
||||||
// field. This can only happen on a Go version this code was not built
|
|
||||||
// against; the package init() self-test guards against ever reaching
|
|
||||||
// this at runtime, so this is a defensive fail-fast.
|
|
||||||
panic(fmt.Sprintf("noiseutil: could not extract FIPS AEAD from %T on %s: no `aead` field (incompatible Go version)", xorNonceAEAD, runtime.Version()))
|
|
||||||
}
|
|
||||||
v2 := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
|
|
||||||
aead, ok := v2.Interface().(cipher.AEAD)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("noiseutil: extracted FIPS `aead` field is %s, not a cipher.AEAD, on %s (incompatible Go version)", v2.Type(), runtime.Version()))
|
|
||||||
}
|
|
||||||
return aead
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *aeadGCMFIPS140Cipher) init(nonce []byte) {
|
|
||||||
// GCMWithXORCounterNonce expects that the first call to Seal
|
|
||||||
// is with a counter of `0`, this is how it extracts the nonce mask.
|
|
||||||
// We can clean this up in the future when NewGCMWithCounterNonce or
|
|
||||||
// NewGCMForQUIC are available:
|
|
||||||
if !bytes.Equal(emptyNonce, nonce) {
|
|
||||||
c.AEAD.Seal([]byte{}, emptyNonce, []byte{}, []byte{})
|
|
||||||
}
|
|
||||||
c.ready = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *aeadGCMFIPS140Cipher) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
|
||||||
if !c.ready {
|
|
||||||
c.init(nonce)
|
|
||||||
}
|
|
||||||
return c.AEAD.Seal(dst, nonce, plaintext, additionalData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *aeadGCMFIPS140Cipher) Encrypt(out []byte, n uint64, ad, plaintext []byte) []byte {
|
|
||||||
return c.Seal(out, aeadGCMFIPS140CipherNonce(n), plaintext, ad)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *aeadGCMFIPS140Cipher) Decrypt(out []byte, n uint64, ad, ciphertext []byte) ([]byte, error) {
|
|
||||||
return c.Open(out, aeadGCMFIPS140CipherNonce(n), ciphertext, ad)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *aeadGCMFIPS140Cipher) EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error) {
|
|
||||||
binary.BigEndian.PutUint64(nb[4:], n)
|
|
||||||
out = c.Seal(out, nb, plaintext, ad)
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *aeadGCMFIPS140Cipher) DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error) {
|
|
||||||
binary.BigEndian.PutUint64(nb[4:], n)
|
|
||||||
return c.Open(out, nb, ciphertext, ad)
|
|
||||||
}
|
|
||||||
|
|
||||||
func aeadGCMFIPS140CipherNonce(n uint64) []byte {
|
|
||||||
// GCMWithXORCounterNonce uses a 4 byte static prefix and an 8 byte nonce
|
|
||||||
var nonce [12]byte
|
|
||||||
binary.BigEndian.PutUint64(nonce[4:], n)
|
|
||||||
return nonce[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// init validates the go:linkname + reflection extraction and the nonce-reuse
|
|
||||||
// protection at startup, in every build. cipherAESGCMFIPS140 relies on unexported
|
|
||||||
// crypto/tls and crypto/internal/fips140 internals; if a future Go version changes
|
|
||||||
// those, this fails fast with a clear message instead of panicking per-handshake
|
|
||||||
// (or, worse, silently losing the strictly-increasing nonce check that is the whole
|
|
||||||
// point of using this cipher). Because this file has no build tag, this self-test
|
|
||||||
// runs even in non-FIPS builds, so the default CI lane catches an incompatible Go.
|
|
||||||
func init() {
|
|
||||||
var key [32]byte
|
|
||||||
c := cipherAESGCMFIPS140(key)
|
|
||||||
|
|
||||||
// Verify the extracted AEAD produces a working encrypt/decrypt roundtrip.
|
|
||||||
plaintext := []byte("nebula fips140 self-test")
|
|
||||||
ad := []byte("ad")
|
|
||||||
ct := c.Encrypt(nil, 1, ad, plaintext)
|
|
||||||
pt, err := c.Decrypt(nil, 1, ad, ct)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test roundtrip failed on %s: %v", runtime.Version(), err))
|
|
||||||
}
|
|
||||||
if !bytes.Equal(pt, plaintext) {
|
|
||||||
panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test roundtrip returned wrong plaintext on %s", runtime.Version()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the nonce-reuse protection still fires: re-encrypting with the same
|
|
||||||
// counter must panic. This is the guarantee we depend on for nonce safety, so
|
|
||||||
// if the extraction ever silently yields an AEAD without it, refuse to start.
|
|
||||||
// The strictly-increasing nonce check only exists under boringcrypto/fips140;
|
|
||||||
// in a plain build aeadAESGCMTLS13 wraps a standard GCM that does not enforce
|
|
||||||
// it (and CipherAESGCMFIPS140 is unused there anyway), so only assert it when
|
|
||||||
// one of those modes is active.
|
|
||||||
if (boringEnabled || fips140.Enabled()) && !reusePanics(c) {
|
|
||||||
panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test did not reject a reused nonce on %s; nonce-reuse protection is missing (incompatible Go version)", runtime.Version()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reusePanics reports whether re-encrypting with an already-used counter panics,
|
|
||||||
// as GCMWithXORCounterNonce is expected to.
|
|
||||||
func reusePanics(c noise.Cipher) (panicked bool) {
|
|
||||||
c.Encrypt(nil, 2, nil, nil)
|
|
||||||
defer func() {
|
|
||||||
if recover() != nil {
|
|
||||||
panicked = true
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
c.Encrypt(nil, 2, nil, nil)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package noiseutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/fips140"
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure NewAESGCM validates the nonce is non-repeating
|
|
||||||
func TestNewAESGCM(t *testing.T) {
|
|
||||||
if !boringEnabled && !fips140.Enabled() {
|
|
||||||
t.Skip("TestNewAESGCM is only for fips140/boringcrypto")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, _ := hex.DecodeString("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308")
|
|
||||||
iv, _ := hex.DecodeString("00000000facedbaddecaf888")
|
|
||||||
plaintext, _ := hex.DecodeString("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39")
|
|
||||||
aad, _ := hex.DecodeString("feedfacedeadbeeffeedfacedeadbeefabaddad2")
|
|
||||||
expected, _ := hex.DecodeString("6a65c2edd45bd63c7e29f40e3d2ed8ba2b99f4c83135383d5676652f255059ceb24863ff10afb1089db701245da87fb88d3acd5f9dd0770cac220c3c04145caf25e190aeb775e7080401c628")
|
|
||||||
|
|
||||||
var keyArray [32]byte
|
|
||||||
copy(keyArray[:], key)
|
|
||||||
c := CipherAESGCM.Cipher(keyArray)
|
|
||||||
aead := c.(cipher.AEAD)
|
|
||||||
|
|
||||||
dst := aead.Seal([]byte{}, iv, plaintext, aad)
|
|
||||||
t.Logf("%x", dst)
|
|
||||||
assert.Equal(t, expected, dst)
|
|
||||||
|
|
||||||
// We expect this to fail since we are re-encrypting with a repeat IV
|
|
||||||
switch {
|
|
||||||
case boringEnabled:
|
|
||||||
assert.PanicsWithError(t, "boringcrypto: EVP_AEAD_CTX_seal failed", func() {
|
|
||||||
dst = aead.Seal([]byte{}, iv, plaintext, aad)
|
|
||||||
})
|
|
||||||
case fips140.Version() == "v1.0.0":
|
|
||||||
assert.PanicsWithValue(t, "crypto/cipher: counter decreased", func() {
|
|
||||||
dst = aead.Seal([]byte{}, iv, plaintext, aad)
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
assert.PanicsWithValue(t, "crypto/cipher: counter decreased or remained the same", func() {
|
|
||||||
dst = aead.Seal([]byte{}, iv, plaintext, aad)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//go:build fips140enforce
|
|
||||||
|
|
||||||
package noiseutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/fips140"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if !fips140.Enforced() {
|
|
||||||
panic("Nebula compiled with fips140 expects FIPS140 to be enforced. Do not set GODEBUG=fips140, or if you do it must be set as GODEBUG=fips140=only")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-15
@@ -1,25 +1,14 @@
|
|||||||
//go:build !boringcrypto
|
//go:build !boringcrypto
|
||||||
|
// +build !boringcrypto
|
||||||
|
|
||||||
package noiseutil
|
package noiseutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/fips140"
|
|
||||||
|
|
||||||
"github.com/flynn/noise"
|
"github.com/flynn/noise"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncryptLockNeeded indicates if calls to Encrypt need a lock
|
// EncryptLockNeeded indicates if calls to Encrypt need a lock
|
||||||
var EncryptLockNeeded = fips140.Enabled()
|
const EncryptLockNeeded = false
|
||||||
|
|
||||||
var CipherAESGCM noise.CipherFunc = initAESGCM()
|
// CipherAESGCM is the standard noise.CipherAESGCM when boringcrypto is not enabled
|
||||||
|
var CipherAESGCM noise.CipherFunc = noise.CipherAESGCM
|
||||||
func initAESGCM() noise.CipherFunc {
|
|
||||||
if fips140.Enabled() {
|
|
||||||
return CipherAESGCMFIPS140
|
|
||||||
} else {
|
|
||||||
return noise.CipherAESGCM
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var boringEnabled = false
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !boringcrypto
|
||||||
|
// +build !boringcrypto
|
||||||
|
|
||||||
|
package noiseutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncryptLockNeeded(t *testing.T) {
|
||||||
|
assert.False(t, EncryptLockNeeded)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/fips140"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -238,17 +237,10 @@ func (cs *CertState) getCertificate(v cert.Version) cert.Certificate {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCipherSuite builds the noise.CipherSuite for the given curve and cipher.
|
func newCipherSuite(curve cert.Curve, pkcs11backed bool, cipher string) (noise.CipherSuite, error) {
|
||||||
// When fips140Enforced is true (FIPS 140-only mode), non-approved algorithms
|
|
||||||
// (Curve25519 and ChaChaPoly) are rejected with an error. Callers pass
|
|
||||||
// fips140.Enforced() for fips140Enforced.
|
|
||||||
func newCipherSuite(curve cert.Curve, pkcs11backed bool, cipher string, fips140Enforced bool) (noise.CipherSuite, error) {
|
|
||||||
var dhFunc noise.DHFunc
|
var dhFunc noise.DHFunc
|
||||||
switch curve {
|
switch curve {
|
||||||
case cert.Curve_CURVE25519:
|
case cert.Curve_CURVE25519:
|
||||||
if fips140Enforced {
|
|
||||||
return nil, errors.New("pki: use of Curve25519 is not allowed in FIPS 140-only mode")
|
|
||||||
}
|
|
||||||
dhFunc = noise.DH25519
|
dhFunc = noise.DH25519
|
||||||
case cert.Curve_P256:
|
case cert.Curve_P256:
|
||||||
if pkcs11backed {
|
if pkcs11backed {
|
||||||
@@ -261,9 +253,6 @@ func newCipherSuite(curve cert.Curve, pkcs11backed bool, cipher string, fips140E
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cipher == "chachapoly" {
|
if cipher == "chachapoly" {
|
||||||
if fips140Enforced {
|
|
||||||
return nil, errors.New("pki: use of ChaChaPoly is not allowed in FIPS 140-only mode")
|
|
||||||
}
|
|
||||||
return noise.NewCipherSuite(dhFunc, noise.CipherChaChaPoly, noise.HashSHA256), nil
|
return noise.NewCipherSuite(dhFunc, noise.CipherChaChaPoly, noise.HashSHA256), nil
|
||||||
}
|
}
|
||||||
return noise.NewCipherSuite(dhFunc, noiseutil.CipherAESGCM, noise.HashSHA256), nil
|
return noise.NewCipherSuite(dhFunc, noiseutil.CipherAESGCM, noise.HashSHA256), nil
|
||||||
@@ -337,10 +326,6 @@ func newCertStateFromConfig(c *config.C, cipher string) (*CertState, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if fips140.Enforced() && crt.Curve() != cert.Curve_P256 {
|
|
||||||
return nil, fmt.Errorf("pki: use of %s is not allowed in FIPS 140-only mode", crt.Curve())
|
|
||||||
}
|
|
||||||
|
|
||||||
switch crt.Version() {
|
switch crt.Version() {
|
||||||
case cert.Version1:
|
case cert.Version1:
|
||||||
if v1 != nil {
|
if v1 != nil {
|
||||||
@@ -420,7 +405,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
|
|||||||
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
|
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
|
||||||
} else {
|
} else {
|
||||||
if err := v1.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
|
if err := v1.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
|
||||||
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert: %w", err)
|
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +413,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error marshalling v1 certificate for handshake: %w", err)
|
return nil, fmt.Errorf("error marshalling v1 certificate for handshake: %w", err)
|
||||||
}
|
}
|
||||||
ncs, err := newCipherSuite(v1.Curve(), pkcs11backed, cipher, fips140.Enforced())
|
ncs, err := newCipherSuite(v1.Curve(), pkcs11backed, cipher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -445,7 +430,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
|
|||||||
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
|
//NOTE: We do not currently have a method to verify a public private key pair when the private key is in an hsm
|
||||||
} else {
|
} else {
|
||||||
if err := v2.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
|
if err := v2.VerifyPrivateKey(privateKeyCurve, privateKey); err != nil {
|
||||||
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert: %w", err)
|
return nil, fmt.Errorf("private key is not a pair with public key in nebula cert")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +438,7 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error marshalling v2 certificate for handshake: %w", err)
|
return nil, fmt.Errorf("error marshalling v2 certificate for handshake: %w", err)
|
||||||
}
|
}
|
||||||
ncs, err := newCipherSuite(v2.Curve(), pkcs11backed, cipher, fips140.Enforced())
|
ncs, err := newCipherSuite(v2.Curve(), pkcs11backed, cipher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
-94
@@ -1,94 +0,0 @@
|
|||||||
package nebula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/slackhq/nebula/cert"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewCipherSuite(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
curve cert.Curve
|
|
||||||
cipher string
|
|
||||||
fips140Enforced bool
|
|
||||||
wantErr string
|
|
||||||
// wantName is the full expected CipherSuite name (<DH>_<Cipher>_<Hash>),
|
|
||||||
// only checked when wantErr is empty. Asserting the whole name makes both
|
|
||||||
// the curve and cipher selection load-bearing.
|
|
||||||
wantName string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "curve25519 aesgcm, not enforced",
|
|
||||||
curve: cert.Curve_CURVE25519,
|
|
||||||
cipher: "aesgcm",
|
|
||||||
wantName: "25519_AESGCM_SHA256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "curve25519 chachapoly, not enforced",
|
|
||||||
curve: cert.Curve_CURVE25519,
|
|
||||||
cipher: "chachapoly",
|
|
||||||
wantName: "25519_ChaChaPoly_SHA256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "p256 aesgcm, not enforced",
|
|
||||||
curve: cert.Curve_P256,
|
|
||||||
cipher: "aesgcm",
|
|
||||||
wantName: "P256_AESGCM_SHA256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "p256 aesgcm, enforced is allowed",
|
|
||||||
curve: cert.Curve_P256,
|
|
||||||
cipher: "aesgcm",
|
|
||||||
fips140Enforced: true,
|
|
||||||
wantName: "P256_AESGCM_SHA256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "curve25519 rejected when enforced",
|
|
||||||
curve: cert.Curve_CURVE25519,
|
|
||||||
cipher: "aesgcm",
|
|
||||||
fips140Enforced: true,
|
|
||||||
wantErr: "pki: use of Curve25519 is not allowed in FIPS 140-only mode",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "chachapoly rejected when enforced",
|
|
||||||
curve: cert.Curve_P256,
|
|
||||||
cipher: "chachapoly",
|
|
||||||
fips140Enforced: true,
|
|
||||||
wantErr: "pki: use of ChaChaPoly is not allowed in FIPS 140-only mode",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Curve is checked before cipher, so a Curve25519+ChaChaPoly
|
|
||||||
// request reports the Curve25519 rejection.
|
|
||||||
name: "curve25519 chachapoly rejected on curve when enforced",
|
|
||||||
curve: cert.Curve_CURVE25519,
|
|
||||||
cipher: "chachapoly",
|
|
||||||
fips140Enforced: true,
|
|
||||||
wantErr: "pki: use of Curve25519 is not allowed in FIPS 140-only mode",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
cs, err := newCipherSuite(tt.curve, false, tt.cipher, tt.fips140Enforced)
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
require.EqualError(t, err, tt.wantErr)
|
|
||||||
assert.Nil(t, cs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, cs)
|
|
||||||
assert.Equal(t, tt.wantName, string(cs.Name()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewCipherSuiteUnsupportedCurve(t *testing.T) {
|
|
||||||
cs, err := newCipherSuite(cert.Curve(99), false, "aesgcm", false)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.True(t, strings.HasPrefix(err.Error(), "unsupported curve:"), "got: %v", err)
|
|
||||||
assert.Nil(t, cs)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package nebula
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/fips140"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -292,9 +291,6 @@ func (s *statsServer) buildRuntime(cfg statsConfig) ([]func(), *http.Server) {
|
|||||||
"version": s.buildVersion,
|
"version": s.buildVersion,
|
||||||
"goversion": runtime.Version(),
|
"goversion": runtime.Version(),
|
||||||
"boringcrypto": strconv.FormatBool(boringEnabled()),
|
"boringcrypto": strconv.FormatBool(boringEnabled()),
|
||||||
"fips140Version": fips140.Version(),
|
|
||||||
"fips140Enabled": strconv.FormatBool(fips140.Enabled()),
|
|
||||||
"fips140Enforced": strconv.FormatBool(fips140.Enforced()),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
pr.MustRegister(g)
|
pr.MustRegister(g)
|
||||||
|
|||||||
Reference in New Issue
Block a user