diff --git a/.github/workflows/build-macos.yaml b/.github/workflows/build-macos.yaml new file mode 100644 index 00000000..cdafa43f --- /dev/null +++ b/.github/workflows/build-macos.yaml @@ -0,0 +1,92 @@ +name: Build macOS app +on: + push: + branches: + - main + - dev + - "release/**" + paths-ignore: + - "*.md" + - "LICENSE" + tags: + - v*.*.* + +jobs: + build-macos: + runs-on: + - self-hosted + - macOS + env: + APPLE_SIGNING_IDENTITY: "Apple Distribution: defguard sp. z o.o. (82GZ7KN29J)" + APPLE_SIGNING_IDENTITY_INSTALLER: "3rd Party Mac Developer Installer: defguard sp. z o.o. (82GZ7KN29J)" + APPLE_PROVIDER_SHORT_NAME: "82GZ7KN29J" + APPLE_ID: "kamil@defguard.net" + APPLE_TEAM_ID: "82GZ7KN29J" + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Write release version + run: | + VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d '-' -f1) + echo Version: $VERSION + echo "VERSION=$VERSION" >> ${GITHUB_ENV} + + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: "pnpm" + + - uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> ${GITHUB_ENV} + + - name: Install deps + run: pnpm install --frozen-lockfile + + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin + + - name: Unlock keychain + run: security -v unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" login.keychain + + - name: Set build number + run: | + sed -i '' "s,@BUILD_NUMBER@,${{ github.run_number }}," src-tauri/tauri.conf.json + sed -i '' "s,@BUILD_NUMBER@,${{ github.run_number }}," swift/extension/VPNExtension.xcodeproj/project.pbxproj + + - name: Build app + # Switch back to tauri-action when this gets merged https://github.com/tauri-apps/tauri/pull/14379 + # uses: tauri-apps/tauri-action@v0 # 0.5.24 seems to be broken, TODO: update when fixed + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: cd src-tauri && cargo tauri build --bundles app --target universal-apple-darwin + # with: + # args: --target universal-apple-darwin + + - name: Build installation package + run: | + security -v unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" login.keychain + xcrun productbuild --sign "${{ env.APPLE_SIGNING_IDENTITY_INSTALLER }}" --component "src-tauri/target/universal-apple-darwin/release/bundle/macos/Defguard.app" /Applications defguard-client.pkg + xcrun altool --upload-app --type macos --file defguard-client.pkg --apiKey ${{ secrets.APPLE_API_KEY }} --apiIssuer ${{ secrets.APPLE_API_ISSUER }} + # xcrun notarytool submit --wait --apple-id ${{ env.APPLE_ID }} --password ${{ secrets.NOTARYTOOL_APP_SPECIFIC_PASSWORD }} --team-id ${{ env.APPLE_TEAM_ID }} defguard-client.pkg + # xcrun stapler staple defguard-client.pkg + + # - name: Upload installation package + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # upload_url: ${{ needs.create-release.outputs.upload_url }} + # asset_path: defguard-client.pkg + # asset_name: defguard-client-universal-${{ env.VERSION }}.pkg + # asset_content_type: application/octet-stream diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 300ee8ea..0bcee8ad 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -3,41 +3,8 @@ on: push: tags: - v*.*.* -jobs: - build-wireguard-go: - strategy: - fail-fast: false - matrix: - architecture: [arm64, amd64] - runs-on: [self-hosted, macOS] - steps: - - uses: actions/checkout@v5 - with: - repository: WireGuard/wireguard-go - ref: master - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.24" - - name: Build wireguard-go binary - run: make - env: - GOOS: darwin - GOARCH: ${{ matrix.architecture }} - - name: Upload binary artifact arm64 - if: matrix.architecture == 'arm64' - uses: actions/upload-artifact@v4 - with: - name: wireguard-go-aarch64-apple-darwin - path: wireguard-go - - name: Upload binary artifact amd64 - if: matrix.architecture == 'amd64' - uses: actions/upload-artifact@v4 - with: - name: wireguard-go-x86_64-apple-darwin - path: wireguard-go +jobs: create-release: name: create-release runs-on: self-hosted @@ -148,6 +115,8 @@ jobs: build-linux: needs: - create-release + outputs: + deb_sha256_amd64: ${{ steps.calculate-sha256.outputs.deb_sha256_amd64 }} runs-on: - self-hosted - Linux @@ -198,11 +167,20 @@ jobs: sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libssl-dev libxdo-dev unzip protobuf-compiler libprotobuf-dev rpm - name: Build packages - uses: tauri-apps/tauri-action@v0.5.23 + uses: tauri-apps/tauri-action@v0.5.23 # .24 seems broken, TODO: update when fixed env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: args: "--bundles deb,rpm" + - name: Calculate DEB SHA256 + id: calculate-sha256 + if: matrix.deb_arch == 'amd64' + run: | + DEB_FILE="src-tauri/target/release/bundle/deb/defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}.deb" + DEB_SHA256=$(sha256sum "$DEB_FILE" | cut -d ' ' -f1) + echo "DEB SHA256: $DEB_SHA256" + echo "DEB_SHA256=$DEB_SHA256" >> ${GITHUB_ENV} + echo "deb_sha256_${{ matrix.deb_arch }}=$DEB_SHA256" >> ${GITHUB_OUTPUT} - name: Upload RPM uses: actions/upload-release-asset@v1 env: @@ -221,6 +199,16 @@ jobs: asset_path: src-tauri/target/release/bundle/deb/defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}.deb asset_name: defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}.deb asset_content_type: application/octet-stream + - name: Install ruby with deb-s3 + if: matrix.build != 'freebsd' + run: | + sudo apt-get install -y ruby + gem install deb-s3 + echo "$(ruby -r rubygems -e 'puts Gem.user_dir')/bin" >> $GITHUB_PATH + - name: Upload DEB to APT repository #Add this to ubuntu 22.04 job (on merge dev -> main) with --codename=bookworm + run: | + COMPONENT=$([[ "${{ github.ref_name }}" == *"-"* ]] && echo "pre-release" || echo "release") # if tag contain "-" assume it's pre-release. + deb-s3 upload -l --bucket=apt.defguard.net --access-key-id=${{ secrets.AWS_ACCESS_KEY_APT }} --secret-access-key=${{ secrets.AWS_SECRET_KEY_APT }} --s3-region=eu-north-1 --no-fail-if-exists --codename=trixie --component="$COMPONENT" src-tauri/target/release/bundle/deb/defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}.deb - name: Rename client binary run: mv src-tauri/target/release/defguard-client defguard-client-linux-${{ matrix.binary_arch }}-${{ github.ref_name }} - name: Tar client binary @@ -305,100 +293,109 @@ jobs: asset_name: dg-linux-${{ matrix.binary_arch }}-${{ github.ref_name }}.rpm asset_content_type: application/octet-stream - build-macos: + apt-sign: + needs: #Add needs: -ubuntu-22-04-build (on merge dev -> main) + - build-linux + runs-on: + - self-hosted + - Linux + - X64 + steps: + - name: Sign APT repository + run: | + export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_APT }} + export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_KEY_APT }} + export AWS_REGION=eu-north-1 + sudo apt update -y + sudo apt install -y awscli curl jq + + for DIST in trixie bookworm; do + aws s3 cp s3://apt.defguard.net/dists/${DIST}/Release . + + curl -X POST "${{ secrets.DEFGUARD_SIGNING_URL }}?signature_type=both" \ + -H "Authorization: Bearer ${{ secrets.DEFGUARD_SIGNING_API_KEY }}" \ + -F "file=@Release" \ + -o response.json + + cat response.json | jq -r '.files["Release.gpg"].content' | base64 --decode > Release.gpg + cat response.json | jq -r '.files.Release.content' | base64 --decode > InRelease + + aws s3 cp Release.gpg s3://apt.defguard.net/dists/${DIST}/ --acl public-read + aws s3 cp InRelease s3://apt.defguard.net/dists/${DIST}/ --acl public-read + + done + (aws s3 ls s3://apt.defguard.net/dists/ --recursive; aws s3 ls s3://apt.defguard.net/pool/ --recursive) | awk '{print ""$4"
"}' > index.html + aws s3 cp index.html s3://apt.defguard.net/ --acl public-read + + update-aur: needs: - create-release - - build-wireguard-go + - build-linux + if: "!contains(github.ref_name, '-')" + runs-on: + - self-hosted + - Linux + - ${{ matrix.architecture }} + container: archlinux:latest strategy: fail-fast: false matrix: - target: [aarch64-apple-darwin, x86_64-apple-darwin] - runs-on: - - self-hosted - - macOS - env: - APPLE_SIGNING_IDENTITY_APPLICATION: "Developer ID Application: defguard sp. z o.o. (82GZ7KN29J)" - APPLE_SIGNING_IDENTITY_INSTALLER: "Developer ID Installer: defguard sp. z o.o. (82GZ7KN29J)" - APPLE_ID: "kamil@defguard.net" - APPLE_TEAM_ID: "82GZ7KN29J" + architecture: [X64] + include: + - architecture: X64 + deb_arch: amd64 + binary_arch: x86_64 steps: - - uses: actions/checkout@v5 - with: - submodules: "recursive" - - name: Write release version + - name: Install dependencies run: | - VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d '-' -f1) - echo Version: $VERSION - echo "VERSION=$VERSION" >> ${GITHUB_ENV} - - uses: actions/setup-node@v4 - with: - node-version: "22" - - uses: pnpm/action-setup@v4 - with: - version: 10 - run_install: false - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> ${GITHUB_ENV} - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-build-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-build-store- - - name: Install deps - run: pnpm install --frozen-lockfile - - uses: dtolnay/rust-toolchain@stable - - name: Install protobuf compiler - run: brew install protobuf - - name: Install ARM target - run: rustup target add aarch64-apple-darwin - - name: Download wireguard-go binary - uses: actions/download-artifact@v4 - with: - name: wireguard-go-${{ matrix.target }} - path: src-tauri/resources-macos/binaries - - name: Rename wireguard-go binary + pacman -Syu --noconfirm + pacman -S --noconfirm git openssh base-devel + - name: Setup SSH for AUR run: | - ls -l src-tauri/resources-macos/binaries - mv src-tauri/resources-macos/binaries/wireguard-go src-tauri/resources-macos/binaries/wireguard-go-${{ matrix.target }} - - name: Unlock keychain - run: security -v unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" /Users/admin/Library/Keychains/login.keychain - - name: Build app - uses: tauri-apps/tauri-action@v0.5.23 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY_APPLICATION }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_ID: ${{ env.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.NOTARYTOOL_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }} - with: - args: --target ${{ matrix.target }} -v - - name: Build installation package + mkdir -p ~/.ssh + echo "${{ secrets.AUR_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts + # Create SSH config file + cat > ~/.ssh/config << EOF + Host aur.archlinux.org + IdentityFile ~/.ssh/id_rsa + User aur + StrictHostKeyChecking accept-new + EOF + chmod 600 ~/.ssh/config + - name: Update AUR Package run: | - bash build-macos-package.sh src-tauri/target/${{ matrix.target }} src-tauri/resources-macos/scripts '${{ env.APPLE_SIGNING_IDENTITY_INSTALLER }}' /Users/admin/Library/Keychains/login.keychain - xcrun notarytool submit --wait --apple-id ${{ env.APPLE_ID }} --password ${{ secrets.NOTARYTOOL_APP_SPECIFIC_PASSWORD }} --team-id ${{ env.APPLE_TEAM_ID }} src-tauri/target/${{ matrix.target }}/product-signed/defguard.pkg - xcrun stapler staple src-tauri/target/${{ matrix.target }}/product-signed/defguard.pkg - - name: Upload installation package - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: src-tauri/target/${{ matrix.target }}/product-signed/defguard.pkg - asset_name: defguard-${{ matrix.target }}-${{ env.VERSION }}.pkg - asset_content_type: application/octet-stream - # Building signed Windows bundle involves a few steps as described here: - # https://wixtoolset.org/docs/tools/signing/#signing-bundles-at-the-command-line - # 1. Build Defguard and bundle the binaries (Defguard and WireGuard) using Wix (Windows) - # 2. Detach the burn engine from the bundle so that it can be signed (also Windows) - # 3. Sign the burn engine (Linux) - # 4. Reattach the burn engine back to the bundle (Windows again) - # 5. Sign the whole bundle (Linux) + git config --global user.name "Defguard Build System" + git config --global user.email "community@defguard.net" + git config --global --add safe.directory '*' + + rm -rf aur-repo || true + GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=accept-new" \ + git clone "ssh://aur@aur.archlinux.org/defguard-client.git" aur-repo + cd aur-repo + git config --global --add safe.directory "$(pwd)" + VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d '-' -f1) + echo "Updating to version: $VERSION" + sed -i "s/^pkgver=.*/pkgver=$VERSION/" PKGBUILD + + AMD64_SHA="${{ needs.build-linux.outputs.deb_sha256_amd64 }}" + echo "AMD64 DEB SHA256: $AMD64_SHA" + sed -i "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('$AMD64_SHA')/" PKGBUILD + + useradd -m builduser + chown -R builduser:builduser . + + su builduser -c "makepkg --printsrcinfo" > .SRCINFO + git add PKGBUILD .SRCINFO + git commit -m "Updated to $VERSION" + GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=accept-new" git push + cat PKGBUILD + cat .SRCINFO + + # Builds Windows MSI and uploads it as artifact build-windows: needs: - create-release @@ -412,7 +409,7 @@ jobs: $env:VERSION=echo ($env:GITHUB_REF_NAME.Substring(1) -Split "-")[0] echo Version: $env:VERSION echo "VERSION=$env:VERSION" >> $env:GITHUB_ENV - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: "22" - uses: pnpm/action-setup@v4 @@ -433,78 +430,27 @@ jobs: run: pnpm install --frozen-lockfile - uses: dtolnay/rust-toolchain@stable - name: Install Protoc - uses: arduino/setup-protoc@v2 + uses: arduino/setup-protoc@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Remove "default-run" line from Cargo.toml run: | Set-Content -Path ".\src-tauri\Cargo.toml" -Value (get-content -Path ".\src-tauri\Cargo.toml" | Select-String -Pattern 'default-run =' -NotMatch) - name: Build packages - uses: tauri-apps/tauri-action@v0.5.23 + uses: tauri-apps/tauri-action@v0.5.23 # .24 seems broken, TODO: update when fixed env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Bundle application - run: | - dotnet tool install --global wix --version 4.0.5 - wix extension add WixToolset.Bal.wixext/4 - wix build .\src-tauri\resources-windows\defguard-client.wxs -ext .\.wix\extensions\WixToolset.Bal.wixext\4\wixext4\WixToolset.Bal.wixext.dll - wix burn detach .\src-tauri\resources-windows\defguard-client.exe -engine .\src-tauri\resources-windows\burnengine.exe - - name: Upload unsigned bundle and burn-engine - uses: actions/upload-artifact@v4 - with: - name: unsigned-bundle-and-burnengine - path: | - src-tauri/resources-windows/defguard-client.exe - src-tauri/resources-windows/burnengine.exe - sign-burn-engine: - needs: - - build-windows - runs-on: - - self-hosted - - Linux - - X64 - steps: - - name: Write release version - run: | - VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d '-' -f1) - echo Version: $VERSION - echo "VERSION=$VERSION" >> ${GITHUB_ENV} - - name: Download unsigned bundle & burn-engine - uses: actions/download-artifact@v4 - with: - name: unsigned-bundle-and-burnengine - - name: Sign burn-engine - run: osslsigncode sign -pkcs11module /srv/codesign/certum/sc30pkcs11-3.0.6.71-MS.so -pkcs11cert ${{ secrets.CODESIGN_KEYID }} -key ${{ secrets.CODESIGN_KEYID }} -pass ${{ secrets.CODESIGN_PIN }} -h sha256 -t http://time.certum.pl/ -in burnengine.exe -out burnengine-signed.exe - - name: Upload bundle and burn-engine artifact + - name: Upload unsigned bundle uses: actions/upload-artifact@v4 with: - name: unsigned-bundle-and-signed-burnengine - path: | - defguard-client.exe - burnengine-signed.exe - reattach-burn-engine: - needs: - - sign-burn-engine - runs-on: windows-latest - steps: - - name: Download unsigned bundle and signed burn-engine - uses: actions/download-artifact@v4 - with: - name: unsigned-bundle-and-signed-burnengine - - name: Reattach burn-engine - run: | - dotnet tool install --global wix --version 4.0.5 - wix extension add WixToolset.Bal.wixext/4 - wix burn reattach defguard-client.exe -engine burnengine-signed.exe -o defguard-client-reattached.exe - - name: Upload bundle with reattached burn-engine - uses: actions/upload-artifact@v4 - with: - name: unsigned-bundle-with-reattached-signed-burn-engine - path: defguard-client-reattached.exe + name: unsigned-bundle + path: src-tauri/target/release/bundle/msi/defguard-client_${{ env.VERSION }}_x64_en-US.msi + + # Signs the MSI and uploads it as release asset sign-bundle: needs: - create-release - - reattach-burn-engine + - build-windows runs-on: - self-hosted - Linux @@ -515,55 +461,18 @@ jobs: VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d '-' -f1) echo Version: $VERSION echo "VERSION=$VERSION" >> ${GITHUB_ENV} - - name: Download unsigned bundle & signed burn-engine + - name: Download unsigned bundle uses: actions/download-artifact@v4 with: - name: unsigned-bundle-with-reattached-signed-burn-engine + name: unsigned-bundle - name: Sign bundle - run: osslsigncode sign -pkcs11module /srv/codesign/certum/sc30pkcs11-3.0.6.71-MS.so -pkcs11cert ${{ secrets.CODESIGN_KEYID }} -key ${{ secrets.CODESIGN_KEYID }} -pass ${{ secrets.CODESIGN_PIN }} -h sha256 -t http://time.certum.pl/ -in defguard-client-reattached.exe -out defguard-client-signed.exe + run: osslsigncode sign -pkcs11module /srv/codesign/certum/sc30pkcs11-3.0.6.71-MS.so -pkcs11cert ${{ secrets.CODESIGN_KEYID }} -key ${{ secrets.CODESIGN_KEYID }} -pass ${{ secrets.CODESIGN_PIN }} -h sha256 -t http://time.certum.pl/ -in defguard-client_${{ env.VERSION }}_x64_en-US.msi -out defguard-client-signed.msi - name: Upload installer asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: defguard-client-signed.exe - asset_name: defguard-client_${{ env.VERSION }}_x64_en-US.exe + asset_path: defguard-client-signed.msi + asset_name: defguard-client_${{ env.VERSION }}_x64_en-US.msi asset_content_type: application/octet-stream - - apt-sign: - needs: - - build-linux - - ubuntu-22-04-build - runs-on: - - self-hosted - - Linux - - X64 - strategy: - fail-fast: false - steps: - - name: Sign APT repository - run: | - export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_APT }} - export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_KEY_APT }} - export AWS_REGION=eu-north-1 - sudo apt update -y - sudo apt install -y awscli curl jq - - for DIST in trixie bookworm; do - aws s3 cp s3://apt.defguard.net/dists/${DIST}/Release . - - curl -X POST "${{ secrets.DEFGUARD_SIGNING_URL }}?signature_type=both" \ - -H "Authorization: Bearer ${{ secrets.DEFGUARD_SIGNING_API_KEY }}" \ - -F "file=@Release" \ - -o response.json - - cat response.json | jq -r '.files["Release.gpg"].content' | base64 --decode > Release.gpg - cat response.json | jq -r '.files.Release.content' | base64 --decode > InRelease - - aws s3 cp Release.gpg s3://apt.defguard.net/dists/${DIST}/ --acl public-read - aws s3 cp InRelease s3://apt.defguard.net/dists/${DIST}/ --acl public-read - - done - (aws s3 ls s3://apt.defguard.net/dists/ --recursive; aws s3 ls s3://apt.defguard.net/pool/ --recursive) | awk '{print ""$4"
"}' > index.html - aws s3 cp index.html s3://apt.defguard.net/ --acl public-read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d7d50b2..24da6179 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: - name: Run cargo deny working-directory: ./src-tauri run: | - cargo install cargo-deny + cargo install cargo-deny --version 0.18.6 cargo deny check - name: Run tests run: cargo test --locked --no-fail-fast diff --git a/.gitmodules b/.gitmodules index 5aee25bb..d4d2f74e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src-tauri/proto"] path = src-tauri/proto url = ../proto.git +[submodule "swift/boringtun"] + path = swift/boringtun + url = ../boringtun.git diff --git a/README.md b/README.md index 64c52278..2880a3f0 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,33 @@ Built packages are available after in `src-tauri/target/release/bundle`. ### Windows -Remove `default-run` line from `[package]` section in `Cargo.toml` to build the project. +For windows development you'll need: + +1. The `stable-x86_64-pc-windows-gnu` Rust toolchain. Use `rustup` to change the toolchain: + +``` +rustup install stable-x86_64-pc-windows-gnu +rustup default stable-x86_64-pc-windows-gnu +``` + +2. Install [MSYS2](https://www.msys2.org/) + +3. Then run this in the MSYS2 terminal: + +``` +pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain mingw-w64-ucrt-x86_64-nasm +``` + +4. Finally add msys to your PATH: + +``` +# cmd +set PATH=C:\msys64\ucrt64\bin;%PATH% +# power-shell +$env:PATH = "C:\msys64\ucrt64\bin;" + $env:PATH +``` + +More info can be found [here](https://stackoverflow.com/a/79640980). # Legal diff --git a/biome.json b/biome.json index c2ca6e40..13918eb9 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -27,9 +27,7 @@ "bracketSpacing": true, "expand": "auto", "useEditorconfig": true, - "includes": [ - "./src/**" - ] + "includes": ["./src/**"] }, "linter": { "enabled": true, @@ -70,9 +68,7 @@ "noArrayIndexKey": "off" } }, - "includes": [ - "src/**" - ] + "includes": ["src/**"] }, "javascript": { "formatter": { @@ -94,9 +90,7 @@ }, "overrides": [ { - "includes": [ - "**/*.js" - ] + "includes": ["**/*.js"] } ], "assist": { diff --git a/build-macos-package.sh b/build-macos-package.sh deleted file mode 100755 index 99ed7b29..00000000 --- a/build-macos-package.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -set -e - -TARGET_DIRECTORY=$1 -SCRIPTS_DIRECTORY=$2 -APPLE_PACKAGE_SIGNING_IDENTITY=$3 -KEYCHAIN=$4 - -mkdir -p "${TARGET_DIRECTORY}/package" -mkdir -p "${TARGET_DIRECTORY}/product" -mkdir -p "${TARGET_DIRECTORY}/product-signed" - -APP_ROOT="${TARGET_DIRECTORY}/release/bundle/macos/defguard-client.app" - -chmod -R 755 ${APP_ROOT} - -pkgbuild \ - --analyze \ - --root ${APP_ROOT} \ - "${TARGET_DIRECTORY}/defguard-client.plist" - -PACKAGE_PATH="${TARGET_DIRECTORY}/package/defguard.pkg" - -pkgbuild \ - --identifier "net.defguard" \ - --root ${APP_ROOT} \ - --component-plist ${TARGET_DIRECTORY}/defguard-client.plist \ - --install-location "/Applications/defguard-client.app" \ - --scripts ${SCRIPTS_DIRECTORY} \ - "${PACKAGE_PATH}" - -productbuild \ - --package "${PACKAGE_PATH}" \ - "${TARGET_DIRECTORY}/product/defguard.pkg" - -productsign \ - --sign "${APPLE_PACKAGE_SIGNING_IDENTITY}" \ - --keychain "${KEYCHAIN}" \ - "${TARGET_DIRECTORY}/product/defguard.pkg" \ - "${TARGET_DIRECTORY}/product-signed/defguard.pkg" diff --git a/flake.lock b/flake.lock index b10558f0..3582629c 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,29 @@ { "nodes": { + "boringtun": { + "flake": false, + "locked": { + "path": "swift/boringtun", + "type": "path" + }, + "original": { + "path": "swift/boringtun", + "type": "path" + }, + "parent": [] + }, + "defguard-ui": { + "flake": false, + "locked": { + "path": "src/shared/defguard-ui", + "type": "path" + }, + "original": { + "path": "src/shared/defguard-ui", + "type": "path" + }, + "parent": [] + }, "flake-utils": { "inputs": { "systems": "systems" @@ -20,11 +44,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1758213207, - "narHash": "sha256-rqoqF0LEi+6ZT59tr+hTQlxVwrzQsET01U4uUdmqRtM=", + "lastModified": 1765644376, + "narHash": "sha256-yqHBL2wYGwjGL2GUF2w3tofWl8qO9tZEuI4wSqbCrtE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f4b140d5b253f5e2a1ff4e5506edbf8267724bde", + "rev": "23735a82a828372c4ef92c660864e82fbe2f5fbe", "type": "github" }, "original": { @@ -48,10 +72,25 @@ "type": "github" } }, + "proto": { + "flake": false, + "locked": { + "path": "src-tauri/proto", + "type": "path" + }, + "original": { + "path": "src-tauri/proto", + "type": "path" + }, + "parent": [] + }, "root": { "inputs": { + "boringtun": "boringtun", + "defguard-ui": "defguard-ui", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", + "proto": "proto", "rust-overlay": "rust-overlay" } }, @@ -60,11 +99,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1758249250, - "narHash": "sha256-bg228atm49IZ8koNOlT3bsrFKE9sFjq6vn6Tx8eVgpc=", + "lastModified": 1765766816, + "narHash": "sha256-m2au5a2x9L3ikyBi0g3/NRJSjmHVDvT42mn+O6FlyPs=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "e26a009e7edab102bd569dc041459deb6c0009f4", + "rev": "4f53a635709d82652567f51ef7af4365fbc0c88b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6bd1404c..2621b4c0 100644 --- a/flake.nix +++ b/flake.nix @@ -4,8 +4,20 @@ flake-utils.url = "github:numtide/flake-utils"; rust-overlay.url = "github:oxalica/rust-overlay"; - # include git submodules + # let git manage submodules self.submodules = true; + proto = { + url = "path:src-tauri/proto"; + flake = false; + }; + defguard-ui = { + url = "path:src/shared/defguard-ui"; + flake = false; + }; + boringtun = { + url = "path:swift/boringtun"; + flake = false; + }; }; outputs = { @@ -13,6 +25,7 @@ nixpkgs, flake-utils, rust-overlay, + ... }: flake-utils.lib.eachDefaultSystem (system: let # add rust overlay diff --git a/nix/package.nix b/nix/package.nix index fba7831e..d975e027 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -3,10 +3,12 @@ lib, stdenv, rustPlatform, + rustc, + cargo, makeDesktopItem, }: let pname = "defguard-client"; - version = "1.5.3"; # TODO: Get this from Cargo.toml or git + version = "1.6.1"; # TODO: Get this from Cargo.toml or git desktopItem = makeDesktopItem { name = pname; @@ -17,7 +19,7 @@ categories = ["Network" "Security"]; }; - rustToolchain = pkgs.rust-bin.stable.latest.default; + pnpm = pkgs.pnpm_10; buildInputs = with pkgs; [ at-spi2-atk @@ -38,22 +40,21 @@ desktop-file-utils ]; - nativeBuildInputs = with pkgs; [ - rustToolchain - pkg-config - gobject-introspection - cargo-tauri - nodejs_24 - protobuf + nativeBuildInputs = [ + rustc + cargo + pkgs.pkg-config + pkgs.gobject-introspection + pkgs.cargo-tauri + pkgs.nodejs_24 + pkgs.protobuf pnpm # configures pnpm to use pre-fetched dependencies pnpm.configHook # configures cargo to use pre-fetched dependencies rustPlatform.cargoSetupHook - # perl - wrapGAppsHook # helper to add dynamic library paths - makeWrapper + pkgs.makeWrapper ]; in stdenv.mkDerivation (finalAttrs: rec { @@ -79,7 +80,7 @@ in ; fetcherVersion = 2; - hash = "sha256-GlgQuPpOibPrItt6X9EqV4QmCOyajZh5yy7gHh+O+ME="; + hash = "sha256-v47yaNnt7vLDPR7WVLSonmZBBOkYWnmTUqMiPZ/WCGo="; }; buildPhase = '' @@ -87,19 +88,20 @@ in ''; postInstall = '' - # copy client binary mkdir -p $out/bin + + # copy client binary cp src-tauri/target/release/${pname} $out/bin/ - # copy service binary - mkdir -p $out/bin + + # copy background service binary cp src-tauri/target/release/defguard-service $out/bin/ - # copy cli binary - mkdir -p $out/bin + + # copy CLI binary cp src-tauri/target/release/dg $out/bin/ # add required library to client binary RPATH wrapProgram $out/bin/${pname} \ - --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath [pkgs.libayatana-appindicator]} + --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath [pkgs.libayatana-appindicator pkgs.desktop-file-utils]} mkdir -p $out/share/applications cp ${desktopItem}/share/applications/* $out/share/applications/ diff --git a/nix/shell.nix b/nix/shell.nix index 3de733fd..34c2be3a 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -5,7 +5,17 @@ targets = ["x86_64-apple-darwin" "aarch64-apple-darwin" "x86_64-pc-windows-gnu"]; }; - defguard-client = pkgs.callPackage ./package.nix {}; + # share custom toolchain with package + rustPlatform = pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }; + + defguard-client = pkgs.callPackage ./package.nix { + inherit rustPlatform; + cargo = rustToolchain; + rustc = rustToolchain; + }; # runtime libraries needed to run the dev server libraries = with pkgs; [ @@ -18,6 +28,7 @@ in # add additional dev tools packages = with pkgs; [ + rustToolchain trunk sqlx-cli vtsls diff --git a/package.json b/package.json index 0cc2e3a3..e38096f7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "defguard-client", "private": false, - "version": "1.5.3", + "version": "1.6.1", "type": "module", "scripts": { "dev": "npm-run-all --parallel vite typesafe-i18n", @@ -44,7 +44,10 @@ "onlyBuiltDependencies": [ "@swc/core", "esbuild" - ] + ], + "overrides": { + "mdast-util-to-hast": "13.2.1" + } }, "dependencies": { "@floating-ui/react": "^0.27.16", @@ -52,84 +55,84 @@ "@react-hook/resize-observer": "^2.0.2", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/query-core": "^5.90.2", - "@tanstack/react-virtual": "3.13.12", - "@tauri-apps/api": "^2.8.0", - "@tauri-apps/plugin-clipboard-manager": "^2.3.0", - "@tauri-apps/plugin-deep-link": "^2.4.3", - "@tauri-apps/plugin-dialog": "^2.4.0", - "@tauri-apps/plugin-fs": "^2.4.2", - "@tauri-apps/plugin-http": "^2.5.2", - "@tauri-apps/plugin-log": "^2.7.0", - "@tauri-apps/plugin-notification": "^2.3.1", - "@tauri-apps/plugin-opener": "^2.5.0", - "@tauri-apps/plugin-os": "^2.3.1", - "@tauri-apps/plugin-process": "^2.3.0", - "@tauri-apps/plugin-window-state": "^2.4.0", + "@tanstack/query-core": "^5.90.12", + "@tanstack/react-virtual": "3.13.13", + "@tauri-apps/api": "^2.9.1", + "@tauri-apps/plugin-clipboard-manager": "^2.3.2", + "@tauri-apps/plugin-deep-link": "^2.4.5", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@tauri-apps/plugin-fs": "^2.4.4", + "@tauri-apps/plugin-http": "^2.5.4", + "@tauri-apps/plugin-log": "^2.7.1", + "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-opener": "^2.5.2", + "@tauri-apps/plugin-os": "^2.3.2", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-window-state": "^2.4.1", "@types/byte-size": "^8.1.2", "@use-gesture/react": "^10.3.1", "byte-size": "^9.0.1", "classnames": "^2.5.1", "clsx": "^2.1.1", "compare-versions": "^6.1.1", - "dayjs": "^1.11.18", + "dayjs": "^1.11.19", "deepmerge-ts": "^7.1.5", "detect-browser": "^5.3.0", "fast-deep-equal": "^3.1.3", "file-saver": "^2.0.5", "get-text-width": "^1.0.3", - "html-react-parser": "^5.2.6", + "html-react-parser": "^5.2.10", "itertools": "^2.5.0", "js-base64": "^3.7.8", "lodash-es": "^4.17.21", "merge-refs": "^2.0.0", "millify": "^6.1.0", - "motion": "^12.23.21", + "motion": "^12.23.26", "p-timeout": "^6.1.4", "prop-types": "^15.8.1", "radash": "^12.1.1", - "react": "^19.1.1", + "react": "^19.2.3", "react-auth-code-input": "^3.2.1", "react-click-away-listener": "^2.4.0", - "react-dom": "^19.1.1", - "react-hook-form": "^7.63.0", + "react-dom": "^19.2.3", + "react-hook-form": "^7.68.0", "react-hotkeys-hook": "^5.2.1", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "react-qr-code": "^2.0.18", - "react-router-dom": "^6.30.1", + "react-router-dom": "^6.30.2", "react-use-websocket": "^4.13.0", "react-virtualized-auto-sizer": "^1.0.26", - "recharts": "^3.2.1", + "recharts": "^3.6.0", "rehype-sanitize": "^6.0.0", "rxjs": "^7.8.2", - "use-breakpoint": "^4.0.6", + "use-breakpoint": "^4.0.10", "zod": "^3.25.76", - "zustand": "^5.0.8" + "zustand": "^5.0.9" }, "devDependencies": { - "@biomejs/biome": "^2.2.4", + "@biomejs/biome": "^2.3.8", "@hookform/devtools": "^4.4.0", "@svgr/cli": "^8.1.0", - "@tanstack/react-query": "^5.90.2", - "@tanstack/react-query-devtools": "^5.90.2", - "@tauri-apps/cli": "^2.8.4", + "@tanstack/react-query": "^5.90.12", + "@tanstack/react-query-devtools": "^5.91.1", + "@tauri-apps/cli": "^2.9.6", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.5.2", - "@types/react": "^19.1.13", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.3", - "@vitejs/plugin-react-swc": "^4.1.0", - "autoprefixer": "^10.4.21", + "@types/node": "^24.10.4", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-react-swc": "^4.2.2", + "autoprefixer": "^10.4.23", "npm-run-all": "^4.1.5", "postcss": "^8.5.6", - "prettier": "^3.6.2", + "prettier": "^3.7.4", "sass": "~1.92.1", - "typedoc": "^0.28.13", + "typedoc": "^0.28.15", "typesafe-i18n": "^5.26.2", - "typescript": "^5.9.2", - "vite": "^7.1.7" + "typescript": "^5.9.3", + "vite": "^7.2.7" }, "volta": { "node": "20.5.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 524c4d09..94bafb63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,19 +4,22 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + mdast-util-to-hast: 13.2.1 + importers: .: dependencies: '@floating-ui/react': specifier: ^0.27.16 - version: 0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@hookform/resolvers': specifier: ^3.10.0 - version: 3.10.0(react-hook-form@7.63.0(react@19.1.1)) + version: 3.10.0(react-hook-form@7.68.0(react@19.2.3)) '@react-hook/resize-observer': specifier: ^2.0.2 - version: 2.0.2(react@19.1.1) + version: 2.0.2(react@19.2.3) '@stablelib/base64': specifier: ^2.0.1 version: 2.0.1 @@ -24,53 +27,53 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/query-core': - specifier: ^5.90.2 - version: 5.90.2 + specifier: ^5.90.12 + version: 5.90.12 '@tanstack/react-virtual': - specifier: 3.13.12 - version: 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: 3.13.13 + version: 3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tauri-apps/api': - specifier: ^2.8.0 - version: 2.8.0 + specifier: ^2.9.1 + version: 2.9.1 '@tauri-apps/plugin-clipboard-manager': - specifier: ^2.3.0 - version: 2.3.0 + specifier: ^2.3.2 + version: 2.3.2 '@tauri-apps/plugin-deep-link': - specifier: ^2.4.3 - version: 2.4.3 + specifier: ^2.4.5 + version: 2.4.5 '@tauri-apps/plugin-dialog': - specifier: ^2.4.0 - version: 2.4.0 - '@tauri-apps/plugin-fs': specifier: ^2.4.2 version: 2.4.2 + '@tauri-apps/plugin-fs': + specifier: ^2.4.4 + version: 2.4.4 '@tauri-apps/plugin-http': - specifier: ^2.5.2 - version: 2.5.2 + specifier: ^2.5.4 + version: 2.5.4 '@tauri-apps/plugin-log': - specifier: ^2.7.0 - version: 2.7.0 + specifier: ^2.7.1 + version: 2.7.1 '@tauri-apps/plugin-notification': - specifier: ^2.3.1 - version: 2.3.1 + specifier: ^2.3.3 + version: 2.3.3 '@tauri-apps/plugin-opener': - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^2.5.2 + version: 2.5.2 '@tauri-apps/plugin-os': + specifier: ^2.3.2 + version: 2.3.2 + '@tauri-apps/plugin-process': specifier: ^2.3.1 version: 2.3.1 - '@tauri-apps/plugin-process': - specifier: ^2.3.0 - version: 2.3.0 '@tauri-apps/plugin-window-state': - specifier: ^2.4.0 - version: 2.4.0 + specifier: ^2.4.1 + version: 2.4.1 '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 '@use-gesture/react': specifier: ^10.3.1 - version: 10.3.1(react@19.1.1) + version: 10.3.1(react@19.2.3) byte-size: specifier: ^9.0.1 version: 9.0.1 @@ -84,8 +87,8 @@ importers: specifier: ^6.1.1 version: 6.1.1 dayjs: - specifier: ^1.11.18 - version: 1.11.18 + specifier: ^1.11.19 + version: 1.11.19 deepmerge-ts: specifier: ^7.1.5 version: 7.1.5 @@ -102,8 +105,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 html-react-parser: - specifier: ^5.2.6 - version: 5.2.6(@types/react@19.1.13)(react@19.1.1) + specifier: ^5.2.10 + version: 5.2.10(@types/react@19.2.7)(react@19.2.3) itertools: specifier: ^2.5.0 version: 2.5.0 @@ -115,13 +118,13 @@ importers: version: 4.17.21 merge-refs: specifier: ^2.0.0 - version: 2.0.0(@types/react@19.1.13) + version: 2.0.0(@types/react@19.2.7) millify: specifier: ^6.1.0 version: 6.1.0 motion: - specifier: ^12.23.21 - version: 12.23.21(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^12.23.26 + version: 12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) p-timeout: specifier: ^6.1.4 version: 6.1.4 @@ -132,44 +135,44 @@ importers: specifier: ^12.1.1 version: 12.1.1 react: - specifier: ^19.1.1 - version: 19.1.1 + specifier: ^19.2.3 + version: 19.2.3 react-auth-code-input: specifier: ^3.2.1 - version: 3.2.1(react@19.1.1) + version: 3.2.1(react@19.2.3) react-click-away-listener: specifier: ^2.4.0 - version: 2.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-dom: - specifier: ^19.1.1 - version: 19.1.1(react@19.1.1) + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) react-hook-form: - specifier: ^7.63.0 - version: 7.63.0(react@19.1.1) + specifier: ^7.68.0 + version: 7.68.0(react@19.2.3) react-hotkeys-hook: specifier: ^5.2.1 - version: 5.2.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 5.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-loading-skeleton: specifier: ^3.5.0 - version: 3.5.0(react@19.1.1) + version: 3.5.0(react@19.2.3) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.1.13)(react@19.1.1) + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) react-qr-code: specifier: ^2.0.18 - version: 2.0.18(react@19.1.1) + version: 2.0.18(react@19.2.3) react-router-dom: - specifier: ^6.30.1 - version: 6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^6.30.2 + version: 6.30.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-use-websocket: specifier: ^4.13.0 version: 4.13.0 react-virtualized-auto-sizer: specifier: ^1.0.26 - version: 1.0.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.0.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3) recharts: - specifier: ^3.2.1 - version: 3.2.1(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1) + specifier: ^3.6.0 + version: 3.6.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1) rehype-sanitize: specifier: ^6.0.0 version: 6.0.0 @@ -177,33 +180,33 @@ importers: specifier: ^7.8.2 version: 7.8.2 use-breakpoint: - specifier: ^4.0.6 - version: 4.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^4.0.10 + version: 4.0.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) zod: specifier: ^3.25.76 version: 3.25.76 zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@19.1.13)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + specifier: ^5.0.9 + version: 5.0.9(@types/react@19.2.7)(immer@11.0.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: '@biomejs/biome': - specifier: ^2.2.4 - version: 2.2.4 + specifier: ^2.3.8 + version: 2.3.8 '@hookform/devtools': specifier: ^4.4.0 - version: 4.4.0(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.4.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@svgr/cli': specifier: ^8.1.0 - version: 8.1.0(typescript@5.9.2) + version: 8.1.0(typescript@5.9.3) '@tanstack/react-query': - specifier: ^5.90.2 - version: 5.90.2(react@19.1.1) + specifier: ^5.90.12 + version: 5.90.12(react@19.2.3) '@tanstack/react-query-devtools': - specifier: ^5.90.2 - version: 5.90.2(@tanstack/react-query@5.90.2(react@19.1.1))(react@19.1.1) + specifier: ^5.91.1 + version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3) '@tauri-apps/cli': - specifier: ^2.8.4 - version: 2.8.4 + specifier: ^2.9.6 + version: 2.9.6 '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -211,23 +214,23 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.5.2 - version: 24.5.2 + specifier: ^24.10.4 + version: 24.10.4 '@types/react': - specifier: ^19.1.13 - version: 19.1.13 + specifier: ^19.2.7 + version: 19.2.7 '@types/react-dom': - specifier: ^19.1.9 - version: 19.1.9(@types/react@19.1.13) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': - specifier: ^5.0.3 - version: 5.0.3(vite@7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1)) + specifier: ^5.1.2 + version: 5.1.2(vite@7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2)) '@vitejs/plugin-react-swc': - specifier: ^4.1.0 - version: 4.1.0(vite@7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1)) + specifier: ^4.2.2 + version: 4.2.2(vite@7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2)) autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -235,23 +238,23 @@ importers: specifier: ^8.5.6 version: 8.5.6 prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.7.4 + version: 3.7.4 sass: specifier: ~1.92.1 version: 1.92.1 typedoc: - specifier: ^0.28.13 - version: 0.28.13(typescript@5.9.2) + specifier: ^0.28.15 + version: 0.28.15(typescript@5.9.3) typesafe-i18n: specifier: ^5.26.2 - version: 5.26.2(typescript@5.9.2) + version: 5.26.2(typescript@5.9.3) typescript: - specifier: ^5.9.2 - version: 5.9.2 + specifier: ^5.9.3 + version: 5.9.3 vite: - specifier: ^7.1.7 - version: 7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1) + specifier: ^7.2.7 + version: 7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2) packages: @@ -259,16 +262,16 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': @@ -297,8 +300,8 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': @@ -309,8 +312,8 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -334,63 +337,63 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.2.4': - resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} + '@biomejs/biome@2.3.8': + resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.4': - resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} + '@biomejs/cli-darwin-arm64@2.3.8': + resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.4': - resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} + '@biomejs/cli-darwin-x64@2.3.8': + resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.4': - resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} + '@biomejs/cli-linux-arm64-musl@2.3.8': + resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.4': - resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} + '@biomejs/cli-linux-arm64@2.3.8': + resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.4': - resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} + '@biomejs/cli-linux-x64-musl@2.3.8': + resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.4': - resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} + '@biomejs/cli-linux-x64@2.3.8': + resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.4': - resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} + '@biomejs/cli-win32-arm64@2.3.8': + resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.4': - resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} + '@biomejs/cli-win32-x64@2.3.8': + resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -449,158 +452,158 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -626,8 +629,8 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@gerrit0/mini-shiki@3.13.0': - resolution: {integrity: sha512-mCrNvZNYNrwKer5PWLF6cOc0OEe2eKzgy976x+IT2tynwJYl+7UpHTSeXQJGijgTcoOf+f359L946unWlYRnsg==} + '@gerrit0/mini-shiki@3.20.0': + resolution: {integrity: sha512-Wa57i+bMpK6PGJZ1f2myxo3iO+K/kZikcyvH8NIqNNZhQUbDav7V9LQmWOXhf946mz5c1NZ19WMsGYiDKTryzQ==} '@hookform/devtools@4.4.0': resolution: {integrity: sha512-Mtlic+uigoYBPXlfvPBfiYYUZuyMrD3pTjDpVIhL6eCZTvQkHsKBSKeZCvXWUZr8fqrkzDg27N+ZuazLKq6Vmg==} @@ -753,8 +756,8 @@ packages: peerDependencies: react: '>=18' - '@reduxjs/toolkit@2.9.0': - resolution: {integrity: sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==} + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 || ^19 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -764,134 +767,137 @@ packages: react-redux: optional: true - '@remix-run/router@1.23.0': - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + '@remix-run/router@1.23.1': + resolution: {integrity: sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==} engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-beta.35': - resolution: {integrity: sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} - '@rollup/rollup-android-arm-eabi@4.52.2': - resolution: {integrity: sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==} + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.2': - resolution: {integrity: sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==} + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.2': - resolution: {integrity: sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==} + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.2': - resolution: {integrity: sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==} + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.2': - resolution: {integrity: sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==} + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.2': - resolution: {integrity: sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==} + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.2': - resolution: {integrity: sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==} + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.2': - resolution: {integrity: sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==} + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.2': - resolution: {integrity: sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==} + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.2': - resolution: {integrity: sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==} + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.2': - resolution: {integrity: sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==} + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.2': - resolution: {integrity: sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==} + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.2': - resolution: {integrity: sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==} + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.2': - resolution: {integrity: sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==} + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.2': - resolution: {integrity: sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==} + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.2': - resolution: {integrity: sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==} + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.2': - resolution: {integrity: sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==} + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.2': - resolution: {integrity: sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==} + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.2': - resolution: {integrity: sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==} + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.2': - resolution: {integrity: sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==} + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.2': - resolution: {integrity: sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==} + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.2': - resolution: {integrity: sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==} + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} cpu: [x64] os: [win32] - '@shikijs/engine-oniguruma@3.13.0': - resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==} + '@shikijs/engine-oniguruma@3.20.0': + resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} - '@shikijs/langs@3.13.0': - resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==} + '@shikijs/langs@3.20.0': + resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} - '@shikijs/themes@3.13.0': - resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==} + '@shikijs/themes@3.20.0': + resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} - '@shikijs/types@3.13.0': - resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==} + '@shikijs/types@3.20.0': + resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1011,68 +1017,68 @@ packages: peerDependencies: '@svgr/core': '*' - '@swc/core-darwin-arm64@1.13.5': - resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} + '@swc/core-darwin-arm64@1.15.4': + resolution: {integrity: sha512-NU/Of+ShFGG/i0lXKsF6GaGeTBNsr9iD8uUzdXxFfGbEjTeuKNXc5CWn3/Uo4Gr4LMAGD3hsRwG2Jq5iBDMalw==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.5': - resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} + '@swc/core-darwin-x64@1.15.4': + resolution: {integrity: sha512-9oWYMZHiEfHLqjjRGrXL17I8HdAOpWK/Rps34RKQ74O+eliygi1Iyq1TDUzYqUXcNvqN2K5fHgoMLRIni41ClQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.5': - resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} + '@swc/core-linux-arm-gnueabihf@1.15.4': + resolution: {integrity: sha512-I1dPxXli3N1Vr71JXogUTLcspM5ICgCYaA16RE+JKchj3XKKmxLlYjwAHAA4lh/Cy486ikzACaG6pIBcegoGkg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.5': - resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} + '@swc/core-linux-arm64-gnu@1.15.4': + resolution: {integrity: sha512-iGpuS/2PDZ68ioAlhkxiN5M4+pB9uDJolTKk4mZ0JM29uFf9YIkiyk7Bbr2y1QtmD82rF0tDHhoG9jtnV8mZMg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.13.5': - resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} + '@swc/core-linux-arm64-musl@1.15.4': + resolution: {integrity: sha512-Ly95wc+VXDhl08pjAoPUhVu5vNbuPMbURknRZa5QOZuiizJ6DkaSI0/zsEc26PpC6HTc4prNLY3ARVwZ7j/IJQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.13.5': - resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} + '@swc/core-linux-x64-gnu@1.15.4': + resolution: {integrity: sha512-7pIG0BnaMn4zTpHeColPwyrWoTY9Drr+ISZQIgYHUKh3oaPtNCrXb289ScGbPPPjLsSfcGTeOy2pXmNczMC+yg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.13.5': - resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} + '@swc/core-linux-x64-musl@1.15.4': + resolution: {integrity: sha512-oaqTV25V9H+PpSkvTcK25q6Q56FvXc6d2xBu486dv9LAPCHWgeAworE8WpBLV26g8rubcN5nGhO5HwSunXA7Ww==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.13.5': - resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} + '@swc/core-win32-arm64-msvc@1.15.4': + resolution: {integrity: sha512-VcPuUJw27YbGo1HcOaAriI50dpM3ZZeDW3x2cMnJW6vtkeyzUFk1TADmTwFax0Fn+yicCxhaWjnFE3eAzGAxIQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.5': - resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} + '@swc/core-win32-ia32-msvc@1.15.4': + resolution: {integrity: sha512-dREjghAZEuKAK9nQzJETAiCSihSpAVS6Vk9+y2ElaoeTj68tNB1txV/m1RTPPD/+Kgbz6ITPNyXRWxPdkP5aXw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.5': - resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} + '@swc/core-win32-x64-msvc@1.15.4': + resolution: {integrity: sha512-o/odIBuQkoxKbRweJWOMI9LeRSOenFKN2zgPeaaNQ/cyuVk2r6DCAobKMOodvDdZWlMn6N1xJrldeCRSTZIgiQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.5': - resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} + '@swc/core@1.15.4': + resolution: {integrity: sha512-fH81BPo6EiJ7BUb6Qa5SY/NLWIRVambqU3740g0XPFPEz5KFPnzRYpR6zodQNOcEb9XUtZzRO1Y0WyIJP7iBxQ==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1086,138 +1092,138 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tanstack/query-core@5.90.2': - resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} - '@tanstack/query-devtools@5.90.1': - resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} + '@tanstack/query-devtools@5.91.1': + resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} - '@tanstack/react-query-devtools@5.90.2': - resolution: {integrity: sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==} + '@tanstack/react-query-devtools@5.91.1': + resolution: {integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==} peerDependencies: - '@tanstack/react-query': ^5.90.2 + '@tanstack/react-query': ^5.90.10 react: ^18 || ^19 - '@tanstack/react-query@5.90.2': - resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + '@tanstack/react-query@5.90.12': + resolution: {integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-virtual@3.13.12': - resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + '@tanstack/react-virtual@3.13.13': + resolution: {integrity: sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/virtual-core@3.13.12': - resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tanstack/virtual-core@3.13.13': + resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==} - '@tauri-apps/api@2.8.0': - resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} + '@tauri-apps/api@2.9.1': + resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} - '@tauri-apps/cli-darwin-arm64@2.8.4': - resolution: {integrity: sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA==} + '@tauri-apps/cli-darwin-arm64@2.9.6': + resolution: {integrity: sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.8.4': - resolution: {integrity: sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g==} + '@tauri-apps/cli-darwin-x64@2.9.6': + resolution: {integrity: sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.8.4': - resolution: {integrity: sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': + resolution: {integrity: sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.8.4': - resolution: {integrity: sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw==} + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': + resolution: {integrity: sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.8.4': - resolution: {integrity: sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw==} + '@tauri-apps/cli-linux-arm64-musl@2.9.6': + resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.8.4': - resolution: {integrity: sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ==} + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': + resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.8.4': - resolution: {integrity: sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw==} + '@tauri-apps/cli-linux-x64-gnu@2.9.6': + resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.8.4': - resolution: {integrity: sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA==} + '@tauri-apps/cli-linux-x64-musl@2.9.6': + resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.8.4': - resolution: {integrity: sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w==} + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': + resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.8.4': - resolution: {integrity: sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA==} + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': + resolution: {integrity: sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.8.4': - resolution: {integrity: sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA==} + '@tauri-apps/cli-win32-x64-msvc@2.9.6': + resolution: {integrity: sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.8.4': - resolution: {integrity: sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g==} + '@tauri-apps/cli@2.9.6': + resolution: {integrity: sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-clipboard-manager@2.3.0': - resolution: {integrity: sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==} + '@tauri-apps/plugin-clipboard-manager@2.3.2': + resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==} - '@tauri-apps/plugin-deep-link@2.4.3': - resolution: {integrity: sha512-yVCZpVG1ZrtfCvE7K5LRSrGqlyPlCrqlKgoREJHnfjyYdDtUhFmZqScOXpL8XL2PizJHDsoahEweuTaUPEokPA==} + '@tauri-apps/plugin-deep-link@2.4.5': + resolution: {integrity: sha512-Zf2RTj1D9IQQ45/jqW8XTKvql24HqlPjcpv0mV/O2jHQkNe11HOTZBVj6IK37qs+MWV7xZzcmazx/QVZnhAwaQ==} - '@tauri-apps/plugin-dialog@2.4.0': - resolution: {integrity: sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==} + '@tauri-apps/plugin-dialog@2.4.2': + resolution: {integrity: sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==} - '@tauri-apps/plugin-fs@2.4.2': - resolution: {integrity: sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==} + '@tauri-apps/plugin-fs@2.4.4': + resolution: {integrity: sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ==} - '@tauri-apps/plugin-http@2.5.2': - resolution: {integrity: sha512-x1mQKHSLDk4mS2S938OTeyk8L7QyLpCrKZCZcjkljGsvTvRMojCvI9SeJ1kaxc7t8xSilkC7WdId8xER9TIGLg==} + '@tauri-apps/plugin-http@2.5.4': + resolution: {integrity: sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg==} - '@tauri-apps/plugin-log@2.7.0': - resolution: {integrity: sha512-81XQ2f93x4vmIB5OY0XlYAxy60cHdYLs0Ki8Qp38tNATRiuBit+Orh3frpY3qfYQnqEvYVyRub7YRJWlmW2RRA==} + '@tauri-apps/plugin-log@2.7.1': + resolution: {integrity: sha512-jdb+o0wxQc8PjnLktgGpOs9Dh1YupaOGDXzO+Y8peA1UZ1ep3eXv4E1oiJ7nIQVN0XUFDDhnn3aBszl8ijhR+A==} - '@tauri-apps/plugin-notification@2.3.1': - resolution: {integrity: sha512-7gqgfANSREKhh35fY1L4j3TUjUdePmU735FYDqRGeIf8nMXWpcx6j4FhN9/4nYz+m0mv79DCTPLqIPTySggGgg==} + '@tauri-apps/plugin-notification@2.3.3': + resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} - '@tauri-apps/plugin-opener@2.5.0': - resolution: {integrity: sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==} + '@tauri-apps/plugin-opener@2.5.2': + resolution: {integrity: sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew==} - '@tauri-apps/plugin-os@2.3.1': - resolution: {integrity: sha512-ty5V8XDUIFbSnrk3zsFoP3kzN+vAufYzalJSlmrVhQTImIZa1aL1a03bOaP2vuBvfR+WDRC6NgV2xBl8G07d+w==} + '@tauri-apps/plugin-os@2.3.2': + resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} - '@tauri-apps/plugin-process@2.3.0': - resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} - '@tauri-apps/plugin-window-state@2.4.0': - resolution: {integrity: sha512-hRSzPNi2NG0lPFthfVY0V5C1MyWN/gGaQtQYw7i9zZhLzrhZveHZ2omHG1rIiIsjfTGbO7fhjydSoeTTK9GqLw==} + '@tauri-apps/plugin-window-state@2.4.1': + resolution: {integrity: sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==} '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} @@ -1283,8 +1289,8 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1292,19 +1298,19 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.5.2': - resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/react-dom@19.1.9': - resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 - '@types/react@19.1.13': - resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1326,14 +1332,14 @@ packages: peerDependencies: react: '>= 16.8.0' - '@vitejs/plugin-react-swc@4.1.0': - resolution: {integrity: sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==} + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitejs/plugin-react@5.0.3': - resolution: {integrity: sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg==} + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1365,8 +1371,8 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -1386,8 +1392,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.6: - resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==} + baseline-browser-mapping@2.9.7: + resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} hasBin: true boolbase@1.0.0: @@ -1403,8 +1409,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.2: - resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1437,8 +1443,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001743: - resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==} + caniuse-lite@1.0.30001760: + resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1550,8 +1556,8 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} @@ -1613,8 +1619,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.18: - resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -1682,8 +1688,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.223: - resolution: {integrity: sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1699,8 +1705,8 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -1723,11 +1729,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.39.10: - resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + es-toolkit@1.43.0: + resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -1778,11 +1784,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.23.21: - resolution: {integrity: sha512-UWDtzzPdRA3UpSNGril5HjUtPF1Uo/BCt5VKG/YQ8tVpSkAZ22+q8o+hYO0C1uDAZuotQjcfzsTsDtQxD46E/Q==} + framer-motion@12.23.26: + resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1813,6 +1819,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1898,11 +1908,11 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - html-dom-parser@5.1.1: - resolution: {integrity: sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg==} + html-dom-parser@5.1.2: + resolution: {integrity: sha512-9nD3Rj3/FuQt83AgIa1Y3ruzspwFFA54AJbQnohXN+K6fL1/bhcDQJJY5Ne4L4A163ADQFVESd/0TLyNoV0mfg==} - html-react-parser@5.2.6: - resolution: {integrity: sha512-qcpPWLaSvqXi+TndiHbCa+z8qt0tVzjMwFGFBAa41ggC+ZA5BHaMIeMJla9g3VSp4SmiZb9qyQbmbpHYpIfPOg==} + html-react-parser@5.2.10: + resolution: {integrity: sha512-DjOLloguuDA+Ed7Q7PKhvMQmCl2+Yk/pfvvca68fvn15QFBbL4uHGxXwoXQ4sqS0UyuRH2lJb0S8yZCL3lvehQ==} peerDependencies: '@types/react': 0.14 || 15 || 16 || 17 || 18 || 19 react: 0.14 || 15 || 16 || 17 || 18 || 19 @@ -1916,11 +1926,14 @@ packages: htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} - immer@10.1.3: - resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - immutable@5.1.3: - resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + immer@11.0.1: + resolution: {integrity: sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -1933,8 +1946,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} @@ -2000,8 +2013,8 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} is-glob@4.0.3: @@ -2082,8 +2095,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsesc@3.1.0: @@ -2162,8 +2175,8 @@ packages: mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -2274,14 +2287,14 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - motion-dom@12.23.21: - resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} motion-utils@12.23.6: resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} - motion@12.23.21: - resolution: {integrity: sha512-FzgbQNeZXHWXXEKmpfenYvF5wdc5i7lT/Kwr3xV4dmGVsU7Y30QcgCZsWHAlE/4McAWhNGbOAhgdiabXZ1EjnA==} + motion@12.23.26: + resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2311,16 +2324,12 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-releases@2.0.21: - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - npm-run-all@4.1.5: resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} engines: {node: '>= 4'} @@ -2422,8 +2431,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true @@ -2456,13 +2465,13 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom@19.1.1: - resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: - react: ^19.1.1 + react: ^19.2.3 - react-hook-form@7.63.0: - resolution: {integrity: sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==} + react-hook-form@7.68.0: + resolution: {integrity: sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -2476,9 +2485,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-loading-skeleton@3.5.0: resolution: {integrity: sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==} peerDependencies: @@ -2510,19 +2516,19 @@ packages: redux: optional: true - react-refresh@0.17.0: - resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} - react-router-dom@6.30.1: - resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==} + react-router-dom@6.30.2: + resolution: {integrity: sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' - react-router@6.30.1: - resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==} + react-router@6.30.2: + resolution: {integrity: sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' @@ -2541,8 +2547,8 @@ packages: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.1.1: - resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} read-pkg@3.0.0: @@ -2553,8 +2559,8 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - recharts@3.2.1: - resolution: {integrity: sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==} + recharts@3.6.0: + resolution: {integrity: sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==} engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2597,13 +2603,13 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true - rollup@4.52.2: - resolution: {integrity: sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==} + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2627,8 +2633,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - scheduler@0.26.0: - resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} @@ -2739,11 +2745,11 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - style-to-js@1.1.17: - resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} - style-to-object@1.0.9: - resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -2768,8 +2774,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tabbable@6.3.0: + resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -2807,8 +2813,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedoc@0.28.13: - resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==} + typedoc@0.28.15: + resolution: {integrity: sha512-mw2/2vTL7MlT+BVo43lOsufkkd2CJO4zeOSuWQQsiXoV2VuEn7f6IZp2jsUDPmBMABpgR0R5jlcJ2OGEFYmkyg==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: @@ -2820,8 +2826,8 @@ packages: peerDependencies: typescript: '>=3.5.1' - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -2832,14 +2838,14 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.12.0: - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} @@ -2847,20 +2853,20 @@ packages: unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.2.2: + resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - use-breakpoint@4.0.6: - resolution: {integrity: sha512-1s7vUjf36eeZYTgY1KkmPNXrTbKJVRA9cjBFQdYjK8+pDr0qJgH6/cuX5qQ2zcfkqxN5LieVd/DTVK6ofnwRTQ==} + use-breakpoint@4.0.10: + resolution: {integrity: sha512-rnUpZwCQCTtexbpM8S5aiJrfIx6NTvt0WwATiH4hCBN6gQNgkYPFoFt6g/3pAuyqU9D9tLKwXfsVqEWMBnwo6A==} peerDependencies: react: '>=18' react-dom: '>=18' @@ -2871,8 +2877,8 @@ packages: peerDependencies: react: '>=16.13' - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2892,8 +2898,8 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite@7.1.7: - resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + vite@7.2.7: + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2970,8 +2976,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -2986,8 +2992,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zustand@5.0.8: - resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -3011,23 +3017,23 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.4': {} + '@babel/compat-data@7.28.5': {} - '@babel/core@7.28.4': + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -3037,19 +3043,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.4 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.2 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -3057,17 +3063,17 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -3075,27 +3081,27 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/runtime@7.28.4': {} @@ -3103,59 +3109,59 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.28.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.2.4': + '@biomejs/biome@2.3.8': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.4 - '@biomejs/cli-darwin-x64': 2.2.4 - '@biomejs/cli-linux-arm64': 2.2.4 - '@biomejs/cli-linux-arm64-musl': 2.2.4 - '@biomejs/cli-linux-x64': 2.2.4 - '@biomejs/cli-linux-x64-musl': 2.2.4 - '@biomejs/cli-win32-arm64': 2.2.4 - '@biomejs/cli-win32-x64': 2.2.4 - - '@biomejs/cli-darwin-arm64@2.2.4': + '@biomejs/cli-darwin-arm64': 2.3.8 + '@biomejs/cli-darwin-x64': 2.3.8 + '@biomejs/cli-linux-arm64': 2.3.8 + '@biomejs/cli-linux-arm64-musl': 2.3.8 + '@biomejs/cli-linux-x64': 2.3.8 + '@biomejs/cli-linux-x64-musl': 2.3.8 + '@biomejs/cli-win32-arm64': 2.3.8 + '@biomejs/cli-win32-x64': 2.3.8 + + '@biomejs/cli-darwin-arm64@2.3.8': optional: true - '@biomejs/cli-darwin-x64@2.2.4': + '@biomejs/cli-darwin-x64@2.3.8': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.4': + '@biomejs/cli-linux-arm64-musl@2.3.8': optional: true - '@biomejs/cli-linux-arm64@2.2.4': + '@biomejs/cli-linux-arm64@2.3.8': optional: true - '@biomejs/cli-linux-x64-musl@2.2.4': + '@biomejs/cli-linux-x64-musl@2.3.8': optional: true - '@biomejs/cli-linux-x64@2.2.4': + '@biomejs/cli-linux-x64@2.3.8': optional: true - '@biomejs/cli-win32-arm64@2.2.4': + '@biomejs/cli-win32-arm64@2.3.8': optional: true - '@biomejs/cli-win32-x64@2.2.4': + '@biomejs/cli-win32-x64@2.3.8': optional: true '@emotion/babel-plugin@11.13.5': @@ -3190,19 +3196,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1)': + '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.3) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.7 transitivePeerDependencies: - supports-color @@ -3212,111 +3218,111 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.3 '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.4.0 - '@emotion/react': 11.14.0(@types/react@19.1.13)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.3) '@emotion/utils': 1.4.2 - react: 19.1.1 + react: 19.2.3 optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.7 transitivePeerDependencies: - supports-color '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.1)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 '@emotion/utils@1.4.2': {} '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.25.10': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.10': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.10': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.10': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.10': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.10': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.10': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.10': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.10': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.10': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.10': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.10': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.10': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.10': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.10': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.10': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.10': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.10': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.10': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.10': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.10': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.10': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.10': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.10': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.10': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.10': + '@esbuild/win32-x64@0.25.12': optional: true '@floating-ui/core@1.7.3': @@ -3328,49 +3334,49 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@floating-ui/dom': 1.7.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - '@floating-ui/react@0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react@0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@floating-ui/utils': 0.2.10 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - tabbable: 6.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tabbable: 6.3.0 '@floating-ui/utils@0.2.10': {} - '@gerrit0/mini-shiki@3.13.0': + '@gerrit0/mini-shiki@3.20.0': dependencies: - '@shikijs/engine-oniguruma': 3.13.0 - '@shikijs/langs': 3.13.0 - '@shikijs/themes': 3.13.0 - '@shikijs/types': 3.13.0 + '@shikijs/engine-oniguruma': 3.20.0 + '@shikijs/langs': 3.20.0 + '@shikijs/themes': 3.20.0 + '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 - '@hookform/devtools@4.4.0(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@hookform/devtools@4.4.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@emotion/react': 11.14.0(@types/react@19.1.13)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1) - '@types/lodash': 4.17.20 - little-state-machine: 4.8.1(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.3) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.3))(@types/react@19.2.7)(react@19.2.3) + '@types/lodash': 4.17.21 + little-state-machine: 4.8.1(react@19.2.3) lodash: 4.17.21 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-simple-animate: 3.5.3(react-dom@19.1.1(react@19.1.1)) - use-deep-compare-effect: 1.8.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-simple-animate: 3.5.3(react-dom@19.2.3(react@19.2.3)) + use-deep-compare-effect: 1.8.1(react@19.2.3) uuid: 8.3.2 transitivePeerDependencies: - '@types/react' - supports-color - '@hookform/resolvers@3.10.0(react-hook-form@7.63.0(react@19.1.1))': + '@hookform/resolvers@3.10.0(react-hook-form@7.68.0(react@19.2.3))': dependencies: - react-hook-form: 7.63.0(react@19.1.1) + react-hook-form: 7.68.0(react@19.2.3) '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -3452,116 +3458,118 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@react-hook/latest@1.0.3(react@19.1.1)': + '@react-hook/latest@1.0.3(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 - '@react-hook/passive-layout-effect@1.2.1(react@19.1.1)': + '@react-hook/passive-layout-effect@1.2.1(react@19.2.3)': dependencies: - react: 19.1.1 + react: 19.2.3 - '@react-hook/resize-observer@2.0.2(react@19.1.1)': + '@react-hook/resize-observer@2.0.2(react@19.2.3)': dependencies: - '@react-hook/latest': 1.0.3(react@19.1.1) - '@react-hook/passive-layout-effect': 1.2.1(react@19.1.1) - react: 19.1.1 + '@react-hook/latest': 1.0.3(react@19.2.3) + '@react-hook/passive-layout-effect': 1.2.1(react@19.2.3) + react: 19.2.3 - '@reduxjs/toolkit@2.9.0(react-redux@9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 - immer: 10.1.3 + immer: 11.0.1 redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 optionalDependencies: - react: 19.1.1 - react-redux: 9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1) + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + + '@remix-run/router@1.23.1': {} - '@remix-run/router@1.23.0': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rolldown/pluginutils@1.0.0-beta.35': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} - '@rollup/rollup-android-arm-eabi@4.52.2': + '@rollup/rollup-android-arm-eabi@4.53.3': optional: true - '@rollup/rollup-android-arm64@4.52.2': + '@rollup/rollup-android-arm64@4.53.3': optional: true - '@rollup/rollup-darwin-arm64@4.52.2': + '@rollup/rollup-darwin-arm64@4.53.3': optional: true - '@rollup/rollup-darwin-x64@4.52.2': + '@rollup/rollup-darwin-x64@4.53.3': optional: true - '@rollup/rollup-freebsd-arm64@4.52.2': + '@rollup/rollup-freebsd-arm64@4.53.3': optional: true - '@rollup/rollup-freebsd-x64@4.52.2': + '@rollup/rollup-freebsd-x64@4.53.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.2': + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.2': + '@rollup/rollup-linux-arm-musleabihf@4.53.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.2': + '@rollup/rollup-linux-arm64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.2': + '@rollup/rollup-linux-arm64-musl@4.53.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.2': + '@rollup/rollup-linux-loong64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.2': + '@rollup/rollup-linux-ppc64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.2': + '@rollup/rollup-linux-riscv64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.2': + '@rollup/rollup-linux-riscv64-musl@4.53.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.2': + '@rollup/rollup-linux-s390x-gnu@4.53.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.2': + '@rollup/rollup-linux-x64-gnu@4.53.3': optional: true - '@rollup/rollup-linux-x64-musl@4.52.2': + '@rollup/rollup-linux-x64-musl@4.53.3': optional: true - '@rollup/rollup-openharmony-arm64@4.52.2': + '@rollup/rollup-openharmony-arm64@4.53.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.2': + '@rollup/rollup-win32-arm64-msvc@4.53.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.2': + '@rollup/rollup-win32-ia32-msvc@4.53.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.2': + '@rollup/rollup-win32-x64-gnu@4.53.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.2': + '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true - '@shikijs/engine-oniguruma@3.13.0': + '@shikijs/engine-oniguruma@3.20.0': dependencies: - '@shikijs/types': 3.13.0 + '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.13.0': + '@shikijs/langs@3.20.0': dependencies: - '@shikijs/types': 3.13.0 + '@shikijs/types': 3.20.0 - '@shikijs/themes@3.13.0': + '@shikijs/themes@3.20.0': dependencies: - '@shikijs/types': 3.13.0 + '@shikijs/types': 3.20.0 - '@shikijs/types@3.13.0': + '@shikijs/types@3.20.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -3599,56 +3607,56 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.4)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.4)': + '@svgr/babel-preset@8.1.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.4) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.5) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) - '@svgr/cli@8.1.0(typescript@5.9.2)': + '@svgr/cli@8.1.0(typescript@5.9.3)': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.2) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2)) - '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2))(typescript@5.9.2) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) camelcase: 6.3.0 chalk: 4.1.2 commander: 9.5.0 @@ -3659,12 +3667,12 @@ snapshots: - supports-color - typescript - '@svgr/core@8.1.0(typescript@5.9.2)': + '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.9.2) + cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -3672,79 +3680,79 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: - '@babel/core': 7.28.4 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) - '@svgr/core': 8.1.0(typescript@5.9.2) + '@babel/core': 7.28.5 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) + '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))': + '@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.2) + '@svgr/core': 8.1.0(typescript@5.9.3) deepmerge: 4.3.1 prettier: 2.8.8 - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))(typescript@5.9.2)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.2) - cosmiconfig: 8.3.6(typescript@5.9.2) + '@svgr/core': 8.1.0(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: - typescript - '@swc/core-darwin-arm64@1.13.5': + '@swc/core-darwin-arm64@1.15.4': optional: true - '@swc/core-darwin-x64@1.13.5': + '@swc/core-darwin-x64@1.15.4': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.5': + '@swc/core-linux-arm-gnueabihf@1.15.4': optional: true - '@swc/core-linux-arm64-gnu@1.13.5': + '@swc/core-linux-arm64-gnu@1.15.4': optional: true - '@swc/core-linux-arm64-musl@1.13.5': + '@swc/core-linux-arm64-musl@1.15.4': optional: true - '@swc/core-linux-x64-gnu@1.13.5': + '@swc/core-linux-x64-gnu@1.15.4': optional: true - '@swc/core-linux-x64-musl@1.13.5': + '@swc/core-linux-x64-musl@1.15.4': optional: true - '@swc/core-win32-arm64-msvc@1.13.5': + '@swc/core-win32-arm64-msvc@1.15.4': optional: true - '@swc/core-win32-ia32-msvc@1.13.5': + '@swc/core-win32-ia32-msvc@1.15.4': optional: true - '@swc/core-win32-x64-msvc@1.13.5': + '@swc/core-win32-x64-msvc@1.15.4': optional: true - '@swc/core@1.13.5': + '@swc/core@1.15.4': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.5 - '@swc/core-darwin-x64': 1.13.5 - '@swc/core-linux-arm-gnueabihf': 1.13.5 - '@swc/core-linux-arm64-gnu': 1.13.5 - '@swc/core-linux-arm64-musl': 1.13.5 - '@swc/core-linux-x64-gnu': 1.13.5 - '@swc/core-linux-x64-musl': 1.13.5 - '@swc/core-win32-arm64-msvc': 1.13.5 - '@swc/core-win32-ia32-msvc': 1.13.5 - '@swc/core-win32-x64-msvc': 1.13.5 + '@swc/core-darwin-arm64': 1.15.4 + '@swc/core-darwin-x64': 1.15.4 + '@swc/core-linux-arm-gnueabihf': 1.15.4 + '@swc/core-linux-arm64-gnu': 1.15.4 + '@swc/core-linux-arm64-musl': 1.15.4 + '@swc/core-linux-x64-gnu': 1.15.4 + '@swc/core-linux-x64-musl': 1.15.4 + '@swc/core-win32-arm64-msvc': 1.15.4 + '@swc/core-win32-ia32-msvc': 1.15.4 + '@swc/core-win32-x64-msvc': 1.15.4 '@swc/counter@0.1.3': {} @@ -3752,144 +3760,144 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.90.2': {} + '@tanstack/query-core@5.90.12': {} - '@tanstack/query-devtools@5.90.1': {} + '@tanstack/query-devtools@5.91.1': {} - '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.2(react@19.1.1))(react@19.1.1)': + '@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-devtools': 5.90.1 - '@tanstack/react-query': 5.90.2(react@19.1.1) - react: 19.1.1 + '@tanstack/query-devtools': 5.91.1 + '@tanstack/react-query': 5.90.12(react@19.2.3) + react: 19.2.3 - '@tanstack/react-query@5.90.2(react@19.1.1)': + '@tanstack/react-query@5.90.12(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.2 - react: 19.1.1 + '@tanstack/query-core': 5.90.12 + react: 19.2.3 - '@tanstack/react-virtual@3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@tanstack/react-virtual@3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/virtual-core': 3.13.12 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + '@tanstack/virtual-core': 3.13.13 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - '@tanstack/virtual-core@3.13.12': {} + '@tanstack/virtual-core@3.13.13': {} - '@tauri-apps/api@2.8.0': {} + '@tauri-apps/api@2.9.1': {} - '@tauri-apps/cli-darwin-arm64@2.8.4': + '@tauri-apps/cli-darwin-arm64@2.9.6': optional: true - '@tauri-apps/cli-darwin-x64@2.8.4': + '@tauri-apps/cli-darwin-x64@2.9.6': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.8.4': + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.8.4': + '@tauri-apps/cli-linux-arm64-gnu@2.9.6': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.8.4': + '@tauri-apps/cli-linux-arm64-musl@2.9.6': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.8.4': + '@tauri-apps/cli-linux-riscv64-gnu@2.9.6': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.8.4': + '@tauri-apps/cli-linux-x64-gnu@2.9.6': optional: true - '@tauri-apps/cli-linux-x64-musl@2.8.4': + '@tauri-apps/cli-linux-x64-musl@2.9.6': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.8.4': + '@tauri-apps/cli-win32-arm64-msvc@2.9.6': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.8.4': + '@tauri-apps/cli-win32-ia32-msvc@2.9.6': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.8.4': + '@tauri-apps/cli-win32-x64-msvc@2.9.6': optional: true - '@tauri-apps/cli@2.8.4': + '@tauri-apps/cli@2.9.6': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.8.4 - '@tauri-apps/cli-darwin-x64': 2.8.4 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.8.4 - '@tauri-apps/cli-linux-arm64-gnu': 2.8.4 - '@tauri-apps/cli-linux-arm64-musl': 2.8.4 - '@tauri-apps/cli-linux-riscv64-gnu': 2.8.4 - '@tauri-apps/cli-linux-x64-gnu': 2.8.4 - '@tauri-apps/cli-linux-x64-musl': 2.8.4 - '@tauri-apps/cli-win32-arm64-msvc': 2.8.4 - '@tauri-apps/cli-win32-ia32-msvc': 2.8.4 - '@tauri-apps/cli-win32-x64-msvc': 2.8.4 + '@tauri-apps/cli-darwin-arm64': 2.9.6 + '@tauri-apps/cli-darwin-x64': 2.9.6 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.6 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.6 + '@tauri-apps/cli-linux-arm64-musl': 2.9.6 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-gnu': 2.9.6 + '@tauri-apps/cli-linux-x64-musl': 2.9.6 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.6 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.6 + '@tauri-apps/cli-win32-x64-msvc': 2.9.6 - '@tauri-apps/plugin-clipboard-manager@2.3.0': + '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-deep-link@2.4.3': + '@tauri-apps/plugin-deep-link@2.4.5': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-dialog@2.4.0': + '@tauri-apps/plugin-dialog@2.4.2': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-fs@2.4.2': + '@tauri-apps/plugin-fs@2.4.4': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-http@2.5.2': + '@tauri-apps/plugin-http@2.5.4': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-log@2.7.0': + '@tauri-apps/plugin-log@2.7.1': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-notification@2.3.1': + '@tauri-apps/plugin-notification@2.3.3': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-opener@2.5.0': + '@tauri-apps/plugin-opener@2.5.2': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-os@2.3.1': + '@tauri-apps/plugin-os@2.3.2': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-process@2.3.0': + '@tauri-apps/plugin-process@2.3.1': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 - '@tauri-apps/plugin-window-state@2.4.0': + '@tauri-apps/plugin-window-state@2.4.1': dependencies: - '@tauri-apps/api': 2.8.0 + '@tauri-apps/api': 2.9.1 '@trysound/sax@0.2.0': {} '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/byte-size@8.1.2': {} @@ -3935,9 +3943,9 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.20 + '@types/lodash': 4.17.21 - '@types/lodash@4.17.20': {} + '@types/lodash@4.17.21': {} '@types/mdast@4.0.4': dependencies: @@ -3945,19 +3953,19 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.5.2': + '@types/node@24.10.4': dependencies: - undici-types: 7.12.0 + undici-types: 7.16.0 '@types/parse-json@4.0.2': {} - '@types/react-dom@19.1.9(@types/react@19.1.13)': + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.7 - '@types/react@19.1.13': + '@types/react@19.2.7': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 '@types/unist@2.0.11': {} @@ -3969,28 +3977,28 @@ snapshots: '@use-gesture/core@10.3.1': {} - '@use-gesture/react@10.3.1(react@19.1.1)': + '@use-gesture/react@10.3.1(react@19.2.3)': dependencies: '@use-gesture/core': 10.3.1 - react: 19.1.1 + react: 19.2.3 - '@vitejs/plugin-react-swc@4.1.0(vite@7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.35 - '@swc/core': 1.13.5 - vite: 7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1) + '@rolldown/pluginutils': 1.0.0-beta.47 + '@swc/core': 1.15.4 + vite: 7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.0.3(vite@7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1))': + '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2))': dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) - '@rolldown/pluginutils': 1.0.0-beta.35 + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1) + react-refresh: 0.18.0 + vite: 7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -4016,19 +4024,18 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 async-function@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.23(postcss@8.5.6): dependencies: - browserslist: 4.26.2 - caniuse-lite: 1.0.30001743 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001760 + fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -4041,13 +4048,13 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 - resolve: 1.22.10 + resolve: 1.22.11 bail@2.0.2: {} balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.6: {} + baseline-browser-mapping@2.9.7: {} boolbase@1.0.0: {} @@ -4065,13 +4072,13 @@ snapshots: fill-range: 7.1.1 optional: true - browserslist@4.26.2: + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.8.6 - caniuse-lite: 1.0.30001743 - electron-to-chromium: 1.5.223 - node-releases: 2.0.21 - update-browserslist-db: 1.1.3(browserslist@4.26.2) + baseline-browser-mapping: 2.9.7 + caniuse-lite: 1.0.30001760 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.2(browserslist@4.28.1) byte-size@9.0.1: {} @@ -4096,7 +4103,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001743: {} + caniuse-lite@1.0.30001760: {} ccount@2.0.1: {} @@ -4167,14 +4174,14 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@5.9.2): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.2 + typescript: 5.9.3 cross-spawn@6.0.6: dependencies: @@ -4208,7 +4215,7 @@ snapshots: dependencies: css-tree: 2.2.1 - csstype@3.1.3: {} + csstype@3.2.3: {} d3-array@3.2.4: dependencies: @@ -4268,7 +4275,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.18: {} + dayjs@1.11.19: {} debug@4.4.3: dependencies: @@ -4336,7 +4343,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.223: {} + electron-to-chromium@1.5.267: {} emoji-regex@8.0.0: {} @@ -4348,7 +4355,7 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.24.0: + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -4426,36 +4433,36 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.39.10: {} + es-toolkit@1.43.0: {} - esbuild@0.25.10: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -4488,17 +4495,17 @@ snapshots: dependencies: is-callable: 1.2.7 - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} - framer-motion@12.23.21(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + framer-motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.23.21 + motion-dom: 12.23.23 motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.4.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) fs.realpath@1.0.0: {} @@ -4518,6 +4525,8 @@ snapshots: functions-have-names@1.2.3: {} + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -4609,7 +4618,7 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - style-to-js: 1.1.17 + style-to-js: 1.1.21 unist-util-position: 5.0.0 vfile-message: 4.0.3 transitivePeerDependencies: @@ -4625,20 +4634,20 @@ snapshots: hosted-git-info@2.8.9: {} - html-dom-parser@5.1.1: + html-dom-parser@5.1.2: dependencies: domhandler: 5.0.3 htmlparser2: 10.0.0 - html-react-parser@5.2.6(@types/react@19.1.13)(react@19.1.1): + html-react-parser@5.2.10(@types/react@19.2.7)(react@19.2.3): dependencies: domhandler: 5.0.3 - html-dom-parser: 5.1.1 - react: 19.1.1 + html-dom-parser: 5.1.2 + react: 19.2.3 react-property: 2.0.2 - style-to-js: 1.1.17 + style-to-js: 1.1.21 optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.7 html-url-attributes@3.0.1: {} @@ -4649,9 +4658,11 @@ snapshots: domutils: 3.2.2 entities: 6.0.1 - immer@10.1.3: {} + immer@10.2.0: {} + + immer@11.0.1: {} - immutable@5.1.3: {} + immutable@5.1.4: {} import-fresh@3.3.1: dependencies: @@ -4665,7 +4676,7 @@ snapshots: inherits@2.0.4: {} - inline-style-parser@0.2.4: {} + inline-style-parser@0.2.7: {} internal-slot@1.1.0: dependencies: @@ -4735,9 +4746,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.0: + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 + generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -4812,7 +4824,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -4830,9 +4842,9 @@ snapshots: dependencies: uc.micro: 2.1.0 - little-state-machine@4.8.1(react@19.1.1): + little-state-machine@4.8.1(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 load-json-file@4.0.0: dependencies: @@ -4931,9 +4943,9 @@ snapshots: mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 - unist-util-is: 6.0.0 + unist-util-is: 6.0.1 - mdast-util-to-hast@13.2.0: + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -4969,9 +4981,9 @@ snapshots: memorystream@0.3.1: {} - merge-refs@2.0.0(@types/react@19.1.13): + merge-refs@2.0.0(@types/react@19.2.7): optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.7 micromark-core-commonmark@2.0.3: dependencies: @@ -5128,20 +5140,20 @@ snapshots: dependencies: brace-expansion: 2.0.2 - motion-dom@12.23.21: + motion-dom@12.23.23: dependencies: motion-utils: 12.23.6 motion-utils@12.23.6: {} - motion@12.23.21(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.23.21(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + framer-motion: 12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.4.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) ms@2.1.3: {} @@ -5157,17 +5169,15 @@ snapshots: node-addon-api@7.1.1: optional: true - node-releases@2.0.21: {} + node-releases@2.0.27: {} normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.10 + resolve: 1.22.11 semver: 5.7.2 validate-npm-package-license: 3.0.4 - normalize-range@0.1.2: {} - npm-run-all@4.1.5: dependencies: ansi-styles: 3.2.1 @@ -5270,7 +5280,7 @@ snapshots: prettier@2.8.8: {} - prettier@3.6.2: {} + prettier@3.7.4: {} prop-types@15.8.1: dependencies: @@ -5286,47 +5296,45 @@ snapshots: radash@12.1.1: {} - react-auth-code-input@3.2.1(react@19.1.1): + react-auth-code-input@3.2.1(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 - react-click-away-listener@2.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-click-away-listener@2.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - react-dom@19.1.1(react@19.1.1): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.1.1 - scheduler: 0.26.0 + react: 19.2.3 + scheduler: 0.27.0 - react-hook-form@7.63.0(react@19.1.1): + react-hook-form@7.68.0(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 - react-hotkeys-hook@5.2.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-hotkeys-hook@5.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) react-is@16.13.1: {} - react-is@18.3.1: {} - - react-loading-skeleton@3.5.0(react@19.1.1): + react-loading-skeleton@3.5.0(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 - react-markdown@10.1.0(@types/react@19.1.13)(react@19.1.1): + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.3): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.1.13 + '@types/react': 19.2.7 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.0 - react: 19.1.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -5337,47 +5345,47 @@ snapshots: react-property@2.0.2: {} - react-qr-code@2.0.18(react@19.1.1): + react-qr-code@2.0.18(react@19.2.3): dependencies: prop-types: 15.8.1 qr.js: 0.0.0 - react: 19.1.1 + react: 19.2.3 - react-redux@9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1): + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 19.1.1 - use-sync-external-store: 1.5.0(react@19.1.1) + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.7 redux: 5.0.1 - react-refresh@0.17.0: {} + react-refresh@0.18.0: {} - react-router-dom@6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-router-dom@6.30.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@remix-run/router': 1.23.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-router: 6.30.1(react@19.1.1) + '@remix-run/router': 1.23.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-router: 6.30.2(react@19.2.3) - react-router@6.30.1(react@19.1.1): + react-router@6.30.2(react@19.2.3): dependencies: - '@remix-run/router': 1.23.0 - react: 19.1.1 + '@remix-run/router': 1.23.1 + react: 19.2.3 - react-simple-animate@3.5.3(react-dom@19.1.1(react@19.1.1)): + react-simple-animate@3.5.3(react-dom@19.2.3(react@19.2.3)): dependencies: - react-dom: 19.1.1(react@19.1.1) + react-dom: 19.2.3(react@19.2.3) react-use-websocket@4.13.0: {} - react-virtualized-auto-sizer@1.0.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-virtualized-auto-sizer@1.0.26(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - react@19.1.1: {} + react@19.2.3: {} read-pkg@3.0.0: dependencies: @@ -5387,21 +5395,21 @@ snapshots: readdirp@4.1.2: {} - recharts@3.2.1(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react-is@18.3.1)(react@19.1.1)(redux@5.0.1): + recharts@3.6.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.9.0(react-redux@9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1))(react@19.1.1) + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.39.10 + es-toolkit: 1.43.0 eventemitter3: 5.0.1 - immer: 10.1.3 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-is: 18.3.1 - react-redux: 9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1) + immer: 10.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 - use-sync-external-store: 1.5.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.2.3) victory-vendor: 37.3.6 transitivePeerDependencies: - '@types/react' @@ -5417,7 +5425,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -5451,7 +5459,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 + mdast-util-to-hast: 13.2.1 unified: 11.0.5 vfile: 6.0.3 @@ -5461,38 +5469,38 @@ snapshots: resolve-from@4.0.0: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.52.2: + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.2 - '@rollup/rollup-android-arm64': 4.52.2 - '@rollup/rollup-darwin-arm64': 4.52.2 - '@rollup/rollup-darwin-x64': 4.52.2 - '@rollup/rollup-freebsd-arm64': 4.52.2 - '@rollup/rollup-freebsd-x64': 4.52.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.2 - '@rollup/rollup-linux-arm-musleabihf': 4.52.2 - '@rollup/rollup-linux-arm64-gnu': 4.52.2 - '@rollup/rollup-linux-arm64-musl': 4.52.2 - '@rollup/rollup-linux-loong64-gnu': 4.52.2 - '@rollup/rollup-linux-ppc64-gnu': 4.52.2 - '@rollup/rollup-linux-riscv64-gnu': 4.52.2 - '@rollup/rollup-linux-riscv64-musl': 4.52.2 - '@rollup/rollup-linux-s390x-gnu': 4.52.2 - '@rollup/rollup-linux-x64-gnu': 4.52.2 - '@rollup/rollup-linux-x64-musl': 4.52.2 - '@rollup/rollup-openharmony-arm64': 4.52.2 - '@rollup/rollup-win32-arm64-msvc': 4.52.2 - '@rollup/rollup-win32-ia32-msvc': 4.52.2 - '@rollup/rollup-win32-x64-gnu': 4.52.2 - '@rollup/rollup-win32-x64-msvc': 4.52.2 + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 rxjs@7.8.2: @@ -5521,12 +5529,12 @@ snapshots: sass@1.92.1: dependencies: chokidar: 4.0.3 - immutable: 5.1.3 + immutable: 5.1.4 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.1 - scheduler@0.26.0: {} + scheduler@0.27.0: {} semver@5.7.2: {} @@ -5630,7 +5638,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 string.prototype.trim@1.2.10: @@ -5639,7 +5647,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -5667,13 +5675,13 @@ snapshots: strip-bom@3.0.0: {} - style-to-js@1.1.17: + style-to-js@1.1.21: dependencies: - style-to-object: 1.0.9 + style-to-object: 1.0.14 - style-to-object@1.0.9: + style-to-object@1.0.14: dependencies: - inline-style-parser: 0.2.4 + inline-style-parser: 0.2.7 stylis@4.2.0: {} @@ -5699,7 +5707,7 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - tabbable@6.2.0: {} + tabbable@6.3.0: {} tiny-invariant@1.3.3: {} @@ -5752,20 +5760,20 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedoc@0.28.13(typescript@5.9.2): + typedoc@0.28.15(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.13.0 + '@gerrit0/mini-shiki': 3.20.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - typescript: 5.9.2 - yaml: 2.8.1 + typescript: 5.9.3 + yaml: 2.8.2 - typesafe-i18n@5.26.2(typescript@5.9.2): + typesafe-i18n@5.26.2(typescript@5.9.3): dependencies: - typescript: 5.9.2 + typescript: 5.9.3 - typescript@5.9.2: {} + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -5776,7 +5784,7 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@7.12.0: {} + undici-types@7.16.0: {} unified@11.0.5: dependencies: @@ -5788,7 +5796,7 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-util-is@6.0.0: + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -5800,37 +5808,37 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@6.0.1: + unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 - unist-util-is: 6.0.0 + unist-util-is: 6.0.1 unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 - update-browserslist-db@1.1.3(browserslist@4.26.2): + update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: - browserslist: 4.26.2 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 - use-breakpoint@4.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + use-breakpoint@4.0.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) - use-deep-compare-effect@1.8.1(react@19.1.1): + use-deep-compare-effect@1.8.1(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 dequal: 2.0.3 - react: 19.1.1 + react: 19.2.3 - use-sync-external-store@1.5.0(react@19.1.1): + use-sync-external-store@1.6.0(react@19.2.3): dependencies: - react: 19.1.1 + react: 19.2.3 uuid@8.3.2: {} @@ -5866,19 +5874,19 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.1.7(@types/node@24.5.2)(sass@1.92.1)(yaml@2.8.1): + vite@7.2.7(@types/node@24.10.4)(sass@1.92.1)(yaml@2.8.2): dependencies: - esbuild: 0.25.10 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.2 + rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 24.10.4 fsevents: 2.3.3 sass: 1.92.1 - yaml: 2.8.1 + yaml: 2.8.2 which-boxed-primitive@1.1.1: dependencies: @@ -5896,7 +5904,7 @@ snapshots: is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 + is-generator-function: 1.1.2 is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 @@ -5939,7 +5947,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yargs-parser@21.1.1: {} @@ -5955,11 +5963,11 @@ snapshots: zod@3.25.76: {} - zustand@5.0.8(@types/react@19.1.13)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + zustand@5.0.9(@types/react@19.2.7)(immer@11.0.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): optionalDependencies: - '@types/react': 19.1.13 - immer: 10.1.3 - react: 19.1.1 - use-sync-external-store: 1.5.0(react@19.1.1) + '@types/react': 19.2.7 + immer: 11.0.1 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) zwitch@2.0.4: {} diff --git a/src-tauri/.sqlx/query-0b161da55e36df9d0a10a52474a8cd49d4659d2d6faafdd2de0c985679d0703d.json b/src-tauri/.sqlx/query-0b161da55e36df9d0a10a52474a8cd49d4659d2d6faafdd2de0c985679d0703d.json new file mode 100644 index 00000000..0f1dbfc0 --- /dev/null +++ b/src-tauri/.sqlx/query-0b161da55e36df9d0a10a52474a8cd49d4659d2d6faafdd2de0c985679d0703d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5, client_traffic_policy = $6, enterprise_enabled = $7, token = $8, openid_display_name = $9 WHERE id = $10;", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "0b161da55e36df9d0a10a52474a8cd49d4659d2d6faafdd2de0c985679d0703d" +} diff --git a/src-tauri/.sqlx/query-65b503d8af2eb227b555274ec98411966a0a410314d5c109df6477ce799b40d1.json b/src-tauri/.sqlx/query-2d9b3c0595f2d385336d0a86cfdae1a4c327622977707117bf88b6a43e9e8f96.json similarity index 81% rename from src-tauri/.sqlx/query-65b503d8af2eb227b555274ec98411966a0a410314d5c109df6477ce799b40d1.json rename to src-tauri/.sqlx/query-2d9b3c0595f2d385336d0a86cfdae1a4c327622977707117bf88b6a43e9e8f96.json index 57f2b8f7..13579fc7 100644 --- a/src-tauri/.sqlx/query-65b503d8af2eb227b555274ec98411966a0a410314d5c109df6477ce799b40d1.json +++ b/src-tauri/.sqlx/query-2d9b3c0595f2d385336d0a86cfdae1a4c327622977707117bf88b6a43e9e8f96.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", disable_all_traffic, enterprise_enabled, openid_display_name FROM instance ORDER BY name ASC;", + "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", client_traffic_policy, enterprise_enabled, openid_display_name FROM instance ORDER BY name ASC;", "describe": { "columns": [ { @@ -39,9 +39,9 @@ "type_info": "Text" }, { - "name": "disable_all_traffic", + "name": "client_traffic_policy", "ordinal": 7, - "type_info": "Bool" + "type_info": "Integer" }, { "name": "enterprise_enabled", @@ -70,5 +70,5 @@ true ] }, - "hash": "65b503d8af2eb227b555274ec98411966a0a410314d5c109df6477ce799b40d1" + "hash": "2d9b3c0595f2d385336d0a86cfdae1a4c327622977707117bf88b6a43e9e8f96" } diff --git a/src-tauri/.sqlx/query-f4b187b6f90edb7fd65e82a60e786942680cd7c66c42a315de0e62e47c4f2df4.json b/src-tauri/.sqlx/query-3a157b6bcdba07c456e924798f797cdbdd6290ef6fa0420ddc0682ab10e14727.json similarity index 80% rename from src-tauri/.sqlx/query-f4b187b6f90edb7fd65e82a60e786942680cd7c66c42a315de0e62e47c4f2df4.json rename to src-tauri/.sqlx/query-3a157b6bcdba07c456e924798f797cdbdd6290ef6fa0420ddc0682ab10e14727.json index 881650b4..79b463d4 100644 --- a/src-tauri/.sqlx/query-f4b187b6f90edb7fd65e82a60e786942680cd7c66c42a315de0e62e47c4f2df4.json +++ b/src-tauri/.sqlx/query-3a157b6bcdba07c456e924798f797cdbdd6290ef6fa0420ddc0682ab10e14727.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token, disable_all_traffic, enterprise_enabled, openid_display_name FROM instance\n WHERE token IS NOT NULL ORDER BY name ASC;", + "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token, client_traffic_policy, enterprise_enabled, openid_display_name FROM instance WHERE token IS NOT NULL ORDER BY name ASC;", "describe": { "columns": [ { @@ -39,9 +39,9 @@ "type_info": "Text" }, { - "name": "disable_all_traffic", + "name": "client_traffic_policy", "ordinal": 7, - "type_info": "Bool" + "type_info": "Integer" }, { "name": "enterprise_enabled", @@ -70,5 +70,5 @@ true ] }, - "hash": "f4b187b6f90edb7fd65e82a60e786942680cd7c66c42a315de0e62e47c4f2df4" + "hash": "3a157b6bcdba07c456e924798f797cdbdd6290ef6fa0420ddc0682ab10e14727" } diff --git a/src-tauri/.sqlx/query-758d3c67336eecafab5fd20e4b03574995383092cdb4d7c3ebda6f933ea0f472.json b/src-tauri/.sqlx/query-758d3c67336eecafab5fd20e4b03574995383092cdb4d7c3ebda6f933ea0f472.json deleted file mode 100644 index 40a4087a..00000000 --- a/src-tauri/.sqlx/query-758d3c67336eecafab5fd20e4b03574995383092cdb4d7c3ebda6f933ea0f472.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5, disable_all_traffic = $6, enterprise_enabled = $7, token = $8, openid_display_name = $9 WHERE id = $10;", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "758d3c67336eecafab5fd20e4b03574995383092cdb4d7c3ebda6f933ea0f472" -} diff --git a/src-tauri/.sqlx/query-e91278b90769f39e2cdf1677ffa1193580af693f9871a7162c47393daac8af11.json b/src-tauri/.sqlx/query-76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175.json similarity index 83% rename from src-tauri/.sqlx/query-e91278b90769f39e2cdf1677ffa1193580af693f9871a7162c47393daac8af11.json rename to src-tauri/.sqlx/query-76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175.json index eb35ee44..9c6f65ed 100644 --- a/src-tauri/.sqlx/query-e91278b90769f39e2cdf1677ffa1193580af693f9871a7162c47393daac8af11.json +++ b/src-tauri/.sqlx/query-76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM location WHERE id = $1", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE id = $1", "describe": { "columns": [ { @@ -62,6 +62,11 @@ "name": "location_mfa_mode: LocationMfaMode", "ordinal": 11, "type_info": "Integer" + }, + { + "name": "service_location_mode: ServiceLocationMode", + "ordinal": 12, + "type_info": "Integer" } ], "parameters": { @@ -79,8 +84,9 @@ false, false, false, + false, false ] }, - "hash": "e91278b90769f39e2cdf1677ffa1193580af693f9871a7162c47393daac8af11" + "hash": "76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175" } diff --git a/src-tauri/.sqlx/query-aa7d2c4c2100151b6f555ade668e72ceef8e7d1e39fcf0bcef0b7433457d3d2a.json b/src-tauri/.sqlx/query-7b9e30e8f67a024fb1ad94f76d16a22e221bdffc8ee37b777f26d02988f69bb1.json similarity index 82% rename from src-tauri/.sqlx/query-aa7d2c4c2100151b6f555ade668e72ceef8e7d1e39fcf0bcef0b7433457d3d2a.json rename to src-tauri/.sqlx/query-7b9e30e8f67a024fb1ad94f76d16a22e221bdffc8ee37b777f26d02988f69bb1.json index bfefdf36..3b65dac0 100644 --- a/src-tauri/.sqlx/query-aa7d2c4c2100151b6f555ade668e72ceef8e7d1e39fcf0bcef0b7433457d3d2a.json +++ b/src-tauri/.sqlx/query-7b9e30e8f67a024fb1ad94f76d16a22e221bdffc8ee37b777f26d02988f69bb1.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", disable_all_traffic, enterprise_enabled, openid_display_name FROM instance WHERE id = $1;", + "query": "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", client_traffic_policy, enterprise_enabled, openid_display_name FROM instance WHERE id = $1;", "describe": { "columns": [ { @@ -39,9 +39,9 @@ "type_info": "Text" }, { - "name": "disable_all_traffic", + "name": "client_traffic_policy", "ordinal": 7, - "type_info": "Bool" + "type_info": "Integer" }, { "name": "enterprise_enabled", @@ -70,5 +70,5 @@ true ] }, - "hash": "aa7d2c4c2100151b6f555ade668e72ceef8e7d1e39fcf0bcef0b7433457d3d2a" + "hash": "7b9e30e8f67a024fb1ad94f76d16a22e221bdffc8ee37b777f26d02988f69bb1" } diff --git a/src-tauri/.sqlx/query-7bbc28ee5a141e5b531a6ac5a1cbf120828a0b9c19301c92a3f71531c08c698d.json b/src-tauri/.sqlx/query-85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6.json similarity index 83% rename from src-tauri/.sqlx/query-7bbc28ee5a141e5b531a6ac5a1cbf120828a0b9c19301c92a3f71531c08c698d.json rename to src-tauri/.sqlx/query-85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6.json index f5faadd8..1615e9b4 100644 --- a/src-tauri/.sqlx/query-7bbc28ee5a141e5b531a6ac5a1cbf120828a0b9c19301c92a3f71531c08c698d.json +++ b/src-tauri/.sqlx/query-85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM location WHERE instance_id = $1 ORDER BY name ASC", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE pubkey = $1;", "describe": { "columns": [ { @@ -62,6 +62,11 @@ "name": "location_mfa_mode: LocationMfaMode", "ordinal": 11, "type_info": "Integer" + }, + { + "name": "service_location_mode: ServiceLocationMode", + "ordinal": 12, + "type_info": "Integer" } ], "parameters": { @@ -79,8 +84,9 @@ false, false, false, + false, false ] }, - "hash": "7bbc28ee5a141e5b531a6ac5a1cbf120828a0b9c19301c92a3f71531c08c698d" + "hash": "85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6" } diff --git a/src-tauri/.sqlx/query-ac02b04f6490a768571290d7dc77444eb0ca55a3a7e159c3b2e529ebf75f224f.json b/src-tauri/.sqlx/query-9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056.json similarity index 80% rename from src-tauri/.sqlx/query-ac02b04f6490a768571290d7dc77444eb0ca55a3a7e159c3b2e529ebf75f224f.json rename to src-tauri/.sqlx/query-9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056.json index 6df78777..012a54b3 100644 --- a/src-tauri/.sqlx/query-ac02b04f6490a768571290d7dc77444eb0ca55a3a7e159c3b2e529ebf75f224f.json +++ b/src-tauri/.sqlx/query-9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM location WHERE pubkey = $1;", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE instance_id = $1 AND service_location_mode <= $2 ORDER BY name ASC", "describe": { "columns": [ { @@ -62,10 +62,15 @@ "name": "location_mfa_mode: LocationMfaMode", "ordinal": 11, "type_info": "Integer" + }, + { + "name": "service_location_mode: ServiceLocationMode", + "ordinal": 12, + "type_info": "Integer" } ], "parameters": { - "Right": 1 + "Right": 2 }, "nullable": [ false, @@ -79,8 +84,9 @@ false, false, false, + false, false ] }, - "hash": "ac02b04f6490a768571290d7dc77444eb0ca55a3a7e159c3b2e529ebf75f224f" + "hash": "9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056" } diff --git a/src-tauri/.sqlx/query-3421da72f01d726c2931071203d663b197cb518dd65ec73108f85b2cb7270741.json b/src-tauri/.sqlx/query-b882379427740576d70c89eaeb815dede3c312162dcc73cea9c883289ba9fa8e.json similarity index 64% rename from src-tauri/.sqlx/query-3421da72f01d726c2931071203d663b197cb518dd65ec73108f85b2cb7270741.json rename to src-tauri/.sqlx/query-b882379427740576d70c89eaeb815dede3c312162dcc73cea9c883289ba9fa8e.json index a994e60f..5163c8ca 100644 --- a/src-tauri/.sqlx/query-3421da72f01d726c2931071203d663b197cb518dd65ec73108f85b2cb7270741.json +++ b/src-tauri/.sqlx/query-b882379427740576d70c89eaeb815dede3c312162dcc73cea9c883289ba9fa8e.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, keepalive_interval = $10, location_mfa_mode = $11 WHERE id = $12", + "query": "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12 WHERE id = $13", "describe": { "columns": [], "parameters": { - "Right": 12 + "Right": 13 }, "nullable": [] }, - "hash": "3421da72f01d726c2931071203d663b197cb518dd65ec73108f85b2cb7270741" + "hash": "b882379427740576d70c89eaeb815dede3c312162dcc73cea9c883289ba9fa8e" } diff --git a/src-tauri/.sqlx/query-95b89ca80a622d2c625c8d5523a8a59d947ae38b9ddf85a12e6c7bc9a6767f08.json b/src-tauri/.sqlx/query-c4a6b8e1c94eefc619c768ff4aac9aa248baf9553cea4f47ca9b1f6499bedaa3.json similarity index 61% rename from src-tauri/.sqlx/query-95b89ca80a622d2c625c8d5523a8a59d947ae38b9ddf85a12e6c7bc9a6767f08.json rename to src-tauri/.sqlx/query-c4a6b8e1c94eefc619c768ff4aac9aa248baf9553cea4f47ca9b1f6499bedaa3.json index 4b7ed9e3..ec67166e 100644 --- a/src-tauri/.sqlx/query-95b89ca80a622d2c625c8d5523a8a59d947ae38b9ddf85a12e6c7bc9a6767f08.json +++ b/src-tauri/.sqlx/query-c4a6b8e1c94eefc619c768ff4aac9aa248baf9553cea4f47ca9b1f6499bedaa3.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO instance (name, uuid, url, proxy_url, username, token, disable_all_traffic, enterprise_enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id;", + "query": "INSERT INTO instance (name, uuid, url, proxy_url, username, token, client_traffic_policy , enterprise_enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id;", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "95b89ca80a622d2c625c8d5523a8a59d947ae38b9ddf85a12e6c7bc9a6767f08" + "hash": "c4a6b8e1c94eefc619c768ff4aac9aa248baf9553cea4f47ca9b1f6499bedaa3" } diff --git a/src-tauri/.sqlx/query-f660459ee3beed1e88815560c3f16259e63975a3ec89a3c9b95d833774e9dfef.json b/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json similarity index 81% rename from src-tauri/.sqlx/query-f660459ee3beed1e88815560c3f16259e63975a3ec89a3c9b95d833774e9dfef.json rename to src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json index e895e552..e023a3a7 100644 --- a/src-tauri/.sqlx/query-f660459ee3beed1e88815560c3f16259e63975a3ec89a3c9b95d833774e9dfef.json +++ b/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM location ORDER BY name ASC;", + "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE service_location_mode <= $1 ORDER BY name ASC;", "describe": { "columns": [ { @@ -62,10 +62,15 @@ "name": "location_mfa_mode: LocationMfaMode", "ordinal": 11, "type_info": "Integer" + }, + { + "name": "service_location_mode: ServiceLocationMode", + "ordinal": 12, + "type_info": "Integer" } ], "parameters": { - "Right": 0 + "Right": 1 }, "nullable": [ false, @@ -79,8 +84,9 @@ false, false, false, + false, false ] }, - "hash": "f660459ee3beed1e88815560c3f16259e63975a3ec89a3c9b95d833774e9dfef" + "hash": "d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e" } diff --git a/src-tauri/.sqlx/query-e02047df7deea862cceca537e49ae16a8237e91eff0ee684cacd2ec1c77adb58.json b/src-tauri/.sqlx/query-ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f.json similarity index 59% rename from src-tauri/.sqlx/query-e02047df7deea862cceca537e49ae16a8237e91eff0ee684cacd2ec1c77adb58.json rename to src-tauri/.sqlx/query-ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f.json index 3d77f025..a05f49a0 100644 --- a/src-tauri/.sqlx/query-e02047df7deea862cceca537e49ae16a8237e91eff0ee684cacd2ec1c77adb58.json +++ b/src-tauri/.sqlx/query-ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id \"id!\"", + "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id \"id!\"", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 11 + "Right": 12 }, "nullable": [ true ] }, - "hash": "e02047df7deea862cceca537e49ae16a8237e91eff0ee684cacd2ec1c77adb58" + "hash": "ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d43f39a2..70184626 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1,15 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] +version = 4 [[package]] name = "adler2" @@ -17,6 +8,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" version = "0.7.8" @@ -30,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -86,9 +87,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -101,9 +102,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -116,22 +117,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -149,11 +150,11 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", "windows-sys 0.60.2", @@ -206,6 +207,48 @@ dependencies = [ "zbus", ] +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.111", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow 0.7.14", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -294,9 +337,9 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.2", + "rustix", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -336,7 +379,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix 1.1.2", + "rustix", ] [[package]] @@ -347,7 +390,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -362,10 +405,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.2", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -394,6 +437,28 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-task" version = "4.7.1" @@ -408,7 +473,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -457,9 +522,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", @@ -473,8 +538,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", "tower", "tower-layer", @@ -483,9 +547,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -494,27 +558,11 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.0", -] - [[package]] name = "base64" version = "0.21.7" @@ -529,9 +577,18 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] [[package]] name = "bitflags" @@ -541,11 +598,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -560,6 +617,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -580,11 +646,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -602,9 +668,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases", @@ -612,15 +678,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -652,11 +718,12 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-unit" -version = "5.1.6" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" dependencies = [ "rust_decimal", + "schemars 1.1.0", "serde", "utf8-width", ] @@ -685,9 +752,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -703,9 +770,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -716,7 +783,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cairo-sys-rs", "glib", "libc", @@ -737,9 +804,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -764,7 +831,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -774,14 +841,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.7", + "toml 0.9.8", ] [[package]] name = "cc" -version = "1.2.39" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -818,9 +885,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -828,6 +895,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.42" @@ -839,14 +930,25 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", ] [[package]] name = "clap" -version = "4.5.48" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -854,9 +956,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -866,21 +968,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clipboard-win" @@ -909,7 +1011,7 @@ dependencies = [ [[package]] name = "common" -version = "1.5.3" +version = "1.6.1" dependencies = [ "nix", ] @@ -1016,7 +1118,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -1029,7 +1131,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "libc", ] @@ -1045,9 +1147,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -1099,11 +1201,12 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1131,7 +1234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1141,7 +1244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1168,7 +1271,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1216,7 +1319,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1230,7 +1333,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1241,7 +1344,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1252,7 +1355,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1263,19 +1366,27 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "defguard-client" -version = "1.5.3" +version = "1.6.1" dependencies = [ "anyhow", + "async-stream", "base64 0.22.1", + "block2 0.6.2", "chrono", "clap", "common", "dark-light", "defguard_wireguard_rs", "dirs-next", + "futures-core", "hyper-util", + "known-folders", "log", "nix", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "objc2-network-extension", + "os_info", "prost", "regex", "reqwest", @@ -1287,6 +1398,7 @@ dependencies = [ "sqlx", "struct-patch", "strum", + "swift-rs", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", @@ -1301,7 +1413,7 @@ dependencies = [ "tauri-plugin-process", "tauri-plugin-single-instance", "tauri-plugin-window-state", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -1315,14 +1427,16 @@ dependencies = [ "tracing-subscriber", "vergen-git2", "webbrowser", - "winapi", + "windows 0.62.2", + "windows-acl", "windows-service", + "windows-sys 0.61.2", "x25519-dalek", ] [[package]] name = "defguard-dg" -version = "1.5.3" +version = "1.6.1" dependencies = [ "clap", "common", @@ -1332,7 +1446,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tonic", "tonic-prost", @@ -1341,12 +1455,41 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "defguard_boringtun" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491589622056dd3c47b7aa3bc4727633ebf5d38462486209b7f04bef6c50075f" +dependencies = [ + "aead", + "base64 0.22.1", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "libc", + "nix", + "parking_lot", + "ring", + "socket2", + "thiserror 2.0.17", + "tracing", + "uniffi", + "untrusted", + "x25519-dalek", +] + [[package]] name = "defguard_wireguard_rs" -version = "0.7.7" -source = "git+https://github.com/DefGuard/wireguard-rs?rev=2d3d3af2c9239ec047d2a51acbc27d62d4ab950c#2d3d3af2c9239ec047d2a51acbc27d62d4ab950c" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf853e6ffe9d6ab91aa1f706cbbf84a446c002cdf469ae0eb085fbcca07648e4" dependencies = [ "base64 0.22.1", + "defguard_boringtun", + "ipnet", "libc", "log", "netlink-packet-core", @@ -1358,7 +1501,9 @@ dependencies = [ "nix", "regex", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", + "windows 0.62.2", + "wireguard-nt", "x25519-dalek", ] @@ -1375,9 +1520,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", "serde_core", @@ -1401,7 +1546,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1411,7 +1556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1424,7 +1569,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1467,7 +1612,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1493,10 +1638,10 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.10.0", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1507,7 +1652,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1521,9 +1666,9 @@ dependencies = [ [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -1533,13 +1678,13 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1553,9 +1698,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -1626,7 +1771,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.7", + "toml 0.9.8", "vswhom", "winreg 0.55.0", ] @@ -1648,9 +1793,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -1670,14 +1815,14 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -1691,9 +1836,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -1707,7 +1852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1777,7 +1922,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1816,15 +1961,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" - -[[package]] -name = "fixedbitset" -version = "0.4.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" @@ -1834,9 +1973,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -1892,7 +2031,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1916,6 +2055,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "funty" version = "2.0.0" @@ -1997,7 +2145,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2149,12 +2297,12 @@ dependencies = [ [[package]] name = "gethostname" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.2", - "windows-targets 0.52.6", + "rustix", + "windows-link 0.2.1", ] [[package]] @@ -2183,24 +2331,18 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gio" version = "0.18.4" @@ -2235,11 +2377,11 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", @@ -2252,7 +2394,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "futures-channel", "futures-core", "futures-executor", @@ -2280,7 +2422,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2322,6 +2464,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "gtk" version = "0.18.2" @@ -2371,7 +2524,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2386,7 +2539,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -2395,12 +2548,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -2429,6 +2583,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -2482,11 +2642,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2503,12 +2663,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2549,9 +2708,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2618,9 +2777,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -2639,7 +2798,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.6.1", ] [[package]] @@ -2654,7 +2813,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.1", + "windows-core 0.62.2", ] [[package]] @@ -2678,9 +2837,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2691,9 +2850,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -2704,11 +2863,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2719,42 +2877,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2791,9 +2945,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2816,12 +2970,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2836,16 +2990,36 @@ dependencies = [ ] [[package]] -name = "io-uring" -version = "0.7.10" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", + "generic-array", +] + +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", ] +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + [[package]] name = "ipnet" version = "2.11.0" @@ -2854,9 +3028,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2883,9 +3057,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2953,15 +3127,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2995,11 +3169,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "serde", "unicode-segmentation", ] +[[package]] +name = "known-folders" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d463f34ca3c400fde3a054da0e0b8c6ffa21e4590922f3e18281bb5eeef4cbdc" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -3008,7 +3191,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.11.4", + "indexmap 2.12.1", "selectors", ] @@ -3056,15 +3239,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.176" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -3089,7 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -3104,7 +3287,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall", ] @@ -3122,9 +3305,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -3134,45 +3317,38 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ - "serde", + "serde_core", "value-bag", ] @@ -3190,13 +3366,13 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.6" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" dependencies = [ "cc", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "time", ] @@ -3222,7 +3398,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3295,20 +3471,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "moxcms" -version = "0.7.5" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -3324,14 +3500,14 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "png 0.17.16", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows-sys 0.60.2", ] @@ -3364,7 +3540,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -3412,7 +3588,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "log", "netlink-packet-core", @@ -3426,7 +3602,7 @@ checksum = "3176f18d11a1ae46053e59ec89d46ba318ae1343615bd3f8c908bfc84edae35c" dependencies = [ "byteorder", "pastey", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3464,7 +3640,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -3487,6 +3663,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "notify-rust" version = "4.11.7" @@ -3503,20 +3688,19 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -3565,9 +3749,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -3575,14 +3759,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3612,9 +3796,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -3622,77 +3806,114 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.10.0", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", "objc2-core-graphics", "objc2-core-image", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core", ] [[package]] name = "objc2-cloud-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-data" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-io-surface", ] [[package]] name = "objc2-core-image" -version = "0.3.1" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", ] [[package]] @@ -3716,7 +3937,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3724,122 +3945,122 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.10.0", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-io-surface" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.10.0", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-javascript-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-network-extension" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "9126ecdaea052ce2a6b7a894303d0806368b92c77165052da1c3eb52d9e5f9b1" dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "objc2-security", ] [[package]] name = "objc2-quartz-core" -version = "0.2.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] -name = "objc2-quartz-core" -version = "0.3.1" +name = "objc2-security" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", ] [[package]] -name = "objc2-security" -version = "0.3.1" +name = "objc2-ui-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation 0.3.2", + "objc2-quartz-core", + "objc2-user-notifications", ] [[package]] -name = "objc2-ui-kit" -version = "0.3.1" +name = "objc2-user-notifications" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-web-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-javascript-core", "objc2-security", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -3848,15 +4069,21 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.2" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "dunce", "is-wsl", @@ -3866,11 +4093,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3887,7 +4114,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3898,18 +4125,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.2+3.5.2" +version = "300.5.4+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3946,24 +4173,28 @@ dependencies = [ [[package]] name = "os_info" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ + "android_system_properties", "log", - "plist", + "nix", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "objc2-ui-kit", "serde", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "os_pipe" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3999,9 +4230,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -4009,15 +4240,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4055,22 +4286,23 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.11.4", + "fixedbitset", + "indexmap 2.12.1", ] [[package]] name = "petgraph" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.5.7", - "indexmap 2.11.4", + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.12.1", ] [[package]] @@ -4177,7 +4409,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4224,7 +4456,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4277,6 +4509,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -4284,8 +4522,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.11.4", - "quick-xml 0.38.3", + "indexmap 2.12.1", + "quick-xml 0.38.4", "serde", "time", ] @@ -4309,7 +4547,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -4326,15 +4564,26 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.2", - "windows-sys 0.61.1", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", ] [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -4367,7 +4616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4396,7 +4645,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.23.9", ] [[package]] @@ -4431,9 +4680,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -4466,7 +4715,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.106", + "syn 2.0.111", "tempfile", ] @@ -4480,7 +4729,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4534,25 +4783,25 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "memchr", "unicase", ] [[package]] name = "pulldown-cmark-to-cmark" -version = "21.0.0" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" dependencies = [ "pulldown-cmark", ] [[package]] name = "pxfm" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -4574,9 +4823,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] @@ -4595,7 +4844,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4608,7 +4857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -4616,7 +4865,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -4638,9 +4887,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -4746,7 +4995,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -4775,11 +5024,11 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -4801,27 +5050,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4849,9 +5098,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rend" @@ -4864,9 +5113,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64 0.22.1", "bytes", @@ -4918,17 +5167,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd 0.11.0", - "block2 0.6.1", + "block2 0.6.2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -4981,9 +5230,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -5001,20 +5250,19 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" dependencies = [ "cfg-if", "ordered-multimap", - "trim-in-place", ] [[package]] name = "rust_decimal" -version = "1.38.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ "arrayvec", "borsh", @@ -5026,12 +5274,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5047,37 +5289,24 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.1", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -5090,21 +5319,21 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.0", + "security-framework 3.5.1", ] [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -5112,9 +5341,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -5148,7 +5377,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -5180,9 +5409,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -5199,7 +5428,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5214,6 +5443,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "seahash" version = "4.1.0" @@ -5226,7 +5475,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5235,11 +5484,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5286,9 +5535,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -5308,22 +5557,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5334,7 +5583,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5358,7 +5607,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5372,9 +5621,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -5393,19 +5642,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.1.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -5413,14 +5661,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5442,7 +5690,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5494,9 +5742,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -5513,9 +5761,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simdutf8" @@ -5550,36 +5798,42 @@ dependencies = [ "serde", ] +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types 0.5.0", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core", "raw-window-handle", "redox_syscall", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5659,7 +5913,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.4", + "indexmap 2.12.1", "log", "memchr", "once_cell", @@ -5668,7 +5922,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -5686,7 +5940,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5709,7 +5963,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.106", + "syn 2.0.111", "tokio", "url", ] @@ -5722,7 +5976,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -5752,7 +6006,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5766,7 +6020,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -5791,7 +6045,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5817,7 +6071,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", "uuid", @@ -5825,9 +6079,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -5894,7 +6148,7 @@ checksum = "68c6387c1c7b53060605101b63d93edca618c6cf7ce61839f2ec2a527419fdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5915,7 +6169,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5948,9 +6202,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -5974,7 +6228,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5992,7 +6246,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6022,12 +6276,12 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.3" +version = "0.34.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.10.0", + "block2 0.6.2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", @@ -6044,9 +6298,9 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "parking_lot", "raw-window-handle", @@ -6054,7 +6308,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -6068,7 +6322,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6085,9 +6339,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.8.5" +version = "2.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" dependencies = [ "anyhow", "bytes", @@ -6095,7 +6349,7 @@ dependencies = [ "dirs", "dunce", "embed_plist", - "getrandom 0.3.3", + "getrandom 0.3.4", "glob", "gtk", "heck 0.5.0", @@ -6106,9 +6360,9 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -6125,22 +6379,21 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", @@ -6154,15 +6407,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.7", + "toml 0.9.8", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" dependencies = [ "base64 0.22.1", "brotli", @@ -6176,9 +6429,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.106", + "syn 2.0.111", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "url", "uuid", @@ -6187,23 +6440,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.4.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" dependencies = [ "anyhow", "glob", @@ -6212,15 +6465,15 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.7", + "toml 0.9.8", "walkdir", ] [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.3.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" dependencies = [ "arboard", "log", @@ -6228,14 +6481,14 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "tauri-plugin-deep-link" -version = "2.4.3" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd67112fb1131834c2a7398ffcba520dbbf62c17de3b10329acd1a3554b1a9bb" +checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73" dependencies = [ "dunce", "plist", @@ -6245,18 +6498,18 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", - "windows-registry", + "windows-registry 0.5.3", "windows-result 0.3.4", ] [[package]] name = "tauri-plugin-dialog" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" dependencies = [ "log", "raw-window-handle", @@ -6266,15 +6519,15 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.4.2" +version = "2.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" dependencies = [ "anyhow", "dunce", @@ -6287,16 +6540,16 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.16", - "toml 0.9.7", + "thiserror 2.0.17", + "toml 0.9.8", "url", ] [[package]] name = "tauri-plugin-http" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938a3d7051c9a82b431e3a0f3468f85715b3442b3c3a3913095e9fa509e2652c" +checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70" dependencies = [ "bytes", "cookie_store", @@ -6310,7 +6563,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "url", "urlpattern", @@ -6318,31 +6571,31 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c1438bc7662acd16d508c919b3c087efd63669a4c75625dff829b1c75975ec" +checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6" dependencies = [ "android_logger", "byte-unit", "fern", "log", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "serde", "serde_json", "serde_repr", "swift-rs", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] [[package]] name = "tauri-plugin-notification" -version = "2.3.1" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fbc86b929b5376ab84b25c060f966d146b2fbd59b6af8264027b343c82c219" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" dependencies = [ "log", "notify-rust", @@ -6352,38 +6605,38 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "url", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" dependencies = [ "dunce", "glob", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "open", "schemars 0.8.22", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", - "windows", + "windows 0.61.3", "zbus", ] [[package]] name = "tauri-plugin-os" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" dependencies = [ "gethostname", "log", @@ -6394,14 +6647,14 @@ dependencies = [ "sys-locale", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "tauri-plugin-process" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" dependencies = [ "tauri", "tauri-plugin", @@ -6409,15 +6662,15 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.3.4" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb9cac815bf11c4a80fb498666bcdad66d65b89e3ae24669e47806febb76389c" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" dependencies = [ "serde", "serde_json", "tauri", "tauri-plugin-deep-link", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "windows-sys 0.60.2", "zbus", @@ -6425,57 +6678,57 @@ dependencies = [ [[package]] name = "tauri-plugin-window-state" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "log", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -6486,15 +6739,15 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" dependencies = [ "anyhow", "brotli", @@ -6520,8 +6773,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.16", - "toml 0.9.7", + "thiserror 2.0.17", + "toml 0.9.8", "url", "urlpattern", "uuid", @@ -6530,12 +6783,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ + "dunce", "embed-resource", - "toml 0.9.7", + "toml 0.9.8", ] [[package]] @@ -6545,8 +6799,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.16", - "windows", + "thiserror 2.0.17", + "windows 0.61.3", "windows-version", ] @@ -6557,10 +6811,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.1", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -6574,6 +6828,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6585,11 +6848,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -6600,18 +6863,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6681,9 +6944,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -6706,34 +6969,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6748,9 +7008,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -6769,9 +7029,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -6794,17 +7054,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -6818,9 +7078,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -6831,7 +7091,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -6842,7 +7102,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -6851,30 +7111,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", + "indexmap 2.12.1", + "toml_datetime 0.7.3", "toml_parser", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tonic" @@ -6917,7 +7177,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6942,7 +7202,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.106", + "syn 2.0.111", "tempfile", "tonic-build", ] @@ -6955,7 +7215,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.11.4", + "indexmap 2.12.1", "pin-project-lite", "slab", "sync_wrapper", @@ -6968,11 +7228,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -6998,9 +7258,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -7010,32 +7270,32 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -7064,9 +7324,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -7085,44 +7345,37 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "png 0.17.16", "serde", - "thiserror 2.0.16", - "windows-sys 0.59.0", + "thiserror 2.0.17", + "windows-sys 0.60.2", ] [[package]] name = "tree_magic_mini" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ "memchr", - "nom", - "once_cell", - "petgraph 0.6.5", + "nom 8.0.0", + "petgraph 0.8.3", ] -[[package]] -name = "trim-in-place" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" - [[package]] name = "try-lock" version = "0.2.5" @@ -7137,9 +7390,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" @@ -7207,24 +7460,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -7232,6 +7485,149 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "uniffi" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c866f627c3f04c3df068b68bb2d725492caaa539dd313e2a9d26bb85b1a32f4e" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8ca600167641ebe7c8ba9254af40492dda3397c528cc3b2f511bd23e8541a5" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "indexmap 2.12.1", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.9.8", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e55c05228f4858bb258f651d21d743fcc1fe5a2ec20d3c0f9daefddb105ee4d" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_core" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7a5a038ebffe8f4cf91416b154ef3c2468b18e828b7009e01b1b99938089f9" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c2a6f93e7b73726e2015696ece25ca0ac5a5f1cf8d6a7ab5214dd0a01d2edf" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "uniffi_macros" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c6309fc36c7992afc03bc0c5b059c656bccbef3f2a4bc362980017f8936141" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.111", + "toml 0.9.8", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a138823392dba19b0aa494872689f97d0ee157de5852e2bec157ce6de9cdc22" +dependencies = [ + "anyhow", + "siphasher 0.3.11", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c27c4b515d25f8e53cc918e238c39a79c3144a40eaf2e51c4a7958973422c29" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.12.1", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0adacdd848aeed7af4f5af7d2f621d5e82531325d405e29463482becfdeafca" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -7270,9 +7666,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" [[package]] name = "utf8_iter" @@ -7288,13 +7684,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -7306,9 +7702,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" [[package]] name = "vcpkg" @@ -7357,9 +7753,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -7418,15 +7814,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -7444,9 +7831,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -7455,25 +7842,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -7484,9 +7857,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7494,22 +7867,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -7535,7 +7908,7 @@ checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.2", + "rustix", "scoped-tls", "smallvec", "wayland-sys", @@ -7547,8 +7920,8 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", - "rustix 1.1.2", + "bitflags 2.10.0", + "rustix", "wayland-backend", "wayland-scanner", ] @@ -7559,7 +7932,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7571,7 +7944,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7602,9 +7975,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -7622,16 +7995,16 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", "jni", "log", "ndk-context", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "url", "web-sys", ] @@ -7682,9 +8055,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -7697,7 +8070,7 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -7711,7 +8084,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7720,16 +8093,25 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror 2.0.16", - "windows", + "thiserror 2.0.17", + "windows 0.61.3", "windows-core 0.61.2", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "whoami" @@ -7743,9 +8125,15 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -7769,7 +8157,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -7784,10 +8172,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -7799,11 +8187,35 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-acl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177b1723986bcb4c606058e77f6e8614b51c7f9ad2face6f6fd63dd5c8b3cec3" +dependencies = [ + "field-offset", + "libc", + "widestring 0.4.3", + "winapi", ] [[package]] @@ -7815,6 +8227,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -7830,15 +8251,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -7849,29 +8270,40 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7882,9 +8314,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -7896,6 +8328,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -7907,6 +8349,17 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -7918,11 +8371,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -7931,8 +8384,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" dependencies = [ - "bitflags 2.9.4", - "widestring", + "bitflags 2.10.0", + "widestring 1.2.1", "windows-sys 0.52.0", ] @@ -7947,11 +8400,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -7996,16 +8449,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -8056,19 +8509,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.0", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -8080,13 +8533,22 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -8109,9 +8571,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -8133,9 +8595,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -8157,9 +8619,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -8169,9 +8631,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -8193,9 +8655,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -8217,9 +8679,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -8241,9 +8703,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -8265,9 +8727,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -8280,9 +8742,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -8307,6 +8769,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wireguard-nt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b4dbcc6c93786cf22e420ef96e8976bfb92a455070282302b74de5848191f4" +dependencies = [ + "bitflags 2.10.0", + "getrandom 0.2.16", + "ipnet", + "libloading 0.8.9", + "log", + "thiserror 1.0.69", + "widestring 0.4.3", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -8315,16 +8793,15 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wl-clipboard-rs" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" dependencies = [ "libc", "log", "os_pipe", - "rustix 0.38.44", - "tempfile", - "thiserror 2.0.16", + "rustix", + "thiserror 2.0.17", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -8334,18 +8811,18 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" dependencies = [ "base64 0.22.1", - "block2 0.6.1", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs", @@ -8360,10 +8837,10 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -8372,12 +8849,12 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -8420,7 +8897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 1.1.2", + "rustix", "x11rb-protocol", ] @@ -8444,11 +8921,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -8456,21 +8932,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] [[package]] name = "zbus" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", "async-executor", @@ -8493,8 +8969,9 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "windows-sys 0.60.2", - "winnow 0.7.13", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", "zbus_macros", "zbus_names", "zvariant", @@ -8502,14 +8979,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "zbus_names", "zvariant", "zvariant_utils", @@ -8523,28 +9000,28 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.13", + "winnow 0.7.14", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8564,15 +9041,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -8585,14 +9062,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -8601,9 +9078,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -8612,13 +9089,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8638,29 +9115,29 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.7.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 0.7.13", + "winnow 0.7.14", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "zvariant_utils", ] @@ -8673,6 +9150,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", - "winnow 0.7.13", + "syn 2.0.111", + "winnow 0.7.14", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4c3e19ca..0c161af9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ default-members = [".", "cli"] [workspace.dependencies] clap = { version = "4.5", features = ["cargo", "derive", "env"] } -defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs", rev = "2d3d3af2c9239ec047d2a51acbc27d62d4ab950c" } +defguard_wireguard_rs = "0.8.0" dirs-next = "2.0" prost = "0.14" reqwest = { version = "0.12", features = ["cookies", "json"] } @@ -30,14 +30,15 @@ authors = ["Defguard"] edition = "2021" homepage = "https://github.com/DefGuard/client" license-file = "../LICENSE.md" -rust-version = "1.80" -version = "1.5.3" +rust-version = "1.85" +version = "1.6.1" [package] name = "defguard-client" description = "Defguard desktop client" repository = "https://github.com/DefGuard/client" default-run = "defguard-client" +autobins = false authors.workspace = true edition.workspace = true homepage.workspace = true @@ -45,6 +46,13 @@ license-file.workspace = true rust-version.workspace = true version.workspace = true +[[bin]] +name = "defguard-client" + +[[bin]] +name = "defguard-service" +required-features = ["service"] + [build-dependencies] tauri-build = { version = "2", features = [] } tonic-prost-build.workspace = true @@ -59,12 +67,12 @@ common = { path = "common" } dark-light = "2.0" defguard_wireguard_rs = { workspace = true, features = ["check_dependencies"] } dirs-next.workspace = true +hyper-util = "0.1" log = { version = "0.4", features = ["serde"] } prost.workspace = true regex = "1.12" reqwest.workspace = true -# 0.21.2 causes config parsing errors -rust-ini = "=0.21.1" +rust-ini = "0.21" semver = "1.0" serde.workspace = true serde_json.workspace = true @@ -91,8 +99,9 @@ tauri-plugin-fs = "2" tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } tauri-plugin-log = "2" tauri-plugin-notification = "2" -tauri-plugin-opener = "2.5.0" -tauri-plugin-os = "2.3.1" +tauri-plugin-opener = "2" +tauri-plugin-os = "2" +tauri-plugin-process = "2" tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } tauri-plugin-window-state = "2" thiserror.workspace = true @@ -101,6 +110,7 @@ tokio.workspace = true tokio-util = "0.7" tonic.workspace = true tonic-prost.workspace = true +tower = "0.5" tracing.workspace = true tracing-appender = "0.2" tracing-subscriber.workspace = true @@ -110,23 +120,54 @@ x25519-dalek = { version = "2", features = [ "serde", "static_secrets", ] } -tauri-plugin-process = "2.3.0" +os_info = "3.12" + +[target.'cfg(target_os = "macos")'.dependencies] +block2 = "0.6" +objc2 = "0.6" +objc2-foundation = "0.3" +objc2-network-extension = "0.3" + +[target.'cfg(target_os = "macos")'.build-dependencies] +swift-rs = { version = "1.0", features = ["build"] } [target.'cfg(unix)'.dependencies] -hyper-util = "0.1" nix = { version = "0.30.1", features = ["user", "fs"] } tokio-stream = "0.1" -tower = "0.5" [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["winsvc", "winerror"] } +async-stream = "0.3" +futures-core = "0.3" +known-folders = "1.3" +windows = { version = "0.62", features = [ + "Win32", + "Win32_System", + "Win32_System_RemoteDesktop", +] } +windows-acl = "0.3" windows-service = "0.7" +windows-sys = { version = "0.61", features = [ + # Core Win32 types + "Win32_Foundation", + + # Security descriptors, ACLs, SDDL + "Win32_Security", + "Win32_Security_Authorization", + + # Named pipes (CreateNamedPipeW) + "Win32_System_Pipes", + + # HANDLE & file functions + "Win32_System_IO", + "Win32_System_Threading", +] } [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # DO NOT REMOVE!! custom-protocol = ["tauri/custom-protocol"] +service = [] [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/Client.entitlements b/src-tauri/Client.entitlements new file mode 100644 index 00000000..0fcd76d3 --- /dev/null +++ b/src-tauri/Client.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.application-identifier + 82GZ7KN29J.net.defguard + com.apple.developer.team-identifier + 82GZ7KN29J + com.apple.security.application-groups + + group.net.defguard + + + diff --git a/src-tauri/Defguard_Client_Mac_App_Store.provisionprofile b/src-tauri/Defguard_Client_Mac_App_Store.provisionprofile new file mode 100644 index 00000000..7eaff6cd Binary files /dev/null and b/src-tauri/Defguard_Client_Mac_App_Store.provisionprofile differ diff --git a/src-tauri/Defguard_VPNExtension_Mac_App_Store.provisionprofile b/src-tauri/Defguard_VPNExtension_Mac_App_Store.provisionprofile new file mode 100644 index 00000000..c762e521 Binary files /dev/null and b/src-tauri/Defguard_VPNExtension_Mac_App_Store.provisionprofile differ diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 00000000..cceceddc --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,8 @@ + + + + + ITSAppUsesNonExemptEncryption + + + diff --git a/src-tauri/cli/src/bin/dg.rs b/src-tauri/cli/src/bin/dg.rs index 3f94f339..d1ea1d80 100644 --- a/src-tauri/cli/src/bin/dg.rs +++ b/src-tauri/cli/src/bin/dg.rs @@ -156,18 +156,16 @@ async fn connect(config: CliConfig, ifname: String, trigger: Arc) -> Res debug!("Connecting to network {network_name}."); #[cfg(not(target_os = "macos"))] - let wgapi = WGApi::::new(ifname.to_string()).expect("Failed to setup WireGuard API"); + let mut wgapi = + WGApi::::new(ifname.to_string()).expect("Failed to setup WireGuard API"); #[cfg(target_os = "macos")] - let wgapi = WGApi::::new(ifname.to_string()).expect("Failed to setup WireGuard API"); + let mut wgapi = WGApi::::new(ifname.clone()).expect("Failed to setup WireGuard API"); - #[cfg(not(windows))] - { - // Create new interface. - debug!("Creating new interface {ifname}"); - wgapi - .create_interface() - .expect("Failed to create WireGuard interface"); - } + // Create new interface. + debug!("Creating new interface {ifname}"); + wgapi + .create_interface() + .expect("Failed to create WireGuard interface"); debug!("Preparing DNS configuration for interface {ifname}"); let dns_string = config.device_config.dns.clone().unwrap_or_default(); @@ -235,14 +233,11 @@ async fn connect(config: CliConfig, ifname: String, trigger: Arc) -> Res name: config.instance_info.name.clone(), prvkey: config.private_key.to_string(), addresses, - port: u32::from(find_free_tcp_port().ok_or(CliError::FreeTCPPort)?), + port: find_free_tcp_port().ok_or(CliError::FreeTCPPort)?, peers: vec![peer.clone()], mtu: None, }; - #[cfg(not(windows))] let configure_interface_result = wgapi.configure_interface(&config); - #[cfg(windows)] - let configure_interface_result = wgapi.configure_interface(&config, &dns, &search_domains); configure_interface_result.expect("Failed to configure WireGuard interface"); @@ -252,20 +247,17 @@ async fn connect(config: CliConfig, ifname: String, trigger: Arc) -> Res wgapi .configure_peer_routing(&config.peers) .expect("Failed to configure routing for WireGuard interface"); - - if dns.is_empty() { - debug!( - "No DNS configuration provided for interface {ifname}, skipping DNS configuration" - ); - } else { - debug!( - "The following DNS servers will be set: {dns:?}, search domains: \ - {search_domains:?}" - ); - wgapi - .configure_dns(&dns, &search_domains) - .expect("Failed to configure DNS for WireGuard interface"); - } + } + if dns.is_empty() { + debug!("No DNS configuration provided for interface {ifname}, skipping DNS configuration"); + } else { + debug!( + "The following DNS servers will be set: {dns:?}, search domains: \ + {search_domains:?}" + ); + wgapi + .configure_dns(&dns, &search_domains) + .expect("Failed to configure DNS for WireGuard interface"); } debug!("Finished creating a new interface {ifname}"); @@ -624,14 +616,18 @@ async fn main() { let token = submatches .get_one::("token") .expect("No enrollment token was provided or it's invalid") - .to_string(); + .clone(); let url = submatches .get_one::("url") .expect("No enrollment URL was provided or it's invalid"); debug!("Successfully parsed enrollment token and URL"); - let config = enroll(url, token) - .await - .expect("The enrollment process has failed"); + let config = match enroll(url, token).await { + Ok(cfg) => cfg, + Err(err) => { + error!("Enrollment process failed with error: {err}"); + return; + } + }; debug!("Successfully enrolled the device, saving the configuration."); if let Err(err) = config.save(&config_path) { error!("{err}"); diff --git a/src-tauri/common/src/lib.rs b/src-tauri/common/src/lib.rs index bd4a0117..4d2f836b 100644 --- a/src-tauri/common/src/lib.rs +++ b/src-tauri/common/src/lib.rs @@ -11,14 +11,11 @@ pub fn find_free_tcp_port() -> Option { .map(|local_addr| local_addr.port()) } -#[cfg(not(windows))] -/// Find next available interface. On macOS, search for available `utun` interface. -/// On other UNIX, search for available `wg` interface. +#[cfg(not(any(windows, target_os = "macos")))] +/// Find next available interface. +/// Search for available `wg` interface. #[must_use] pub fn get_interface_name(_name: &str) -> String { - #[cfg(target_os = "macos")] - let base_ifname = "utun"; - #[cfg(not(target_os = "macos"))] let base_ifname = "wg"; if let Ok(interfaces) = nix::net::if_::if_nameindex() { for index in 0..=u16::MAX { @@ -36,7 +33,7 @@ pub fn get_interface_name(_name: &str) -> String { } /// Strips location name of all non-alphanumeric characters returning usable interface name. -#[cfg(windows)] +#[cfg(any(windows, target_os = "macos"))] #[must_use] pub fn get_interface_name(name: &str) -> String { name.chars().filter(|c| c.is_alphanumeric()).collect() diff --git a/src-tauri/deny.toml b/src-tauri/deny.toml index d7769622..47d8a68d 100644 --- a/src-tauri/deny.toml +++ b/src-tauri/deny.toml @@ -111,6 +111,7 @@ allow = [ "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "MPL-2.0", + "BSD-2-Clause", "BSD-3-Clause", "Unicode-3.0", "Unicode-DFS-2016", # unicode-ident diff --git a/src-tauri/migrations/20251009102408_service_locations.sql b/src-tauri/migrations/20251009102408_service_locations.sql new file mode 100644 index 00000000..aa1e67ba --- /dev/null +++ b/src-tauri/migrations/20251009102408_service_locations.sql @@ -0,0 +1,4 @@ +-- 1 - disabled +-- 2 - pre-logon +-- 3 - always-on +ALTER TABLE location ADD COLUMN service_location_mode INTEGER NOT NULL DEFAULT 1; diff --git a/src-tauri/migrations/20251117093105_force_all_traffic.sql b/src-tauri/migrations/20251117093105_force_all_traffic.sql new file mode 100644 index 00000000..b6c2ca79 --- /dev/null +++ b/src-tauri/migrations/20251117093105_force_all_traffic.sql @@ -0,0 +1,16 @@ +-- add client_traffic_policy column to `instance` table +-- since SQLite does not support native enums we'll store them as integers +-- 0 - None +-- 1 - Disable all traffic +-- 2 - Force all traffic +ALTER TABLE instance ADD COLUMN client_traffic_policy INTEGER NOT NULL DEFAULT 0; + +-- populate new column based on value in `disable_all_traffic` column +UPDATE instance +SET client_traffic_policy = CASE + WHEN disable_all_traffic = true THEN 1 + ELSE 0 +END; + +-- drop the `disable_all_traffic` column since it's no longer needed +ALTER TABLE instance DROP COLUMN disable_all_traffic; diff --git a/src-tauri/proto b/src-tauri/proto index fa9c14ef..5dfc8c8d 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit fa9c14efd121182ec39c8716370e1250c77fa652 +Subproject commit 5dfc8c8d23ac0613108a2b7b921fd9a97613bb3a diff --git a/src-tauri/resources-macos/binaries/.gitkeep b/src-tauri/resources-macos/binaries/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src-tauri/resources-macos/resources/net.defguard.plist b/src-tauri/resources-macos/resources/net.defguard.plist deleted file mode 100644 index 479b9737..00000000 --- a/src-tauri/resources-macos/resources/net.defguard.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Label - net.defguard - EnvironmentVariables - - PATH - /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/sbin - - Program - /usr/local/bin/defguard-service - KeepAlive - - RunAtLoad - - GroupName - staff - - diff --git a/src-tauri/resources-macos/resources/uninstall.sh b/src-tauri/resources-macos/resources/uninstall.sh deleted file mode 100644 index 9004910e..00000000 --- a/src-tauri/resources-macos/resources/uninstall.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -SERVICE_BINARY=defguard-service -DAEMON_PROPERTY_FILE=net.defguard.plist -WIREGUARD_GO_BINARY=wireguard-go -DAEMON_NAME=net.defguard -PACKAGE_ID=net.defguard - -#Check running user -if (( $EUID != 0 )); then - echo "Please run as root." - exit -fi - -# Remove wireguard-go shortcut at /usr/local/bin -rm -f /usr/local/bin/${WIREGUARD_GO_BINARY} - -# Remove service shortcut at /usr/local/bin -rm -f /usr/local/bin/${SERVICE_BINARY} - -# Remove daemon -launchctl stop ${DAEMON_NAME} -launchctl unload /Library/LaunchDaemons/${DAEMON_PROPERTY_FILE} -rm -f /Library/LaunchDaemons/${DAEMON_PROPERTY_FILE} - -pkgutil --forget ${PACKAGE_ID} > /dev/null 2>&1 - -rm -rf /Applications/defguard-client.app - -echo "Application uninstall process finished" diff --git a/src-tauri/resources-macos/scripts/postinstall b/src-tauri/resources-macos/scripts/postinstall deleted file mode 100755 index d660c2a9..00000000 --- a/src-tauri/resources-macos/scripts/postinstall +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -SERVICE_BINARY=defguard-service -DAEMON_PROPERTY_FILE=net.defguard.plist -WIREGUARD_GO_BINARY=wireguard-go -DAEMON_NAME=net.defguard -APP_BUNDLE=defguard-client.app -PRODUCT_HOME=/Applications/${APP_BUNDLE} -BINARY_PATH=${PRODUCT_HOME}/Contents/MacOS -RESOURCES_PATH=${PRODUCT_HOME}/Contents/Resources/resources-macos/resources - -echo "Post installation process started" - -mkdir -p /usr/local/bin - -# Add wireguard-go shortcut to /usr/local/bin -ln -sf ${BINARY_PATH}/${WIREGUARD_GO_BINARY} /usr/local/bin/${WIREGUARD_GO_BINARY} - -# Add service shortcut to /usr/local/bin -ln -sf ${BINARY_PATH}/${SERVICE_BINARY} /usr/local/bin/${SERVICE_BINARY} - -# Launch daemon -ln -sf ${RESOURCES_PATH}/${DAEMON_PROPERTY_FILE} /Library/LaunchDaemons/${DAEMON_PROPERTY_FILE} -sudo launchctl load /Library/LaunchDaemons/${DAEMON_PROPERTY_FILE} -# Restart -sudo launchctl stop ${DAEMON_NAME} - -echo "Post installation process finished" diff --git a/src-tauri/resources-windows/admin-scripts/GenerateEnrollmentTokensAD.ps1 b/src-tauri/resources-windows/admin-scripts/GenerateEnrollmentTokensAD.ps1 new file mode 100644 index 00000000..35935b97 --- /dev/null +++ b/src-tauri/resources-windows/admin-scripts/GenerateEnrollmentTokensAD.ps1 @@ -0,0 +1,240 @@ +param( + [Parameter(Mandatory=$true)] + [string]$Url, + + [Parameter(Mandatory=$true)] + [string]$ApiToken, + + [Parameter(Mandatory=$true)] + [string]$GroupName, + + [Parameter(Mandatory=$true)] + [string]$ADAttribute, + + [Parameter(Mandatory=$false)] + [string]$ADUsername, + + [Parameter(Mandatory=$false)] + [string]$DomainController, + + [Parameter(Mandatory=$false)] + [string]$EnrollmentTokenExpirationTime +) + +# Function to make authenticated API calls to Defguard +function Invoke-AuthenticatedRestMethod { + param( + [string]$Method, + [string]$Endpoint, + [object]$Body = $null + ) + + $headers = @{ + "Authorization" = "Bearer $ApiToken" + "Content-Type" = "application/json" + "Accept" = "application/json" + } + + $uri = "$Url/$Endpoint" + + try { + if ($Body) { + $jsonBody = $Body | ConvertTo-Json + $response = Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers -Body $jsonBody + } else { + $response = Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers + } + return $response + } + catch { + Write-Error "Defguard API call failed: $($_.Exception.Message)" + return $null + } +} + +# Function to update Active Directory user attribute +function Set-ADUserEnrollmentToken { + param( + [string]$Username, + [string]$EnrollmentToken, + [string]$EnrollmentUrl, + [string]$AttributeName, + [System.Management.Automation.PSCredential]$Credential + ) + + try { + # Build parameters for AD cmdlets + $adParams = @{ + Identity = $Username + Properties = $AttributeName + ErrorAction = "Stop" + } + + # Add credential if provided + if ($Credential) { + $adParams["Credential"] = $Credential + } + + # Add domain controller if provided + if ($DomainController) { + $adParams["Server"] = $DomainController + } + + # Verify user exists in Active Directory (result not stored, just checking for errors) + Get-ADUser @adParams | Out-Null + + # Create JSON object to store in AD attribute + $enrollmentData = @{ + enrollmentToken = $EnrollmentToken + enrollmentUrl = $EnrollmentUrl + } + + $jsonData = $enrollmentData | ConvertTo-Json -Compress + + # Update AD user attribute + $setParams = @{ + Identity = $Username + Replace = @{$AttributeName = $jsonData} + ErrorAction = "Stop" + } + + # Add credential if provided + if ($Credential) { + $setParams["Credential"] = $Credential + } + + # Add domain controller if provided + if ($DomainController) { + $setParams["Server"] = $DomainController + } + + Set-ADUser @setParams + + Write-Host " Successfully updated AD attribute for $Username" -ForegroundColor Green + return $true + } + catch { + Write-Host " Failed to update AD attribute for $Username : $($_.Exception.Message)" -ForegroundColor Red + return $false + } +} + +# Main script execution +Write-Host "Fetching group members for group: $GroupName" -ForegroundColor Green + +# Handle AD authentication +$ADCredential = $null +if ($ADUsername) { + Write-Host "Using provided AD credentials for authentication" -ForegroundColor Yellow + $ADPassword = Read-Host -Prompt "Enter AD password for $ADUsername" -AsSecureString + $ADCredential = New-Object System.Management.Automation.PSCredential($ADUsername, $ADPassword) +} else { + Write-Host "Using current user context for AD authentication" -ForegroundColor Yellow +} + +# Get group members +Write-Host "Fetching group members from Defguard..." -ForegroundColor Yellow +$groupEndpoint = "api/v1/group/$GroupName" +$groupResponse = Invoke-AuthenticatedRestMethod -Method "GET" -Endpoint $groupEndpoint + +if (-not $groupResponse) { + Write-Error "Failed to fetch group members" + exit 1 +} + +# Extract usernames from the response +$usernames = $groupResponse.members + +if (-not $usernames -or $usernames.Count -eq 0) { + Write-Host "No members found in group: $GroupName" -ForegroundColor Yellow + exit 0 +} + +Write-Host "Found $($usernames.Count) members in the group" -ForegroundColor Green + +# Import Active Directory module +try { + Import-Module ActiveDirectory -ErrorAction Stop + Write-Host "Active Directory module loaded successfully" -ForegroundColor Green +} +catch { + Write-Error "Failed to load Active Directory module: $($_.Exception.Message)" + exit 1 +} + +# Test AD connectivity +try { + $testParams = @{ Filter = "Name -like '*'" } + if ($ADCredential) { + $testParams["Credential"] = $ADCredential + Write-Host "Testing AD connectivity with provided credentials..." -ForegroundColor Yellow + } else { + Write-Host "Testing AD connectivity with current user context..." -ForegroundColor Yellow + } + if ($DomainController) { $testParams["Server"] = $DomainController } + + Get-ADUser @testParams -ResultSetSize 1 | Out-Null + Write-Host "Active Directory connectivity test successful" -ForegroundColor Green +} +catch { + Write-Error "Active Directory connectivity test failed: $($_.Exception.Message)" + Write-Host "Please check your credentials, domain controller, and network connectivity" -ForegroundColor Red + exit 1 +} + +# Array to store enrollment tokens +$enrollmentTokens = @() +$adUpdateResults = @() + +# Loop through each user and generate enrollment token +foreach ($username in $usernames) { + Write-Host "Processing user: $username" -ForegroundColor Cyan + + $enrollmentEndpoint = "api/v1/user/$username/start_enrollment" + $requestBody = @{ + email = $null + send_enrollment_notification = $false + } + + # Add token expiration time if provided + if ($EnrollmentTokenExpirationTime) { + $requestBody["token_expiration_time"] = $EnrollmentTokenExpirationTime + } + + $enrollmentResponse = Invoke-AuthenticatedRestMethod -Method "POST" -Endpoint $enrollmentEndpoint -Body $requestBody + + if ($enrollmentResponse) { + $tokenInfo = @{ + username = $username + enrollment_token = $enrollmentResponse.enrollment_token + enrollment_url = $enrollmentResponse.enrollment_url + } + $enrollmentTokens += $tokenInfo + + Write-Host " Enrollment token generated for $username" -ForegroundColor Green + + # Update Active Directory + $adResult = Set-ADUserEnrollmentToken -Username $username -EnrollmentToken $enrollmentResponse.enrollment_token -EnrollmentUrl $enrollmentResponse.enrollment_url -AttributeName $ADAttribute -Credential $ADCredential + + $adUpdateResults += @{ + username = $username + success = $adResult + enrollment_token = $enrollmentResponse.enrollment_token + enrollment_url = $enrollmentResponse.enrollment_url + } + } + else { + Write-Host " Failed to generate enrollment token for $username" -ForegroundColor Red + $adUpdateResults += @{ + username = $username + success = $false + enrollment_token = $null + enrollment_url = $null + } + } +} + +# Output summary +Write-Host "Enrollment token generation and AD update completed!" -ForegroundColor Green +$successfulADUpdates = ($adUpdateResults | Where-Object { $_.success }).Count +Write-Host "Successfully updated AD attributes: $successfulADUpdates/$($usernames.Count)" -ForegroundColor $(if ($successfulADUpdates -eq $usernames.Count) { "Green" } else { "Yellow" }) diff --git a/src-tauri/resources-windows/admin-scripts/GenerateEnrollmentTokensEntraID.ps1 b/src-tauri/resources-windows/admin-scripts/GenerateEnrollmentTokensEntraID.ps1 new file mode 100644 index 00000000..a40ef6bf --- /dev/null +++ b/src-tauri/resources-windows/admin-scripts/GenerateEnrollmentTokensEntraID.ps1 @@ -0,0 +1,352 @@ +param( + [Parameter(Mandatory=$true)] + [string]$Url, + + [Parameter(Mandatory=$true)] + [string]$ApiToken, + + [Parameter(Mandatory=$true)] + [string]$GroupName, + + [Parameter(Mandatory=$false)] + [string]$AttributeSetName = "Defguard", + + [Parameter(Mandatory=$false)] + [string]$EnrollmentTokenExpirationTime +) + +# Function to make authenticated API calls to Defguard +function Invoke-AuthenticatedRestMethod { + param( + [string]$Method, + [string]$Endpoint, + [object]$Body = $null + ) + + $headers = @{ + "Authorization" = "Bearer $ApiToken" + "Content-Type" = "application/json" + "Accept" = "application/json" + } + + $uri = "$Url/$Endpoint" + + try { + if ($Body) { + $jsonBody = $Body | ConvertTo-Json + $response = Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers -Body $jsonBody + } else { + $response = Invoke-RestMethod -Uri $uri -Method $Method -Headers $headers + } + return $response + } + catch { + Write-Error "Defguard API call failed: $($_.Exception.Message)" + return $null + } +} + + +# Function to find user in Entra ID by email address using Microsoft.Graph module +function Get-EntraIDUser { + param( + [string]$Email + ) + + Write-Host " Searching for user with email: $Email" -ForegroundColor Cyan + + # Try to find user by mail (primary email) + try { + Write-Host " Trying search by 'mail' property..." -ForegroundColor Gray + $user = Get-MgUser -Filter "mail eq '$Email'" -ErrorAction Stop + if ($user) { + Write-Host " Found user by mail: $($user.UserPrincipalName)" -ForegroundColor Green + return $user + } + } + catch { + Write-Host " Error searching by mail: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " Full error details:" -ForegroundColor Red + Write-Host " Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red + Write-Host " Inner Exception: $($_.Exception.InnerException.Message)" -ForegroundColor Red + } + + # Try to find user by userPrincipalName (often matches email) + try { + Write-Host " Trying search by 'userPrincipalName' property..." -ForegroundColor Gray + $user = Get-MgUser -Filter "userPrincipalName eq '$Email'" -ErrorAction Stop + if ($user) { + Write-Host " Found user by userPrincipalName: $($user.UserPrincipalName)" -ForegroundColor Green + return $user + } + } + catch { + Write-Host " Error searching by userPrincipalName: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " Full error details:" -ForegroundColor Red + Write-Host " Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red + Write-Host " Inner Exception: $($_.Exception.InnerException.Message)" -ForegroundColor Red + } + + # Try other mail properties if the above don't work + try { + Write-Host " Trying search by 'otherMails' property..." -ForegroundColor Gray + $user = Get-MgUser -Filter "otherMails/any(m:m eq '$Email')" -ErrorAction Stop + if ($user) { + Write-Host " Found user by otherMails: $($user.UserPrincipalName)" -ForegroundColor Green + return $user + } + } + catch { + Write-Host " Error searching by otherMails: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " Full error details:" -ForegroundColor Red + Write-Host " Exception Type: $($_.Exception.GetType().FullName)" -ForegroundColor Red + Write-Host " Inner Exception: $($_.Exception.InnerException.Message)" -ForegroundColor Red + } + + # Try a broader search to see if we can find any users at all + try { + Write-Host " Testing basic user query..." -ForegroundColor Gray + $testUser = Get-MgUser -Top 1 -ErrorAction Stop + Write-Host " Basic user query successful - permissions appear valid" -ForegroundColor Green + } + catch { + Write-Host " Basic user query failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " This suggests a permissions issue with User.ReadWrite.All" -ForegroundColor Red + } + + Write-Host " User not found in Entra ID for email: $Email" -ForegroundColor Yellow + return $null +} + +# Function to update Entra ID user custom security attributes using Microsoft.Graph module +function Set-EntraIDUserEnrollmentToken { + param( + [string]$UserId, + [string]$EnrollmentToken, + [string]$EnrollmentUrl, + [string]$AttributeSetName + ) + + try { + # Build the custom security attributes payload + $attributes = @{ + "$AttributeSetName" = @{ + "@odata.type" = "#microsoft.graph.customSecurityAttributeValue" + "EnrollmentToken" = $EnrollmentToken + "EnrollmentUrl" = $EnrollmentUrl + } + } + + # Update user with custom security attributes + Update-MgUser -UserId $UserId -CustomSecurityAttributes $attributes -ErrorAction Stop + + Write-Host " Successfully updated Entra ID custom security attributes for user ID: $UserId" -ForegroundColor Green + return $true + } + catch { + Write-Host " Failed to update Entra ID custom security attributes for user ID: $UserId" -ForegroundColor Red + Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red + + return $false + } +} + +# Main script execution +Write-Host "Starting enrollment token generation for Entra ID users in group: $GroupName" -ForegroundColor Green + +# Check for Microsoft.Graph module and install/import if needed +Write-Host "Setting up Microsoft.Graph modules..." -ForegroundColor Yellow + +# Install only the specific modules we need to avoid function capacity issues +$requiredModules = @( + "Microsoft.Graph.Authentication", + "Microsoft.Graph.Users" +) + +foreach ($module in $requiredModules) { + try { + # Check if module is installed + if (-not (Get-Module -ListAvailable -Name $module)) { + Write-Host "$module module is required but not installed. Attempting to install..." -ForegroundColor Yellow + Install-Module $module -Scope CurrentUser -Force -ErrorAction Stop + Write-Host "$module module installed successfully" -ForegroundColor Green + } else { + # Update to latest version to avoid dependency issues + Write-Host "Updating $module module to latest version..." -ForegroundColor Yellow + Update-Module $module -Force -ErrorAction SilentlyContinue + Write-Host "$module module is up to date" -ForegroundColor Green + } + + # Import the module + Import-Module $module -Force -ErrorAction Stop + Write-Host "$module module imported successfully" -ForegroundColor Green + } + catch { + Write-Error "Failed to setup $module module: $($_.Exception.Message)" + Write-Host "Please try installing manually: Install-Module $module -Scope CurrentUser -Force" -ForegroundColor Red + exit 1 + } +} + +# Connect to Microsoft Graph +Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow +try { + # Check if we're already connected + $context = Get-MgContext -ErrorAction SilentlyContinue + if ($context -and $context.Scopes -contains "CustomSecAttributeAssignment.ReadWrite.All" -and $context.Scopes -contains +"User.ReadWrite.All") { + Write-Host "Already connected to Microsoft Graph with required permissions" -ForegroundColor Green + } else { + # Disconnect from any existing Graph sessions first + Disconnect-MgGraph -ErrorAction SilentlyContinue + + # Try authentication methods in order of preference + Write-Host "Attempting authentication methods..." -ForegroundColor Cyan + + # Method 1: Try interactive browser authentication + try { + Write-Host "Trying interactive browser authentication" -ForegroundColor Yellow + Connect-MgGraph -Scopes @('CustomSecAttributeAssignment.ReadWrite.All', 'User.ReadWrite.All') -ErrorAction Stop + Write-Host "Successfully connected using interactive authentication" -ForegroundColor Green + } + catch { + # Method 2: Fall back to device code authentication + Write-Host "Interactive browser authentication failed, trying device code flow..." -ForegroundColor Yellow + Connect-MgGraph -Scopes @('CustomSecAttributeAssignment.ReadWrite.All', 'User.ReadWrite.All') -UseDeviceCode -ErrorAction Stop + Write-Host "Successfully connected using device code flow" -ForegroundColor Green + } + } +} +catch { + Write-Error "Failed to connect to Microsoft Graph: $($_.Exception.Message)" + Write-Host "Authentication troubleshooting:" -ForegroundColor Red + Write-Host " - Ensure you're on a domain-joined machine and connected to corporate network" -ForegroundColor Yellow + Write-Host " - Verify you have the required Entra ID permissions" -ForegroundColor Yellow + Write-Host " - Check that custom security attributes are enabled in your tenant" -ForegroundColor Yellow + Write-Host " - Complete any MFA prompts if required" -ForegroundColor Yellow + Write-Host "Required permissions: CustomSecAttributeAssignment.ReadWrite.All, User.ReadWrite.All" -ForegroundColor Cyan + exit 1 +} + +# Get group members from Defguard +Write-Host "Fetching group members from Defguard..." -ForegroundColor Yellow +$groupEndpoint = "api/v1/group/$GroupName" +$groupResponse = Invoke-AuthenticatedRestMethod -Method "GET" -Endpoint $groupEndpoint + +if (-not $groupResponse) { + Write-Error "Failed to fetch group members from Defguard" + exit 1 +} + +# Extract usernames from the response +$usernames = $groupResponse.members + +if (-not $usernames -or $usernames.Count -eq 0) { + Write-Host "No members found in group: $GroupName" -ForegroundColor Yellow + exit 0 +} + +Write-Host "Found $($usernames.Count) members in the group" -ForegroundColor Green + +# Arrays to store results +$enrollmentTokens = @() +$entraUpdateResults = @() + +# Loop through each username, fetch user details to get email, then generate enrollment token and update Entra ID +foreach ($username in $usernames) { + Write-Host "Processing user: $username" -ForegroundColor Cyan + + # Get user details from Defguard to fetch email + $userDetailsEndpoint = "api/v1/user/$username" + $userDetailsResponse = Invoke-AuthenticatedRestMethod -Method "GET" -Endpoint $userDetailsEndpoint + + if (-not $userDetailsResponse -or -not $userDetailsResponse.user -or -not $userDetailsResponse.user.email) { + Write-Host " Failed to fetch user details or email not found for user: $username" -ForegroundColor Red + $entraUpdateResults += @{ + username = $username + email = $null + entra_id = $null + user_principal_name = $null + success = $false + enrollment_token = $null + enrollment_url = $null + } + continue + } + + $userEmail = $userDetailsResponse.user.email + Write-Host " Found email: $userEmail" -ForegroundColor Green + + # Generate enrollment token from Defguard using username + $enrollmentEndpoint = "api/v1/user/$username/start_enrollment" + $requestBody = @{ + email = $null + send_enrollment_notification = $false + } + + # Add token expiration time if provided + if ($EnrollmentTokenExpirationTime) { + $requestBody["token_expiration_time"] = $EnrollmentTokenExpirationTime + } + + $enrollmentResponse = Invoke-AuthenticatedRestMethod -Method "POST" -Endpoint $enrollmentEndpoint -Body $requestBody + + if ($enrollmentResponse) { + Write-Host " Enrollment token generated for $username" -ForegroundColor Green + + # Find user in Entra ID by email + Write-Host " Searching for user in Entra ID by email..." -ForegroundColor Yellow + $entraUser = Get-EntraIDUser -Email $userEmail + + if ($entraUser) { + Write-Host " Found user in Entra ID: $($entraUser.UserPrincipalName) (ID: $($entraUser.Id))" -ForegroundColor Green + + # Update Entra ID custom security attributes + $updateResult = Set-EntraIDUserEnrollmentToken -UserId $entraUser.Id -EnrollmentToken $enrollmentResponse.enrollment_token -EnrollmentUrl $enrollmentResponse.enrollment_url -AttributeSetName $AttributeSetName + + $entraUpdateResults += @{ + username = $username + email = $userEmail + entra_id = $entraUser.Id + user_principal_name = $entraUser.UserPrincipalName + success = $updateResult + enrollment_token = $enrollmentResponse.enrollment_token + enrollment_url = $enrollmentResponse.enrollment_url + } + } + else { + Write-Host " User with email $userEmail not found in Entra ID" -ForegroundColor Red + $entraUpdateResults += @{ + username = $username + email = $userEmail + entra_id = $null + user_principal_name = $null + success = $false + enrollment_token = $enrollmentResponse.enrollment_token + enrollment_url = $enrollmentResponse.enrollment_url + } + } + } + else { + Write-Host " Failed to generate enrollment token for $username" -ForegroundColor Red + $entraUpdateResults += @{ + username = $username + email = $userEmail + entra_id = $null + user_principal_name = $null + success = $false + enrollment_token = $null + enrollment_url = $null + } + } +} + +# Output summary +Write-Host "`nEnrollment token generation and Entra ID update completed!" -ForegroundColor Green +$successfulEntraUpdates = ($entraUpdateResults | Where-Object { $_.success }).Count +Write-Host "Successfully updated Entra ID custom security attributes: $successfulEntraUpdates/$($usernames.Count)" -ForegroundColor $(if ($successfulEntraUpdates -eq $usernames.Count) { "Green" } else { "Yellow" }) + +# Disconnect from Microsoft Graph +Write-Host "`nDisconnecting from Microsoft Graph..." -ForegroundColor Yellow +Disconnect-MgGraph -ErrorAction SilentlyContinue +Write-Host "Disconnected successfully" -ForegroundColor Green diff --git a/src-tauri/resources-windows/binaries/wireguard-amd64-0.5.3.msi b/src-tauri/resources-windows/binaries/wireguard-amd64-0.5.3.msi deleted file mode 100644 index 853f6a68..00000000 Binary files a/src-tauri/resources-windows/binaries/wireguard-amd64-0.5.3.msi and /dev/null differ diff --git a/src-tauri/resources-windows/binaries/wireguard.dll b/src-tauri/resources-windows/binaries/wireguard.dll new file mode 100644 index 00000000..efc03626 Binary files /dev/null and b/src-tauri/resources-windows/binaries/wireguard.dll differ diff --git a/src-tauri/resources-windows/defguard-client.wxs b/src-tauri/resources-windows/defguard-client.wxs deleted file mode 100644 index a045af0a..00000000 --- a/src-tauri/resources-windows/defguard-client.wxs +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/src-tauri/resources-windows/fragments/provisioning.wxs b/src-tauri/resources-windows/fragments/provisioning.wxs new file mode 100644 index 00000000..06f993a4 --- /dev/null +++ b/src-tauri/resources-windows/fragments/provisioning.wxs @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + PROVISIONING AND NOT REMOVE + + + diff --git a/src-tauri/resources-windows/service-fragment.wxs b/src-tauri/resources-windows/fragments/service.wxs similarity index 95% rename from src-tauri/resources-windows/service-fragment.wxs rename to src-tauri/resources-windows/fragments/service.wxs index 1c16bda7..835720c5 100644 --- a/src-tauri/resources-windows/service-fragment.wxs +++ b/src-tauri/resources-windows/fragments/service.wxs @@ -2,7 +2,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#if allow_downgrades}} + + {{else}} + + {{/if}} + + + Installed AND NOT UPGRADINGPRODUCTCODE + + + + + {{#if banner_path}} + + {{/if}} + {{#if dialog_image_path}} + + {{/if}} + {{#if license}} + + {{/if}} + + + + + + + {{#if homepage}} + + + + {{/if}} + + + + + + + + + + + + + + + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + + + {{#unless license}} + + 1 + 1 + {{/unless}} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#each deep_link_protocols as |protocol| ~}} + + + + + + + + + + + {{/each~}} + + + + {{#each file_associations as |association| ~}} + {{#each association.ext as |ext| ~}} + + + + + + {{/each~}} + {{/each~}} + + {{#if enable_elevated_update_task}} + + + + + + + + + + {{/if}} + {{resources}} + + + + + + + + + + + + + + + + + + + + + {{#each merge_modules as |msm| ~}} + + + + + + + + {{/each~}} + + + + + + {{#each resource_file_ids as |resource_file_id| ~}} + + {{/each~}} + + {{#if enable_elevated_update_task}} + + + + {{/if}} + + + + + + + + + + + + + + + {{#each component_group_refs as |id| ~}} + + {{/each~}} + {{#each component_refs as |id| ~}} + + {{/each~}} + {{#each feature_group_refs as |id| ~}} + + {{/each~}} + {{#each feature_refs as |id| ~}} + + {{/each~}} + {{#each merge_refs as |id| ~}} + + {{/each~}} + + + {{#if install_webview}} + + + + + + + {{#if download_bootstrapper}} + + + + + + + {{/if}} + + + {{#if webview2_bootstrapper_path}} + + + + + + + + {{/if}} + + + {{#if webview2_installer_path}} + + + + + + + + {{/if}} + + {{/if}} + + {{#if enable_elevated_update_task}} + + + + + NOT(REMOVE) + + + + + + + (REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE + + + {{/if}} + + + AUTOLAUNCHAPP AND NOT Installed + + + + + diff --git a/src-tauri/resources-windows/msi/side_banner.png b/src-tauri/resources-windows/msi/side_banner.png new file mode 100644 index 00000000..4bb1bb2b Binary files /dev/null and b/src-tauri/resources-windows/msi/side_banner.png differ diff --git a/src-tauri/resources-windows/msi/top_banner.png b/src-tauri/resources-windows/msi/top_banner.png new file mode 100644 index 00000000..9bbca64f Binary files /dev/null and b/src-tauri/resources-windows/msi/top_banner.png differ diff --git a/src-tauri/resources-windows/scripts/Get-ProvisioningConfig.ps1 b/src-tauri/resources-windows/scripts/Get-ProvisioningConfig.ps1 new file mode 100644 index 00000000..9a6d3bf9 --- /dev/null +++ b/src-tauri/resources-windows/scripts/Get-ProvisioningConfig.ps1 @@ -0,0 +1,316 @@ +#Requires -Version 5.1 + +<# +.SYNOPSIS + Retrieves Defguard client provisioning configuration for the currently logged-in user from Active Directory or Entra ID. + +.DESCRIPTION + This script detects whether the computer is joined to on-premises Active Directory + or Entra ID (Azure AD), then fetches Defguard provisioning data (URL and enrollment token) from the appropriate source. + - On-premises AD: Reads from specified attribute (default: defguardProvisioningConfig) + - Entra ID: Reads from custom security attributes under the 'Defguard' set + - Workgroup: Exits gracefully + The retrieved enrollment data is saved to a JSON file for the Defguard client to use. + +.PARAMETER ADAttribute + Specifies which Active Directory attribute to read from (default: defguardProvisioningConfig) +#> + +param( + [string]$ADAttribute = "defguardProvisioningConfig" +) + +# Check device join status +function Get-DomainJoinStatus { + try { + $computerSystem = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop + + # Check for traditional domain join + if ($computerSystem.PartOfDomain -eq $true) { + return @{ + JoinType = "OnPremisesAD" + Domain = $computerSystem.Domain + } + } + + # Check for Entra ID (Azure AD) join + $dsregStatus = dsregcmd /status + if ($dsregStatus -match "AzureAdJoined\s*:\s*YES") { + $tenantName = ($dsregStatus | Select-String "TenantName\s*:\s*(.+)").Matches.Groups[1].Value.Trim() + return @{ + JoinType = "EntraID" + Domain = $tenantName + } + } + + # Check for Hybrid join + if ($dsregStatus -match "DomainJoined\s*:\s*YES" -and $dsregStatus -match "AzureAdJoined\s*:\s*YES") { + return @{ + JoinType = "Hybrid" + Domain = $computerSystem.Domain + } + } + + # Not joined to any directory + return @{ + JoinType = "Workgroup" + Domain = $null + } + + } catch { + Write-Host "Unable to determine domain status: $_" -ForegroundColor Yellow + return @{ + JoinType = "Unknown" + Domain = $null + } + } +} + +# Save Defguard enrollment data to JSON +function Save-DefguardEnrollmentData { + param( + [string]$EnrollmentUrl, + [string]$EnrollmentToken + ) + + # Create Defguard directory in AppData\Roaming + $defguardDir = Join-Path $env:APPDATA "net.defguard" + $jsonOutputPath = Join-Path $defguardDir "provisioning.json" + + try { + # Create directory if it doesn't exist + if (-not (Test-Path -Path $defguardDir)) { + New-Item -ItemType Directory -Path $defguardDir -Force | Out-Null + Write-Host "`nCreated directory: $defguardDir" -ForegroundColor Gray + } + + $jsonData = @{ + enrollment_url = $EnrollmentUrl + enrollment_token = $EnrollmentToken + } + + $jsonData | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonOutputPath -Encoding UTF8 -Force + Write-Host "`nDefguard enrollment data saved to: $jsonOutputPath" -ForegroundColor Green + return $true + } catch { + Write-Host "`nFailed to save JSON file: $_" -ForegroundColor Red + return $false + } +} + +# Get Defguard client provisioning config from on-premises AD +function Get-OnPremisesADProvisioningConfig { + param( + [string]$Username, + [string]$ADAttribute + ) + + # Check if Active Directory module is available + if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { + Write-Host "Active Directory module is not installed. Please install RSAT tools." -ForegroundColor Red + return + } + + # Import the Active Directory module + try { + Import-Module ActiveDirectory -ErrorAction Stop + } catch { + Write-Host "Failed to import Active Directory module: $_" -ForegroundColor Red + return + } + + # Fetch AD user information + try { + $adUser = Get-ADUser -Identity $Username -Properties * -ErrorAction Stop + + # Display user information + Write-Host "`n=== On-Premises Active Directory User Information ===" -ForegroundColor Cyan + Write-Host "Display Name: $($adUser.DisplayName)" + Write-Host "Username (SAM): $($adUser.SamAccountName)" + Write-Host "User Principal Name: $($adUser.UserPrincipalName)" + Write-Host "Email: $($adUser.EmailAddress)" + Write-Host "Enabled: $($adUser.Enabled)" + Write-Host "Created: $($adUser.Created)" + Write-Host "Distinguished Name: $($adUser.DistinguishedName)" + Write-Host "======================================================`n" -ForegroundColor Cyan + + # Check for Defguard enrollment data in the specified AD attribute + Write-Host "`n--- Active Directory Attribute ---" -ForegroundColor Yellow + + # Read JSON data from the specified AD attribute + $jsonData = $adUser.$ADAttribute + + Write-Host "Defguard Enrollment JSON ($ADAttribute): $jsonData" + + if ($jsonData) { + try { + # Parse the JSON data + $enrollmentConfig = $jsonData | ConvertFrom-Json -ErrorAction Stop + + # Extract URL and token from the parsed JSON + $enrollmentUrl = $enrollmentConfig.enrollmentUrl + $enrollmentToken = $enrollmentConfig.enrollmentToken + + Write-Host "Defguard Enrollment URL: $enrollmentUrl" + Write-Host "Defguard Enrollment Token: $enrollmentToken" + + # Save enrollment data to JSON file only if both URL and token exist + if ($enrollmentUrl -and $enrollmentToken) { + Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl ` + -EnrollmentToken $enrollmentToken + } else { + Write-Host "`nWarning: Incomplete Defguard enrollment data in JSON. Both URL and token are required." -ForegroundColor Yellow + } + } catch { + Write-Host "Failed to parse JSON from AD attribute '$ADAttribute': $_" -ForegroundColor Red + Write-Host "JSON data should be in format: {`"enrollmentUrl`":`"https://...`",`"enrollmentToken`":`"token-value`"}" -ForegroundColor Yellow + } + } else { + Write-Host "No Defguard enrollment data found in the specified AD attribute." -ForegroundColor Yellow + } + + Write-Host "======================================================`n" -ForegroundColor Cyan + + + return + + } catch { + Write-Host "Failed to retrieve AD user information for '$Username': $_" -ForegroundColor Red + return + } +} + +# Get Defguard client provisioning config from Entra ID +function Get-EntraIDProvisioningConfig { + # Check if Microsoft.Graph module is available + if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) { + Write-Host "Microsoft.Graph.Users module is not installed." -ForegroundColor Yellow + Write-Host "Install it with: Install-Module Microsoft.Graph.Users -Scope CurrentUser" -ForegroundColor Yellow + return + } + + # Import the module + try { + Import-Module Microsoft.Graph.Users -ErrorAction Stop + } catch { + Write-Host "Failed to import Microsoft.Graph.Users module: $_" -ForegroundColor Red + return + } + + # Connect to Microsoft Graph + try { + $context = Get-MgContext -ErrorAction SilentlyContinue + + if (-not $context) { + Write-Host "Connecting to Microsoft Graph (authentication required)..." -ForegroundColor Yellow + Write-Host "Note: Requesting additional permissions for custom security attributes..." -ForegroundColor Gray + Connect-MgGraph -Scopes "User.Read", "CustomSecAttributeAssignment.Read.All" -ErrorAction Stop + } else { + # Check if we have the required scope for custom attributes + $hasCustomAttrScope = $context.Scopes -contains "CustomSecAttributeAssignment.Read.All" + if (-not $hasCustomAttrScope) { + Write-Host "Warning: Missing 'CustomSecAttributeAssignment.Read.All' permission." -ForegroundColor Yellow + Write-Host "Custom security attributes will not be available. Reconnect with:" -ForegroundColor Yellow + Write-Host " Connect-MgGraph -Scopes 'User.Read', 'CustomSecAttributeAssignment.Read.All'" -ForegroundColor Gray + return + } + } + + # Get current user info including custom security attributes + $properties = @( + "DisplayName", + "UserPrincipalName", + "Mail", + "AccountEnabled", + "CreatedDateTime", + "Id", + "CustomSecurityAttributes" + ) + + $mgUser = Get-MgUser -UserId (Get-MgContext).Account -Property $properties -ErrorAction Stop + + # Display user information + Write-Host "`n=== Entra ID (Azure AD) User Information ===" -ForegroundColor Cyan + Write-Host "Display Name: $($mgUser.DisplayName)" + Write-Host "User Principal Name: $($mgUser.UserPrincipalName)" + Write-Host "Email: $($mgUser.Mail)" + Write-Host "Account Enabled: $($mgUser.AccountEnabled)" + Write-Host "Created: $($mgUser.CreatedDateTime)" + Write-Host "User ID: $($mgUser.Id)" + + # Try to get custom security attributes + if ($mgUser.CustomSecurityAttributes) { + Write-Host "`n--- Custom Security Attributes ---" -ForegroundColor Yellow + + # Access Defguard attributes + if ($mgUser.CustomSecurityAttributes.AdditionalProperties) { + $defguardAttrs = $mgUser.CustomSecurityAttributes.AdditionalProperties["Defguard"] + + if ($defguardAttrs) { + $enrollmentUrl = $defguardAttrs["EnrollmentUrl"] + $enrollmentToken = $defguardAttrs["EnrollmentToken"] + + Write-Host "Defguard Enrollment URL: $enrollmentUrl" + Write-Host "Defguard Enrollment Token: $enrollmentToken" + + # Save enrollment data to JSON file only if both URL and token exist + if ($enrollmentUrl -and $enrollmentToken) { + Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl ` + -EnrollmentToken $enrollmentToken + } else { + Write-Host "`nWarning: Incomplete Defguard enrollment data. Both URL and token are required." -ForegroundColor Yellow + } + } else { + Write-Host "No Defguard attributes found for this user." -ForegroundColor Gray + } + } else { + Write-Host "No custom security attributes found." -ForegroundColor Gray + } + } else { + Write-Host "`nCustom security attributes not available." -ForegroundColor Gray + Write-Host "(May require additional permissions or attributes not set)" -ForegroundColor Gray + } + + Write-Host "=============================================`n" -ForegroundColor Cyan + + } catch { + Write-Host "Failed to retrieve Entra ID user information: $_" -ForegroundColor Red + Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red + } +} + +# Log all script output to file +$defguardDir = Join-Path $env:APPDATA "net.defguard" +$logFilePath = Join-Path $defguardDir "provisioning_log.txt" +Start-Transcript -Path $logFilePath + +# Main script execution +Write-Host "Detecting domain join status..." -ForegroundColor Gray + +$joinStatus = Get-DomainJoinStatus +$joinType = $joinStatus.JoinType + +Write-Host "Join Type = '$joinType'" -ForegroundColor Magenta + +if ($joinType -eq "OnPremisesAD") { + Write-Host "Connected to on-premises Active Directory: $($joinStatus.Domain)" -ForegroundColor Green + $currentUser = $env:USERNAME + Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute +} elseif ($joinType -eq "Hybrid") { + Write-Host "Hybrid join detected (both on-premises AD and Entra ID): $($joinStatus.Domain)" -ForegroundColor Green + Write-Host "Querying on-premises Active Directory..." -ForegroundColor Gray + $currentUser = $env:USERNAME + Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute +} elseif ($joinType -eq "EntraID") { + Write-Host "Connected to Entra ID (Azure AD)" -ForegroundColor Green + if ($joinStatus.Domain) { + Write-Host " Tenant: $($joinStatus.Domain)" -ForegroundColor Gray + } + Get-EntraIDProvisioningConfig +} elseif ($joinType -eq "Workgroup") { + Write-Host "This computer is not connected to a domain (Workgroup). Exiting." -ForegroundColor Yellow +} else { + Write-Host "Unable to determine domain connection status. Exiting." -ForegroundColor Yellow +} + +Stop-Transcript diff --git a/src-tauri/src/active_connections.rs b/src-tauri/src/active_connections.rs index ed0ef4b2..970a31bd 100644 --- a/src-tauri/src/active_connections.rs +++ b/src-tauri/src/active_connections.rs @@ -82,7 +82,7 @@ pub(crate) async fn find_connection( pub(crate) async fn active_connections( instance: &Instance, ) -> Result, Error> { - let locations: HashSet = Location::find_by_instance_id(&*DB_POOL, instance.id) + let locations: HashSet = Location::find_by_instance_id(&*DB_POOL, instance.id, false) .await? .iter() .map(|location| location.id) diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 492d9023..c851bd92 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -71,6 +71,8 @@ pub struct AppConfig { pub log_level: LevelFilter, /// In seconds. How much time after last network activity the connection is automatically dropped. pub peer_alive_period: u32, + /// Maximal transmission unit. 0 means default value. + mtu: u32, } // Important: keep in sync with client store default in frontend @@ -82,6 +84,7 @@ impl Default for AppConfig { tray_theme: AppTrayTheme::Color, log_level: LevelFilter::Info, peer_alive_period: 300, + mtu: 0, } } } @@ -130,4 +133,15 @@ impl AppConfig { } } } + + /// Wraps MTU in an Option. We don't store Option directly in AppConfig to avoid struct-patch + /// ambiguity when applying updates coming from the frontend. An incoming MTU value of 0 is + /// interpreted as a request to fall back to the default. + #[must_use] + pub fn mtu(&self) -> Option { + match self.mtu { + 0 => None, + v => Some(v), + } + } } diff --git a/src-tauri/src/apple.rs b/src-tauri/src/apple.rs new file mode 100644 index 00000000..4321adca --- /dev/null +++ b/src-tauri/src/apple.rs @@ -0,0 +1,1197 @@ +//! Interchangeability and communication with VPNExtension (written in Swift). + +use std::{ + collections::HashMap, + hint::spin_loop, + net::IpAddr, + ptr::NonNull, + str::FromStr, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, channel, Receiver, RecvTimeoutError, Sender}, + Arc, LazyLock, Mutex, + }, + time::Duration, +}; + +use block2::RcBlock; +use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; +use objc2::{ + rc::Retained, + runtime::{AnyObject, ProtocolObject}, +}; +use objc2_foundation::{ + ns_string, NSArray, NSData, NSDate, NSDictionary, NSError, NSMutableArray, NSMutableDictionary, + NSNotification, NSNotificationCenter, NSNotificationName, NSNumber, NSObjectProtocol, + NSOperationQueue, NSRunLoop, NSString, +}; +use objc2_network_extension::{ + NETunnelProviderManager, NETunnelProviderProtocol, NETunnelProviderSession, NEVPNStatus, +}; +use serde::Deserialize; +use sqlx::SqliteExecutor; +use tauri::{AppHandle, Emitter, Manager}; +use tracing::Level; + +use crate::{ + active_connections::find_connection, + appstate::AppState, + database::{ + models::{location::Location, tunnel::Tunnel, wireguard_keys::WireguardKeys, Id}, + DB_POOL, + }, + error::Error, + events::EventKey, + log_watcher::service_log_watcher::spawn_log_watcher_task, + tray::show_main_window, + utils::{DEFAULT_ROUTE_IPV4, DEFAULT_ROUTE_IPV6}, + ConnectionType, +}; + +const PLUGIN_BUNDLE_ID: &str = "net.defguard.VPNExtension"; +const SYSTEM_SYNC_DELAY_MS: u64 = 500; +const LOCATION_ID: &str = "locationId"; +const TUNNEL_ID: &str = "tunnelId"; + +static OBSERVER_COMMS: LazyLock<( + Mutex>, + Mutex>>, +)> = LazyLock::new(|| { + let (tx, rx) = mpsc::channel(); + (Mutex::new(tx), Mutex::new(Some(rx))) +}); +static VPN_STATE_UPDATE_COMMS: LazyLock<(Mutex>, Mutex>>)> = + LazyLock::new(|| { + let (tx, rx) = mpsc::channel(); + (Mutex::new(tx), Mutex::new(Some(rx))) + }); + +/// Thread responsible for handling VPN status update requests. +/// This is an async function. +/// It has access to the `AppHandle` to be able to emit events. +pub async fn connection_state_update_thread(app_handle: &AppHandle) { + let receiver = { + let mut rx_opt = VPN_STATE_UPDATE_COMMS + .1 + .lock() + .expect("Failed to lock state update receiver"); + rx_opt.take().expect("Receiver already taken") + }; + + debug!("Waiting for status update message from channel..."); + while receiver.recv().is_ok() { + debug!("Status update message received, synchronizing state..."); + tokio::time::sleep(Duration::from_millis(SYSTEM_SYNC_DELAY_MS)).await; + sync_connections_with_system(app_handle).await; + debug!("Processed status update message."); + } +} + +/// Synchronize the app's connection state with the system's VPN state. +/// This checks all locations and tunnels and updates the app state to match +/// what's actually running in the system. +pub async fn sync_connections_with_system(app_handle: &AppHandle) { + let pool = DB_POOL.clone(); + let app_state = app_handle.state::(); + + if let Ok(locations) = Location::all(&pool, false).await { + for location in locations { + debug!( + "Synchronizing VPN status for location with system status: {}. Querying status...", + location.name + ); + let status = get_location_status(&location); + debug!( + "Location {} (ID {}) status: {status:?}", + location.name, location.id + ); + + match status { + Some(NEVPNStatus::Connected) => { + debug!("Location {} is connected", location.name); + if find_connection(location.id, crate::ConnectionType::Location) + .await + .is_some() + { + debug!( + "Location {} has already a connected state, skipping synchronization", + location.name + ); + } else { + // Check if location requires MFA - if so, we need to cancel this connection + // and trigger MFA flow through the app + if location.mfa_enabled() { + info!( + "Location {} requires MFA but was started from system settings, \ + canceling system connection and triggering MFA flow", + location.name + ); + stop_tunnel_for_location(&location); + show_main_window(app_handle); + let _ = app_handle.emit(EventKey::MfaTrigger.into(), &location); + continue; + } + + debug!("Adding connection for location {}", location.name); + + app_state + .add_connection( + location.id, + &location.name, + crate::ConnectionType::Location, + ) + .await; + app_handle + .emit(EventKey::ConnectionChanged.into(), ()) + .unwrap(); + + debug!( + "Spawning log watcher for location {} (started from system settings)", + location.name + ); + if let Err(e) = spawn_log_watcher_task( + app_handle.clone(), + location.id, + location.name.clone(), + ConnectionType::Location, + Level::DEBUG, + None, + ) + .await + { + warn!( + "Failed to spawn log watcher for location {}: {e}", + location.name + ); + } + } + } + Some(NEVPNStatus::Disconnected) => { + debug!("Location {} is disconnected", location.name); + if find_connection(location.id, crate::ConnectionType::Location) + .await + .is_some() + { + debug!("Removing connection for location {}", location.name); + app_state + .remove_connection(location.id, crate::ConnectionType::Location) + .await; + app_handle + .emit(EventKey::ConnectionChanged.into(), ()) + .unwrap(); + } else { + debug!( + "Location {} has no active connection, skipping removal", + location.name + ); + } + } + Some(unknown_status) => { + debug!( + "Location {} has unknown status {unknown_status:?}, skipping synchronization", + location.name + ); + } + None => { + debug!( + "Couldn't find configuration for tunnel {}, skipping synchronization", + location.name + ); + } + } + } + } + + if let Ok(tunnels) = Tunnel::all(&pool).await { + for tunnel in tunnels { + debug!( + "Synchronizing VPN status for tunnel with system status: {}. Querying status...", + tunnel.name + ); + let status = get_tunnel_status(&tunnel); + debug!( + "Location {} (ID {}) status: {status:?}", + tunnel.name, tunnel.id + ); + + match status { + Some(NEVPNStatus::Connected) => { + debug!("Location {} is connected", tunnel.name); + if find_connection(tunnel.id, crate::ConnectionType::Tunnel) + .await + .is_some() + { + debug!( + "Location {} has already a connected state, skipping synchronization", + tunnel.name + ); + } else { + debug!("Adding connection for location {}", tunnel.name); + + app_state + .add_connection(tunnel.id, &tunnel.name, crate::ConnectionType::Tunnel) + .await; + + app_handle + .emit(EventKey::ConnectionChanged.into(), ()) + .unwrap(); + + // Spawn log watcher for this tunnel (VPN was started from system settings) + debug!( + "Spawning log watcher for tunnel {} (started from system settings)", + tunnel.name + ); + if let Err(e) = spawn_log_watcher_task( + app_handle.clone(), + tunnel.id, + tunnel.name.clone(), + ConnectionType::Tunnel, + Level::DEBUG, + None, + ) + .await + { + warn!( + "Failed to spawn log watcher for tunnel {}: {e}", + tunnel.name + ); + } + } + } + Some(NEVPNStatus::Disconnected) => { + debug!("Location {} is disconnected", tunnel.name); + if find_connection(tunnel.id, crate::ConnectionType::Tunnel) + .await + .is_some() + { + debug!("Removing connection for location {}", tunnel.name); + app_state + .remove_connection(tunnel.id, crate::ConnectionType::Tunnel) + .await; + app_handle + .emit(EventKey::ConnectionChanged.into(), ()) + .unwrap(); + } else { + debug!( + "Location {} has no active connection, skipping removal", + tunnel.name + ); + } + } + Some(unknown_status) => { + debug!( + "Location {} has unknown status {:?}, skipping synchronization", + tunnel.name, unknown_status + ); + } + None => { + debug!( + "Couldn't find configuration for tunnel {}, skipping synchronization", + tunnel.name + ); + } + } + } + } +} + +const OBSERVER_CLEANUP_INTERVAL: Duration = Duration::from_secs(30); + +/// Thread responsible for observing VPN status changes. +/// This is intentionally a blocking function, as it uses the objective-c objects which are not thread safe. +pub fn observer_thread(initial_managers: HashMap<(String, Id), Retained>) { + debug!("Starting VPN connection observer thread"); + let receiver = { + let mut rx_opt = OBSERVER_COMMS + .1 + .lock() + .expect("Failed to lock observer receiver"); + rx_opt.take().expect("Receiver already taken") + }; + + let mut observers = HashMap::new(); + + // spawn initial observers for existing managers + for ((key, value), manager) in initial_managers { + debug!("Spawning initial observer for manager with key: {key}, value: {value}"); + let connection = unsafe { manager.connection() }; + + let observer = create_observer( + &NSNotificationCenter::defaultCenter(), + unsafe { objc2_network_extension::NEVPNStatusDidChangeNotification }, + vpn_status_change_handler, + Some(connection.as_ref()), + ); + debug!("Registered initial observer for manager with key: {key}, value: {value}"); + observers.insert((key, value), observer); + } + + loop { + match receiver.recv_timeout(OBSERVER_CLEANUP_INTERVAL) { + Ok(message) => { + debug!("Received message to observe the following connection: {message:?}"); + + let (key, value) = message; + + if observers.contains_key(&(key.clone(), value)) { + debug!( + "Observer for manager with key: {key}, value: {value} already exists, + skipping", + ); + continue; + } + + let manager = manager_for_key_and_value(&key, value).unwrap(); + let connection = unsafe { manager.connection() }; + let observer = create_observer( + &NSNotificationCenter::defaultCenter(), + unsafe { objc2_network_extension::NEVPNStatusDidChangeNotification }, + vpn_status_change_handler, + Some(connection.as_ref()), + ); + + observers.insert((key.clone(), value), observer); + debug!("Registered observer for manager with key: {key}, value: {value}"); + } + Err(RecvTimeoutError::Timeout) => { + debug!("Performing periodic cleanup of dead observers"); + let mut dead_keys = Vec::new(); + + for (key, value) in observers.keys() { + if manager_for_key_and_value(key, *value).is_none() { + debug!( + "Manager for key: {key}, value: {value} no longer exists, marking for + removal" + ); + dead_keys.push((key.clone(), *value)); + } + } + + for dead_key in dead_keys { + if let Some(_observer) = observers.remove(&dead_key) { + debug!( + "Removed dead VPN connection observer for key: {}, value: {}", + dead_key.0, dead_key.1 + ); + } + } + } + Err(RecvTimeoutError::Disconnected) => { + error!("Observer receiver channel disconnected, exiting observer thread"); + break; + } + } + } + + debug!("Exiting VPN connection observer thread"); +} + +/// Tunnel statistics shared with VPNExtension (written in Swift). +#[derive(Deserialize)] +#[repr(C)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Stats { + pub(crate) location_id: Option, + pub(crate) tunnel_id: Option, + pub(crate) tx_bytes: u64, + pub(crate) rx_bytes: u64, + pub(crate) last_handshake: u64, +} + +/// Run [`NSRunLoop`] until semaphore becomes `true`. +pub fn spawn_runloop_and_wait_for(semaphore: Arc) { + const ONE_SECOND: f64 = 1.; + let run_loop = NSRunLoop::currentRunLoop(); + let mut date = NSDate::dateWithTimeIntervalSinceNow(ONE_SECOND); + loop { + run_loop.runUntilDate(&date); + if semaphore.load(Ordering::Acquire) { + break; + } + date = date.dateByAddingTimeInterval(ONE_SECOND); + } +} + +fn vpn_status_change_handler(notification: &NSNotification) { + let name = notification.name(); + debug!("Received VPN status change notification: {name:?}"); + VPN_STATE_UPDATE_COMMS + .0 + .lock() + .expect("Failed to lock state update sender") + .send(()) + .expect("Failed to send to state update channel"); + debug!("Sent status update request to channel"); +} + +fn create_observer( + center: &NSNotificationCenter, + name: &NSNotificationName, + handler: impl Fn(&NSNotification) + 'static, + object: Option<&AnyObject>, +) -> Retained> { + let block = RcBlock::new(move |notification: NonNull| { + handler(unsafe { notification.as_ref() }); + }); + let queue = NSOperationQueue::mainQueue(); + unsafe { + center.addObserverForName_object_queue_usingBlock(Some(name), object, Some(&queue), &block) + } +} + +#[must_use] +pub fn get_managers_for_tunnels_and_locations( + tunnels: &[Tunnel], + locations: &[Location], +) -> HashMap<(String, Id), Retained> { + let mut managers = HashMap::new(); + + for location in locations { + if let Some(manager) = manager_for_key_and_value(LOCATION_ID, location.id) { + managers.insert((LOCATION_ID.to_string(), location.id), manager); + } + } + + for tunnel in tunnels { + if let Some(manager) = manager_for_key_and_value(TUNNEL_ID, tunnel.id) { + managers.insert((TUNNEL_ID.to_string(), tunnel.id), manager); + } + } + + managers +} + +/// Try to get `Id` out of manager. ID is embedded in configuration dictionary under `key`. +fn id_from_manager(manager: &NETunnelProviderManager, key: &NSString) -> Option { + // TODO: This variable should be static. + let plugin_bundle_id = NSString::from_str(PLUGIN_BUNDLE_ID); + + let Some(vpn_protocol) = (unsafe { manager.protocolConfiguration() }) else { + return None; + }; + let Ok(tunnel_protocol) = vpn_protocol.downcast::() else { + error!("Failed to downcast to NETunnelProviderProtocol"); + return None; + }; + // Sometimes all managers from all apps come through, so filter by bundle ID. + if let Some(bundle_id) = unsafe { tunnel_protocol.providerBundleIdentifier() } { + if bundle_id != plugin_bundle_id { + return None; + } + } + + if let Some(config_dict) = unsafe { tunnel_protocol.providerConfiguration() } { + if let Some(any_object) = config_dict.objectForKey(key) { + let Ok(id) = any_object.downcast::() else { + warn!("Failed to downcast ID to NSNumber"); + return None; + }; + return Some(id.as_i64()); + } + } + + None +} + +/// Try to find [`NETunnelProviderManager`] in system settings that matches key and value. +/// Key is usually `locationId` or `tunnelId`. +fn manager_for_key_and_value(key: &str, value: Id) -> Option> { + let key_string = NSString::from_str(key); + let (tx, rx) = channel(); + + let handler = RcBlock::new( + move |managers_ptr: *mut NSArray, error_ptr: *mut NSError| { + if !error_ptr.is_null() { + error!("Failed to load tunnel provider managers."); + return; + } + + let Some(managers) = (unsafe { managers_ptr.as_ref() }) else { + error!("No managers"); + return; + }; + + for manager in managers { + if let Some(id) = id_from_manager(&manager, &key_string) { + if id == value { + // This is the manager we were looking for. + tx.send(Some(manager)).expect("Sender is dead"); + return; + } + } + } + + tx.send(None).expect("Sender is dead"); + }, + ); + unsafe { + NETunnelProviderManager::loadAllFromPreferencesWithCompletionHandler(&handler); + } + + rx.recv().expect("Receiver is dead") +} + +/// Tunnel configuration shared with VPNExtension (written in Swift). +pub(crate) struct TunnelConfiguration { + location_id: Option, + tunnel_id: Option, + name: String, + private_key: String, + addresses: Vec, + listen_port: Option, + peers: Vec, + mtu: Option, + dns: Vec, + dns_search: Vec, +} + +impl TunnelConfiguration { + /// Convert to [`NSDictionary`]. + fn as_nsdict(&self) -> Retained> { + let dict = NSMutableDictionary::new(); + + if let Some(location_id) = self.location_id { + dict.insert( + ns_string!(LOCATION_ID), + NSNumber::new_i64(location_id).as_ref(), + ); + } + + if let Some(tunnel_id) = self.tunnel_id { + dict.insert(ns_string!(TUNNEL_ID), NSNumber::new_i64(tunnel_id).as_ref()); + } + + dict.insert(ns_string!("name"), NSString::from_str(&self.name).as_ref()); + + dict.insert( + ns_string!("privateKey"), + NSString::from_str(&self.private_key).as_ref(), + ); + + // IpAddrMask + let addresses = NSMutableArray::>::new(); + for addr in &self.addresses { + let addr_dict = NSMutableDictionary::::new(); + addr_dict.insert( + ns_string!("address"), + NSString::from_str(&addr.address.to_string()).as_ref(), + ); + addr_dict.insert(ns_string!("cidr"), NSNumber::new_u8(addr.cidr).as_ref()); + addresses.addObject(addr_dict.into_super().as_ref()); + } + dict.insert(ns_string!("addresses"), addresses.as_ref()); + + if let Some(listen_port) = self.listen_port { + dict.insert( + ns_string!("listenPort"), + NSNumber::new_u16(listen_port).as_ref(), + ); + } + + // Peer + let peers = NSMutableArray::>::new(); + for peer in &self.peers { + let peer_dict = NSMutableDictionary::::new(); + peer_dict.insert( + ns_string!("publicKey"), + NSString::from_str(&peer.public_key.to_string()).as_ref(), + ); + + if let Some(preshared_key) = &peer.preshared_key { + peer_dict.insert( + ns_string!("preSharedKey"), + NSString::from_str(&preshared_key.to_string()).as_ref(), + ); + } + + if let Some(endpoint) = &peer.endpoint { + peer_dict.insert( + ns_string!("endpoint"), + NSString::from_str(&endpoint.to_string()).as_ref(), + ); + } + + // Skipping: lastHandshake, txBytes, rxBytes. + + if let Some(persistent_keep_alive) = peer.persistent_keepalive_interval { + peer_dict.insert( + ns_string!("persistentKeepAlive"), + NSNumber::new_u16(persistent_keep_alive).as_ref(), + ); + } + + // IpAddrMask + let allowed_ips = NSMutableArray::>::new(); + for addr in &peer.allowed_ips { + let addr_dict = NSMutableDictionary::::new(); + addr_dict.insert( + ns_string!("address"), + NSString::from_str(&addr.address.to_string()).as_ref(), + ); + addr_dict.insert(ns_string!("cidr"), NSNumber::new_u8(addr.cidr).as_ref()); + allowed_ips.addObject(addr_dict.into_super().as_ref()); + } + peer_dict.insert(ns_string!("allowedIPs"), allowed_ips.as_ref()); + + peers.addObject(peer_dict.into_super().as_ref()); + } + dict.insert(ns_string!("peers"), peers.into_super().as_ref()); + + if let Some(mtu) = self.mtu { + dict.insert(ns_string!("mtu"), NSNumber::new_u32(mtu).as_ref()); + } + + let dns = NSMutableArray::::new(); + for entry in &self.dns { + dns.addObject(NSString::from_str(&entry.to_string()).as_ref()); + } + dict.insert(ns_string!("dns"), dns.as_ref()); + + let dns_search = NSMutableArray::::new(); + for entry in &self.dns_search { + dns_search.addObject(NSString::from_str(entry).as_ref()); + } + dict.insert(ns_string!("dnsSearch"), dns_search.as_ref()); + + dict.into_super() + } + + /// Try to find `NETunnelProviderManager` for this configuration, based on location ID or + /// tunnel ID. + pub(crate) fn tunnel_provider_manager(&self) -> Option> { + let (key, value) = match (self.location_id, self.tunnel_id) { + (Some(location_id), None) => (LOCATION_ID, location_id), + (None, Some(tunnel_id)) => (TUNNEL_ID, tunnel_id), + _ => return None, + }; + + manager_for_key_and_value(key, value) + } + + /// Create or update system VPN settings with this configuration. + pub(crate) fn save(&self) { + let spinlock = Arc::new(AtomicBool::new(false)); + let spinlock_clone = Arc::clone(&spinlock); + let plugin_bundle_id = NSString::from_str(PLUGIN_BUNDLE_ID); + + let provider_manager = self + .tunnel_provider_manager() + .unwrap_or_else(|| unsafe { NETunnelProviderManager::new() }); + + unsafe { + let tunnel_protocol = NETunnelProviderProtocol::new(); + tunnel_protocol.setProviderBundleIdentifier(Some(&plugin_bundle_id)); + let server_address = self.peers.first().map_or(String::new(), |peer| { + peer.endpoint.map_or(String::new(), |sa| sa.to_string()) + }); + let server_address = NSString::from_str(&server_address); + // `serverAddress` must have a non-nil string value for the protocol configuration to be + // valid. + tunnel_protocol.setServerAddress(Some(&server_address)); + + let provider_config = self.as_nsdict(); + tunnel_protocol.setProviderConfiguration(Some(&*provider_config)); + + provider_manager.setProtocolConfiguration(Some(&tunnel_protocol)); + let name = NSString::from_str(&self.name); + provider_manager.setLocalizedDescription(Some(&name)); + provider_manager.setEnabled(true); + + // Save to system settings. + let handler = RcBlock::new(move |error_ptr: *mut NSError| { + if error_ptr.is_null() { + debug!("Saved tunnel configuration for {name} to system settings"); + } else { + error!("Failed to save tunnel configuration for: {name} to system settings"); + } + spinlock_clone.store(true, Ordering::Release); + }); + provider_manager.saveToPreferencesWithCompletionHandler(Some(&*handler)); + } + + while !spinlock.load(Ordering::Acquire) { + spin_loop(); + } + } + + /// Start tunnel for this configuration. + pub(crate) fn start_tunnel(&self) { + if let Some(provider_manager) = self.tunnel_provider_manager() { + if let Err(err) = + unsafe { provider_manager.connection().startVPNTunnelAndReturnError() } + { + error!("Failed to start VPN: {err}"); + } else { + OBSERVER_COMMS + .0 + .lock() + .expect("Failed to lock observer sender") + .send(( + self.location_id.map_or_else( + || TUNNEL_ID.to_string(), + |_location_id| LOCATION_ID.to_string(), + ), + self.location_id.or(self.tunnel_id).unwrap(), + )) + .expect("Failed to send to observer channel"); + info!("VPN started"); + } + } else { + debug!( + "Couldn't find configuration from system settings for {}", + self.name + ); + } + } +} + +/// Remove configuration from system settings for [`Location`]. +pub(crate) fn remove_config_for_location(location: &Location) { + if let Some(provider_manager) = manager_for_key_and_value(LOCATION_ID, location.id) { + unsafe { + provider_manager.removeFromPreferencesWithCompletionHandler(None); + } + } else { + debug!( + "Couldn't find configuration in system settings for location {}", + location.name + ); + } +} + +/// Remove configuration from system settings for [`Tunnel`]. +pub(crate) fn remove_config_for_tunnel(tunnel: &Tunnel) { + if let Some(provider_manager) = manager_for_key_and_value(TUNNEL_ID, tunnel.id) { + unsafe { + provider_manager.removeFromPreferencesWithCompletionHandler(None); + } + } else { + debug!( + "Couldn't find configuration in system settings for tunnel {}", + tunnel.name + ); + } +} + +/// Stop tunnel for [`Location`]. +pub(crate) fn stop_tunnel_for_location(location: &Location) -> bool { + manager_for_key_and_value(LOCATION_ID, location.id).map_or_else( + || { + debug!( + "Couldn't find configuration in system settings for location {}", + location.name + ); + false + }, + |provider_manager| { + unsafe { + provider_manager.connection().stopVPNTunnel(); + } + + info!("VPN stopped"); + true + }, + ) +} + +/// Stop tunnel for [`Tunnel`]. +pub(crate) fn stop_tunnel_for_tunnel(tunnel: &Tunnel) -> bool { + manager_for_key_and_value(TUNNEL_ID, tunnel.id).map_or_else( + || { + debug!( + "Couldn't find configuration in system settings for location {}", + tunnel.name + ); + false + }, + |provider_manager| { + unsafe { + provider_manager.connection().stopVPNTunnel(); + } + + info!("VPN stopped"); + true + }, + ) +} + +/// Check whether tunnel is running for [`Location`]. +pub(crate) fn get_location_status(location: &Location) -> Option { + manager_for_key_and_value(LOCATION_ID, location.id).map_or_else( + || { + debug!( + "Couldn't find configuration in system settings for location {}", + location.name + ); + None + }, + |provider_manager| unsafe { + let connection = provider_manager.connection(); + Some(connection.status()) + }, + ) +} + +/// Check whether tunnel is running for [`Tunnel`]. +pub(crate) fn get_tunnel_status(tunnel: &Tunnel) -> Option { + manager_for_key_and_value(TUNNEL_ID, tunnel.id).map_or_else( + || { + debug!( + "Couldn't find configuration in system settings for tunnel {}", + tunnel.name + ); + None + }, + |provider_manager| unsafe { + let connection = provider_manager.connection(); + Some(connection.status()) + }, + ) +} + +pub(crate) fn tunnel_stats(id: Id, connection_type: &ConnectionType) -> Option { + let new_stats = Arc::new(Mutex::new(None)); + let plugin_bundle_id = NSString::from_str(PLUGIN_BUNDLE_ID); + + let new_stats_clone = Arc::clone(&new_stats); + + let finished = Arc::new(AtomicBool::new(false)); + let finished_clone = Arc::clone(&finished); + + let response_handler = RcBlock::new(move |data_ptr: *mut NSData| { + if let Some(data) = unsafe { data_ptr.as_ref() } { + if let Ok(stats) = serde_json::from_slice(data.to_vec().as_slice()) { + if let Ok(mut new_stats_locked) = new_stats_clone.lock() { + *new_stats_locked = Some(stats); + } + } else { + warn!("Failed to deserialize tunnel stats"); + } + } else { + debug!("No data received in tunnel stats response, skipping"); + } + finished_clone.store(true, Ordering::Release); + }); + + let manager = manager_for_key_and_value( + match connection_type { + ConnectionType::Location => LOCATION_ID, + ConnectionType::Tunnel => TUNNEL_ID, + }, + id, + )?; + + let vpn_protocol = (unsafe { manager.protocolConfiguration() })?; + let Ok(tunnel_protocol) = vpn_protocol.downcast::() else { + error!("Failed to downcast to NETunnelProviderProtocol"); + return None; + }; + + // Sometimes all managers from all apps come through, so filter by bundle ID. + if let Some(bundle_id) = unsafe { tunnel_protocol.providerBundleIdentifier() } { + if bundle_id != plugin_bundle_id { + return None; + } + } + + let Ok(session) = unsafe { manager.connection() }.downcast::() else { + error!("Failed to downcast to NETunnelProviderSession"); + return None; + }; + + let message_data = NSData::new(); + if unsafe { + session.sendProviderMessage_returnError_responseHandler( + &message_data, + None, + Some(&response_handler), + ) + } { + debug!("Message sent to NETunnelProviderSession"); + } else { + error!("Failed to send to NETunnelProviderSession while requesting stats"); + } + + // Wait for all handlers to complete. + while !finished.load(Ordering::Acquire) { + spin_loop(); + } + + let stats = new_stats + .lock() + .map_or(None, |mut new_stats_locked| new_stats_locked.take()); + + stats +} + +/// Synchronize locations and tunnels with system settings. +pub async fn sync_locations_and_tunnels(mtu: Option) -> Result<(), sqlx::Error> { + // Update location settings. + let all_locations = Location::all(&*DB_POOL, false).await?; + for location in &all_locations { + // For syncing, set `preshred_key` to `None`. + let Ok(tunnel_config) = location.tunnel_configurarion(&*DB_POOL, None, mtu).await else { + error!( + "Failed to convert location {} to tunnel configuration.", + location.name + ); + continue; + }; + tunnel_config.save(); + } + + // Update tunnel settings. + let all_tunnels = Tunnel::all(&*DB_POOL).await?; + for tunnel in &all_tunnels { + let Ok(tunnel_config) = tunnel.tunnel_configurarion(mtu) else { + error!( + "Failed to convert tunnel {} to tunnel configuration.", + tunnel.name + ); + continue; + }; + tunnel_config.save(); + } + + debug!("Saved all configurations with system settings."); + + // Convert to Vec. + let mut all_location_ids = all_locations + .into_iter() + .map(|entry| entry.id) + .collect::>(); + let mut all_tunnel_ids = all_tunnels + .into_iter() + .map(|entry| entry.id) + .collect::>(); + // For faster lookup using binary search (see below). + all_location_ids.sort_unstable(); + all_tunnel_ids.sort_unstable(); + + let spinlock = Arc::new(AtomicBool::new(false)); + let spinlock_clone = Arc::clone(&spinlock); + let handler = RcBlock::new( + move |managers_ptr: *mut NSArray, error_ptr: *mut NSError| { + if !error_ptr.is_null() { + error!("Failed to load tunnel provider managers."); + return; + } + + let Some(managers) = (unsafe { managers_ptr.as_ref() }) else { + error!("No managers"); + return; + }; + + let location_key = NSString::from_str(LOCATION_ID); + let tunnel_key = NSString::from_str(TUNNEL_ID); + for manager in managers { + if let Some(id) = id_from_manager(&manager, &location_key) { + if all_location_ids.binary_search(&id).is_ok() { + // Known location - skip. + continue; + } + } + if let Some(id) = id_from_manager(&manager, &tunnel_key) { + if all_tunnel_ids.binary_search(&id).is_ok() { + // Known tunnel - skip. + continue; + } + } + unsafe { manager.removeFromPreferencesWithCompletionHandler(None) }; + } + + spinlock_clone.store(true, Ordering::Release); + }, + ); + unsafe { + NETunnelProviderManager::loadAllFromPreferencesWithCompletionHandler(&handler); + } + + while !spinlock.load(Ordering::Acquire) { + spin_loop(); + } + + debug!("Removed unknown configurations from system settings."); + + Ok(()) +} + +impl Location { + /// Build [`TunnelConfiguration`] from [`Location`]. + pub(crate) async fn tunnel_configurarion<'e, E>( + &self, + executor: E, + preshared_key: Option, + mtu: Option, + ) -> Result + where + E: SqliteExecutor<'e>, + { + debug!("Looking for WireGuard keys for location {self} instance"); + let Some(keys) = WireguardKeys::find_by_instance_id(executor, self.instance_id).await? + else { + error!("No keys found for instance: {}", self.instance_id); + return Err(Error::InternalError( + "No keys found for instance".to_string(), + )); + }; + debug!("WireGuard keys found for location {self} instance"); + + // prepare peer config + debug!("Decoding location {self} public key: {}.", self.pubkey); + let peer_key = Key::from_str(&self.pubkey)?; + debug!("Location {self} public key decoded: {peer_key}"); + let mut peer = Peer::new(peer_key); + + debug!("Parsing location {self} endpoint: {}", self.endpoint); + peer.set_endpoint(&self.endpoint)?; + peer.persistent_keepalive_interval = Some(25); + debug!("Parsed location {self} endpoint: {}", self.endpoint); + + if let Some(psk) = preshared_key { + debug!("Decoding location {self} preshared key."); + let peer_psk = Key::from_str(&psk)?; + info!("Location {self} preshared key decoded."); + peer.preshared_key = Some(peer_psk); + } + + debug!("Parsing location {self} allowed IPs: {}", self.allowed_ips); + let allowed_ips = if self.route_all_traffic { + debug!("Using all traffic routing for location {self}"); + vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()] + } else { + debug!( + "Using predefined location {self} traffic: {}", + self.allowed_ips + ); + self.allowed_ips.split(',').map(str::to_string).collect() + }; + for allowed_ip in &allowed_ips { + match IpAddrMask::from_str(allowed_ip) { + Ok(addr) => { + peer.allowed_ips.push(addr); + } + Err(err) => { + // Handle the error from IpAddrMask::from_str, if needed + error!( + "Error parsing IP address {allowed_ip} while setting up interface for \ + location {self}, error details: {err}" + ); + } + } + } + debug!( + "Parsed allowed IPs for location {self}: {:?}", + peer.allowed_ips + ); + + let addresses = self + .address + .split(',') + .map(str::trim) + .map(IpAddrMask::from_str) + .collect::>() + .map_err(|err| { + let msg = format!("Failed to parse IP addresses '{}': {err}", self.address); + error!("{msg}"); + Error::InternalError(msg) + })?; + let (dns, dns_search) = self.dns(); + Ok(TunnelConfiguration { + location_id: Some(self.id), + tunnel_id: None, + name: self.name.clone(), + private_key: keys.prvkey, + addresses, + listen_port: Some(0), + peers: vec![peer], + mtu, + dns, + dns_search, + }) + } +} + +impl Tunnel { + /// Build [`TunnelConfiguration`] from [`Tunnel`]. + pub(crate) fn tunnel_configurarion( + &self, + mtu: Option, + ) -> Result { + // prepare peer config + debug!("Decoding tunnel {self} public key: {}.", self.server_pubkey); + let peer_key = Key::from_str(&self.server_pubkey)?; + debug!("Tunnel {self} public key decoded."); + let mut peer = Peer::new(peer_key); + + debug!("Parsing tunnel {self} endpoint: {}", self.endpoint); + peer.set_endpoint(&self.endpoint)?; + peer.persistent_keepalive_interval = Some( + self.persistent_keep_alive + .try_into() + .expect("Failed to parse persistent keep alive"), + ); + debug!("Parsed tunnel {self} endpoint: {}", self.endpoint); + + if let Some(psk) = &self.preshared_key { + debug!("Decoding tunnel {self} preshared key."); + let peer_psk = Key::from_str(psk)?; + debug!("Preshared key for tunnel {self} decoded."); + peer.preshared_key = Some(peer_psk); + } + + debug!("Parsing tunnel {self} allowed ips: {:?}", self.allowed_ips); + let allowed_ips = if self.route_all_traffic { + debug!("Using all traffic routing for tunnel {self}"); + vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()] + } else { + let msg = self.allowed_ips.as_ref().map_or_else( + || "No allowed IP addresses found in tunnel {self} configuration".to_string(), + |ips| format!("Using predefined location traffic for tunnel {self}: {ips}"), + ); + debug!("{msg}"); + self.allowed_ips + .as_ref() + .map(|ips| ips.split(',').map(str::to_string).collect()) + .unwrap_or_default() + }; + for allowed_ip in &allowed_ips { + match IpAddrMask::from_str(allowed_ip.trim()) { + Ok(addr) => { + peer.allowed_ips.push(addr); + } + Err(err) => { + // Handle the error from IpAddrMask::from_str, if needed + error!("Error parsing IP address {allowed_ip}: {err}"); + // Continue to the next iteration of the loop + } + } + } + debug!("Parsed tunnel {self} allowed IPs: {:?}", peer.allowed_ips); + + let addresses = self + .address + .split(',') + .map(str::trim) + .map(IpAddrMask::from_str) + .collect::>() + .map_err(|err| { + let msg = format!("Failed to parse IP addresses '{}': {err}", self.address); + error!("{msg}"); + Error::InternalError(msg) + })?; + let (dns, dns_search) = self.dns(); + Ok(TunnelConfiguration { + location_id: None, + tunnel_id: Some(self.id), + name: self.name.clone(), + private_key: self.prvkey.clone(), + addresses, + listen_port: Some(0), + peers: vec![peer], + mtu, + dns, + dns_search, + }) + } +} diff --git a/src-tauri/src/appstate.rs b/src-tauri/src/appstate.rs index f8fa0d96..179b9a6b 100644 --- a/src-tauri/src/appstate.rs +++ b/src-tauri/src/appstate.rs @@ -6,10 +6,8 @@ use tokio_util::sync::CancellationToken; use crate::{ active_connections::ACTIVE_CONNECTIONS, app_config::AppConfig, - database::{ - models::{connection::ActiveConnection, Id}, - DB_POOL, - }, + database::models::{connection::ActiveConnection, Id}, + enterprise::provisioning::ProvisioningConfig, utils::stats_handler, ConnectionType, }; @@ -18,15 +16,17 @@ pub struct AppState { pub log_watchers: Mutex>, pub app_config: Mutex, stat_threads: Mutex>>, // location ID is the key + pub provisioning_config: Mutex>, } impl AppState { #[must_use] - pub fn new(config: AppConfig) -> Self { - AppState { + pub fn new(config: AppConfig, provisioning_config: Option) -> Self { + Self { log_watchers: Mutex::new(HashMap::new()), app_config: Mutex::new(config), stat_threads: Mutex::new(HashMap::new()), + provisioning_config: Mutex::new(provisioning_config), } } @@ -45,13 +45,17 @@ impl AppState { drop(connections); debug!("Spawning thread for network statistics for location ID {location_id}"); - let handle = spawn(stats_handler(DB_POOL.clone(), ifname, connection_type)); + #[cfg(target_os = "macos")] + let handle = spawn(stats_handler(location_id, connection_type)); + #[cfg(not(target_os = "macos"))] + let handle = spawn(stats_handler(ifname, connection_type)); let Some(old_handle) = self .stat_threads .lock() .unwrap() .insert(location_id, handle) else { + debug!("Added new network statistics thread for location ID {location_id}"); return; }; warn!("Something went wrong: old network statistics thread still exists"); diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 0b4411e6..46039bda 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -3,6 +3,11 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#[cfg(target_os = "macos")] +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use std::{env, str::FromStr, sync::LazyLock}; #[cfg(unix)] @@ -15,18 +20,18 @@ use defguard_client::{ appstate::AppState, commands::*, database::{ + handle_db_migrations, models::{location_stats::LocationStats, tunnel::TunnelStats}, DB_POOL, }, + enterprise::provisioning::handle_client_initialization, periodic::run_periodic_tasks, service, tray::{configure_tray_icon, setup_tray, show_main_window}, utils::load_log_targets, - VERSION, + LOG_FILENAME, VERSION, }; use log::{Level, LevelFilter}; -#[cfg(target_os = "macos")] -use tauri::{process, Env}; use tauri::{AppHandle, Builder, Manager, RunEvent, WindowEvent}; use tauri_plugin_log::{Target, TargetKind}; @@ -40,14 +45,6 @@ const LOGGING_TARGET_IGNORE_LIST: [&str; 5] = ["tauri", "sqlx", "hyper", "h2", " static LOG_INCLUDES: LazyLock> = LazyLock::new(load_log_targets); async fn startup(app_handle: &AppHandle) { - debug!("Running database migrations, if there are any."); - sqlx::migrate!() - .run(&*DB_POOL) - .await - .expect("Failed to apply database migrations."); - debug!("Applied all database migrations that were pending. If any."); - debug!("Database setup has been completed successfully."); - debug!("Purging old stats from the database."); if let Err(err) = LocationStats::purge(&*DB_POOL).await { error!("Failed to purge location stats: {err}"); @@ -65,7 +62,7 @@ async fn startup(app_handle: &AppHandle) { // and they are still running after the restart. We sync them here to // reflect the real system's state. // TODO: Find a way to intercept the shutdown event and close all connections - #[cfg(target_os = "windows")] + #[cfg(windows)] { match sync_connections(app_handle).await { Ok(_) => { @@ -84,6 +81,50 @@ async fn startup(app_handle: &AppHandle) { } }; } + #[cfg(target_os = "macos")] + { + use defguard_client::{ + apple::get_managers_for_tunnels_and_locations, utils::get_all_tunnels_locations, + }; + + let semaphore = Arc::new(AtomicBool::new(false)); + let semaphore_clone = Arc::clone(&semaphore); + + // Retrieve MTU from `AppConfig`. + let app_state = app_handle.state::(); + let mtu = app_state + .app_config + .lock() + .expect("failed to lock app state") + .mtu(); + let handle = tauri::async_runtime::spawn(async move { + if let Err(err) = defguard_client::apple::sync_locations_and_tunnels(mtu).await { + error!("Failed to sync locations and tunnels: {err}"); + } + semaphore_clone.store(true, Ordering::Release); + }); + defguard_client::apple::spawn_runloop_and_wait_for(semaphore); + let _ = handle.await; + + let (tunnels, locations) = get_all_tunnels_locations().await; + let handle = app_handle.clone(); + // Observer thread is blocking, so its better not to mess with the tauri runtime, + // hence std::thread::spawn. + std::thread::spawn(move || { + defguard_client::apple::observer_thread(get_managers_for_tunnels_and_locations( + &tunnels, &locations, + )); + error!("VPN observer thread has exited unexpectedly, quitting the app."); + handle.exit(0); + }); + + let handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + defguard_client::apple::connection_state_update_thread(&handle).await; + error!("Connection state update thread has exited unexpectedly, quitting the app."); + handle.exit(0); + }); + } // Run periodic tasks. let periodic_tasks_handle = app_handle.clone(); @@ -113,23 +154,6 @@ async fn startup(app_handle: &AppHandle) { } fn main() { - // add bundled `wireguard-go` binary to PATH - #[cfg(target_os = "macos")] - { - debug!("Adding bundled wireguard-go binary to PATH"); - let current_bin_path = - process::current_binary(&Env::default()).expect("Failed to get current binary path"); - let current_bin_dir = current_bin_path - .parent() - .expect("Failed to get current binary directory"); - let current_path = env::var("PATH").expect("Failed to get current PATH variable"); - env::set_var( - "PATH", - format!("{current_path}:{}", current_bin_dir.to_str().unwrap()), - ); - debug!("Added binary dir {} to PATH", current_bin_dir.display()); - } - let app = Builder::default() .invoke_handler(tauri::generate_handler![ all_locations, @@ -156,7 +180,9 @@ fn main() { start_global_logwatcher, stop_global_logwatcher, command_get_app_config, - command_set_app_config + command_set_app_config, + get_provisioning_config, + get_platform_header ]) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { @@ -186,7 +212,47 @@ fn main() { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_process::init()) .setup(|app| { - // Register for linux and dev windows builds + // Create Help menu on macOS. + // https://github.com/tauri-apps/tauri/issues/9371 + #[cfg(target_os = "macos")] + { + use tauri_plugin_opener::OpenerExt; + + const DOC_ITEM_ID: &str = "doc"; + const REPORT_ITEM_ID: &str = "issue"; + const DOC_URL: &str = "https://docs.defguard.net/using-defguard-for-end-users/desktop-client"; + const REPORT_URL: &str = "https://github.com/DefGuard/client/issues/new?labels=bug&template=bug_report.md"; + if let Some(menu) = app.menu() { + if let Some(help_submenu) = menu.get(tauri::menu::HELP_SUBMENU_ID) { + let report_item = tauri::menu::MenuItem::with_id( + app, + REPORT_ITEM_ID, + "Report an issue", + true, + None::<&str>, + )?; + let _ = help_submenu.as_submenu_unchecked().append(&report_item); + let doc_item = tauri::menu::MenuItem::with_id( + app, + DOC_ITEM_ID, + "Defguard Desktop Client Help", + true, + None::<&str>, + )?; + let _ = help_submenu.as_submenu_unchecked().append(&doc_item); + } + } + app.on_menu_event(move |app, event| { + let id = event.id(); + if id == DOC_ITEM_ID { + let _ = app.opener().open_url(DOC_URL, None::<&str>); + } else if id == REPORT_ITEM_ID { + let _ = app.opener().open_url(REPORT_URL, None::<&str>); + } + }); + } + + // Register for Linux and debug Windows builds. #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { use tauri_plugin_deep_link::DeepLinkExt; @@ -227,7 +293,7 @@ fn main() { }) .targets([ Target::new(TargetKind::Stdout), - Target::new(TargetKind::LogDir { file_name: None }), + Target::new(TargetKind::LogDir { file_name: Some(LOG_FILENAME.to_string()) }), ]) .level(log_level) .filter(|metadata| { @@ -264,7 +330,15 @@ fn main() { .build(), )?; - let state = AppState::new(config); + // run DB migrations + tauri::async_runtime::block_on(handle_db_migrations()); + + // Check if client needs to be initialized + // and try to load provisioning config if necessary + let provisioning_config = + tauri::async_runtime::block_on(handle_client_initialization(app_handle)); + + let state = AppState::new(config, provisioning_config); app.manage(state); info!("App setup completed, log level: {log_level}"); @@ -291,13 +365,17 @@ fn main() { // Ensure directories have appropriate permissions (dg25-28). #[cfg(unix)] - set_perms(&data_dir); - #[cfg(unix)] - set_perms(&log_dir); + { + set_perms(&data_dir); + set_perms(&log_dir); + } + info!( - "Application data (database file) will be stored in: {data_dir:?} and application \ - logs in: {log_dir:?}. Logs of the background Defguard service responsible for \ - managing VPN connections at the network level will be stored in: {}.", + "Application data (database file) will be stored in: {} and application logs in: \ + {}. Logs of the background Defguard service responsible for managing VPN \ + connections at the network level will be stored in: {}.", + data_dir.display(), + log_dir.display(), service::config::DEFAULT_LOG_DIR ); tauri::async_runtime::block_on(startup(app_handle)); @@ -325,11 +403,32 @@ fn main() { // Handle shutdown. RunEvent::Exit => { debug!("Exiting the application's main event loop."); - tauri::async_runtime::block_on(async { - let _ = close_all_connections().await; - // This will clean the database file, pruning write-ahead log. - DB_POOL.close().await; - }); + #[cfg(target_os = "macos")] + { + let semaphore = Arc::new(AtomicBool::new(false)); + let semaphore_clone = Arc::clone(&semaphore); + + let handle = tauri::async_runtime::spawn(async move { + let _ = close_all_connections().await; + // This will clean the database file, pruning write-ahead log. + DB_POOL.close().await; + semaphore_clone.store(true, Ordering::Release); + }); + // Obj-C API needs a runtime, but at this point Tauri has closed its runtime, so + // create a temporary one. + defguard_client::apple::spawn_runloop_and_wait_for(semaphore); + tauri::async_runtime::block_on(async move { + let _ = handle.await; + }); + } + #[cfg(not(target_os = "macos"))] + { + tauri::async_runtime::block_on(async move { + let _ = close_all_connections().await; + // This will clean the database file, pruning write-ahead log. + DB_POOL.close().await; + }); + } } _ => { trace!("Received event: {event:?}"); diff --git a/src-tauri/src/bin/defguard-service.rs b/src-tauri/src/bin/defguard-service.rs index b7083e53..7b4ec0cb 100644 --- a/src-tauri/src/bin/defguard-service.rs +++ b/src-tauri/src/bin/defguard-service.rs @@ -1,4 +1,4 @@ -//! defguard interface management daemon +//! Defguard interface management daemon //! //! This binary is meant to run as a daemon with root privileges //! and communicate with the desktop client over HTTP. @@ -7,7 +7,7 @@ #[tokio::main] async fn main() -> anyhow::Result<()> { use clap::Parser; - use defguard_client::service::{config::Config, run_server, utils::logging_setup}; + use defguard_client::service::{config::Config, daemon::run_server, utils::logging_setup}; // parse config let config: Config = Config::parse(); diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index c638e105..018a77d5 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -13,6 +13,10 @@ use tauri::{AppHandle, Emitter, Manager, State}; const UPDATE_URL: &str = "https://pkgs.defguard.net/api/update/check"; +#[cfg(target_os = "macos")] +use crate::apple::{ + remove_config_for_location, remove_config_for_tunnel, stop_tunnel_for_location, +}; use crate::{ active_connections::{find_connection, get_connection_id_by_type}, app_config::{AppConfig, AppConfigPatch}, @@ -20,7 +24,7 @@ use crate::{ database::{ models::{ connection::{ActiveConnection, Connection, ConnectionInfo}, - instance::{Instance, InstanceInfo}, + instance::{ClientTrafficPolicy, Instance, InstanceInfo}, location::{Location, LocationMfaMode}, location_stats::LocationStats, tunnel::{Tunnel, TunnelConnection, TunnelConnectionInfo, TunnelStats}, @@ -29,7 +33,7 @@ use crate::{ }, DB_POOL, }, - enterprise::periodic::config::poll_instance, + enterprise::{periodic::config::poll_instance, provisioning::ProvisioningConfig}, error::Error, events::EventKey, log_watcher::{ @@ -37,18 +41,27 @@ use crate::{ service_log_watcher::stop_log_watcher_task, }, proto::DeviceConfigResponse, - service::{proto::RemoveInterfaceRequest, utils::DAEMON_CLIENT}, tray::{configure_tray_icon, reload_tray_menu}, utils::{ - disconnect_interface, execute_command, get_location_interface_details, + construct_platform_header, disconnect_interface, get_location_interface_details, get_tunnel_interface_details, get_tunnel_or_location_name, handle_connection_for_location, handle_connection_for_tunnel, }, wg_config::parse_wireguard_config, CommonConnection, CommonConnectionInfo, CommonLocationStats, ConnectionType, }; +#[cfg(not(target_os = "macos"))] +use crate::{ + service::{ + client::DAEMON_CLIENT, + proto::{ + DeleteServiceLocationsRequest, RemoveInterfaceRequest, SaveServiceLocationsRequest, + }, + }, + utils::execute_command, +}; -// Create new WireGuard interface +/// Open new WireGuard connection. #[tauri::command(async)] pub async fn connect( location_id: Id, @@ -202,7 +215,7 @@ async fn maybe_update_instance_config(location_id: Id, handle: &AppHandle) -> Re Ok(()) } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize, Serialize)] pub struct Device { pub id: Id, pub name: String, @@ -211,7 +224,7 @@ pub struct Device { pub created_at: i64, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Serialize)] pub struct InstanceResponse { // uuid pub id: String, @@ -219,7 +232,7 @@ pub struct InstanceResponse { pub url: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Serialize)] pub struct SaveDeviceConfigResponse { locations: Vec>, instance: Instance, @@ -240,13 +253,13 @@ pub async fn save_device_config( let mut instance: Instance = instance_info.into(); if response.token.is_some() { debug!( - "The newly saved device config has a polling token, automatic configuration \ - polling will be possible if the core has an enterprise license." + "The newly saved device config has a polling token, automatic configuration polling \ + will be possible if the core has an enterprise license." ); } else { warn!( - "Missing polling token for instance {}, core and/or proxy services may need an \ - update, configuration polling won't work", + "Missing polling token for instance {}, core and/or proxy services may need an update, \ + configuration polling won't work", instance.name, ); } @@ -284,17 +297,81 @@ pub async fn save_device_config( transaction.commit().await?; info!("New instance {instance} created."); trace!("Created following instance: {instance:#?}"); - let locations = Location::find_by_instance_id(&*DB_POOL, instance.id).await?; - trace!("Created following locations: {locations:#?}"); + + let locations = push_service_locations(&instance, keys).await?; + handle.emit(EventKey::InstanceUpdate.into(), ())?; let res: SaveDeviceConfigResponse = SaveDeviceConfigResponse { locations, instance, }; reload_tray_menu(&handle).await; + Ok(res) } +#[cfg(target_os = "macos")] +async fn push_service_locations( + _instance: &Instance, + _keys: WireguardKeys, +) -> Result>, Error> { + // Nothing here... yet + + Ok(Vec::new()) +} + +#[cfg(not(target_os = "macos"))] +async fn push_service_locations( + instance: &Instance, + keys: WireguardKeys, +) -> Result>, Error> { + let locations = Location::find_by_instance_id(&*DB_POOL, instance.id, true).await?; + trace!("Created following locations: {locations:#?}"); + + let mut service_locations = Vec::new(); + + for saved_location in &locations { + if saved_location.is_service_location() { + debug!( + "Adding service location {}({}) for instance {}({}) to be saved to the daemon.", + saved_location.name, saved_location.id, instance.name, instance.id, + ); + service_locations.push(saved_location.to_service_location()?); + } + } + + if !service_locations.is_empty() { + let save_request = SaveServiceLocationsRequest { + service_locations: service_locations.clone(), + instance_id: instance.uuid.clone(), + private_key: keys.prvkey, + }; + debug!( + "Saving {} service locations to the daemon for instance {}({}).", + save_request.service_locations.len(), + instance.name, + instance.id, + ); + DAEMON_CLIENT + .clone() + .save_service_locations(save_request) + .await + .map_err(|err| { + error!( + "Error while saving service locations to the daemon for instance {}({}): {err}", + instance.name, instance.id, + ); + Error::InternalError(err.to_string()) + })?; + debug!( + "Saved service locations to the daemon for instance {}({}).", + instance.name, instance.id, + ); + } + + Ok(locations) +} + #[tauri::command(async)] pub async fn all_instances() -> Result>, Error> { debug!("Getting information about all instances."); @@ -307,7 +384,7 @@ pub async fn all_instances() -> Result>, Error> { let mut instance_info = Vec::new(); let connection_ids = get_connection_id_by_type(ConnectionType::Location).await; for instance in instances { - let locations = Location::find_by_instance_id(&*DB_POOL, instance.id).await?; + let locations = Location::find_by_instance_id(&*DB_POOL, instance.id, false).await?; let location_ids: Vec = locations.iter().map(|location| location.id).collect(); let connected = connection_ids .iter() @@ -323,7 +400,7 @@ pub async fn all_instances() -> Result>, Error> { proxy_url: instance.proxy_url, active: connected, pubkey: keys.pubkey, - disable_all_traffic: instance.disable_all_traffic, + client_traffic_policy: instance.client_traffic_policy, enterprise_enabled: instance.enterprise_enabled, openid_display_name: instance.openid_display_name, }); @@ -381,7 +458,7 @@ pub async fn all_locations(instance_id: Id) -> Result, Error> "Getting information about all locations for instance {}.", instance.name ); - let locations = Location::find_by_instance_id(&*DB_POOL, instance_id).await?; + let locations = Location::find_by_instance_id(&*DB_POOL, instance_id, false).await?; trace!( "Found {} locations for instance {instance} to return information about.", locations.len() @@ -471,7 +548,7 @@ pub(crate) async fn locations_changed( device_config: &DeviceConfigResponse, ) -> Result { let db_locations: HashSet> = - Location::find_by_instance_id(transaction.as_mut(), instance.id) + Location::find_by_instance_id(transaction.as_mut(), instance.id, true) .await? .into_iter() .map(|location| { @@ -506,15 +583,14 @@ pub(crate) async fn do_update_instance( instance.proxy_url = instance_info.proxy_url; instance.username = instance_info.username; // Make sure to update the locations too if we are disabling all traffic - if instance.disable_all_traffic != instance_info.disable_all_traffic - && instance_info.disable_all_traffic + let policy = instance_info.client_traffic_policy.into(); + if instance.client_traffic_policy != policy && policy == ClientTrafficPolicy::DisableAllTraffic { debug!("Disabling all traffic for all locations of instance {instance}"); Location::disable_all_traffic_for_all(transaction.as_mut(), instance.id).await?; debug!("Disabled all traffic for all locations of instance {instance}"); } - instance.disable_all_traffic = instance_info.disable_all_traffic; - instance.enterprise_enabled = instance_info.enterprise_enabled; + instance.client_traffic_policy = instance_info.client_traffic_policy.into(); instance.openid_display_name = instance_info.openid_display_name; instance.uuid = instance_info.id; // Token may be empty if it was not issued @@ -533,6 +609,8 @@ pub(crate) async fn do_update_instance( "A new base configuration has been applied to instance {instance}, even if nothing changed" ); + let mut service_locations = Vec::new(); + // check if locations have changed if locations_changed { // process locations received in response @@ -540,15 +618,15 @@ pub(crate) async fn do_update_instance( "Updating locations for instance {}({}).", instance.name, instance.id ); - // fetch existing locations for given instance + // Fetch existing locations for a given instance. let mut current_locations = - Location::find_by_instance_id(transaction.as_mut(), instance.id).await?; + Location::find_by_instance_id(transaction.as_mut(), instance.id, true).await?; for dev_config in response.configs { // parse device config let new_location = dev_config.into_location(instance.id); // check if location is already present in current locations - if let Some(position) = current_locations + let saved_location = if let Some(position) = current_locations .iter() .position(|loc| loc.network_id == new_location.network_id) { @@ -567,13 +645,24 @@ pub(crate) async fn do_update_instance( current_location.keepalive_interval = new_location.keepalive_interval; current_location.dns = new_location.dns; current_location.location_mfa_mode = new_location.location_mfa_mode; + current_location.service_location_mode = new_location.service_location_mode; current_location.save(transaction.as_mut()).await?; info!("Location {current_location} configuration updated for instance {instance}"); + current_location } else { // create new location debug!("Creating new location {new_location} for instance instance {instance}"); let new_location = new_location.save(transaction.as_mut()).await?; info!("New location {new_location} created for instance {instance}"); + new_location + }; + + if saved_location.is_service_location() { + debug!( + "Adding service location {}({}) for instance {}({}) to be saved to the daemon.", + saved_location.name, saved_location.id, instance.name, instance.id, + ); + service_locations.push(saved_location.to_service_location()?); } } @@ -590,6 +679,88 @@ pub(crate) async fn do_update_instance( } else { info!("Locations for instance {instance} didn't change. Not updating them."); } + + if service_locations.is_empty() { + debug!( + "No service locations for instance {}({}), removing all existing service locations connections if there are any.", + instance.name, instance.id + ); + + #[cfg(not(target_os = "macos"))] + { + let delete_request = DeleteServiceLocationsRequest { + instance_id: instance.uuid.clone(), + }; + DAEMON_CLIENT + .clone() + .delete_service_locations(delete_request) + .await + .map_err(|err| { + error!( + "Error while deleting service locations from the daemon for instance {}({}): {err}", + instance.name, instance.id, + ); + Error::InternalError(err.to_string()) + })?; + debug!( + "Successfully removed all service locations from daemon for instance {}({})", + instance.name, instance.id + ); + } + } else { + debug!( + "Processing {} service location(s) for instance {}({})", + service_locations.len(), + instance.name, + instance.id + ); + + #[cfg(not(target_os = "macos"))] + { + let private_key = WireguardKeys::find_by_instance_id(transaction.as_mut(), instance.id) + .await? + .ok_or(Error::NotFound)? + .prvkey; + + let save_request = SaveServiceLocationsRequest { + service_locations: service_locations.clone(), + instance_id: instance.uuid.clone(), + private_key, + }; + + debug!( + "Sending request to daemon to save {} service location(s) for instance {}({})", + save_request.service_locations.len(), + instance.name, + instance.id + ); + + DAEMON_CLIENT + .clone() + .save_service_locations(save_request) + .await + .map_err(|err| { + error!( + "Error while saving service locations to the daemon for instance {}({}): {err}", + instance.name, instance.id, + ); + Error::InternalError(err.to_string()) + })?; + + info!( + "Successfully saved {} service location(s) to daemon for instance {}({})", + service_locations.len(), + instance.name, + instance.id + ); + + debug!( + "Completed processing all service locations for instance {}({})", + instance.name, instance.id + ); + } + } + Ok(()) } @@ -758,11 +929,13 @@ pub async fn update_location_routing( match connection_type { ConnectionType::Location => { if let Some(mut location) = Location::find_by_id(&*DB_POOL, location_id).await? { - // Check if the instance has route_all_traffic disabled let instance = Instance::find_by_id(&*DB_POOL, location.instance_id) .await? .ok_or(Error::NotFound)?; - if instance.disable_all_traffic && route_all_traffic { + // Check if the instance has route_all_traffic disabled + if (instance.client_traffic_policy == ClientTrafficPolicy::DisableAllTraffic) + && route_all_traffic + { error!( "Couldn't update location routing: instance with id {} has \ route_all_traffic disabled.", @@ -772,6 +945,19 @@ pub async fn update_location_routing( "Instance has route_all_traffic disabled".into(), )); } + // Check if the instance has route_all_traffic enforced + if (instance.client_traffic_policy == ClientTrafficPolicy::ForceAllTraffic) + && !route_all_traffic + { + error!( + "Couldn't update location routing: instance with id {} has \ + route_all_traffic enforced.", + instance.id + ); + return Err(Error::InternalError( + "Instance has route_all_traffic enforced".into(), + )); + } location.route_all_traffic = route_all_traffic; location.save(&*DB_POOL).await?; @@ -800,6 +986,54 @@ pub async fn update_location_routing( } } +#[cfg(target_os = "macos")] +#[tauri::command(async)] +pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), Error> { + let app_state = handle.state::(); + let mut transaction = DB_POOL.begin().await?; + + let Some(instance) = Instance::find_by_id(&mut *transaction, instance_id).await? else { + error!("Couldn't delete instance: instance with ID {instance_id} could not be found."); + return Err(Error::NotFound); + }; + debug!("The instance that is being deleted has been identified as {instance}"); + + let instance_locations = + Location::find_by_instance_id(&mut *transaction, instance_id, false).await?; + if !instance_locations.is_empty() { + debug!( + "Found locations associated with the instance {instance}, closing their connections." + ); + } + for location in instance_locations { + if let Some(_connection) = app_state + .remove_connection(location.id, ConnectionType::Location) + .await + { + let result = stop_tunnel_for_location(&location); + error!("stop_tunnel() for location returned {result:?}"); + if !result { + return Err(Error::InternalError("Error from tunnel".into())); + } + remove_config_for_location(&location); + } + } + + instance.delete(&mut *transaction).await?; + + transaction.commit().await?; + + reload_tray_menu(&handle).await; + + let theme = { app_state.app_config.lock().unwrap().tray_theme }; + configure_tray_icon(&handle, theme).await?; + + handle.emit(EventKey::InstanceUpdate.into(), ())?; + info!("Successfully deleted instance {instance}."); + Ok(()) +} + +#[cfg(not(target_os = "macos"))] #[tauri::command(async)] pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), Error> { debug!("Deleting instance with ID {instance_id}"); @@ -813,7 +1047,8 @@ pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), E }; debug!("The instance that is being deleted has been identified as {instance}"); - let instance_locations = Location::find_by_instance_id(&mut *transaction, instance_id).await?; + let instance_locations = + Location::find_by_instance_id(&mut *transaction, instance_id, false).await?; if !instance_locations.is_empty() { debug!( "Found locations associated with the instance {instance}, closing their connections." @@ -851,8 +1086,24 @@ pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), E transaction.commit().await?; + client + .delete_service_locations(DeleteServiceLocationsRequest { + instance_id: instance.uuid.clone(), + }) + .await + .map_err(|err| { + error!( + "Error while deleting service locations from the daemon for instance {}({}): {err}", + instance.name, instance.id, + ); + Error::InternalError(err.to_string()) + })?; + reload_tray_menu(&handle).await; + let theme = { app_state.app_config.lock().unwrap().tray_theme }; + configure_tray_icon(&handle, theme).await?; + handle.emit(EventKey::InstanceUpdate.into(), ())?; info!("Successfully deleted instance {instance}."); Ok(()) @@ -871,7 +1122,7 @@ pub fn parse_tunnel_config(config: &str) -> Result { #[tauri::command(async)] pub async fn update_tunnel(mut tunnel: Tunnel, handle: AppHandle) -> Result<(), Error> { - debug!("Received tunnel configuration to update: {tunnel:?}"); + debug!("Received tunnel configuration to update: {tunnel}"); tunnel.save(&*DB_POOL).await?; info!("The tunnel {tunnel} configuration has been updated."); handle.emit(EventKey::LocationUpdate.into(), ())?; @@ -880,7 +1131,7 @@ pub async fn update_tunnel(mut tunnel: Tunnel, handle: AppHandle) -> Result< #[tauri::command(async)] pub async fn save_tunnel(tunnel: Tunnel, handle: AppHandle) -> Result<(), Error> { - debug!("Received tunnel configuration to save: {tunnel:?}"); + debug!("Received tunnel configuration to save: {tunnel}"); let tunnel = tunnel.save(&*DB_POOL).await?; info!("The tunnel {tunnel} configuration has been saved."); handle.emit(EventKey::LocationUpdate.into(), ())?; @@ -904,7 +1155,6 @@ pub async fn all_tunnels() -> Result>, Error> { let tunnels = Tunnel::all(&*DB_POOL).await?; trace!("Found ({}) tunnels to get information about", tunnels.len()); - trace!("Tunnels found: {tunnels:#?}"); let mut tunnel_info = Vec::new(); let active_tunnel_ids = get_connection_id_by_type(ConnectionType::Tunnel).await; @@ -944,7 +1194,6 @@ pub async fn tunnel_details(tunnel_id: Id) -> Result, Error> { pub async fn delete_tunnel(tunnel_id: Id, handle: AppHandle) -> Result<(), Error> { debug!("Deleting tunnel with ID {tunnel_id}"); let app_state = handle.state::(); - let mut client = DAEMON_CLIENT.clone(); let mut transaction = DB_POOL.begin().await?; let Some(tunnel) = Tunnel::find_by_id(&mut *transaction, tunnel_id).await? else { @@ -964,53 +1213,66 @@ pub async fn delete_tunnel(tunnel_id: Id, handle: AppHandle) -> Result<(), Error "Found active connection for tunnel {tunnel} which is being deleted, closing the \ connection." ); - if let Some(pre_down) = &tunnel.pre_down { - debug!( - "Executing defined PreDown command before removing the interface {} for the \ - tunnel {tunnel}: {pre_down}", - connection.interface_name - ); - let _ = execute_command(pre_down); - info!( - "Executed defined PreDown command before removing the interface {} for the \ - tunnel {tunnel}: {pre_down}", - connection.interface_name - ); + + #[cfg(target_os = "macos")] + { + remove_config_for_tunnel(&tunnel); } - let request = RemoveInterfaceRequest { - interface_name: connection.interface_name.clone(), - endpoint: tunnel.endpoint.clone(), - }; - client.remove_interface(request).await.map_err(|status| { - error!( - "An error occurred while removing interface {} for tunnel {tunnel}, status: \ - {status}", - connection.interface_name - ); - Error::InternalError(format!( - "An error occurred while removing interface {} for tunnel {tunnel}, error \ - message: {}. Check logs for more details.", - connection.interface_name, - status.message() - )) - })?; - info!( + + #[cfg(not(target_os = "macos"))] + { + if let Some(pre_down) = &tunnel.pre_down { + debug!( + "Executing defined PreDown command before removing the interface {} for the \ + tunnel {tunnel}: {pre_down}", + connection.interface_name + ); + let _ = execute_command(pre_down); + info!( + "Executed defined PreDown command before removing the interface {} for the \ + tunnel {tunnel}: {pre_down}", + connection.interface_name + ); + } + let request = RemoveInterfaceRequest { + interface_name: connection.interface_name.clone(), + endpoint: tunnel.endpoint.clone(), + }; + DAEMON_CLIENT + .clone() + .remove_interface(request) + .await + .map_err(|status| { + error!( + "An error occurred while removing interface {} for tunnel {tunnel}, \ + status: {status}", + connection.interface_name + ); + Error::InternalError(format!( + "An error occurred while removing interface {} for tunnel {tunnel}, error \ + message: {}. Check logs for more details.", + connection.interface_name, + status.message() + )) + })?; + info!( "Network interface {} has been removed and the connection to tunnel {tunnel} has been \ closed.", connection.interface_name ); - if let Some(post_down) = &tunnel.post_down { - debug!( - "Executing defined PostDown command after removing the interface {} for the \ + if let Some(post_down) = &tunnel.post_down { + debug!( + "Executing defined PostDown command after removing the interface {} for the \ tunnel {tunnel}: {post_down}", - connection.interface_name - ); - let _ = execute_command(post_down); - info!( - "Executed defined PostDown command after removing the interface {} for the \ + connection.interface_name + ); + let _ = execute_command(post_down); + info!( + "Executed defined PostDown command after removing the interface {} for the \ tunnel {tunnel}: {post_down}", - connection.interface_name - ); + connection.interface_name + ); + } } } tunnel.delete(&mut *transaction).await?; @@ -1120,3 +1382,26 @@ pub async fn command_set_app_config( } Ok(res) } + +#[tauri::command] +pub fn get_provisioning_config( + app_state: State<'_, AppState>, +) -> Result, Error> { + debug!("Running command get_provisioning_config."); + let res = app_state + .provisioning_config + .lock() + .map_err(|_err| { + error!("Failed to acquire lock on client provisioning config"); + Error::StateLockFail + })? + .clone(); + trace!("Returning config: {res:?}"); + Ok(res) +} + +#[tauri::command] +#[must_use] +pub fn get_platform_header() -> String { + construct_platform_header() +} diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index d6f59736..99866016 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -93,3 +93,13 @@ fn prepare_db_url() -> Result { )) } } + +pub async fn handle_db_migrations() { + debug!("Running database migrations, if there are any."); + sqlx::migrate!() + .run(&*DB_POOL) + .await + .expect("Failed to apply database migrations."); + debug!("Applied all database migrations that were pending. If any."); + debug!("Database setup has been completed successfully."); +} diff --git a/src-tauri/src/database/models/instance.rs b/src-tauri/src/database/models/instance.rs index a9aac5b6..0227afce 100644 --- a/src-tauri/src/database/models/instance.rs +++ b/src-tauri/src/database/models/instance.rs @@ -1,7 +1,7 @@ -use core::fmt; +use std::fmt; use serde::{Deserialize, Serialize}; -use sqlx::{query, query_as, SqliteExecutor}; +use sqlx::{prelude::Type, query, query_as, SqliteExecutor}; use super::{Id, NoId}; use crate::proto; @@ -15,7 +15,7 @@ pub struct Instance { pub proxy_url: String, pub username: String, pub token: Option, - pub disable_all_traffic: bool, + pub client_traffic_policy: ClientTrafficPolicy, pub enterprise_enabled: bool, pub openid_display_name: Option, } @@ -28,6 +28,7 @@ impl fmt::Display for Instance { impl From for Instance { fn from(instance_info: proto::InstanceInfo) -> Self { + let client_traffic_policy = ClientTrafficPolicy::from(&instance_info); Self { id: NoId, name: instance_info.name, @@ -36,7 +37,7 @@ impl From for Instance { proxy_url: instance_info.proxy_url, username: instance_info.username, token: None, - disable_all_traffic: instance_info.disable_all_traffic, + client_traffic_policy, enterprise_enabled: instance_info.enterprise_enabled, openid_display_name: instance_info.openid_display_name, } @@ -50,13 +51,15 @@ impl Instance { { query!( "UPDATE instance SET name = $1, uuid = $2, url = $3, proxy_url = $4, username = $5, \ - disable_all_traffic = $6, enterprise_enabled = $7, token = $8, openid_display_name = $9 WHERE id = $10;", + client_traffic_policy = $6, enterprise_enabled = $7, token = $8, \ + openid_display_name = $9 \ + WHERE id = $10;", self.name, self.uuid, self.url, self.proxy_url, self.username, - self.disable_all_traffic, + self.client_traffic_policy, self.enterprise_enabled, self.token, self.openid_display_name, @@ -67,14 +70,15 @@ impl Instance { Ok(()) } - pub(crate) async fn all<'e, E>(executor: E) -> Result, sqlx::Error> + pub async fn all<'e, E>(executor: E) -> Result, sqlx::Error> where E: SqliteExecutor<'e>, { let instances = query_as!( Self, "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", \ - disable_all_traffic, enterprise_enabled, openid_display_name FROM instance ORDER BY name ASC;" + client_traffic_policy, enterprise_enabled, openid_display_name \ + FROM instance ORDER BY name ASC;" ) .fetch_all(executor) .await?; @@ -88,7 +92,8 @@ impl Instance { let instance = query_as!( Self, "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token \"token?\", \ - disable_all_traffic, enterprise_enabled, openid_display_name FROM instance WHERE id = $1;", + client_traffic_policy, enterprise_enabled, openid_display_name \ + FROM instance WHERE id = $1;", id ) .fetch_optional(executor) @@ -122,7 +127,8 @@ impl Instance { let instances = query_as!( Self, "SELECT id \"id: _\", name, uuid, url, proxy_url, username, token, \ - disable_all_traffic, enterprise_enabled, openid_display_name FROM instance + client_traffic_policy, enterprise_enabled, openid_display_name \ + FROM instance \ WHERE token IS NOT NULL ORDER BY name ASC;" ) .fetch_all(executor) @@ -134,12 +140,13 @@ impl Instance { // This compares proto::InstanceInfo, not to be confused with regular InstanceInfo defined below impl PartialEq for Instance { fn eq(&self, other: &proto::InstanceInfo) -> bool { + let other_policy = ClientTrafficPolicy::from(other); self.name == other.name && self.uuid == other.id && self.url == other.url && self.proxy_url == other.proxy_url && self.username == other.username - && self.disable_all_traffic == other.disable_all_traffic + && self.client_traffic_policy == other_policy && self.enterprise_enabled == other.enterprise_enabled && self.openid_display_name == other.openid_display_name } @@ -154,7 +161,7 @@ impl Instance { let proxy_url = self.proxy_url.clone(); let result = query!( "INSERT INTO instance (name, uuid, url, proxy_url, username, token, \ - disable_all_traffic, enterprise_enabled) \ + client_traffic_policy , enterprise_enabled) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id;", self.name, self.uuid, @@ -162,7 +169,7 @@ impl Instance { proxy_url, self.username, self.token, - self.disable_all_traffic, + self.client_traffic_policy, self.enterprise_enabled ) .fetch_one(executor) @@ -175,7 +182,7 @@ impl Instance { proxy_url: self.proxy_url, username: self.username, token: self.token, - disable_all_traffic: self.disable_all_traffic, + client_traffic_policy: self.client_traffic_policy, enterprise_enabled: self.enterprise_enabled, openid_display_name: self.openid_display_name, }) @@ -191,7 +198,7 @@ pub struct InstanceInfo { pub proxy_url: String, pub active: bool, pub pubkey: String, - pub disable_all_traffic: bool, + pub client_traffic_policy: ClientTrafficPolicy, pub enterprise_enabled: bool, pub openid_display_name: Option, } @@ -201,3 +208,56 @@ impl fmt::Display for InstanceInfo { write!(f, "{}(ID: {})", self.name, self.id) } } + +/// Describes allowed traffic options for clients connecting to an instance. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] +#[repr(u32)] +#[serde(rename_all = "snake_case")] +pub enum ClientTrafficPolicy { + /// No restrictions + None = 0, + /// Clients are not allowed to route all traffic through the VPN. + DisableAllTraffic = 1, + /// Clients are forced to route all traffic through the VPN. + ForceAllTraffic = 2, +} + +/// Retrieves `ClientTrafficPolicy` from `proto::InstanceInfo` while ensuring backwards compatibility +impl From<&proto::InstanceInfo> for ClientTrafficPolicy { + fn from(instance: &proto::InstanceInfo) -> Self { + match ( + instance.client_traffic_policy, + #[allow(deprecated)] + instance.disable_all_traffic, + ) { + (Some(policy), _) => ClientTrafficPolicy::from(policy), + (None, true) => ClientTrafficPolicy::DisableAllTraffic, + (None, false) => ClientTrafficPolicy::None, + } + } +} + +impl From for ClientTrafficPolicy { + fn from(value: i32) -> Self { + match value { + 1 => ClientTrafficPolicy::DisableAllTraffic, + 2 => ClientTrafficPolicy::ForceAllTraffic, + _ => ClientTrafficPolicy::None, + } + } +} + +impl From> for ClientTrafficPolicy { + fn from(value: Option) -> Self { + match value { + None => Self::None, + Some(v) => Self::from(v), + } + } +} + +impl From for ClientTrafficPolicy { + fn from(value: i64) -> Self { + Self::from(value as i32) + } +} diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index 97d20dc6..fdf7f876 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -1,10 +1,25 @@ use std::fmt; +#[cfg(target_os = "macos")] +use std::net::IpAddr; +#[cfg(not(target_os = "macos"))] +use std::str::FromStr; +#[cfg(not(target_os = "macos"))] +use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration}; use serde::{Deserialize, Serialize}; use sqlx::{prelude::Type, query, query_as, query_scalar, Error as SqlxError, SqliteExecutor}; +#[cfg(not(target_os = "macos"))] +use super::wireguard_keys::WireguardKeys; use super::{Id, NoId}; -use crate::{error::Error, proto::LocationMfaMode as ProtoLocationMfaMode}; +#[cfg(not(target_os = "macos"))] +use crate::utils::{DEFAULT_ROUTE_IPV4, DEFAULT_ROUTE_IPV6}; +use crate::{ + error::Error, + proto::{ + LocationMfaMode as ProtoLocationMfaMode, ServiceLocationMode as ProtoServiceLocationMode, + }, +}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] #[repr(u32)] @@ -27,6 +42,27 @@ impl From for LocationMfaMode { } } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] +#[repr(u32)] +#[serde(rename_all = "lowercase")] +pub enum ServiceLocationMode { + Disabled = 1, + PreLogon = 2, + AlwaysOn = 3, +} + +impl From for ServiceLocationMode { + fn from(value: ProtoServiceLocationMode) -> Self { + match value { + ProtoServiceLocationMode::Unspecified | ProtoServiceLocationMode::Disabled => { + ServiceLocationMode::Disabled + } + ProtoServiceLocationMode::Prelogon => ServiceLocationMode::PreLogon, + ProtoServiceLocationMode::Alwayson => ServiceLocationMode::AlwaysOn, + } + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct Location { pub id: I, @@ -35,13 +71,14 @@ pub struct Location { pub network_id: Id, pub name: String, pub address: String, - pub pubkey: String, + pub pubkey: String, // Remote pub endpoint: String, pub allowed_ips: String, pub dns: Option, pub route_all_traffic: bool, pub keepalive_interval: i64, pub location_mfa_mode: LocationMfaMode, + pub service_location_mode: ServiceLocationMode, } impl fmt::Display for Location { @@ -57,17 +94,25 @@ impl fmt::Display for Location { } impl Location { - #[cfg(windows)] - pub(crate) async fn all<'e, E>(executor: E) -> Result, SqlxError> + /// Ignores service locations + #[cfg(any(windows, target_os = "macos"))] + pub(crate) async fn all<'e, E>( + executor: E, + include_service_locations: bool, + ) -> Result, SqlxError> where E: SqliteExecutor<'e>, { + let max_service_location_mode = + Self::get_service_location_mode_filter(include_service_locations); query_as!( Self, - "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,\ - route_all_traffic, keepalive_interval, \ - location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ - FROM location ORDER BY name ASC;" + "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,\ + route_all_traffic, keepalive_interval, \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ + FROM location WHERE service_location_mode <= $1 \ + ORDER BY name ASC;", + max_service_location_mode ) .fetch_all(executor) .await @@ -81,7 +126,7 @@ impl Location { query!( "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, \ endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, \ - keepalive_interval = $10, location_mfa_mode = $11 WHERE id = $12", + keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12 WHERE id = $13", self.instance_id, self.name, self.address, @@ -93,6 +138,7 @@ impl Location { self.route_all_traffic, self.keepalive_interval, self.location_mfa_mode, + self.service_location_mode, self.id, ) .execute(executor) @@ -112,7 +158,7 @@ impl Location { Self, "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ network_id, route_all_traffic, keepalive_interval, \ - location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM location WHERE id = $1", location_id ) @@ -123,16 +169,21 @@ impl Location { pub(crate) async fn find_by_instance_id<'e, E>( executor: E, instance_id: Id, + include_service_locations: bool, ) -> Result, SqlxError> where E: SqliteExecutor<'e>, { + let max_service_location_mode = + Self::get_service_location_mode_filter(include_service_locations); query_as!( Self, "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ - network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ - FROM location WHERE instance_id = $1 ORDER BY name ASC", - instance_id + network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ + FROM location WHERE instance_id = $1 AND service_location_mode <= $2 \ + ORDER BY name ASC", + instance_id, + max_service_location_mode ) .fetch_all(executor) .await @@ -148,7 +199,7 @@ impl Location { query_as!( Self, "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ - network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM location WHERE pubkey = $1;", pubkey ) @@ -189,6 +240,128 @@ impl Location { LocationMfaMode::Internal | LocationMfaMode::External => true, } } + + /// Split DNS settings into resolver IP addresses and search domains. + #[cfg(target_os = "macos")] + pub(crate) fn dns(&self) -> (Vec, Vec) { + let mut dns = Vec::new(); + let mut dns_search = Vec::new(); + + if let Some(dns_string) = &self.dns { + for entry in dns_string.split(',').map(str::trim) { + // Assume that every entry that can't be parsed as an IP address is a domain name. + if let Ok(ip) = entry.parse::() { + dns.push(ip); + } else { + dns_search.push(entry.into()); + } + } + } + + (dns, dns_search) + } + + #[cfg(not(target_os = "macos"))] + pub(crate) async fn interface_configuration<'e, E>( + &self, + executor: E, + interface_name: String, + preshared_key: Option, + mtu: Option, + ) -> Result + where + E: SqliteExecutor<'e>, + { + debug!("Looking for WireGuard keys for location {self} instance"); + let Some(keys) = WireguardKeys::find_by_instance_id(executor, self.instance_id).await? + else { + error!("No keys found for instance: {}", self.instance_id); + return Err(Error::InternalError( + "No keys found for instance".to_string(), + )); + }; + debug!("WireGuard keys found for location {self} instance"); + + // prepare peer config + debug!("Decoding location {self} public key: {}.", self.pubkey); + let peer_key = Key::from_str(&self.pubkey)?; + debug!("Location {self} public key decoded: {peer_key}"); + let mut peer = Peer::new(peer_key); + + debug!("Parsing location {self} endpoint: {}", self.endpoint); + peer.set_endpoint(&self.endpoint)?; + peer.persistent_keepalive_interval = Some(25); + debug!("Parsed location {self} endpoint: {}", self.endpoint); + + if let Some(psk) = preshared_key { + debug!("Decoding location {self} preshared key."); + let peer_psk = Key::from_str(&psk)?; + info!("Location {self} preshared key decoded."); + peer.preshared_key = Some(peer_psk); + } + + debug!("Parsing location {self} allowed IPs: {}", self.allowed_ips); + let allowed_ips = if self.route_all_traffic { + debug!("Using all traffic routing for location {self}"); + vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()] + } else { + debug!( + "Using predefined location {self} traffic: {}", + self.allowed_ips + ); + self.allowed_ips.split(',').map(str::to_string).collect() + }; + for allowed_ip in &allowed_ips { + match IpAddrMask::from_str(allowed_ip) { + Ok(addr) => { + peer.allowed_ips.push(addr); + } + Err(err) => { + // Handle the error from IpAddrMask::from_str, if needed + error!( + "Error parsing IP address {allowed_ip} while setting up interface for \ + location {self}, error details: {err}" + ); + } + } + } + debug!( + "Parsed allowed IPs for location {self}: {:?}", + peer.allowed_ips + ); + + let addresses = self + .address + .split(',') + .map(str::trim) + .map(IpAddrMask::from_str) + .collect::>() + .map_err(|err| { + let msg = format!("Failed to parse IP addresses '{}': {err}", self.address); + error!("{msg}"); + Error::InternalError(msg) + })?; + let interface_config = InterfaceConfiguration { + name: interface_name, + prvkey: keys.prvkey, + addresses, + port: 0, + peers: vec![peer], + mtu, + }; + + Ok(interface_config) + } + + /// Returns a filter value that can be used in SQL queries like `service_location_mode <= ?` when querying locations + /// to exclude (<= 1) or include service locations (all service locations modes). + fn get_service_location_mode_filter(include_service_locations: bool) -> i32 { + if include_service_locations { + i32::MAX + } else { + ServiceLocationMode::Disabled as i32 + } + } } impl Location { @@ -199,8 +372,8 @@ impl Location { // Insert a new record when there is no ID let id = query_scalar!( "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, \ - dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \ + dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) \ RETURNING id \"id!\"", self.instance_id, self.name, @@ -212,7 +385,8 @@ impl Location { self.network_id, self.route_all_traffic, self.keepalive_interval, - self.location_mfa_mode + self.location_mfa_mode, + self.service_location_mode, ) .fetch_one(executor) .await?; @@ -230,10 +404,18 @@ impl Location { route_all_traffic: self.route_all_traffic, keepalive_interval: self.keepalive_interval, location_mfa_mode: self.location_mfa_mode, + service_location_mode: self.service_location_mode, }) } } +impl Location { + pub fn is_service_location(&self) -> bool { + self.service_location_mode != ServiceLocationMode::Disabled + && self.location_mfa_mode == LocationMfaMode::Disabled + } +} + impl From> for Location { fn from(location: Location) -> Self { Self { @@ -249,6 +431,7 @@ impl From> for Location { route_all_traffic: location.route_all_traffic, keepalive_interval: location.keepalive_interval, location_mfa_mode: location.location_mfa_mode, + service_location_mode: location.service_location_mode, } } } diff --git a/src-tauri/src/database/models/location_stats.rs b/src-tauri/src/database/models/location_stats.rs index 3401487b..506df9f9 100644 --- a/src-tauri/src/database/models/location_stats.rs +++ b/src-tauri/src/database/models/location_stats.rs @@ -3,7 +3,7 @@ use std::time::SystemTime; use chrono::{NaiveDateTime, Utc}; use defguard_wireguard_rs::host::Peer; use serde::{Deserialize, Serialize}; -use sqlx::{query, query_as, query_scalar, Error as SqlxError, SqliteExecutor}; +use sqlx::{query, query_as, query_scalar, SqliteExecutor}; use super::{location::Location, Id, NoId, PURGE_DURATION}; use crate::{commands::DateTimeAggregation, error::Error, CommonLocationStats, ConnectionType}; @@ -59,7 +59,8 @@ where } impl LocationStats { - pub(crate) async fn get_name<'e, E>(&self, executor: E) -> Result + #[cfg(not(target_os = "macos"))] + pub(crate) async fn get_name<'e, E>(&self, executor: E) -> Result where E: SqliteExecutor<'e>, { diff --git a/src-tauri/src/database/models/tunnel.rs b/src-tauri/src/database/models/tunnel.rs index 974861c7..7522e7c7 100644 --- a/src-tauri/src/database/models/tunnel.rs +++ b/src-tauri/src/database/models/tunnel.rs @@ -1,5 +1,6 @@ -use core::fmt; -use std::time::SystemTime; +#[cfg(target_os = "macos")] +use std::net::IpAddr; +use std::{fmt, time::SystemTime}; use chrono::{NaiveDateTime, Utc}; use defguard_wireguard_rs::host::Peer; @@ -14,13 +15,13 @@ use crate::{ }; #[serde_as] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct Tunnel { pub id: I, pub name: String, - // user keys - pub pubkey: String, - pub prvkey: String, + // encryption keys + pub pubkey: String, // Remote + pub prvkey: String, // Local // server config pub address: String, pub server_pubkey: String, @@ -32,7 +33,7 @@ pub struct Tunnel { pub endpoint: String, #[serde_as(as = "NoneAsEmptyString")] pub dns: Option, - pub persistent_keep_alive: i64, // New field + pub persistent_keep_alive: i64, pub route_all_traffic: bool, // additional commands #[serde_as(as = "NoneAsEmptyString")] @@ -51,6 +52,12 @@ impl fmt::Display for Tunnel { } } +impl fmt::Display for Tunnel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + impl Tunnel { pub(crate) async fn save<'e, E>(&mut self, executor: E) -> Result<(), SqlxError> where @@ -247,10 +254,32 @@ impl Tunnel { } } +impl Tunnel { + /// Split DNS settings into resolver IP addresses and search domains. + #[cfg(target_os = "macos")] + pub(crate) fn dns(&self) -> (Vec, Vec) { + let mut dns = Vec::new(); + let mut dns_search = Vec::new(); + + if let Some(dns_string) = &self.dns { + for entry in dns_string.split(',').map(str::trim) { + // Assume that every entry that can't be parsed as an IP address is a domain name. + if let Ok(ip) = entry.parse::() { + dns.push(ip); + } else { + dns_search.push(entry.into()); + } + } + } + + (dns, dns_search) + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct TunnelStats { id: I, - pub tunnel_id: Id, + pub(crate) tunnel_id: Id, upload: i64, download: i64, pub(crate) last_handshake: i64, diff --git a/src-tauri/src/database/models/wireguard_keys.rs b/src-tauri/src/database/models/wireguard_keys.rs index 38a27273..dce89689 100644 --- a/src-tauri/src/database/models/wireguard_keys.rs +++ b/src-tauri/src/database/models/wireguard_keys.rs @@ -5,7 +5,6 @@ use x25519_dalek::{PublicKey, StaticSecret}; use super::{Id, NoId}; // User key pair -#[derive(Debug)] pub struct WireguardKeys { pub id: I, pub instance_id: Id, diff --git a/src-tauri/src/enterprise/mod.rs b/src-tauri/src/enterprise/mod.rs index f9a20825..8e1f8e8a 100644 --- a/src-tauri/src/enterprise/mod.rs +++ b/src-tauri/src/enterprise/mod.rs @@ -1,2 +1,4 @@ pub mod models; pub mod periodic; +pub mod provisioning; +pub mod service_locations; diff --git a/src-tauri/src/enterprise/models/instance.rs b/src-tauri/src/enterprise/models/instance.rs index 6da45fa0..b0a03437 100644 --- a/src-tauri/src/enterprise/models/instance.rs +++ b/src-tauri/src/enterprise/models/instance.rs @@ -1,7 +1,10 @@ use sqlx::SqliteExecutor; use crate::{ - database::models::{instance::Instance, Id}, + database::models::{ + instance::{ClientTrafficPolicy, Instance}, + Id, + }, error::Error, }; @@ -14,8 +17,7 @@ impl Instance { "Disabling enterprise features for instance {}({})", self.name, self.id ); - self.enterprise_enabled = false; - self.disable_all_traffic = false; + self.client_traffic_policy = ClientTrafficPolicy::None; self.save(executor).await?; debug!( "Disabled enterprise features for instance {}({})", diff --git a/src-tauri/src/enterprise/periodic/config.rs b/src-tauri/src/enterprise/periodic/config.rs index 46b87acf..935953c9 100644 --- a/src-tauri/src/enterprise/periodic/config.rs +++ b/src-tauri/src/enterprise/periodic/config.rs @@ -22,7 +22,9 @@ use crate::{ error::Error, events::EventKey, proto::{DeviceConfigResponse, InstanceInfoRequest, InstanceInfoResponse}, - MIN_CORE_VERSION, MIN_PROXY_VERSION, + utils::construct_platform_header, + CLIENT_PLATFORM_HEADER, CLIENT_VERSION_HEADER, MIN_CORE_VERSION, MIN_PROXY_VERSION, + PKG_VERSION, }; const INTERVAL_SECONDS: Duration = Duration::from_secs(30); @@ -137,6 +139,8 @@ pub async fn poll_instance( let response = Client::new() .post(url) .json(&request) + .header(CLIENT_VERSION_HEADER, PKG_VERSION) + .header(CLIENT_PLATFORM_HEADER, construct_platform_header()) .timeout(HTTP_REQ_TIMEOUT) .send() .await; @@ -265,7 +269,7 @@ fn build_request(instance: &Instance) -> Result let token = instance.token.as_ref().ok_or_else(|| Error::NoToken)?; Ok(InstanceInfoRequest { - token: (*token).to_string(), + token: (*token).clone(), }) } diff --git a/src-tauri/src/enterprise/provisioning/mod.rs b/src-tauri/src/enterprise/provisioning/mod.rs new file mode 100644 index 00000000..2eabf0b8 --- /dev/null +++ b/src-tauri/src/enterprise/provisioning/mod.rs @@ -0,0 +1,107 @@ +use std::{fmt, fs, path::Path}; + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Manager}; + +use crate::database::{models::instance::Instance, DB_POOL}; + +const CONFIG_FILE_NAME: &str = "provisioning.json"; + +#[derive(Clone, Deserialize, Serialize)] +pub struct ProvisioningConfig { + pub enrollment_url: String, + pub enrollment_token: String, +} + +impl fmt::Debug for ProvisioningConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + enrollment_url, + enrollment_token: _, + } = self; + + f.debug_struct("ProvisioningConfig") + .field("enrollment_url", enrollment_url) + .field("enrollment_token", &"***") + .finish() + } +} + +impl ProvisioningConfig { + /// Load configuration from a file at `path`. + fn load(path: &Path) -> Option { + // read content to string first to handle Windows encoding issues + let file_content = match fs::read_to_string(path) { + Ok(content) => content, + Err(err) => { + warn!( + "Failed to open provisioning configuration file at {path:?}. Error details: \ + {err}" + ); + return None; + } + }; + + // strip Windows BOM manually + let file_content = file_content.trim_start_matches('\u{FEFF}'); + + match serde_json::from_str::(file_content) { + Ok(config) => Some(config), + Err(err) => { + warn!( + "Failed to parse provisioning configuration file at {path:?}. Error details: \ + {err}" + ); + None + } + } + } +} + +#[must_use] +pub fn try_get_provisioning_config(app_data_dir: &Path) -> Option { + debug!("Trying to find provisioning config in {app_data_dir:?}"); + + let config_file_path = app_data_dir.join(CONFIG_FILE_NAME); + ProvisioningConfig::load(&config_file_path) +} + +/// Checks if the client has already been initialized +/// and tries to load provisioning config from file if necessary +pub async fn handle_client_initialization(app_handle: &AppHandle) -> Option { + // check if client has already been initialized + // we assume that if any instances exist the client has been initialized + match Instance::all(&*DB_POOL).await { + Ok(instances) => { + if instances.is_empty() { + debug!( + "Client has not been initialized yet. Checking if provisioning config exists" + ); + let data_dir = app_handle + .path() + .app_data_dir() + .unwrap_or_else(|_| "UNDEFINED DATA DIRECTORY".into()); + match try_get_provisioning_config(&data_dir) { + Some(config) => { + info!( + "Provisioning config found in {}: {config:?}", + data_dir.display() + ); + return Some(config); + } + None => { + debug!( + "Provisioning config not found in {}. Proceeding with normal startup.", + data_dir.display() + ); + } + } + } + } + Err(err) => { + error!("Failed to verify if the client has already been initialized: {err}"); + } + } + + None +} diff --git a/src-tauri/src/enterprise/service_locations/mod.rs b/src-tauri/src/enterprise/service_locations/mod.rs new file mode 100644 index 00000000..b8dffa4c --- /dev/null +++ b/src-tauri/src/enterprise/service_locations/mod.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; + +use defguard_wireguard_rs::{error::WireguardInterfaceError, WGApi}; +use serde::{Deserialize, Serialize}; + +use crate::{ + database::models::{ + location::{Location, ServiceLocationMode}, + Id, + }, + service::proto::ServiceLocation, +}; + +#[cfg(windows)] +pub mod windows; + +#[derive(Debug, thiserror::Error)] +pub enum ServiceLocationError { + #[error("Error occurred while initializing service location API: {0}")] + InitError(String), + #[error("Failed to load service location storage: {0}")] + LoadError(String), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + DecodeError(#[from] base64::DecodeError), + #[error(transparent)] + WireGuardError(#[from] WireguardInterfaceError), + #[error(transparent)] + AddrParseError(#[from] defguard_wireguard_rs::net::IpAddrParseError), + #[error("WireGuard interface error: {0}")] + InterfaceError(String), + #[error(transparent)] + JsonError(#[from] serde_json::Error), + #[error(transparent)] + ProtoEnumError(#[from] prost::UnknownEnumValue), + #[cfg(windows)] + #[error(transparent)] + WindowsServiceError(#[from] windows_service::Error), +} + +#[allow(dead_code)] +#[derive(Default)] +pub(crate) struct ServiceLocationManager { + // Interface name: WireGuard API instance + wgapis: HashMap, + // Instance ID: Service locations connected under that instance + connected_service_locations: HashMap>, +} + +#[allow(dead_code)] +#[derive(Serialize, Deserialize)] +pub(crate) struct ServiceLocationData { + pub service_locations: Vec, + pub instance_id: String, + pub private_key: String, +} + +#[allow(dead_code)] +pub(crate) struct SingleServiceLocationData { + pub service_location: ServiceLocation, + pub instance_id: String, + pub private_key: String, +} + +impl std::fmt::Debug for ServiceLocationData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ServiceLocationData") + .field("service_locations", &self.service_locations) + .field("instance_id", &self.instance_id) + .field("private_key", &"***") + .finish() + } +} + +impl std::fmt::Debug for SingleServiceLocationData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SingleServiceLocationData") + .field("service_locations", &self.service_location) + .field("instance_id", &self.instance_id) + .field("private_key", &"***") + .finish() + } +} + +impl Location { + pub fn to_service_location(&self) -> Result { + if !self.is_service_location() { + warn!("Location {self} is not a service location, so it can't be converted to one."); + return Err(crate::error::Error::ConversionError(format!( + "Failed to convert location {self} to a service location as it's either not marked as one or has MFA enabled." + ))); + } + + let mode = match self.service_location_mode { + ServiceLocationMode::Disabled => { + warn!( + "Location {self} has an invalid service location mode, so it can't be converted to one." + ); + return Err( + crate::error::Error::ConversionError(format!("Location {} has an invalid service location mode ({:?}), so it can't be converted to one.", self, self.service_location_mode)) + ); + } + ServiceLocationMode::PreLogon => 0, + ServiceLocationMode::AlwaysOn => 1, + }; + + Ok(ServiceLocation { + name: self.name.clone(), + address: self.address.clone(), + pubkey: self.pubkey.clone(), + endpoint: self.endpoint.clone(), + allowed_ips: self.allowed_ips.clone(), + dns: self.dns.clone().unwrap_or_default(), + keepalive_interval: self.keepalive_interval.try_into().unwrap_or(0), + mode, + }) + } +} diff --git a/src-tauri/src/enterprise/service_locations/windows.rs b/src-tauri/src/enterprise/service_locations/windows.rs new file mode 100644 index 00000000..53f6ce67 --- /dev/null +++ b/src-tauri/src/enterprise/service_locations/windows.rs @@ -0,0 +1,890 @@ +use std::{ + collections::HashMap, + fs::{self, create_dir_all}, + net::IpAddr, + path::PathBuf, + result::Result, + str::FromStr, + sync::{Arc, RwLock}, + time::Duration, +}; + +use common::{find_free_tcp_port, get_interface_name}; +use defguard_wireguard_rs::{ + host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WireguardInterfaceApi, +}; +use known_folders::get_known_folder_path; +use log::{debug, error, warn}; +use windows::{ + core::PSTR, + Win32::System::RemoteDesktop::{ + self, WTSQuerySessionInformationA, WTSWaitSystemEvent, WTS_CURRENT_SERVER_HANDLE, + WTS_EVENT_LOGOFF, WTS_EVENT_LOGON, WTS_SESSION_INFOA, + }, +}; +use windows_acl::acl::ACL; + +use crate::{ + enterprise::service_locations::{ + ServiceLocationData, ServiceLocationError, ServiceLocationManager, + SingleServiceLocationData, + }, + service::{ + daemon::setup_wgapi, + proto::{ServiceLocation, ServiceLocationMode}, + }, +}; + +const LOGIN_LOGOFF_EVENT_RETRY_DELAY_SECS: u64 = 5; +const DEFAULT_WIREGUARD_PORT: u16 = 51820; +const DEFGUARD_DIR: &str = "Defguard"; +const SERVICE_LOCATIONS_SUBDIR: &str = "service_locations"; + +pub(crate) async fn watch_for_login_logoff( + service_location_manager: Arc>, +) -> Result<(), ServiceLocationError> { + loop { + let mut event_flags = 0; + let success = unsafe { + WTSWaitSystemEvent( + Some(WTS_CURRENT_SERVER_HANDLE), + WTS_EVENT_LOGON | WTS_EVENT_LOGOFF, + &mut event_flags, + ) + }; + + match success { + Ok(_) => { + debug!("Waiting for system event returned with event_flags: 0x{event_flags:x}"); + } + Err(err) => { + error!("Failed waiting for login/logoff event: {err:?}"); + tokio::time::sleep(Duration::from_secs(LOGIN_LOGOFF_EVENT_RETRY_DELAY_SECS)).await; + continue; + } + }; + + if event_flags & WTS_EVENT_LOGON != 0 { + debug!("Detected user logon, attempting to auto-disconnect from service locations."); + service_location_manager + .clone() + .write() + .unwrap() + .disconnect_service_locations(Some(ServiceLocationMode::PreLogon))?; + } + if event_flags & WTS_EVENT_LOGOFF != 0 { + debug!("Detected user logoff, attempting to auto-connect to service locations."); + service_location_manager + .clone() + .write() + .unwrap() + .connect_to_service_locations()?; + } + } +} + +fn get_shared_directory() -> Result { + match get_known_folder_path(known_folders::KnownFolder::ProgramData) { + Some(mut path) => { + path.push(DEFGUARD_DIR); + path.push(SERVICE_LOCATIONS_SUBDIR); + Ok(path) + } + None => Err(ServiceLocationError::LoadError( + "Could not find ProgramData known folder".to_string(), + )), + } +} + +fn set_protected_acls(path: &str) -> Result<(), ServiceLocationError> { + debug!("Setting secure ACLs on: {path}"); + + const SYSTEM_SID: &str = "S-1-5-18"; // NT AUTHORITY\SYSTEM + const ADMINISTRATORS_SID: &str = "S-1-5-32-544"; // BUILTIN\Administrators + + const FILE_ALL_ACCESS: u32 = 0x1F01FF; + + match ACL::from_file_path(path, false) { + Ok(mut acl) => { + // Remove everything else from access + debug!("Removing all existing ACL entries for {path}"); + let all_entries = acl.all().map_err(|e| { + ServiceLocationError::LoadError(format!("Failed to get ACL entries: {e}")) + })?; + + for entry in all_entries { + if let Some(sid) = entry.sid { + if let Err(e) = acl.remove(sid.as_ptr() as *mut _, None, None) { + debug!("Note: Could not remove ACL entry (might be expected): {e}"); + } + } + } + + debug!("Cleared existing ACL entries, now adding secure entries"); + + // Add SYSTEM with full control + debug!("Adding SYSTEM with full control"); + let system_sid_result = windows_acl::helper::string_to_sid(SYSTEM_SID); + match system_sid_result { + Ok(system_sid) => { + acl.allow(system_sid.as_ptr() as *mut _, true, FILE_ALL_ACCESS) + .map_err(|e| { + ServiceLocationError::LoadError(format!( + "Failed to add SYSTEM ACL: {e}" + )) + })?; + } + Err(e) => { + return Err(ServiceLocationError::LoadError(format!( + "Failed to convert SYSTEM SID: {e}" + ))); + } + } + + // Add Administrators with full control + debug!("Adding Administrators with full control"); + let admin_sid_result = windows_acl::helper::string_to_sid(ADMINISTRATORS_SID); + match admin_sid_result { + Ok(admin_sid) => { + acl.allow(admin_sid.as_ptr() as *mut _, true, FILE_ALL_ACCESS) + .map_err(|e| { + ServiceLocationError::LoadError(format!( + "Failed to add Administrators ACL: {e}" + )) + })?; + } + Err(e) => { + return Err(ServiceLocationError::LoadError(format!( + "Failed to convert Administrators SID: {e}" + ))); + } + } + + debug!("Successfully set secure ACLs on {path} for SYSTEM and Administrators"); + Ok(()) + } + Err(e) => { + error!("Failed to get ACL for {path}: {e}"); + Err(ServiceLocationError::LoadError(format!( + "Failed to get ACL for {path}: {e}" + ))) + } + } +} + +fn get_instance_file_path(instance_id: &str) -> Result { + let mut path = get_shared_directory()?; + path.push(format!("{instance_id}.json")); + Ok(path) +} + +pub(crate) fn is_user_logged_in() -> bool { + debug!("Starting checking if user is logged in..."); + + unsafe { + let mut pp_sessions: *mut WTS_SESSION_INFOA = std::ptr::null_mut(); + let mut count: u32 = 0; + + debug!("Calling WTSEnumerateSessionsA..."); + let ret = RemoteDesktop::WTSEnumerateSessionsA(None, 0, 1, &mut pp_sessions, &mut count); + + match ret { + Ok(_) => { + debug!("WTSEnumerateSessionsA succeeded, found {count} sessions"); + let sessions = std::slice::from_raw_parts(pp_sessions, count as usize); + + for (index, session) in sessions.iter().enumerate() { + debug!( + "Session {index}: SessionId={}, State={:?}, WinStationName={:?}", + session.SessionId, + session.State, + std::ffi::CStr::from_ptr(session.pWinStationName.0 as *const i8) + .to_string_lossy() + ); + + if session.State == windows::Win32::System::RemoteDesktop::WTSActive { + let mut buffer = PSTR::null(); + let mut bytes_returned: u32 = 0; + + let result = WTSQuerySessionInformationA( + None, + session.SessionId, + windows::Win32::System::RemoteDesktop::WTSUserName, + &mut buffer, + &mut bytes_returned, + ); + + match result { + Ok(_) => { + if !buffer.is_null() { + let username = std::ffi::CStr::from_ptr(buffer.0 as *const i8) + .to_string_lossy() + .into_owned(); + + debug!( + "Found session {} username: {username}", + session.SessionId + ); + + windows::Win32::System::RemoteDesktop::WTSFreeMemory( + buffer.0 as *mut _, + ); + + // We found an active session with a username + return true; + } + } + Err(err) => { + debug!( + "Failed to get username for session {}: {err:?}", + session.SessionId + ); + } + } + } + } + windows::Win32::System::RemoteDesktop::WTSFreeMemory(pp_sessions as _); + debug!("No active sessions found"); + } + Err(err) => { + error!("Failed to enumerate user sessions: {err:?}"); + debug!("WTSEnumerateSessionsA failed: {err:?}"); + } + } + } + + debug!("User is not logged in."); + false +} + +impl ServiceLocationManager { + pub fn init() -> Result { + debug!("Initializing ServiceLocationApi"); + let path = get_shared_directory()?; + + debug!("Creating directory: {path:?}"); + create_dir_all(&path)?; + + if let Some(path_str) = path.to_str() { + debug!("Setting ACLs on service locations directory"); + if let Err(e) = set_protected_acls(path_str) { + warn!("Failed to set ACLs on service locations directory: {e}. Continuing anyway."); + } + } else { + warn!("Failed to convert path to string for ACL setting"); + } + + let manager = Self { + wgapis: HashMap::new(), + connected_service_locations: HashMap::new(), + }; + + debug!("ServiceLocationApi initialized successfully"); + Ok(manager) + } + + /// Check if a specific service location is already connected + fn is_service_location_connected(&self, instance_id: &str, location_pubkey: &str) -> bool { + if let Some(locations) = self.connected_service_locations.get(instance_id) { + for location in locations { + if location.pubkey == location_pubkey { + return true; + } + } + } + false + } + + /// Add a connected service location + fn add_connected_service_location( + &mut self, + instance_id: &str, + location: &ServiceLocation, + ) -> Result<(), ServiceLocationError> { + self.connected_service_locations + .entry(instance_id.to_string()) + .or_default() + .push(location.clone()); + + debug!( + "Added connected service location for instance '{instance_id}', location '{}'", + location.name + ); + Ok(()) + } + + /// Remove connected service locations by filter (write disk-first, then memory) + fn remove_connected_service_locations( + &mut self, + filter: F, + ) -> Result<(), ServiceLocationError> + where + F: Fn(&str, &ServiceLocation) -> bool, + { + // Iterate through connected_service_locations and remove matching locations + let mut instances_to_remove = Vec::new(); + + for (instance_id, locations) in self.connected_service_locations.iter_mut() { + locations.retain(|location| !filter(instance_id, location)); + + // Mark instance for removal if it has no more locations + if locations.is_empty() { + instances_to_remove.push(instance_id.clone()); + } + } + + // Remove instances with no locations + for instance_id in instances_to_remove { + self.connected_service_locations.remove(&instance_id); + } + + debug!("Removed connected service locations matching filter"); + Ok(()) + } + + // Resets the state of the service location: + // 1. If it's an always on location, disconnects and reconnects it. + // 2. Otherwise, just disconnects it if the user is not logged in. + pub(crate) fn reset_service_location_state( + &mut self, + instance_id: &str, + location_pubkey: &str, + ) -> Result<(), ServiceLocationError> { + debug!( + "Reseting the state of service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + ); + + let service_location_data = self + .load_service_location(instance_id, location_pubkey)? + .ok_or_else(|| { + ServiceLocationError::LoadError(format!( + "Service location with pubkey {} for instance {} not found", + location_pubkey, instance_id + )) + })?; + + debug!( + "Disconnecting service location for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + service_location_data.service_location.name + ); + + self.disconnect_service_location(instance_id, location_pubkey)?; + + debug!( + "Disconnected service location for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + service_location_data.service_location.name + ); + + debug!( + "Reconnecting service location if needed for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + service_location_data.service_location.name + ); + + // We should reconnect only if: + // 1. It's an always on location + // 2. It's a pre-logon location and the user is not logged in + if service_location_data.service_location.mode == ServiceLocationMode::AlwaysOn as i32 + || (service_location_data.service_location.mode == ServiceLocationMode::PreLogon as i32 + && !is_user_logged_in()) + { + debug!( + "Reconnecting service location for instance_id: {instance_id}, location_pubkey: {location_pubkey} ({})", + service_location_data.service_location.name + ); + self.connect_to_service_location(&service_location_data)?; + } + + debug!("Service location state reset completed."); + + Ok(()) + } + + pub(crate) fn disconnect_service_locations_by_instance( + &mut self, + instance_id: &str, + ) -> Result<(), ServiceLocationError> { + debug!("Disconnecting all service locations for instance_id: {instance_id}"); + + if let Some(locations) = self.connected_service_locations.get(instance_id) { + // Collect locations to disconnect to avoid borrowing issues + let locations_to_disconnect = locations.to_vec(); + + for location in locations_to_disconnect { + let ifname = get_interface_name(&location.name); + debug!("Tearing down interface: {ifname}"); + if let Some(mut wgapi) = self.wgapis.remove(&ifname) { + if let Err(err) = wgapi.remove_interface() { + error!("Failed to remove interface {ifname}: {err}"); + } else { + debug!("Interface {ifname} removed successfully"); + } + debug!( + "Removing connected service location for instance_id: {instance_id}, location_pubkey: {}", + location.pubkey + ); + debug!( + "Disconnected service location for instance_id: {instance_id}, location_pubkey: {}", + location.pubkey + ); + } else { + error!("Failed to find WireGuard API for interface {ifname}"); + } + } + + self.connected_service_locations.remove(instance_id); + } else { + debug!( + "No connected service locations found for instance_id: {instance_id}. Skipping disconnect" + ); + return Ok(()); + } + + debug!("Disconnected all service locations for instance_id: {instance_id}"); + + Ok(()) + } + + pub(crate) fn disconnect_service_location( + &mut self, + instance_id: &str, + location_pubkey: &str, + ) -> Result<(), ServiceLocationError> { + debug!( + "Disconnecting service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + ); + + if let Some(locations) = self.connected_service_locations.get_mut(instance_id) { + if let Some(pos) = locations + .iter() + .position(|loc| loc.pubkey == location_pubkey) + { + let location = locations.remove(pos); + let ifname = get_interface_name(&location.name); + debug!("Tearing down interface: {ifname}"); + if let Some(mut wgapi) = self.wgapis.remove(&ifname) { + if let Err(err) = wgapi.remove_interface() { + error!("Failed to remove interface {ifname}: {err}"); + } else { + debug!("Interface {ifname} removed successfully."); + } + } else { + error!("Failed to find WireGuard API for interface {ifname}. "); + } + } else { + debug!( + "Service location with pubkey {location_pubkey} for instance {instance_id} is not connected, skipping disconnect" + ); + return Ok(()); + } + } else { + debug!( + "No connected service locations found for instance_id: {instance_id}, skipping disconnect" + ); + return Ok(()); + } + + debug!( + "Disconnected service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + ); + + Ok(()) + } + + /// Helper function to setup a WireGuard interface for a service location + fn setup_service_location_interface( + &mut self, + location: &ServiceLocation, + private_key: &str, + ) -> Result<(), ServiceLocationError> { + let peer_key = Key::from_str(&location.pubkey)?; + + let mut peer = Peer::new(peer_key.clone()); + peer.set_endpoint(&location.endpoint)?; + + peer.persistent_keepalive_interval = location.keepalive_interval.try_into().ok(); + + let allowed_ips = location + .allowed_ips + .split(',') + .map(str::to_string) + .collect::>(); + + for allowed_ip in &allowed_ips { + match IpAddrMask::from_str(allowed_ip) { + Ok(addr) => { + peer.allowed_ips.push(addr); + } + Err(err) => { + error!( + "Error parsing IP address {allowed_ip} while setting up interface for \ + location {location:?}, error details: {err}" + ); + } + } + } + + let mut addresses = Vec::new(); + + for address in location.address.split(',') { + addresses.push(IpAddrMask::from_str(address.trim())?); + } + + let config = InterfaceConfiguration { + name: location.name.clone(), + prvkey: private_key.to_string(), + addresses, + port: find_free_tcp_port().unwrap_or(DEFAULT_WIREGUARD_PORT), + peers: vec![peer.clone()], + mtu: None, + }; + + let ifname = location.name.clone(); + let ifname = get_interface_name(&ifname); + let mut wgapi = match setup_wgapi(&ifname) { + Ok(api) => api, + Err(err) => { + let msg = format!("Failed to setup WireGuard API for interface {ifname}: {err:?}"); + debug!("{msg}"); + return Err(ServiceLocationError::InterfaceError(msg)); + } + }; + + wgapi.create_interface()?; + + // Extract DNS configuration if available + let dns_string = location.dns.clone(); + let dns_entries = dns_string.split(',').map(str::trim).collect::>(); + // We assume that every entry that can't be parsed as an IP address is a domain name. + let mut dns = Vec::new(); + let mut search_domains = Vec::new(); + for entry in dns_entries { + if let Ok(ip) = entry.parse::() { + dns.push(ip); + } else { + search_domains.push(entry); + } + } + + debug!( + "Configuring interface {ifname} with DNS: {:?} and search domains: {:?}", + dns, search_domains + ); + debug!("Interface Configuration: {:?}", config); + + wgapi.configure_interface(&config)?; + wgapi.configure_dns(&dns, &search_domains)?; + + self.wgapis.insert(ifname.clone(), wgapi); + + debug!("Interface {ifname} configured successfully."); + Ok(()) + } + + pub(crate) fn connect_to_service_location( + &mut self, + location_data: &SingleServiceLocationData, + ) -> Result<(), ServiceLocationError> { + let instance_id = &location_data.instance_id; + let location_pubkey = &location_data.service_location.pubkey; + debug!( + "Connecting to service location for instance_id: {instance_id}, location_pubkey: {location_pubkey}" + ); + + // Check if already connected to this service location + if self.is_service_location_connected(instance_id, location_pubkey) { + debug!( + "Service location with pubkey {location_pubkey} for instance {instance_id} is already connected, skipping" + ); + return Ok(()); + } + + let location_data = self + .load_service_location(instance_id, location_pubkey)? + .ok_or_else(|| { + ServiceLocationError::LoadError(format!( + "Service location with pubkey {} for instance {} not found", + location_pubkey, instance_id + )) + })?; + + self.setup_service_location_interface( + &location_data.service_location, + &location_data.private_key, + )?; + self.add_connected_service_location( + &location_data.instance_id, + &location_data.service_location, + )?; + let ifname = get_interface_name(&location_data.service_location.name); + debug!("Successfully connected to service location '{ifname}'"); + + Ok(()) + } + + pub(crate) fn disconnect_service_locations( + &mut self, + mode: Option, + ) -> Result<(), ServiceLocationError> { + debug!("Disconnecting service locations with mode: {mode:?}"); + + for (instance, locations) in self.connected_service_locations.iter() { + for location in locations { + debug!( + "Found connected service location for instance_id: {instance}, location_pubkey: {}", + location.pubkey + ); + if let Some(m) = mode { + let location_mode: ServiceLocationMode = location.mode.try_into()?; + if location_mode != m { + debug!( + "Skipping interface {} due to the service location mode doesn't match the requested mode (expected {m:?}, found {:?})", + location.name, location.mode + ); + continue; + } + } + + let ifname = get_interface_name(&location.name); + debug!("Tearing down interface: {ifname}"); + if let Some(mut wgapi) = self.wgapis.remove(&ifname) { + if let Err(err) = wgapi.remove_interface() { + error!("Failed to remove interface {ifname}: {err}"); + } else { + debug!("Interface {ifname} removed successfully."); + } + } else { + error!("Failed to find WireGuard API for interface {ifname}"); + } + } + } + + self.remove_connected_service_locations(|_, location| { + if let Some(m) = mode { + let location_mode: ServiceLocationMode = location + .mode + .try_into() + .unwrap_or(ServiceLocationMode::AlwaysOn); + location_mode == m + } else { + true + } + })?; + + debug!("Service locations disconnected."); + + Ok(()) + } + + pub(crate) fn connect_to_service_locations(&mut self) -> Result<(), ServiceLocationError> { + debug!("Attempting to auto-connect to VPN..."); + + let data = self.load_service_locations()?; + debug!("Loaded {} instance(s) from ServiceLocationApi", data.len()); + + for instance_data in data { + debug!( + "Found service locations for instance ID: {}", + instance_data.instance_id + ); + debug!( + "Instance has {} service location(s)", + instance_data.service_locations.len() + ); + for location in instance_data.service_locations { + debug!("Service Location: {location:?}"); + + if location.mode == ServiceLocationMode::PreLogon as i32 { + if is_user_logged_in() { + debug!( + "Skipping pre-logon service location '{}' because user is logged in", + location.name + ); + continue; + } + debug!( + "Proceeding to connect pre-logon service location '{}' because no user is logged in", + location.name + ); + } + + if self.is_service_location_connected(&instance_data.instance_id, &location.pubkey) + { + debug!( + "Skipping service location '{}' because it's already connected", + location.name + ); + continue; + } + + if let Err(err) = + self.setup_service_location_interface(&location, &instance_data.private_key) + { + debug!( + "Failed to setup service location interface for '{}': {err:?}", + location.name + ); + continue; + } + + if let Err(err) = + self.add_connected_service_location(&instance_data.instance_id, &location) + { + debug!( + "Failed to persist connected service location after auto-connect: {err:?}" + ); + } + + debug!( + "Successfully connected to service location '{}'", + location.name + ); + } + } + + debug!("Auto-connect attempt completed"); + + Ok(()) + } + + pub fn save_service_locations( + &self, + service_locations: &[ServiceLocation], + instance_id: &str, + private_key: &str, + ) -> Result<(), ServiceLocationError> { + debug!( + "Received a request to save {} service location(s) for instance {instance_id}", + service_locations.len(), + ); + + debug!("Service locations to save: {service_locations:?}"); + + create_dir_all(get_shared_directory()?)?; + + let instance_file_path = get_instance_file_path(instance_id)?; + + let service_location_data = ServiceLocationData { + service_locations: service_locations.to_vec(), + instance_id: instance_id.to_string(), + private_key: private_key.to_string(), + }; + + let json = serde_json::to_string_pretty(&service_location_data)?; + + debug!("Writing service location data to file: {instance_file_path:?}"); + + fs::write(&instance_file_path, &json)?; + + if let Some(file_path_str) = instance_file_path.to_str() { + debug!("Setting ACLs on service location file: {file_path_str}"); + if let Err(e) = set_protected_acls(file_path_str) { + warn!( + "Failed to set ACLs on service location file {file_path_str}: {e}. File saved but may have insecure permissions." + ); + } else { + debug!("Successfully set ACLs on service location file"); + } + } else { + warn!("Failed to convert file path to string for ACL setting"); + } + + debug!( + "Service locations saved successfully for instance {instance_id} to {:?}", + instance_file_path + ); + Ok(()) + } + + fn load_service_locations(&self) -> Result, ServiceLocationError> { + let base_dir = get_shared_directory()?; + let mut all_locations_data = Vec::new(); + + if base_dir.exists() { + for entry in fs::read_dir(base_dir)? { + let entry = entry?; + let file_path = entry.path(); + + if file_path.is_file() + && file_path.extension().and_then(|s| s.to_str()) == Some("json") + { + match fs::read_to_string(&file_path) { + Ok(data) => match serde_json::from_str::(&data) { + Ok(locations_data) => { + all_locations_data.push(locations_data); + } + Err(e) => { + error!( + "Failed to parse service locations from file {:?}: {e}", + file_path + ); + } + }, + Err(e) => { + error!("Failed to read service locations file {:?}: {e}", file_path); + } + } + } + } + } + + debug!( + "Loaded service locations data for {} instances", + all_locations_data.len() + ); + Ok(all_locations_data) + } + + fn load_service_location( + &self, + instance_id: &str, + location_pubkey: &str, + ) -> Result, ServiceLocationError> { + debug!("Loading service location for instance {instance_id} and pubkey {location_pubkey}"); + + let instance_file_path = get_instance_file_path(instance_id)?; + + if instance_file_path.exists() { + let data = fs::read_to_string(&instance_file_path)?; + let service_location_data = serde_json::from_str::(&data)?; + + for location in service_location_data.service_locations { + if location.pubkey == location_pubkey { + debug!( + "Successfully loaded service location for instance {instance_id} and pubkey {location_pubkey}" + ); + return Ok(Some(SingleServiceLocationData { + service_location: location, + instance_id: service_location_data.instance_id, + private_key: service_location_data.private_key, + })); + } + } + + debug!( + "No service location found for instance {instance_id} with pubkey {location_pubkey}" + ); + Ok(None) + } else { + debug!("No service location file found for instance {instance_id}"); + Ok(None) + } + } + + pub(crate) fn delete_all_service_locations_for_instance( + &self, + instance_id: &str, + ) -> Result<(), ServiceLocationError> { + debug!("Deleting all service locations for instance {instance_id}"); + + let instance_file_path = get_instance_file_path(instance_id)?; + + if instance_file_path.exists() { + fs::remove_file(&instance_file_path)?; + debug!("Successfully deleted all service locations for instance {instance_id}"); + } else { + debug!("No service location file found for instance {instance_id}"); + } + + Ok(()) + } +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 4ba25ef5..176710cb 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -42,8 +42,10 @@ pub enum Error { NoToken, #[error("Failed to lock app state member.")] StateLockFail, - #[error("Failed to acquire lock on mutex. {0}")] - PoisonError(String), + #[error("Failed to convert value. {0}")] + ConversionError(String), + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), } // we must manually implement serde::Serialize diff --git a/src-tauri/src/events.rs b/src-tauri/src/events.rs index 68be593b..e7267d08 100644 --- a/src-tauri/src/events.rs +++ b/src-tauri/src/events.rs @@ -4,7 +4,7 @@ use tauri_plugin_notification::NotificationExt; use crate::{tray::show_main_window, ConnectionType}; -// Match src/page/client/types.ts. +// Match src/pages/client/types.ts. #[non_exhaustive] pub enum EventKey { ConnectionChanged, @@ -95,9 +95,9 @@ impl DeadConnReconnected { } #[derive(Clone, Serialize)] -struct AddInstancePayload<'a> { - token: &'a str, - url: &'a str, +pub struct AddInstancePayload<'a> { + pub token: &'a str, + pub url: &'a str, } /// Handle deep-link URLs. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e22f4938..ae09fd4e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,8 @@ use self::database::models::{Id, NoId}; pub mod active_connections; pub mod app_config; +#[cfg(target_os = "macos")] +pub mod apple; pub mod appstate; pub mod commands; pub mod database; @@ -25,60 +27,23 @@ pub mod error; pub mod events; pub mod log_watcher; pub mod periodic; +pub mod proto; pub mod service; pub mod tray; pub mod utils; pub mod wg_config; -pub mod proto { - use crate::database::models::{ - location::{Location, LocationMfaMode as MfaMode}, - Id, NoId, - }; - - tonic::include_proto!("defguard.proxy"); - - impl DeviceConfig { - #[must_use] - pub(crate) fn into_location(self, instance_id: Id) -> Location { - let location_mfa_mode = match self.location_mfa_mode { - Some(_location_mfa_mode) => self.location_mfa_mode().into(), - None => { - // handle legacy core response - // DEPRECATED(1.5): superseeded by location_mfa_mode - #[allow(deprecated)] - if self.mfa_enabled { - MfaMode::Internal - } else { - MfaMode::Disabled - } - } - }; - - Location { - id: NoId, - instance_id, - network_id: self.network_id, - name: self.network_name, - address: self.assigned_ip, // Transforming assigned_ip to address - pubkey: self.pubkey, - endpoint: self.endpoint, - allowed_ips: self.allowed_ips, - dns: self.dns, - route_all_traffic: false, - keepalive_interval: self.keepalive_interval.into(), - location_mfa_mode, - } - } - } -} - pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA")); -pub const MIN_CORE_VERSION: Version = Version::new(1, 5, 0); -pub const MIN_PROXY_VERSION: Version = Version::new(1, 5, 0); +pub const MIN_CORE_VERSION: Version = Version::new(1, 6, 0); +pub const MIN_PROXY_VERSION: Version = Version::new(1, 6, 0); +pub const CLIENT_VERSION_HEADER: &str = "defguard-client-version"; +pub const CLIENT_PLATFORM_HEADER: &str = "defguard-client-platform"; +pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +// Must be without ".log" suffix! +pub const LOG_FILENAME: &str = "defguard-client"; // This must match tauri.bundle.identifier from tauri.conf.json. const BUNDLE_IDENTIFIER: &str = "net.defguard"; -// Returns the path to the user’s data directory. +// Returns the path to the user's data directory. #[must_use] pub fn app_data_dir() -> Option { dirs_next::data_dir().map(|dir| dir.join(BUNDLE_IDENTIFIER)) @@ -91,7 +56,10 @@ pub fn app_data_dir() -> Option { pub fn set_perms(path: &Path) { let perms = if path.is_dir() { 0o700 } else { 0o600 }; if let Err(err) = set_permissions(path, Permissions::from_mode(perms)) { - warn!("Failed to set permissions on path {path:?}: {err}"); + warn!( + "Failed to set permissions on path {}: {err}", + path.display() + ); } } @@ -152,6 +120,7 @@ pub struct CommonLocationStats { pub persistent_keepalive_interval: Option, pub connection_type: ConnectionType, } + // Common fields for ConnectionInfo and TunnelConnectionInfo due to shared command #[derive(Debug, Serialize)] pub struct CommonConnectionInfo { diff --git a/src-tauri/src/log_watcher/global_log_watcher.rs b/src-tauri/src/log_watcher/global_log_watcher.rs index 38ca7b41..7679aade 100644 --- a/src-tauri/src/log_watcher/global_log_watcher.rs +++ b/src-tauri/src/log_watcher/global_log_watcher.rs @@ -2,36 +2,52 @@ //! // FIXME: Some of the code here overlaps with the `log_watcher` module and could be refactored to avoid duplication. +#[cfg(not(target_os = "macos"))] +use std::fs::read_dir; use std::{ - fs::{read_dir, File}, + fs::File, io::{BufRead, BufReader}, path::PathBuf, str::FromStr, - thread::sleep, time::Duration, }; -use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone, Utc}; +#[cfg(not(target_os = "macos"))] +use chrono::NaiveDate; +use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use regex::Regex; use tauri::{async_runtime::JoinHandle, AppHandle, Emitter, Manager}; +use tokio::time::sleep; use tokio_util::sync::CancellationToken; use tracing::Level; +#[cfg(target_os = "macos")] +use crate::log_watcher::get_vpn_extension_log_dir_path; use crate::{ appstate::AppState, error::Error, - log_watcher::{extract_timestamp, LogLine, LogLineFields, LogSource, LogWatcherError}, - utils::get_service_log_dir, + log_watcher::{LogLine, LogLineFields, LogSource, LogWatcherError}, + LOG_FILENAME, }; +#[cfg(not(target_os = "macos"))] +use crate::{log_watcher::extract_timestamp, utils::get_service_log_dir}; + +#[cfg(target_os = "macos")] +pub(crate) const VPN_EXTENSION_LOG_FILENAME: &str = "vpn-extension.log"; /// Helper struct to handle log directory logic #[derive(Debug)] pub struct LogDirs { // Service + #[cfg(not(target_os = "macos"))] service_log_dir: PathBuf, + #[cfg(not(target_os = "macos"))] current_service_log_file: Option, // Client client_log_dir: PathBuf, + // VPN Extension (macOS only) + #[cfg(target_os = "macos")] + vpn_extension_log_dir: PathBuf, } const DELAY: Duration = Duration::from_secs(2); @@ -39,19 +55,37 @@ const DELAY: Duration = Duration::from_secs(2); impl LogDirs { pub fn new(handle: &AppHandle) -> Result { debug!("Getting log directories for service and client to watch."); + #[cfg(not(target_os = "macos"))] let service_log_dir = get_service_log_dir().to_path_buf(); let client_log_dir = handle.path().app_log_dir().map_err(|_| { LogWatcherError::LogPathError("Path to client logs directory is empty.".to_string()) })?; + + #[cfg(target_os = "macos")] + let vpn_extension_log_dir = get_vpn_extension_log_dir_path()?; + + #[cfg(not(target_os = "macos"))] debug!( - "Log directories of service and client have been identified by the global log watcher: \ - {} and {}", service_log_dir.display(), client_log_dir.display() + "Log directories identified by global log watcher: service={}, client={}", + service_log_dir.display(), + client_log_dir.display() + ); + + #[cfg(target_os = "macos")] + debug!( + "Log directories identified by global log watcher: client={}, vpn_extension={}", + client_log_dir.display(), + vpn_extension_log_dir.display() ); Ok(Self { + #[cfg(not(target_os = "macos"))] service_log_dir, + #[cfg(not(target_os = "macos"))] current_service_log_file: None, client_log_dir, + #[cfg(target_os = "macos")] + vpn_extension_log_dir, }) } @@ -59,12 +93,14 @@ impl LogDirs { /// /// Log files are rotated daily and have a known naming format, /// with the last 10 characters specifying a date (e.g. `2023-12-15`). + #[cfg(not(target_os = "macos"))] fn get_latest_log_file(&self) -> Result, LogWatcherError> { - trace!( + debug!( "Getting latest log file from directory: {}", self.service_log_dir.display() ); let entries = read_dir(&self.service_log_dir)?; + debug!("Read entries from service log directory"); let mut latest_log = None; let mut latest_time = NaiveDate::MIN; @@ -81,46 +117,45 @@ impl LogDirs { } } + debug!( + "Latest log file determined: {:?}", + latest_log.as_ref().map(|p| p.display()) + ); + Ok(latest_log) } + #[cfg(not(target_os = "macos"))] fn get_current_service_file(&self) -> Result { - trace!( - "Opening service log file: {:?}", - self.current_service_log_file - ); match &self.current_service_log_file { Some(path) => { + trace!("Opening service log file: {}", path.display()); let file = File::open(path)?; - trace!( - "Successfully opened service log file at {:?}", - self.current_service_log_file - ); + trace!("Successfully opened service log file at {}", path.display()); Ok(file) } - None => Err(LogWatcherError::LogPathError(format!( - "Couldn't find service log file at: {:?}", - self.current_service_log_file - ))), + None => Err(LogWatcherError::LogPathError( + "Service log file not defined".to_string(), + )), } } fn get_client_file(&self) -> Result { - trace!( - "Opening the log file for the client, using directory: {}", - self.client_log_dir.display() - ); - let dir_str = self - .client_log_dir - .to_str() - .ok_or(LogWatcherError::LogPathError(format!( - "Couldn't convert the client log directory path ({}) to a string slice", - self.client_log_dir.display() - )))?; - let path = format!("{dir_str}/defguard-client.log"); - trace!("Constructed client log file path: {path}"); + let path = self.client_log_dir.join(format!("{LOG_FILENAME}.log")); + trace!("Constructed client log file path: {}", path.display()); let file = File::open(&path)?; - trace!("Client log file at {path:?} opened successfully"); + trace!("Client log file at {} opened successfully", path.display()); + Ok(file) + } + + /// Get the VPN extension log file (macOS only) + /// The VPN extension writes logs to the App Group shared container + #[cfg(target_os = "macos")] + fn get_vpn_extension_file(&self) -> Result { + let path = self.vpn_extension_log_dir.join(VPN_EXTENSION_LOG_FILENAME); + trace!("Opening VPN extension log file: {}", path.display()); + let file = File::open(&path)?; + trace!("VPN extension log file opened successfully"); Ok(file) } } @@ -154,8 +189,9 @@ impl GlobalLogWatcher { } /// Start log watching, calls the [`parse_log_dirs`] function. - pub fn run(&mut self) -> Result<(), LogWatcherError> { - self.parse_log_dirs() + pub async fn run(&mut self) -> Result<(), LogWatcherError> { + debug!("Starting global log watcher run loop."); + self.parse_log_dirs().await } /// Parse the log files @@ -163,10 +199,11 @@ impl GlobalLogWatcher { /// This function will open the log files and read them line by line, parsing each line /// into a [`LogLine`] struct and emitting it to the frontend. It can be stopped by cancelling /// the token by calling [`stop_global_log_watcher_task()`] - fn parse_log_dirs(&mut self) -> Result<(), LogWatcherError> { - trace!("Processing log directories for service and client."); + #[cfg(not(target_os = "macos"))] + async fn parse_log_dirs(&mut self) -> Result<(), LogWatcherError> { + debug!("Processing log directories for service and client."); self.log_dirs.current_service_log_file = self.log_dirs.get_latest_log_file()?; - trace!( + debug!( "Latest service log file found: {:?}", self.log_dirs.current_service_log_file ); @@ -182,7 +219,7 @@ impl GlobalLogWatcher { None }; - trace!("Checking if log files are available"); + debug!("Checking if log files are available"); if service_reader.is_none() && client_reader.is_none() { warn!( "Couldn't read files at {:?} and {}, there will be no logs reported in the client.", @@ -190,10 +227,10 @@ impl GlobalLogWatcher { self.log_dirs.client_log_dir.display() ); // Wait for logs to appear. - sleep(DELAY); + sleep(DELAY).await; return Ok(()); } - trace!("Log files are available, starting to read lines."); + debug!("Log files are available, starting to read lines."); let mut service_line = String::new(); let mut client_line = String::new(); @@ -282,7 +319,122 @@ impl GlobalLogWatcher { parsed_lines.clear(); } trace!("Sleeping for {DELAY:?} seconds before reading again"); - sleep(DELAY); + sleep(DELAY).await; + } + + Ok(()) + } + + #[cfg(target_os = "macos")] + async fn parse_log_dirs(&self) -> Result<(), LogWatcherError> { + debug!("Processing log directories for client and VPN extension."); + let mut client_reader = self + .log_dirs + .get_client_file() + .map_or_else(|_| None, |file| Some(BufReader::new(file))); + + let mut vpn_extension_reader = self.log_dirs.get_vpn_extension_file().map_or_else( + |_| { + debug!("VPN extension log file not available yet"); + None + }, + |file| { + debug!("VPN extension log file opened successfully"); + Some(BufReader::new(file)) + }, + ); + + debug!("Checking if log files are available"); + if client_reader.is_none() && vpn_extension_reader.is_none() { + warn!( + "Couldn't read client logs at {} or VPN extension logs at {}, there will be no logs reported.", + self.log_dirs.client_log_dir.display(), + self.log_dirs.vpn_extension_log_dir.display() + ); + // Wait for logs to appear. + sleep(DELAY).await; + return Ok(()); + } + debug!("Log files are available, starting to read lines."); + + let mut client_line = String::new(); + let mut vpn_extension_line = String::new(); + let mut parsed_lines = Vec::new(); + + // Track the amount of bytes read from the log lines + let mut client_line_read; + let mut vpn_extension_line_read; + + debug!("Global log watcher is starting the loop for reading client and VPN extension log files"); + loop { + if self.cancellation_token.is_cancelled() { + debug!("Received cancellation request. Stopping global log watcher"); + break; + } + + // Client logs + // If the reader is present, read the log file to the end. + // Parse every line. + // Warning: don't use anything other than a trace log level in this loop for logs that would appear on every iteration (or very often) + // This could result in the reader constantly producing and consuming logs without any progress. + if let Some(reader) = &mut client_reader { + loop { + client_line_read = reader.read_line(&mut client_line)?; + if client_line_read > 0 { + if let Ok(Some(parsed_line)) = self.parse_client_log_line(&client_line) { + parsed_lines.push(parsed_line); + } else { + // Don't log it, as it will cause an endless loop + } + client_line.clear(); + } else { + break; + } + } + } else { + // Try to open the client log file if it wasn't available before + if let Ok(file) = self.log_dirs.get_client_file() { + debug!("Client log file is now available, opening reader"); + client_reader = Some(BufReader::new(file)); + } + } + + // VPN Extension logs + // Read the VPN extension log file written by the Swift network extension + if let Some(reader) = &mut vpn_extension_reader { + loop { + vpn_extension_line_read = reader.read_line(&mut vpn_extension_line)?; + if vpn_extension_line_read > 0 { + if let Ok(Some(parsed_line)) = + self.parse_vpn_extension_log_line(&vpn_extension_line) + { + parsed_lines.push(parsed_line); + } else { + // Don't log it, as it will cause an endless loop + } + vpn_extension_line.clear(); + } else { + break; + } + } + } else { + // Try to open the VPN extension log file if it wasn't available before + if let Ok(file) = self.log_dirs.get_vpn_extension_file() { + debug!("VPN extension log file is now available, opening reader"); + vpn_extension_reader = Some(BufReader::new(file)); + } + } + + trace!("Reached EOF in all log files."); + if !parsed_lines.is_empty() { + parsed_lines.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + trace!("Emitting parsed lines for the frontend"); + self.handle.emit(&self.event_topic, &parsed_lines)?; + trace!("Emitted {} lines to the frontend", parsed_lines.len()); + parsed_lines.clear(); + } + trace!("Sleeping for {DELAY:?} seconds before reading again"); + sleep(DELAY).await; } Ok(()) @@ -292,6 +444,7 @@ impl GlobalLogWatcher { /// /// Deserializes the log line into a known struct. /// Also performs filtering by log level and optional timestamp. + #[cfg(not(target_os = "macos"))] fn parse_service_log_line(&self, line: &str) -> Option { let Ok(mut log_line) = serde_json::from_str::(line) else { warn!("Failed to parse service log line: {line}"); @@ -388,6 +541,91 @@ impl GlobalLogWatcher { Ok(Some(log_line)) } + + /// Parse a VPN extension log line into a known struct using regex. + #[cfg(target_os = "macos")] + fn parse_vpn_extension_log_line(&self, line: &str) -> Result, LogWatcherError> { + use crate::log_watcher::LOG_LINE_REGEX; + + let trimmed = line.trim(); + + if trimmed.is_empty() || trimmed.starts_with('#') { + return Ok(None); + } + + // Example log: + // 2024-01-15 14:32:45.123 [INFO] [Adapter] Tunnel started successfully + // Format: YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [Category] Message + + let captures = LOG_LINE_REGEX + .captures(trimmed) + .ok_or(LogWatcherError::LogParseError(line.to_string()))?; + + let timestamp_str = captures + .get(1) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + // Parse timestamp as UTC (Swift FileLogger is configured to use UTC timezone) + let timestamp = Utc.from_utc_datetime( + &NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%d %H:%M:%S%.3f").map_err( + |err| { + LogWatcherError::LogParseError(format!( + "Failed to parse VPN extension timestamp {timestamp_str} with error: {err}" + )) + }, + )?, + ); + + let level_str = captures + .get(2) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + // Map VPN extension log levels to tracing levels + // VPN extension uses: DEBUG, INFO, WARN, ERROR + let level = match level_str.to_uppercase().as_str() { + "DEBUG" => Level::DEBUG, + "WARN" => Level::WARN, + "ERROR" => Level::ERROR, + _ => Level::INFO, // Default to INFO for unknown (and INFO) levels + }; + + let category = captures + .get(3) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + let message = captures + .get(4) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + let fields = LogLineFields { + message: message.to_string(), + }; + + let log_line = LogLine { + timestamp, + level, + target: format!("VPNExtension::{category}"), + fields, + span: None, + source: Some(LogSource::VpnExtension), + }; + + if log_line.level > self.log_level { + return Ok(None); + } + + if let Some(from) = self.from { + if log_line.timestamp < from { + return Ok(None); + } + } + + Ok(Some(log_line)) + } } /// Starts a global log watcher in a separate thread @@ -415,7 +653,8 @@ pub async fn spawn_global_log_watcher_task( let _join_handle: JoinHandle> = tauri::async_runtime::spawn(async move { GlobalLogWatcher::new(handle_clone, token_clone, topic_clone, log_level, from)? - .run()?; + .run() + .await?; Ok(()) }); @@ -444,14 +683,17 @@ pub fn stop_global_log_watcher_task(handle: &AppHandle) -> Result<(), Error> { .lock() .expect("Failed to lock log watchers mutex"); - if let Some(token) = log_watchers.remove("GLOBAL") { - debug!("Using cancellation token for global log watcher"); - token.cancel(); - debug!("Global log watcher cancelled"); - Ok(()) - } else { - // Silently ignore if global log watcher is not found, as there is nothing to cancel - debug!("Global log watcher not found, nothing to cancel"); - Ok(()) - } + log_watchers.remove("GLOBAL").map_or_else( + || { + // Silently ignore if global log watcher is not found, as there is nothing to cancel + debug!("Global log watcher not found, nothing to cancel"); + Ok(()) + }, + |token| { + debug!("Using cancellation token for global log watcher"); + token.cancel(); + debug!("Global log watcher cancelled"); + Ok(()) + }, + ) } diff --git a/src-tauri/src/log_watcher/mod.rs b/src-tauri/src/log_watcher/mod.rs index c7e34c67..b2c5bb8d 100644 --- a/src-tauri/src/log_watcher/mod.rs +++ b/src-tauri/src/log_watcher/mod.rs @@ -1,4 +1,9 @@ +#[cfg(target_os = "macos")] +use std::{path::PathBuf, sync::LazyLock}; + use chrono::{DateTime, NaiveDate, Utc}; +#[cfg(target_os = "macos")] +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use thiserror::Error; @@ -25,8 +30,15 @@ pub enum LogWatcherError { #[derive(Debug, Deserialize, Serialize, Clone)] pub enum LogSource { + /// Service logs (Linux) or VPN Extension logs (macOS) + /// Serializes to "Vpn" for frontend compatibility + #[cfg(not(target_os = "macos"))] + #[serde(rename = "VPN")] Service, Client, + #[cfg(target_os = "macos")] + #[serde(rename = "VPN")] + VpnExtension, } /// Represents a single line in log file @@ -62,3 +74,28 @@ fn extract_timestamp(filename: &str) -> Option { // parse and convert to `NaiveDate` NaiveDate::parse_from_str(timestamp, "%Y-%m-%d").ok() } + +#[cfg(target_os = "macos")] +static LOG_LINE_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[(\w+)\] (.*)$").unwrap() +}); + +/// Get the VPN extension log file path on macOS +#[cfg(target_os = "macos")] +fn get_vpn_extension_log_dir_path() -> Result { + use objc2_foundation::{ns_string, NSFileManager}; + + let manager = NSFileManager::defaultManager(); + manager + .containerURLForSecurityApplicationGroupIdentifier(ns_string!("group.net.defguard")) + .and_then(|url| url.to_file_path()) + .map_or_else( + || { + error!("Failed to get container URL for VPN extension logs"); + Err(LogWatcherError::LogPathError( + "Failed to get container URL for VPN extension logs".to_string(), + )) + }, + |url| Ok(url.join("Logs/")), + ) +} diff --git a/src-tauri/src/log_watcher/service_log_watcher.rs b/src-tauri/src/log_watcher/service_log_watcher.rs index 94bd9a57..f7f4c931 100644 --- a/src-tauri/src/log_watcher/service_log_watcher.rs +++ b/src-tauri/src/log_watcher/service_log_watcher.rs @@ -3,6 +3,9 @@ //! This is meant to handle passing relevant logs from `defguard-service` daemon to the client GUI. //! The watcher monitors a given directory for any changes. Whenever a change is detected //! it parses the log files and sends logs relevant to a specified interface to the fronted. +//! +//! On macOS, this module also provides a VPN extension log watcher that reads from the +//! App Group shared container where the Swift network extension writes its logs. use std::{ fs::{read_dir, File}, @@ -14,18 +17,20 @@ use std::{ }; use chrono::{DateTime, NaiveDate, Utc}; +#[cfg(target_os = "macos")] +use chrono::{NaiveDateTime, TimeZone}; use tauri::{async_runtime::JoinHandle, AppHandle, Emitter, Manager}; use tokio_util::sync::CancellationToken; use tracing::Level; +#[cfg(target_os = "macos")] +use super::LogLineFields; use super::{LogLine, LogWatcherError}; +#[cfg(not(target_os = "macos"))] +use crate::utils::get_service_log_dir; use crate::{ - appstate::AppState, - database::models::Id, - error::Error, - log_watcher::extract_timestamp, - utils::{get_service_log_dir, get_tunnel_or_location_name}, - ConnectionType, + appstate::AppState, database::models::Id, error::Error, log_watcher::extract_timestamp, + utils::get_tunnel_or_location_name, ConnectionType, }; const DELAY: Duration = Duration::from_secs(2); @@ -206,12 +211,195 @@ impl<'a> ServiceLogWatcher<'a> { } } +/// macOS-specific log watcher for VPN extension logs +/// +/// On macOS, the VPN functionality is handled by a Network Extension which writes +/// its logs to an App Group shared container. This watcher reads those logs. +#[cfg(target_os = "macos")] +#[derive(Debug)] +pub struct VpnExtensionLogWatcher { + log_level: Level, + from: Option>, + log_file: PathBuf, + handle: AppHandle, + cancellation_token: CancellationToken, + event_topic: String, +} + +#[cfg(target_os = "macos")] +impl VpnExtensionLogWatcher { + #[must_use] + pub fn new( + handle: AppHandle, + cancellation_token: CancellationToken, + event_topic: String, + log_level: Level, + from: Option>, + log_file: PathBuf, + ) -> Self { + Self { + log_level, + from, + log_file, + handle, + cancellation_token, + event_topic, + } + } + + /// Run the VPN extension log watcher + pub fn run(&mut self) -> Result<(), LogWatcherError> { + debug!( + "Starting VPN extension log watcher, reading from: {}", + self.log_file.display() + ); + + // Wait for the log file to exist + while !self.log_file.exists() { + if self.cancellation_token.is_cancelled() { + return Ok(()); + } + debug!( + "VPN extension log file not found at {}, waiting...", + self.log_file.display() + ); + sleep(DELAY); + } + + let file = File::open(&self.log_file)?; + let mut reader = BufReader::new(file); + let mut line = String::new(); + let mut parsed_lines = Vec::new(); + + loop { + if self.cancellation_token.is_cancelled() { + info!("VPN extension log watcher is being stopped."); + break; + } + + let size = reader.read_line(&mut line)?; + if size == 0 { + // EOF reached, emit collected logs and wait + if !parsed_lines.is_empty() { + trace!( + "Emitting {} VPN extension log lines for the frontend", + parsed_lines.len() + ); + self.handle.emit(&self.event_topic, &parsed_lines)?; + parsed_lines.clear(); + } + sleep(DELAY); + } else { + match self.parse_log_line(&line) { + Ok(Some(parsed_line)) => { + parsed_lines.push(parsed_line); + } + Ok(None) => { + // Line was filtered out + } + Err(e) => { + trace!("Failed to parse VPN extension log line: {e}"); + } + } + line.clear(); + } + } + + Ok(()) + } + + /// Parse a VPN extension log line + /// + /// Log format: `2024-01-15 14:32:45.123 [INFO] [Adapter] Message here` + fn parse_log_line(&self, line: &str) -> Result, LogWatcherError> { + use crate::log_watcher::LOG_LINE_REGEX; + + let trimmed = line.trim(); + + // Skip empty lines and separator/header lines + if trimmed.is_empty() || trimmed.starts_with('#') { + return Ok(None); + } + + let captures = LOG_LINE_REGEX + .captures(trimmed) + .ok_or(LogWatcherError::LogParseError(line.to_string()))?; + + let timestamp_str = captures + .get(1) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + // Parse timestamp as UTC (Swift FileLogger is configured to use UTC timezone) + let timestamp = Utc.from_utc_datetime( + &NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%d %H:%M:%S%.3f").map_err( + |err| { + LogWatcherError::LogParseError(format!( + "Failed to parse VPN extension timestamp {timestamp_str} with error: {err}" + )) + }, + )?, + ); + + let level_str = captures + .get(2) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + let level = match level_str.to_uppercase().as_str() { + "DEBUG" => Level::DEBUG, + "WARN" => Level::WARN, + "ERROR" => Level::ERROR, + _ => Level::INFO, + }; + + let category = captures + .get(3) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + let message = captures + .get(4) + .ok_or(LogWatcherError::LogParseError(line.to_string()))? + .as_str(); + + // Filter by log level + if level > self.log_level { + return Ok(None); + } + + // Filter by timestamp + if let Some(from) = self.from { + if timestamp < from { + return Ok(None); + } + } + + let fields = LogLineFields { + message: message.to_string(), + }; + + Ok(Some(LogLine { + timestamp, + level, + target: format!("VPNExtension::{category}"), + fields, + span: None, + source: None, + })) + } +} + /// Starts a log watcher in a separate thread /// /// The watcher parses `defguard-service` log files and extracts logs relevant /// to the WireGuard interface for a given location. /// Logs are then transmitted to the frontend by using `tauri` `Events`. /// Returned value is the name of an event topic to monitor. +/// +/// On macOS, this uses the VPN extension log watcher instead, reading from +/// the App Group shared container where the Swift network extension writes logs. +#[cfg(not(target_os = "macos"))] pub async fn spawn_log_watcher_task( handle: AppHandle, location_id: Id, @@ -287,6 +475,90 @@ pub async fn spawn_log_watcher_task( Ok(event_topic) } +/// macOS version: Starts a VPN extension log watcher in a separate thread +/// +/// On macOS, the VPN functionality is handled by a Network Extension which writes +/// its logs to an App Group shared container. This function spawns a watcher for those logs. +/// +/// TODO: Currently the "service log watcher" should watch only given interface, this is not yet implemented for VPN extension logs. +#[cfg(target_os = "macos")] +pub async fn spawn_log_watcher_task( + handle: AppHandle, + location_id: Id, + interface_name: String, + connection_type: ConnectionType, + log_level: Level, + from: Option, +) -> Result { + use crate::log_watcher::{ + get_vpn_extension_log_dir_path, global_log_watcher::VPN_EXTENSION_LOG_FILENAME, + }; + + debug!( + "Spawning VPN extension log watcher task for location ID {location_id}, interface {interface_name}" + ); + let app_state = handle.state::(); + + let from = from.and_then(|from| DateTime::::from_str(&from).ok()); + + let connection_type_str = if connection_type.eq(&ConnectionType::Tunnel) { + "Tunnel" + } else { + "Location" + }; + let event_topic = format!("log-update-{connection_type_str}-{location_id}"); + debug!("Using the following event topic for the VPN extension log watcher: {event_topic}"); + + let log_dir = + get_vpn_extension_log_dir_path().map_err(|e| Error::InternalError(e.to_string()))?; + let log_file = log_dir.join(VPN_EXTENSION_LOG_FILENAME); + debug!("VPN extension log file path: {}", log_file.display()); + + let topic_clone = event_topic.clone(); + let handle_clone = handle.clone(); + + let token = CancellationToken::new(); + let token_clone = token.clone(); + + let mut log_watcher = VpnExtensionLogWatcher::new( + handle_clone, + token_clone, + topic_clone, + log_level, + from, + log_file, + ); + + // spawn task + let _join_handle: JoinHandle> = + tauri::async_runtime::spawn(async move { + log_watcher.run()?; + Ok(()) + }); + + // store `CancellationToken` to manually stop watcher thread + // keep this in a block as we .await later, which should not be done while holding a lock like this + { + let mut log_watchers = app_state + .log_watchers + .lock() + .expect("Failed to lock log watchers mutex"); + if let Some(old_token) = log_watchers.insert(interface_name.clone(), token) { + // cancel previous log watcher for this interface + debug!("Existing log watcher for interface {interface_name} found. Cancelling..."); + old_token.cancel(); + } + } + + let name = get_tunnel_or_location_name(location_id, connection_type).await; + info!( + "A background task has been spawned to watch the VPN extension log file for \ + {connection_type} {name} (interface {interface_name}), location's specific collected logs \ + will be displayed in the {connection_type}'s detailed view." + ); + Ok(event_topic) +} + /// Stops the log watcher thread pub fn stop_log_watcher_task(handle: &AppHandle, interface_name: &str) -> Result<(), Error> { debug!("Stopping service log watcher task for interface {interface_name}"); @@ -298,15 +570,20 @@ pub fn stop_log_watcher_task(handle: &AppHandle, interface_name: &str) -> Result .lock() .expect("Failed to lock log watchers mutex"); - if let Some(token) = log_watchers.remove(interface_name) { - debug!("Using cancellation token for service log watcher on interface {interface_name}"); - token.cancel(); - debug!("Service log watcher for interface {interface_name} stopped"); - Ok(()) - } else { - debug!( - "Service log watcher for interface {interface_name} couldn't be found, nothing to stop" - ); - Err(Error::NotFound) - } + log_watchers.remove(interface_name).map_or_else( + || { + warn!( + "Service log watcher for interface {interface_name} couldn't be found, nothing to stop" + ); + Ok(()) + }, + |token| { + debug!( + "Using cancellation token for service log watcher on interface {interface_name}" + ); + token.cancel(); + debug!("Service log watcher for interface {interface_name} stopped"); + Ok(()) + }, + ) } diff --git a/src-tauri/src/periodic/mod.rs b/src-tauri/src/periodic/mod.rs index 0b4fab97..37daff37 100644 --- a/src-tauri/src/periodic/mod.rs +++ b/src-tauri/src/periodic/mod.rs @@ -1,9 +1,9 @@ -use self::{ - connection::verify_active_connections, purge_stats::purge_stats, version::poll_version, -}; use tauri::AppHandle; use tokio::select; +use self::{ + connection::verify_active_connections, purge_stats::purge_stats, version::poll_version, +}; use crate::enterprise::periodic::config::poll_config; pub mod connection; diff --git a/src-tauri/src/proto.rs b/src-tauri/src/proto.rs new file mode 100644 index 00000000..ad041709 --- /dev/null +++ b/src-tauri/src/proto.rs @@ -0,0 +1,46 @@ +use crate::database::models::{ + location::{Location, LocationMfaMode as MfaMode, ServiceLocationMode as SLocationMode}, + Id, NoId, +}; + +tonic::include_proto!("defguard.proxy"); + +impl DeviceConfig { + #[must_use] + pub(crate) fn into_location(self, instance_id: Id) -> Location { + let location_mfa_mode = match self.location_mfa_mode { + Some(_location_mfa_mode) => self.location_mfa_mode().into(), + None => { + // handle legacy core response + // DEPRECATED(1.5): superseeded by location_mfa_mode + #[allow(deprecated)] + if self.mfa_enabled { + MfaMode::Internal + } else { + MfaMode::Disabled + } + } + }; + + let service_location_mode = match self.service_location_mode { + Some(_service_location_mode) => self.service_location_mode().into(), + None => SLocationMode::Disabled, // Default to disabled if not set + }; + + Location { + id: NoId, + instance_id, + network_id: self.network_id, + name: self.network_name, + address: self.assigned_ip, // Transforming assigned_ip to address + pubkey: self.pubkey, + endpoint: self.endpoint, + allowed_ips: self.allowed_ips, + dns: self.dns, + route_all_traffic: false, + keepalive_interval: self.keepalive_interval.into(), + location_mfa_mode, + service_location_mode, + } + } +} diff --git a/src-tauri/src/service/client.rs b/src-tauri/src/service/client.rs new file mode 100644 index 00000000..abf9afb1 --- /dev/null +++ b/src-tauri/src/service/client.rs @@ -0,0 +1,69 @@ +use std::sync::LazyLock; + +use hyper_util::rt::TokioIo; +#[cfg(windows)] +use tokio::net::windows::named_pipe::ClientOptions; +#[cfg(unix)] +use tokio::net::UnixStream; +use tonic::transport::channel::{Channel, Endpoint}; +#[cfg(unix)] +use tonic::transport::Uri; +use tower::service_fn; +#[cfg(windows)] +use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY; + +#[cfg(unix)] +use super::daemon::DAEMON_SOCKET_PATH; +#[cfg(windows)] +use super::named_pipe::PIPE_NAME; +use super::proto::desktop_daemon_service_client::DesktopDaemonServiceClient; + +pub(crate) static DAEMON_CLIENT: LazyLock> = + LazyLock::new(|| { + debug!("Setting up gRPC client"); + // URL is ignored since we provide our own connectors for unix socket and windows named pipes. + let endpoint = Endpoint::from_static("http://localhost"); + let channel; + #[cfg(unix)] + { + channel = endpoint.connect_with_connector_lazy(service_fn(|_: Uri| async { + // Connect to a Unix domain socket. + let stream = match UnixStream::connect(DAEMON_SOCKET_PATH).await { + Ok(stream) => stream, + Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { + error!( + "Permission denied for UNIX domain socket; please refer to \ + https://docs.defguard.net/support-1/troubleshooting#\ + unix-socket-permission-errors-when-desktop-client-attempts-to-connect-\ + to-vpn-on-linux-machines" + ); + return Err(err); + } + Err(err) => { + error!("Problem connecting to UNIX domain socket: {err}"); + return Err(err); + } + }; + info!("Created unix gRPC client"); + Ok::<_, std::io::Error>(TokioIo::new(stream)) + })); + }; + #[cfg(windows)] + { + channel = endpoint.connect_with_connector_lazy(service_fn(|_| async { + let client = loop { + match ClientOptions::new().open(PIPE_NAME) { + Ok(client) => break client, + Err(err) if err.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (), + Err(err) => { + error!("Problem connecting to named pipe: {err}"); + return Err(err); + } + } + }; + info!("Created windows gRPC client"); + Ok::<_, std::io::Error>(TokioIo::new(client)) + })); + } + DesktopDaemonServiceClient::new(channel) + }); diff --git a/src-tauri/src/service/config.rs b/src-tauri/src/service/config.rs index 6277d19f..e2f8528b 100644 --- a/src-tauri/src/service/config.rs +++ b/src-tauri/src/service/config.rs @@ -1,8 +1,8 @@ use clap::Parser; -#[cfg(target_os = "windows")] +#[cfg(windows)] pub const DEFAULT_LOG_DIR: &str = "/Logs/defguard-service"; -#[cfg(not(target_os = "windows"))] +#[cfg(not(windows))] pub const DEFAULT_LOG_DIR: &str = "/var/log/defguard-service"; #[derive(Debug, Parser, Clone)] diff --git a/src-tauri/src/service/daemon.rs b/src-tauri/src/service/daemon.rs new file mode 100644 index 00000000..655ec3f3 --- /dev/null +++ b/src-tauri/src/service/daemon.rs @@ -0,0 +1,575 @@ +use std::{ + collections::HashMap, + net::IpAddr, + pin::Pin, + sync::{Arc, Mutex, RwLock}, + time::{Duration, SystemTime}, +}; +#[cfg(unix)] +use std::{fs, os::unix::fs::PermissionsExt, path::Path}; + +use defguard_wireguard_rs::{ + error::WireguardInterfaceError, InterfaceConfiguration, Kernel, WGApi, WireguardInterfaceApi, +}; +#[cfg(unix)] +use nix::unistd::{chown, Group}; +#[cfg(unix)] +use tokio::net::UnixListener; +use tokio::{sync::mpsc, task::JoinHandle, time::interval}; +#[cfg(unix)] +use tokio_stream::wrappers::UnixListenerStream; +use tonic::{ + codegen::tokio_stream::{wrappers::ReceiverStream, Stream}, + transport::Server, + Code, Response, Status, +}; +use tracing::{debug, error, info, info_span, Instrument}; + +use super::{ + config::Config, + proto::{ + desktop_daemon_service_server::{DesktopDaemonService, DesktopDaemonServiceServer}, + CreateInterfaceRequest, InterfaceData, ReadInterfaceDataRequest, RemoveInterfaceRequest, + }, +}; +#[cfg(windows)] +use crate::enterprise::service_locations::ServiceLocationManager; +#[cfg(windows)] +use crate::service::named_pipe::{get_named_pipe_server_stream, PIPE_NAME}; +use crate::{ + enterprise::service_locations::ServiceLocationError, + service::proto::{DeleteServiceLocationsRequest, SaveServiceLocationsRequest}, + VERSION, +}; + +#[cfg(unix)] +pub(super) const DAEMON_SOCKET_PATH: &str = "/var/run/defguard.socket"; + +#[cfg(target_os = "linux")] +pub(super) const DAEMON_SOCKET_GROUP: &str = "defguard"; + +#[derive(Debug, thiserror::Error)] +pub enum DaemonError { + #[error(transparent)] + WireguardError(#[from] WireguardInterfaceError), + #[error("Unexpected error: {0}")] + Unexpected(String), + #[error(transparent)] + TransportError(#[from] tonic::transport::Error), + #[error(transparent)] + ServiceLocationError(#[from] ServiceLocationError), + #[cfg(windows)] + #[error(transparent)] + WindowsServiceError(#[from] windows_service::Error), +} + +type IfName = String; +#[cfg(not(target_os = "macos"))] +type WG = WGApi; +#[cfg(target_os = "macos")] +type WG = WGApi; + +#[derive(Default)] +pub(crate) struct DaemonService { + // Map of running `WGApi`s; key is interface name. + wgapis: Arc>>, + stats_period: Duration, + stat_tasks: Arc>>>, + #[cfg(windows)] + service_location_manager: Arc>, +} + +impl DaemonService { + #[must_use] + pub fn new( + config: &Config, + #[cfg(windows)] service_location_manager: Arc>, + ) -> Self { + Self { + wgapis: Arc::new(RwLock::new(HashMap::new())), + stats_period: Duration::from_secs(config.stats_period), + stat_tasks: Arc::new(Mutex::new(HashMap::new())), + #[cfg(windows)] + service_location_manager, + } + } +} + +/// Helper function used to perform required configuration steps for a new interface. +/// +/// This allows us to roll back interface creation if some configuration step fails. +fn configure_new_interface( + ifname: &str, + request: &CreateInterfaceRequest, + wgapi: &mut WGApi, + interface_config: &InterfaceConfiguration, +) -> Result<(), Status> { + // The WireGuard DNS config value can be a list of IP addresses and domain names, which will + // be used as DNS servers and search domains respectively. + debug!("Preparing DNS configuration for interface {ifname}"); + let dns_string = request.dns.clone().unwrap_or_default(); + let dns_entries = dns_string.split(',').map(str::trim).collect::>(); + // We assume that every entry that can't be parsed as an IP address is a domain name. + let mut dns = Vec::new(); + let mut search_domains = Vec::new(); + for entry in dns_entries { + if let Ok(ip) = entry.parse::() { + dns.push(ip); + } else { + search_domains.push(entry); + } + } + debug!( + "DNS configuration for interface {ifname}: DNS: {dns:?}, Search domains: \ + {search_domains:?}" + ); + + let configure_interface_result = wgapi.configure_interface(interface_config); + + configure_interface_result.map_err(|err| { + let msg = format!("Failed to configure WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + + #[cfg(not(windows))] + { + debug!("Configuring interface {ifname} routing"); + wgapi + .configure_peer_routing(&interface_config.peers) + .map_err(|err| { + let msg = + format!("Failed to configure routing for WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + } + if dns.is_empty() { + debug!( + "No DNS configuration provided for interface {ifname}, skipping DNS \ + configuration" + ); + } else { + debug!( + "The following DNS servers will be set: {dns:?}, search domains: \ + {search_domains:?}" + ); + wgapi.configure_dns(&dns, &search_domains).map_err(|err| { + let msg = format!("Failed to configure DNS for WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + }; + + Ok(()) +} + +type InterfaceDataStream = Pin> + Send>>; + +pub(crate) fn setup_wgapi(ifname: &str) -> Result { + let wgapi = WG::new(ifname).map_err(|err| { + let msg = format!("Failed to setup WireGuard API for interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + + Ok(wgapi) +} + +#[tonic::async_trait] +impl DesktopDaemonService for DaemonService { + type ReadInterfaceDataStream = InterfaceDataStream; + + #[cfg(not(windows))] + async fn save_service_locations( + &self, + _request: tonic::Request, + ) -> Result, Status> { + debug!("Save service location request received, this is currently not supported on Unix systems"); + Ok(Response::new(())) + } + + #[cfg(not(windows))] + async fn delete_service_locations( + &self, + _request: tonic::Request, + ) -> Result, Status> { + debug!("Delete service location request received, this is currently not supported on Unix systems"); + Ok(Response::new(())) + } + + #[cfg(windows)] + async fn save_service_locations( + &self, + request: tonic::Request, + ) -> Result, Status> { + debug!("Received a request to save service location"); + let service_location = request.into_inner(); + + match self + .service_location_manager + .clone() + .read() + .unwrap() + .save_service_locations( + service_location.service_locations.as_slice(), + &service_location.instance_id, + &service_location.private_key, + ) { + Ok(()) => { + debug!("Service location saved successfully"); + } + Err(e) => { + let msg = format!("Failed to save service location: {e}"); + error!(msg); + return Err(Status::internal(msg)); + } + } + + for saved_location in service_location.service_locations { + match self + .service_location_manager + .clone() + .write() + .unwrap() + .reset_service_location_state(&service_location.instance_id, &saved_location.pubkey) + { + Ok(()) => { + debug!( + "Service location '{}' state reset successfully", + saved_location.name + ); + } + Err(e) => { + error!( + "Failed to reset state for service location '{}': {e}", + saved_location.name + ); + } + } + } + + Ok(Response::new(())) + } + + #[cfg(windows)] + async fn delete_service_locations( + &self, + request: tonic::Request, + ) -> Result, Status> { + debug!("Received a request to delete service location"); + let instance_id = request.into_inner().instance_id; + + self.service_location_manager + .clone() + .write() + .unwrap() + .disconnect_service_locations_by_instance(&instance_id) + .map_err(|e| { + let msg = format!("Failed to disconnect service location: {e}"); + error!(msg); + Status::internal(msg) + })?; + + match self + .service_location_manager + .clone() + .read() + .unwrap() + .delete_all_service_locations_for_instance(&instance_id) + { + Ok(()) => { + debug!("Service location deleted successfully"); + Ok(Response::new(())) + } + Err(e) => { + error!("Failed to delete service location: {}", e); + Err(Status::internal(format!( + "Failed to delete service location: {}", + e + ))) + } + } + } + + async fn create_interface( + &self, + request: tonic::Request, + ) -> Result, Status> { + debug!("Received a request to create a new interface"); + let request = request.into_inner(); + let config: InterfaceConfiguration = request + .config + .clone() + .ok_or(Status::new( + Code::InvalidArgument, + "Missing interface config in request", + ))? + .into(); + let ifname = &config.name; + let _span = info_span!("create_interface", interface_name = &ifname).entered(); + // Setup WireGuard API. + let Ok(mut wgapis_map) = self.wgapis.write() else { + error!("Failed to acquire read-write lock for WGApis"); + return Err(Status::new(Code::Internal, "read-write lock error")); + }; + let wgapi = wgapis_map + .entry(ifname.clone()) + .or_insert(setup_wgapi(ifname)?); + + // create new interface + debug!("Creating new interface {ifname}"); + wgapi.create_interface().map_err(|err| { + let msg = format!("Failed to create WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + info!("Done creating a new interface {ifname}"); + + // attempt to configure new interface + // remove interface if configuration fails to avoid duplicate interfaces + match configure_new_interface(ifname, &request, wgapi, &config) { + Ok(_) => info!("Finished configuring new interface {ifname}"), + Err(err) => { + error!("Failed to configure interface {ifname}. Error: {err}"); + + debug!("Removing newly created interface {ifname} due to configuration failure"); + wgapi.remove_interface().map_err(|err| { + let msg = format!("Failed to remove WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + + return Err(err); + } + }; + + debug!("Finished creating a new interface {ifname}"); + Ok(Response::new(())) + } + + async fn remove_interface( + &self, + request: tonic::Request, + ) -> Result, Status> { + debug!("Received a request to remove an interface"); + let request = request.into_inner(); + let ifname = request.interface_name; + let _span = info_span!("remove_interface", interface_name = &ifname).entered(); + debug!("Removing interface {ifname}"); + + // Stop stats task. + if let Ok(mut tasks) = self.stat_tasks.lock() { + if let Some(handle) = tasks.remove(&ifname) { + info!("Stopping statistics collector task for interface {ifname}"); + handle.abort(); + } + } + + // `WGApi::remove_interface`` takes `&mut self` under Windows. + #[allow(unused_mut)] + let mut wgapi = { + let Ok(mut wgapis_map) = self.wgapis.write() else { + error!("Failed to acquire read-write lock for WGApis"); + return Err(Status::new(Code::Internal, "read-write lock error")); + }; + let Some(wgapi) = wgapis_map.remove(&ifname) else { + error!("Unknown interface {ifname}"); + return Err(Status::new(Code::Internal, "unknown interface")); + }; + wgapi + }; + + #[cfg(not(windows))] + { + debug!("Cleaning up interface {ifname} routing"); + // Ignore error as this should not be considered fatal, + // e.g. endpoint might fail to resolve DNS name. + if let Err(err) = wgapi.remove_endpoint_routing(&request.endpoint) { + error!( + "Failed to remove routing for endpoint {}: {err}", + request.endpoint + ); + } + } + + wgapi.remove_interface().map_err(|err| { + let msg = format!("Failed to remove WireGuard interface {ifname}: {err}"); + error!("{msg}"); + Status::new(Code::Internal, msg) + })?; + + debug!("Finished removing interface {ifname}"); + Ok(Response::new(())) + } + + async fn read_interface_data( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + let ifname = request.interface_name.clone(); + debug!( + "Received a request to start a new network usage stats data stream for interface \ + {ifname}" + ); + let span = info_span!("read_interface_data", interface_name = &ifname); + + let wgapis = Arc::clone(&self.wgapis); + let mut interval = interval(self.stats_period); + let (tx, rx) = mpsc::channel(64); + + span.in_scope(|| { + info!("Spawning statistics collector task for interface {ifname}"); + }); + let handle = tokio::spawn( + async move { + // Helper map to track if peer data is actually changing to avoid sending duplicate + // stats. + let mut peer_map = HashMap::new(); + + loop { + // Loop delay + interval.tick().await; + debug!( + "Gathering network usage statistics for client's network activity on {ifname}"); + let result = { + let Ok(wgapis_map) = wgapis.read() else { + error!("Failed to acquire read-write lock for WGApis"); + break; + }; + let Some(wgapi) = wgapis_map.get(&ifname) else { + error!("Unknown interface {ifname}"); + break; + }; + wgapi.read_interface_data() + }; + match result { + Ok(mut host) => { + let peers = &mut host.peers; + debug!( + "Found {} peers configured on WireGuard interface", + peers.len() + ); + // Filter out never connected peers. + peers.retain(|_, peer| { + // Last handshake time-stamp must exist. + if let Some(last_hs) = peer.last_handshake { + // ...and not be UNIX epoch. + if last_hs != SystemTime::UNIX_EPOCH + && match peer_map.get(&peer.public_key) { + Some(last_peer) => last_peer != peer, + None => true, + } + { + debug!( + "Peer {} statistics changed; keeping it.", + peer.public_key + ); + peer_map.insert(peer.public_key.clone(), peer.clone()); + return true; + } + } + debug!( + "Peer {} statistics didn't change; ignoring it.", + peer.public_key + ); + false + }); + if let Err(err) = tx.send(Ok(host.into())).await { + error!( + "Couldn't send network usage stats update for {ifname}: {err}" + ); + break; + } + } + Err(err) => { + error!( + "Failed to retrieve network usage stats for interface {ifname}: \ + {err}" + ); + break; + } + } + debug!("Network activity statistics for interface {ifname} sent to the client"); + } + debug!( + "The client has disconnected from the network usage statistics data stream \ + for interface {ifname}, stopping the statistics data collection task." + ); + } + .instrument(span), + ); + if let Ok(mut tasks) = self.stat_tasks.lock() { + tasks.insert(request.interface_name, handle); + } + + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::ReadInterfaceDataStream + )) + } +} + +#[cfg(unix)] +pub async fn run_server(config: Config) -> anyhow::Result<()> { + debug!("Starting Defguard interface management daemon"); + + let daemon_service = DaemonService::new(&config); + + // Remove existing socket if it exists + if Path::new(DAEMON_SOCKET_PATH).exists() { + fs::remove_file(DAEMON_SOCKET_PATH)?; + } + + let uds = UnixListener::bind(DAEMON_SOCKET_PATH)?; + + // change owner group for socket file + // get the group ID by name + let group = Group::from_name(DAEMON_SOCKET_GROUP)?.ok_or_else(|| { + error!("Group '{DAEMON_SOCKET_GROUP}' not found"); + crate::error::Error::InternalError(format!("Group '{DAEMON_SOCKET_GROUP}' not found")) + })?; + + // change ownership - keep current user, change group + chown(DAEMON_SOCKET_PATH, None, Some(group.gid))?; + + // Set socket permissions to allow client access + // 0o660 allows read/write for owner and group only + fs::set_permissions(DAEMON_SOCKET_PATH, fs::Permissions::from_mode(0o660))?; + + let uds_stream = UnixListenerStream::new(uds); + + info!("Defguard daemon version {VERSION} started, listening on socket {DAEMON_SOCKET_PATH}",); + debug!("Defguard daemon configuration: {config:?}"); + + Server::builder() + .trace_fn(|_| tracing::info_span!("defguard_service")) + .add_service(DesktopDaemonServiceServer::new(daemon_service)) + .serve_with_incoming(uds_stream) + .await?; + + Ok(()) +} + +#[cfg(windows)] +pub(crate) async fn run_server( + config: Config, + service_location_manager: Arc>, +) -> anyhow::Result<()> { + debug!("Starting Defguard interface management daemon"); + + let stream = get_named_pipe_server_stream()?; + let daemon_service = DaemonService::new(&config, service_location_manager); + + info!("Defguard daemon version {VERSION} started, listening on named pipe {PIPE_NAME}"); + debug!("Defguard daemon configuration: {config:?}"); + + Server::builder() + .trace_fn(|_| tracing::info_span!("defguard_service")) + .add_service(DesktopDaemonServiceServer::new(daemon_service)) + .serve_with_incoming(stream) + .await?; + + Ok(()) +} diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index db05c90b..0d9925c7 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -1,406 +1,28 @@ +#[cfg(not(target_os = "macos"))] +pub mod client; pub mod config; pub mod proto { tonic::include_proto!("client"); } +#[cfg(not(target_os = "macos"))] +pub mod daemon; +#[cfg(windows)] +pub mod named_pipe; pub mod utils; #[cfg(windows)] pub mod windows; -#[cfg(windows)] -use std::net::{Ipv4Addr, SocketAddr}; use std::{ - collections::HashMap, - net::IpAddr, - pin::Pin, str::FromStr, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::{Duration, UNIX_EPOCH}, }; -#[cfg(unix)] -use std::{fs, os::unix::fs::PermissionsExt, path::Path}; -#[cfg(not(target_os = "macos"))] -use defguard_wireguard_rs::Kernel; -#[cfg(target_os = "macos")] -use defguard_wireguard_rs::Userspace; use defguard_wireguard_rs::{ - error::WireguardInterfaceError, host::{Host, Peer}, key::Key, net::IpAddrMask, - InterfaceConfiguration, WGApi, WireguardInterfaceApi, + InterfaceConfiguration, }; -#[cfg(unix)] -use nix::unistd::{chown, Group}; -use proto::{ - desktop_daemon_service_server::{DesktopDaemonService, DesktopDaemonServiceServer}, - CreateInterfaceRequest, InterfaceData, ReadInterfaceDataRequest, RemoveInterfaceRequest, -}; -#[cfg(unix)] -use tokio::net::UnixListener; -use tokio::{sync::mpsc, time::interval}; -#[cfg(unix)] -use tokio_stream::wrappers::UnixListenerStream; -use tonic::{ - codegen::tokio_stream::{wrappers::ReceiverStream, Stream}, - transport::Server, - Code, Response, Status, -}; -use tracing::{debug, error, info, info_span, Instrument}; - -use self::config::Config; -use super::VERSION; - -#[cfg(windows)] -const DAEMON_HTTP_PORT: u16 = 54127; -pub(super) const DAEMON_BASE_URL: &str = "http://localhost:54127"; - -#[cfg(unix)] -pub(super) const DAEMON_SOCKET_PATH: &str = "/var/run/defguard.socket"; - -#[cfg(target_os = "macos")] -pub(super) const DAEMON_SOCKET_GROUP: &str = "staff"; - -#[cfg(target_os = "linux")] -pub(super) const DAEMON_SOCKET_GROUP: &str = "defguard"; - -#[derive(Debug, thiserror::Error)] -pub enum DaemonError { - #[error(transparent)] - WireguardError(#[from] WireguardInterfaceError), - #[error("Unexpected error: {0}")] - Unexpected(String), - #[error(transparent)] - TransportError(#[from] tonic::transport::Error), -} - -#[derive(Debug, Default)] -pub struct DaemonService { - stats_period: Duration, -} - -impl DaemonService { - #[must_use] - pub fn new(config: &Config) -> Self { - Self { - stats_period: Duration::from_secs(config.stats_period), - } - } -} - -type InterfaceDataStream = Pin> + Send>>; - -#[cfg(not(target_os = "macos"))] -#[allow(clippy::result_large_err)] -pub fn setup_wgapi(ifname: &str) -> Result, Status> { - let wgapi = WGApi::::new(ifname.to_string()).map_err(|err| { - let msg = format!("Failed to setup kernel WireGuard API for interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - - Ok(wgapi) -} - -#[cfg(target_os = "macos")] -#[allow(clippy::result_large_err)] -pub fn setup_wgapi(ifname: &str) -> Result, Status> { - let wgapi = WGApi::::new(ifname.to_string()).map_err(|err| { - let msg = format!("Failed to setup userspace WireGuard API for interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - - Ok(wgapi) -} - -#[tonic::async_trait] -impl DesktopDaemonService for DaemonService { - type ReadInterfaceDataStream = InterfaceDataStream; - - async fn create_interface( - &self, - request: tonic::Request, - ) -> Result, Status> { - debug!("Received a request to create a new interface"); - let request = request.into_inner(); - let config: InterfaceConfiguration = request - .config - .ok_or(Status::new( - Code::InvalidArgument, - "Missing interface config in request", - ))? - .into(); - let ifname = &config.name; - let _span = info_span!("create_interface", interface_name = &ifname).entered(); - // setup WireGuard API - let wgapi = setup_wgapi(ifname)?; - - #[cfg(not(windows))] - { - // create new interface - debug!("Creating new interface {ifname}"); - wgapi.create_interface().map_err(|err| { - let msg = format!("Failed to create WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - debug!("Done creating a new interface {ifname}"); - } - - // The WireGuard DNS config value can be a list of IP addresses and domain names, which will - // be used as DNS servers and search domains respectively. - debug!("Preparing DNS configuration for interface {ifname}"); - let dns_string = request.dns.unwrap_or_default(); - let dns_entries = dns_string.split(',').map(str::trim).collect::>(); - // We assume that every entry that can't be parsed as an IP address is a domain name. - let mut dns = Vec::new(); - let mut search_domains = Vec::new(); - for entry in dns_entries { - if let Ok(ip) = entry.parse::() { - dns.push(ip); - } else { - search_domains.push(entry); - } - } - debug!( - "DNS configuration for interface {ifname}: DNS: {dns:?}, Search domains: \ - {search_domains:?}" - ); - - #[cfg(not(windows))] - let configure_interface_result = wgapi.configure_interface(&config); - #[cfg(windows)] - let configure_interface_result = wgapi.configure_interface(&config, &dns, &search_domains); - - configure_interface_result.map_err(|err| { - let msg = format!("Failed to configure WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - - #[cfg(not(windows))] - { - debug!("Configuring interface {ifname} routing"); - wgapi.configure_peer_routing(&config.peers).map_err(|err| { - let msg = - format!("Failed to configure routing for WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - - if dns.is_empty() { - debug!( - "No DNS configuration provided for interface {ifname}, skipping DNS \ - configuration" - ); - } else { - debug!( - "The following DNS servers will be set: {dns:?}, search domains: \ - {search_domains:?}" - ); - wgapi.configure_dns(&dns, &search_domains).map_err(|err| { - let msg = - format!("Failed to configure DNS for WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - } - } - - debug!("Finished creating a new interface {ifname}"); - Ok(Response::new(())) - } - - async fn remove_interface( - &self, - request: tonic::Request, - ) -> Result, Status> { - debug!("Received a request to remove an interface"); - let request = request.into_inner(); - let ifname = request.interface_name; - let _span = info_span!("remove_interface", interface_name = &ifname).entered(); - debug!("Removing interface {ifname}"); - - let wgapi = setup_wgapi(&ifname)?; - - #[cfg(not(windows))] - { - debug!("Cleaning up interface {ifname} routing"); - // Ignore error as this should not be considered fatal, - // e.g. endpoint might fail to resolve DNS name. - if let Err(err) = wgapi.remove_endpoint_routing(&request.endpoint) { - error!( - "Failed to remove routing for endpoint {}: {err}", - request.endpoint - ); - } - } - - wgapi.remove_interface().map_err(|err| { - let msg = format!("Failed to remove WireGuard interface {ifname}: {err}"); - error!("{msg}"); - Status::new(Code::Internal, msg) - })?; - - debug!("Finished removing interface {ifname}"); - Ok(Response::new(())) - } - - async fn read_interface_data( - &self, - request: tonic::Request, - ) -> Result, Status> { - let request = request.into_inner(); - let ifname = request.interface_name; - debug!( - "Received a request to start a new network usage stats data stream for interface \ - {ifname}" - ); - let span = info_span!("read_interface_data", interface_name = &ifname); - - // Setup WireGuard API. - let wgapi = setup_wgapi(&ifname)?; - let mut interval = interval(self.stats_period); - let (tx, rx) = mpsc::channel(64); - - span.in_scope(|| { - info!("Spawning statistics collector task for interface {ifname}"); - }); - - tokio::spawn( - async move { - // Helper map to track if peer data is actually changing to avoid sending duplicate - // stats. - let mut peer_map = HashMap::new(); - - loop { - // Loop delay - interval.tick().await; - debug!( - "Gathering network usage statistics for client's network activity on {ifname}"); - match wgapi.read_interface_data() { - Ok(mut host) => { - let peers = &mut host.peers; - debug!( - "Found {} peers configured on WireGuard interface", - peers.len() - ); - // Filter out never connected peers. - peers.retain(|_, peer| { - // Last handshake time-stamp must exist... - if let Some(last_hs) = peer.last_handshake { - // ...and not be UNIX epoch. - if last_hs != SystemTime::UNIX_EPOCH - && match peer_map.get(&peer.public_key) { - Some(last_peer) => last_peer != peer, - None => true, - } - { - debug!( - "Peer {} statistics changed; keeping it.", - peer.public_key - ); - peer_map.insert(peer.public_key.clone(), peer.clone()); - return true; - } - } - debug!( - "Peer {} statistics didn't change; ignoring it.", - peer.public_key - ); - false - }); - if let Err(err) = tx.send(Ok(host.into())).await { - error!( - "Couldn't send network usage stats update for {ifname}: {err}" - ); - break; - } - } - Err(err) => { - error!( - "Failed to retrieve network usage stats for interface {ifname}: \ - {err}" - ); - break; - } - } - debug!("Network activity statistics for interface {ifname} sent to the client"); - } - debug!( - "The client has disconnected from the network usage statistics data stream \ - for interface {ifname}, stopping the statistics data collection task." - ); - } - .instrument(span), - ); - - let output_stream = ReceiverStream::new(rx); - Ok(Response::new( - Box::pin(output_stream) as Self::ReadInterfaceDataStream - )) - } -} - -#[cfg(unix)] -pub async fn run_server(config: Config) -> anyhow::Result<()> { - debug!("Starting Defguard interface management daemon"); - - let daemon_service = DaemonService::new(&config); - - // Remove existing socket if it exists - if Path::new(DAEMON_SOCKET_PATH).exists() { - fs::remove_file(DAEMON_SOCKET_PATH)?; - } - - let uds = UnixListener::bind(DAEMON_SOCKET_PATH)?; - - // change owner group for socket file - // get the group ID by name - let group = Group::from_name(DAEMON_SOCKET_GROUP)?.ok_or_else(|| { - error!("Group '{DAEMON_SOCKET_GROUP}' not found"); - super::error::Error::InternalError(format!("Group '{DAEMON_SOCKET_GROUP}' not found")) - })?; - - // change ownership - keep current user, change group - chown(DAEMON_SOCKET_PATH, None, Some(group.gid))?; - - // Set socket permissions to allow client access - // 0o660 allows read/write for owner and group only - fs::set_permissions(DAEMON_SOCKET_PATH, fs::Permissions::from_mode(0o660))?; - - let uds_stream = UnixListenerStream::new(uds); - - info!("Defguard daemon version {VERSION} started, listening on socket {DAEMON_SOCKET_PATH}",); - debug!("Defguard daemon configuration: {config:?}"); - - Server::builder() - .trace_fn(|_| tracing::info_span!("defguard_service")) - .add_service(DesktopDaemonServiceServer::new(daemon_service)) - .serve_with_incoming(uds_stream) - .await?; - - Ok(()) -} - -#[cfg(windows)] -pub async fn run_server(config: Config) -> anyhow::Result<()> { - debug!("Starting Defguard interface management daemon"); - - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DAEMON_HTTP_PORT); - let daemon_service = DaemonService::new(&config); - - info!("Defguard daemon version {VERSION} started, listening on {addr}",); - debug!("Defguard daemon configuration: {config:?}"); - - Server::builder() - .trace_fn(|_| tracing::info_span!("defguard_service")) - .add_service(DesktopDaemonServiceServer::new(daemon_service)) - .serve(addr) - .await?; - - Ok(()) -} impl From for proto::InterfaceConfig { fn from(config: InterfaceConfiguration) -> Self { @@ -413,8 +35,9 @@ impl From for proto::InterfaceConfig { .map(ToString::to_string) .collect::>() .join(","), - port: config.port, + port: u32::from(config.port), peers: config.peers.into_iter().map(Into::into).collect(), + mtu: config.mtu, } } } @@ -430,9 +53,9 @@ impl From for InterfaceConfiguration { name: config.name, prvkey: config.prvkey, addresses, - port: config.port, + port: config.port as u16, peers: config.peers.into_iter().map(Into::into).collect(), - mtu: None, + mtu: config.mtu, } } } @@ -490,7 +113,7 @@ impl From for Peer { } } -impl From for InterfaceData { +impl From for proto::InterfaceData { fn from(host: Host) -> Self { Self { listen_port: u32::from(host.listen_port), @@ -501,9 +124,8 @@ impl From for InterfaceData { #[cfg(test)] mod tests { - use std::{str::FromStr, time::SystemTime}; + use std::time::SystemTime; - use defguard_wireguard_rs::{key::Key, net::IpAddrMask}; use x25519_dalek::{EphemeralSecret, PublicKey}; use super::*; diff --git a/src-tauri/src/service/named_pipe.rs b/src-tauri/src/service/named_pipe.rs new file mode 100644 index 00000000..321d76cc --- /dev/null +++ b/src-tauri/src/service/named_pipe.rs @@ -0,0 +1,178 @@ +use std::{os::windows::io::RawHandle, pin::Pin}; + +use async_stream::stream; +use futures_core::stream::Stream; +use tokio::{ + io::{self, AsyncRead, AsyncWrite}, + net::windows::named_pipe::NamedPipeServer, +}; +use tonic::transport::server::Connected; +use windows_sys::Win32::{ + Foundation::{LocalFree, HANDLE, INVALID_HANDLE_VALUE}, + Security::{ + Authorization::{ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1}, + PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, + }, + Storage::FileSystem::{FILE_FLAG_OVERLAPPED, PIPE_ACCESS_DUPLEX}, + System::Pipes::{CreateNamedPipeW, PIPE_TYPE_BYTE}, +}; + +// Named-pipe name used for IPC between defguard client and windows service. +pub static PIPE_NAME: &str = r"\\.\pipe\defguard_daemon"; + +// SDDL defining named pipe ACL: +/// - `SY` (LocalSystem) - full control +/// - `BA` (Administrators) - full control +/// - `BU` (Built-in Users) - read/write +pub static SDDL: &str = "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;BU)"; + +/// Tonic-compatible wrapper around a Windows named pipe server handle. +pub struct TonicNamedPipeServer { + inner: NamedPipeServer, +} + +impl TonicNamedPipeServer { + pub fn new(inner: NamedPipeServer) -> Self { + Self { inner } + } +} + +impl Connected for TonicNamedPipeServer { + type ConnectInfo = (); + + fn connect_info(&self) -> Self::ConnectInfo {} +} + +impl AsyncRead for TonicNamedPipeServer { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.inner).poll_read(cx, buf) + } +} + +impl AsyncWrite for TonicNamedPipeServer { + /// Delegate async write to the underlying pipe. + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + Pin::new(&mut self.inner).poll_write(cx, buf) + } + + /// Delegate flush to the underlying pipe. + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + /// Delegate shutdown to the underlying pipe. + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } +} + +/// Convert a Rust `&str` to a null-terminated UTF-16 buffer suitable for Win32 APIs. +fn str_to_wide_null_terminated(s: &str) -> Vec { + s.encode_utf16().chain(Some(0)).collect() +} + +/// Create a secure Windows named pipe handle with appropriate ACL. +/// Uses `FILE_FLAG_OVERLAPPED` for Tokio compatibility and sets `nMaxInstances = 2` +/// (one client + one service instance). +fn create_secure_pipe() -> Result { + debug!("Creating secure named pipe {PIPE_NAME}"); + + // Compose SDDL: SYSTEM & Administrators full access, users read-write. + let sddl_wide = str_to_wide_null_terminated(SDDL); + + let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut(); + + let result = unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl_wide.as_ptr(), + SDDL_REVISION_1, + &mut descriptor as *mut PSECURITY_DESCRIPTOR, + std::ptr::null_mut(), + ) + }; + if result == 0 { + error!("Error calling ConvertStringSecurityDescriptorToSecurityDescriptorW"); + return Err(std::io::Error::last_os_error()); + } + + // Build SECURITY_ATTRIBUTES pointing to the security descriptor + let attributes = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: descriptor as *mut _, + bInheritHandle: 0, + }; + + let name_wide = str_to_wide_null_terminated(PIPE_NAME); + + let handle = unsafe { + CreateNamedPipeW( + name_wide.as_ptr(), + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE, + // 1 client + 1 service + 2, + 65536, + 65536, + 0, + &attributes, + ) + }; + unsafe { + // Free memory allocated by ConvertStringSecurityDescriptorToSecurityDescriptorW. + LocalFree(descriptor); + } + + if handle == INVALID_HANDLE_VALUE || handle.is_null() { + error!("CreateNamedPipeW returned invalid handle: {handle:?}"); + return Err(std::io::Error::last_os_error()); + } + + info!("Created secure named pipe {PIPE_NAME}"); + Ok(handle) +} + +/// Wrap a raw pipe `HANDLE` into a Tokio `NamedPipeServer`. +fn create_tokio_secure_pipe() -> Result { + debug!("Creating tokio secure pipe"); + let raw = create_secure_pipe()?; + let pipe = unsafe { NamedPipeServer::from_raw_handle(raw as RawHandle)? }; + + info!("Created tokio secure pipe"); + Ok(pipe) +} + +/// Produce a `Stream` of connected pipe servers for `tonic::transport::Server::serve_with_incoming`. +/// +/// Each loop: +/// 1. Creates a fresh listening instance. +/// 2. Awaits a client connection (`connect().await`). +/// 3. Yields the connected `TonicNamedPipeServer`. +pub fn get_named_pipe_server_stream( +) -> Result>, std::io::Error> { + debug!("Creating named pipe server stream"); + let stream = stream! { + let mut server = create_tokio_secure_pipe()?; + + loop { + server.connect().await?; + yield Ok(TonicNamedPipeServer::new(server)); + server = create_tokio_secure_pipe()?; + } + }; + info!("Created named pipe server stream"); + Ok(stream) +} diff --git a/src-tauri/src/service/utils.rs b/src-tauri/src/service/utils.rs index 2572f2ad..7796cf56 100644 --- a/src-tauri/src/service/utils.rs +++ b/src-tauri/src/service/utils.rs @@ -1,60 +1,12 @@ -use std::{io::stdout, sync::LazyLock}; +use std::io::stdout; -#[cfg(unix)] -use hyper_util::rt::TokioIo; -#[cfg(unix)] -use tokio::net::UnixStream; -use tonic::transport::channel::{Channel, Endpoint}; -#[cfg(unix)] -use tonic::transport::Uri; -#[cfg(unix)] -use tower::service_fn; -use tracing::{debug, Level}; +use tracing::Level; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{ fmt, fmt::writer::MakeWriterExt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, }; -use crate::service::{ - proto::desktop_daemon_service_client::DesktopDaemonServiceClient, DAEMON_BASE_URL, -}; - -pub(crate) static DAEMON_CLIENT: LazyLock> = - LazyLock::new(|| { - debug!("Setting up gRPC client"); - let endpoint = Endpoint::from_static(DAEMON_BASE_URL); // Should not panic. - let channel; - #[cfg(unix)] - { - channel = endpoint.connect_with_connector_lazy(service_fn(|_: Uri| async { - // Connect to a Unix domain socket. - let stream = match UnixStream::connect(crate::service::DAEMON_SOCKET_PATH).await { - Ok(stream) => stream, - Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { - error!( - "Permission denied for UNIX domain socket; please refer to \ - https://docs.defguard.net/support-1/troubleshooting#\ - unix-socket-permission-errors-when-desktop-client-attempts-to-connect-\ - to-vpn-on-linux-machines" - ); - return Err(err); - } - Err(err) => { - error!("Problem connecting to UNIX domain socket: {err}"); - return Err(err); - } - }; - Ok::<_, std::io::Error>(TokioIo::new(stream)) - })); - }; - #[cfg(windows)] - { - channel = endpoint.connect_lazy(); - } - DesktopDaemonServiceClient::new(channel) - }); - pub fn logging_setup(log_dir: &str, log_level: &str) -> WorkerGuard { // prepare log file appender let file_appender = tracing_appender::rolling::daily(log_dir, "defguard-service.log"); diff --git a/src-tauri/src/service/windows.rs b/src-tauri/src/service/windows.rs index aa79ab5a..b843cf58 100644 --- a/src-tauri/src/service/windows.rs +++ b/src-tauri/src/service/windows.rs @@ -1,7 +1,12 @@ -use std::{ffi::OsString, sync::mpsc, time::Duration}; +use std::{ + ffi::OsString, + result::Result, + sync::{mpsc, Arc, RwLock}, + time::Duration, +}; use clap::Parser; -use log::error; +use error; use tokio::runtime::Runtime; use windows_service::{ define_windows_service, @@ -10,15 +15,25 @@ use windows_service::{ ServiceType, }, service_control_handler::{register, ServiceControlHandlerResult}, - service_dispatcher, Result, + service_dispatcher, }; -use crate::service::{run_server, utils::logging_setup, Config}; +use crate::{ + enterprise::service_locations::{ + windows::watch_for_login_logoff, ServiceLocationError, ServiceLocationManager, + }, + service::{ + config::Config, + daemon::{run_server, DaemonError}, + utils::logging_setup, + }, +}; static SERVICE_NAME: &str = "DefguardService"; const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; +const LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS: Duration = Duration::from_secs(5); -pub fn run() -> Result<()> { +pub fn run() -> Result<(), windows_service::Error> { // Register generated `ffi_service_main` with the system and start the service, blocking // this thread until the service is stopped. service_dispatcher::start(SERVICE_NAME, ffi_service_main) @@ -33,7 +48,7 @@ pub fn service_main(_arguments: Vec) { } } -fn run_service() -> Result<()> { +fn run_service() -> Result<(), DaemonError> { // Create a channel to be able to poll a stop event from the service worker loop. let (shutdown_tx, shutdown_rx) = mpsc::channel::(); let shutdown_tx_server = shutdown_tx.clone(); @@ -81,12 +96,77 @@ fn run_service() -> Result<()> { std::process::exit(1); })); + let service_location_manager = match ServiceLocationManager::init() { + Ok(api) => { + info!("Service locations storage initialized successfully"); + Ok(api) + } + Err(e) => { + error!( + "Failed to initialize service locations storage: {}. Shutting down service location thread", + e + ); + Err(ServiceLocationError::InitError(e.to_string())) + } + }?; + + let service_location_manager = Arc::new(RwLock::new(service_location_manager)); + + // Spawn service location management task + let service_location_manager_clone = service_location_manager.clone(); runtime.spawn(async move { - let server_result = run_server(config).await; + let manager = service_location_manager_clone; - if server_result.is_err() { - let _ = shutdown_tx_server.send(2); + info!("Starting service location management task"); + + info!("Attempting to auto-connect to service locations"); + match manager.write().unwrap().connect_to_service_locations() { + Ok(_) => { + info!("Auto-connect to service locations completed successfully"); + } + Err(e) => { + warn!( + "Error while trying to auto-connect to service locations: {e}. \ + Will continue monitoring for login/logoff events.", + ); + } } + + info!("Starting login/logoff event monitoring"); + loop { + match watch_for_login_logoff( + manager.clone(), + ).await { + Ok(_) => { + warn!("Login/logoff event monitoring ended unexpectedly. Restarting in {LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS:?}..."); + tokio::time::sleep(LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS).await; + } + Err(e) => { + error!( + "Error in login/logoff event monitoring: {e}. Restarting in {LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS:?}...", + ); + tokio::time::sleep(LOGIN_LOGOFF_MONITORING_RESTART_DELAY_SECS).await; + info!("Restarting login/logoff event monitoring"); + } + } + } + + }); + + // Spawn the main gRPC server task + let service_location_manager_clone = service_location_manager.clone(); + runtime.spawn(async move { + let result = run_server(config, service_location_manager_clone).await; + + let signal = if result.is_err() { + error!("Server task ended with error: {:?}", result.err()); + 2 + } else { + warn!("Server task ended without an error."); + 1 + }; + + let _ = shutdown_tx_server.send(signal); }); loop { diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 987c1358..ebc6e3c0 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,19 +1,32 @@ -use std::{env, path::Path, process::Command, str::FromStr}; - +#[cfg(not(target_os = "macos"))] +use std::str::FromStr; +#[cfg(target_os = "macos")] +use std::time::Duration; +use std::{env, path::Path, process::Command}; + +use base64::{prelude::BASE64_STANDARD, Engine}; +#[cfg(not(target_os = "macos"))] use common::{find_free_tcp_port, get_interface_name}; +#[cfg(not(target_os = "macos"))] use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration}; +use prost::Message; use sqlx::query; use tauri::{AppHandle, Emitter, Manager}; +#[cfg(not(target_os = "macos"))] use tonic::Code; use tracing::Level; -#[cfg(target_os = "windows")] -use winapi::shared::winerror::ERROR_SERVICE_DOES_NOT_EXIST; -#[cfg(target_os = "windows")] +#[cfg(windows)] use windows_service::{ service::{ServiceAccess, ServiceState}, service_manager::{ServiceManager, ServiceManagerAccess}, }; +#[cfg(windows)] +use windows_sys::Win32::Foundation::ERROR_SERVICE_DOES_NOT_EXIST; +#[cfg(windows)] +use crate::active_connections::find_connection; +#[cfg(target_os = "macos")] +use crate::apple::{stop_tunnel_for_location, stop_tunnel_for_tunnel, tunnel_stats}; use crate::{ appstate::AppState, commands::LocationInterfaceDetails, @@ -21,8 +34,7 @@ use crate::{ models::{ connection::{ActiveConnection, Connection}, location::Location, - location_stats::peer_to_location_stats, - tunnel::{peer_to_tunnel_stats, Tunnel, TunnelConnection}, + tunnel::{Tunnel, TunnelConnection}, wireguard_keys::WireguardKeys, Id, }, @@ -31,103 +43,38 @@ use crate::{ error::Error, events::EventKey, log_watcher::service_log_watcher::spawn_log_watcher_task, + proto::ClientPlatformInfo, + ConnectionType, +}; +#[cfg(not(target_os = "macos"))] +use crate::{ + database::models::{location_stats::peer_to_location_stats, tunnel::peer_to_tunnel_stats}, service::{ + client::DAEMON_CLIENT, proto::{CreateInterfaceRequest, ReadInterfaceDataRequest, RemoveInterfaceRequest}, - utils::DAEMON_CLIENT, }, - ConnectionType, }; -#[cfg(target_os = "windows")] -use crate::active_connections::find_connection; - pub(crate) static DEFAULT_ROUTE_IPV4: &str = "0.0.0.0/0"; pub(crate) static DEFAULT_ROUTE_IPV6: &str = "::/0"; +// Work-around MFA propagation delay. FIXME: remove once Core API is corrected. +#[cfg(target_os = "macos")] +static TUNNEL_START_DELAY: Duration = Duration::from_secs(1); -/// Setup client interface +/// Setup client interface for `Instance`. +#[cfg(not(target_os = "macos"))] pub(crate) async fn setup_interface( location: &Location, - interface_name: String, + name: &str, preshared_key: Option, + mtu: Option, pool: &DbPool, -) -> Result<(), Error> { +) -> Result { debug!("Setting up interface for location: {location}"); - - debug!("Looking for WireGuard keys for location {location} instance"); - let Some(keys) = WireguardKeys::find_by_instance_id(pool, location.instance_id).await? else { - error!("No keys found for instance: {}", location.instance_id); - return Err(Error::InternalError( - "No keys found for instance".to_string(), - )); - }; - debug!("WireGuard keys found for location {location} instance"); - - // prepare peer config - debug!( - "Decoding location {location} public key: {}.", - location.pubkey - ); - let peer_key: Key = Key::from_str(&location.pubkey)?; - debug!("Location {location} public key decoded: {peer_key}"); - let mut peer = Peer::new(peer_key); - - debug!( - "Parsing location {location} endpoint: {}", - location.endpoint - ); - peer.set_endpoint(&location.endpoint)?; - peer.persistent_keepalive_interval = Some(25); - debug!("Parsed location {location} endpoint: {}", location.endpoint); - - if let Some(psk) = preshared_key { - debug!("Decoding location {location} preshared key."); - let peer_psk = Key::from_str(&psk)?; - info!("Location {location} preshared key decoded."); - peer.preshared_key = Some(peer_psk); - } - - debug!( - "Parsing location {location} allowed IPs: {}", - location.allowed_ips - ); - let allowed_ips = if location.route_all_traffic { - debug!( - "Using all traffic routing for location {location}: {DEFAULT_ROUTE_IPV4} \ - {DEFAULT_ROUTE_IPV6}" - ); - vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()] - } else { - debug!( - "Using predefined location {location} traffic: {}", - location.allowed_ips - ); - location - .allowed_ips - .split(',') - .map(str::to_string) - .collect() - }; - for allowed_ip in &allowed_ips { - match IpAddrMask::from_str(allowed_ip) { - Ok(addr) => { - peer.allowed_ips.push(addr); - } - Err(err) => { - // Handle the error from IpAddrMask::from_str, if needed - error!( - "Error parsing IP address {allowed_ip} while setting up interface for \ - location {location}, error details: {err}" - ); - } - } - } - debug!( - "Parsed allowed IPs for location {location}: {:?}", - peer.allowed_ips - ); + let interface_name = get_interface_name(name); // request interface configuration - debug!("Looking for a free port for interface {interface_name}..."); + debug!("Looking for a free port for interface {interface_name}."); let Some(port) = find_free_tcp_port() else { let msg = format!( "Couldn't find free port during interface {interface_name} setup for location \ @@ -137,29 +84,14 @@ pub(crate) async fn setup_interface( return Err(Error::InternalError(msg)); }; debug!("Found free port: {port} for interface {interface_name}."); - let addresses = location - .address - .split(',') - .map(str::trim) - .map(IpAddrMask::from_str) - .collect::>() - .map_err(|err| { - let msg = format!("Failed to parse IP addresses '{}': {err}", location.address); - error!("{msg}"); - Error::InternalError(msg) - })?; - let interface_config = InterfaceConfiguration { - name: interface_name, - prvkey: keys.prvkey, - addresses, - port: port.into(), - peers: vec![peer.clone()], - mtu: None, - }; + + let mut interface_config = location + .interface_configuration(pool, interface_name.clone(), preshared_key, mtu) + .await?; + interface_config.mtu = mtu; debug!("Creating interface for location {location} with configuration {interface_config:?}"); let request = CreateInterfaceRequest { config: Some(interface_config.clone().into()), - allowed_ips, dns: location.dns.clone(), }; if let Err(error) = DAEMON_CLIENT.clone().create_interface(request).await { @@ -189,15 +121,106 @@ pub(crate) async fn setup_interface( name: {}.", interface_config.name ); - Ok(()) + Ok(interface_name) } } -pub(crate) async fn stats_handler( - pool: DbPool, - interface_name: String, - connection_type: ConnectionType, -) { +#[cfg(target_os = "macos")] +pub(crate) async fn setup_interface( + location: &Location, + _name: &str, + preshared_key: Option, + mtu: Option, + pool: &DbPool, +) -> Result { + let tunnel_config = location + .tunnel_configurarion(pool, preshared_key, mtu) + .await?; + + tunnel_config.save(); + tokio::time::sleep(TUNNEL_START_DELAY).await; + tunnel_config.start_tunnel(); + + // FIXME: not really useful nor true. + Ok(String::new()) +} + +#[cfg(target_os = "macos")] +pub(crate) async fn stats_handler(id: Id, connection_type: ConnectionType) { + use crate::database::models::{location_stats::LocationStats, tunnel::TunnelStats}; + + const CHECK_INTERVAL: Duration = Duration::from_secs(10); + + debug!("Starting stats handler for ID {id} and connection type {connection_type:?}"); + + let mut interval = tokio::time::interval(CHECK_INTERVAL); + let pool = DB_POOL.clone(); + + loop { + debug!("Waiting for the next stats collection interval for ID {id} and connection type {connection_type:?}"); + interval.tick().await; + + let stats = tunnel_stats(id, &connection_type); + let Some(stats) = stats else { + continue; + }; + + let mut transaction = match pool.begin().await { + Ok(transactions) => transactions, + Err(err) => { + error!( + "Failed to begin database transaction for saving location/tunnel stats: {err}", + ); + continue; + } + }; + + if connection_type == ConnectionType::Location { + let location_stats = LocationStats::new( + id, + stats.tx_bytes as i64, + stats.rx_bytes as i64, + stats.last_handshake as i64, + 0, + None, + ); + match location_stats.save(&mut *transaction).await { + Ok(_) => { + debug!("Saved network usage stats for location ID {id}"); + } + Err(err) => { + error!("Failed to save network usage stats for location ID {id}: {err}"); + } + } + } else { + let tunnel_stats = TunnelStats::new( + id, + stats.tx_bytes as i64, + stats.rx_bytes as i64, + stats.last_handshake as i64, + chrono::Utc::now().naive_utc(), + 0, + 0, + ); + match tunnel_stats.save(&mut *transaction).await { + Ok(_) => { + debug!("Saved network usage stats for tunnel ID {id}"); + } + Err(err) => { + error!("Failed to save network usage stats for tunnel ID {id}: {err}"); + } + } + } + + if let Err(err) = transaction.commit().await { + error!("Failed to commit database transaction for saving location/tunnel stats: {err}"); + } + } +} + +#[cfg(not(target_os = "macos"))] +pub(crate) async fn stats_handler(interface_name: String, connection_type: ConnectionType) { + let pool = DB_POOL.clone(); let request = ReadInterfaceDataRequest { interface_name: interface_name.clone(), }; @@ -331,24 +354,27 @@ pub fn load_log_targets() -> Vec { Vec::new() } -// helper function to get log file directory for the defguard-service daemon +/// Helper function to get log file directory for `defguard-service` daemon. #[must_use] pub fn get_service_log_dir() -> &'static Path { - #[cfg(target_os = "windows")] + #[cfg(windows)] let path = "/Logs/defguard-service"; - #[cfg(not(target_os = "windows"))] + #[cfg(not(windows))] let path = "/var/log/defguard-service"; Path::new(path) } /// Setup client interface +#[cfg(not(target_os = "macos"))] pub async fn setup_interface_tunnel( tunnel: &Tunnel, - interface_name: String, -) -> Result<(), Error> { + name: &str, + mtu: Option, +) -> Result { debug!("Setting up interface for tunnel {tunnel}"); + let interface_name = get_interface_name(name); // prepare peer config debug!( "Decoding tunnel {tunnel} public key: {}.", @@ -380,10 +406,7 @@ pub async fn setup_interface_tunnel( tunnel.allowed_ips ); let allowed_ips = if tunnel.route_all_traffic { - debug!( - "Using all traffic routing for tunnel {tunnel}: {DEFAULT_ROUTE_IPV4} \ - {DEFAULT_ROUTE_IPV6}" - ); + debug!("Using all traffic routing for tunnel {tunnel}"); vec![DEFAULT_ROUTE_IPV4.into(), DEFAULT_ROUTE_IPV6.into()] } else { let msg = match &tunnel.allowed_ips { @@ -412,7 +435,7 @@ pub async fn setup_interface_tunnel( debug!("Parsed tunnel {tunnel} allowed IPs: {:?}", peer.allowed_ips); // request interface configuration - debug!("Looking for a free port for interface {interface_name}..."); + debug!("Looking for a free port for interface {interface_name}."); let Some(port) = find_free_tcp_port() else { let msg = format!( "Couldn't find free port for interface {interface_name} while setting up tunnel {tunnel}" @@ -434,17 +457,17 @@ pub async fn setup_interface_tunnel( Error::InternalError(msg) })?; let interface_config = InterfaceConfiguration { - name: interface_name, + name: interface_name.clone(), prvkey: tunnel.prvkey.clone(), addresses, - port: port.into(), + port, peers: vec![peer.clone()], - mtu: None, + mtu, }; + debug!("Creating interface {interface_config:?}"); let request = CreateInterfaceRequest { config: Some(interface_config.clone().into()), - allowed_ips, dns: tunnel.dns.clone(), }; if let Some(pre_up) = &tunnel.pre_up { @@ -465,12 +488,12 @@ pub async fn setup_interface_tunnel( "Failed to create a network interface ({}) for tunnel {tunnel}: {error}", interface_config.name ); - Err(Error::InternalError(format!( + return Err(Error::InternalError(format!( "Failed to create a network interface ({}) for tunnel {tunnel}, error message: {}. \ Check logs for more details.", interface_config.name, error.message() - ))) + ))); } else { info!( "Network interface {} for tunnel {tunnel} created successfully.", @@ -493,8 +516,27 @@ pub async fn setup_interface_tunnel( "Created interface {} with config: {interface_config:?}", interface_config.name ); - Ok(()) } + + Ok(interface_name) +} + +#[cfg(target_os = "macos")] +pub async fn setup_interface_tunnel( + tunnel: &Tunnel, + _name: &str, + mtu: Option, +) -> Result { + debug!("Setting up interface for tunnel: {tunnel}"); + + let tunnel_config = tunnel.tunnel_configurarion(mtu)?; + + tunnel_config.save(); + tokio::time::sleep(TUNNEL_START_DELAY).await; + tunnel_config.start_tunnel(); + + // FIXME: not really useful nor true. + Ok(String::new()) } pub async fn get_tunnel_interface_details( @@ -507,7 +549,15 @@ pub async fn get_tunnel_interface_details( let peer_pubkey = &tunnel.pubkey; // generate interface name - let interface_name = get_interface_name(&tunnel.name); + let interface_name; + #[cfg(not(target_os = "macos"))] + { + interface_name = get_interface_name(&tunnel.name); + } + #[cfg(target_os = "macos")] + { + interface_name = String::new(); + }; debug!("Fetching tunnel stats for tunnel ID {tunnel_id}"); let result = query!( @@ -538,7 +588,7 @@ pub async fn get_tunnel_interface_details( address: tunnel.address, dns: tunnel.dns, listen_port, - peer_pubkey: peer_pubkey.to_string(), + peer_pubkey: peer_pubkey.clone(), peer_endpoint: tunnel.endpoint, allowed_ips: tunnel.allowed_ips.unwrap_or_default(), persistent_keepalive_interval, @@ -566,8 +616,15 @@ pub async fn get_location_interface_details( ); let peer_pubkey = keys.pubkey; - // generate interface name - let interface_name = get_interface_name(&location.name); + let interface_name; + #[cfg(not(target_os = "macos"))] + { + interface_name = get_interface_name(&location.name); + } + #[cfg(target_os = "macos")] + { + interface_name = String::new(); + } debug!("Fetching location stats for location ID {location_id}"); let result = query!( @@ -619,8 +676,13 @@ pub(crate) async fn handle_connection_for_location( ) -> Result<(), Error> { debug!("Setting up the connection for location {}", location.name); let state = handle.state::(); - let interface_name = get_interface_name(&location.name); - setup_interface(location, interface_name.clone(), preshared_key, &DB_POOL).await?; + let mtu = state + .app_config + .lock() + .expect("failed to lock app state") + .mtu(); + let interface_name = + setup_interface(location, &location.name, preshared_key, mtu, &DB_POOL).await?; state .add_connection(location.id, &interface_name, ConnectionType::Location) .await; @@ -651,8 +713,12 @@ pub(crate) async fn handle_connection_for_tunnel( ) -> Result<(), Error> { debug!("Setting up the connection for tunnel: {}", tunnel.name); let state = handle.state::(); - let interface_name = get_interface_name(&tunnel.name); - setup_interface_tunnel(tunnel, interface_name.clone()).await?; + let mtu = state + .app_config + .lock() + .expect("failed to lock app state") + .mtu(); + let interface_name = setup_interface_tunnel(tunnel, &tunnel.name, mtu).await?; state .add_connection(tunnel.id, &interface_name, ConnectionType::Tunnel) .await; @@ -708,7 +774,6 @@ pub(crate) async fn disconnect_interface( "Disconnecting interface {}.", active_connection.interface_name ); - let mut client = DAEMON_CLIENT.clone(); let location_id = active_connection.location_id; let interface_name = active_connection.interface_name.clone(); @@ -721,32 +786,48 @@ pub(crate) async fn disconnect_interface( ); return Err(Error::NotFound); }; - let request = RemoveInterfaceRequest { - interface_name, - endpoint: location.endpoint.clone(), - }; - debug!( - "Sending request to the background service to remove interface {} for location \ - {}...", - active_connection.interface_name, location.name - ); - if let Err(error) = client.remove_interface(request).await { - let msg = if error.code() == Code::Unavailable { - format!( - "Couldn't remove interface {}. Background service is unavailable. \ - Please make sure the service is running. Error: {error}.", - active_connection.interface_name - ) - } else { - format!( - "Failed to send a request to the background service to remove interface \ - {}. Error: {error}.", - active_connection.interface_name - ) + + #[cfg(target_os = "macos")] + { + let result = stop_tunnel_for_location(&location); + error!( + "stop_tunnel() for location {} returned {result:?}", + location.name + ); + if !result { + return Err(Error::InternalError("Error from tunnel".into())); + } + } + + #[cfg(not(target_os = "macos"))] + { + let request = RemoveInterfaceRequest { + interface_name, + endpoint: location.endpoint.clone(), }; - error!("{msg}"); - return Err(Error::InternalError(msg)); + debug!( + "Sending request to the background service to remove interface {} for location \ + {}...", + active_connection.interface_name, location.name + ); + if let Err(error) = DAEMON_CLIENT.clone().remove_interface(request).await { + let msg = if error.code() == Code::Unavailable { + format!( + "Couldn't remove interface {}. Background service is unavailable. \ + Please make sure the service is running. Error: {error}.", + active_connection.interface_name + ) + } else { + format!( + "Failed to send a request to the background service to remove interface \ + {}. Error: {error}.", + active_connection.interface_name + ) + }; + error!("{msg}"); + } } + let connection: Connection = active_connection.into(); let connection = connection.save(&*DB_POOL).await?; debug!( @@ -781,19 +862,35 @@ pub(crate) async fn disconnect_interface( active_connection.interface_name ); } - let request = RemoveInterfaceRequest { - interface_name, - endpoint: tunnel.endpoint.clone(), - }; - if let Err(error) = client.remove_interface(request).await { + + #[cfg(target_os = "macos")] + { + let result = stop_tunnel_for_tunnel(&tunnel); error!( - "Error while removing interface {}, error details: {error:?}", - active_connection.interface_name + "stop_tunnel() for tunnel {} returned {result:?}", + tunnel.name ); - return Err(Error::InternalError(format!( - "Failed to remove interface, error message: {}", - error.message() - ))); + if !result { + return Err(Error::InternalError("Error from tunnel".into())); + } + } + + #[cfg(not(target_os = "macos"))] + { + let request = RemoveInterfaceRequest { + interface_name, + endpoint: tunnel.endpoint.clone(), + }; + if let Err(error) = DAEMON_CLIENT.clone().remove_interface(request).await { + error!( + "Error while removing interface {}, error details: {error:?}", + active_connection.interface_name + ); + return Err(Error::InternalError(format!( + "Failed to remove interface, error message: {}", + error.message() + ))); + } } if let Some(post_down) = &tunnel.post_down { debug!( @@ -855,7 +952,7 @@ pub async fn get_tunnel_or_location_name(id: Id, connection_type: ConnectionType // Check if location/tunnel is connected and WireGuard Windows service is running. // `id`: location or tunnel Id // `name`: location or tunnel name -#[cfg(target_os = "windows")] +#[cfg(windows)] async fn check_connection( service_manager: &ServiceManager, id: Id, @@ -939,10 +1036,10 @@ async fn check_connection( // TODO: Move the connection handling to a seperate, common function, // so `handle_connection_for_location` and `handle_connection_for_tunnel` are not // partially duplicated here. -#[cfg(target_os = "windows")] +#[cfg(windows)] pub async fn sync_connections(app_handle: &AppHandle) -> Result<(), Error> { debug!("Synchronizing active connections with the systems' state..."); - let all_locations = Location::all(&*DB_POOL).await?; + let all_locations = Location::all(&*DB_POOL, false).await?; let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT).map_err( |err| { @@ -990,3 +1087,33 @@ pub async fn sync_connections(app_handle: &AppHandle) -> Result<(), Error> { Ok(()) } + +#[must_use] +pub(crate) fn construct_platform_header() -> String { + let os = os_info::get(); + + let platform_info = ClientPlatformInfo { + os_family: std::env::consts::OS.to_string(), + os_type: os.os_type().to_string(), + version: os.version().to_string(), + edition: os.edition().map(str::to_string), + codename: os.codename().map(str::to_string), + bitness: Some(os.bitness().to_string()), + architecture: os.architecture().map(str::to_string), + }; + + debug!("Constructed platform info header: {platform_info:?}"); + + let buffer = platform_info.encode_to_vec(); + + BASE64_STANDARD.encode(buffer) +} + +#[must_use] +/// Utility function to get all tunnels and locations from the database. +#[cfg(target_os = "macos")] +pub async fn get_all_tunnels_locations() -> (Vec>, Vec>) { + let tunnels = Tunnel::all(&*DB_POOL).await.unwrap_or_default(); + let locations = Location::all(&*DB_POOL, false).await.unwrap_or_default(); + (tunnels, locations) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3ca24233..92c0eddb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,7 +10,10 @@ "active": true, "category": "Utility", "copyright": "Defguard", - "targets": ["deb", "app"], + "targets": [ + "deb", + "app" + ], "externalBin": [], "icon": [ "icons/32x32.png", @@ -24,18 +27,32 @@ "digestAlgorithm": "sha256", "timestampUrl": "", "wix": { - "fragmentPaths": ["./resources-windows/service-fragment.wxs"], - "componentRefs": ["DefGuardServiceFragment"] + "upgradeCode": "923b21f5-7d3f-4f5e-8dcb-43fe1c65fb43", + "bannerPath": "./resources-windows/msi/top_banner.png", + "dialogImagePath": "./resources-windows/msi/side_banner.png", + "fragmentPaths": [ + "./resources-windows/fragments/service.wxs", + "./resources-windows/fragments/provisioning.wxs" + ], + "componentRefs": [ + "DefguardServiceFragment", + "ProvisioningScriptFragment" + ], + "template": "./resources-windows/msi/main.wxs" } }, "macOS": { - "entitlements": null, - "exceptionDomain": "", - "frameworks": [], - "providerShortName": null, - "signingIdentity": null + "bundleVersion": "@BUILD_NUMBER@", + "entitlements": "./Client.entitlements", + "files": { + "embedded.provisionprofile": "Defguard_Client_Mac_App_Store.provisionprofile", + "PlugIns/VPNExtension.appex": "../swift/extension/build/Release/VPNExtension.appex" + }, + "minimumSystemVersion": "13.5" }, - "resources": ["resources/icons/*"], + "resources": [ + "resources/icons/*" + ], "shortDescription": "Defguard desktop client", "longDescription": "Defguard desktop client", "linux": { @@ -47,26 +64,34 @@ "../control/postinst": "../resources-linux/postinst", "../control/prerm": "../resources-linux/prerm", "../control/postrm": "../resources-linux/postrm" - } + }, + "depends": [ + "desktop-file-utils" + ] }, "rpm": { "files": { "/usr/sbin/defguard-service": "target/release/defguard-service", "/lib/systemd/system/defguard-service.service": "../resources-linux/defguard-service.service" }, + "depends": [ + "desktop-file-utils" + ], "postInstallScript": "../resources-linux/postinst", "preRemoveScript": "../resources-linux/prerm", "postRemoveScript": "../resources-linux/postrm" } } }, - "productName": "defguard-client", + "productName": "Defguard", "mainBinaryName": "defguard-client", "identifier": "net.defguard", - "version": "1.5.3", + "version": "1.6.1", "app": { "security": { - "capabilities": ["main-capability"], + "capabilities": [ + "main-capability" + ], "csp": null }, "windows": [ @@ -90,7 +115,9 @@ "plugins": { "deep-link": { "desktop": { - "schemes": ["defguard"] + "schemes": [ + "defguard" + ] } } } diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 1ceacbf7..c946ac5d 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -1,5 +1,10 @@ { + "build": { + "features": [ + "service" + ] + }, "bundle": { "longDescription": "IMPORTANT: Reboot or Re-login Required\nOn initial install the user is added to the defguard group.\nA reboot or logging out and back in is required for group membership changes to take effect.\nThis is not required on subsequent updates." } -} \ No newline at end of file +} diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index bb2a7dfc..39e50dd0 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -1,11 +1,8 @@ { - "bundle": { - "externalBin": [ - "resources-macos/binaries/*" - ], - "resources": [ - "resources-macos/resources/*", - "resources/icons/*" - ] + "build": { + "beforeBundleCommand": { + "cwd": "../swift", + "script": "./build.sh" + } } } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 5027918d..eb86e8c7 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,8 +1,16 @@ { + "build": { + "features": [ + "service" + ] + }, "bundle": { - "targets": ["msi"], + "targets": [ + "msi" + ], "resources": [ "resources-windows/binaries/*", + "resources-windows/scripts/*", "resources/icons/*" ] } diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index df7132cf..8b70f22c 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -4,8 +4,10 @@ import '../../shared/scss/index.scss'; import { QueryClient } from '@tanstack/query-core'; import { QueryClientProvider } from '@tanstack/react-query'; +import { getVersion } from '@tauri-apps/api/app'; import { debug, info } from '@tauri-apps/plugin-log'; import { openUrl } from '@tauri-apps/plugin-opener'; +import { exit } from '@tauri-apps/plugin-process'; import dayjs from 'dayjs'; import customParseData from 'dayjs/plugin/customParseFormat'; import duration from 'dayjs/plugin/duration'; @@ -15,6 +17,7 @@ import timezone from 'dayjs/plugin/timezone'; import updateLocale from 'dayjs/plugin/updateLocale'; import utc from 'dayjs/plugin/utc'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import { localStorageDetector } from 'typesafe-i18n/detectors'; import TypesafeI18n from '../../i18n/i18n-react'; @@ -22,6 +25,7 @@ import { detectLocale } from '../../i18n/i18n-util'; import { loadLocaleAsync } from '../../i18n/i18n-util.async'; import { ClientPage } from '../../pages/client/ClientPage'; import { clientApi } from '../../pages/client/clientAPI/clientApi'; +import type { PlatformInfo } from '../../pages/client/clientAPI/types'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; import { CarouselPage } from '../../pages/client/pages/CarouselPage/CarouselPage'; import { ClientAddedPage } from '../../pages/client/pages/ClientAddedPage/ClientAddedPage'; @@ -38,8 +42,6 @@ import { useTheme } from '../../shared/defguard-ui/hooks/theme/useTheme'; import { ThemeProvider } from '../../shared/providers/ThemeProvider/ThemeProvider'; import { routes } from '../../shared/routes'; import { ApplicationUpdateManager } from '../ApplicationUpdateManager/ApplicationUpdateManager'; -import { exit } from '@tauri-apps/plugin-process'; -import { useHotkeys } from 'react-hotkeys-hook'; dayjs.extend(duration); dayjs.extend(utc); @@ -127,12 +129,13 @@ export const App = () => { const localeLoadRef = useRef(false); const [localeLoaded, setWasLoaded] = useState(false); const [settingsLoaded, setSettingsLoaded] = useState(false); + const [platformInfoLoaded, setPlatformInfoLoaded] = useState(false); const setClientState = useClientStore((state) => state.setState); const { changeTheme } = useTheme(); const appLoaded = useMemo( - () => localeLoaded && settingsLoaded, - [localeLoaded, settingsLoaded], + () => localeLoaded && settingsLoaded && platformInfoLoaded, + [localeLoaded, settingsLoaded, platformInfoLoaded], ); // load locales @@ -190,10 +193,26 @@ export const App = () => { // register ctrl+q keyboard shortcut useHotkeys('ctrl+q', () => { - info("Ctrl-Q pressed, exiting."); - exit(0); + info('Ctrl-Q pressed, exiting.'); + exit(0); }); + useEffect(() => { + const loadPlatformInfo = async () => { + debug('Loading platform info from Tauri'); + const version = await getVersion().catch(() => 'unknown'); + const platformHeader = await clientApi.getPlatformHeader(); + const platformInfo: PlatformInfo = { + client_version: `${version}`, + platform_info: platformHeader, + }; + setClientState({ platformInfo }); + debug('Platform info loaded from Tauri'); + setPlatformInfoLoaded(true); + }; + void loadPlatformInfo(); + }, [setClientState]); + if (!appLoaded) return null; return ( diff --git a/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx b/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx index 0a756419..393ac619 100644 --- a/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx +++ b/src/components/ApplicationUpdateManager/ApplicationUpdateManager.tsx @@ -1,7 +1,7 @@ import { getVersion } from '@tauri-apps/api/app'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; +import { error } from '@tauri-apps/plugin-log'; import { useEffect, useState } from 'react'; - import { clientApi } from '../../pages/client/clientAPI/clientApi.ts'; import { useClientStore } from '../../pages/client/hooks/useClientStore'; import { TauriEventKey } from '../../pages/client/types'; @@ -71,16 +71,20 @@ export const ApplicationUpdateManager = () => { const getNewVersion = async (appVersion: string) => { if (!appVersion) return; - const response = await getLatestAppVersion(); - - setApplicationUpdateData({ - currentVersion: appVersion, - latestVersion: response.version, - releaseDate: response.release_date, - releaseNotesUrl: response.release_notes_url, - updateUrl: response.update_url, - dismissed: false, - }); + try { + const response = await getLatestAppVersion(); + + setApplicationUpdateData({ + currentVersion: appVersion, + latestVersion: response.version, + releaseDate: response.release_date, + releaseNotesUrl: response.release_notes_url, + updateUrl: response.update_url, + dismissed: false, + }); + } catch (e) { + error(`Failed to check latest app version: ${e}`); + } }; getNewVersion(appVersion); diff --git a/src/components/AutoProvisioningManager.tsx b/src/components/AutoProvisioningManager.tsx new file mode 100644 index 00000000..536418d3 --- /dev/null +++ b/src/components/AutoProvisioningManager.tsx @@ -0,0 +1,46 @@ +import { useQuery } from '@tanstack/react-query'; +import { error } from '@tauri-apps/plugin-log'; +import { type PropsWithChildren, useEffect } from 'react'; +import { clientApi } from '../pages/client/clientAPI/clientApi'; +import type { ProvisioningConfig } from '../pages/client/clientAPI/types'; +import { clientQueryKeys } from '../pages/client/query'; +import { useToaster } from '../shared/defguard-ui/hooks/toasts/useToaster'; +import useAddInstance from '../shared/hooks/useAddInstance'; + +const { getProvisioningConfig } = clientApi; + +export default function AutoProvisioningManager({ children }: PropsWithChildren) { + const toaster = useToaster(); + const { handleAddInstance } = useAddInstance(); + const { data: provisioningConfig } = useQuery({ + queryFn: getProvisioningConfig, + queryKey: [clientQueryKeys.getProvisioningConfig], + refetchOnMount: false, + refetchOnWindowFocus: false, + }); + + const handleProvisioning = async (config: ProvisioningConfig) => { + try { + await handleAddInstance({ + url: config.enrollment_url, + token: config.enrollment_token, + }); + } catch (e) { + error( + `Failed to handle automatic client provisioning with ${JSON.stringify(config)}.\n Error: ${JSON.stringify(e)}`, + ); + toaster.error( + 'Automatic client provisioning failed, please contact your administrator.', + ); + } + }; + + // biome-ignore lint/correctness/useExhaustiveDependencies: migration, checkMeLater + useEffect(() => { + if (provisioningConfig) { + handleProvisioning(provisioningConfig); + } + }, [provisioningConfig]); + + return <>{children}; +} diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts index 2dca9b4f..20b3c0e7 100644 --- a/src/i18n/en/index.ts +++ b/src/i18n/en/index.ts @@ -161,6 +161,11 @@ If you are an admin/devops - all your customers (instances) and all their tunnel helper: 'If active connection exceeds given time without making an handshake with the server. The connection will be considered invalid and disconnected automatically.', }, + mtu: { + title: 'MTU (Maximum Transmission Unit)', + helper: + 'MTU sets the largest packet size sent through the network. Lowering it can improve connection stability in restrictive or unreliable ISP networks. The default value on most systems is 1500. Try lowering it to 1300-1400 if you encounter ISP-related issues. 0 = default.', + }, tray: { title: 'System tray', label: 'Tray icon theme', @@ -184,11 +189,11 @@ If you are an admin/devops - all your customers (instances) and all their tunnel globalLogs: { logSources: { client: 'Client', - service: 'Service', + vpn: 'VPN', all: 'All', }, logSourceHelper: - 'The source of the logs. Logs can come from the Defguard client or the background Defguard service that manages VPN conncetions at the network level.', + 'The source of the logs. Logs can come from the Defguard client or the VPN service/extension that manages VPN connections at the network level.', }, theme: { title: 'Theme', @@ -765,6 +770,8 @@ If you want to disengage your VPN connection, simply press "deactivate". mfaNotConfigured: 'Selected method has not been configured.', mfaStartGeneric: 'Could not start MFA process. Please try again or contact administrator.', + mfaFinishGeneric: + 'Could not finish MFA process. Please try again or contact administrator.', instanceNotFound: 'Could not find instance.', locationNotSpecified: 'Location is not specified.', invalidCode: diff --git a/src/i18n/fr/index.ts b/src/i18n/fr/index.ts index 1c4e24d3..7b7aa7af 100644 --- a/src/i18n/fr/index.ts +++ b/src/i18n/fr/index.ts @@ -175,11 +175,11 @@ Si vous êtes un administrateur/devops - tous vos clients (instances) et tous le globalLogs: { logSources: { client: 'Client', - service: 'Service', + vpn: 'VPN', all: 'Tous', }, logSourceHelper: - 'La source des journaux. Les journaux peuvent provenir du client Defguard ou du service Defguard en arrière-plan qui gère les connexions VPN au niveau du réseau.', + 'La source des journaux. Les journaux peuvent provenir du client Defguard ou du service/extension VPN qui gère les connexions VPN au niveau du réseau.', }, theme: { title: 'Thème', diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts index 088f2ae0..a88a420e 100644 --- a/src/i18n/i18n-types.ts +++ b/src/i18n/i18n-types.ts @@ -376,6 +376,16 @@ type RootTranslation = { */ helper: string } + mtu: { + /** + * M​T​U​ ​(​M​a​x​i​m​u​m​ ​T​r​a​n​s​m​i​s​s​i​o​n​ ​U​n​i​t​) + */ + title: string + /** + * M​T​U​ ​s​e​t​s​ ​t​h​e​ ​l​a​r​g​e​s​t​ ​p​a​c​k​e​t​ ​s​i​z​e​ ​s​e​n​t​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​n​e​t​w​o​r​k​.​ ​L​o​w​e​r​i​n​g​ ​i​t​ ​c​a​n​ ​i​m​p​r​o​v​e​ ​c​o​n​n​e​c​t​i​o​n​ ​s​t​a​b​i​l​i​t​y​ ​i​n​ ​r​e​s​t​r​i​c​t​i​v​e​ ​o​r​ ​u​n​r​e​l​i​a​b​l​e​ ​I​S​P​ ​n​e​t​w​o​r​k​s​.​ ​T​h​e​ ​d​e​f​a​u​l​t​ ​v​a​l​u​e​ ​o​n​ ​m​o​s​t​ ​s​y​s​t​e​m​s​ ​i​s​ ​1​5​0​0​.​ ​T​r​y​ ​l​o​w​e​r​i​n​g​ ​i​t​ ​t​o​ ​1​3​0​0​-​1​4​0​0​ ​i​f​ ​y​o​u​ ​e​n​c​o​u​n​t​e​r​ ​I​S​P​-​r​e​l​a​t​e​d​ ​i​s​s​u​e​s​.​ ​0​ ​=​ ​d​e​f​a​u​l​t​. + */ + helper: string + } tray: { /** * S​y​s​t​e​m​ ​t​r​a​y @@ -439,16 +449,16 @@ type RootTranslation = { */ client: string /** - * S​e​r​v​i​c​e + * V​P​N */ - service: string + vpn: string /** * A​l​l */ all: string } /** - * T​h​e​ ​s​o​u​r​c​e​ ​o​f​ ​t​h​e​ ​l​o​g​s​.​ ​L​o​g​s​ ​c​a​n​ ​c​o​m​e​ ​f​r​o​m​ ​t​h​e​ ​D​e​f​g​u​a​r​d​ ​c​l​i​e​n​t​ ​o​r​ ​t​h​e​ ​b​a​c​k​g​r​o​u​n​d​ ​D​e​f​g​u​a​r​d​ ​s​e​r​v​i​c​e​ ​t​h​a​t​ ​m​a​n​a​g​e​s​ ​V​P​N​ ​c​o​n​n​c​e​t​i​o​n​s​ ​a​t​ ​t​h​e​ ​n​e​t​w​o​r​k​ ​l​e​v​e​l​. + * T​h​e​ ​s​o​u​r​c​e​ ​o​f​ ​t​h​e​ ​l​o​g​s​.​ ​L​o​g​s​ ​c​a​n​ ​c​o​m​e​ ​f​r​o​m​ ​t​h​e​ ​D​e​f​g​u​a​r​d​ ​c​l​i​e​n​t​ ​o​r​ ​t​h​e​ ​V​P​N​ ​s​e​r​v​i​c​e​/​e​x​t​e​n​s​i​o​n​ ​t​h​a​t​ ​m​a​n​a​g​e​s​ ​V​P​N​ ​c​o​n​n​e​c​t​i​o​n​s​ ​a​t​ ​t​h​e​ ​n​e​t​w​o​r​k​ ​l​e​v​e​l​. */ logSourceHelper: string } @@ -1692,6 +1702,10 @@ type RootTranslation = { * C​o​u​l​d​ ​n​o​t​ ​s​t​a​r​t​ ​M​F​A​ ​p​r​o​c​e​s​s​.​ ​P​l​e​a​s​e​ ​t​r​y​ ​a​g​a​i​n​ ​o​r​ ​c​o​n​t​a​c​t​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​. */ mfaStartGeneric: string + /** + * C​o​u​l​d​ ​n​o​t​ ​f​i​n​i​s​h​ ​M​F​A​ ​p​r​o​c​e​s​s​.​ ​P​l​e​a​s​e​ ​t​r​y​ ​a​g​a​i​n​ ​o​r​ ​c​o​n​t​a​c​t​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​. + */ + mfaFinishGeneric: string /** * C​o​u​l​d​ ​n​o​t​ ​f​i​n​d​ ​i​n​s​t​a​n​c​e​. */ @@ -2066,6 +2080,16 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + mtu: { + /** + * MTU (Maximum Transmission Unit) + */ + title: () => LocalizedString + /** + * MTU sets the largest packet size sent through the network. Lowering it can improve connection stability in restrictive or unreliable ISP networks. The default value on most systems is 1500. Try lowering it to 1300-1400 if you encounter ISP-related issues. 0 = default. + */ + helper: () => LocalizedString + } tray: { /** * System tray @@ -2129,16 +2153,16 @@ export type TranslationFunctions = { */ client: () => LocalizedString /** - * Service + * VPN */ - service: () => LocalizedString + vpn: () => LocalizedString /** * All */ all: () => LocalizedString } /** - * The source of the logs. Logs can come from the Defguard client or the background Defguard service that manages VPN conncetions at the network level. + * The source of the logs. Logs can come from the Defguard client or the VPN service/extension that manages VPN connections at the network level. */ logSourceHelper: () => LocalizedString } @@ -3371,6 +3395,10 @@ export type TranslationFunctions = { * Could not start MFA process. Please try again or contact administrator. */ mfaStartGeneric: () => LocalizedString + /** + * Could not finish MFA process. Please try again or contact administrator. + */ + mfaFinishGeneric: () => LocalizedString /** * Could not find instance. */ diff --git a/src/pages/client/ClientPage.tsx b/src/pages/client/ClientPage.tsx index 74b04bd4..9358bf44 100644 --- a/src/pages/client/ClientPage.tsx +++ b/src/pages/client/ClientPage.tsx @@ -5,7 +5,7 @@ import { listen } from '@tauri-apps/api/event'; import { useEffect } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { shallow } from 'zustand/shallow'; - +import AutoProvisioningManager from '../../components/AutoProvisioningManager'; import { useI18nContext } from '../../i18n/i18n-react'; import { DeepLinkProvider } from '../../shared/components/providers/DeepLinkProvider'; import { useToaster } from '../../shared/defguard-ui/hooks/toasts/useToaster'; @@ -20,10 +20,10 @@ import { useClientStore } from './hooks/useClientStore'; import { useMFAModal } from './pages/ClientInstancePage/components/LocationsList/modals/MFAModal/useMFAModal'; import { clientQueryKeys } from './query'; import { + ClientConnectionType, type CommonWireguardFields, type DeadConDroppedPayload, TauriEventKey, - ClientConnectionType, } from './types'; const { getInstances, getTunnels, getAppConfig } = clientApi; @@ -235,12 +235,15 @@ export const ClientPage = () => { }, [navigate, listChecked, instances, tunnels]); return ( - - - - - - - + + + + + + + + + + ); }; diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index b689e1aa..855416a8 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -17,6 +17,7 @@ import type { GetLocationsRequest, LocationDetails, LocationDetailsRequest, + ProvisioningConfig, RoutingRequest, SaveConfigRequest, SaveDeviceConfigResponse, @@ -129,6 +130,12 @@ const stopGlobalLogWatcher = async (): Promise => const getAppConfig = async (): Promise => invokeWrapper('command_get_app_config'); +const getProvisioningConfig = async (): Promise => + invokeWrapper('get_provisioning_config'); + +const getPlatformHeader = async (): Promise => + invokeWrapper('get_platform_header'); + const setAppConfig = async ( appConfig: Partial, emitEvent: boolean, @@ -164,4 +171,6 @@ export const clientApi = { getLatestAppVersion, startGlobalLogWatcher, stopGlobalLogWatcher, + getProvisioningConfig, + getPlatformHeader, }; diff --git a/src/pages/client/clientAPI/types.ts b/src/pages/client/clientAPI/types.ts index 39d3ad1b..0b970e95 100644 --- a/src/pages/client/clientAPI/types.ts +++ b/src/pages/client/clientAPI/types.ts @@ -1,6 +1,6 @@ import type { ThemeKey } from '../../../shared/defguard-ui/hooks/theme/types'; import type { CreateDeviceResponse } from '../../../shared/hooks/api/types'; -import type { DefguardInstance, DefguardLocation, ClientConnectionType } from '../types'; +import type { ClientConnectionType, DefguardInstance, DefguardLocation } from '../types'; export type GetLocationsRequest = { instanceId: number; @@ -52,7 +52,7 @@ export type LogLevel = 'ERROR' | 'INFO' | 'DEBUG' | 'TRACE' | 'WARN'; export const availableLogLevels: LogLevel[] = ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE']; export type GlobalLogLevel = 'ERROR' | 'INFO' | 'DEBUG'; -export type LogSource = 'Client' | 'Service' | 'All'; +export type LogSource = 'Client' | 'VPN' | 'All'; export type ClientView = 'grid' | 'detail'; @@ -80,6 +80,17 @@ export type AppConfig = { tray_theme: TrayIconTheme; check_for_updates: boolean; peer_alive_period: number; + mtu: number; +}; + +export type PlatformInfo = { + client_version: string; + platform_info: string; +}; + +export type ProvisioningConfig = { + enrollment_token: string; + enrollment_url: string; }; export type LocationDetails = { @@ -142,4 +153,6 @@ export type TauriCommandKey = | 'start_global_logwatcher' | 'stop_global_logwatcher' | 'command_get_app_config' - | 'command_set_app_config'; + | 'command_set_app_config' + | 'get_provisioning_config' + | 'get_platform_header'; diff --git a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx index 36a5efb5..7e8a9c94 100644 --- a/src/pages/client/components/ClientSideBar/ClientSideBar.tsx +++ b/src/pages/client/components/ClientSideBar/ClientSideBar.tsx @@ -60,8 +60,9 @@ export const ClientSideBar = () => { {instances.map((instance) => ( { const localLL = LL.pages.client.pages.addInstancePage.forms.device; const toaster = useToaster(); const setClientStore = useClientStore((state) => state.setState); + const platformInfo = useClientStore((state) => state.platformInfo); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const response = useAddInstanceStore((s) => s.response as AddInstanceInitResponse); @@ -91,9 +92,12 @@ export const AddInstanceDeviceForm = () => { name: values.name, pubkey: publicKey, }; + const headers = { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }; try { await fetch(`${proxyUrl}/enrollment/create_device`, { diff --git a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx index f5b0a39b..3252bab4 100644 --- a/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx +++ b/src/pages/client/pages/ClientAddInstancePage/components/AddInstanceFormCard/components/AddInstanceInitForm/AddInstanceInitForm.tsx @@ -27,7 +27,7 @@ import { routes } from '../../../../../../../../shared/routes'; import { useEnrollmentStore } from '../../../../../../../enrollment/hooks/store/useEnrollmentStore'; import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; -import { type SelectedInstance, ClientConnectionType } from '../../../../../../types'; +import { ClientConnectionType, type SelectedInstance } from '../../../../../../types'; import { AddInstanceFormStep } from '../../../../hooks/types'; import { useAddInstanceStore } from '../../../../hooks/useAddInstanceStore'; diff --git a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx index 34fc9b34..b6d40cf3 100644 --- a/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx +++ b/src/pages/client/pages/ClientInstancePage/ClientInstancePage.tsx @@ -12,7 +12,7 @@ import { routes } from '../../../../shared/routes'; import { clientApi } from '../../clientAPI/clientApi'; import { useClientStore } from '../../hooks/useClientStore'; import { clientQueryKeys } from '../../query'; -import { type DefguardInstance, ClientConnectionType } from '../../types'; +import { ClientConnectionType, type DefguardInstance } from '../../types'; import { LocationsList } from './components/LocationsList/LocationsList'; import { StatsFilterSelect } from './components/StatsFilterSelect/StatsFilterSelect'; import { StatsLayoutSelect } from './components/StatsLayoutSelect/StatsLayoutSelect'; @@ -72,8 +72,7 @@ export const ClientInstancePage = () => { }); useEffect(() => { - const isDefguardInstance = - selectedInstanceType === ClientConnectionType.LOCATION; + const isDefguardInstance = selectedInstanceType === ClientConnectionType.LOCATION; const isTunnelInstance = selectedInstanceType === ClientConnectionType.TUNNEL; if (isDefguardInstance && !selectedInstance) { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx index c708b45e..3c1546ab 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/LocationsList.tsx @@ -9,9 +9,9 @@ import { useToaster } from '../../../../../../shared/defguard-ui/hooks/toasts/us import { routes } from '../../../../../../shared/routes'; import { useClientStore } from '../../../../hooks/useClientStore'; import { + ClientConnectionType, type CommonWireguardFields, type DefguardInstance, - ClientConnectionType, } from '../../../../types'; import { LocationsDetailView } from './components/LocationsDetailView/LocationsDetailView'; import { LocationsGridView } from './components/LocationsGridView/LocationsGridView'; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx index 61cc8032..fa2c6003 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationCardRoute/LocationCardRoute.tsx @@ -7,7 +7,11 @@ import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { Toggle } from '../../../../../../../../shared/defguard-ui/components/Layout/Toggle/Toggle'; import type { ToggleOption } from '../../../../../../../../shared/defguard-ui/components/Layout/Toggle/types'; import { clientApi } from '../../../../../../clientAPI/clientApi'; -import type { CommonWireguardFields, DefguardInstance } from '../../../../../../types'; +import { + ClientTrafficPolicy, + type CommonWireguardFields, + type DefguardInstance, +} from '../../../../../../types'; type Props = { location?: CommonWireguardFields; @@ -37,25 +41,37 @@ export const LocationCardRoute = ({ location, selectedDefguardInstance }: Props) { text: LL.pages.client.pages.instancePage.controls.traffic.predefinedTraffic(), value: 0, + disabled: + selectedDefguardInstance?.client_traffic_policy === + ClientTrafficPolicy.FORCE_ALL_TRAFFIC, }, { text: LL.pages.client.pages.instancePage.controls.traffic.allTraffic(), value: 1, - disabled: selectedDefguardInstance?.disable_all_traffic, + disabled: + selectedDefguardInstance?.client_traffic_policy === + ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, }, ]; return res; - }, [LL.pages, selectedDefguardInstance?.disable_all_traffic]); + }, [LL.pages, selectedDefguardInstance?.client_traffic_policy]); + let selected: number; + if (selectedDefguardInstance?.client_traffic_policy === ClientTrafficPolicy.NONE) { + selected = Number(location?.route_all_traffic); + } else if ( + selectedDefguardInstance?.client_traffic_policy === + ClientTrafficPolicy.DISABLE_ALL_TRAFFIC + ) { + selected = 0; + } else { + selected = 1; + } return ( { if (!location?.active) { diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx index 54dfc3fe..72fb46f6 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/LocationsDetailView.tsx @@ -13,9 +13,9 @@ import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../../query'; import { + ClientConnectionType, type CommonWireguardFields, type DefguardInstance, - ClientConnectionType, } from '../../../../../../types'; import { LocationConnectionHistory } from './components/LocationConnectionHistory/LocationConnectionHistory'; import { LocationDetailCard } from './components/LocationDetailCard/LocationDetailCard'; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx index 3e547527..ca76cba4 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationConnectionHistory/LocationConnectionHistory.tsx @@ -7,8 +7,8 @@ import { Card } from '../../../../../../../../../../shared/defguard-ui/component import { clientApi } from '../../../../../../../../clientAPI/clientApi'; import { clientQueryKeys } from '../../../../../../../../query'; import type { - DefguardLocation, ClientConnectionType, + DefguardLocation, } from '../../../../../../../../types'; import { LocationCardNeverConnected } from '../../../LocationCardNeverConnected/LocationCardNeverConnected'; import { LocationHistoryTable } from './LocationHistoryTable/LocationHistoryTable'; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx index 94c33000..4fa78d60 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationDetails/LocationDetails.tsx @@ -11,8 +11,8 @@ import { Label } from '../../../../../../../../../../shared/defguard-ui/componen import { clientApi } from '../../../../../../../../clientAPI/clientApi'; import { clientQueryKeys } from '../../../../../../../../query'; import type { - DefguardLocation, ClientConnectionType, + DefguardLocation, } from '../../../../../../../../types'; import { LocationLogs } from '../LocationLogs/LocationLogs'; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx index 94fd2f88..603d77ab 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsDetailView/components/LocationLogs/LocationLogs.tsx @@ -13,8 +13,8 @@ import { Card } from '../../../../../../../../../../shared/defguard-ui/component import type { LogItem, LogLevel } from '../../../../../../../../clientAPI/types'; import { useClientStore } from '../../../../../../../../hooks/useClientStore'; import type { - DefguardLocation, ClientConnectionType, + DefguardLocation, } from '../../../../../../../../types'; import { LocationLogsSelect } from './LocationLogsSelect'; @@ -62,11 +62,15 @@ export const LocationLogs = ({ locationId, connectionType }: Props) => { logsContainerElement.current && filterLogByLevel(locationLogLevelRef.current, item.level) ) { - const messageString = `${item.timestamp} ${item.level} ${item.fields.message}`; + const utcTimestamp = item.timestamp.endsWith('Z') + ? item.timestamp + : `${item.timestamp}Z`; + const dateTime = new Date(utcTimestamp).toLocaleString(); + const messageString = `${dateTime} ${item.level} ${item.fields.message}`; const element = createLogLineElement(messageString); const scrollAfterAppend = logsContainerElement.current.scrollHeight - - logsContainerElement.current.scrollTop === + logsContainerElement.current.scrollTop === logsContainerElement.current.clientHeight; logsContainerElement.current.appendChild(element); // auto scroll to bottom if user didn't scroll up diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx index 926e94dc..2503380b 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/components/LocationsGridView/LocationsGridView.tsx @@ -13,9 +13,9 @@ import { clientApi } from '../../../../../../clientAPI/clientApi'; import { useClientStore } from '../../../../../../hooks/useClientStore'; import { clientQueryKeys } from '../../../../../../query'; import type { + ClientConnectionType, CommonWireguardFields, DefguardInstance, - ClientConnectionType, } from '../../../../../../types'; import { LocationUsageChart } from '../../../LocationUsageChart/LocationUsageChart'; import { LocationUsageChartType } from '../../../LocationUsageChart/types'; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx index 2307f534..86cc148d 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal.tsx @@ -11,7 +11,6 @@ import { type SubmitHandler, useForm } from 'react-hook-form'; import ReactMarkdown from 'react-markdown'; import { z } from 'zod'; import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../../../../../i18n/i18n-react'; import { Button } from '../../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; import { @@ -81,6 +80,7 @@ export const MFAModal = () => { return instances.find((i) => i.id === instanceId); } }, [location, instances]); + const platformInfo = useClientStore((state) => state.platformInfo); const resetState = () => { reset(); @@ -88,10 +88,16 @@ export const MFAModal = () => { setStartResponse(undefined); }; - const resetAuthState = () => { + const resetAuthState = useCallback(() => { setScreen('start'); setStartResponse(undefined); - }; + }, []); + + useEffect(() => { + if (location) { + resetAuthState(); + } + }, [location, resetAuthState]); // selectedMethod: 0 = authenticator app, 1 = email, 2 = OpenID, 3 = MobileApprove const startMFA = useCallback( @@ -111,54 +117,62 @@ export const MFAModal = () => { location_id: location.network_id, }; - const response = await fetch(mfaStartUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); + try { + const response = await fetch(mfaStartUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, + }, + body: JSON.stringify(data), + }); - if (response.ok) { - const data = (await response.json()) as MFAStartResponse; - - switch (method) { - case 0: - setScreen('authenticator_app'); - break; - case 1: - setScreen('email'); - break; - case 2: - setScreen('openid_login'); - break; - case 4: - // just to be safe - if (!isPresent(data.challenge)) { - toaster.error('Unsupported response from proxy'); - } - setScreen('mobile_approve'); - break; - default: - toaster.error(localLL.errors.mfaStartGeneric()); + if (response.ok) { + const data = (await response.json()) as MFAStartResponse; + + switch (method) { + case 0: + setScreen('authenticator_app'); + break; + case 1: + setScreen('email'); + break; + case 2: + setScreen('openid_login'); + break; + case 4: + // just to be safe + if (!isPresent(data.challenge)) { + toaster.error('Unsupported response from proxy'); + } + setScreen('mobile_approve'); + break; + default: + toaster.error(localLL.errors.mfaStartGeneric()); + return; + } + setStartResponse(data); + return data; + } else { + const errorData = ((await response.json()) as unknown as MFAError).error; + error(`MFA failed to start with the following error: ${errorData}`); + if (method === 2) { + setScreen('openid_unavailable'); return; - } - setStartResponse(data); - return data; - } else { - const errorData = ((await response.json()) as unknown as MFAError).error; - error(`MFA failed to start with the following error: ${errorData}`); - if (method === 2) { - setScreen('openid_unavailable'); - return; - } + } - if (errorData === 'selected MFA method not available') { - toaster.error(localLL.errors.mfaNotConfigured()); - } else { - toaster.error(localLL.errors.mfaStartGeneric()); - } + if (errorData === 'selected MFA method not available') { + toaster.error(localLL.errors.mfaNotConfigured()); + } else { + toaster.error(localLL.errors.mfaStartGeneric()); + } + return; + } + } catch (rej) { + error(`Failed to execute proxy request: ${rej}`); + toaster.error(localLL.errors.mfaStartGeneric()); return; } }, @@ -170,6 +184,7 @@ export const MFAModal = () => { location, selectedInstance, toaster.error, + platformInfo, ], ); @@ -427,6 +442,7 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps const location = useMFAModal((state) => state.instance); const closeModal = useMFAModal((state) => state.close); const [errorMessage, setErrorMessage] = useState(null); + const platformInfo = useClientStore((state) => state.platformInfo); useEffect(() => { const TIMEOUT_DURATION = 5 * 1000 * 60; // 5 minutes timeout @@ -444,6 +460,8 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps method: 'POST', headers: { 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }, body: JSON.stringify(body_token), }); @@ -497,7 +515,7 @@ const OpenIDMFAPending = ({ proxyUrl, token, resetState }: OpenIDMFAPendingProps clearInterval(interval); clearTimeout(timeoutId); }; - }, [proxyUrl, token, location, closeModal, localLL.errors, toaster]); + }, [proxyUrl, token, location, closeModal, localLL.errors, toaster, platformInfo]); return (
@@ -546,6 +564,7 @@ const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) const [mfaError, setMFAError] = useState(''); const localLL = LL.modals.mfa.authentication; + const platformInfo = useClientStore((state) => state.platformInfo); const schema = useMemo( () => @@ -556,48 +575,56 @@ const MFACodeForm = ({ description, token, proxyUrl, resetState }: MFACodeForm) ); const finishMFA = async (code: string) => { - if (!location) return toaster.error(localLL.errors.mfaStartGeneric()); + if (!location) return toaster.error(localLL.errors.mfaFinishGeneric()); const data = { token, code: code }; - const response = await fetch(`${proxyUrl + CLIENT_MFA_ENDPOINT}/finish`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - if (response.ok) { - closeModal(); - const data = (await response.json()) as MFAFinishResponse; - await connect({ - locationId: location?.id, - connectionType: location.connection_type, - presharedKey: data.preshared_key, + try { + const response = await fetch(`${proxyUrl + CLIENT_MFA_ENDPOINT}/finish`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, + }, + body: JSON.stringify(data), }); - } else { - const data = (await response.json()) as unknown as MFAError; - const { error: errorMessage } = data; - let message = ''; - - if (errorMessage === 'Unauthorized') { - message = localLL.errors.invalidCode(); - } else if ( - errorMessage === 'invalid token' || - errorMessage === 'login session not found' - ) { - console.error(data); - toaster.error(localLL.errors.tokenExpired()); - resetState(); + + if (response.ok) { + closeModal(); + const data = (await response.json()) as MFAFinishResponse; + await connect({ + locationId: location?.id, + connectionType: location.connection_type, + presharedKey: data.preshared_key, + }); + } else { + const data = (await response.json()) as unknown as MFAError; + const { error: errorMessage } = data; + let message = ''; + + if (errorMessage === 'Unauthorized') { + message = localLL.errors.invalidCode(); + } else if ( + errorMessage === 'invalid token' || + errorMessage === 'login session not found' + ) { + console.error(data); + toaster.error(localLL.errors.tokenExpired()); + resetState(); + error(JSON.stringify(data)); + return; + } else { + toaster.error(localLL.errors.mfaFinishGeneric()); + } + + setMFAError(message); error(JSON.stringify(data)); return; - } else { - toaster.error(localLL.errors.mfaStartGeneric()); } - - setMFAError(message); - error(JSON.stringify(data)); + } catch (rej) { + error(`Failed to execute proxy request: ${rej}`); + toaster.error(localLL.errors.mfaFinishGeneric()); return; } }; diff --git a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx index e4870037..b6670e84 100644 --- a/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx +++ b/src/pages/client/pages/ClientInstancePage/components/LocationsList/modals/MFAModal/components/MfaMobileApprove/MfaMobileApprove.tsx @@ -48,7 +48,8 @@ export const MfaMobileApprove = ({ [proxyUrl], ); - const { lastMessage } = useWebSocket(wsUrl, { + var manuallyCancelled = false; + const { getWebSocket, lastMessage } = useWebSocket(wsUrl, { queryParams: { token, }, @@ -56,12 +57,14 @@ export const MfaMobileApprove = ({ debug('WebSocket connection to proxy for mobile app MFA closed.'); }, onError: () => { - toaster.error('Unexpected error in WebSocket connection to proxy'); - error( - 'MFA auth using mobile app failed. Unexpected error in WebSocket connection to proxy.', - ); - // go back to previous step - onCancel(); + if (!manuallyCancelled) { + toaster.error('Unexpected error in WebSocket connection to proxy'); + error( + 'MFA auth using mobile app failed. Unexpected error in WebSocket connection to proxy.', + ); + // go back to previous step + onCancel(); + } }, }); @@ -102,19 +105,27 @@ export const MfaMobileApprove = ({ } }, [lastMessage]); + const cancel = () => { + manuallyCancelled = true; + const socket = getWebSocket(); + socket?.close(); + // go back to previous step + onCancel(); + } + return (
- +

{'Go to the mobile app, select this instance and click the Biometry button'} - {'in the botom right corner.'} + {'in the bottom right corner.'}

-
); }; diff --git a/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx b/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx index 48ea334a..1b684680 100644 --- a/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx +++ b/src/pages/client/pages/ClientInstancePage/modals/UpdateInstanceModal/components/UpdateInstanceModalForm.tsx @@ -5,7 +5,6 @@ import { useMemo } from 'react'; import { type SubmitHandler, useForm } from 'react-hook-form'; import { z } from 'zod'; import { shallow } from 'zustand/shallow'; - import { useI18nContext } from '../../../../../../../i18n/i18n-react'; import { FormInput } from '../../../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { Button } from '../../../../../../../shared/defguard-ui/components/Layout/Button/Button'; @@ -42,6 +41,7 @@ export const UpdateInstanceModalForm = () => { const toaster = useToaster(); const queryClient = useQueryClient(); const setClientState = useClientStore((s) => s.setState, shallow); + const platformInfo = useClientStore((state) => state.platformInfo); const defaultValues = useMemo( (): FormFields => ({ @@ -85,9 +85,10 @@ export const UpdateInstanceModalForm = () => { }; const endpointUrl = url(); - const headers: Record = { 'Content-Type': 'application/json', + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, }; const data = { diff --git a/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx b/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx index c6f14053..c7fc7162 100644 --- a/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogs.tsx @@ -30,20 +30,24 @@ export const GlobalLogs = () => { const { startGlobalLogWatcher, stopGlobalLogWatcher } = clientApi; const handleLogsDownload = async () => { - const path = await save({ - filters: [ - { - name: 'Logs', - extensions: ['txt', 'log'], - }, - ], - }); + try { + const path = await save({ + filters: [ + { + name: 'Logs', + extensions: ['txt', 'log'], + }, + ], + }); - if (path) { - const logs = getAllLogs(); - await writeTextFile(path, logs); - } else { - error('Failed to save logs! Path was null'); + if (path) { + const logs = getAllLogs(); + await writeTextFile(path, logs); + } else { + throw new Error('No path selected'); + } + } catch (e) { + error(`Failed to save logs: ${e}`); } }; @@ -72,7 +76,10 @@ export const GlobalLogs = () => { filterLogByLevel(globalLogLevelRef.current, item.level) && filterLogBySource(logSourceRef.current, item.source) ) { - const dateTime = new Date(item.timestamp).toLocaleString(); + const utcTimestamp = item.timestamp.endsWith('Z') + ? item.timestamp + : `${item.timestamp}Z`; + const dateTime = new Date(utcTimestamp).toLocaleString(); const messageString = `[${dateTime}][${item.level}][${item.source}] ${item.fields.message}`; const element = createLogLineElement(messageString); const scrollAfterAppend = diff --git a/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogsSourceSelect.tsx b/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogsSourceSelect.tsx index 04234b07..b962b115 100644 --- a/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogsSourceSelect.tsx +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalLogs/GlobalLogsSourceSelect.tsx @@ -33,8 +33,8 @@ export const GlobalLogsSourceSelect = ({ initSelected, onChange }: Props) => { }, { key: 2, - label: localLL.service(), - value: 'Service', + label: localLL.vpn(), + value: 'VPN', }, ]; }, [localLL]); diff --git a/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx index 4d6dea45..8f2ff8a5 100644 --- a/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx +++ b/src/pages/client/pages/ClientSettingsPage/components/GlobalSettingsTab/GlobalSettingsTab.tsx @@ -81,6 +81,12 @@ export const GlobalSettingsTab = () => { required_error: LL.form.errors.required(), }) .gte(120, LL.form.errors.minValue({ min: 120 })), + mtu: z + .number({ + invalid_type_error: LL.form.errors.required(), + required_error: LL.form.errors.required(), + }) + .lte(65535, LL.form.errors.maxValue({ max: 65535 })), }), [LL.form.errors], ); @@ -139,6 +145,15 @@ export const GlobalSettingsTab = () => { +
+
+

{localLL.mtu.title()}

+ +

{localLL.mtu.helper()}

+
+
+ +
); }; diff --git a/src/pages/client/query.ts b/src/pages/client/query.ts index 93d1c945..e7931262 100644 --- a/src/pages/client/query.ts +++ b/src/pages/client/query.ts @@ -8,4 +8,6 @@ export const clientQueryKeys = { getLocationDetails: 'GET_LOCATION_DETAILS', getTunnels: 'GET_TUNNELS', getApplicationConfig: 'GET_APPLICATION_CONFIG', + getProvisioningConfig: 'GET_PROVISIONING_CONFIG', + getPlatformHeader: 'GET_PLATFORM_HEADER', }; diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 97136f0c..3434187b 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -1,3 +1,9 @@ +export enum ClientTrafficPolicy { + NONE = 'none', + DISABLE_ALL_TRAFFIC = 'disable_all_traffic', + FORCE_ALL_TRAFFIC = 'force_all_traffic', +} + export type DefguardInstance = { id: number; uuid: string; @@ -7,7 +13,7 @@ export type DefguardInstance = { // connected active: boolean; pubkey: string; - disable_all_traffic: boolean; + client_traffic_policy: ClientTrafficPolicy; openid_display_name?: string; }; @@ -92,6 +98,11 @@ export type DeadConDroppedPayload = { peer_alive_period: number; }; +export type AddInstancePayload = { + token: string; + url: string; +}; + export enum TauriEventKey { CONNECTION_CHANGED = 'connection-changed', INSTANCE_UPDATE = 'instance-update', diff --git a/src/pages/enrollment/hooks/useEnrollmentApi.tsx b/src/pages/enrollment/hooks/useEnrollmentApi.tsx index 1557d1f8..14c93561 100644 --- a/src/pages/enrollment/hooks/useEnrollmentApi.tsx +++ b/src/pages/enrollment/hooks/useEnrollmentApi.tsx @@ -2,12 +2,14 @@ import { fetch } from '@tauri-apps/plugin-http'; import { useEnrollmentStore } from '../../../pages/enrollment/hooks/store/useEnrollmentStore'; import type { UseApi } from '../../../shared/hooks/api/types'; +import { useClientStore } from '../../client/hooks/useClientStore'; export const useEnrollmentApi = (): UseApi => { const [proxyUrl, cookie] = useEnrollmentStore((state) => [ state.proxy_url, state.cookie, ]); + const platformInfo = useClientStore((state) => state.platformInfo); const networkInfo: UseApi['enrollment']['networkInfo'] = async ( data, @@ -19,6 +21,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: overrideCookie ?? cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -33,6 +37,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify({ method: method.valueOf(), @@ -49,6 +55,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -62,6 +70,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify({ token: data.token, @@ -76,6 +86,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -89,6 +101,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, body: JSON.stringify(data), }); @@ -102,6 +116,8 @@ export const useEnrollmentApi = (): UseApi => { headers: { 'Content-Type': 'application/json', Cookie: cookie, + CLIENT_VERSION_HEADER: platformInfo.client_version, + CLIENT_PLATFORM_HEADER: platformInfo.platform_info, } as Record, }); diff --git a/src/shared/components/providers/DeepLinkProvider.tsx b/src/shared/components/providers/DeepLinkProvider.tsx index 3f8792b5..26658c13 100644 --- a/src/shared/components/providers/DeepLinkProvider.tsx +++ b/src/shared/components/providers/DeepLinkProvider.tsx @@ -1,27 +1,16 @@ -import { invoke } from '@tauri-apps/api/core'; import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link'; -import { debug, error } from '@tauri-apps/plugin-log'; -import dayjs from 'dayjs'; +import { error } from '@tauri-apps/plugin-log'; import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; import z, { string } from 'zod'; -import { clientApi } from '../../../pages/client/clientAPI/clientApi'; -import { useClientStore } from '../../../pages/client/hooks/useClientStore'; -import { AddInstanceFormStep } from '../../../pages/client/pages/ClientAddInstancePage/hooks/types'; -import { useAddInstanceStore } from '../../../pages/client/pages/ClientAddInstancePage/hooks/useAddInstanceStore'; -import { ClientConnectionType } from '../../../pages/client/types'; -import { useEnrollmentStore } from '../../../pages/enrollment/hooks/store/useEnrollmentStore'; -import { useEnrollmentApi } from '../../../pages/enrollment/hooks/useEnrollmentApi'; -import type { EnrollmentStartResponse } from '../../hooks/api/types'; -import { routes } from '../../routes'; +import useAddInstance from '../../hooks/useAddInstance'; enum DeepLink { AddInstance = 'addinstance', } -const linkStorageKey = 'lastSuccessfullyHandledDeepLink'; +export const linkStorageKey = 'lastSuccessfullyHandledDeepLink'; -const storeLink = (value: string) => { +export const storeLink = (value: string) => { sessionStorage.setItem(linkStorageKey, value); }; @@ -63,123 +52,25 @@ const linkIntoPayload = (link: URL | null): LinkPayload | null => { return null; }; -const prepareProxyUrl = (value: string) => { - let proxyUrl = value; - if (proxyUrl[proxyUrl.length - 1] === '/') { - proxyUrl = proxyUrl.slice(0, -1); - } - proxyUrl = `${proxyUrl}/api/v1`; - return proxyUrl; -}; - export const DeepLinkProvider = ({ children }: PropsWithChildren) => { const mounted = useRef(false); - const { - enrollment: { start, networkInfo }, - } = useEnrollmentApi(); - - const setEnrollmentState = useEnrollmentStore((s) => s.init); - const setAddInstanceState = useAddInstanceStore((s) => s.setState); - const setClientState = useClientStore((s) => s.setState); - - const navigate = useNavigate(); - - // biome-ignore lint/correctness/useExhaustiveDependencies: should init once - const handleValidLink = useCallback(async (payload: LinkPayload, rawLink?: string) => { - const { data, link } = payload; - switch (link) { - case DeepLink.AddInstance: - await start({ - token: data.token, - proxyUrl: prepareProxyUrl(data.url), - }).then(async (response) => { - if (response.ok) { - const authCookie = response.headers - .getSetCookie() - .find((cookie) => cookie.startsWith('defguard_proxy=')); - if (authCookie === undefined) { - error('Failed to open deep link, auth cookie missing from proxy response.'); - return; - } - const respData = (await response.json()) as EnrollmentStartResponse; - const instances = await clientApi.getInstances(); - const proxy_api_url = prepareProxyUrl( - respData.instance.proxy_url ?? respData.instance.url, - ); - const existingInstance = instances.find( - (instance) => instance.uuid === respData.instance.id, - ); - if (existingInstance) { - // update existing instance instead - const networkInfoResp = await networkInfo( - { - pubkey: existingInstance.pubkey, - }, - proxy_api_url, - authCookie, - ); - await invoke('update_instance', { - instanceId: existingInstance.id, - response: networkInfoResp, - }); - setClientState({ - selectedInstance: { - type: ClientConnectionType.LOCATION, - id: existingInstance.id, - }, - }); - if (rawLink) { - storeLink(rawLink); - } - debug(`Updated ${existingInstance.name} via deep link`); - navigate(routes.client.base, { replace: true }); - return; - } - if (!respData.user.enrolled) { - // user needs full enrollment - const sessionEnd = dayjs - .unix(respData.deadline_timestamp) - .utc() - .local() - .format(); - const sessionStart = dayjs().local().format(); - // set enrollment - setEnrollmentState({ - enrollmentSettings: respData.settings, - proxy_url: proxy_api_url, - userInfo: respData.user, - adminInfo: respData.admin, - endContent: respData.final_page_content, - cookie: authCookie, - sessionEnd, - sessionStart, - }); - navigate('/enrollment', { replace: true }); - } else { - // only needs to register this device - setAddInstanceState({ - step: AddInstanceFormStep.DEVICE, - response: { - cookie: authCookie, - device_names: respData.user.device_names, - url: proxy_api_url, - }, - }); - navigate('/client/add-instance', { replace: true }); - } - } else { - error( - `Add instance from deep link failed! Proxy enrollment start request failed! status: ${response.status}`, - ); - } - }); - break; - } - if (rawLink) { - storeLink(rawLink); - } - }, []); + const { handleAddInstance } = useAddInstance(); + + const handleValidLink = useCallback( + async (payload: LinkPayload, rawLink?: string) => { + const { data, link } = payload; + switch (link) { + case DeepLink.AddInstance: + await handleAddInstance(data, rawLink); + break; + } + if (rawLink) { + storeLink(rawLink); + } + }, + [handleAddInstance], + ); // biome-ignore lint/correctness/useExhaustiveDependencies: only on mount useEffect(() => { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index fb750bca..25182bef 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -12,3 +12,7 @@ export const mastodonUrl = export const githubUrl = 'https://github.com/Defguard/defguard'; export const matrixUrl = 'https://matrix.to/#/#defguard:teonite.com'; + +export const CLIENT_VERSION_HEADER = 'defguard-client-version'; + +export const CLIENT_PLATFORM_HEADER = 'defguard-client-platform'; diff --git a/src/shared/hooks/useAddInstance.ts b/src/shared/hooks/useAddInstance.ts new file mode 100644 index 00000000..ea3fde7a --- /dev/null +++ b/src/shared/hooks/useAddInstance.ts @@ -0,0 +1,146 @@ +/** + * Hook which handles adding an instance in the background and triggering enrollment process (if necessary) + * in automated scenarios e.g. deep-link, client provisioning etc. + */ + +import { invoke } from '@tauri-apps/api/core'; +import { debug, error } from '@tauri-apps/plugin-log'; +import dayjs from 'dayjs'; +import { useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { clientApi } from '../../pages/client/clientAPI/clientApi'; +import { useClientStore } from '../../pages/client/hooks/useClientStore'; +import { AddInstanceFormStep } from '../../pages/client/pages/ClientAddInstancePage/hooks/types'; +import { useAddInstanceStore } from '../../pages/client/pages/ClientAddInstancePage/hooks/useAddInstanceStore'; +import { type AddInstancePayload, ClientConnectionType } from '../../pages/client/types'; +import { useEnrollmentStore } from '../../pages/enrollment/hooks/store/useEnrollmentStore'; +import { useEnrollmentApi } from '../../pages/enrollment/hooks/useEnrollmentApi'; +import { storeLink } from '../components/providers/DeepLinkProvider'; +import type { EnrollmentStartResponse } from '../hooks/api/types'; +import { routes } from '../routes'; + +const prepareProxyUrl = (value: string) => { + let proxyUrl = value; + if (proxyUrl[proxyUrl.length - 1] === '/') { + proxyUrl = proxyUrl.slice(0, -1); + } + proxyUrl = `${proxyUrl}/api/v1`; + return proxyUrl; +}; + +export default function useAddInstance() { + const [loading, setLoading] = useState(false); + + const setEnrollmentState = useEnrollmentStore((s) => s.init); + const setAddInstanceState = useAddInstanceStore((s) => s.setState); + const setClientState = useClientStore((s) => s.setState); + + const navigate = useNavigate(); + + const { + enrollment: { start, networkInfo }, + } = useEnrollmentApi(); + + const handleAddInstance = useCallback( + async (payload: AddInstancePayload, rawLink?: string) => { + setLoading(true); + + await start({ + token: payload.token, + proxyUrl: prepareProxyUrl(payload.url), + }).then(async (response) => { + if (response.ok) { + const authCookie = response.headers + .getSetCookie() + .find((cookie) => cookie.startsWith('defguard_proxy=')); + if (authCookie === undefined) { + error( + 'Failed to automatically add new instance, auth cookie missing from proxy response.', + ); + return; + } + const respData = (await response.json()) as EnrollmentStartResponse; + const instances = await clientApi.getInstances(); + const proxy_api_url = prepareProxyUrl( + respData.instance.proxy_url ?? respData.instance.url, + ); + const existingInstance = instances.find( + (instance) => instance.uuid === respData.instance.id, + ); + if (existingInstance) { + // update existing instance instead + const networkInfoResp = await networkInfo( + { + pubkey: existingInstance.pubkey, + }, + proxy_api_url, + authCookie, + ); + await invoke('update_instance', { + instanceId: existingInstance.id, + response: networkInfoResp, + }); + setClientState({ + selectedInstance: { + type: ClientConnectionType.LOCATION, + id: existingInstance.id, + }, + }); + if (rawLink) { + storeLink(rawLink); + } + debug(`Automatically updated ${existingInstance.name}`); + navigate(routes.client.base, { replace: true }); + return; + } + if (!respData.user.enrolled) { + // user needs full enrollment + const sessionEnd = dayjs + .unix(respData.deadline_timestamp) + .utc() + .local() + .format(); + const sessionStart = dayjs().local().format(); + // set enrollment + setEnrollmentState({ + enrollmentSettings: respData.settings, + proxy_url: proxy_api_url, + userInfo: respData.user, + adminInfo: respData.admin, + endContent: respData.final_page_content, + cookie: authCookie, + sessionEnd, + sessionStart, + }); + navigate('/enrollment', { replace: true }); + } else { + // only needs to register this device + setAddInstanceState({ + step: AddInstanceFormStep.DEVICE, + response: { + cookie: authCookie, + device_names: respData.user.device_names, + url: proxy_api_url, + }, + }); + navigate('/client/add-instance', { replace: true }); + } + } else { + error( + `Adding instance automatically failed. Proxy enrollment start request failed with status: ${response.status}`, + ); + } + }); + }, + [ + setClientState, + networkInfo, + start, + setEnrollmentState, + setAddInstanceState, + navigate, + ], + ); + + return { handleAddInstance, loading, error }; +} diff --git a/swift/.gitignore b/swift/.gitignore new file mode 100644 index 00000000..b996f867 --- /dev/null +++ b/swift/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +*/.build +build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved +extension/BoringTun diff --git a/swift/boringtun b/swift/boringtun new file mode 160000 index 00000000..fd82bf92 --- /dev/null +++ b/swift/boringtun @@ -0,0 +1 @@ +Subproject commit fd82bf92f1a3901cd307923670ded2a244b564bc diff --git a/swift/build.sh b/swift/build.sh new file mode 100755 index 00000000..04112714 --- /dev/null +++ b/swift/build.sh @@ -0,0 +1,53 @@ +#!/bin/sh +set -e + +DST="${PWD}/extension/BoringTun" +CARGO="${HOME}/.cargo/bin/cargo" +RUSTUP="${HOME}/.cargo/bin/rustup" + +export MACOSX_DEPLOYMENT_TARGET=13.5 + +# Build BoringTun. + +pushd boringtun + +for TARGET in aarch64-apple-darwin x86_64-apple-darwin +do + ${RUSTUP} target add "${TARGET}" + ${CARGO} build --lib --locked --release --target ${TARGET} +done + +# Create universal library. + +mkdir -p target/universal/release +lipo -create \ + target/aarch64-apple-darwin/release/libboringtun.a \ + target/x86_64-apple-darwin/release/libboringtun.a \ + -output target/universal/release/libboringtun.a + +rm -f -r target/uniffi +${CARGO} run --release --bin uniffi-bindgen -- \ + --xcframework --headers --modulemap --swift-sources \ + target/aarch64-apple-darwin/release/libboringtun.a target/uniffi + +# Install BoringTun framework. + +mkdir -p "${DST}" +cp -c target/uniffi/boringtun.swift "${DST}/" +rm -f -r "${DST}/boringtun.xcframework" +xcodebuild -create-xcframework \ + -library target/universal/release/libboringtun.a \ + -headers target/uniffi \ + -output ${DST}/boringtun.xcframework +cp -c target/uniffi/boringtunFFI.h "${DST}/" + +popd + +# Build VPNExtension. + +# if [ "${TAURI_ENV_DEBUG}" = 'false' ]; then + CONFIG=Release +# else +# CONFIG=Debug +# fi +xcodebuild -project extension/VPNExtension.xcodeproj -target VPNExtension -configuration ${CONFIG} build diff --git a/swift/extension/VPNExtension.xcodeproj/project.pbxproj b/swift/extension/VPNExtension.xcodeproj/project.pbxproj new file mode 100644 index 00000000..283bbd1d --- /dev/null +++ b/swift/extension/VPNExtension.xcodeproj/project.pbxproj @@ -0,0 +1,463 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 66CABCD82EA76D070057D1AF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66CABCD72EA76D060057D1AF /* NetworkExtension.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 66CABCD42EA76D060057D1AF /* VPNExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 66CABCD72EA76D060057D1AF /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 66CABCDE2EA76D070057D1AF /* Exceptions for "VPNExtension" folder in "VPNExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 66CABCD32EA76D060057D1AF /* VPNExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 660FE5002EA779E8006A7447 /* Defguard */ = { + isa = PBXFileSystemSynchronizedRootGroup; + name = Defguard; + path = ../plugin/Sources/Defguard; + sourceTree = SOURCE_ROOT; + }; + 660FE50B2EA77C68006A7447 /* BoringTun */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = BoringTun; + sourceTree = ""; + }; + 66CABCD92EA76D070057D1AF /* VPNExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 66CABCDE2EA76D070057D1AF /* Exceptions for "VPNExtension" folder in "VPNExtension" target */, + ); + path = VPNExtension; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 66CABCD12EA76D060057D1AF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CABCD82EA76D070057D1AF /* NetworkExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 66CABCC92EA76CD80057D1AF = { + isa = PBXGroup; + children = ( + 660FE50B2EA77C68006A7447 /* BoringTun */, + 660FE5002EA779E8006A7447 /* Defguard */, + 66CABCD92EA76D070057D1AF /* VPNExtension */, + 66CABCD62EA76D060057D1AF /* Frameworks */, + 66CABCD52EA76D060057D1AF /* Products */, + ); + sourceTree = ""; + }; + 66CABCD52EA76D060057D1AF /* Products */ = { + isa = PBXGroup; + children = ( + 66CABCD42EA76D060057D1AF /* VPNExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 66CABCD62EA76D060057D1AF /* Frameworks */ = { + isa = PBXGroup; + children = ( + 66CABCD72EA76D060057D1AF /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 66CABCD32EA76D060057D1AF /* VPNExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 66CABCDF2EA76D070057D1AF /* Build configuration list for PBXNativeTarget "VPNExtension" */; + buildPhases = ( + 66CABCD02EA76D060057D1AF /* Sources */, + 66CABCD12EA76D060057D1AF /* Frameworks */, + 66CABCD22EA76D060057D1AF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 660FE5002EA779E8006A7447 /* Defguard */, + 660FE50B2EA77C68006A7447 /* BoringTun */, + 66CABCD92EA76D070057D1AF /* VPNExtension */, + ); + name = VPNExtension; + packageProductDependencies = ( + ); + productName = VPNExtension; + productReference = 66CABCD42EA76D060057D1AF /* VPNExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 66CABCCA2EA76CD80057D1AF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2610; + TargetAttributes = { + 66CABCD32EA76D060057D1AF = { + CreatedOnToolsVersion = 26.0.1; + }; + }; + }; + buildConfigurationList = 66CABCCD2EA76CD80057D1AF /* Build configuration list for PBXProject "VPNExtension" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 66CABCC92EA76CD80057D1AF; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 66CABCD52EA76D060057D1AF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 66CABCD32EA76D060057D1AF /* VPNExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 66CABCD22EA76D060057D1AF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 66CABCD02EA76D060057D1AF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 66CABCCE2EA76CD80057D1AF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 82GZ7KN29J; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + ONLY_ACTIVE_ARCH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/BoringTun/boringtunFFI.h"; + }; + name = Debug; + }; + 66CABCCF2EA76CD80057D1AF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 82GZ7KN29J; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/BoringTun/boringtunFFI.h"; + }; + name = Release; + }; + 66CABCE02EA76D070057D1AF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = VPNExtension/VPNExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = "@BUILD_NUMBER@"; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 82GZ7KN29J; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.6.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = net.defguard.VPNExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = macosx; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 66CABCE12EA76D070057D1AF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = VPNExtension/VPNExtension.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = "@BUILD_NUMBER@"; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 82GZ7KN29J; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.6.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = net.defguard.VPNExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Defguard VPNExtension Mac App Store"; + REGISTER_APP_GROUPS = YES; + SDKROOT = macosx; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 66CABCCD2EA76CD80057D1AF /* Build configuration list for PBXProject "VPNExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CABCCE2EA76CD80057D1AF /* Debug */, + 66CABCCF2EA76CD80057D1AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66CABCDF2EA76D070057D1AF /* Build configuration list for PBXNativeTarget "VPNExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CABCE02EA76D070057D1AF /* Debug */, + 66CABCE12EA76D070057D1AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 66CABCCA2EA76CD80057D1AF /* Project object */; +} diff --git a/swift/extension/VPNExtension.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/swift/extension/VPNExtension.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/swift/extension/VPNExtension.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/swift/extension/VPNExtension/Adapter.swift b/swift/extension/VPNExtension/Adapter.swift new file mode 100644 index 00000000..51d15257 --- /dev/null +++ b/swift/extension/VPNExtension/Adapter.swift @@ -0,0 +1,299 @@ +import Foundation +import Network +import NetworkExtension + +/// State of Adapter. +enum State { + /// Tunnel is running. + case running + /// Tunnel is stopped. + case stopped + /// Tunnel is temporary unavaiable due to device being offline. + case dormant +} + +@preconcurrency final class Adapter /*: Sendable*/ { + /// Packet tunnel provider. + private weak var packetTunnelProvider: NEPacketTunnelProvider? + /// BortingTun tunnel + private var tunnel: Tunnel? + /// UDP endpoint + private var endpoint: Network.NWEndpoint? + /// Server connection + private var connection: NWConnection? + /// Network routes monitor. + private var networkMonitor: NWPathMonitor? + /// Keep alive timer + private var keepAliveTimer: Timer? + /// Unified logger (writes to both system log and file) + private let log = Log(category: "Adapter") + /// Adapter state. + private var state: State = .stopped + + /// For statistics returned to Rust code. + var locationId: UInt64? + var tunnelId: UInt64? + + private let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() + + /// Designated initializer. + /// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored + init(with packetTunnelProvider: NEPacketTunnelProvider) { + self.packetTunnelProvider = packetTunnelProvider + } + + deinit { + self.stop() + } + + func start(tunnelConfiguration: TunnelConfiguration) throws { + guard case .stopped = self.state else { + log.error("Invalid state - cannot start tunnel") + // TODO: throw invalid state + return + } + + if tunnel != nil { + log.info("Cleaning existing Tunnel") + tunnel = nil + connection = nil + } + + let networkMonitor = NWPathMonitor() + networkMonitor.pathUpdateHandler = { [weak self] path in + self?.networkPathUpdate(path: path) + } + networkMonitor.start(queue: .main) + self.networkMonitor = networkMonitor + + log.info("Initializing Tunnel") + tunnel = try Tunnel.init( + privateKey: tunnelConfiguration.privateKey, + serverPublicKey: tunnelConfiguration.peers[0].publicKey, + presharedKey: tunnelConfiguration.peers[0].preSharedKey, + keepAlive: tunnelConfiguration.peers[0].persistentKeepAlive, + index: 0 + ) + locationId = tunnelConfiguration.locationId + tunnelId = tunnelConfiguration.tunnelId + + log.info( + "Connecting to endpoint (locationId: \(tunnelConfiguration.locationId ?? 0), tunnelId: \(tunnelConfiguration.tunnelId ?? 0))" + ) + guard let endpoint = tunnelConfiguration.peers[0].endpoint else { + log.error("Endpoint is nil, cannot connect") + return + } + self.endpoint = endpoint.asNWEndpoint() + initEndpoint() + + log.info("Starting to sniff packets") + readPackets() + + state = .running + log.info("Tunnel started successfully") + } + + func stop() { + log.info("Stopping Adapter") + connection?.cancel() + connection = nil + tunnel = nil + keepAliveTimer?.invalidate() + keepAliveTimer = nil + // Cancel network monitor + networkMonitor?.cancel() + networkMonitor = nil + + state = .stopped + log.info("Tunnel stopped") + log.flush() + } + + // Obtain tunnel statistics. + func stats() -> Stats? { + if let stats = tunnel?.stats() { + return Stats( + txBytes: stats.txBytes, + rxBytes: stats.rxBytes, + lastHandshake: stats.lastHandshake, + locationId: locationId, + tunnelId: tunnelId + ) + } + return nil + } + + private func handleTunnelResult(_ result: TunnelResult) { + switch result { + case .done: + // Nothing to do. + break + case .err(let error): + log.error("Tunnel error: \(error)") + switch error { + case .InvalidAeadTag: + log.error("Invalid pre-shared key; stopping tunnel") + // The correct way is to call the packet tunnel provider, if there is one. + if let provider = packetTunnelProvider { + provider.cancelTunnelWithError(error) + } else { + stop() + } + case .ConnectionExpired: + log.warning("Connection has expired; re-connecting") + packetTunnelProvider?.reasserting = true + initEndpoint() + packetTunnelProvider?.reasserting = false + default: + break + } + case .writeToNetwork(let data): + sendToEndpoint(data: data) + case .writeToTunnelV4(let data): + packetTunnelProvider?.packetFlow.writePacketObjects([ + NEPacket(data: data, protocolFamily: sa_family_t(AF_INET)) + ]) + case .writeToTunnelV6(let data): + packetTunnelProvider?.packetFlow.writePacketObjects([ + NEPacket(data: data, protocolFamily: sa_family_t(AF_INET6)) + ]) + } + } + + /// Initialise UDP connection to endpoint. + private func initEndpoint() { + guard let endpoint = endpoint else { return } + + log.info("Initializing endpoint connection to: \(endpoint)") + // Cancel previous connection + connection?.cancel() + connection = nil + + let params = NWParameters.udp + params.allowLocalEndpointReuse = true + let connection = NWConnection.init(to: endpoint, using: params) + connection.stateUpdateHandler = { [weak self] state in + self?.endpointStateChange(state: state) + } + + connection.start(queue: .main) + self.connection = connection + } + + /// Setup UDP connection to endpoint. This method should be called when UDP connection is ready to send and receive. + private func setupEndpoint() { + log.info("Setting up endpoint") + + // Send initial handshake packet + if let tunnel = self.tunnel { + log.info("Sending initial handshake") + handleTunnelResult(tunnel.forceHandshake()) + } + log.info("Starting UDP receive loop") + log.debug("NWConnection path: \(String(describing: self.connection?.currentPath))") + receive() + + // Use Timer to send keep-alive packets. + keepAliveTimer?.invalidate() + log.info("Creating keep-alive timer") + let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] timer in + guard let self = self, let tunnel = self.tunnel else { return } + self.handleTunnelResult(tunnel.tick()) + } + keepAliveTimer = timer + RunLoop.main.add(timer, forMode: .common) + } + + /// Send packets to UDP endpoint. + private func sendToEndpoint(data: Data) { + guard let connection = connection else { return } + if connection.state == .ready { + connection.send( + content: data, + completion: .contentProcessed { [weak self] error in + if let error = error { + self?.log.error("UDP connection send error: \(error)") + } + }) + } else { + log.warning("UDP connection not ready to send") + } + } + + /// Handle UDP packets from the endpoint. + private func receive() { + connection?.receiveMessage { [weak self] data, context, isComplete, error in + guard let self = self else { return } + if let data = data, let tunnel = self.tunnel { + self.handleTunnelResult(tunnel.read(src: data)) + } + if error == nil { + // continue receiving + self.receive() + } else { + self.log.error("receive() error: \(String(describing: error))") + } + } + } + + /// Read tunnel packets. + private func readPackets() { + guard let tunnel = self.tunnel else { return } + + // Packets received to the tunnel's virtual interface. + packetTunnelProvider?.packetFlow.readPacketObjects { packets in + for packet in packets { + self.handleTunnelResult(tunnel.write(src: packet.data)) + } + // continue reading + self.readPackets() + } + } + + /// Handle UDP connection state changes. + private func endpointStateChange(state: NWConnection.State) { + log.debug("UDP connection state changed: \(state)") + switch state { + case .ready: + setupEndpoint() + //case .waiting(let error): + // switch error { + // case .posix(_): + // connection?.restart() + // default: + // self.stop() + // } + case .failed(let error): + log.error("Failed to establish endpoint connection: \(error)") + // The correct way is to call the packet tunnel provider, if there is one. + if let provider = packetTunnelProvider { + provider.cancelTunnelWithError(error) + } else { + stop() + } + default: + break + } + } + + /// Handle network path updates. + private func networkPathUpdate(path: Network.NWPath) { + log.debug( + "Network path update - status: \(path.status), interfaces: \(path.availableInterfaces)") + if path.status == .unsatisfied { + if state == .running { + log.warning("Unsatisfied network path: going dormant") + connection?.cancel() + connection = nil + state = .dormant + } + } else { + if state == .dormant { + log.warning("Satisfied network path: going running") + initEndpoint() + state = .running + } + } + } +} diff --git a/swift/extension/VPNExtension/FileLogger.swift b/swift/extension/VPNExtension/FileLogger.swift new file mode 100644 index 00000000..556fba70 --- /dev/null +++ b/swift/extension/VPNExtension/FileLogger.swift @@ -0,0 +1,237 @@ +import Foundation +import os + +/// Log levels +enum LogLevel: String { + case debug = "DEBUG" + case info = "INFO" + case warning = "WARN" + case error = "ERROR" + + var osLogType: OSLogType { + switch self { + case .debug: return .debug + case .info: return .info + case .warning: return .default + case .error: return .error + } + } +} + +/// Logger that writes to both system log (os.Logger) and file. +/// Use this instead of os.Logger directly to get dual logging with a single call. +final class Log { + /// The category for this logger instance (usually class name), e.g. "PacketTunnelProvider" + let category: String + private let systemLogger: Logger + private let fileLogger = FileLogger.shared + + init(category: String) { + self.category = category + self.systemLogger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "net.defguard.VPNExtension", + category: category + ) + } + + func debug(_ message: String) { + systemLogger.debug("\(message, privacy: .public)") + fileLogger.log(level: .debug, message: message, category: category) + } + + func info(_ message: String) { + systemLogger.info("\(message, privacy: .public)") + fileLogger.log(level: .info, message: message, category: category) + } + + func warning(_ message: String) { + systemLogger.warning("\(message, privacy: .public)") + fileLogger.log(level: .warning, message: message, category: category) + } + + func error(_ message: String) { + systemLogger.error("\(message, privacy: .public)") + fileLogger.log(level: .error, message: message, category: category) + } + + func flush() { + fileLogger.flush() + } +} + +/// A file-based logger that writes to an App Group shared container. +/// This allows the main rust app to read logs from the network extension. +/// Use the `Log` class instead of this directly for unified logging. +final class FileLogger { + static let shared = FileLogger() + static let appGroupIdentifier = "group.net.defguard" + private let logFileName = "vpn-extension.log" + private let maxLogFileSize: UInt64 = 5 * 1024 * 1024 // 5 MB + private let maxBackupFiles = 3 + private let flushInterval = 5 // Flush every N log entries + private var fileHandle: FileHandle? + private var logFileURL: URL? + private var unflushedCount = 0 + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(identifier: "UTC") + return formatter + }() + + private let queue = DispatchQueue(label: "net.defguard.VPNExtension.filelogger") + + private let internalLogger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "net.defguard.VPNExtension", + category: "FileLogger") + + private init() { + setupLogFile() + } + + deinit { + closeLogFile() + } + + private func setupLogFile() { + guard + let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: Self.appGroupIdentifier) + else { + internalLogger.error( + "Failed to get App Group container URL for \(Self.appGroupIdentifier)") + return + } + + let logsDirectory = containerURL.appendingPathComponent("Logs", isDirectory: true) + + do { + try FileManager.default.createDirectory( + at: logsDirectory, withIntermediateDirectories: true, attributes: nil) + } catch { + internalLogger.error("Failed to create Logs directory: \(error.localizedDescription)") + return + } + + logFileURL = logsDirectory.appendingPathComponent(logFileName) + + guard let logFileURL = logFileURL else { return } + + if !FileManager.default.fileExists(atPath: logFileURL.path) { + FileManager.default.createFile(atPath: logFileURL.path, contents: nil, attributes: nil) + } + + do { + fileHandle = try FileHandle(forWritingTo: logFileURL) + fileHandle?.seekToEndOfFile() + + let startupMessage = + "# VPN Extension Log Started at \(dateFormatter.string(from: Date()))\n" + if let data = startupMessage.data(using: .utf8) { + fileHandle?.write(data) + } + } catch { + internalLogger.error( + "Failed to open log file for writing: \(error.localizedDescription)") + } + + internalLogger.info("FileLogger initialized at: \(logFileURL.path)") + } + + private func closeLogFile() { + queue.sync { + try? fileHandle?.synchronize() + try? fileHandle?.close() + fileHandle = nil + } + } + + /// Rotate log files if the current one exceeds the maximum size + private func rotateLogFilesIfNeeded() { + guard let logFileURL = logFileURL else { return } + + do { + let attributes = try FileManager.default.attributesOfItem(atPath: logFileURL.path) + if let fileSize = attributes[.size] as? UInt64, fileSize >= maxLogFileSize { + rotateLogFiles() + } + } catch { + } + } + + private func rotateLogFiles() { + guard let logFileURL = logFileURL else { return } + + try? fileHandle?.synchronize() + try? fileHandle?.close() + fileHandle = nil + + let fileManager = FileManager.default + let directory = logFileURL.deletingLastPathComponent() + let baseName = logFileURL.deletingPathExtension().lastPathComponent + let ext = logFileURL.pathExtension + + // Remove oldest backup if it exists + let oldestBackup = directory.appendingPathComponent("\(baseName).\(maxBackupFiles).\(ext)") + try? fileManager.removeItem(at: oldestBackup) + + for i in stride(from: maxBackupFiles - 1, through: 1, by: -1) { + let current = directory.appendingPathComponent("\(baseName).\(i).\(ext)") + let next = directory.appendingPathComponent("\(baseName).\(i + 1).\(ext)") + try? fileManager.moveItem(at: current, to: next) + } + + let firstBackup = directory.appendingPathComponent("\(baseName).1.\(ext)") + try? fileManager.moveItem(at: logFileURL, to: firstBackup) + + fileManager.createFile(atPath: logFileURL.path, contents: nil, attributes: nil) + + do { + fileHandle = try FileHandle(forWritingTo: logFileURL) + fileHandle?.seekToEndOfFile() + } catch { + internalLogger.error( + "Failed to reopen log file after rotation: \(error.localizedDescription)") + } + } + + /// Write a log message to the file + /// - level: Log level (debug, info, warning, error) + /// - message: The message to log + /// - category: Optional category/subsystem + func log(level: LogLevel, message: String, category: String? = nil) { + queue.async { [weak self] in + guard let self = self, let fileHandle = self.fileHandle else { return } + + self.rotateLogFilesIfNeeded() + + let timestamp = self.dateFormatter.string(from: Date()) + let categoryStr = category.map { "[\($0)] " } ?? "" + let logLine = "\(timestamp) [\(level.rawValue)] \(categoryStr)\(message)\n" + + if let data = logLine.data(using: .utf8) { + fileHandle.write(data) + self.unflushedCount += 1 + + // Flush for important messages or periodically + if level == .error || level == .warning || self.unflushedCount >= self.flushInterval + { + try? fileHandle.synchronize() + self.unflushedCount = 0 + } + } + } + } + + func flush() { + queue.sync { + try? fileHandle?.synchronize() + } + } + + var logFilePath: String? { + return logFileURL?.path + } +} diff --git a/swift/extension/VPNExtension/Info.plist b/swift/extension/VPNExtension/Info.plist new file mode 100644 index 00000000..3059459e --- /dev/null +++ b/swift/extension/VPNExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + diff --git a/swift/extension/VPNExtension/PacketTunnelProvider.swift b/swift/extension/VPNExtension/PacketTunnelProvider.swift new file mode 100644 index 00000000..a82ad116 --- /dev/null +++ b/swift/extension/VPNExtension/PacketTunnelProvider.swift @@ -0,0 +1,86 @@ +import NetworkExtension + +enum WireGuardTunnelError: Error { + case invalidTunnelConfiguration +} + +class PacketTunnelProvider: NEPacketTunnelProvider { + /// Unified logger (writes to both system log and file) + private let log = Log(category: "PacketTunnelProvider") + + private lazy var adapter: Adapter = { + return Adapter(with: self) + }() + + override func startTunnel( + options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void + ) { + log.info("\(#function) called") + if let options = options { + log.debug("Options: \(options)") + } + + guard let protocolConfig = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfig = protocolConfig.providerConfiguration, + let tunnelConfig = try? TunnelConfiguration.from(dictionary: providerConfig) + else { + log.error("Failed to parse tunnel configuration") + completionHandler(WireGuardTunnelError.invalidTunnelConfiguration) + return + } + + log.info("Tunnel configuration parsed successfully") + + let networkSettings = tunnelConfig.asNetworkSettings() + self.setTunnelNetworkSettings(networkSettings) { error in + if error != nil { + self.log.error("Set tunnel network settings error: \(String(describing: error))") + } + completionHandler(error) + return + } + + do { + try adapter.start(tunnelConfiguration: tunnelConfig) + } catch { + log.error("Failed to start tunnel: \(error)") + completionHandler(error) + } + log.info("Tunnel started successfully") + + completionHandler(nil) + } + + override func stopTunnel( + with reason: NEProviderStopReason, completionHandler: @escaping () -> Void + ) { + log.info("\(#function) called with reason: \(reason)") + adapter.stop() + log.info("Tunnel stopped") + completionHandler() + } + + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { + log.debug("\(#function) called") + // TODO: messageData should contain a valid message. + if let handler = completionHandler { + if let stats = adapter.stats() { + let data = try? JSONEncoder().encode(stats) + handler(data) + } else { + handler(nil) + } + } + } + + override func sleep(completionHandler: @escaping () -> Void) { + log.info("System going to sleep") + // Add code here to get ready to sleep. + completionHandler() + } + + override func wake() { + log.info("System waking up") + // Add code here to wake up. + } +} diff --git a/swift/extension/VPNExtension/VPNExtension.entitlements b/swift/extension/VPNExtension/VPNExtension.entitlements new file mode 100644 index 00000000..049cfc05 --- /dev/null +++ b/swift/extension/VPNExtension/VPNExtension.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.net.defguard + + com.apple.security.network.client + + + diff --git a/swift/plugin/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/swift/plugin/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/swift/plugin/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/swift/plugin/Package.swift b/swift/plugin/Package.swift new file mode 100644 index 00000000..4e027349 --- /dev/null +++ b/swift/plugin/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "defguard-vpn-plugin", + platforms: [ + .macOS("13.5"), + .iOS("15.6"), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "defguard-vpn-plugin", + type: .static, + targets: ["defguard-vpn-plugin"]) + ], + dependencies: [ + .package(url: "https://github.com/Brendonovich/swift-rs", from: "1.0.7") + ], + targets: [ + .target( + name: "defguard-vpn-plugin", + dependencies: [ + .product( + name: "SwiftRs", + package: "swift-rs" + ) + ], + path: "Sources") + ] +) diff --git a/swift/plugin/Sources/Defguard/Decodabe+Encodable.swift b/swift/plugin/Sources/Defguard/Decodabe+Encodable.swift new file mode 100644 index 00000000..d3f8a8f0 --- /dev/null +++ b/swift/plugin/Sources/Defguard/Decodabe+Encodable.swift @@ -0,0 +1,22 @@ +import Foundation + +extension Decodable { + static func from(dictionary: [String: Any]) throws -> Self { + let data = try JSONSerialization.data(withJSONObject: dictionary) + let decoder = JSONDecoder() + return try decoder.decode(Self.self, from: data) + } +} + +extension Encodable { + func toDictionary() throws -> [String: Any] { + let data = try JSONEncoder().encode(self) + let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) + guard let dictionary = jsonObject as? [String: Any] else { + throw NSError( + domain: "EncodingError", code: 0, + userInfo: [NSLocalizedDescriptionKey: "Failed to convert to dictionary"]) + } + return dictionary + } +} diff --git a/swift/plugin/Sources/Defguard/Endpoint.swift b/swift/plugin/Sources/Defguard/Endpoint.swift new file mode 100644 index 00000000..64f50a40 --- /dev/null +++ b/swift/plugin/Sources/Defguard/Endpoint.swift @@ -0,0 +1,83 @@ +import Foundation +import Network + +struct Endpoint: Codable, CustomStringConvertible { + let host: NWEndpoint.Host + let port: NWEndpoint.Port + + init(host: NWEndpoint.Host, port: NWEndpoint.Port) { + self.host = host + self.port = port + } + + /// Custom initializer from String. Assume format "host:port". + init?(from string: String) { + let trimmedEndpoint = string.trimmingCharacters(in: .whitespaces) + var endpointHost = trimmedEndpoint + + // Extract host, supporting IPv4, IPv6, and domains + if trimmedEndpoint.hasPrefix("[") { // IPv6 with port, e.g. [fd00::1]:51820 + if let closing = trimmedEndpoint.firstIndex(of: "]") { + endpointHost = String( + trimmedEndpoint[ + trimmedEndpoint.index(after: trimmedEndpoint.startIndex).. 1 { + endpointHost = parts.dropLast().joined(separator: ":") + } + } + + let endpointPort: Network.NWEndpoint.Port + if let portPart = trimmedEndpoint.split(separator: ":").last, let port = Int(portPart), + let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) + { + endpointPort = nwPort + } else { + return nil + } + + self.host = NWEndpoint.Host(endpointHost) + self.port = endpointPort + } + + /// A textual representation of this instance. Required for `CustomStringConvertible`. + var description: String { + "Endpoint(\(host):\(port))" + } + + var hostString: String { + "\(host)" + } + + func toString() -> String { + "\(host):\(port)" + } + + // Encode to a single string "host:port", to smoothly encode into JSON. + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.toString()) + } + + // Decode from a single string "host:port", to smoothly decode from JSON. + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + guard let endpoint = Endpoint(from: value) else { + throw + DecodingError + .dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Not in host:port format") + ) + } + self = endpoint + } + + func asNWEndpoint() -> NWEndpoint { + NWEndpoint.hostPort(host: host, port: port) + } +} diff --git a/swift/plugin/Sources/Defguard/IpAddrMask.swift b/swift/plugin/Sources/Defguard/IpAddrMask.swift new file mode 100644 index 00000000..afb347c9 --- /dev/null +++ b/swift/plugin/Sources/Defguard/IpAddrMask.swift @@ -0,0 +1,109 @@ +import Foundation +import Network + +struct IpAddrMask: Codable, Equatable { + let address: IPAddress + let cidr: UInt8 + + init(address: IPAddress, cidr: UInt8) { + self.address = address + self.cidr = cidr + } + + init?(fromString string: String) { + let parts = string.split( + separator: "/", + maxSplits: 1, + ) + if let ipv4 = IPv4Address(String(parts[0])) { + address = ipv4 + } else if let ipv6 = IPv6Address(String(parts[0])) { + address = ipv6 + } else { + return nil + } + if parts.count > 1 { + cidr = UInt8(parts[1]) ?? 0 + } else { + cidr = 0 + } + } + + var stringRepresentation: String { + return "\(address)/\(cidr)" + } + + enum CodingKeys: String, CodingKey { + case address + case cidr + } + + /// Conform to `Encodable`. + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(address)", forKey: .address) + try container.encode(cidr, forKey: .cidr) + } + + /// Conform to `Decodable`. + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + let address_string = try values.decode(String.self, forKey: .address) + if let ipv4 = IPv4Address(address_string) { + address = ipv4 + } else if let ipv6 = IPv6Address(address_string) { + address = ipv6 + } else { + throw + DecodingError + .dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unable to decode IP address" + )) + } + + cidr = try values.decode(UInt8.self, forKey: .cidr) + } + + /// Conform to `Equatable`. + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.address.rawValue == rhs.address.rawValue && lhs.cidr == rhs.cidr + } + + func mask() -> IPAddress { + if address is IPv4Address { + var bytes = Data(count: 4) + let mask = cidr == 0 ? UInt32(0) : ~UInt32(0) << (32 - cidr) + for i in 0...3 { + bytes[i] = UInt8(truncatingIfNeeded: mask >> (24 - i * 8)) + } + return IPv4Address(bytes)! + } + // Note: UInt128 is available since iOS 18. Use UInt64 implementation. + if address is IPv6Address { + var bytes = Data(count: 16) + let (mask_upper, mask_lower) = + if cidr < 64 { + ( + cidr == 0 ? UInt64.min : UInt64.max << (64 - cidr), + UInt64.min + ) + } else { + ( + UInt64.max, + (cidr - 64) == 0 ? UInt64.min : UInt64.max << (128 - cidr) + ) + } + for i in 0...7 { + bytes[i] = UInt8(truncatingIfNeeded: mask_upper >> (56 - i * 8)) + } + for i in 8...15 { + bytes[i] = UInt8(truncatingIfNeeded: mask_lower >> (56 - (i - 8) * 8)) + } + return IPv6Address(bytes)! + } + fatalError() + } +} diff --git a/swift/plugin/Sources/Defguard/Peer.swift b/swift/plugin/Sources/Defguard/Peer.swift new file mode 100644 index 00000000..fc07220a --- /dev/null +++ b/swift/plugin/Sources/Defguard/Peer.swift @@ -0,0 +1,44 @@ +import Foundation + +final class Peer: Codable { + var publicKey: String + var preSharedKey: String? + var endpoint: Endpoint? + var persistentKeepAlive: UInt16? + var allowedIPs = [IpAddrMask]() + // Statistics + var lastHandshake: Date? + var txBytes: UInt64 = 0 + var rxBytes: UInt64 = 0 + + init( + publicKey: String, preSharedKey: String? = nil, endpoint: Endpoint? = nil, + persistentKeepAlive: UInt16? = nil, allowedIPs: [IpAddrMask] = [IpAddrMask](), + lastHandshake: Date? = nil, txBytes: UInt64 = 0, rxBytes: UInt64 = 0, + ) { + self.publicKey = publicKey + self.preSharedKey = preSharedKey + self.endpoint = endpoint + self.persistentKeepAlive = persistentKeepAlive + self.allowedIPs = allowedIPs + self.lastHandshake = lastHandshake + self.txBytes = txBytes + self.rxBytes = rxBytes + } + + init(publicKey: String) { + self.publicKey = publicKey + } + + enum CodingKeys: String, CodingKey { + case publicKey + case preSharedKey + case endpoint + case persistentKeepAlive + case allowedIPs + // There isn't any need to encode/decode these ephemeral fields. + // case lastHandshake + // case txBytes + // case rxBytes + } +} diff --git a/swift/plugin/Sources/Defguard/Stats.swift b/swift/plugin/Sources/Defguard/Stats.swift new file mode 100644 index 00000000..2c833446 --- /dev/null +++ b/swift/plugin/Sources/Defguard/Stats.swift @@ -0,0 +1,18 @@ +import ObjectiveC + +public class Stats: NSObject, Codable { + var txBytes: UInt64 + var rxBytes: UInt64 + var lastHandshake: UInt64 + // One or the other. + var locationId: UInt64? + var tunnelId: UInt64? + + init(txBytes: UInt64, rxBytes: UInt64, lastHandshake: UInt64, locationId: UInt64?, tunnelId: UInt64?) { + self.txBytes = txBytes + self.rxBytes = rxBytes + self.lastHandshake = lastHandshake + self.locationId = locationId + self.tunnelId = tunnelId + } +} diff --git a/swift/plugin/Sources/Defguard/TunnelConfiguration.swift b/swift/plugin/Sources/Defguard/TunnelConfiguration.swift new file mode 100644 index 00000000..6bdde84f --- /dev/null +++ b/swift/plugin/Sources/Defguard/TunnelConfiguration.swift @@ -0,0 +1,129 @@ +import Foundation +import NetworkExtension + +final class TunnelConfiguration: Codable { + // One or the other. + var locationId: UInt64? + var tunnelId: UInt64? + + var name: String + var privateKey: String + var addresses: [IpAddrMask] = [] + var listenPort: UInt16? + var peers: [Peer] = [] + var mtu: UInt32? + var dns: [String] = [] + var dnsSearch: [String] = [] + + init(name: String, privateKey: String, peers: [Peer]) { + self.name = name + self.privateKey = privateKey + self.peers = peers + + let peerPublicKeysArray = peers.map { $0.publicKey } + let peerPublicKeysSet = Set(peerPublicKeysArray) + if peerPublicKeysArray.count != peerPublicKeysSet.count { + fatalError("Two or more peers cannot have the same public key") + } + } + + /// Only encode these properties. + enum CodingKeys: String, CodingKey { + case locationId + case tunnelId + case name + case privateKey + case addresses + case listenPort + case peers + case mtu + case dns + case dnsSearch + } + + func asNetworkSettings() -> NEPacketTunnelNetworkSettings { + // Keep 127.0.0.1 as remote address for WireGuard. + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + + let (ipv4IncludedRoutes, ipv6IncludedRoutes) = routes() + + // IPv4 addresses + let addrs_v4 = addresses.filter { $0.address is IPv4Address } + .map { String(describing: $0.address) } + let masks_v4 = addresses.filter { $0.address is IPv4Address } + .map { String(describing: $0.mask()) } + let ipv4Settings = NEIPv4Settings(addresses: addrs_v4, subnetMasks: masks_v4) + ipv4Settings.includedRoutes = ipv4IncludedRoutes + networkSettings.ipv4Settings = ipv4Settings + + // IPv6 addresses + let addrs_v6 = addresses.filter { $0.address is IPv6Address } + .map { String(describing: $0.address) } + let masks_v6 = addresses.filter { $0.address is IPv6Address } + .map { NSNumber(value: $0.cidr) } + let ipv6Settings = NEIPv6Settings(addresses: addrs_v6, networkPrefixLengths: masks_v6) + ipv6Settings.includedRoutes = ipv6IncludedRoutes + networkSettings.ipv6Settings = ipv6Settings + + networkSettings.mtu = mtu as NSNumber? + networkSettings.tunnelOverheadBytes = 80 + + let dnsSettings = NEDNSSettings(servers: dns) + dnsSettings.searchDomains = dnsSearch + if !dns.isEmpty { + // Make all DNS queries go through the tunnel. + dnsSettings.matchDomains = [""] + } + networkSettings.dnsSettings = dnsSettings + + return networkSettings + } + + /// Return array of routes for IPv4 and IPv6. + func routes() -> ([NEIPv4Route], [NEIPv6Route]) { + var ipv4IncludedRoutes = [NEIPv4Route]() + var ipv6IncludedRoutes = [NEIPv6Route]() + + // Routes to interface addresses. + for addr_mask in addresses { + if addr_mask.address is IPv4Address { + let route = NEIPv4Route( + destinationAddress: "\(addr_mask.address)", + subnetMask: "\(addr_mask.mask())") + route.gatewayAddress = "\(addr_mask.address)" + ipv4IncludedRoutes.append(route) + } else if addr_mask.address is IPv6Address { + let route = NEIPv6Route( + destinationAddress: "\(addr_mask.address)", + networkPrefixLength: NSNumber(value: addr_mask.cidr) + ) + route.gatewayAddress = "\(addr_mask.address)" + ipv6IncludedRoutes.append(route) + } + } + + // Routes to peer's allowed IPs. + for peer in peers { + for addr_mask in peer.allowedIPs { + if addr_mask.address is IPv4Address { + ipv4IncludedRoutes.append( + NEIPv4Route( + destinationAddress: "\(addr_mask.address)", + subnetMask: "\(addr_mask.mask())")) + } else if addr_mask.address is IPv6Address { + ipv6IncludedRoutes.append( + NEIPv6Route( + destinationAddress: "\(addr_mask.address)", + networkPrefixLength: NSNumber(value: addr_mask.cidr))) + } + } + } + + return (ipv4IncludedRoutes, ipv6IncludedRoutes) + } + + /// Client connection expects one peer, so check for that. + func isValidForClientConnection() -> Bool { + return peers.count == 1 + } +} diff --git a/swift/plugin/Sources/Wireguard.swift b/swift/plugin/Sources/Wireguard.swift new file mode 100644 index 00000000..16a20d78 --- /dev/null +++ b/swift/plugin/Sources/Wireguard.swift @@ -0,0 +1,262 @@ +// Functions to be called from Rust code. + +import NetworkExtension +import SwiftRs +import os + +let appId = Bundle.main.bundleIdentifier ?? "net.defguard" +let pluginAppId = "\(appId).VPNExtension" +let logger = Logger(subsystem: appId, category: "WireguardPlugin") + +/// From preferences load `NETunnelProviderManager` with a given `name. +func managerForName( + _ name: String, + completion: @escaping (NETunnelProviderManager?) -> Void +) { + var providerManager: NETunnelProviderManager? + NETunnelProviderManager.loadAllFromPreferences { managers, error in + guard let managers = managers else { + logger.info("No tunnel managers in user's settings") + return + } + guard error == nil else { + logger.warning( + "Error loading tunnel managers: \(error, privacy: .public)") + providerManager = nil + completion(nil) + return + } + logger.info("Loaded \(managers.count, privacy: .public) tunnel managers.") + + // Find the right protocol manager. + providerManager = nil + for manager in managers { + // Obtain named configuration. + if manager.localizedDescription != name { + continue + } + guard let tunnelProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol + else { + continue + } + // Sometimes all managers from all apps come through, so filter by bundle ID. + if tunnelProtocol.providerBundleIdentifier == pluginAppId { + providerManager = manager + break + } + } + if providerManager == nil { + logger.log("No VPN manager found") + } else { + logger.log( + "Loaded provider manager: \(String(describing: providerManager!.localizedDescription), privacy: .public)" + ) + } + completion(providerManager) + } +} + +@_cdecl("start_tunnel") +public func startTunnel(json: SRString) -> Bool { + let decoder = JSONDecoder() + guard let json_data = json.toString().data(using: .utf8) else { + logger.error("Failed to convert JSON string to data") + return false + } + let config: TunnelConfiguration + do { config = try decoder.decode(TunnelConfiguration.self, from: json_data) } catch { + logger.error( + "Failed to decode tunnel configuration: \(error.localizedDescription, privacy: .public)" + ) + return false + } + + if !config.isValidForClientConnection() { + logger.error("Invalid tunnel configuration: \(json.toString(), privacy: .public)") + return false + } + + logger.info("Saving tunnel with config: \(String(describing: config))") + saveConfig(config) + + // MFA is not that fast to propagate pre-shared key, so wait a moment here. + Thread.sleep(forTimeInterval: 1) + // Note: this will re-load configuration from preferneces which is a desired effect. + startVPN(name: config.name) + + return true +} + +@_cdecl("stop_tunnel") +public func stopTunnel(name: SRString) -> Bool { + // Blocking + let semaphore = DispatchSemaphore(value: 0) + + managerForName(name.toString()) { manager in + if let providerManager = manager { + providerManager.connection.stopVPNTunnel() + logger.info("VPN stopped") + } + semaphore.signal() + } + + semaphore.wait() + return true +} + +@_cdecl("tunnel_stats") +public func tunnelStats(name: SRString) -> Stats? { + // Blocking + let semaphore = DispatchSemaphore(value: 0) + var result: Stats? = nil + + managerForName(name.toString()) { manager in + if let providerManager = manager as NETunnelProviderManager? { + let session = providerManager.connection as! NETunnelProviderSession + do { + // TODO: data should contain a valid message. + let data = Data() + try session.sendProviderMessage(data) { response in + if let data = response { + let decoder = JSONDecoder() + result = try? decoder.decode(Stats.self, from: data) + } + semaphore.signal() + } + } catch { + logger.error("Failed to send message to tunnel extension \(error)") + semaphore.signal() + } + } + } + + semaphore.wait() + return result +} + +@_cdecl("all_tunnel_stats") +public func allTunnelStats() -> SRObjectArray { + // Blocking + let semaphore = DispatchSemaphore(value: 0) + var stats: [Stats] = [] + + // Get all tunnel provider managers. + NETunnelProviderManager.loadAllFromPreferences { managers, error in + guard let managers = managers else { + logger.info("No tunnel managers in user's settings") + return + } + guard error == nil else { + logger.warning( + "Error loading tunnel managers: \(error, privacy: .public)") + semaphore.signal() + return + } + logger.info("Loaded \(managers.count, privacy: .public) tunnel managers.") + + // `NETunnelProviderSession.sendProviderMessage()` is asynchronous, so use `DispatchGroup`. + let dispatchGroup = DispatchGroup() + + for manager in managers { + guard let tunnelProtocol = manager.protocolConfiguration as? NETunnelProviderProtocol + else { + continue + } + // Sometimes all managers from all apps come through, so filter by bundle ID. + if tunnelProtocol.providerBundleIdentifier != pluginAppId { + continue + } + if let providerManager = manager as NETunnelProviderManager? { + let session = providerManager.connection as! NETunnelProviderSession + do { + // TODO: data should contain a valid message. + let data = Data() + dispatchGroup.enter() + try session.sendProviderMessage(data) { response in + if let data = response { + let decoder = JSONDecoder() + if let result = try? decoder.decode(Stats.self, from: data) { + stats.append(result) + } + } + dispatchGroup.leave() + } + } catch { + logger.error("Failed to send message to tunnel extension \(error)") + dispatchGroup.leave() + } + } + } + + // NOTE: `dispatchGroup.wait()` will cause a dead-lock, because it uses the same thread as + // `NETunnelProviderSession.sendProviderMessage()`. Use this pattern instead: + dispatchGroup.notify(queue: DispatchQueue.global()) { + semaphore.signal() + } + } + + semaphore.wait() + return SRObjectArray(stats) +} + +/// Save `TunnelConfiguration` to preferences. +func saveConfig(_ config: TunnelConfiguration) { + // Blocking + let semaphore = DispatchSemaphore(value: 0) + + managerForName(config.name) { manager in + let providerManager = manager ?? NETunnelProviderManager() + let tunnelProtocol = NETunnelProviderProtocol() + tunnelProtocol.providerBundleIdentifier = pluginAppId + // `serverAddress` must have a non-nil string value for the protocol configuration to be valid. + if let endpoint = config.peers[0].endpoint { + tunnelProtocol.serverAddress = endpoint.toString() + } else { + tunnelProtocol.serverAddress = "" + } + let configDict: [String: Any] + do { + configDict = try config.toDictionary() + } catch { + logger.log( + "Failed to convert config to dictionary: \(error.localizedDescription, privacy: .public)" + ) + // TODO: signal failure + semaphore.signal() + return + } + tunnelProtocol.providerConfiguration = configDict + providerManager.protocolConfiguration = tunnelProtocol + providerManager.localizedDescription = config.name + providerManager.isEnabled = true + + providerManager.saveToPreferences { error in + if let error = error { + logger.log("Failed to save provider manager: \(error, privacy: .public)") + // TODO: signal failure + } else { + logger.info("Config saved") + } + + semaphore.signal() + } + } + + semaphore.wait() +} + +/// Start VPN tunnel for a given `name`. +func startVPN(name: String) { + managerForName(name) { manager in + guard let providerManager = manager else { + logger.warning("Couldn't load \(name) configuration from preferences") + return + } + do { + try providerManager.connection.startVPNTunnel() + logger.info("VPN started") + } catch { + logger.error("Failed to start VPN: \(error, privacy: .public)") + } + } +}