diff --git a/.github/workflows/controlplane.yaml b/.github/workflows/controlplane.yaml index 60663acd1..5241de3de 100644 --- a/.github/workflows/controlplane.yaml +++ b/.github/workflows/controlplane.yaml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 with: cache: false - run: mise run ${{ matrix.mise_task }} @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 - name: "Build controlplane" run: | @@ -75,7 +75,6 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 - - name: Get Go version from go.mod id: go-version run: echo "version=$(awk '/^go /{print $2}' go.mod)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/naisdevice.yaml b/.github/workflows/naisdevice.yaml index 3db85c1ae..6b5dd3e51 100644 --- a/.github/workflows/naisdevice.yaml +++ b/.github/workflows/naisdevice.yaml @@ -2,7 +2,7 @@ name: Naisdevice on: pull_request: - types: [opened, reopened, synchronize] + types: [opened, reopened, synchronize, labeled] push: branches: [main] paths: @@ -30,8 +30,9 @@ on: - "pkg/pb/**" env: + PRE_RELEASE: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'pre-release') && 'true' || 'false' }} # some mise tasks use this to determine how they package/sign stuff. - RELEASE: ${{ (github.ref == 'refs/heads/main' && github.actor != 'dependabot[bot]') && 'true' || 'false' }} + RELEASE: ${{ ((github.ref == 'refs/heads/main' && github.actor != 'dependabot[bot]') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'pre-release'))) && 'true' || 'false' }} concurrency: group: ${{ github.ref }} @@ -50,14 +51,18 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 with: fetch-depth: 0 - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 - id: generate run: mise run ci:release-info env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRE_RELEASE: ${{ env.PRE_RELEASE }} + PR_NUMBER: ${{ github.event.pull_request.number }} outputs: version: ${{ steps.generate.outputs.version }} changelog: ${{ steps.generate.outputs.changelog }} + pre_release: ${{ env.PRE_RELEASE }} checks: strategy: @@ -76,7 +81,7 @@ jobs: contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 - run: mise run ${{ matrix.mise_task }} builds: @@ -109,7 +114,7 @@ jobs: OUTFILE: ./release_artifacts/naisdevice${{ matrix.gotags == 'tenant' && '-tenant' || '' }}_${{ matrix.platform.os }}_${{ matrix.arch }}.${{ matrix.platform.ext }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 - if: matrix.platform.os == 'windows' run: sudo apt-get update && sudo apt-get install --yes nsis osslsigncode - if: matrix.platform.os == 'macos' @@ -129,34 +134,83 @@ jobs: run: | mkdir -p "$(dirname $OUTFILE)" mise run "package:${{ matrix.platform.os }}" - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v5 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # ratchet:actions/upload-artifact@v7 with: name: installer-${{ matrix.platform.os }}-${{ matrix.arch }}-${{ matrix.gotags || 'nav' }} path: ${{ env.OUTFILE }} + smoke-tests: + name: smoke test ${{ matrix.artifact }} + needs: [builds] + strategy: + fail-fast: false + matrix: + include: + - os: macos + runner: macos-latest + artifact: installer-macos-arm64-nav + installer_glob: "*.pkg" + - os: macos + runner: macos-latest + artifact: installer-macos-arm64-tenant + installer_glob: "*.pkg" + - os: linux + runner: ubuntu-latest + artifact: installer-linux-amd64-nav + installer_glob: "*.deb" + - os: linux + runner: ubuntu-latest + artifact: installer-linux-amd64-tenant + installer_glob: "*.deb" + - os: windows + runner: windows-latest + artifact: installer-windows-amd64-nav + installer_glob: "*.exe" + - os: windows + runner: windows-latest + artifact: installer-windows-amd64-tenant + installer_glob: "*.exe" + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8 + with: + name: ${{ matrix.artifact }} + path: ./downloaded-artifact/ + - name: run smoke test + shell: bash + run: mise run smoke-test:${{ matrix.os }} ./downloaded-artifact/${{ matrix.installer_glob }} + # Used by GitHub to determine if all checks/builds have passed branch-protection-checkpoint: - needs: [checks, builds] + needs: [checks, builds, smoke-tests] if: ${{ always() }} runs-on: ubuntu-latest steps: - - if: ${{ needs.checks.result != 'success' || needs.builds.result != 'success' }} + - if: ${{ needs.checks.result != 'success' || needs.builds.result != 'success' || needs.smoke-tests.result != 'success' }} run: exit 1 - - run: echo "All checks and builds passed." + - run: echo "All checks, builds, and smoke tests passed." release-github: - if: github.ref == 'refs/heads/main' && github.actor != 'dependabot[bot]' && needs.release-info.outputs.changelog != '' && needs.release-info.outputs.version != '' + if: >- + needs.release-info.outputs.changelog != '' && needs.release-info.outputs.version != '' && + ( + (github.ref == 'refs/heads/main' && github.actor != 'dependabot[bot]') || + needs.release-info.outputs.pre_release == 'true' + ) needs: [release-info, branch-protection-checkpoint] runs-on: ubuntu-latest permissions: contents: write + env: + RELEASE_TARGET_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6 with: - fetch-depth: 0 - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 - - run: git tag ${{ needs.release-info.outputs.version }} - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # ratchet:actions/download-artifact@v6 + ref: ${{ env.RELEASE_TARGET_COMMIT }} + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8 with: merge-multiple: true path: release_artifacts @@ -165,15 +219,18 @@ jobs: id: release with: tag_name: ${{ needs.release-info.outputs.version }} + target_commitish: ${{ env.RELEASE_TARGET_COMMIT }} body: ${{ needs.release-info.outputs.changelog }} - prerelease: false + prerelease: ${{ needs.release-info.outputs.pre_release == 'true' }} files: ./release_artifacts/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - env: + - if: needs.release-info.outputs.pre_release != 'true' + env: VERSION: ${{ needs.release-info.outputs.version }} run: mise run ci:prepare-template-vars ./release_artifacts/checksums.txt -v > template.vars - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v5 + - if: needs.release-info.outputs.pre_release != 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # ratchet:actions/upload-artifact@v7 with: name: template-vars path: ./template.vars @@ -182,12 +239,13 @@ jobs: echo "A new release is available over at https://github.com/${{ github.repository }}/releases/tag/${{ needs.release-info.outputs.version }}." >> $GITHUB_STEP_SUMMARY release-gar: + if: needs.release-info.outputs.pre_release != 'true' strategy: fail-fast: false matrix: arch: [arm64, amd64] suffix: [nav, tenant] - needs: [release-github] + needs: [release-info, release-github] runs-on: ubuntu-latest permissions: contents: read @@ -201,7 +259,7 @@ jobs: service_account: gh-naisdevice@nais-io.iam.gserviceaccount.com token_format: access_token - uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # ratchet:google-github-actions/setup-gcloud@v3 - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # ratchet:actions/download-artifact@v6 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8 with: name: installer-linux-${{ matrix.arch }}-${{ matrix.suffix }} path: ./downloaded-artifact/ @@ -209,7 +267,8 @@ jobs: gcloud artifacts apt upload nais-ppa --project nais-io --quiet --location europe-north1 --source ./downloaded-artifact/* release-external-repos: - needs: [release-github] + if: needs.release-info.outputs.pre_release != 'true' + needs: [release-info, release-github] strategy: fail-fast: false matrix: @@ -234,8 +293,8 @@ jobs: private-key: ${{ secrets.NAIS_APP_PRIVATE_KEY }} app-id: ${{ secrets.NAIS_APP_ID }} repo: ${{ matrix.target.repo }} - - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v3 - - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # ratchet:actions/download-artifact@v6 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # ratchet:jdx/mise-action@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # ratchet:actions/download-artifact@v8 with: name: template-vars - name: update ${{ matrix.target.repo }} diff --git a/.github/workflows/templates/naisdevice-tenant.rb b/.github/workflows/templates/naisdevice-tenant.rb index 4aa029407..ae07e7008 100644 --- a/.github/workflows/templates/naisdevice-tenant.rb +++ b/.github/workflows/templates/naisdevice-tenant.rb @@ -5,11 +5,6 @@ desc "naisdevice is a mechanism enabling developers to connect to internal resources in a secure and friendly manner." homepage "https://docs.nais.io/operate/naisdevice/how-to/install/" - depends_on formula: [ - "wireguard-go", - "wireguard-tools", - ] - if Hardware::CPU.intel? url "https://github.com/nais/device/releases/download/#{version}/$NAISDEVICE_TENANT_MACOS_AMD64_FILENAME", verified: "github.com/nais/device/" sha256 "$NAISDEVICE_TENANT_MACOS_AMD64_HASH_BASE16" diff --git a/.github/workflows/templates/naisdevice.rb b/.github/workflows/templates/naisdevice.rb index 2fcf0d988..1c8a8c78a 100644 --- a/.github/workflows/templates/naisdevice.rb +++ b/.github/workflows/templates/naisdevice.rb @@ -5,11 +5,6 @@ desc "naisdevice is a mechanism enabling developers to connect to internal resources in a secure and friendly manner." homepage "https://docs.nais.io/operate/naisdevice/how-to/install/" - depends_on formula: [ - "wireguard-go", - "wireguard-tools", - ] - if Hardware::CPU.intel? url "https://github.com/nais/device/releases/download/#{version}/$NAISDEVICE_MACOS_AMD64_FILENAME", verified: "github.com/nais/device/" sha256 "$NAISDEVICE_MACOS_AMD64_HASH_BASE16" diff --git a/.gitignore b/.gitignore index ed8a85e52..c3f0f0a3e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ bin .DS_Store *.pkg *.app -wireguard-go-* -wireguard-tools-* cmd/device-agent/main_windows.syso cmd/helper/main_windows.syso packaging/windows/obj diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a9662620b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# Project instructions for AI coding agents + +## Commands + +### After all changes are made, run + +- `mise run test` +- `mise run check` +- `go fix [changed_files]...` + +## Tech stack + +- go (look at go.mod for version) +- gRPC +- protobuf +- wireguard +- sqlite + +## Code style + +- Write obvious code instead of clever code. +- Favor self-explanatory code over code comments. +- If you really have to add a comment, make sure it's short and concise. +- Wrap errors with context: `fmt.Errorf("short description: %w", err)`. +- Use `testify/require` and `testify/assert` for tests. Prefer table-driven tests. +- Platform-specific code goes in files with build-tag suffixes (`_darwin.go`, `_linux.go`, `_windows.go`). + +## Git + +- Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for all commit messages. +- Never commit unless explicitly told to. +- Never push unless explicitly told to. diff --git a/assets/linux/nfpm.yaml b/assets/linux/nfpm.yaml index 43acee713..c4069219d 100644 --- a/assets/linux/nfpm.yaml +++ b/assets/linux/nfpm.yaml @@ -13,7 +13,7 @@ license: "MIT" depends: - "jq" - "sed" - - "wireguard" + - "wireguard | wireguard-tools" scripts: postinstall: "./assets/linux/postinstall" postremove: "./assets/linux/postrm" diff --git a/assets/linux/postinstall b/assets/linux/postinstall index f178dc30c..2c3558e18 100755 --- a/assets/linux/postinstall +++ b/assets/linux/postinstall @@ -28,7 +28,14 @@ make_user_dirs() { user_accounts=$(loginctl list-users --output json | jq '[.[] | select(.uid >= 1000)]') fi - if echo "$user_accounts" | jq -e 'length != 1' >/dev/null; then + num_accounts=$(echo "$user_accounts" | jq 'length') + + if [ "$num_accounts" -eq 0 ]; then + echo "No logged-in user accounts found, skipping user directory setup" + return 0 + fi + + if [ "$num_accounts" -gt 1 ]; then printf "\nMore than 1 user account logged in! naisdevice only permits _one_ user account!\n" exit 1 fi @@ -44,7 +51,9 @@ make_user_dirs() { chmod 700 "$directory" done - cp /sys/devices/virtual/dmi/id/product_serial "${config_dir}" + if [ -f /sys/devices/virtual/dmi/id/product_serial ]; then + cp /sys/devices/virtual/dmi/id/product_serial "${config_dir}" + fi chown -R "${user}:" "${config_dir}" } diff --git a/assets/macos/postinstall b/assets/macos/postinstall index 916192fce..0d2e1de85 100755 --- a/assets/macos/postinstall +++ b/assets/macos/postinstall @@ -5,14 +5,17 @@ daemon_name="io.nais.device.helper" destination="/Library/LaunchDaemons/${daemon_name}.plist" launchctl list | grep -q "$daemon_name" && launchctl unload "$destination" -config_dir="/Users/${user}/Library/Application Support/naisdevice" -log_dir="${config_dir}/logs" +if [ -n "$user" ]; then + config_dir="/Users/${user}/Library/Application Support/naisdevice" + log_dir="${config_dir}/logs" -mkdir -p -m 0700 "${config_dir}" -mkdir -p -m 0700 "${log_dir}" - -chown -R "${user}:staff" "${config_dir}" + mkdir -p -m 0700 "${config_dir}" + mkdir -p -m 0700 "${log_dir}" + chown -R "${user}:staff" "${config_dir}" +else + echo "No console user detected, skipping user config directory setup" +fi cat << EOF > "$destination" @@ -49,4 +52,3 @@ launchctl load "$destination" echo "Installed service $daemon_name" killall -9 -m "naisdevice.*" || true -killall -9 "wireguard-go" || true diff --git a/assets/windows/naisdevice.nsi b/assets/windows/naisdevice.nsi index 0016ccf29..944cd77cf 100644 --- a/assets/windows/naisdevice.nsi +++ b/assets/windows/naisdevice.nsi @@ -7,14 +7,6 @@ !define /ifndef VERSION "develop" ; Override when building release, MUST match '\d+.\d+.\d+.\d+' -!ifndef WIREGUARD - !error "Specify path to the WIREGUARD MSI file using -DWIREGUARD." -!endif - -!ifndef WIREGUARD_FILENAME - !error "Specify filename part of WIREGUARD variable using -DWIREGUARD_FILENAME." -!endif - !define APP_NAME "naisdevice" !define UNINSTALLER "uninstaller.exe" !define BIN_DIR "./bin/windows-client" @@ -86,7 +78,7 @@ Var ProgramDataPath !insertmacro MUI_PAGE_WELCOME Page custom StopInstances !insertmacro MUI_PAGE_INSTFILES -Page custom InstallWireGuard +Page custom StartService ;; Uninstaller pages @@ -121,6 +113,7 @@ Section "Install files" CreateDirectory $INSTDIR SetOutPath $INSTDIR File ${BIN_DIR}/naisdevice-*.exe + File ${BIN_DIR}/wintun.dll File ./assets/windows/icon/naisdevice.ico SectionEnd @@ -212,6 +205,7 @@ Section "Uninstall" GetKnownFolderPath $ProgramDataPath "${FOLDERID_ProgramData}" RMDir /r "$ProgramDataPath\NAV\naisdevice" Delete $INSTDIR\naisdevice-*.exe + Delete $INSTDIR\wintun.dll Delete $INSTDIR\naisdevice.ico Delete $INSTDIR\${UNINSTALLER} RMDir $INSTDIR @@ -221,6 +215,23 @@ SectionEnd ; Functions -------------------------------- +Function .onInstSuccess + ${IfNot} ${Silent} + Return + ${EndIf} + + !insertmacro _Log "Silent install: starting ${SERVICE_NAME} service" + SimpleSC::StartService ${SERVICE_NAME} "" 60 + Pop $0 + ${If} $0 != 0 + SimpleSC::GetErrorMessage + Pop $0 + !insertmacro _Log "Silent install: failed to start service: $0" + ${Else} + !insertmacro _Log "Silent install: service started successfully" + ${EndIf} +FunctionEnd + !macro GUIInit un Function ${un}GUIInit !insertmacro _Log "Inside GUIInit" @@ -402,31 +413,18 @@ ${ProgressPage} \ !insertmacro StopInstances "install" "" !insertmacro StopInstances "uninstall" "un." -; -- InstallWireGuard -------------- +; -- StartService -------------- ; Function that should push 0 on the stack to skip the page, any other value to continue (required) -Function _InstallWireGuard_Abort +Function _StartService_Abort ; Never skip Push 1 FunctionEnd ; Function to initialize any needed state, PP_NoOp to skip -Function _InstallWireGuard_Init - Push $R9 - - SetOutPath $TEMP - File /oname=${WIREGUARD_FILENAME} "${WIREGUARD}" - ExecWait 'msiexec /package "$TEMP\${WIREGUARD_FILENAME}" DO_NOT_LAUNCH=true' $R9 - ${If} ${Errors} - !insertmacro _Log "Error while installing WireGuard" - !insertmacro _Log "Exit code from wireguard installer: $R9" - ${EndIf} - - Pop $R9 -FunctionEnd ; Function called on every step. Should push 0 to the stack to leave the page, any other value to continue (required) -Function _InstallWireGuard_Step +Function _StartService_Step Push $R9 !insertmacro _Log "Attempting to start ${SERVICE_NAME} service" @@ -447,20 +445,14 @@ Function _InstallWireGuard_Step Pop $R9 FunctionEnd -; Function called when successfully leaving the page, PP_NoOp to skip -Function _InstallWireGuard_Leaving - Delete $TEMP\${WIREGUARD} -FunctionEnd - ${ProgressPage} \ - "InstallWireGuard" \ + "StartService" \ "Installation almost complete" \ - "Installing WireGuard and starting services" \ + "Starting services" \ "Installation of naisdevice is almost finished.$\n$\n\ - The final steps are to install WireGuard, which is used by naisdevice to create the VPN tunnels, and start background services.$\n$\n\ - The WireGuard installer finishes by launching WireGuard. You can close that window without making any changes.$\n$\n\ + The final step is to start the background service.$\n$\n\ Have a nais day!" \ - _InstallWireGuard_Abort \ - _InstallWireGuard_Init \ - _InstallWireGuard_Step \ - _InstallWireGuard_Leaving + _StartService_Abort \ + PP_NoOp \ + _StartService_Step \ + PP_NoOp diff --git a/assets/windows/wintun-amd64.dll b/assets/windows/wintun-amd64.dll new file mode 100644 index 000000000..aee04e77b Binary files /dev/null and b/assets/windows/wintun-amd64.dll differ diff --git a/assets/windows/wintun-arm64.dll b/assets/windows/wintun-arm64.dll new file mode 100644 index 000000000..dc4e4aeeb Binary files /dev/null and b/assets/windows/wintun-arm64.dll differ diff --git a/assets/windows/wintun.sha256 b/assets/windows/wintun.sha256 new file mode 100644 index 000000000..e488fea7e --- /dev/null +++ b/assets/windows/wintun.sha256 @@ -0,0 +1,2 @@ +e5da8447dc2c320edc0fc52fa01885c103de8c118481f683643cacc3220dafce wintun-amd64.dll +f7ba89005544be9d85231a9e0d5f23b2d15b3311667e2dad0debd344918a3f80 wintun-arm64.dll diff --git a/assets/windows/wireguard-amd64-0.5.3.msi b/assets/windows/wireguard-amd64-0.5.3.msi deleted file mode 100644 index f97ea5451..000000000 Binary files a/assets/windows/wireguard-amd64-0.5.3.msi and /dev/null differ diff --git a/assets/windows/wireguard-arm64-0.5.3.msi b/assets/windows/wireguard-arm64-0.5.3.msi deleted file mode 100644 index f940ec444..000000000 Binary files a/assets/windows/wireguard-arm64-0.5.3.msi and /dev/null differ diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 3ef56486d..8a778b329 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -1,5 +1,3 @@ -//go:build linux - package main import ( @@ -160,7 +158,7 @@ func run(log *logrus.Entry, cfg config.Config) error { } cfg.WireGuardPrivateKey = key - netConf, err := wg.NewConfigurer(log.WithField("component", "network-configurer"), cfg.WireGuardConfigPath, cfg.WireGuardIPv4Prefix, cfg.WireGuardIPv6Prefix, string(cfg.WireGuardPrivateKey.Private()), "wg0", 51820, nil, nil, nil) + netConf, err := wg.NewConfigurer(log.WithField("component", "network-configurer"), cfg.WireGuardConfigPath, cfg.WireGuardIPv4Prefix, cfg.WireGuardIPv6Prefix, cfg.WireGuardPrivateKey.String(), "wg0", 51820, nil, nil, nil) if err != nil { return fmt.Errorf("create WireGuard configurer: %w", err) } diff --git a/cmd/gateway-agent/main.go b/cmd/gateway-agent/main.go index 798f8f441..051625278 100644 --- a/cmd/gateway-agent/main.go +++ b/cmd/gateway-agent/main.go @@ -26,7 +26,6 @@ import ( "github.com/nais/device/internal/logger" "github.com/coreos/go-iptables/iptables" - "github.com/google/gopacket/routing" "github.com/kelseyhightower/envconfig" "github.com/sirupsen/logrus" @@ -82,7 +81,7 @@ func run(log *logrus.Entry, cfg config.Config) error { ecfg, err := enroll.NewGatewayClient( ctx, - privateKey.Public(), + privateKey.PublicKey().String(), hashedPassword, wireguardListenPort, log.WithField("component", "bootstrap"), @@ -99,7 +98,7 @@ func run(log *logrus.Entry, cfg config.Config) error { } cfg.Name = ecfg.Name - cfg.PrivateKey = string(privateKey.Private()) + cfg.PrivateKey = privateKey.String() cfg.APIServerURL = enrollResp.APIServerGRPCAddress cfg.DeviceIPv4 = enrollResp.WireGuardIPv4 @@ -121,14 +120,16 @@ func run(log *logrus.Entry, cfg config.Config) error { return fmt.Errorf("cannot enable routing: %w", err) } iptablesV4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + _ = iptablesV4 // workaround as statickcheck thinks this in unused (but only on macos :shrug:) if err != nil { return fmt.Errorf("setup iptables: %w", err) } iptablesV6, err := iptables.NewWithProtocol(iptables.ProtocolIPv6) + _ = iptablesV6 // workaround as statickcheck thinks this in unused (but only on macos :shrug:) if err != nil { return fmt.Errorf("setup iptables: %w", err) } - router, err := routing.New() + router, err := NewRouter() if err != nil { return fmt.Errorf("setup routing: %w", err) } diff --git a/cmd/gateway-agent/main_linux.go b/cmd/gateway-agent/main_linux.go new file mode 100644 index 000000000..82c83b250 --- /dev/null +++ b/cmd/gateway-agent/main_linux.go @@ -0,0 +1,7 @@ +package main + +import "github.com/google/gopacket/routing" + +func NewRouter() (routing.Router, error) { + return routing.New() +} diff --git a/cmd/naisdevice-agent/main.go b/cmd/naisdevice-agent/main.go index 5d38b3d41..e33acbac1 100644 --- a/cmd/naisdevice-agent/main.go +++ b/cmd/naisdevice-agent/main.go @@ -291,9 +291,7 @@ func checkNewVersionAvailable(ctx context.Context) (bool, error) { span.RecordError(err) return false, err } - - client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} - resp, err := client.Do(req) + resp, err := otelhttp.DefaultClient.Do(req) if err != nil { span.RecordError(err) return false, fmt.Errorf("retrieve current release version: %s", err) diff --git a/cmd/naisdevice-helper/main.go b/cmd/naisdevice-helper/main.go index fec7ef981..83be493ec 100644 --- a/cmd/naisdevice-helper/main.go +++ b/cmd/naisdevice-helper/main.go @@ -20,12 +20,10 @@ import ( "github.com/nais/device/pkg/pb" ) -var cfg = helper.Config{ - WireGuardConfigPath: filepath.Join(config.ConfigDir, "utun69.conf"), -} +var cfg = helper.Config{} func init() { - flag.StringVar(&cfg.LogLevel, "log-level", "info", "which log level to output") + flag.StringVar(&cfg.LogLevel, "log-level", "debug", "which log level to output") flag.StringVar(&cfg.Interface, "interface", "utun69", "interface name") flag.Parse() @@ -54,7 +52,7 @@ func main() { log.WithError(err).Fatal("starting windows service") } - osConfigurator := helper.NewTracedConfigurator(helper.New(cfg)) + osConfigurator := helper.NewTracedConfigurator(helper.New(cfg, log)) log.WithFields(version.LogFields).WithField("cfg", cfg).Info("starting naisdevice-helper") diff --git a/cmd/prometheus-agent/main.go b/cmd/prometheus-agent/main.go index fb18bee4b..0878cfdfc 100644 --- a/cmd/prometheus-agent/main.go +++ b/cmd/prometheus-agent/main.go @@ -1,5 +1,3 @@ -//go:build linux - package main import ( diff --git a/cmd/smoke-test/main.go b/cmd/smoke-test/main.go new file mode 100644 index 000000000..490758ba6 --- /dev/null +++ b/cmd/smoke-test/main.go @@ -0,0 +1,153 @@ +// smoke-test connects to the running naisdevice-helper via gRPC, +// sends a Configure request with synthetic WireGuard keys and a test +// gateway, then verifies that: +// - the WireGuard interface has the expected peer +// - the OS has routes for the gateway's advertised prefixes +// +// Finally it tears down the configuration and exits. +package main + +import ( + "context" + "fmt" + "log" + "net/netip" + "os" + "path/filepath" + "time" + + "golang.zx2c4.com/wireguard/wgctrl" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/nais/device/internal/helper/config" + "github.com/nais/device/pkg/pb" +) + +const ifaceName = "utun69" + +var wantRoutes = []netip.Prefix{ + netip.MustParsePrefix("10.255.240.0/21"), + netip.MustParsePrefix("10.123.0.0/24"), + netip.MustParsePrefix("10.124.0.0/16"), +} + +func main() { + log.SetOutput(os.Stderr) + + if err := run(); err != nil { + log.Fatalf("FAIL: %v", err) + } + log.Println("PASS: smoke test succeeded") +} + +func run() error { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + socketPath := filepath.Join(config.RuntimeDir, "helper.sock") + conn, err := grpc.NewClient( + "unix:"+socketPath, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return fmt.Errorf("create gRPC client: %w", err) + } + defer func() { _ = conn.Close() }() + + client := pb.NewDeviceHelperClient(conn) + + log.Println("pinging helper...") + if _, err := client.Ping(ctx, &pb.PingRequest{}); err != nil { + return fmt.Errorf("ping helper: %w", err) + } + log.Println("helper is alive") + + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return fmt.Errorf("generate private key: %w", err) + } + + gwPrivateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return fmt.Errorf("generate gateway private key: %w", err) + } + gwPublicKey := gwPrivateKey.PublicKey() + + cfg := &pb.Configuration{ + PrivateKey: privateKey.String(), + DeviceIPv4: "10.255.240.100", + Gateways: []*pb.Gateway{ + { + Name: "smoke-gw", + PublicKey: gwPublicKey.String(), + Endpoint: "127.0.0.1:51820", + Ipv4: "10.255.240.1", + RoutesIPv4: []string{"10.123.0.0/24", "10.124.0.0/16"}, + }, + }, + } + + log.Println("sending Configure request...") + if _, err := client.Configure(ctx, cfg); err != nil { + return fmt.Errorf("configure helper: %w", err) + } + log.Println("configure succeeded") + + defer func() { + teardownCtx, teardownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer teardownCancel() + + log.Println("sending Teardown request...") + if _, err := client.Teardown(teardownCtx, &pb.TeardownRequest{}); err != nil { + log.Printf("WARN: teardown failed: %v", err) + return + } + log.Println("teardown succeeded") + }() + + if err := verifyPeers(gwPublicKey); err != nil { + return fmt.Errorf("verify peers: %w", err) + } + + if err := verifyRoutes(wantRoutes); err != nil { + return fmt.Errorf("verify routes: %w", err) + } + + return nil +} + +func verifyPeers(expectedPubKey wgtypes.Key) error { + log.Println("verifying WireGuard peers...") + + wgClient, err := wgctrl.New() + if err != nil { + return fmt.Errorf("create wgctrl client: %w", err) + } + defer func() { _ = wgClient.Close() }() + + dev, err := wgClient.Device(ifaceName) + if err != nil { + return fmt.Errorf("get device %q: %w", ifaceName, err) + } + + if len(dev.Peers) == 0 { + return fmt.Errorf("interface %q has no peers", ifaceName) + } + + found := false + for _, peer := range dev.Peers { + log.Printf(" peer: %s (endpoint=%v, allowed_ips=%v)", peer.PublicKey, peer.Endpoint, peer.AllowedIPs) + if peer.PublicKey == expectedPubKey { + found = true + } + } + + if !found { + return fmt.Errorf("expected peer %s not found on interface %q", expectedPubKey, ifaceName) + } + + log.Printf("PASS: interface %q has %d peer(s), expected peer found", ifaceName, len(dev.Peers)) + return nil +} diff --git a/cmd/smoke-test/verify_routes_darwin.go b/cmd/smoke-test/verify_routes_darwin.go new file mode 100644 index 000000000..ee7556a08 --- /dev/null +++ b/cmd/smoke-test/verify_routes_darwin.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/netip" + "syscall" + + "golang.org/x/net/route" +) + +func verifyRoutes(prefixes []netip.Prefix) error { + log.Println("verifying macOS routes...") + + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return fmt.Errorf("lookup interface %q: %w", ifaceName, err) + } + + rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP, 0) + if err != nil { + return fmt.Errorf("fetch routing table: %w", err) + } + + msgs, err := route.ParseRIB(route.RIBTypeRoute, rib) + if err != nil { + return fmt.Errorf("parse routing table: %w", err) + } + + routeSet := make(map[netip.Prefix]bool) + for _, msg := range msgs { + rm, ok := msg.(*route.RouteMessage) + if !ok || rm.Index != iface.Index { + continue + } + p, ok := routeMsgToPrefix(rm) + if !ok { + continue + } + routeSet[p] = true + } + + for _, want := range prefixes { + if !routeSet[want] { + log.Printf("routes on interface %q (index %d):", ifaceName, iface.Index) + for p := range routeSet { + log.Printf(" %s", p) + } + return fmt.Errorf("expected route %s not found on interface %q", want, ifaceName) + } + log.Printf("PASS: route %s found", want) + } + + log.Printf("PASS: all expected routes present on interface %q", ifaceName) + return nil +} + +func routeMsgToPrefix(rm *route.RouteMessage) (netip.Prefix, bool) { + if len(rm.Addrs) <= syscall.RTAX_NETMASK { + return netip.Prefix{}, false + } + + dst := rm.Addrs[syscall.RTAX_DST] + mask := rm.Addrs[syscall.RTAX_NETMASK] + + addr, ok := addrToNetipAddr(dst) + if !ok { + return netip.Prefix{}, false + } + + bits := addr.BitLen() + if mask != nil { + bits = maskBits(mask, addr.Is6()) + } + + return netip.PrefixFrom(addr, bits), true +} + +func addrToNetipAddr(a route.Addr) (netip.Addr, bool) { + switch v := a.(type) { + case *route.Inet4Addr: + return netip.AddrFrom4(v.IP), true + case *route.Inet6Addr: + return netip.AddrFrom16(v.IP), true + default: + return netip.Addr{}, false + } +} + +func maskBits(a route.Addr, is6 bool) int { + switch v := a.(type) { + case *route.Inet4Addr: + ones, _ := net.IPv4Mask(v.IP[0], v.IP[1], v.IP[2], v.IP[3]).Size() + return ones + case *route.Inet6Addr: + mask := net.IPMask(v.IP[:]) + ones, _ := mask.Size() + return ones + default: + if is6 { + return 128 + } + return 32 + } +} diff --git a/cmd/smoke-test/verify_routes_linux.go b/cmd/smoke-test/verify_routes_linux.go new file mode 100644 index 000000000..d5e495a35 --- /dev/null +++ b/cmd/smoke-test/verify_routes_linux.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "log" + "net/netip" + + "github.com/vishvananda/netlink" +) + +func verifyRoutes(prefixes []netip.Prefix) error { + log.Println("verifying Linux routes...") + + link, err := netlink.LinkByName(ifaceName) + if err != nil { + return fmt.Errorf("lookup interface %q: %w", ifaceName, err) + } + + routes, err := netlink.RouteList(link, netlink.FAMILY_ALL) + if err != nil { + return fmt.Errorf("list routes on %q: %w", ifaceName, err) + } + + routeSet := make(map[netip.Prefix]bool) + for _, r := range routes { + if r.Dst == nil { + continue + } + addr, ok := netip.AddrFromSlice(r.Dst.IP) + if !ok { + continue + } + ones, _ := r.Dst.Mask.Size() + routeSet[netip.PrefixFrom(addr.Unmap(), ones)] = true + } + + for _, want := range prefixes { + if !routeSet[want] { + log.Printf("routes on interface %q:", ifaceName) + for _, r := range routes { + log.Printf(" %s", r.Dst) + } + return fmt.Errorf("expected route %s not found on interface %q", want, ifaceName) + } + log.Printf("PASS: route %s found", want) + } + + log.Printf("PASS: all expected routes present on interface %q", ifaceName) + return nil +} diff --git a/cmd/smoke-test/verify_routes_windows.go b/cmd/smoke-test/verify_routes_windows.go new file mode 100644 index 000000000..72444646c --- /dev/null +++ b/cmd/smoke-test/verify_routes_windows.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/netip" + + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" +) + +func verifyRoutes(prefixes []netip.Prefix) error { + log.Println("verifying Windows routes...") + + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return fmt.Errorf("lookup interface %q: %w", ifaceName, err) + } + + ifLUID, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) + if err != nil { + return fmt.Errorf("get LUID for interface index %d: %w", iface.Index, err) + } + + for _, prefix := range prefixes { + nextHop := netip.IPv4Unspecified() + if prefix.Addr().Is6() { + nextHop = netip.IPv6Unspecified() + } + _, err := ifLUID.Route(prefix, nextHop) + if err != nil { + return fmt.Errorf("expected route %s not found on interface %q: %w", prefix, ifaceName, err) + } + log.Printf("PASS: route %s found", prefix) + } + + log.Printf("PASS: all expected routes present on interface %q", ifaceName) + return nil +} diff --git a/go.mod b/go.mod index b62159525..cd29ecf2f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nais/device -go 1.26.3 +go 1.26.2 tool ( github.com/akavel/rsrc @@ -35,8 +35,9 @@ require ( github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/urfave/cli/v2 v2.27.7 + github.com/vishvananda/netlink v1.3.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 @@ -45,11 +46,15 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 golang.org/x/crypto v0.51.0 - golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/mod v0.35.0 + golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.35.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.45.0 + golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 + golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/api v0.247.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 @@ -122,6 +127,7 @@ require ( github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/cavaliergopher/cpio v1.0.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.11 // indirect @@ -131,13 +137,13 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect - github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -163,8 +169,8 @@ require ( github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/go-git/go-git/v5 v5.19.1 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.5 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -235,13 +241,13 @@ require ( github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect + github.com/josharian/native v1.1.0 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kisielk/errcheck v1.9.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/klauspost/compress v1.18.2 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect @@ -277,6 +283,9 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-sqlite3 v1.14.18 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.5.1 // indirect github.com/mgechev/revive v1.14.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -302,7 +311,7 @@ require ( github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect github.com/pingcap/log v1.1.0 // indirect github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect - github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -364,6 +373,7 @@ require ( github.com/uudashr/iface v1.4.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/vektra/mockery/v3 v3.6.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect @@ -394,13 +404,13 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp/typeparams v0.0.0-20251125195548-87e1e737ad39 // indirect - golang.org/x/net v0.55.0 // indirect golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.44.0 // indirect golang.org/x/vuln v1.1.4 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/go.sum b/go.sum index 605a053c2..464759106 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -68,6 +70,8 @@ github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= @@ -166,6 +170,8 @@ github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7a github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -186,9 +192,11 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= @@ -203,8 +211,8 @@ github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= @@ -237,13 +245,18 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= -github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o= @@ -276,12 +289,14 @@ github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOES github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= -github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= -github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -372,6 +387,8 @@ github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2b github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= @@ -435,6 +452,8 @@ github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduw github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -484,6 +503,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= @@ -500,8 +521,6 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -592,10 +611,18 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc= github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo= github.com/microsoft/wmi v0.38.3 h1:RVbn+m2jlPRsB2fLADXqabJj/EhMXQbvKM7OYS8VOv0= github.com/microsoft/wmi v0.38.3/go.mod h1:XF+cfluA15xGnSCYkJIYuj2vWzdm2YrNuvqlC+baWY0= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -661,8 +688,8 @@ github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0= github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE= -github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= -github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -828,11 +855,13 @@ github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnn github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vektra/mockery/v3 v3.6.1 h1:YyqAXihdNML8y6SJnvPKYr+2HAHvBjdvqFu/fMYlX8g= github.com/vektra/mockery/v3 v3.6.1/go.mod h1:Oti3Df0WP8wwT31yuVri3QNsDeMUQU5Q4QEg8EabaBw= -github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo= github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= @@ -887,30 +916,54 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -941,11 +994,13 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20251125195548-87e1e737ad39 h1:yzGKB4T4r1nFi65o7dQ96ERTfU2trk8Ige9aqqADqf4= @@ -965,6 +1020,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -987,9 +1044,15 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1003,6 +1066,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1026,15 +1091,21 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1045,6 +1116,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1056,6 +1129,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -1079,6 +1154,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= @@ -1091,8 +1168,17 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc= google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -1102,8 +1188,12 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1111,6 +1201,10 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1124,6 +1218,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1145,6 +1241,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= diff --git a/internal/apiserver/config/config.go b/internal/apiserver/config/config.go index ed7cbdac1..9c12c8e38 100644 --- a/internal/apiserver/config/config.go +++ b/internal/apiserver/config/config.go @@ -11,6 +11,7 @@ import ( "github.com/nais/device/internal/wireguard" "github.com/nais/device/pkg/pb" "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) type Config struct { @@ -44,7 +45,7 @@ type Config struct { WireGuardIP string // for passing in raw string WireGuardIPv6 string // for passing in raw string WireGuardConfigPath string - WireGuardPrivateKey wireguard.PrivateKey + WireGuardPrivateKey wgtypes.Key WireGuardPrivateKeyPath string WireGuardNetworkAddress string WireGuardIPv4Prefix *netip.Prefix `ignored:"true"` @@ -141,7 +142,7 @@ func (cfg *Config) APIServerPeer() *pb.Gateway { return &pb.Gateway{ Name: "apiserver", - PublicKey: string(cfg.WireGuardPrivateKey.Public()), + PublicKey: cfg.WireGuardPrivateKey.PublicKey().String(), Endpoint: cfg.Endpoint, Ipv4: cfg.WireGuardIPv4Prefix.Addr().String(), Ipv6: ipv6, diff --git a/internal/apiserver/enroller/autoenroll_local.go b/internal/apiserver/enroller/autoenroll_local.go index 0089b41d3..015666f47 100644 --- a/internal/apiserver/enroller/autoenroll_local.go +++ b/internal/apiserver/enroller/autoenroll_local.go @@ -89,11 +89,18 @@ func (l *localEnroller) getEnrollments(ctx context.Context) ([]enroll.DeviceRequ } func (l *localEnroller) enroll(ctx context.Context, enrollment enroll.DeviceRequest) error { + normalizedPublicKey, err := enroll.NormalizeWireGuardPublicKey(enrollment.WireGuardPublicKey) + if err != nil { + msg := "local enroller: invalid wireguard public key" + l.log.WithError(err).Error(msg) + return fmt.Errorf("%s: %w", msg, err) + } + device := &pb.Device{ Username: enrollment.Owner, Serial: enrollment.Serial, Platform: enrollment.Platform, - PublicKey: string(enrollment.WireGuardPublicKey), + PublicKey: normalizedPublicKey, } if err := l.db.AddDevice(ctx, device); err != nil { diff --git a/internal/apiserver/enroller/autoenroll_pubsub.go b/internal/apiserver/enroller/autoenroll_pubsub.go index 789d65c36..516e28763 100644 --- a/internal/apiserver/enroller/autoenroll_pubsub.go +++ b/internal/apiserver/enroller/autoenroll_pubsub.go @@ -106,9 +106,16 @@ func (a *autoEnroll) receiveGateway(ctx context.Context, msg *pubsub.Message) { } log := a.log.WithField("gateway", req.Name) - err := a.db.AddGateway(ctx, &pb.Gateway{ + normalizedPublicKey, err := enroll.NormalizeWireGuardPublicKey(req.WireGuardPublicKey) + if err != nil { + log.WithError(err).Error("invalid wireguard public key") + msg.Nack() + return + } + + err = a.db.AddGateway(ctx, &pb.Gateway{ Name: req.Name, - PublicKey: string(req.WireGuardPublicKey), + PublicKey: normalizedPublicKey, Endpoint: req.Endpoint, PasswordHash: req.HashedPassword, }) @@ -171,9 +178,16 @@ func (a *autoEnroll) receiveDevice(ctx context.Context, msg *pubsub.Message) { } log := a.log.WithFields(logrus.Fields{"serial": req.Serial, "platform": req.Platform}) - err := a.db.AddDevice(ctx, &pb.Device{ + normalizedPublicKey, err := enroll.NormalizeWireGuardPublicKey(req.WireGuardPublicKey) + if err != nil { + log.WithError(err).Error("invalid wireguard public key") + msg.Nack() + return + } + + err = a.db.AddDevice(ctx, &pb.Device{ Username: req.Owner, - PublicKey: string(req.WireGuardPublicKey), + PublicKey: normalizedPublicKey, Serial: req.Serial, Platform: req.Platform, }) diff --git a/internal/controlplane-cli/enrollgateway.go b/internal/controlplane-cli/enrollgateway.go index f9551e94c..e4b00506b 100644 --- a/internal/controlplane-cli/enrollgateway.go +++ b/internal/controlplane-cli/enrollgateway.go @@ -7,10 +7,10 @@ import ( "io" "os" - "github.com/nais/device/internal/deviceagent/wireguard" "github.com/nais/device/internal/passwordhash" "github.com/nais/device/pkg/pb" "github.com/urfave/cli/v2" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -133,15 +133,18 @@ func EnrollGateway(c *cli.Context) error { key := passwordhash.HashPassword([]byte(password), salt) passhash := passwordhash.FormatHash(key, salt) - privateKey := wireguard.WgGenKey() - publicKey := wireguard.PublicKey(privateKey) + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return fmt.Errorf("generating private key: %w", err) + } + publicKey := privateKey.PublicKey().String() req := &pb.ModifyGatewayRequest{ Username: AdminUsername, Password: c.String(FlagAdminPassword), Gateway: &pb.Gateway{ Name: c.String(FlagName), - PublicKey: string(publicKey), + PublicKey: publicKey, Endpoint: c.String(FlagEndpoint), PasswordHash: string(passhash), }, @@ -174,7 +177,7 @@ func EnrollGateway(c *cli.Context) error { fmt.Printf("GATEWAY_AGENT_NAME=\"%s\"\n", response.GetGateway().GetName()) fmt.Printf("GATEWAY_AGENT_APISERVERPASSWORD=\"%s\"\n", password) - fmt.Printf("GATEWAY_AGENT_PRIVATEKEY=\"%s\"\n", base64.StdEncoding.EncodeToString(privateKey)) + fmt.Printf("GATEWAY_AGENT_PRIVATEKEY=\"%s\"\n", privateKey.String()) fmt.Printf("GATEWAY_AGENT_DEVICEIP=\"%s/21\"\n", response.GetGateway().GetIpv4()) return err diff --git a/internal/deviceagent/config/config.go b/internal/deviceagent/config/config.go index 588ab0fdb..f0c44e4ae 100644 --- a/internal/deviceagent/config/config.go +++ b/internal/deviceagent/config/config.go @@ -34,7 +34,6 @@ type Config struct { JitaOAuth2Config oauth2.Config Platform string PrivateKeyPath string - WireGuardConfigPath string // TODO(sechmann):remove this as well EnrollProjectID string EnrollTopicName string LocalAPIServer bool @@ -46,7 +45,6 @@ func (c *Config) SetDefaults() { c.Platform = Platform c.Interface = "utun69" c.PrivateKeyPath = filepath.Join(c.ConfigDir, "private.key") - c.WireGuardConfigPath = filepath.Join(c.ConfigDir, c.Interface+".conf") } func DefaultConfig() (*Config, error) { diff --git a/internal/deviceagent/runtimeconfig/runtimeconfig.go b/internal/deviceagent/runtimeconfig/runtimeconfig.go index 1efb1ddd7..b30d5c975 100644 --- a/internal/deviceagent/runtimeconfig/runtimeconfig.go +++ b/internal/deviceagent/runtimeconfig/runtimeconfig.go @@ -3,7 +3,6 @@ package runtimeconfig import ( "bytes" "context" - "encoding/base64" "encoding/json" "fmt" "io" @@ -27,6 +26,7 @@ import ( "github.com/nais/device/pkg/pb" "github.com/sirupsen/logrus" "golang.org/x/oauth2" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/api/iterator" "google.golang.org/api/option" "google.golang.org/grpc" @@ -77,7 +77,7 @@ var _ RuntimeConfig = &runtimeConfig{} type runtimeConfig struct { enrollConfig *bootstrap.Config // TODO: convert to enroll.Config config *config.Config - privateKey []byte + privateKey wgtypes.Key tokens *auth.Tokens tenants []*pb.Tenant log *logrus.Entry @@ -154,7 +154,7 @@ func (rc *runtimeConfig) ConnectToAPIServer(ctx context.Context) (pb.APIServerCl func (rc *runtimeConfig) BuildHelperConfiguration(peers []*pb.Gateway) *pb.Configuration { return &pb.Configuration{ - PrivateKey: base64.StdEncoding.EncodeToString(rc.privateKey), + PrivateKey: rc.privateKey.String(), DeviceIPv4: rc.enrollConfig.DeviceIPv4, DeviceIPv6: rc.enrollConfig.DeviceIPv6, Gateways: peers, @@ -207,7 +207,7 @@ func New(log *logrus.Entry, cfg *config.Config) (RuntimeConfig, error) { return nil, fmt.Errorf("ensuring private key: %w", err) } - rc.log.WithField("public_key", wireguard.PublicKey(rc.privateKey)).Info("runtime config initialized") + rc.log.WithField("public_key", rc.privateKey.PublicKey().String()).Info("runtime config initialized") return rc, nil } @@ -231,7 +231,7 @@ func (r *runtimeConfig) enroll(ctx context.Context, serial, token string) error req := &enroll.DeviceRequest{ Platform: r.config.Platform, Serial: serial, - WireGuardPublicKey: wireguard.PublicKey(r.privateKey), + WireGuardPublicKey: r.privateKey.PublicKey().String(), } buf := &bytes.Buffer{} diff --git a/internal/deviceagent/wireguard/wireguard.go b/internal/deviceagent/wireguard/wireguard.go index e3c080f0e..eb6fd815b 100644 --- a/internal/deviceagent/wireguard/wireguard.go +++ b/internal/deviceagent/wireguard/wireguard.go @@ -1,112 +1,52 @@ package wireguard import ( - "crypto/rand" - "encoding/base64" "fmt" - "io" "os" - "sort" "strings" - "github.com/nais/device/internal/wireguard" - "golang.org/x/crypto/curve25519" - "github.com/nais/device/internal/deviceagent/filesystem" - "github.com/nais/device/pkg/pb" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -func KeyToBase64(key []byte) []byte { - dst := make([]byte, base64.StdEncoding.EncodedLen(len(key))) - base64.StdEncoding.Encode(dst, key) - return dst -} - -func Base64toKey(encoded []byte) ([]byte, error) { - decoded := make([]byte, 32) - _, err := base64.StdEncoding.Decode(decoded, encoded) - if err != nil { - return nil, fmt.Errorf("decoding base64 key: %w", err) - } - - return decoded, nil -} - -func WgGenKey() []byte { - var privateKey [32]byte - - n, err := rand.Read(privateKey[:]) - - if err != nil || n != len(privateKey) { - panic("Unable to generate random bytes") - } - - privateKey[0] &= 248 - privateKey[31] = (privateKey[31] & 127) | 64 - return privateKey[:] -} - -func WGPubKey(privateKeySlice []byte) []byte { - var privateKey [32]byte - var publicKey [32]byte - copy(privateKey[:], privateKeySlice[:]) - - curve25519.ScalarBaseMult(&publicKey, &privateKey) - - return publicKey[:] -} - -func EnsurePrivateKey(keyPath string) ([]byte, error) { +func EnsurePrivateKey(keyPath string) (wgtypes.Key, error) { if err := filesystem.FileMustExist(keyPath); os.IsNotExist(err) { - if err := os.WriteFile(keyPath, KeyToBase64(WgGenKey()), 0o600); err != nil { - return nil, fmt.Errorf("writing private key to disk: %w", err) + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + return wgtypes.Key{}, fmt.Errorf("generating private key: %w", err) } + if err := os.WriteFile(keyPath, []byte(key.String()), 0o600); err != nil { + return wgtypes.Key{}, fmt.Errorf("writing private key to disk: %w", err) + } + return key, nil } else if err != nil { - return nil, fmt.Errorf("ensuring private key exists: %w", err) + return wgtypes.Key{}, fmt.Errorf("ensuring private key exists: %w", err) } privateKeyEncoded, err := os.ReadFile(keyPath) if err != nil { - return nil, fmt.Errorf("reading private key: %v", err) + return wgtypes.Key{}, fmt.Errorf("reading private key: %w", err) } - privateKey, err := Base64toKey(privateKeyEncoded) - if err != nil { - return nil, fmt.Errorf("decoding private key: %v", err) + if key, err := wgtypes.ParseKey(strings.TrimSpace(string(privateKeyEncoded))); err == nil { + return key, nil } - return privateKey, nil -} - -func PublicKey(privateKey []byte) []byte { - return KeyToBase64(WGPubKey(privateKey)) -} - -func sortGateways(gateways []*pb.Gateway) { - if gateways == nil { - return + if len(privateKeyEncoded) != len(wgtypes.Key{}) { + return wgtypes.Key{}, fmt.Errorf("parsing private key: invalid key length %d", len(privateKeyEncoded)) } - sort.Slice(gateways, func(i, j int) bool { - return strings.Compare(gateways[i].Name, gateways[j].Name) < 0 - }) -} -func Marshal(w io.Writer, x *pb.Configuration) error { - // Sort gateways here to let windows helper detect changes in, and prevent unnecessary restarts - gateways := x.GetGateways() - sortGateways(gateways) - - cf := &wireguard.Config{ - Peers: wireguard.CastPeerList(gateways), - PrivateKey: x.GetPrivateKey(), + legacyKey, err := wgtypes.NewKey(privateKeyEncoded) + if err != nil { + return wgtypes.Key{}, fmt.Errorf("parsing legacy private key: %w", err) } - // Address configuration only supported on Windows - if windows { - cf.MTU = mtu - cf.AddressV4 = x.GetDeviceIPv4() - cf.AddressV6 = x.GetDeviceIPv6() + if err := os.WriteFile(keyPath, []byte(legacyKey.String()), 0o600); err != nil { + return wgtypes.Key{}, fmt.Errorf("rewriting legacy private key to disk: %w", err) + } + if err := os.Chmod(keyPath, 0o600); err != nil { + return wgtypes.Key{}, fmt.Errorf("setting private key file mode: %w", err) } - return cf.MarshalINI(w) + return legacyKey, nil } diff --git a/internal/deviceagent/wireguard/wireguard_test.go b/internal/deviceagent/wireguard/wireguard_test.go index 7211c83db..46b13aa16 100644 --- a/internal/deviceagent/wireguard/wireguard_test.go +++ b/internal/deviceagent/wireguard/wireguard_test.go @@ -1,15 +1,85 @@ package wireguard_test import ( + "os" + "path/filepath" "testing" "github.com/nais/device/internal/deviceagent/wireguard" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -func TestWGGenKey(t *testing.T) { - privateKey := wireguard.WgGenKey() - assert.Len(t, privateKey, 32) - privateKeyB64 := wireguard.KeyToBase64(privateKey) - assert.Len(t, privateKeyB64, 44) +func TestGenKey(t *testing.T) { + key, err := wgtypes.GeneratePrivateKey() + assert.NoError(t, err) + assert.Len(t, key, 32) + assert.Len(t, key.String(), 44) +} + +func TestEnsurePrivateKey_CreatesNewKey(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "private.key") + + key, err := wireguard.EnsurePrivateKey(keyPath) + require.NoError(t, err) + assert.NotEqual(t, wgtypes.Key{}, key, "generated key should not be zero") + + // Verify the key was written to disk + data, err := os.ReadFile(keyPath) + require.NoError(t, err) + assert.Equal(t, key.String(), string(data)) +} + +func TestEnsurePrivateKey_ReadsExistingKey(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "private.key") + + // Write a known key to disk + original, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + require.NoError(t, os.WriteFile(keyPath, []byte(original.String()), 0o600)) + + // EnsurePrivateKey should read it back + key, err := wireguard.EnsurePrivateKey(keyPath) + require.NoError(t, err) + assert.Equal(t, original, key) +} + +func TestEnsurePrivateKey_IdempotentAcrossCalls(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "private.key") + + key1, err := wireguard.EnsurePrivateKey(keyPath) + require.NoError(t, err) + + key2, err := wireguard.EnsurePrivateKey(keyPath) + require.NoError(t, err) + + assert.Equal(t, key1, key2, "same key should be returned across calls") +} + +func TestEnsurePrivateKey_MigratesLegacyRawKey(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "private.key") + + original, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + + legacyBytes := make([]byte, len(original)) + copy(legacyBytes, original[:]) + require.NoError(t, os.WriteFile(keyPath, legacyBytes, 0o644)) + + key, err := wireguard.EnsurePrivateKey(keyPath) + require.NoError(t, err) + assert.Equal(t, original, key) + + migrated, err := os.ReadFile(keyPath) + require.NoError(t, err) + assert.Equal(t, original.String(), string(migrated)) + + info, err := os.Stat(keyPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o600), info.Mode().Perm()) } diff --git a/internal/deviceagent/wireguard/wireguard_unix.go b/internal/deviceagent/wireguard/wireguard_unix.go deleted file mode 100644 index 288dc2c30..000000000 --- a/internal/deviceagent/wireguard/wireguard_unix.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build linux || darwin - -package wireguard - -import ( - "fmt" - "io" - - "github.com/nais/device/pkg/pb" -) - -var wireGuardTemplateHeader = `[Interface] -PrivateKey = %s - -` - -// mtu is no-op on non-windows platforms -const ( - mtu = 0 - windows = false -) - -func MarshalHeader(w io.Writer, x *pb.Configuration) (int, error) { - return fmt.Fprintf(w, wireGuardTemplateHeader, x.GetPrivateKey()) -} diff --git a/internal/deviceagent/wireguard/wireguard_unix_test.go b/internal/deviceagent/wireguard/wireguard_unix_test.go deleted file mode 100644 index 3a61281b8..000000000 --- a/internal/deviceagent/wireguard/wireguard_unix_test.go +++ /dev/null @@ -1,57 +0,0 @@ -//go:build linux || darwin - -package wireguard_test - -import ( - "bytes" - "testing" - - "github.com/nais/device/internal/deviceagent/wireguard" - "github.com/nais/device/pkg/pb" - "github.com/stretchr/testify/assert" -) - -func TestMarshalConfiguration(t *testing.T) { - cfg := &pb.Configuration{ - PrivateKey: "abc", - DeviceIPv4: "127.0.0.1", - Gateways: []*pb.Gateway{ - { - Name: "gateway-1", - PublicKey: "PQKmraPOPye5CJq1x7njpl8rRu5RSrIKyHvZXtLvS0E=", - Endpoint: "13.37.13.37:51820", - Ipv4: "10.255.240.2", - Ipv6: "fd00::2", - RoutesIPv4: []string{"13.37.69.0/24", "13.37.59.69/32"}, - }, - { - Name: "gateway-2", - PublicKey: "foobar", - Endpoint: "14.37.13.37:51820", - Ipv4: "11.255.240.2", - RoutesIPv4: []string{"14.37.69.0/24", "14.37.59.69/32"}, - }, - }, - } - - buf := new(bytes.Buffer) - err := wireguard.Marshal(buf, cfg) - - assert.NoError(t, err) - - expected := `[Interface] -PrivateKey = abc - -[Peer] # gateway-1 -PublicKey = PQKmraPOPye5CJq1x7njpl8rRu5RSrIKyHvZXtLvS0E= -AllowedIPs = 13.37.69.0/24,13.37.59.69/32,10.255.240.2/32,fd00::2/128 -Endpoint = 13.37.13.37:51820 - -[Peer] # gateway-2 -PublicKey = foobar -AllowedIPs = 14.37.69.0/24,14.37.59.69/32,11.255.240.2/32 -Endpoint = 14.37.13.37:51820 - -` - assert.Equal(t, expected, buf.String()) -} diff --git a/internal/deviceagent/wireguard/wireguard_windows.go b/internal/deviceagent/wireguard/wireguard_windows.go deleted file mode 100644 index 1c7932586..000000000 --- a/internal/deviceagent/wireguard/wireguard_windows.go +++ /dev/null @@ -1,27 +0,0 @@ -package wireguard - -import ( - "fmt" - "io" - - "github.com/nais/device/pkg/pb" -) - -/* -On windows we use "WireGuard-windows" client, which is basically a GUI wrapper of wg-quick. This config file requires -MTU and Address as additional fields because this also sets up the WireGuard interface for us. -*/ -var wireGuardTemplateHeader = `[Interface] -PrivateKey = %s -MTU = %d -Address = %s -` - -const ( - windows = true - mtu = 1360 -) - -func MarshalHeader(w io.Writer, x *pb.Configuration) (int, error) { - return fmt.Fprintf(w, wireGuardTemplateHeader, x.GetPrivateKey(), mtu, x.GetDeviceIPv4()) -} diff --git a/internal/deviceagent/wireguard/wireguard_windows_test.go b/internal/deviceagent/wireguard/wireguard_windows_test.go deleted file mode 100644 index 0240cd5b1..000000000 --- a/internal/deviceagent/wireguard/wireguard_windows_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package wireguard_test - -import ( - "bytes" - "testing" - - "github.com/nais/device/internal/deviceagent/wireguard" - "github.com/nais/device/pkg/pb" - "github.com/stretchr/testify/assert" -) - -func TestMarshalConfiguration(t *testing.T) { - cfg := &pb.Configuration{ - PrivateKey: "abc", - DeviceIP: "127.0.0.1", - Gateways: []*pb.Gateway{ - { - Name: "gateway-1", - PublicKey: "PQKmraPOPye5CJq1x7njpl8rRu5RSrIKyHvZXtLvS0E=", - Endpoint: "13.37.13.37:51820", - Ip: "10.255.240.2", - Routes: []string{"13.37.69.0/24", "13.37.59.69/32"}, - }, - { - Name: "gateway-2", - PublicKey: "foobar", - Endpoint: "14.37.13.37:51820", - Ip: "11.255.240.2", - Routes: []string{"14.37.69.0/24", "14.37.59.69/32"}, - }, - }, - } - - buf := new(bytes.Buffer) - err := wireguard.Marshal(buf, cfg) - - assert.NoError(t, err) - - expected := `[Interface] -PrivateKey = YWJj -MTU = 1360 -Address = 127.0.0.1 - -[Peer] # gateway-1 -PublicKey = PQKmraPOPye5CJq1x7njpl8rRu5RSrIKyHvZXtLvS0E= -AllowedIPs = 13.37.69.0/24,13.37.59.69/32,10.255.240.2/32 -Endpoint = 13.37.13.37:51820 - -[Peer] # gateway-2 -PublicKey = foobar -AllowedIPs = 14.37.69.0/24,14.37.59.69/32,11.255.240.2/32 -Endpoint = 14.37.13.37:51820 - -` - assert.Equal(t, expected, buf.String()) -} diff --git a/internal/enroll/enroll.go b/internal/enroll/enroll.go index b71898e0b..736661312 100644 --- a/internal/enroll/enroll.go +++ b/internal/enroll/enroll.go @@ -6,11 +6,11 @@ type DeviceRequest struct { Platform string `json:"platform"` Owner string `json:"owner"` Serial string `json:"serial"` - WireGuardPublicKey []byte `json:"wireguard_public_key"` + WireGuardPublicKey string `json:"wireguard_public_key"` } type GatewayRequest struct { - WireGuardPublicKey []byte `json:"wireguard_public_key"` + WireGuardPublicKey string `json:"wireguard_public_key"` Name string `json:"name"` Endpoint string `json:"endpoint"` HashedPassword string `json:"hashed_password"` diff --git a/internal/enroll/gateway.go b/internal/enroll/gateway.go index 2d8e6a3d2..420a94b87 100644 --- a/internal/enroll/gateway.go +++ b/internal/enroll/gateway.go @@ -16,7 +16,7 @@ import ( ) type GatewayClient struct { - wireGuardPublicKey []byte + wireGuardPublicKey string port int hashedPassword string log logrus.FieldLogger @@ -28,7 +28,7 @@ type GatewayClient struct { ExternalIP string `json:"external_ip"` } -func NewGatewayClient(ctx context.Context, publicKey []byte, hashedPassword string, wireguardListenPort int, log logrus.FieldLogger) (*GatewayClient, error) { +func NewGatewayClient(ctx context.Context, publicKey string, hashedPassword string, wireguardListenPort int, log logrus.FieldLogger) (*GatewayClient, error) { b, err := GetGoogleMetadata(ctx, "instance/attributes/enroll-config", log) if err != nil { return nil, err diff --git a/internal/enroll/handler.go b/internal/enroll/handler.go index f2ebf4948..e7f04e386 100644 --- a/internal/enroll/handler.go +++ b/internal/enroll/handler.go @@ -32,6 +32,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } + + normalizedPublicKey, err := NormalizeWireGuardPublicKey(req.WireGuardPublicKey) + if err != nil { + h.log.WithError(err).Error("invalid wireguard public key") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + req.WireGuardPublicKey = normalizedPublicKey + req.Owner = token.GetEmail(r.Context()) resp, err := h.worker.Send(r.Context(), &req) diff --git a/internal/enroll/handler_test.go b/internal/enroll/handler_test.go new file mode 100644 index 000000000..93b9486f8 --- /dev/null +++ b/internal/enroll/handler_test.go @@ -0,0 +1,107 @@ +package enroll + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/nais/device/internal/token" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type capturingWorker struct { + request *DeviceRequest + err error +} + +func (w *capturingWorker) Run(ctx context.Context) error { + return nil +} + +func (w *capturingWorker) Send(ctx context.Context, req *DeviceRequest) (*Response, error) { + w.request = req + if w.err != nil { + return nil, w.err + } + + return &Response{APIServerGRPCAddress: "test"}, nil +} + +func TestHandlerPublicKeyValidationAndNormalization(t *testing.T) { + privateKey, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + + canonical := privateKey.PublicKey().String() + legacyEncoded := base64.StdEncoding.EncodeToString([]byte(canonical)) + + tests := []struct { + name string + publicKey string + wantStatus int + wantWorkerCalled bool + wantStoredPublicKey string + wantErrBody string + }{ + { + name: "canonical key", + publicKey: canonical, + wantStatus: http.StatusOK, + wantWorkerCalled: true, + wantStoredPublicKey: canonical, + }, + { + name: "legacy base64 encoded key", + publicKey: legacyEncoded, + wantStatus: http.StatusOK, + wantWorkerCalled: true, + wantStoredPublicKey: canonical, + }, + { + name: "invalid key", + publicKey: "invalid", + wantStatus: http.StatusBadRequest, + wantWorkerCalled: false, + wantErrBody: "invalid wireguard public key: expected canonical key or base64-encoded canonical key\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + worker := &capturingWorker{} + handler := NewHandler(worker, logrus.NewEntry(logrus.New())) + + payload, err := json.Marshal(&DeviceRequest{ + Platform: "darwin", + Serial: "serial-1", + WireGuardPublicKey: tt.publicKey, + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payload)) + req = req.WithContext(token.WithEmail(req.Context(), "user@example.com")) + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + assert.Equal(t, tt.wantStatus, rr.Code) + if tt.wantErrBody != "" { + assert.Equal(t, tt.wantErrBody, rr.Body.String()) + } + + if tt.wantWorkerCalled { + require.NotNil(t, worker.request) + assert.Equal(t, tt.wantStoredPublicKey, worker.request.WireGuardPublicKey) + assert.Equal(t, "user@example.com", worker.request.Owner) + } else { + assert.Nil(t, worker.request) + } + }) + } +} diff --git a/internal/enroll/publickey.go b/internal/enroll/publickey.go new file mode 100644 index 000000000..36527c1a4 --- /dev/null +++ b/internal/enroll/publickey.go @@ -0,0 +1,34 @@ +package enroll + +import ( + "encoding/base64" + "fmt" + "strings" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func NormalizeWireGuardPublicKey(raw string) (string, error) { + canonicalCandidate := strings.TrimSpace(raw) + if canonicalCandidate == "" { + return "", fmt.Errorf("invalid wireguard public key: empty value") + } + + canonicalKey, canonicalErr := wgtypes.ParseKey(canonicalCandidate) + if canonicalErr == nil { + return canonicalKey.String(), nil + } + + decoded, decodeErr := base64.StdEncoding.DecodeString(canonicalCandidate) + if decodeErr != nil { + return "", fmt.Errorf("invalid wireguard public key: expected canonical key or base64-encoded canonical key") + } + + legacyCandidate := strings.TrimSpace(string(decoded)) + legacyKey, legacyErr := wgtypes.ParseKey(legacyCandidate) + if legacyErr != nil { + return "", fmt.Errorf("invalid wireguard public key: expected canonical key or base64-encoded canonical key") + } + + return legacyKey.String(), nil +} diff --git a/internal/enroll/publickey_test.go b/internal/enroll/publickey_test.go new file mode 100644 index 000000000..a373254cc --- /dev/null +++ b/internal/enroll/publickey_test.go @@ -0,0 +1,60 @@ +package enroll + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func TestNormalizeWireGuardPublicKey(t *testing.T) { + privateKey, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + + canonical := privateKey.PublicKey().String() + legacyEncoded := base64.StdEncoding.EncodeToString([]byte(canonical)) + + tests := []struct { + name string + input string + want string + wantErr string + }{ + { + name: "canonical key", + input: canonical, + want: canonical, + }, + { + name: "legacy base64 encoded canonical key", + input: legacyEncoded, + want: canonical, + }, + { + name: "malformed key", + input: "not-a-wireguard-key", + wantErr: "invalid wireguard public key: expected canonical key or base64-encoded canonical key", + }, + { + name: "empty key", + input: " ", + wantErr: "invalid wireguard public key: empty value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NormalizeWireGuardPublicKey(tt.input) + if tt.wantErr != "" { + require.Error(t, err) + assert.Equal(t, tt.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/helper/config.go b/internal/helper/config.go index 6f4d0a296..352478245 100644 --- a/internal/helper/config.go +++ b/internal/helper/config.go @@ -1,7 +1,6 @@ package helper type Config struct { - Interface string - LogLevel string - WireGuardConfigPath string + Interface string + LogLevel string } diff --git a/internal/helper/dns/dns_linux.go b/internal/helper/dns/dns_linux.go index 6b0c8b3e0..8fe99d01a 100644 --- a/internal/helper/dns/dns_linux.go +++ b/internal/helper/dns/dns_linux.go @@ -9,6 +9,8 @@ import ( "os/exec" "path/filepath" "strings" + + log "github.com/sirupsen/logrus" ) const ( @@ -16,6 +18,8 @@ const ( ) func apply(zones []string) error { + log.WithField("config_file", configFilePath).Debug("applying DNS config for systemd-resolved") + err := os.Mkdir(filepath.Dir(configFilePath), 0o755) if err != nil && !errors.Is(err, fs.ErrExist) { return err @@ -30,6 +34,7 @@ func apply(zones []string) error { if err != nil { return err } + log.WithField("zones", zones).Debug("wrote DNS config file") return reload() } @@ -50,10 +55,12 @@ DNSOverTLS=opportunistic } func reload() error { + log.Debug("restarting systemd-resolved") out, err := exec.Command("systemctl", "restart", "systemd-resolved.service").CombinedOutput() if err != nil { return fmt.Errorf("reloading systemd-resolved: %w: %s", err, string(out)) } + log.Debug("systemd-resolved restarted successfully") return nil } diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 9152627b7..4a6e15776 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -6,20 +6,15 @@ package helper import ( - "bytes" "context" "fmt" - "io" - "os" "time" "github.com/nais/device/internal/helper/serial" - "github.com/nais/device/internal/ioconvenience" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/nais/device/internal/deviceagent/wireguard" "github.com/nais/device/pkg/pb" ) @@ -57,16 +52,7 @@ func (dhs *DeviceHelperServer) Teardown( dhs.log.WithField("interface", dhs.config.Interface).Info("removing network interface and all routes") err := dhs.osConfigurator.TeardownInterface(ctx) if err != nil { - return nil, fmt.Errorf("tearing down interface: %v", err) - } - - dhs.log.Info("flushing WireGuard configuration from disk") - err = os.Remove(dhs.config.WireGuardConfigPath) - if err != nil { - if !os.IsNotExist(err) { - return nil, fmt.Errorf("flush WireGuard configuration from disk: %v", err) - } - dhs.log.Info("WireGuard configuration file does not exist on disk") + return nil, fmt.Errorf("tearing down interface: %w", err) } return &pb.TeardownResponse{}, nil @@ -76,28 +62,38 @@ func (dhs *DeviceHelperServer) Configure( ctx context.Context, cfg *pb.Configuration, ) (*pb.ConfigureResponse, error) { - dhs.log.Info("new configuration received from device-agent") - - err := dhs.writeConfigFile(cfg) - if err != nil { - return nil, status.Errorf(codes.ResourceExhausted, "write WireGuard configuration: %s", err) + dhs.log.WithField("num_gateways", len(cfg.GetGateways())).Info("new configuration received from device-agent") + for _, gw := range cfg.GetGateways() { + dhs.log.WithFields(logrus.Fields{ + "gateway": gw.GetName(), + "endpoint": gw.GetEndpoint(), + "ipv4": gw.GetIpv4(), + "ipv6": gw.GetIpv6(), + "routes_ipv4": gw.GetRoutesIPv4(), + "routes_ipv6": gw.GetRoutesIPv6(), + }).Debug("gateway in configuration") } - dhs.log.Info("wrote WireGuard config to disk") - - err = dhs.osConfigurator.SetupInterface(ctx, cfg) + dhs.log.Debug("setting up interface") + err := dhs.osConfigurator.SetupInterface(ctx, cfg) if err != nil { return nil, status.Errorf(codes.FailedPrecondition, "setup interface and routes: %s", err) } + dhs.log.Debug("interface setup complete") + dhs.log.Debug("syncing wireguard configuration") var loopErr error for attempt := range 5 { loopErr = dhs.osConfigurator.SyncConf(ctx, cfg) if loopErr != nil { - backoff := time.Duration(attempt) * time.Second + backoff := time.Duration(attempt+1) * time.Second dhs.log.WithError(loopErr).Error("synchronize WireGuard configuration") dhs.log.WithField("attempt", attempt+1).WithField("backoff", backoff).Info("configuring failed, sleeping before retrying") - time.Sleep(backoff) + select { + case <-ctx.Done(): + return nil, status.Errorf(codes.Canceled, "context canceled during WireGuard sync retry: %s", loopErr) + case <-time.After(backoff): + } continue } break @@ -109,41 +105,22 @@ func (dhs *DeviceHelperServer) Configure( loopErr, ) } + dhs.log.Debug("wireguard configuration synced") + // TODO: SetupRoutes only adds routes; it does not remove routes for gateways + // that were present in a previous configuration but are now absent. Stale routes + // are effectively harmless (they black-hole to a non-existent WireGuard peer), + // but a proper implementation should diff and clean up removed routes. + dhs.log.Debug("setting up routes") _, err = dhs.osConfigurator.SetupRoutes(ctx, cfg.GetGateways()) if err != nil { return nil, status.Errorf(codes.FailedPrecondition, "setting up routes: %s", err) } + dhs.log.Debug("routes setup complete, configure finished successfully") return &pb.ConfigureResponse{}, nil } -func (dhs *DeviceHelperServer) writeConfigFile(cfg *pb.Configuration) error { - buf := new(bytes.Buffer) - - err := wireguard.Marshal(buf, cfg) - if err != nil { - return fmt.Errorf("render configuration: %s", err) - } - - fd, err := os.OpenFile(dhs.config.WireGuardConfigPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o600) - if err != nil { - return fmt.Errorf("open file: %s", err) - } - defer ioconvenience.CloseWithLog(fd, dhs.log) - - _, err = io.Copy(fd, buf) - if err != nil { - return fmt.Errorf("write to disk: %s", err) - } - - if err := fd.Sync(); err != nil { - return fmt.Errorf("sync file: %s", err) - } - - return nil -} - func (dhs *DeviceHelperServer) GetSerial( context.Context, *pb.GetSerialRequest, diff --git a/internal/helper/helper_darwin.go b/internal/helper/helper_darwin.go index 0e994d761..2981c3341 100644 --- a/internal/helper/helper_darwin.go +++ b/internal/helper/helper_darwin.go @@ -2,96 +2,100 @@ package helper import ( "context" + "errors" "fmt" + "net" + "net/netip" "os" "os/exec" - "strings" + "sync" + "syscall" + "time" + "github.com/sirupsen/logrus" + "golang.org/x/net/route" + "golang.org/x/sys/unix" + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/ipc" + "golang.zx2c4.com/wireguard/tun" + + "github.com/nais/device/internal/iputil" + "github.com/nais/device/internal/wgconfig" "github.com/nais/device/pkg/pb" ) type DarwinConfigurator struct { - helperConfig Config - wireGuardBinary string - wireGuardGoBinary string + helperConfig Config + + mu sync.Mutex + wgDevice *device.Device + tunDev tun.Device + uapi net.Listener + ifaceName string // actual interface name assigned by macOS (may differ from requested) + tunnelNet netip.Prefix } var _ OSConfigurator = &DarwinConfigurator{} -func New(helperConfig Config) *DarwinConfigurator { +func New(helperConfig Config, _ *logrus.Entry) *DarwinConfigurator { return &DarwinConfigurator{ helperConfig: helperConfig, } } -func pathWithFallBacks(binary string, possiblePaths ...string) (string, error) { - if p, err := exec.LookPath(binary); err == nil { - return p, nil - } +func (c *DarwinConfigurator) Prerequisites() error { + return nil +} - for _, p := range possiblePaths { - if s, err := os.Stat(p); err == nil && !s.IsDir() { - return p, nil - } +func (c *DarwinConfigurator) SyncConf(ctx context.Context, cfg *pb.Configuration) error { + c.mu.Lock() + ifaceName := c.ifaceName + c.mu.Unlock() + if ifaceName == "" { + ifaceName = c.helperConfig.Interface } - - return "", fmt.Errorf("%q not found in PATH or any of %+v", binary, possiblePaths) + return wgconfig.ApplyConfig(ctx, ifaceName, cfg) } -func (c *DarwinConfigurator) Prerequisites() error { - var err error - - c.wireGuardBinary, err = pathWithFallBacks("wg", "/usr/local/bin/wg", "/opt/homebrew/bin/wg", "/usr/bin/wg") - if err != nil { - return fmt.Errorf("look for wg: %w", err) +func (c *DarwinConfigurator) SetupRoutes(ctx context.Context, gateways []*pb.Gateway) (int, error) { + c.mu.Lock() + ifaceName := c.ifaceName + tunnelNet := c.tunnelNet + c.mu.Unlock() + if ifaceName == "" { + ifaceName = c.helperConfig.Interface } - c.wireGuardGoBinary, err = pathWithFallBacks("wireguard-go", "/usr/local/bin/wireguard-go", "/opt/homebrew/bin/wireguard-go", "/usr/bin/wireguard-go") + iface, err := net.InterfaceByName(ifaceName) if err != nil { - return fmt.Errorf("look for wireguard-go: %w", err) + return 0, fmt.Errorf("lookup interface %q: %w", ifaceName, err) } - return nil -} - -func (c *DarwinConfigurator) SyncConf(ctx context.Context, cfg *pb.Configuration) error { - cmd := exec.CommandContext(ctx, c.wireGuardBinary, "syncconf", c.helperConfig.Interface, c.helperConfig.WireGuardConfigPath) - if b, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("running syncconf: %w: %v", err, string(b)) + fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC) + if err != nil { + return 0, fmt.Errorf("open routing socket: %w", err) } + defer func() { _ = unix.Close(fd) }() - return nil -} - -func (c *DarwinConfigurator) SetupRoutes(ctx context.Context, gateways []*pb.Gateway) (int, error) { routesAdded := 0 for _, gw := range gateways { - applyRoute := func(cidr, family string) error { - cmd := exec.CommandContext(ctx, "route", "-q", "-n", "add", family, cidr, "-interface", c.helperConfig.Interface) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("executing %v, err: %w, stderr: %s", cmd, err, string(output)) - } - - return nil - } - for _, cidr := range gw.GetRoutesIPv4() { - if strings.HasPrefix(cidr, TunnelNetworkPrefix) { - // Don't add routes for the tunnel network, as the whole /21 net is already routed to utun + if IsTunnelRoute(tunnelNet, cidr) { continue } - err := applyRoute(cidr, "-inet") - if err != nil { - return routesAdded, err + if err := addRouteViaInterface(fd, cidr, iface); err != nil { + return routesAdded, fmt.Errorf("add IPv4 route %s: %w", cidr, err) } routesAdded++ } for _, cidr := range gw.GetRoutesIPv6() { - err := applyRoute(cidr, "-inet6") - if err != nil { - return routesAdded, err + if IsTunnelRoute(tunnelNet, cidr) { + continue + } + if err := addRouteViaInterface(fd, cidr, iface); err != nil { + return routesAdded, fmt.Errorf("add IPv6 route %s: %w", cidr, err) } routesAdded++ } @@ -101,37 +105,208 @@ func (c *DarwinConfigurator) SetupRoutes(ctx context.Context, gateways []*pb.Gat } func (c *DarwinConfigurator) SetupInterface(ctx context.Context, cfg *pb.Configuration) error { - if c.interfaceExists(ctx) { + c.mu.Lock() + defer c.mu.Unlock() + + tunnelNet, err := TunnelNetworkFromIP(cfg.GetDeviceIPv4()) + if err != nil { + return fmt.Errorf("derive tunnel network: %w", err) + } + c.tunnelNet = tunnelNet + + if c.wgDevice != nil { return nil } + tunDev, err := tun.CreateTUN(c.helperConfig.Interface, wireguardMTU) + if err != nil { + return fmt.Errorf("create TUN device: %w", err) + } + + ifaceName, err := tunDev.Name() + if err != nil { + _ = tunDev.Close() + return fmt.Errorf("get TUN device name: %w", err) + } + + logger := &device.Logger{ + Verbosef: device.DiscardLogf, + Errorf: func(format string, args ...any) { fmt.Fprintf(os.Stderr, "wireguard: "+format+"\n", args...) }, + } + wgDev := device.NewDevice(tunDev, conn.NewDefaultBind(), logger) + + if err := wgDev.Up(); err != nil { + wgDev.Close() + return fmt.Errorf("bring up wireguard device: %w", err) + } + + // UAPI socket allows wgctrl to configure the device + fileUAPI, err := ipc.UAPIOpen(ifaceName) + if err != nil { + wgDev.Close() + return fmt.Errorf("open UAPI socket: %w", err) + } + + uapi, err := ipc.UAPIListen(ifaceName, fileUAPI) + if err != nil { + _ = fileUAPI.Close() + wgDev.Close() + return fmt.Errorf("listen on UAPI socket: %w", err) + } + + c.wgDevice = wgDev + c.tunDev = tunDev + c.uapi = uapi + c.ifaceName = ifaceName + + // Configure IP addresses and bring the interface up. + // We shell out to ifconfig because macOS has no stable public API for assigning + // addresses to utun interfaces — the required SIOCAIFADDR_IN6 / SIOCSIFADDR + // ioctls are undocumented and vary across OS versions. ifconfig is the standard + // tool used by the macOS networking stack itself. commands := [][]string{ - {c.wireGuardGoBinary, c.helperConfig.Interface}, - {"ifconfig", c.helperConfig.Interface, "inet", cfg.GetDeviceIPv4() + "/21", cfg.GetDeviceIPv4(), "alias"}, - {"ifconfig", c.helperConfig.Interface, "inet6", cfg.GetDeviceIPv6() + "/64", "alias"}, - {"ifconfig", c.helperConfig.Interface, "mtu", "1360"}, - {"ifconfig", c.helperConfig.Interface, "up"}, - {"route", "-q", "-n", "add", "-inet", cfg.GetDeviceIPv4() + "/21", "-interface", c.helperConfig.Interface}, + {"ifconfig", ifaceName, "inet", cfg.GetDeviceIPv4() + "/21", cfg.GetDeviceIPv4(), "alias"}, } + if cfg.GetDeviceIPv6() != "" { + commands = append(commands, []string{"ifconfig", ifaceName, "inet6", cfg.GetDeviceIPv6() + "/64", "alias"}) + } + commands = append(commands, []string{"ifconfig", ifaceName, "up"}) + + for _, s := range commands { + cmd := exec.CommandContext(ctx, s[0], s[1:]...) - return runCommands(ctx, commands) + if out, err := cmd.CombinedOutput(); err != nil { + c.closeLocked() + return fmt.Errorf("running %v: %w: %v", cmd, err, string(out)) + } + + // Small delay between ifconfig calls to avoid kernel ENOMEM / EBUSY + // errors when rapidly configuring addresses on a freshly-created utun. + time.Sleep(100 * time.Millisecond) + } + + // Add /21 route for the tunnel network + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + c.closeLocked() + return fmt.Errorf("lookup interface %q: %w", ifaceName, err) + } + + routeFd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC) + if err != nil { + c.closeLocked() + return fmt.Errorf("open routing socket: %w", err) + } + defer func() { _ = unix.Close(routeFd) }() + + if err := addRouteViaInterface(routeFd, cfg.GetDeviceIPv4()+"/21", iface); err != nil { + c.closeLocked() + return fmt.Errorf("add tunnel network route: %w", err) + } + + // Start accepting UAPI connections only after all initialization has succeeded. + // This ensures wgctrl can't race against incomplete interface setup. + go func() { + for { + uapiConn, err := uapi.Accept() + if err != nil { + return + } + go wgDev.IpcHandle(uapiConn) + } + }() + + return nil } func (c *DarwinConfigurator) TeardownInterface(ctx context.Context) error { - if !c.interfaceExists(ctx) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.wgDevice == nil { return nil } - cmd := exec.CommandContext(ctx, "pkill", "-f", fmt.Sprintf("%s %s", c.wireGuardGoBinary, c.helperConfig.Interface)) - out, err := cmd.CombinedOutput() + c.closeLocked() + return nil +} + +// closeLocked shuts down the UAPI listener, WireGuard device, and TUN. +// Must be called with c.mu held. +func (c *DarwinConfigurator) closeLocked() { + if c.uapi != nil { + _ = c.uapi.Close() + c.uapi = nil + } + if c.wgDevice != nil { + c.wgDevice.Close() + c.wgDevice = nil + } + // wgDevice.Close() also closes tunDev + c.tunDev = nil + c.ifaceName = "" +} + +// addRouteViaInterface adds a route for the given CIDR through the specified +// network interface using the provided BSD routing socket fd. +func addRouteViaInterface(fd int, cidr string, iface *net.Interface) error { + prefix, err := iputil.ParsePrefix(cidr) if err != nil { - return fmt.Errorf("teardown failed: %w, stderr: %s", err, string(out)) + return fmt.Errorf("add route: %w", err) } - return nil -} + addrs := make([]route.Addr, syscall.RTAX_MAX) + + if prefix.Addr().Is6() { + dst := prefix.Addr().As16() + addrs[syscall.RTAX_DST] = &route.Inet6Addr{IP: dst} -func (c *DarwinConfigurator) interfaceExists(ctx context.Context) bool { - cmd := exec.CommandContext(ctx, "pgrep", "-f", fmt.Sprintf("%s %s", c.wireGuardGoBinary, c.helperConfig.Interface)) - return cmd.Run() == nil + var mask [16]byte + ones := prefix.Bits() + for i := range ones { + mask[i/8] |= 1 << (7 - i%8) + } + addrs[syscall.RTAX_NETMASK] = &route.Inet6Addr{IP: mask} + } else { + dst := prefix.Addr().As4() + addrs[syscall.RTAX_DST] = &route.Inet4Addr{IP: dst} + + var mask [4]byte + ones := prefix.Bits() + for i := range ones { + mask[i/8] |= 1 << (7 - i%8) + } + addrs[syscall.RTAX_NETMASK] = &route.Inet4Addr{IP: mask} + } + + // LinkAddr as gateway routes directly through the interface + addrs[syscall.RTAX_GATEWAY] = &route.LinkAddr{ + Index: iface.Index, + Name: iface.Name, + } + + rtm := &route.RouteMessage{ + Version: syscall.RTM_VERSION, + Type: syscall.RTM_ADD, + Flags: syscall.RTF_UP | syscall.RTF_STATIC, + Index: iface.Index, + ID: uintptr(os.Getpid()), + Seq: int(time.Now().UnixNano() & 0x7fffffff), + Addrs: addrs, + } + + b, err := rtm.Marshal() + if err != nil { + return fmt.Errorf("marshal route message: %w", err) + } + + _, err = unix.Write(fd, b) + if err != nil { + if errors.Is(err, unix.EEXIST) { + return nil + } + return fmt.Errorf("write route message: %w", err) + } + + return nil } diff --git a/internal/helper/helper_linux.go b/internal/helper/helper_linux.go index e47363c51..3aba6d7b5 100644 --- a/internal/helper/helper_linux.go +++ b/internal/helper/helper_linux.go @@ -2,120 +2,237 @@ package helper import ( "context" + "errors" "fmt" - "os/exec" - "strings" + "net" + "net/netip" + "sync" + "syscall" + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + + "github.com/nais/device/internal/iputil" + "github.com/nais/device/internal/wgconfig" "github.com/nais/device/pkg/pb" ) -var wireguardBinary = "" - -func New(helperConfig Config) *LinuxConfigurator { +func New(helperConfig Config, log *logrus.Entry) *LinuxConfigurator { return &LinuxConfigurator{ helperConfig: helperConfig, + log: log, } } type LinuxConfigurator struct { helperConfig Config + mu sync.RWMutex + tunnelNet netip.Prefix + log *logrus.Entry } var _ OSConfigurator = &LinuxConfigurator{} func (c *LinuxConfigurator) Prerequisites() error { - var err error - wireguardBinary, err = exec.LookPath("wg") - if err != nil { - return fmt.Errorf("unable to find wg binary: %w", err) - } - if wireguardBinary == "" { - return fmt.Errorf("wg path is empty string") - } - return nil } func (c *LinuxConfigurator) SyncConf(ctx context.Context, cfg *pb.Configuration) error { - cmd := exec.CommandContext( - ctx, - wireguardBinary, - "syncconf", - c.helperConfig.Interface, - c.helperConfig.WireGuardConfigPath, - ) - if b, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("running syncconf: %w: %v", err, string(b)) + for _, gw := range cfg.GetGateways() { + c.log.WithFields(logrus.Fields{ + "peer": gw.GetName(), + "endpoint": gw.GetEndpoint(), + "public_key": fmt.Sprintf("%.8s...", gw.GetPublicKey()), + "allowed_ips": gw.GetAllowedIPs(), + }).Debug("configuring wireguard peer") } - - return nil + err := wgconfig.ApplyConfig(ctx, c.helperConfig.Interface, cfg) + if err != nil { + c.log.WithError(err).Debug("wireguard config sync failed") + } else { + c.log.Debug("wireguard config sync complete") + } + return err } func (c *LinuxConfigurator) SetupRoutes(ctx context.Context, gateways []*pb.Gateway) (int, error) { + c.mu.RLock() + tunnelNet := c.tunnelNet + c.mu.RUnlock() + + link, err := netlink.LinkByName(c.helperConfig.Interface) + if err != nil { + return 0, fmt.Errorf("lookup interface %q: %w", c.helperConfig.Interface, err) + } + + c.log.WithFields(logrus.Fields{ + "interface": c.helperConfig.Interface, + "link_index": link.Attrs().Index, + "num_gateways": len(gateways), + }).Debug("setting up routes") + routesAdded := 0 for _, gw := range gateways { - // For Linux we can handle ipv4/6 addreses the same - the `ip` utility handles this for us for _, cidr := range append(gw.GetRoutesIPv4(), gw.GetRoutesIPv6()...) { - if strings.HasPrefix(cidr, TunnelNetworkPrefix) { - // Don't add routes for the tunnel network, as the whole /21 net is already routed to utun + if IsTunnelRoute(tunnelNet, cidr) { + c.log.WithFields(logrus.Fields{ + "cidr": cidr, + "gateway": gw.GetName(), + }).Debug("skipping tunnel route") continue } - cidr = strings.TrimSpace(cidr) - - cmd := exec.CommandContext( - ctx, - "ip", - "route", - "add", - cidr, - "dev", - c.helperConfig.Interface, - ) - output, err := cmd.CombinedOutput() - if exitErr, ok := err.(*exec.ExitError); ok { - if exitErr.ExitCode() == 2 && strings.Contains(string(output), "File exists") { + prefix, err := iputil.ParsePrefix(cidr) + if err != nil { + return routesAdded, fmt.Errorf("parse route: %w", err) + } + addr := prefix.Addr() + ones := prefix.Bits() + var bits int + if addr.Is4() { + bits = 32 + } else { + bits = 128 + } + dst := &net.IPNet{ + IP: addr.AsSlice(), + Mask: net.CIDRMask(ones, bits), + } + + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: dst, + } + + if err := netlink.RouteAdd(route); err != nil { + if errors.Is(err, syscall.EEXIST) { + c.log.WithFields(logrus.Fields{ + "cidr": cidr, + "gateway": gw.GetName(), + }).Debug("route already exists") continue } - return routesAdded, fmt.Errorf("executing %v: %w, stderr: %s", cmd, exitErr, string(output)) + return routesAdded, fmt.Errorf("add route %s: %w", cidr, err) } + c.log.WithFields(logrus.Fields{ + "cidr": cidr, + "gateway": gw.GetName(), + }).Debug("route added") routesAdded++ } } + c.log.WithField("routes_added", routesAdded).Debug("route setup complete") return routesAdded, nil } func (c *LinuxConfigurator) SetupInterface(ctx context.Context, cfg *pb.Configuration) error { + tunnelNet, err := TunnelNetworkFromIP(cfg.DeviceIPv4) + if err != nil { + return fmt.Errorf("derive tunnel network: %w", err) + } + c.mu.Lock() + c.tunnelNet = tunnelNet + c.mu.Unlock() + c.log.WithFields(logrus.Fields{ + "device_ipv4": cfg.DeviceIPv4, + "device_ipv6": cfg.DeviceIPv6, + "tunnel_net": tunnelNet.String(), + "interface": c.helperConfig.Interface, + }).Debug("setting up interface") + if c.interfaceExists(ctx) { + c.log.WithField("interface", c.helperConfig.Interface).Debug("interface already exists, skipping creation") return nil } - commands := [][]string{ - {"ip", "link", "add", "dev", c.helperConfig.Interface, "type", "wireguard"}, - {"ip", "link", "set", "mtu", "1360", "up", "dev", c.helperConfig.Interface}, - {"ip", "address", "add", "dev", c.helperConfig.Interface, cfg.DeviceIPv4 + "/21"}, - {"ip", "address", "add", "dev", c.helperConfig.Interface, cfg.DeviceIPv6 + "/64"}, + wgLink := &netlink.Wireguard{ + LinkAttrs: netlink.LinkAttrs{ + Name: c.helperConfig.Interface, + MTU: wireguardMTU, + }, + } + + c.log.WithFields(logrus.Fields{ + "interface": c.helperConfig.Interface, + "mtu": wireguardMTU, + }).Debug("creating wireguard interface") + if err := netlink.LinkAdd(wgLink); err != nil { + return fmt.Errorf("create wireguard interface: %w", err) + } + + link, err := netlink.LinkByName(c.helperConfig.Interface) + if err != nil { + cleanupErr := netlink.LinkDel(wgLink) + if cleanupErr != nil { + return fmt.Errorf("lookup interface after creation: %w (additionally, failed to delete interface: %v)", err, cleanupErr) + } + return fmt.Errorf("lookup interface after creation: %w", err) + } + c.log.WithFields(logrus.Fields{ + "interface": c.helperConfig.Interface, + "index": link.Attrs().Index, + "type": link.Type(), + }).Debug("interface created") + + // cleanup deletes the interface if any subsequent configuration step fails. + cleanup := func(cause error) error { + if delErr := netlink.LinkDel(link); delErr != nil { + return fmt.Errorf("%w (additionally, failed to delete interface: %v)", cause, delErr) + } + return cause + } + + ipv4Addr, err := netlink.ParseAddr(cfg.DeviceIPv4 + "/21") + if err != nil { + return cleanup(fmt.Errorf("parse IPv4 address: %w", err)) + } + c.log.WithField("address", ipv4Addr.String()).Debug("adding IPv4 address") + if err := netlink.AddrAdd(link, ipv4Addr); err != nil { + return cleanup(fmt.Errorf("add IPv4 address: %w", err)) + } + + if cfg.DeviceIPv6 != "" { + ipv6Addr, err := netlink.ParseAddr(cfg.DeviceIPv6 + "/64") + if err != nil { + return cleanup(fmt.Errorf("parse IPv6 address: %w", err)) + } + c.log.WithField("address", ipv6Addr.String()).Debug("adding IPv6 address") + if err := netlink.AddrAdd(link, ipv6Addr); err != nil { + return cleanup(fmt.Errorf("add IPv6 address: %w", err)) + } } - return runCommands(ctx, commands) + c.log.Debug("bringing interface up") + if err := netlink.LinkSetUp(link); err != nil { + return cleanup(fmt.Errorf("bring interface up: %w", err)) + } + + c.log.WithField("interface", c.helperConfig.Interface).Debug("interface setup complete") + return nil } func (c *LinuxConfigurator) TeardownInterface(ctx context.Context) error { if !c.interfaceExists(ctx) { + c.log.WithField("interface", c.helperConfig.Interface).Debug("interface does not exist, skipping teardown") return nil } - cmd := exec.CommandContext(ctx, "ip", "link", "del", c.helperConfig.Interface) - out, err := cmd.CombinedOutput() + link, err := netlink.LinkByName(c.helperConfig.Interface) if err != nil { - return fmt.Errorf("teardown failed: %w, stderr: %s", err, string(out)) + return fmt.Errorf("lookup interface %q: %w", c.helperConfig.Interface, err) + } + + c.log.WithField("interface", c.helperConfig.Interface).Debug("deleting interface") + if err := netlink.LinkDel(link); err != nil { + return fmt.Errorf("delete interface: %w", err) } + c.log.WithField("interface", c.helperConfig.Interface).Debug("interface deleted") return nil } func (c *LinuxConfigurator) interfaceExists(ctx context.Context) bool { - cmd := exec.CommandContext(ctx, "ip", "link", "show", "dev", c.helperConfig.Interface) - return cmd.Run() == nil + _, err := netlink.LinkByName(c.helperConfig.Interface) + return err == nil } diff --git a/internal/helper/helper_windows.go b/internal/helper/helper_windows.go index eb8e8d25b..17b74cf79 100644 --- a/internal/helper/helper_windows.go +++ b/internal/helper/helper_windows.go @@ -1,135 +1,205 @@ package helper import ( - "bytes" "context" + "errors" "fmt" + "net" + "net/netip" "os" - "os/exec" - "time" - + "sync" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/ipc" + "golang.zx2c4.com/wireguard/tun" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" + + "github.com/nais/device/internal/iputil" + "github.com/nais/device/internal/wgconfig" "github.com/nais/device/pkg/pb" ) -const wireGuardBinary = `c:\Program Files\WireGuard\wireguard.exe` - type WindowsConfigurator struct { - helperConfig Config - oldWireGuardConfig []byte - wgNeedsRestart bool + helperConfig Config + + mu sync.Mutex + wgDevice *device.Device + tunDev tun.Device + uapi net.Listener + tunnelNet netip.Prefix } var _ OSConfigurator = &WindowsConfigurator{} -func New(helperConfig Config) *WindowsConfigurator { +func New(helperConfig Config, _ *logrus.Entry) *WindowsConfigurator { return &WindowsConfigurator{ helperConfig: helperConfig, } } -func filesExist(files ...string) error { - for _, file := range files { - if err := RegularFileExists(file); err != nil { - return err - } - } - +func (c *WindowsConfigurator) Prerequisites() error { return nil } -func (configurator *WindowsConfigurator) Prerequisites() error { - if err := filesExist(wireGuardBinary); err != nil { - return fmt.Errorf("verifying if file exists: %w", err) - } - - return nil +func (c *WindowsConfigurator) SyncConf(ctx context.Context, cfg *pb.Configuration) error { + return wgconfig.ApplyConfig(ctx, c.helperConfig.Interface, cfg) } -func interfaceExists(ctx context.Context, iface string) bool { - queryService := exec.CommandContext(ctx, "sc", "query", tunnelName(iface)) - if err := queryService.Run(); err != nil { - return false - } else { - return true - } -} +func (c *WindowsConfigurator) SetupRoutes(ctx context.Context, gateways []*pb.Gateway) (int, error) { + c.mu.Lock() + defer c.mu.Unlock() -func (configurator *WindowsConfigurator) SetupInterface(ctx context.Context, cfg *pb.Configuration) error { - if interfaceExists(ctx, configurator.helperConfig.Interface) { - return nil + if c.tunDev == nil { + return 0, fmt.Errorf("TUN device not initialized") } - installService := exec.CommandContext(ctx, wireGuardBinary, "/installtunnelservice", configurator.helperConfig.WireGuardConfigPath) - if b, err := installService.CombinedOutput(); err != nil { - return fmt.Errorf("installing tunnel service: %v: %v", err, string(b)) - } else { - time.Sleep(6 * time.Second) + nativeTun, ok := c.tunDev.(*tun.NativeTun) + if !ok { + return 0, fmt.Errorf("unexpected TUN device type %T", c.tunDev) + } + ifLUID := winipcfg.LUID(nativeTun.LUID()) + + routesAdded := 0 + for _, gw := range gateways { + for _, cidr := range append(gw.GetRoutesIPv4(), gw.GetRoutesIPv6()...) { + if IsTunnelRoute(c.tunnelNet, cidr) { + continue + } + + dst, err := iputil.ParsePrefix(cidr) + if err != nil { + return routesAdded, fmt.Errorf("parse route: %w", err) + } + + nextHop := netip.IPv4Unspecified() + if dst.Addr().Is6() { + nextHop = netip.IPv6Unspecified() + } + + if err := ifLUID.AddRoute(dst, nextHop, 0); err != nil { + if errors.Is(err, windows.ERROR_OBJECT_ALREADY_EXISTS) { + continue + } + return routesAdded, fmt.Errorf("add route %s: %w", cidr, err) + } + routesAdded++ + } } - configurator.wgNeedsRestart = false - - return nil + return routesAdded, nil } -func (configurator *WindowsConfigurator) SetupRoutes(ctx context.Context, gateways []*pb.Gateway) (int, error) { - return 0, nil -} +func (c *WindowsConfigurator) SetupInterface(ctx context.Context, cfg *pb.Configuration) error { + c.mu.Lock() + defer c.mu.Unlock() -func (configurator *WindowsConfigurator) SyncConf(ctx context.Context, cfg *pb.Configuration) error { - newWireGuardConfig, err := os.ReadFile(configurator.helperConfig.WireGuardConfigPath) + tunnelNet, err := TunnelNetworkFromIP(cfg.DeviceIPv4) if err != nil { - return fmt.Errorf("reading WireGuard config file: %w", err) + return fmt.Errorf("derive tunnel network: %w", err) } + c.tunnelNet = tunnelNet - defer func() { - configurator.oldWireGuardConfig = newWireGuardConfig - configurator.wgNeedsRestart = true - }() - - if !configurator.wgNeedsRestart { + if c.wgDevice != nil { return nil } - if fileActuallyChanged(configurator.oldWireGuardConfig, newWireGuardConfig) { - commands := [][]string{ - {"net", "stop", tunnelName(configurator.helperConfig.Interface)}, - {"net", "start", tunnelName(configurator.helperConfig.Interface)}, - } + tunDev, err := tun.CreateTUN(c.helperConfig.Interface, wireguardMTU) + if err != nil { + return fmt.Errorf("create TUN device: %w", err) + } - return runCommands(ctx, commands) + logger := &device.Logger{ + Verbosef: device.DiscardLogf, + Errorf: func(format string, args ...any) { fmt.Fprintf(os.Stderr, "wireguard: "+format+"\n", args...) }, } + wgDev := device.NewDevice(tunDev, conn.NewDefaultBind(), logger) - return nil -} + if err := wgDev.Up(); err != nil { + wgDev.Close() + return fmt.Errorf("bring up wireguard device: %w", err) + } -func (configurator *WindowsConfigurator) TeardownInterface(ctx context.Context) error { - if !interfaceExists(ctx, configurator.helperConfig.Interface) { - return nil + uapi, err := ipc.UAPIListen(c.helperConfig.Interface) + if err != nil { + wgDev.Close() + return fmt.Errorf("listen on UAPI named pipe: %w", err) } - uninstallService := exec.CommandContext(ctx, wireGuardBinary, "/uninstalltunnelservice", configurator.helperConfig.Interface) + c.wgDevice = wgDev + c.tunDev = tunDev + c.uapi = uapi - b, err := uninstallService.CombinedOutput() + nativeTun, ok := tunDev.(*tun.NativeTun) + if !ok { + c.closeLocked() + return fmt.Errorf("unexpected TUN device type %T", tunDev) + } + ifLUID := winipcfg.LUID(nativeTun.LUID()) + + ipv4, err := netip.ParsePrefix(cfg.DeviceIPv4 + "/21") if err != nil { - return fmt.Errorf("uninstalling tunnel service: %v: %v", err, string(b)) + c.closeLocked() + return fmt.Errorf("parse IPv4 address: %w", err) + } + + if err := ifLUID.AddIPAddress(ipv4); err != nil { + c.closeLocked() + return fmt.Errorf("add IPv4 address: %w", err) } - select { - case <-ctx.Done(): - case <-time.After(3 * time.Second): + if cfg.DeviceIPv6 != "" { + ipv6, err := netip.ParsePrefix(cfg.DeviceIPv6 + "/64") + if err != nil { + c.closeLocked() + return fmt.Errorf("parse IPv6 address: %w", err) + } + + if err := ifLUID.AddIPAddress(ipv6); err != nil { + c.closeLocked() + return fmt.Errorf("add IPv6 address: %w", err) + } } + // Start accepting UAPI connections only after all initialization has succeeded. + // This ensures wgctrl can't race against incomplete interface setup. + go func() { + for { + uapiConn, err := uapi.Accept() + if err != nil { + return + } + go wgDev.IpcHandle(uapiConn) + } + }() + return nil } -func tunnelName(interfaceName string) string { - return fmt.Sprintf("WireGuardTunnel$%s", interfaceName) -} +func (c *WindowsConfigurator) TeardownInterface(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() -func fileActuallyChanged(old, new []byte) bool { - if old == nil || new == nil { - return true + if c.wgDevice == nil { + return nil } - return !bytes.Equal(old, new) + c.closeLocked() + return nil +} + +// closeLocked shuts down the UAPI listener, WireGuard device, and TUN. +// Must be called with c.mu held. +func (c *WindowsConfigurator) closeLocked() { + if c.uapi != nil { + _ = c.uapi.Close() + c.uapi = nil + } + if c.wgDevice != nil { + c.wgDevice.Close() + c.wgDevice = nil + } + c.tunDev = nil } diff --git a/internal/helper/util.go b/internal/helper/util.go index e91c10fc0..9f8785d43 100644 --- a/internal/helper/util.go +++ b/internal/helper/util.go @@ -2,46 +2,42 @@ package helper import ( "archive/zip" - "context" "errors" "fmt" "io" + "net/netip" "os" - "os/exec" "path/filepath" - "time" "github.com/nais/device/internal/ioconvenience" + "github.com/nais/device/internal/iputil" "github.com/sirupsen/logrus" ) const ( - TunnelNetworkPrefix = "10.255.24." + // wireguardMTU is the MTU used for WireGuard tunnel interfaces across all platforms. + wireguardMTU = 1360 + + // tunnelIPv4PrefixLen is the prefix length used for the IPv4 tunnel network. + tunnelIPv4PrefixLen = 21 ) -func RegularFileExists(filepath string) error { - info, err := os.Stat(filepath) +// TunnelNetworkFromIP derives the tunnel network prefix from the device's IPv4 address. +func TunnelNetworkFromIP(deviceIPv4 string) (netip.Prefix, error) { + addr, err := netip.ParseAddr(deviceIPv4) if err != nil { - return err - } - if info.IsDir() { - return fmt.Errorf("%v is a directory", filepath) + return netip.Prefix{}, fmt.Errorf("parse device IPv4 %q: %w", deviceIPv4, err) } - - return nil + return netip.PrefixFrom(addr, tunnelIPv4PrefixLen).Masked(), nil } -func runCommands(ctx context.Context, commands [][]string) error { - for _, s := range commands { - cmd := exec.CommandContext(ctx, s[0], s[1:]...) - - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("running %v: %w: %v", cmd, err, string(out)) - } - - time.Sleep(100 * time.Millisecond) // avoid serializable race conditions with kernel +// IsTunnelRoute reports whether cidr falls within the tunnel network. +func IsTunnelRoute(tunnelNet netip.Prefix, cidr string) bool { + prefix, err := iputil.ParsePrefix(cidr) + if err != nil { + return false } - return nil + return tunnelNet.Contains(prefix.Addr()) && prefix.Bits() >= tunnelNet.Bits() } func ZipLogFiles(files []string, log logrus.FieldLogger) (string, error) { @@ -61,7 +57,7 @@ func ZipLogFiles(files []string, log logrus.FieldLogger) (string, error) { } logFile, err := os.Open(filename) if err != nil { - return "nil", fmt.Errorf("%s %v", filename, err) + return "nil", fmt.Errorf("%s: %w", filename, err) } zipEntryWiter, err := zipWriter.Create(filepath.Base(filename)) if err != nil { diff --git a/internal/integration_test/device_helper_test.go b/internal/integration_test/device_helper_test.go index ae153a48e..0e0e42242 100644 --- a/internal/integration_test/device_helper_test.go +++ b/internal/integration_test/device_helper_test.go @@ -21,7 +21,6 @@ func NewHelper(t *testing.T, log *logrus.Entry, osConfigurator helper.OSConfigur } tempDir, err := os.MkdirTemp(testDir, "naisdevice_helper_test_*") assert.NoError(t, err) - tempfile := filepath.Join(tempDir, "test_interface.conf") t.Cleanup(func() { if t.Failed() { t.Logf("test failed, leaving temp dir in: %v", tempDir) @@ -36,9 +35,8 @@ func NewHelper(t *testing.T, log *logrus.Entry, osConfigurator helper.OSConfigur }) helperConfig := helper.Config{ - Interface: `test_interface`, - LogLevel: logrus.DebugLevel.String(), - WireGuardConfigPath: tempfile, + Interface: `test_interface`, + LogLevel: logrus.DebugLevel.String(), } deviceHelperServer := helper.NewDeviceHelperServer(log, helperConfig, osConfigurator) diff --git a/internal/iputil/iputil.go b/internal/iputil/iputil.go new file mode 100644 index 000000000..cccef27e6 --- /dev/null +++ b/internal/iputil/iputil.go @@ -0,0 +1,26 @@ +// Package iputil provides IP address parsing utilities. +package iputil + +import ( + "fmt" + "net/netip" + "strings" +) + +// ParsePrefix parses s as a CIDR prefix. +// Bare IP addresses without a prefix length are treated as host addresses (/32 or /128). +func ParsePrefix(s string) (netip.Prefix, error) { + s = strings.TrimSpace(s) + + prefix, err := netip.ParsePrefix(s) + if err == nil { + return prefix, nil + } + + addr, addrErr := netip.ParseAddr(s) + if addrErr != nil { + return netip.Prefix{}, fmt.Errorf("parse prefix %q: %w", s, err) + } + + return netip.PrefixFrom(addr, addr.BitLen()), nil +} diff --git a/internal/iputil/iputil_test.go b/internal/iputil/iputil_test.go new file mode 100644 index 000000000..d2a4ada98 --- /dev/null +++ b/internal/iputil/iputil_test.go @@ -0,0 +1,76 @@ +package iputil + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParsePrefix(t *testing.T) { + tests := []struct { + name string + input string + want netip.Prefix + wantErr bool + }{ + { + name: "IPv4 CIDR", + input: "10.0.0.0/24", + want: netip.MustParsePrefix("10.0.0.0/24"), + }, + { + name: "IPv4 host CIDR", + input: "10.43.0.60/32", + want: netip.MustParsePrefix("10.43.0.60/32"), + }, + { + name: "bare IPv4 address", + input: "10.43.0.60", + want: netip.MustParsePrefix("10.43.0.60/32"), + }, + { + name: "IPv6 CIDR", + input: "fd01::/64", + want: netip.MustParsePrefix("fd01::/64"), + }, + { + name: "bare IPv6 address", + input: "fd00::1", + want: netip.MustParsePrefix("fd00::1/128"), + }, + { + name: "CIDR with surrounding whitespace", + input: " 10.0.0.0/24 ", + want: netip.MustParsePrefix("10.0.0.0/24"), + }, + { + name: "bare IP with surrounding whitespace", + input: " 10.43.0.60 ", + want: netip.MustParsePrefix("10.43.0.60/32"), + }, + { + name: "garbage input", + input: "not-an-ip", + wantErr: true, + }, + { + name: "empty string", + input: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePrefix(tt.input) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/wgconfig/config.go b/internal/wgconfig/config.go new file mode 100644 index 000000000..4a6c612e5 --- /dev/null +++ b/internal/wgconfig/config.go @@ -0,0 +1,122 @@ +// Package wgconfig converts protobuf Configuration types to wgtypes.Config +// and applies them via wgctrl.Client.ConfigureDevice(). +package wgconfig + +import ( + "context" + "fmt" + "net" + "time" + + "golang.zx2c4.com/wireguard/wgctrl" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/nais/device/internal/iputil" + "github.com/nais/device/internal/wireguard" + "github.com/nais/device/pkg/pb" +) + +const ( + defaultPersistentKeepalive = 25 * time.Second +) + +// ApplyConfig configures the named WireGuard device with the given configuration. +// It replaces all existing peers. +func ApplyConfig(ctx context.Context, ifaceName string, cfg *pb.Configuration) error { + wgCfg, err := BuildConfig(cfg) + if err != nil { + return fmt.Errorf("build wgctrl config: %w", err) + } + + client, err := wgctrl.New() + if err != nil { + return fmt.Errorf("create wgctrl client: %w", err) + } + defer func() { _ = client.Close() }() + + if err := client.ConfigureDevice(ifaceName, *wgCfg); err != nil { + return fmt.Errorf("configure device %q: %w", ifaceName, err) + } + + return nil +} + +// BuildConfig converts a protobuf Configuration into a wgtypes.Config. +func BuildConfig(cfg *pb.Configuration) (*wgtypes.Config, error) { + privateKey, err := wgtypes.ParseKey(cfg.GetPrivateKey()) + if err != nil { + return nil, fmt.Errorf("parse private key: %w", err) + } + + gateways := cfg.GetGateways() + peers := make([]wgtypes.PeerConfig, 0, len(gateways)) + for _, gw := range gateways { + peerCfg, err := buildPeerConfig(gw) + if err != nil { + return nil, fmt.Errorf("build peer config for %q: %w", gw.GetName(), err) + } + peers = append(peers, *peerCfg) + } + + return &wgtypes.Config{ + PrivateKey: &privateKey, + ReplacePeers: true, + Peers: peers, + }, nil +} + +func buildPeerConfig(peer wireguard.Peer) (*wgtypes.PeerConfig, error) { + publicKey, err := wgtypes.ParseKey(peer.GetPublicKey()) + if err != nil { + return nil, fmt.Errorf("parse public key: %w", err) + } + + allowedIPs, err := parseAllowedIPs(peer.GetAllowedIPs()) + if err != nil { + return nil, fmt.Errorf("parse allowed IPs: %w", err) + } + + peerCfg := &wgtypes.PeerConfig{ + PublicKey: publicKey, + ReplaceAllowedIPs: true, + AllowedIPs: allowedIPs, + } + + if endpoint := peer.GetEndpoint(); endpoint != "" { + addr, err := net.ResolveUDPAddr("udp", endpoint) + if err != nil { + return nil, fmt.Errorf("resolve endpoint %q: %w", endpoint, err) + } + peerCfg.Endpoint = addr + } + + if peer.GetName() == wireguard.PrometheusPeerName { + keepalive := defaultPersistentKeepalive + peerCfg.PersistentKeepaliveInterval = &keepalive + } + + return peerCfg, nil +} + +func parseAllowedIPs(cidrs []string) ([]net.IPNet, error) { + nets := make([]net.IPNet, 0, len(cidrs)) + for _, cidr := range cidrs { + prefix, err := iputil.ParsePrefix(cidr) + if err != nil { + return nil, fmt.Errorf("parse allowed IP: %w", err) + } + addr := prefix.Addr() + ones := prefix.Bits() + var bits int + if addr.Is4() { + bits = 32 + } else { + bits = 128 + } + nets = append(nets, net.IPNet{ + IP: addr.AsSlice(), + Mask: net.CIDRMask(ones, bits), + }) + } + return nets, nil +} diff --git a/internal/wgconfig/config_test.go b/internal/wgconfig/config_test.go new file mode 100644 index 000000000..25038147f --- /dev/null +++ b/internal/wgconfig/config_test.go @@ -0,0 +1,231 @@ +package wgconfig_test + +import ( + "net" + "testing" + "time" + + "github.com/nais/device/internal/wgconfig" + "github.com/nais/device/internal/wireguard" + "github.com/nais/device/pkg/pb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +var testPrivateKey string + +func init() { + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + panic("generate test private key: " + err.Error()) + } + testPrivateKey = key.String() +} + +func generateTestPublicKey() string { + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + panic("generate test key: " + err.Error()) + } + return key.PublicKey().String() +} + +func TestBuildConfig_BasicConversion(t *testing.T) { + gwPubKey := generateTestPublicKey() + + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + DeviceIPv4: "10.255.24.1", + DeviceIPv6: "fd00::1", + Gateways: []*pb.Gateway{ + { + Name: "test-gw", + PublicKey: gwPubKey, + Endpoint: "1.2.3.4:51820", + Ipv4: "10.255.24.10", + Ipv6: "fd00::10", + RoutesIPv4: []string{"10.0.0.0/24", "172.16.0.0/16"}, + RoutesIPv6: []string{"fd01::/64"}, + }, + }, + } + + wgCfg, err := wgconfig.BuildConfig(cfg) + require.NoError(t, err) + + expectedPrivKey, _ := wgtypes.ParseKey(testPrivateKey) + assert.Equal(t, expectedPrivKey, *wgCfg.PrivateKey) + assert.True(t, wgCfg.ReplacePeers) + require.Len(t, wgCfg.Peers, 1) + + peer := wgCfg.Peers[0] + + expectedPubKey, _ := wgtypes.ParseKey(gwPubKey) + assert.Equal(t, expectedPubKey, peer.PublicKey) + + assert.Equal(t, "1.2.3.4", peer.Endpoint.IP.String()) + assert.Equal(t, 51820, peer.Endpoint.Port) + + expectedCIDRs := []string{ + "10.0.0.0/24", + "172.16.0.0/16", + "10.255.24.10/32", + "fd00::10/128", + "fd01::/64", + } + require.Len(t, peer.AllowedIPs, len(expectedCIDRs)) + for i, cidr := range expectedCIDRs { + _, expected, _ := net.ParseCIDR(cidr) + assert.Equal(t, *expected, peer.AllowedIPs[i], "AllowedIP mismatch at index %d", i) + } + + assert.Nil(t, peer.PersistentKeepaliveInterval) + assert.True(t, peer.ReplaceAllowedIPs) +} + +func TestBuildConfig_PrometheusPeerKeepalive(t *testing.T) { + promPubKey := generateTestPublicKey() + + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + Gateways: []*pb.Gateway{ + { + Name: wireguard.PrometheusPeerName, + PublicKey: promPubKey, + Endpoint: "5.6.7.8:51820", + Ipv4: "10.255.24.20", + }, + }, + } + + wgCfg, err := wgconfig.BuildConfig(cfg) + require.NoError(t, err) + + require.Len(t, wgCfg.Peers, 1) + peer := wgCfg.Peers[0] + + require.NotNil(t, peer.PersistentKeepaliveInterval) + assert.Equal(t, 25*time.Second, *peer.PersistentKeepaliveInterval) +} + +func TestBuildConfig_MultiplePeers(t *testing.T) { + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + Gateways: []*pb.Gateway{ + { + Name: "gw-1", + PublicKey: generateTestPublicKey(), + Endpoint: "1.1.1.1:51820", + Ipv4: "10.255.24.10", + }, + { + Name: "gw-2", + PublicKey: generateTestPublicKey(), + Endpoint: "2.2.2.2:51820", + Ipv4: "10.255.24.11", + }, + { + Name: "gw-3", + PublicKey: generateTestPublicKey(), + Endpoint: "3.3.3.3:51820", + Ipv4: "10.255.24.12", + }, + }, + } + + wgCfg, err := wgconfig.BuildConfig(cfg) + require.NoError(t, err) + assert.Len(t, wgCfg.Peers, 3) +} + +func TestBuildConfig_PeerWithoutEndpoint(t *testing.T) { + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + Gateways: []*pb.Gateway{ + { + Name: "no-endpoint", + PublicKey: generateTestPublicKey(), + Ipv4: "10.255.24.10", + }, + }, + } + + wgCfg, err := wgconfig.BuildConfig(cfg) + require.NoError(t, err) + require.Len(t, wgCfg.Peers, 1) + assert.Nil(t, wgCfg.Peers[0].Endpoint) +} + +func TestBuildConfig_BareIPWithoutCIDRPrefix(t *testing.T) { + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + Gateways: []*pb.Gateway{ + { + Name: "gw-bare-ip", + PublicKey: generateTestPublicKey(), + Endpoint: "1.2.3.4:51820", + Ipv4: "10.255.24.10", + Ipv6: "fd00::10", + RoutesIPv4: []string{"10.43.0.60", "10.0.0.0/24"}, + RoutesIPv6: []string{"fd00::1"}, + }, + }, + } + + wgCfg, err := wgconfig.BuildConfig(cfg) + require.NoError(t, err) + require.Len(t, wgCfg.Peers, 1) + + peer := wgCfg.Peers[0] + expectedCIDRs := []string{ + "10.43.0.60/32", + "10.0.0.0/24", + "10.255.24.10/32", + "fd00::10/128", + "fd00::1/128", + } + require.Len(t, peer.AllowedIPs, len(expectedCIDRs)) + for i, cidr := range expectedCIDRs { + _, expected, _ := net.ParseCIDR(cidr) + assert.Equal(t, *expected, peer.AllowedIPs[i], "AllowedIP mismatch at index %d", i) + } +} + +func TestBuildConfig_InvalidPrivateKey(t *testing.T) { + cfg := &pb.Configuration{ + PrivateKey: "not-a-valid-key", + } + + _, err := wgconfig.BuildConfig(cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "parse private key") +} + +func TestBuildConfig_InvalidPeerPublicKey(t *testing.T) { + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + Gateways: []*pb.Gateway{ + { + Name: "bad-key", + PublicKey: "invalid", + Ipv4: "10.0.0.1", + }, + }, + } + + _, err := wgconfig.BuildConfig(cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "parse public key") +} + +func TestBuildConfig_NoGateways(t *testing.T) { + cfg := &pb.Configuration{ + PrivateKey: testPrivateKey, + } + + wgCfg, err := wgconfig.BuildConfig(cfg) + require.NoError(t, err) + assert.Empty(t, wgCfg.Peers) + assert.True(t, wgCfg.ReplacePeers) +} diff --git a/internal/wireguard/interfaces.go b/internal/wireguard/interfaces.go deleted file mode 100644 index 62ca67f9e..000000000 --- a/internal/wireguard/interfaces.go +++ /dev/null @@ -1,15 +0,0 @@ -package wireguard - -type NetworkConfigurer interface { - ApplyWireGuardConfig(peers []Peer) error - ForwardRoutesV4(routes []string) error - ForwardRoutesV6(routes []string) error - SetupInterface() error - SetupIPTables() error -} - -type IPTables interface { - AppendUnique(table, chain string, rulespec ...string) error - NewChain(table, chain string) error - ChangePolicy(table, chain, target string) error -} diff --git a/internal/wireguard/iptables_linux.go b/internal/wireguard/iptables.go similarity index 100% rename from internal/wireguard/iptables_linux.go rename to internal/wireguard/iptables.go diff --git a/internal/wireguard/keys.go b/internal/wireguard/keys.go index 23500d0de..229de84c4 100644 --- a/internal/wireguard/keys.go +++ b/internal/wireguard/keys.go @@ -1,83 +1,62 @@ package wireguard import ( - "crypto/rand" - "encoding/base64" "errors" "fmt" "io/fs" "os" "path/filepath" + "strings" "github.com/sirupsen/logrus" - "golang.org/x/crypto/curve25519" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -type PrivateKey []byte - -// Public returns the public key base64 encoded -func (p PrivateKey) Public() []byte { - return keyToBase64(pubKey(p)) -} - -// Private returns the private key base64 encoded -func (p PrivateKey) Private() []byte { - return keyToBase64(p) -} - -func GenKey() (PrivateKey, error) { - var privateKey [32]byte - - n, err := rand.Read(privateKey[:]) - - if err != nil || n != len(privateKey) { - return nil, fmt.Errorf("unable to generate random bytes") - } - - privateKey[0] &= 248 - privateKey[31] = (privateKey[31] & 127) | 64 - return PrivateKey(privateKey[:]), nil -} - -func pubKey(privateKeySlice []byte) []byte { - var privateKey [32]byte - var publicKey [32]byte - copy(privateKey[:], privateKeySlice[:]) - - curve25519.ScalarBaseMult(&publicKey, &privateKey) - - return publicKey[:] -} - -func keyToBase64(key []byte) []byte { - dst := make([]byte, base64.StdEncoding.EncodedLen(len(key))) - base64.StdEncoding.Encode(dst, key) - return dst -} - -func ReadOrCreatePrivateKey(path string, log *logrus.Entry) (PrivateKey, error) { +func ReadOrCreatePrivateKey(path string, log *logrus.Entry) (wgtypes.Key, error) { b, err := os.ReadFile(path) if err != nil && !errors.Is(err, fs.ErrNotExist) { - return nil, fmt.Errorf("read private key: %w", err) + return wgtypes.Key{}, fmt.Errorf("read private key: %w", err) } if errors.Is(err, fs.ErrNotExist) { log.Info("no private key found, generating new one...") - b, err = GenKey() + key, err := wgtypes.GeneratePrivateKey() if err != nil { - return nil, fmt.Errorf("generate private key: %w", err) + return wgtypes.Key{}, fmt.Errorf("generate private key: %w", err) } if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - return nil, fmt.Errorf("create config dir: %w", err) + return wgtypes.Key{}, fmt.Errorf("create config dir: %w", err) } - if err := os.WriteFile(path, b, 0o600); err != nil { - return nil, fmt.Errorf("write private key: %w", err) + if err := os.WriteFile(path, []byte(key.String()), 0o600); err != nil { + return wgtypes.Key{}, fmt.Errorf("write private key: %w", err) } - } else { - log.Info("found private key, using it...") + + return key, nil + } + + log.Info("found private key, using it...") + + if key, err := wgtypes.ParseKey(strings.TrimSpace(string(b))); err == nil { + return key, nil + } + + if len(b) != len(wgtypes.Key{}) { + return wgtypes.Key{}, fmt.Errorf("parse private key: invalid key length %d", len(b)) + } + + legacyKey, err := wgtypes.NewKey(b) + if err != nil { + return wgtypes.Key{}, fmt.Errorf("parse legacy private key: %w", err) + } + + if err := os.WriteFile(path, []byte(legacyKey.String()), 0o600); err != nil { + return wgtypes.Key{}, fmt.Errorf("rewrite private key: %w", err) + } + if err := os.Chmod(path, 0o600); err != nil { + return wgtypes.Key{}, fmt.Errorf("set private key file mode: %w", err) } - return PrivateKey(b), nil + return legacyKey, nil } diff --git a/internal/wireguard/keys_test.go b/internal/wireguard/keys_test.go new file mode 100644 index 000000000..0ce4c62ab --- /dev/null +++ b/internal/wireguard/keys_test.go @@ -0,0 +1,54 @@ +package wireguard_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/nais/device/internal/wireguard" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func TestReadOrCreatePrivateKey_CreatesKeyWhenMissing(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "private.key") + + key, err := wireguard.ReadOrCreatePrivateKey(keyPath, logrus.New().WithField("test", t.Name())) + require.NoError(t, err) + assert.NotEqual(t, wgtypes.Key{}, key) + + stored, err := os.ReadFile(keyPath) + require.NoError(t, err) + assert.Equal(t, key.String(), string(stored)) + + info, err := os.Stat(keyPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o600), info.Mode().Perm()) +} + +func TestReadOrCreatePrivateKey_MigratesLegacyRawKey(t *testing.T) { + dir := t.TempDir() + keyPath := filepath.Join(dir, "private.key") + + original, err := wgtypes.GeneratePrivateKey() + require.NoError(t, err) + + legacyBytes := make([]byte, len(original)) + copy(legacyBytes, original[:]) + require.NoError(t, os.WriteFile(keyPath, legacyBytes, 0o644)) + + key, err := wireguard.ReadOrCreatePrivateKey(keyPath, logrus.New().WithField("test", t.Name())) + require.NoError(t, err) + assert.Equal(t, original, key) + + migrated, err := os.ReadFile(keyPath) + require.NoError(t, err) + assert.Equal(t, original.String(), string(migrated)) + + info, err := os.Stat(keyPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o600), info.Mode().Perm()) +} diff --git a/internal/wireguard/network_configurer.go b/internal/wireguard/network_configurer.go index 7db2f57b2..093fe5009 100644 --- a/internal/wireguard/network_configurer.go +++ b/internal/wireguard/network_configurer.go @@ -1,5 +1,3 @@ -//go:build linux - package wireguard import ( @@ -15,6 +13,20 @@ import ( "github.com/sirupsen/logrus" ) +type NetworkConfigurer interface { + ApplyWireGuardConfig(peers []Peer) error + ForwardRoutesV4(routes []string) error + ForwardRoutesV6(routes []string) error + SetupInterface() error + SetupIPTables() error +} + +type IPTables interface { + AppendUnique(table, chain string, rulespec ...string) error + NewChain(table, chain string) error + ChangePolicy(table, chain, target string) error +} + type subNetworkConfigurer struct { ip *netip.Prefix iface *net.Interface diff --git a/internal/wireguard/wireguardpeerconfig.go b/internal/wireguard/wireguardpeerconfig.go deleted file mode 100644 index 3ce34a6ce..000000000 --- a/internal/wireguard/wireguardpeerconfig.go +++ /dev/null @@ -1,9 +0,0 @@ -package wireguard - -import "io" - -type WireGuardPeerConfig interface { - GetTunnelIP() string - GetWireGuardConfigPath() string - WriteWireGuardBase(io.Writer) error -} diff --git a/mise/config.toml b/mise/config.toml index 13c24a7fb..6c7cfe05d 100644 --- a/mise/config.toml +++ b/mise/config.toml @@ -1,14 +1,7 @@ [tools] buf = "1.59.0" git-cliff = "2.12.0" -go = """ -{%- set gomod = read_file(path=config_root ~ "/go.mod") -%} -{%- for line in gomod | split(pat="\n") -%} -{%- if line is starting_with("go ") -%} -{{- line | replace(from="go ", to="") | trim -}} -{%- endif -%} -{%- endfor -%} -""" +go = "1.26.4" jq = "1.8.1" protoc = "34.1" protoc-gen-go = "1.36.9" diff --git a/mise/tasks/build/windows.sh b/mise/tasks/build/windows.sh index 614ab871a..567411f99 100755 --- a/mise/tasks/build/windows.sh +++ b/mise/tasks/build/windows.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash #MISE description="Build windows client" +#MISE depends=["build:windows:verify-wintun"] #MISE env={ GOOS = "windows" } set -o errexit @@ -17,3 +18,5 @@ GOOS="" GOARCH="" go tool github.com/akavel/rsrc -arch "$REALGOARCH" -ico assets go build -o bin/windows-client/naisdevice-systray.exe --tags "$gotags" -ldflags "-s $ldflags -H=windowsgui" ./cmd/naisdevice-systray go build -o bin/windows-client/naisdevice-agent.exe --tags "$gotags" -ldflags "-s $ldflags -H=windowsgui" ./cmd/naisdevice-agent go build -o bin/windows-client/naisdevice-helper.exe --tags "$gotags" -ldflags "-s $ldflags" ./cmd/naisdevice-helper + +cp "${MISE_PROJECT_ROOT}/assets/windows/wintun-${REALGOARCH}.dll" ./bin/windows-client/wintun.dll diff --git a/mise/tasks/build/windows/update-wintun.sh b/mise/tasks/build/windows/update-wintun.sh new file mode 100755 index 000000000..c8ab101a5 --- /dev/null +++ b/mise/tasks/build/windows/update-wintun.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +#MISE description="Update bundled wintun DLLs" + +set -o errexit +set -o pipefail +set -o nounset + +MISE_PROJECT_ROOT="${MISE_PROJECT_ROOT:-$(pwd)}" + +version="${1:?usage: mise run build:windows:update-wintun }" +if [[ "$version" =~ ^v ]]; then + version="${version#v}" +fi + +base_url="https://www.wintun.net" +zip_rel_path="builds/wintun-${version}.zip" +zip_url="${base_url}/${zip_rel_path}" +manifest_url="${base_url}/" + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT + +dst_dir="${MISE_PROJECT_ROOT}/assets/windows" +manifest="${dst_dir}/wintun.sha256" + +curl -fsSL "$manifest_url" -o "$workdir/index.html" + +zip_sha="$(sed -nE 's/.*SHA2-256: ([0-9a-f]{64})<\/code>.*/\1/p' "$workdir/index.html" | head -n 1)" +if [[ -z "$zip_sha" ]]; then + echo "failed to read wintun zip SHA256 from ${manifest_url}" >&2 + exit 1 +fi + +declared_zip_path="$(grep -Eo 'builds/wintun-[0-9]+\.[0-9]+\.[0-9]+\.zip' "$workdir/index.html" | head -n 1)" +if [[ -n "$declared_zip_path" && "$declared_zip_path" != "$zip_rel_path" ]]; then + echo "requested wintun version ${version}, but website currently publishes ${declared_zip_path}" >&2 + exit 1 +fi + +curl -fsSL "$zip_url" -o "$workdir/wintun.zip" + +actual_zip_sha="$(shasum -a 256 "$workdir/wintun.zip" | awk '{print $1}')" +if [[ "$actual_zip_sha" != "$zip_sha" ]]; then + echo "wintun zip checksum mismatch: got $actual_zip_sha, want $zip_sha" >&2 + exit 1 +fi + +unzip -q "$workdir/wintun.zip" -d "$workdir" + +install -m 0644 "$workdir/wintun/bin/amd64/wintun.dll" "${dst_dir}/wintun-amd64.dll" +install -m 0644 "$workdir/wintun/bin/arm64/wintun.dll" "${dst_dir}/wintun-arm64.dll" + +amd64_sha="$(shasum -a 256 "${dst_dir}/wintun-amd64.dll" | awk '{print $1}')" +arm64_sha="$(shasum -a 256 "${dst_dir}/wintun-arm64.dll" | awk '{print $1}')" + +{ + echo "${amd64_sha} wintun-amd64.dll" + echo "${arm64_sha} wintun-arm64.dll" +} >"${manifest}" + +cat "${manifest}" diff --git a/mise/tasks/build/windows/verify-wintun.sh b/mise/tasks/build/windows/verify-wintun.sh new file mode 100755 index 000000000..d8dce007a --- /dev/null +++ b/mise/tasks/build/windows/verify-wintun.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +#MISE description="Verify wintun checksums" +#MISE env={ GOOS = "windows" } + +set -o errexit +set -o pipefail +set -o nounset + +readonly WINTUN_MANIFEST="${MISE_PROJECT_ROOT}/assets/windows/wintun.sha256" + +verify_wintun() { + local arch dll expected actual + arch="$1" + dll="${MISE_PROJECT_ROOT}/assets/windows/wintun-${arch}.dll" + + if [[ ! -f "$dll" ]]; then + echo "wintun DLL not found: $dll" >&2 + exit 1 + fi + + expected="$(awk '$2 == "wintun-'"${arch}"'.dll" {print $1}' "$WINTUN_MANIFEST")" + if [[ -z "$expected" ]]; then + echo "No checksum entry for wintun-${arch}.dll in $WINTUN_MANIFEST" >&2 + exit 1 + fi + + actual="$(shasum -a 256 "$dll" | awk '{print $1}')" + if [[ "$actual" != "$expected" ]]; then + echo "wintun checksum mismatch for $arch: got $actual, want $expected" >&2 + exit 1 + fi +} + +REALGOARCH="${GOARCH:-amd64}" +verify_wintun "$REALGOARCH" diff --git a/mise/tasks/ci/release-info.sh b/mise/tasks/ci/release-info.sh index 3d3dc64cb..d09e4f238 100755 --- a/mise/tasks/ci/release-info.sh +++ b/mise/tasks/ci/release-info.sh @@ -48,6 +48,10 @@ main() { return fi + if [[ "${PRE_RELEASE:-}" == "true" && -n "${PR_NUMBER:-}" ]]; then + version="${version}-rc.${PR_NUMBER}" + fi + output "changelog" "$changelog" output "version" "$version" } diff --git a/mise/tasks/clean.sh b/mise/tasks/clean.sh index 84a35236f..68991cc53 100755 --- a/mise/tasks/clean.sh +++ b/mise/tasks/clean.sh @@ -2,7 +2,5 @@ #MISE description="Clean up build artifacts" rm -f ./*.deb ./*.pkg rm -rf ./*.app -rm -rf wireguard-go-* -rm -rf wireguard-tools-* rm -f ./packaging/windows/naisdevice*.exe rm -rf ./bin diff --git a/mise/tasks/package/linux.sh b/mise/tasks/package/linux.sh index f2866876e..35e03de66 100755 --- a/mise/tasks/package/linux.sh +++ b/mise/tasks/package/linux.sh @@ -16,7 +16,9 @@ outfile="$OUTFILE" name=$(basename "$outfile" | cut -d '_' -f 1) arch="$GOARCH" -VERSION="1:${VERSION#v}" NAME="$name" ARCH="$arch" GOARCH="" go tool github.com/goreleaser/nfpm/v2/cmd/nfpm package \ +deb_version="${VERSION#v}" +deb_version="${deb_version//-/\~}" # convert semver pre-release '-' to debian '~' so pre-releases sort lower +VERSION="1:${deb_version}" NAME="$name" ARCH="$arch" GOARCH="" go tool github.com/goreleaser/nfpm/v2/cmd/nfpm package \ --packager deb \ --config "./assets/linux/nfpm.yaml" \ --target "$outfile" diff --git a/mise/tasks/package/windows.sh b/mise/tasks/package/windows.sh index d19435ad5..7ff6d45e1 100755 --- a/mise/tasks/package/windows.sh +++ b/mise/tasks/package/windows.sh @@ -8,7 +8,12 @@ set -o nounset # Windows MSI requires X.X.X.X version format # shellcheck disable=SC2153 -version="${VERSION#"v"}.0" +version="${VERSION#"v"}" +if [[ "$version" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-rc\.([0-9]+)$ ]]; then + version="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" +else + version="${version%%-*}.0" +fi # shellcheck disable=SC2153 outfile=$OUTFILE # shellcheck disable=SC2153 @@ -36,7 +41,5 @@ else sign_flags="" fi -wireguard="${MISE_PROJECT_ROOT}/assets/windows/wireguard-${GOARCH}-0.5.3.msi" -wireguard_filename=$(basename "$wireguard") # shellcheck disable=SC2086 -makensis -NOCD "-DWIREGUARD=$wireguard" "-DWIREGUARD_FILENAME=$wireguard_filename" "-DOUTFILE=$outfile" "-DVERSION=$version" $sign_flags ./assets/windows/naisdevice.nsi +makensis -NOCD "-DOUTFILE=$outfile" "-DVERSION=$version" $sign_flags ./assets/windows/naisdevice.nsi diff --git a/mise/tasks/smoke-test/linux.sh b/mise/tasks/smoke-test/linux.sh new file mode 100755 index 000000000..2ed98ff13 --- /dev/null +++ b/mise/tasks/smoke-test/linux.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +#MISE description="Run smoke test on Linux using the .deb installer" + +set -o errexit +set -o pipefail +set -o nounset + +installer_deb="${1:?usage: mise run smoke-test:linux }" + +cleanup() { + echo "Cleaning up..." + sudo dpkg -r naisdevice 2>/dev/null || true +} +trap cleanup EXIT + +echo "==> Building smoke-test binary" +go build -o ./smoke-test ./cmd/smoke-test + +echo "==> Installing dependencies" +sudo apt-get update && sudo apt-get install --yes jq + +echo "==> Installing $installer_deb" +sudo apt-get install --yes "$installer_deb" + +echo "==> Waiting for helper to start" +for i in $(seq 1 10); do + if sudo test -S /run/naisdevice/helper.sock; then + echo "helper is running" + break + fi + if [ "$i" -eq 10 ]; then + echo "helper socket not found after install" + sudo journalctl -u naisdevice-helper.service --no-pager -n 50 || true + exit 1 + fi + sleep 3 +done + +echo "==> Running smoke test" +sudo ./smoke-test +echo "==> Smoke test passed" diff --git a/mise/tasks/smoke-test/macos.sh b/mise/tasks/smoke-test/macos.sh new file mode 100755 index 000000000..94e7c7418 --- /dev/null +++ b/mise/tasks/smoke-test/macos.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +#MISE description="Run smoke test on macOS using the .pkg installer" + +set -o errexit +set -o pipefail +set -o nounset + +installer_pkg="${1:?usage: mise run smoke-test:macos }" + +cleanup() { + echo "Cleaning up..." + sudo launchctl unload /Library/LaunchDaemons/io.nais.device.helper.plist 2>/dev/null || true + sudo rm -rf /Applications/naisdevice.app + sudo rm -f /Library/LaunchDaemons/io.nais.device.helper.plist +} +trap cleanup EXIT + +echo "==> Building smoke-test binary" +go build -o ./smoke-test ./cmd/smoke-test + +echo "==> Installing $installer_pkg" +sudo installer -pkg "$installer_pkg" -target / + +echo "==> Waiting for helper to start" +for i in $(seq 1 10); do + if sudo test -S /var/run/naisdevice/helper.sock; then + echo "helper is running" + break + fi + if [ "$i" -eq 10 ]; then + echo "helper socket not found after install" + cat /Library/Logs/device-agent-helper-*.log 2>/dev/null || true + exit 1 + fi + sleep 3 +done + +echo "==> Running smoke test" +sudo ./smoke-test +echo "==> Smoke test passed" diff --git a/mise/tasks/smoke-test/windows.sh b/mise/tasks/smoke-test/windows.sh new file mode 100755 index 000000000..094813f1e --- /dev/null +++ b/mise/tasks/smoke-test/windows.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +#MISE description="Run smoke test on Windows using the NSIS installer" + +set -o errexit +set -o pipefail +set -o nounset + +installer_exe="${1:?usage: mise run smoke-test:windows }" +install_dir="${PROGRAMFILES:-C:\\Program Files}\\NAV\\naisdevice" + +cleanup() { + echo "Cleaning up..." + uninstaller="$install_dir\\uninstaller.exe" + if [ -f "$uninstaller" ]; then + powershell.exe -Command "Start-Process -FilePath '$uninstaller' -ArgumentList '/S' -Wait" + echo "Uninstaller completed" + fi +} +trap cleanup EXIT + +echo "==> Building smoke-test binary" +go build -o ./smoke-test.exe ./cmd/smoke-test + +echo "==> Running installer" +powershell.exe -Command "Start-Process -FilePath '$(cygpath -w "$installer_exe")' -ArgumentList '/S' -Wait" + +echo "==> Verifying files installed" +if [ ! -f "$install_dir/naisdevice-helper.exe" ]; then + echo "naisdevice-helper.exe not found in $install_dir" + exit 1 +fi +echo "Binary installed at $install_dir" + +echo "==> Waiting for service to start" +for i in $(seq 1 10); do + status=$(powershell.exe -Command "(Get-Service -Name NaisDeviceHelper -ErrorAction SilentlyContinue).Status" | tr -d '\r') + if [ "$status" = "Running" ]; then + echo "NaisDeviceHelper is running" + break + fi + if [ "$i" -eq 10 ]; then + echo "Service is not running after waiting: $status" + log_dir="${PROGRAMDATA:-C:\\ProgramData}\\NAV\\naisdevice\\logs" + if [ -d "$log_dir" ]; then + for f in "$log_dir"/*; do + echo "--- $(basename "$f") ---" + tail -50 "$f" 2>/dev/null || true + done + fi + exit 1 + fi + sleep 3 +done + +echo "==> Running smoke test" +./smoke-test.exe +echo "==> Smoke test passed" diff --git a/packaging/nix/naisdevice/module.nix b/packaging/nix/naisdevice/module.nix index ec3e3c658..74f453604 100644 --- a/packaging/nix/naisdevice/module.nix +++ b/packaging/nix/naisdevice/module.nix @@ -22,10 +22,6 @@ in { systemd.services.naisdevice-helper = { description = "naisdevice-helper service"; wantedBy = ["multi-user.target"]; - path = [ - pkgs.wireguard-tools - pkgs.iproute2 - ]; serviceConfig.ExecStart = "${cfg.package}/bin/naisdevice-helper"; serviceConfig.Restart = "always"; };