diff --git a/biome.json b/biome.json index a8cc0ab0..9780e54d 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.7/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/package.json b/package.json index 326e60c6..11a719dd 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "zustand": "^5.0.8" }, "devDependencies": { - "@biomejs/biome": "^2.3.6", + "@biomejs/biome": "^2.3.7", "@hookform/devtools": "^4.4.0", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.90.10", @@ -129,7 +129,7 @@ "typedoc": "^0.28.14", "typesafe-i18n": "^5.26.2", "typescript": "^5.9.3", - "vite": "^7.2.2" + "vite": "^7.2.4" }, "volta": { "node": "20.5.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1c7b50c..79a0806f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,8 +187,8 @@ importers: version: 5.0.8(@types/react@19.2.6)(immer@10.2.0)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) devDependencies: '@biomejs/biome': - specifier: ^2.3.6 - version: 2.3.6 + specifier: ^2.3.7 + version: 2.3.7 '@hookform/devtools': specifier: ^4.4.0 version: 4.4.0(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -221,10 +221,10 @@ importers: version: 19.2.3(@types/react@19.2.6) '@vitejs/plugin-react': specifier: ^5.1.1 - version: 5.1.1(vite@7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1)) + version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1)) '@vitejs/plugin-react-swc': specifier: ^4.2.2 - version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1)) + version: 4.2.2(vite@7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.22 version: 10.4.22(postcss@8.5.6) @@ -250,8 +250,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 vite: - specifier: ^7.2.2 - version: 7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1) + specifier: ^7.2.4 + version: 7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1) packages: @@ -342,55 +342,55 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.3.6': - resolution: {integrity: sha512-oqUhWyU6tae0MFsr/7iLe++QWRg+6jtUhlx9/0GmCWDYFFrK366sBLamNM7D9Y+c7YSynUFKr8lpEp1r6Sk7eA==} + '@biomejs/biome@2.3.7': + resolution: {integrity: sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.3.6': - resolution: {integrity: sha512-P4JWE5d8UayBxYe197QJwyW4ZHp0B+zvRIGCusOm1WbxmlhpAQA1zEqQuunHgSIzvyEEp4TVxiKGXNFZPg7r9Q==} + '@biomejs/cli-darwin-arm64@2.3.7': + resolution: {integrity: sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.3.6': - resolution: {integrity: sha512-I4rTebj+F/L9K93IU7yTFs8nQ6EhaCOivxduRha4w4WEZK80yoZ8OAdR1F33m4yJ/NfUuTUbP/Wjs+vKjlCoWA==} + '@biomejs/cli-darwin-x64@2.3.7': + resolution: {integrity: sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.3.6': - resolution: {integrity: sha512-oK1NpIXIixbJ/4Tcx40cwiieqah6rRUtMGOHDeK2ToT7yUFVEvXUGRKqH0O4hqZ9tW8TcXNZKfgRH6xrsjVtGg==} + '@biomejs/cli-linux-arm64-musl@2.3.7': + resolution: {integrity: sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.3.6': - resolution: {integrity: sha512-JjYy83eVBnvuINZiqyFO7xx72v8Srh4hsgaacSBCjC22DwM6+ZvnX1/fj8/SBiLuUOfZ8YhU2pfq2Dzakeyg1A==} + '@biomejs/cli-linux-arm64@2.3.7': + resolution: {integrity: sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.3.6': - resolution: {integrity: sha512-QvxB8GHQeaO4FCtwJpJjCgJkbHBbWxRHUxQlod+xeaYE6gtJdSkYkuxdKAQUZEOIsec+PeaDAhW9xjzYbwmOFA==} + '@biomejs/cli-linux-x64-musl@2.3.7': + resolution: {integrity: sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.3.6': - resolution: {integrity: sha512-ZjPXzy5yN9wusIoX+8Zp4p6cL8r0NzJCXg/4r1KLVveIPXd2jKVlqZ6ZyzEq385WwU3OX5KOwQYLQsOc788waQ==} + '@biomejs/cli-linux-x64@2.3.7': + resolution: {integrity: sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.3.6': - resolution: {integrity: sha512-YM7hLHpwjdt8R7+O2zS1Vo2cKgqEeptiXB1tWW1rgjN5LlpZovBVKtg7zfwfRrFx3i08aNZThYpTcowpTlczug==} + '@biomejs/cli-win32-arm64@2.3.7': + resolution: {integrity: sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.3.6': - resolution: {integrity: sha512-psgNEYgMAobY5h+QHRBVR9xvg2KocFuBKm6axZWB/aD12NWhQjiVFQUjV6wMXhlH4iT0Q9c3yK5JFRiDC/rzHA==} + '@biomejs/cli-win32-x64@2.3.7': + resolution: {integrity: sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1011,68 +1011,68 @@ packages: peerDependencies: '@svgr/core': '*' - '@swc/core-darwin-arm64@1.15.2': - resolution: {integrity: sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==} + '@swc/core-darwin-arm64@1.15.3': + resolution: {integrity: sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.2': - resolution: {integrity: sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==} + '@swc/core-darwin-x64@1.15.3': + resolution: {integrity: sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.2': - resolution: {integrity: sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==} + '@swc/core-linux-arm-gnueabihf@1.15.3': + resolution: {integrity: sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.2': - resolution: {integrity: sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==} + '@swc/core-linux-arm64-gnu@1.15.3': + resolution: {integrity: sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.15.2': - resolution: {integrity: sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==} + '@swc/core-linux-arm64-musl@1.15.3': + resolution: {integrity: sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.15.2': - resolution: {integrity: sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==} + '@swc/core-linux-x64-gnu@1.15.3': + resolution: {integrity: sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.15.2': - resolution: {integrity: sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==} + '@swc/core-linux-x64-musl@1.15.3': + resolution: {integrity: sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.15.2': - resolution: {integrity: sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==} + '@swc/core-win32-arm64-msvc@1.15.3': + resolution: {integrity: sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.2': - resolution: {integrity: sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==} + '@swc/core-win32-ia32-msvc@1.15.3': + resolution: {integrity: sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.2': - resolution: {integrity: sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==} + '@swc/core-win32-x64-msvc@1.15.3': + resolution: {integrity: sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.2': - resolution: {integrity: sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==} + '@swc/core@1.15.3': + resolution: {integrity: sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -1386,8 +1386,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.29: - resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} + baseline-browser-mapping@2.8.30: + resolution: {integrity: sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==} hasBin: true boolbase@1.0.0: @@ -1682,8 +1682,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.256: - resolution: {integrity: sha512-uqYq1IQhpXXLX+HgiXdyOZml7spy4xfy42yPxcCCRjswp0fYM2X+JwCON07lqnpLEGVCj739B7Yr+FngmHBMEQ==} + electron-to-chromium@1.5.259: + resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2896,8 +2896,8 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite@7.2.2: - resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} + vite@7.2.4: + resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3127,39 +3127,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.3.6': + '@biomejs/biome@2.3.7': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.6 - '@biomejs/cli-darwin-x64': 2.3.6 - '@biomejs/cli-linux-arm64': 2.3.6 - '@biomejs/cli-linux-arm64-musl': 2.3.6 - '@biomejs/cli-linux-x64': 2.3.6 - '@biomejs/cli-linux-x64-musl': 2.3.6 - '@biomejs/cli-win32-arm64': 2.3.6 - '@biomejs/cli-win32-x64': 2.3.6 - - '@biomejs/cli-darwin-arm64@2.3.6': + '@biomejs/cli-darwin-arm64': 2.3.7 + '@biomejs/cli-darwin-x64': 2.3.7 + '@biomejs/cli-linux-arm64': 2.3.7 + '@biomejs/cli-linux-arm64-musl': 2.3.7 + '@biomejs/cli-linux-x64': 2.3.7 + '@biomejs/cli-linux-x64-musl': 2.3.7 + '@biomejs/cli-win32-arm64': 2.3.7 + '@biomejs/cli-win32-x64': 2.3.7 + + '@biomejs/cli-darwin-arm64@2.3.7': optional: true - '@biomejs/cli-darwin-x64@2.3.6': + '@biomejs/cli-darwin-x64@2.3.7': optional: true - '@biomejs/cli-linux-arm64-musl@2.3.6': + '@biomejs/cli-linux-arm64-musl@2.3.7': optional: true - '@biomejs/cli-linux-arm64@2.3.6': + '@biomejs/cli-linux-arm64@2.3.7': optional: true - '@biomejs/cli-linux-x64-musl@2.3.6': + '@biomejs/cli-linux-x64-musl@2.3.7': optional: true - '@biomejs/cli-linux-x64@2.3.6': + '@biomejs/cli-linux-x64@2.3.7': optional: true - '@biomejs/cli-win32-arm64@2.3.6': + '@biomejs/cli-win32-arm64@2.3.7': optional: true - '@biomejs/cli-win32-x64@2.3.6': + '@biomejs/cli-win32-x64@2.3.7': optional: true '@emotion/babel-plugin@11.13.5': @@ -3704,51 +3704,51 @@ snapshots: transitivePeerDependencies: - typescript - '@swc/core-darwin-arm64@1.15.2': + '@swc/core-darwin-arm64@1.15.3': optional: true - '@swc/core-darwin-x64@1.15.2': + '@swc/core-darwin-x64@1.15.3': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.2': + '@swc/core-linux-arm-gnueabihf@1.15.3': optional: true - '@swc/core-linux-arm64-gnu@1.15.2': + '@swc/core-linux-arm64-gnu@1.15.3': optional: true - '@swc/core-linux-arm64-musl@1.15.2': + '@swc/core-linux-arm64-musl@1.15.3': optional: true - '@swc/core-linux-x64-gnu@1.15.2': + '@swc/core-linux-x64-gnu@1.15.3': optional: true - '@swc/core-linux-x64-musl@1.15.2': + '@swc/core-linux-x64-musl@1.15.3': optional: true - '@swc/core-win32-arm64-msvc@1.15.2': + '@swc/core-win32-arm64-msvc@1.15.3': optional: true - '@swc/core-win32-ia32-msvc@1.15.2': + '@swc/core-win32-ia32-msvc@1.15.3': optional: true - '@swc/core-win32-x64-msvc@1.15.2': + '@swc/core-win32-x64-msvc@1.15.3': optional: true - '@swc/core@1.15.2': + '@swc/core@1.15.3': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.2 - '@swc/core-darwin-x64': 1.15.2 - '@swc/core-linux-arm-gnueabihf': 1.15.2 - '@swc/core-linux-arm64-gnu': 1.15.2 - '@swc/core-linux-arm64-musl': 1.15.2 - '@swc/core-linux-x64-gnu': 1.15.2 - '@swc/core-linux-x64-musl': 1.15.2 - '@swc/core-win32-arm64-msvc': 1.15.2 - '@swc/core-win32-ia32-msvc': 1.15.2 - '@swc/core-win32-x64-msvc': 1.15.2 + '@swc/core-darwin-arm64': 1.15.3 + '@swc/core-darwin-x64': 1.15.3 + '@swc/core-linux-arm-gnueabihf': 1.15.3 + '@swc/core-linux-arm64-gnu': 1.15.3 + '@swc/core-linux-arm64-musl': 1.15.3 + '@swc/core-linux-x64-gnu': 1.15.3 + '@swc/core-linux-x64-musl': 1.15.3 + '@swc/core-win32-arm64-msvc': 1.15.3 + '@swc/core-win32-ia32-msvc': 1.15.3 + '@swc/core-win32-x64-msvc': 1.15.3 '@swc/counter@0.1.3': {} @@ -3978,15 +3978,15 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.0 - '@vitejs/plugin-react-swc@4.2.2(vite@7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.47 - '@swc/core': 1.15.2 - vite: 7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1) + '@swc/core': 1.15.3 + vite: 7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@5.1.1(vite@7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1))': + '@vitejs/plugin-react@5.1.1(vite@7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -3994,7 +3994,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.47 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -4051,7 +4051,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.29: {} + baseline-browser-mapping@2.8.30: {} boolbase@1.0.0: {} @@ -4071,9 +4071,9 @@ snapshots: browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.29 + baseline-browser-mapping: 2.8.30 caniuse-lite: 1.0.30001756 - electron-to-chromium: 1.5.256 + electron-to-chromium: 1.5.259 node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) @@ -4340,7 +4340,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.256: {} + electron-to-chromium@1.5.259: {} emoji-regex@8.0.0: {} @@ -5873,7 +5873,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.2.2(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1): + vite@7.2.4(@types/node@24.10.1)(sass@1.92.1)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) diff --git a/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json b/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json new file mode 100644 index 00000000..e023a3a7 --- /dev/null +++ b/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json @@ -0,0 +1,92 @@ +{ + "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\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE service_location_mode <= $1 ORDER BY name ASC;", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "instance_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "name", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "address", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "pubkey", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "endpoint", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "allowed_ips", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "dns", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "network_id", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "route_all_traffic", + "ordinal": 9, + "type_info": "Bool" + }, + { + "name": "keepalive_interval", + "ordinal": 10, + "type_info": "Integer" + }, + { + "name": "location_mfa_mode: LocationMfaMode", + "ordinal": 11, + "type_info": "Integer" + }, + { + "name": "service_location_mode: ServiceLocationMode", + "ordinal": 12, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e" +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 132e7051..ddc2c46a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -234,7 +234,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -390,7 +390,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -456,7 +456,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -473,7 +473,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -711,7 +711,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -743,11 +743,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", ] @@ -870,9 +871,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "jobserver", @@ -999,7 +1000,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1258,7 +1259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1268,7 +1269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1295,7 +1296,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1343,7 +1344,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1357,7 +1358,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1368,7 +1369,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1379,7 +1380,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1543,7 +1544,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1553,7 +1554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1566,7 +1567,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1649,7 +1650,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1681,7 +1682,7 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1812,7 +1813,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1919,7 +1920,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2028,7 +2029,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2142,7 +2143,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2419,7 +2420,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2521,7 +2522,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2536,7 +2537,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -2582,9 +2583,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" @@ -2968,12 +2969,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -3189,7 +3190,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.12.0", + "indexmap 2.12.1", "selectors", ] @@ -3402,7 +3403,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -3770,7 +3771,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4143,7 +4144,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4320,7 +4321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.0", + "indexmap 2.12.1", ] [[package]] @@ -4331,7 +4332,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.12.0", + "indexmap 2.12.1", ] [[package]] @@ -4438,7 +4439,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4485,7 +4486,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4551,7 +4552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "quick-xml 0.38.4", "serde", "time", @@ -4645,7 +4646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4744,7 +4745,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.110", + "syn 2.0.111", "tempfile", ] @@ -4758,7 +4759,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5099,7 +5100,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5470,7 +5471,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5502,7 +5503,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5614,7 +5615,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5625,7 +5626,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5649,7 +5650,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5692,7 +5693,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", "schemars 1.1.0", "serde_core", @@ -5710,7 +5711,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5732,7 +5733,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -5784,9 +5785,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", ] @@ -5955,7 +5956,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.12.0", + "indexmap 2.12.1", "log", "memchr", "once_cell", @@ -5982,7 +5983,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6005,7 +6006,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.110", + "syn 2.0.111", "tokio", "url", ] @@ -6190,7 +6191,7 @@ checksum = "68c6387c1c7b53060605101b63d93edca618c6cf7ce61839f2ec2a527419fdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6211,7 +6212,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6244,9 +6245,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -6270,7 +6271,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6364,7 +6365,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6471,7 +6472,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.110", + "syn 2.0.111", "tauri-utils", "thiserror 2.0.17", "time", @@ -6489,7 +6490,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "tauri-codegen", "tauri-utils", ] @@ -6905,7 +6906,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6916,7 +6917,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7035,7 +7036,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7100,7 +7101,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde_core", "serde_spanned 1.0.3", "toml_datetime 0.7.3", @@ -7133,7 +7134,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -7144,7 +7145,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -7157,7 +7158,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", @@ -7219,7 +7220,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7244,7 +7245,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.110", + "syn 2.0.111", "tempfile", "tonic-build", ] @@ -7257,7 +7258,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.12.0", + "indexmap 2.12.1", "pin-project-lite", "slab", "sync_wrapper", @@ -7330,7 +7331,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7558,7 +7559,7 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.12.1", "once_cell", "serde", "tempfile", @@ -7600,10 +7601,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c2a6f93e7b73726e2015696ece25ca0ac5a5f1cf8d6a7ab5214dd0a01d2edf" dependencies = [ "anyhow", - "indexmap 2.12.0", + "indexmap 2.12.1", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7618,7 +7619,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.110", + "syn 2.0.111", "toml 0.9.8", "uniffi_meta", ] @@ -7643,7 +7644,7 @@ checksum = "8c27c4b515d25f8e53cc918e238c39a79c3144a40eaf2e51c4a7958973422c29" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.12.1", "tempfile", "uniffi_internal_macros", ] @@ -7708,9 +7709,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" @@ -7916,7 +7917,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -8126,7 +8127,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -8334,7 +8335,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -8345,7 +8346,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -8981,7 +8982,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -9029,7 +9030,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "zbus_names", "zvariant", "zvariant_utils", @@ -9049,22 +9050,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -9084,7 +9085,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -9105,7 +9106,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -9138,7 +9139,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -9180,7 +9181,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "zvariant_utils", ] @@ -9193,6 +9194,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.110", + "syn 2.0.111", "winnow 0.7.13", ] diff --git a/src-tauri/src/apple.rs b/src-tauri/src/apple.rs index b9aa991b..4d7fafb6 100644 --- a/src-tauri/src/apple.rs +++ b/src-tauri/src/apple.rs @@ -15,8 +15,8 @@ use block2::RcBlock; use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; use objc2::{rc::Retained, runtime::AnyObject}; use objc2_foundation::{ - ns_string, NSArray, NSData, NSDictionary, NSError, NSMutableArray, NSMutableDictionary, - NSNumber, NSString, + ns_string, NSArray, NSData, NSDate, NSDictionary, NSError, NSMutableArray, NSMutableDictionary, + NSNumber, NSRunLoop, NSString, }; use objc2_network_extension::{ NETunnelProviderManager, NETunnelProviderProtocol, NETunnelProviderSession, @@ -25,15 +25,17 @@ use serde::{Deserialize, Serialize}; use sqlx::SqliteExecutor; use crate::{ - database::models::{location::Location, tunnel::Tunnel, wireguard_keys::WireguardKeys, Id}, + database::{ + models::{location::Location, tunnel::Tunnel, wireguard_keys::WireguardKeys, Id}, + DB_POOL, + }, error::Error, utils::{DEFAULT_ROUTE_IPV4, DEFAULT_ROUTE_IPV6}, }; -// const BUNDLE_ID: &str = "net.defguard"; const PLUGIN_BUNDLE_ID: &str = "net.defguard.VPNExtension"; -// Should match the declaration in Swift. +/// Tunnel statistics shared with VPNExtension (written in Swift). #[derive(Deserialize)] #[repr(C)] #[serde(rename_all = "camelCase")] @@ -45,9 +47,25 @@ pub(crate) struct Stats { pub(crate) last_handshake: u64, } -/// Find `NETunnelProviderManager` in system preferences. -fn manager_for_name(name: &str) -> Option> { - let name_string = NSString::from_str(name); +/// 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); + } +} + +pub(crate) fn manager_for_key_and_value( + key: &str, + value: Id, +) -> Option> { + let key_string = NSString::from_str(key); let plugin_bundle_id = NSString::from_str(PLUGIN_BUNDLE_ID); let (tx, rx) = channel(); @@ -78,24 +96,33 @@ fn manager_for_name(name: &str) -> Option> { continue; } } - if let Some(descr) = unsafe { manager.localizedDescription() } { - if descr == name_string { - tx.send(Some(manager)).unwrap(); - return; + + if let Some(config_dict) = unsafe { tunnel_protocol.providerConfiguration() } { + if let Some(any_object) = config_dict.objectForKey(&key_string) { + let Ok(id) = any_object.downcast::() else { + warn!("Failed to downcast ID to NSNumber"); + continue; + }; + if id.as_i64() == value { + // This is the manager we were looking for. + tx.send(Some(manager)).expect("Sender is dead"); + return; + } } } } - tx.send(None).unwrap(); + tx.send(None).expect("Sender is dead"); }, ); unsafe { NETunnelProviderManager::loadAllFromPreferencesWithCompletionHandler(&handler); } - rx.recv().unwrap() + rx.recv().expect("Receiver is dead") } +/// Tunnel configuration shared with VPNExtension (written in Swift). #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct TunnelConfiguration { @@ -234,14 +261,30 @@ impl TunnelConfiguration { 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) => ("locationId", location_id), + (None, Some(tunnel_id)) => ("tunnelId", 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); + unsafe { - let provider_manager = - manager_for_name(&self.name).unwrap_or_else(|| NETunnelProviderManager::new()); + let provider_manager = self + .tunnel_provider_manager() + .unwrap_or_else(|| NETunnelProviderManager::new()); let tunnel_protocol = NETunnelProviderProtocol::new(); - let plugin_bundle_id = NSString::from_str(PLUGIN_BUNDLE_ID); 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()) @@ -259,10 +302,7 @@ impl TunnelConfiguration { provider_manager.setLocalizedDescription(Some(&name)); provider_manager.setEnabled(true); - // Save to preferences. - let spinlock = Arc::new(AtomicBool::new(false)); - let spinlock_clone = Arc::clone(&spinlock); - let name = self.name.clone(); + // Save to system settings. let handler = RcBlock::new(move |error_ptr: *mut NSError| { if error_ptr.is_null() { info!("Saved tunnel configuration for {name}"); @@ -272,36 +312,90 @@ impl TunnelConfiguration { spinlock_clone.store(true, Ordering::Release); }); provider_manager.saveToPreferencesWithCompletionHandler(Some(&*handler)); - while !spinlock.load(Ordering::Acquire) { - spin_loop(); + } + + 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 { + info!("VPN started"); } + } else { + error!( + "Couldn't find configuration from system settings for {}", + self.name + ); } } } -/// IMPORTANT: This is currently for testing. Assume the config has been saved. -pub(crate) fn start_tunnel(name: &str) { - if let Some(provider_manager) = manager_for_name(name) { - if let Err(err) = unsafe { provider_manager.connection().startVPNTunnelAndReturnError() } { - error!("Failed to start VPN: {err}"); - } else { - info!("VPN started"); +/// 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("locationId", location.id) { + unsafe { + provider_manager.removeFromPreferencesWithCompletionHandler(None); } } else { - error!("Couldn't find configuration from preferences for {name}"); + error!( + "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("locationId", tunnel.id) { + unsafe { + provider_manager.removeFromPreferencesWithCompletionHandler(None); + } + } else { + error!( + "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 { + if let Some(provider_manager) = manager_for_key_and_value("locationId", location.id) { + unsafe { + provider_manager.connection().stopVPNTunnel(); + } + info!("VPN stopped"); + true + } else { + error!( + "Couldn't find configuration in system settings for location {}", + location.name + ); + false } } -/// IMPORTANT: This is currently for testing. Assume the config has been saved. -pub(crate) fn stop_tunnel(name: &str) -> bool { - if let Some(provider_manager) = manager_for_name(name) { +/// Stop tunnel for [`Tunnel`]. +pub(crate) fn stop_tunnel_for_tunnel(tunnel: &Tunnel) -> bool { + if let Some(provider_manager) = manager_for_key_and_value("tunnelId", tunnel.id) { unsafe { provider_manager.connection().stopVPNTunnel(); } info!("VPN stopped"); true } else { - error!("Couldn't find configuration from preferences for {name}"); + error!( + "Couldn't find configuration in system settings for location {}", + tunnel.name + ); false } } @@ -391,17 +485,92 @@ pub(crate) fn tunnel_stats() -> Vec { spin_loop(); } - let stats = new_stats.lock().unwrap().drain(..).collect(); + let stats = new_stats + .lock() + .expect("Failed to acquire lock") + .drain(..) + .collect(); stats } +/// Remove all locations and tunnels from system settings. +pub fn purge_system_settings() { + 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; + }; + + for manager in managers { + unsafe { manager.removeFromPreferencesWithCompletionHandler(None) }; + } + + spinlock_clone.store(true, Ordering::Release); + }, + ); + unsafe { + NETunnelProviderManager::loadAllFromPreferencesWithCompletionHandler(&handler); + } + + while !spinlock.load(Ordering::Acquire) { + spin_loop(); + } + + debug!("Removed all configurations from system settings."); +} + +/// Synchronize locations with system settings. +pub async fn sync_locations_and_tunnels() { + if let Ok(all_locations) = Location::all(&*DB_POOL, false).await { + for location in all_locations { + // For syncing, set `preshred_key` and `mtu` to `None`. + let Ok(tunnel_config) = location.tunnel_configurarion(&*DB_POOL, None, None).await + else { + error!( + "Failed to convert location {} to tunnel configuration.", + location.name + ); + continue; + }; + tunnel_config.save(); + } + } else { + error!("Failed to fetch all location from the database"); + } + + if let Ok(all_tunnels) = Tunnel::all(&*DB_POOL).await { + for tunnel in all_tunnels { + // For syncing, set `mtu` to `None`. + let Ok(tunnel_config) = tunnel.tunnel_configurarion(None) else { + error!( + "Failed to convert tunnel {} to tunnel configuration.", + tunnel.name + ); + continue; + }; + tunnel_config.save(); + } + } else { + error!("Failed to fetch all location from the database"); + } + + debug!("Saved all configurations with system settings."); +} + impl Location { + /// Build [`TunnelConfiguration`] from [`Location`]. pub(crate) async fn tunnel_configurarion<'e, E>( &self, executor: E, preshared_key: Option, - dns: Vec, - dns_search: Vec, mtu: Option, ) -> Result where @@ -476,6 +645,7 @@ impl Location { error!("{msg}"); Error::InternalError(msg) })?; + let (dns, dns_search) = self.dns(); Ok(TunnelConfiguration { location_id: Some(self.id), tunnel_id: None, @@ -492,10 +662,9 @@ impl Location { } impl Tunnel { + /// Build [`TunnelConfiguration`] from [`Tunnel`]. pub(crate) fn tunnel_configurarion( &self, - dns: Vec, - dns_search: Vec, mtu: Option, ) -> Result { // prepare peer config @@ -562,6 +731,7 @@ impl Tunnel { error!("{msg}"); Error::InternalError(msg) })?; + let (dns, dns_search) = self.dns(); Ok(TunnelConfiguration { location_id: None, tunnel_id: Some(self.id), diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 4ff7c2cc..20910a78 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)] @@ -76,6 +81,19 @@ async fn startup(app_handle: &AppHandle) { } }; } + #[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 { + defguard_client::apple::purge_system_settings(); + defguard_client::apple::sync_locations_and_tunnels().await; + semaphore_clone.store(true, Ordering::Release); + }); + defguard_client::apple::spawn_runloop_and_wait_for(semaphore); + let _ = handle.await; + } // Run periodic tasks. let periodic_tasks_handle = app_handle.clone(); @@ -300,15 +318,6 @@ fn main() { app_handle_clone.exit(0); }); debug!("Ctrl-C handler has been set up successfully"); - - let app_handle_clone = app_handle.clone(); - tauri::async_runtime::spawn(async move { - // Wait for frontend to be ready - tokio::time::sleep(std::time::Duration::from_secs(15)).await; - - // Handle client initialization if necessary - handle_client_initialization(&app_handle_clone).await; - }); } RunEvent::ExitRequested { code, api, .. } => { debug!("Received exit request"); @@ -321,24 +330,32 @@ fn main() { // Handle shutdown. RunEvent::Exit => { debug!("Exiting the application's main event loop."); - let handle = tauri::async_runtime::spawn(async { - let _ = close_all_connections().await; - // This will clean the database file, pruning write-ahead log. - DB_POOL.close().await; - }); - // Obj-C API needs a runtime, but at this point Tauri has closed its runtime, so - // create a temporary one. #[cfg(target_os = "macos")] { - use objc2_foundation::{NSDate, NSRunLoop}; - let run_loop = NSRunLoop::currentRunLoop(); - // Should be enough to quit. - let date = NSDate::dateWithTimeIntervalSinceNow(2.0); - run_loop.runUntilDate(&date); + 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; + }); } - tauri::async_runtime::block_on(async { - let _ = handle.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 81fa993f..54c77bb6 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -14,7 +14,9 @@ use tauri::{AppHandle, Emitter, Manager, State}; const UPDATE_URL: &str = "https://pkgs.defguard.net/api/update/check"; #[cfg(target_os = "macos")] -use crate::apple::stop_tunnel; +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}, @@ -51,10 +53,10 @@ use crate::{ #[cfg(not(target_os = "macos"))] use crate::{ service::{ + client::DAEMON_CLIENT, proto::{ DeleteServiceLocationsRequest, RemoveInterfaceRequest, SaveServiceLocationsRequest, }, - utils::DAEMON_CLIENT, }, utils::execute_command, }; @@ -213,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, @@ -222,7 +224,7 @@ pub struct Device { pub created_at: i64, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Serialize)] pub struct InstanceResponse { // uuid pub id: String, @@ -230,7 +232,7 @@ pub struct InstanceResponse { pub url: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Serialize)] pub struct SaveDeviceConfigResponse { locations: Vec>, instance: Instance, @@ -678,11 +680,6 @@ pub(crate) async fn do_update_instance( info!("Locations for instance {instance} didn't change. Not updating them."); } - let private_key = WireguardKeys::find_by_instance_id(transaction.as_mut(), instance.id) - .await? - .ok_or(Error::NotFound)? - .prvkey; - if service_locations.is_empty() { debug!( "No service locations for instance {}({}), removing all existing service locations connections if there are any.", @@ -720,10 +717,15 @@ pub(crate) async fn do_update_instance( #[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: private_key.clone(), + private_key, }; debug!( @@ -1004,15 +1006,16 @@ pub async fn delete_instance(instance_id: Id, handle: AppHandle) -> Result<(), E ); } for location in instance_locations { - if let Some(connection) = app_state + if let Some(_connection) = app_state .remove_connection(location.id, ConnectionType::Location) .await { - let result = stop_tunnel(&location.name); + 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); } } @@ -1187,19 +1190,10 @@ pub async fn tunnel_details(tunnel_id: Id) -> Result, Error> { } } -#[cfg(target_os = "macos")] -#[tauri::command(async)] -pub async fn delete_tunnel(tunnel_id: Id, handle: AppHandle) -> Result<(), Error> { - // TODO: implementation - Ok(()) -} - -#[cfg(not(target_os = "macos"))] #[tauri::command(async)] 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 { @@ -1219,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?; diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index ebd74f9f..fdf7f876 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -1,19 +1,24 @@ +use std::fmt; #[cfg(target_os = "macos")] use std::net::IpAddr; -use std::{fmt, str::FromStr}; +#[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}; +#[cfg(not(target_os = "macos"))] +use crate::utils::{DEFAULT_ROUTE_IPV4, DEFAULT_ROUTE_IPV6}; use crate::{ - database::models::wireguard_keys::WireguardKeys, error::Error, proto::{ LocationMfaMode as ProtoLocationMfaMode, ServiceLocationMode as ProtoServiceLocationMode, }, - utils::{DEFAULT_ROUTE_IPV4, DEFAULT_ROUTE_IPV6}, }; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Type)] @@ -90,7 +95,7 @@ impl fmt::Display for Location { impl Location { /// Ignores service locations - #[cfg(windows)] + #[cfg(any(windows, target_os = "macos"))] pub(crate) async fn all<'e, E>( executor: E, include_service_locations: bool, 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/lib.rs b/src-tauri/src/lib.rs index e7025f85..9d565f93 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,7 +18,7 @@ use self::database::models::{Id, NoId}; pub mod active_connections; pub mod app_config; #[cfg(target_os = "macos")] -mod apple; +pub mod apple; pub mod appstate; pub mod commands; pub mod database; diff --git a/src-tauri/src/service/client.rs b/src-tauri/src/service/client.rs new file mode 100644 index 00000000..1e266cb3 --- /dev/null +++ b/src-tauri/src/service/client.rs @@ -0,0 +1,63 @@ +use std::sync::LazyLock; + +use hyper_util::rt::TokioIo; +#[cfg(unix)] +use tokio::net::UnixStream; +use tonic::transport::channel::{Channel, Endpoint}; +#[cfg(unix)] +use tonic::transport::Uri; +use tower::service_fn; + +use super::{ + daemon::DAEMON_SOCKET_PATH, 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/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 c9d54416..0d9925c7 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -1,7 +1,11 @@ +#[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; @@ -9,592 +13,16 @@ pub mod utils; pub mod windows; use std::{ - collections::HashMap, - net::IpAddr, - pin::Pin, str::FromStr, - sync::{Arc, Mutex, RwLock}, - 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, -}; -#[cfg(unix)] -use nix::unistd::{chown, Group}; -use proto::{ - desktop_daemon_service_server::{DesktopDaemonService, DesktopDaemonServiceServer}, - CreateInterfaceRequest, InterfaceData, ReadInterfaceDataRequest, RemoveInterfaceRequest, + InterfaceConfiguration, }; -#[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 self::config::Config; -use super::VERSION; -#[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}, -}; - -#[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), - #[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. -#[cfg(not(target_os = "macos"))] -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) -} - -#[cfg(not(target_os = "macos"))] -#[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(all(unix, not(target_os = "macos")))] -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(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(()) -} impl From for proto::InterfaceConfig { fn from(config: InterfaceConfiguration) -> Self { @@ -685,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), @@ -696,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/utils.rs b/src-tauri/src/service/utils.rs index a5612110..920f2f9d 100644 --- a/src-tauri/src/service/utils.rs +++ b/src-tauri/src/service/utils.rs @@ -1,17 +1,8 @@ use std::io::stdout; -#[cfg(not(target_os = "macos"))] -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; -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, @@ -22,58 +13,6 @@ use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY; #[cfg(windows)] use crate::service::named_pipe::PIPE_NAME; -use crate::service::proto::desktop_daemon_service_client::DesktopDaemonServiceClient; - -#[cfg(not(target_os = "macos"))] -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(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); - } - }; - 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) - }); pub fn logging_setup(log_dir: &str, log_level: &str) -> WorkerGuard { // prepare log file appender diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index ab9e8b41..c2320e11 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,12 +1,13 @@ -use std::{env, path::Path, process::Command, str::FromStr}; +#[cfg(not(target_os = "macos"))] +use std::str::FromStr; #[cfg(target_os = "macos")] -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +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; @@ -25,12 +26,7 @@ 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::{start_tunnel, stop_tunnel, tunnel_stats}; -#[cfg(not(target_os = "macos"))] -use crate::service::{ - proto::{CreateInterfaceRequest, ReadInterfaceDataRequest, RemoveInterfaceRequest}, - utils::DAEMON_CLIENT, -}; +use crate::apple::{stop_tunnel_for_location, stop_tunnel_for_tunnel, tunnel_stats}; use crate::{ appstate::AppState, commands::LocationInterfaceDetails, @@ -38,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, }, @@ -51,6 +46,14 @@ use crate::{ 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}, + }, +}; pub(crate) static DEFAULT_ROUTE_IPV4: &str = "0.0.0.0/0"; pub(crate) static DEFAULT_ROUTE_IPV6: &str = "::/0"; @@ -125,25 +128,23 @@ pub(crate) async fn setup_interface( #[cfg(target_os = "macos")] pub(crate) async fn setup_interface( location: &Location, - name: &str, + _name: &str, preshared_key: Option, mtu: Option, pool: &DbPool, ) -> Result { debug!("Setting up interface for location: {location}"); - // FIXME: not really useful nor true. - let interface_name = get_interface_name(name); - let (dns, dns_search) = location.dns(); let tunnel_config = location - .tunnel_configurarion(pool, preshared_key, dns, dns_search, mtu) + .tunnel_configurarion(pool, preshared_key, mtu) .await?; tunnel_config.save(); tokio::time::sleep(TUNNEL_START_DELAY).await; - start_tunnel(&location.name); + tunnel_config.start_tunnel(); - Ok(interface_name) + // FIXME: not really useful nor true. + Ok(String::new()) } #[cfg(target_os = "macos")] @@ -536,21 +537,19 @@ pub async fn setup_interface_tunnel( #[cfg(target_os = "macos")] pub async fn setup_interface_tunnel( tunnel: &Tunnel, - name: &str, + _name: &str, mtu: Option, ) -> Result { debug!("Setting up interface for tunnel: {tunnel}"); - // FIXME: not really useful nor true. - let interface_name = get_interface_name(name); - let (dns, dns_search) = tunnel.dns(); - let tunnel_config = tunnel.tunnel_configurarion(dns, dns_search, mtu)?; + let tunnel_config = tunnel.tunnel_configurarion(mtu)?; tunnel_config.save(); tokio::time::sleep(TUNNEL_START_DELAY).await; - start_tunnel(&tunnel.name); + tunnel_config.start_tunnel(); - Ok(interface_name) + // FIXME: not really useful nor true. + Ok(String::new()) } pub async fn get_tunnel_interface_details( @@ -563,7 +562,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!( @@ -622,8 +629,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!( @@ -780,7 +794,7 @@ pub(crate) async fn disconnect_interface( #[cfg(target_os = "macos")] { - let result = stop_tunnel(&location.name); + let result = stop_tunnel_for_location(&location); error!( "stop_tunnel() for location {} returned {result:?}", location.name @@ -856,7 +870,7 @@ pub(crate) async fn disconnect_interface( #[cfg(target_os = "macos")] { - let result = stop_tunnel(&tunnel.name); + let result = stop_tunnel_for_tunnel(&tunnel); error!( "stop_tunnel() for tunnel {} returned {result:?}", tunnel.name