diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 2d8ace2e..d864895f 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -3,7 +3,6 @@ on:
push:
tags:
- v*.*.*
-
jobs:
create-release:
name: create-release
@@ -23,6 +22,95 @@ jobs:
uses: ./.github/workflows/sbom.yml
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
+ ubuntu-22-04-build:
+ needs:
+ - create-release
+ runs-on:
+ - self-hosted
+ - Linux
+ - ${{ matrix.architecture }}
+ strategy:
+ fail-fast: false
+ matrix:
+ architecture: [ARM64, X64]
+ include:
+ - architecture: ARM64
+ deb_arch: arm64
+ binary_arch: aarch64
+ - architecture: X64
+ deb_arch: amd64
+ binary_arch: x86_64
+ container:
+ image: ubuntu:22.04
+ env:
+ DEBIAN_FRONTEND: noninteractive
+ HOME: /root
+ RUSTUP_HOME: /root/.rustup
+ CARGO_HOME: /root/.cargo
+ steps:
+ - name: git install
+ run: |
+ apt-get update
+ apt-get install -y git curl ca-certificates
+ git config --global --add safe.directory '*'
+ - uses: actions/checkout@v5
+ with:
+ submodules: "recursive"
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10.17
+ run_install: false
+ - uses: actions/setup-node@v5
+ with:
+ node-version: "24"
+ - name: Get pnpm store directory
+ run: |
+ echo "STORE_PATH=$(pnpm store path --silent)" >> ${GITHUB_ENV}
+ - name: Write release version
+ run: |
+ VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d '-' -f1)
+ echo Version: $VERSION
+ echo "VERSION=$VERSION" >> ${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 Node dependencies
+ run: pnpm install --frozen-lockfile
+ - uses: dtolnay/rust-toolchain@stable
+ - name: Install dependencies
+ run: |
+ apt-get install -y build-essential 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
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ args: "--bundles deb"
+ - name: Upload DEB
+ 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/release/bundle/deb/defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}.deb
+ asset_name: defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}_ubuntu-22-04-lts.deb
+ asset_content_type: application/octet-stream
+ - name: Install ruby with deb-s3
+ if: matrix.build != 'freebsd'
+ run: |
+ 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
+ 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=bookworm --component="$COMPONENT" src-tauri/target/release/bundle/deb/defguard-client_${{ env.VERSION }}_${{ matrix.deb_arch }}.deb
+
build-linux:
needs:
@@ -468,3 +556,40 @@ jobs:
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/package.json b/package.json
index ecc6c437..03e77a3b 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
"@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",
"@types/byte-size": "^8.1.2",
"@use-gesture/react": "^10.3.1",
@@ -92,6 +93,7 @@
"react-click-away-listener": "^2.4.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.63.0",
+ "react-hotkeys-hook": "^5.2.1",
"react-loading-skeleton": "^3.5.0",
"react-markdown": "^10.1.0",
"react-qr-code": "^2.0.18",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 11375949..7a8fc6b6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
'@tauri-apps/plugin-os':
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
@@ -143,6 +146,9 @@ importers:
react-hook-form:
specifier: ^7.63.0
version: 7.63.0(react@19.2.0)
+ react-hotkeys-hook:
+ specifier: ^5.2.1
+ version: 5.2.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-loading-skeleton:
specifier: ^3.5.0
version: 3.5.0(react@19.2.0)
@@ -1210,6 +1216,9 @@ packages:
'@tauri-apps/plugin-os@2.3.1':
resolution: {integrity: sha512-ty5V8XDUIFbSnrk3zsFoP3kzN+vAufYzalJSlmrVhQTImIZa1aL1a03bOaP2vuBvfR+WDRC6NgV2xBl8G07d+w==}
+ '@tauri-apps/plugin-process@2.3.0':
+ resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==}
+
'@tauri-apps/plugin-window-state@2.4.0':
resolution: {integrity: sha512-hRSzPNi2NG0lPFthfVY0V5C1MyWN/gGaQtQYw7i9zZhLzrhZveHZ2omHG1rIiIsjfTGbO7fhjydSoeTTK9GqLw==}
@@ -2465,6 +2474,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
+ react-hotkeys-hook@5.2.1:
+ resolution: {integrity: sha512-xbKh6zJxd/vJHT4Bw4+0pBD662Fk20V+VFhLqciCg+manTVO4qlqRqiwFOYelfHN9dBvWj9vxaPkSS26ZSIJGg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -3854,6 +3869,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.8.0
+ '@tauri-apps/plugin-process@2.3.0':
+ dependencies:
+ '@tauri-apps/api': 2.8.0
+
'@tauri-apps/plugin-window-state@2.4.0':
dependencies:
'@tauri-apps/api': 2.8.0
@@ -5297,6 +5316,11 @@ snapshots:
dependencies:
react: 19.2.0
+ react-hotkeys-hook@5.2.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+
react-is@16.13.1: {}
react-is@18.3.1: {}
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index fd9f542a..a5d3ae86 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1428,6 +1428,7 @@ dependencies = [
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-os",
+ "tauri-plugin-process",
"tauri-plugin-single-instance",
"tauri-plugin-window-state",
"thiserror 2.0.17",
@@ -6639,6 +6640,16 @@ dependencies = [
"thiserror 2.0.17",
]
+[[package]]
+name = "tauri-plugin-process"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab"
+dependencies = [
+ "tauri",
+ "tauri-plugin",
+]
+
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.6"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 9bfbe204..d78acda2 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -112,6 +112,7 @@ x25519-dalek = { version = "2", features = [
"serde",
"static_secrets",
] }
+tauri-plugin-process = "2.3.0"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.30.1", features = ["user", "fs"] }
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index c171d2b6..d8b4de1b 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -48,6 +48,7 @@
"os:allow-hostname",
"dialog:default",
"clipboard-manager:allow-write-text",
+ "process:allow-exit",
{
"identifier": "http:default",
"allow": [
diff --git a/src-tauri/deny.toml b/src-tauri/deny.toml
index b6298cda..47d8a68d 100644
--- a/src-tauri/deny.toml
+++ b/src-tauri/deny.toml
@@ -87,11 +87,11 @@ ignore = [
{ id = "RUSTSEC-2024-0420", reason = "Tauri v2 GTK3 dependency (unmaintained)" },
{ id = "RUSTSEC-2025-0052", reason = "Discontinued, but dark-light v2.0.0 needs it" },
{ id = "RUSTSEC-2025-0057", reason = "Tauri needs it" },
- { id = "RUSTSEC-2025-0075", reason = "Tauri v2 GTK3 dependency (unmaintained)" },
- { id = "RUSTSEC-2025-0080", reason = "Tauri v2 GTK3 dependency (unmaintained)" },
- { id = "RUSTSEC-2025-0081", reason = "Tauri v2 GTK3 dependency (unmaintained)" },
- { id = "RUSTSEC-2025-0098", reason = "Tauri v2 GTK3 dependency (unmaintained)" },
- { id = "RUSTSEC-2025-0100", reason = "Tauri v2 GTK3 dependency (unmaintained)" },
+ { id = "RUSTSEC-2025-0075", reason = "Tauri v2 dependency (unmaintained)" },
+ { id = "RUSTSEC-2025-0080", reason = "Tauri v2 dependency (unmaintained)" },
+ { id = "RUSTSEC-2025-0081", reason = "Tauri v2 dependency (unmaintained)" },
+ { id = "RUSTSEC-2025-0098", reason = "Tauri v2 dependency (unmaintained)" },
+ { id = "RUSTSEC-2025-0100", reason = "Tauri v2 dependency (unmaintained)" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs
index 03c36f1e..e0237f9f 100644
--- a/src-tauri/src/bin/defguard-client.rs
+++ b/src-tauri/src/bin/defguard-client.rs
@@ -160,6 +160,7 @@ fn main() {
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_os::init())
+ .plugin(tauri_plugin_process::init())
.setup(|app| {
// Register for linux and dev windows builds
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 5bc83de6..df7132cf 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -4,7 +4,7 @@ import '../../shared/scss/index.scss';
import { QueryClient } from '@tanstack/query-core';
import { QueryClientProvider } from '@tanstack/react-query';
-import { debug } from '@tauri-apps/plugin-log';
+import { debug, info } from '@tauri-apps/plugin-log';
import { openUrl } from '@tauri-apps/plugin-opener';
import dayjs from 'dayjs';
import customParseData from 'dayjs/plugin/customParseFormat';
@@ -38,6 +38,8 @@ 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);
@@ -186,6 +188,12 @@ export const App = () => {
};
}, []);
+ // register ctrl+q keyboard shortcut
+ useHotkeys('ctrl+q', () => {
+ info("Ctrl-Q pressed, exiting.");
+ exit(0);
+ });
+
if (!appLoaded) return null;
return (