├── src-tauri ├── src │ ├── net_interfaces │ │ ├── mod.rs │ │ └── general.rs │ ├── dns │ │ ├── mod.rs │ │ ├── dns_utils.rs │ │ └── dns_server.rs │ ├── commands │ │ ├── mod.rs │ │ ├── net_interfaces.rs │ │ └── dns.rs │ ├── types.rs │ ├── main.rs │ ├── utils │ │ └── mod.rs │ └── lib.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 32x32.png │ ├── 64x64.png │ ├── icon.icns │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── ios │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x-1.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ └── AppIcon-83.5x83.5@2x.png │ └── android │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png ├── .gitignore ├── capabilities │ ├── desktop.json │ └── default.json ├── build.rs ├── tauri.conf.json └── Cargo.toml ├── src ├── vite-env.d.ts ├── data │ ├── defaultSetting.ts │ └── defaultServers.ts ├── styles │ ├── hero.ts │ └── main.css ├── constants │ ├── interface.ts │ ├── updater.ts │ └── dns-servers.ts ├── types.ts ├── components │ ├── icons │ │ ├── Minimize.tsx │ │ ├── VirtualMachine.tsx │ │ ├── Save.tsx │ │ ├── Bluetooth.tsx │ │ ├── Update.tsx │ │ ├── Maximize.tsx │ │ ├── Close.tsx │ │ ├── Texture.tsx │ │ ├── VPN.tsx │ │ ├── SheildCheck.tsx │ │ ├── CircleDot.tsx │ │ ├── Copy.tsx │ │ ├── Virtual.tsx │ │ ├── Lan.tsx │ │ ├── Network.tsx │ │ ├── Wifi.tsx │ │ ├── Reset.tsx │ │ ├── Connected.tsx │ │ ├── Test.tsx │ │ ├── Server.tsx │ │ ├── Setting.tsx │ │ ├── Disconnect.tsx │ │ ├── DNSServer.tsx │ │ ├── Broom.tsx │ │ └── Ethernet.tsx │ ├── ToggleButton.tsx │ ├── InterfaceIp.tsx │ ├── Titlebar.tsx │ ├── ConfirmModal.tsx │ ├── Setting │ │ └── TestDomain.tsx │ ├── Navigation.tsx │ ├── Updater.tsx │ └── ServerModal.tsx ├── main.tsx ├── layouts │ └── DefaultLayout.tsx ├── hooks │ ├── useDnsState.ts │ ├── useInterfaces.ts │ ├── useDns.ts │ └── useUpdater.ts ├── routes.tsx ├── stores │ ├── useAutoStartStore.ts │ ├── tauriServersStore.ts │ ├── useServersStore.ts │ └── tauriSettingStore.ts ├── providers.tsx ├── screens │ ├── Setting.tsx │ ├── NetworkInterfaces.tsx │ ├── Servers.tsx │ └── main.tsx ├── utils │ └── interface.tsx └── assets │ └── react.svg ├── public └── FunnelDisplay.ttf ├── .vscode └── extensions.json ├── .tauri └── public.pub ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── vite.config.ts ├── .github └── workflows │ └── release.yml ├── package.json └── README.md /src-tauri/src/net_interfaces/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod general; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dns_server; 2 | pub mod dns_utils; 3 | -------------------------------------------------------------------------------- /src-tauri/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dns; 2 | pub mod net_interfaces; 3 | -------------------------------------------------------------------------------- /src/data/defaultSetting.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SETTING = { 2 | test_domain: "youtube.com", 3 | }; 4 | -------------------------------------------------------------------------------- /public/FunnelDisplay.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/public/FunnelDisplay.ttf -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src/styles/hero.ts: -------------------------------------------------------------------------------- 1 | // hero.ts 2 | import { heroui } from "@heroui/theme"; 3 | export default heroui({ 4 | defaultTheme: "dark", 5 | }); 6 | -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.tauri/public.pub: -------------------------------------------------------------------------------- 1 | dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc1MjJBQjRBMUYxREZCMjAKUldRZyt4MGZTcXNpZFFZdDY3cy9vcWcvNDhDdmtkMVJMbUhZclB6dmpkUnBGUzBkTWZBVlVIMUwK -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/constants/interface.ts: -------------------------------------------------------------------------------- 1 | // https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-pnpentity 2 | export const CONFIG_MANAGER_ERROR_CODE_DISABLED = 22; 3 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ho3einWave/better-dns-jumper/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize, Clone)] 4 | pub struct DoHTestResult { 5 | pub success: bool, 6 | pub latency: usize, 7 | pub error: Option, 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | better_dns_jumper_lib::run() 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "window-state:default", 13 | "autostart:default", 14 | "updater:default" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/constants/updater.ts: -------------------------------------------------------------------------------- 1 | // Application Constants 2 | 3 | // GitHub repository URL for releases 4 | // Update this to match your GitHub repository 5 | export const GITHUB_REPO_URL = 6 | "https://github.com/ho3einwave/better-dns-jumper"; 7 | 8 | // Release URL template 9 | export const getReleaseUrl = (version: string) => { 10 | return `${GITHUB_REPO_URL}/releases/tag/v${version}`; 11 | }; 12 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type SERVER = { 2 | type: "doh" | "dns"; 3 | key: string; 4 | name: string; 5 | servers: string[]; 6 | tags: string[]; 7 | }; 8 | 9 | export const PROTOCOLS: Protocol[] = [ 10 | { 11 | key: "dns", 12 | name: "DNS", 13 | }, 14 | { 15 | key: "doh", 16 | name: "DoH", 17 | }, 18 | ]; 19 | 20 | export type Protocol = { 21 | key: string; 22 | name: string; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/icons/Minimize.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Minimize(props: SVGProps) { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | target/rust-analyzer/flycheck0/stderr 26 | target/rust-analyzer/flycheck0/stdout 27 | .tauri/private.key -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Better DNS Jumper 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "store:default", 10 | "core:default", 11 | "opener:default", 12 | "core:window:default", 13 | "core:window:allow-start-dragging", 14 | "core:window:allow-minimize", 15 | "core:window:allow-close", 16 | "core:window:allow-set-decorations" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./styles/main.css"; 4 | import { router } from "./routes"; 5 | import { RouterProvider } from "react-router"; 6 | import Providers from "./providers"; 7 | import { getCurrentWindow } from "@tauri-apps/api/window"; 8 | 9 | getCurrentWindow().setDecorations(false); 10 | 11 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | import Titlebar from "../components/Titlebar"; 3 | import Navigation from "../components/Navigation"; 4 | import Updater from "../components/Updater"; 5 | 6 | const DefaultLayout = () => { 7 | return ( 8 |
9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default DefaultLayout; 20 | -------------------------------------------------------------------------------- /src/components/icons/VirtualMachine.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function VirtualMachine(props: SVGProps) { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/icons/Save.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Save(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/Bluetooth.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Bluetooth(props: SVGProps) { 4 | return ( 5 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/Update.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Update(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/components/icons/Maximize.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Maximize(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/components/icons/Close.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Close(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/Texture.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Texture(props: SVGProps) { 4 | return ( 5 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/VPN.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function VPN(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useDnsState.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface DnsState { 4 | isActive: boolean; 5 | dnsServer: string; 6 | protocol: "dns" | "doh"; 7 | setProtocol: (protocol: "dns" | "doh") => void; 8 | setIsActive: (isActive: boolean) => void; 9 | setDnsServer: (dnsServer: string) => void; 10 | toggleIsActive: () => void; 11 | } 12 | 13 | export const useDnsState = create((set) => ({ 14 | isActive: false, 15 | dnsServer: "", 16 | protocol: "dns", 17 | setProtocol: (protocol) => set({ protocol }), 18 | setIsActive: (isActive) => set({ isActive }), 19 | setDnsServer: (dnsServer) => set({ dnsServer }), 20 | toggleIsActive: () => set((state) => ({ isActive: !state.isActive })), 21 | })); 22 | -------------------------------------------------------------------------------- /src/components/icons/SheildCheck.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function ShieldCheck(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/CircleDot.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function CircleDot(props: SVGProps) { 4 | return ( 5 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/Copy.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Copy(props: SVGProps) { 4 | return ( 5 | 12 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin './hero.ts'; 3 | @source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'; 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @font-face { 7 | font-family: "FunnelDisplay"; 8 | src: url("/FunnelDisplay.ttf") format("truetype"); 9 | font-weight: 100 900; 10 | font-display: swap; 11 | } 12 | 13 | * { 14 | user-select: none; 15 | -webkit-user-select: none; 16 | -moz-user-select: none; 17 | -ms-user-select: none; 18 | } 19 | 20 | /* html, 21 | body { 22 | overscroll-behavior: none; 23 | } */ 24 | 25 | body { 26 | font-family: "FunnelDisplay", sans-serif; 27 | } 28 | 29 | .bg-body, 30 | html { 31 | @apply bg-zinc-950; 32 | } 33 | 34 | .inner-shadow-green-500 { 35 | box-shadow: inset 0 0 15px 0 rgba(0, 255, 0, 0.6); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/icons/Virtual.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Virtual(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/Lan.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Lan(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/icons/Network.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Network(props: SVGProps) { 4 | return ( 5 | 12 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { createHashRouter } from "react-router"; 2 | import Main from "./screens/main"; 3 | import DefaultLayout from "./layouts/DefaultLayout"; 4 | import NetworkInterfaces from "./screens/NetworkInterfaces"; 5 | import Servers from "./screens/Servers"; 6 | import Setting from "./screens/Setting"; 7 | 8 | export const router = createHashRouter([ 9 | { 10 | path: "/", 11 | element: , 12 | children: [ 13 | { 14 | path: "/", 15 | element:
, 16 | }, 17 | { 18 | path: "/servers", 19 | element: , 20 | }, 21 | { 22 | path: "/network-interfaces", 23 | element: , 24 | }, 25 | { 26 | path: "/settings", 27 | element: , 28 | }, 29 | ], 30 | }, 31 | ]); 32 | -------------------------------------------------------------------------------- /src/stores/useAutoStartStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { isEnabled, enable, disable } from "@tauri-apps/plugin-autostart"; 3 | 4 | interface AutoStartStore { 5 | isLoading: boolean; 6 | isAutoStartEnabled: boolean; 7 | load: () => void; 8 | setIsAutoStartEnabled: (isAutoStartEnabled: boolean) => void; 9 | } 10 | 11 | export const useAutoStartStore = create((set) => ({ 12 | isLoading: true, 13 | isAutoStartEnabled: false, 14 | load: async () => { 15 | const isAutoStartEnabled = await isEnabled(); 16 | set({ isAutoStartEnabled, isLoading: false }); 17 | }, 18 | setIsAutoStartEnabled: async (isAutoStartEnabled) => { 19 | if (isAutoStartEnabled) { 20 | await enable(); 21 | } else { 22 | await disable(); 23 | } 24 | const newIsAutoStartEnabled = await isEnabled(); 25 | set({ isAutoStartEnabled: newIsAutoStartEnabled }); 26 | }, 27 | })); 28 | -------------------------------------------------------------------------------- /src/components/icons/Wifi.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Wifi(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let mut windows = tauri_build::WindowsAttributes::new(); 3 | windows = windows.app_manifest( 4 | r#" 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | "#, 27 | ); 28 | 29 | tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows)) 30 | .expect("failed to run build script"); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/icons/Reset.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Reset(props: SVGProps) { 4 | return ( 5 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/icons/Connected.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Connected(props: SVGProps) { 4 | return ( 5 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/icons/Test.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Test(props: SVGProps) { 4 | return ( 5 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | // @ts-expect-error process is a nodejs global 6 | const host = process.env.TAURI_DEV_HOST; 7 | 8 | // https://vite.dev/config/ 9 | export default defineConfig(async () => ({ 10 | plugins: [react(), tailwindcss()], 11 | 12 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 13 | // 14 | // 1. prevent Vite from obscuring rust errors 15 | clearScreen: false, 16 | // 2. tauri expects a fixed port, fail if that port is not available 17 | server: { 18 | port: 1420, 19 | strictPort: true, 20 | host: host || false, 21 | hmr: host 22 | ? { 23 | protocol: "ws", 24 | host, 25 | port: 1421, 26 | } 27 | : undefined, 28 | watch: { 29 | // 3. tell Vite to ignore watching `src-tauri` 30 | ignored: ["**/src-tauri/**"], 31 | }, 32 | }, 33 | })); 34 | -------------------------------------------------------------------------------- /src/providers.tsx: -------------------------------------------------------------------------------- 1 | import { HeroUIProvider } from "@heroui/system"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | import { useState } from "react"; 4 | import { ToastProvider } from "@heroui/toast"; 5 | 6 | export default function Providers({ children }: { children: React.ReactNode }) { 7 | const [queryClient] = useState( 8 | () => 9 | new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | refetchOnWindowFocus: false, 13 | }, 14 | }, 15 | }) 16 | ); 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/icons/Server.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Server(props: SVGProps) { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/stores/tauriServersStore.ts: -------------------------------------------------------------------------------- 1 | import { load } from "@tauri-apps/plugin-store"; 2 | import { DEFAULT_SERVERS } from "../data/defaultServers"; 3 | import { SERVER } from "../types"; 4 | 5 | let storePromise: ReturnType | null = null; 6 | 7 | async function getStore() { 8 | if (!storePromise) { 9 | storePromise = load("servers.json", { 10 | autoSave: true, 11 | defaults: { servers: DEFAULT_SERVERS }, 12 | }); 13 | } 14 | return storePromise; 15 | } 16 | 17 | export async function loadServers(): Promise { 18 | const store = await getStore(); 19 | const servers = await store.get("servers"); 20 | 21 | if (!servers) { 22 | await store.set("servers", DEFAULT_SERVERS); 23 | await store.save(); 24 | return DEFAULT_SERVERS; 25 | } 26 | 27 | return servers; 28 | } 29 | 30 | export async function persistServers(servers: SERVER[]) { 31 | const store = await getStore(); 32 | await store.set("servers", servers); 33 | await store.save(); 34 | } 35 | 36 | export async function resetServers() { 37 | const store = await getStore(); 38 | await store.set("servers", DEFAULT_SERVERS); 39 | await store.save(); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/icons/Setting.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Setting(props: SVGProps) { 4 | return ( 5 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src-tauri/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use std::net::Ipv4Addr; 3 | use wmi::{COMLibrary, WMIConnection}; 4 | 5 | use crate::{ 6 | dns::dns_utils::clear_dns_by_path, 7 | net_interfaces::general::{get_best_interface_idx, get_interface_by_index}, 8 | }; 9 | 10 | pub fn ipv4_to_u32(ipv4: Ipv4Addr) -> u32 { 11 | ipv4.into() 12 | } 13 | 14 | pub fn create_wmi_connection() -> Result { 15 | let com_con = unsafe { COMLibrary::assume_initialized() }; 16 | let wmi_con = match WMIConnection::new(com_con) { 17 | Ok(wmi) => wmi, 18 | Err(e) => { 19 | error!("WMI connection failed: {:?}", e); 20 | let error_msg = format!("WMI connection failed: {:?}", e); 21 | return Err(error_msg); 22 | } 23 | }; 24 | 25 | Ok(wmi_con) 26 | } 27 | 28 | pub fn clear_dns_on_exit() -> Result<(), String> { 29 | let best_interface_idx = get_best_interface_idx()?; 30 | let interface = get_interface_by_index(best_interface_idx)?; 31 | let net_config = interface 32 | .config 33 | .ok_or("Failed to get network configuration")?; 34 | let path = net_config.path.ok_or("Failed to get network path")?; 35 | 36 | clear_dns_by_path(path)?; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/icons/Disconnect.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Disconnect(props: SVGProps) { 4 | return ( 5 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src-tauri/src/commands/net_interfaces.rs: -------------------------------------------------------------------------------- 1 | use crate::net_interfaces::general; 2 | use log::error; 3 | 4 | #[tauri::command(rename_all = "snake_case")] 5 | pub fn change_interface_state(interface_idx: u32, enable: bool) -> Result<(), String> { 6 | let path = general::get_network_adapter_path_by_ifidx(interface_idx) 7 | .map_err(|e| format!("Failed to get network adapter path: {}", e))?; 8 | 9 | let result = general::change_interface_state(path, enable); 10 | match result { 11 | Ok(_) => Ok(()), 12 | Err(e) => { 13 | error!("Failed to change interface state: {:?}", e); 14 | Err(format!("Failed to change interface state: {}", e)) 15 | } 16 | } 17 | } 18 | 19 | #[tauri::command] 20 | pub fn get_best_interface() { 21 | let interface = general::get_best_interface_idx(); 22 | let interface_idx = interface.unwrap_or(0); 23 | if interface_idx != 0 { 24 | let interface = general::get_interface_by_index(interface_idx); 25 | dbg!(&interface); 26 | } 27 | } 28 | 29 | #[tauri::command] 30 | pub fn get_interfaces() -> Vec { 31 | let interfaces = general::get_all_interfaces(); 32 | match interfaces { 33 | Ok(interfaces) => interfaces, 34 | Err(e) => { 35 | dbg!(&e); 36 | vec![] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@heroui/theme"; 2 | 3 | interface ToggleButtonProps { 4 | isActive: boolean; 5 | isLoading?: boolean; 6 | onClick: () => void; 7 | } 8 | 9 | const ToggleButton = ({ 10 | isActive, 11 | isLoading = false, 12 | onClick, 13 | }: ToggleButtonProps) => { 14 | return ( 15 |
24 |
30 | {isLoading && ( 31 |
32 | )} 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default ToggleButton; 39 | -------------------------------------------------------------------------------- /src/components/icons/DNSServer.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function DNSServer(props: SVGProps) { 4 | return ( 5 | 12 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/InterfaceIp.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from "@heroui/tooltip"; 2 | import React from "react"; 3 | import { Copy } from "./icons/Copy"; 4 | import { addToast } from "@heroui/toast"; 5 | 6 | enum IpVersion { 7 | IPv4 = "IPv4", 8 | IPv6 = "IPv6", 9 | } 10 | const InterfaceIp = ({ ip }: { ip: string }) => { 11 | console.log(ip); 12 | const ipVersion = ip.includes(":") ? IpVersion.IPv6 : IpVersion.IPv4; 13 | 14 | const formattedIp = 15 | ipVersion === IpVersion.IPv4 16 | ? ip 17 | : ip.slice(0, ip.indexOf(":")) + 18 | ":...:" + 19 | ip.slice(ip.lastIndexOf(":") + 1); 20 | 21 | const handleCopy = () => { 22 | navigator.clipboard.writeText(ip); 23 | addToast({ 24 | title: "Copied to clipboard", 25 | color: "success", 26 | }); 27 | }; 28 | return ( 29 | 33 | {ip}{" "} 34 | 35 | 36 | } 37 | > 38 |
39 | {formattedIp} 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default InterfaceIp; 46 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Better DNS Jumper", 4 | "version": "0.3.0", 5 | "identifier": "ir.betterdnsjumper.app", 6 | "build": { 7 | "beforeDevCommand": "bun run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "bun run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Better DNS Jumper", 16 | "width": 700, 17 | "height": 450, 18 | "shadow": false, 19 | "resizable": false, 20 | "backgroundColor": "#09090b" 21 | } 22 | ], 23 | "security": { 24 | "csp": null 25 | } 26 | }, 27 | "bundle": { 28 | "active": true, 29 | "targets": "all", 30 | "createUpdaterArtifacts": true, 31 | "icon": [ 32 | "icons/32x32.png", 33 | "icons/128x128.png", 34 | "icons/128x128@2x.png", 35 | "icons/icon.icns", 36 | "icons/icon.ico" 37 | ] 38 | }, 39 | "plugins": { 40 | "updater": { 41 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc1MjJBQjRBMUYxREZCMjAKUldRZyt4MGZTcXNpZFFZdDY3cy9vcWcvNDhDdmtkMVJMbUhZclB6dmpkUnBGUzBkTWZBVlVIMUwK", 42 | "endpoints": [ 43 | "https://github.com/Ho3einWave/better-dns-jumper/releases/latest/download/latest.json" 44 | ] 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/components/icons/Broom.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Broom(props: SVGProps) { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/stores/useServersStore.ts: -------------------------------------------------------------------------------- 1 | // store/useServerStore.ts 2 | import { create } from "zustand"; 3 | import { loadServers, persistServers, resetServers } from "./tauriServersStore"; 4 | import type { SERVER } from "../types"; 5 | 6 | type ServerState = { 7 | servers: SERVER[]; 8 | isLoading: boolean; 9 | load: () => Promise; 10 | addServer: (server: SERVER) => Promise; 11 | updateServer: (server: SERVER) => Promise; 12 | removeServer: (key: string) => Promise; 13 | resetServers: () => Promise; 14 | }; 15 | 16 | export const useServerStore = create((set, get) => ({ 17 | servers: [], 18 | isLoading: true, 19 | 20 | load: async () => { 21 | const data = await loadServers(); 22 | set({ servers: data, isLoading: false }); 23 | }, 24 | 25 | addServer: async (server) => { 26 | const servers = [...get().servers, server]; 27 | set({ servers }); 28 | await persistServers(servers); 29 | }, 30 | 31 | updateServer: async (server) => { 32 | const servers = get().servers.map((s) => 33 | s.key === server.key ? server : s 34 | ); 35 | set({ servers }); 36 | await persistServers(servers); 37 | }, 38 | 39 | removeServer: async (key) => { 40 | const servers = get().servers.filter((s) => s.key !== key); 41 | set({ servers }); 42 | await persistServers(servers); 43 | }, 44 | 45 | resetServers: async () => { 46 | await resetServers(); 47 | set({ servers: await loadServers() }); 48 | }, 49 | })); 50 | -------------------------------------------------------------------------------- /src/components/Titlebar.tsx: -------------------------------------------------------------------------------- 1 | import { Minimize } from "./icons/Minimize"; 2 | import { Close } from "./icons/Close"; 3 | import { getCurrentWindow } from "@tauri-apps/api/window"; 4 | 5 | const Titlebar = () => { 6 | const handleMinimize = () => { 7 | getCurrentWindow().minimize(); 8 | }; 9 | const handleClose = () => { 10 | getCurrentWindow().close(); 11 | }; 12 | return ( 13 |
17 |

18 | 22 | Better 23 | {" "} 24 | DNS Jumper 25 |

26 |
27 | 33 | 34 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default Titlebar; 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Triggers on tags like v1.0.0, v2.1.3, etc. 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | runs-on: windows-latest 13 | 14 | env: 15 | TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} 16 | TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Bun 23 | uses: oven-sh/setup-bun@v2 24 | with: 25 | bun-version: latest 26 | 27 | - name: Install Rust stable 28 | uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Rust cache 31 | uses: swatinem/rust-cache@v2 32 | with: 33 | workspaces: "./src-tauri -> target" 34 | 35 | - name: Install frontend dependencies 36 | run: bun install 37 | 38 | - name: Build and release 39 | uses: tauri-apps/tauri-action@v0 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | tagName: ${{ github.ref_name }} 44 | releaseName: "Better DNS Jumper ${{ github.ref_name }}" 45 | releaseBody: "See the assets below to download this version." 46 | releaseDraft: false 47 | prerelease: false 48 | uploadPlainBinary: true 49 | -------------------------------------------------------------------------------- /src/components/icons/Ethernet.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Ethernet(props: SVGProps) { 4 | return ( 5 | 12 | 13 | 14 | 15 | 25 | 31 | 35 | 36 | 37 | 38 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-dns-jumper", 3 | "private": true, 4 | "version": "0.3.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@heroui/button": "^2.2.27", 14 | "@heroui/chip": "^2.2.22", 15 | "@heroui/input": "^2.4.28", 16 | "@heroui/modal": "^2.2.24", 17 | "@heroui/select": "^2.4.28", 18 | "@heroui/switch": "^2.2.24", 19 | "@heroui/system": "^2.4.23", 20 | "@heroui/tabs": "^2.2.24", 21 | "@heroui/theme": "^2.4.23", 22 | "@heroui/toast": "^2.0.17", 23 | "@heroui/tooltip": "^2.2.24", 24 | "@tailwindcss/vite": "^4.1.14", 25 | "@tanstack/react-query": "^5.90.2", 26 | "@tauri-apps/api": "^2", 27 | "@tauri-apps/plugin-autostart": "~2", 28 | "@tauri-apps/plugin-log": "~2", 29 | "@tauri-apps/plugin-opener": "^2", 30 | "@tauri-apps/plugin-store": "~2", 31 | "@tauri-apps/plugin-updater": "~2", 32 | "@tauri-apps/plugin-window-state": "~2", 33 | "framer-motion": "^12.23.22", 34 | "react": "^19.1.0", 35 | "react-dom": "^19.1.0", 36 | "react-router": "^7.9.4", 37 | "tailwindcss": "^4.1.14", 38 | "usehooks-ts": "^3.1.1", 39 | "zustand": "^5.0.8" 40 | }, 41 | "devDependencies": { 42 | "@types/react": "^19.1.8", 43 | "@types/react-dom": "^19.1.6", 44 | "@vitejs/plugin-react": "^4.6.0", 45 | "typescript": "~5.8.3", 46 | "vite": "^7.0.4", 47 | "@tauri-apps/cli": "^2" 48 | }, 49 | "trustedDependencies": [ 50 | "@heroui/shared-utils", 51 | "@tailwindcss/oxide" 52 | ] 53 | } -------------------------------------------------------------------------------- /src/stores/tauriSettingStore.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 | import { load } from "@tauri-apps/plugin-store"; 3 | 4 | let storePromise: ReturnType | null = null; 5 | 6 | async function getStore() { 7 | if (!storePromise) { 8 | storePromise = load("setting.json", { 9 | autoSave: true, 10 | defaults: { test_domain: "youtube.com" }, 11 | }); 12 | } 13 | return storePromise; 14 | } 15 | 16 | export async function loadTestDomain() { 17 | const store = await getStore(); 18 | const testDomain = await store.get("test_domain"); 19 | if (!testDomain) { 20 | await store.set("test_domain", "youtube.com"); 21 | await store.save(); 22 | return "youtube.com"; 23 | } 24 | return testDomain; 25 | } 26 | 27 | export async function saveTestDomain(testDomain: string) { 28 | const store = await getStore(); 29 | await store.set("test_domain", testDomain); 30 | await store.save(); 31 | } 32 | 33 | export const useTestDomain = () => { 34 | const queryClient = useQueryClient(); 35 | const { data: testDomain, isLoading: isLoadingTestDomain } = useQuery({ 36 | queryKey: ["test_domain"], 37 | queryFn: loadTestDomain, 38 | }); 39 | 40 | const { mutate: saveTestDomainMutation, isPending: isSavingTestDomain } = 41 | useMutation({ 42 | mutationFn: (testDomain: string) => saveTestDomain(testDomain), 43 | onSuccess: () => { 44 | queryClient.invalidateQueries({ queryKey: ["test_domain"] }); 45 | }, 46 | }); 47 | 48 | return { 49 | data: testDomain, 50 | isLoading: isLoadingTestDomain, 51 | mutate: saveTestDomainMutation, 52 | isSaving: isSavingTestDomain, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "better-dns-jumper" 3 | version = "0.3.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "better_dns_jumper_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | log = "0.4.28" 22 | env_logger = "0.11.8" 23 | tauri = { version = "2", features = [] } 24 | tauri-plugin-opener = "2" 25 | serde = { version = "1", features = ["derive"] } 26 | serde_json = "1" 27 | winapi = { version = "0.3.9", features = ["iphlpapi", "winerror"] } 28 | network-interface = "2.0.3" 29 | wmi = "0.17.3" 30 | tokio = { version = "1", features = ["full"] } 31 | async-trait = "0.1" 32 | clap = { version = "4", features = ["derive"] } 33 | anyhow = "1" 34 | hickory-server = { version = "0.25.2", features = ["resolver"] } 35 | hickory-client = "0.25.2" 36 | hickory-resolver = { version = "*", features = [ 37 | "https-aws-lc-rs", 38 | "webpki-roots", 39 | ] } 40 | hickory-proto = { version = "*", features = [ 41 | "https-aws-lc-rs", 42 | "webpki-roots", 43 | ] } 44 | rustls-native-certs = "0.8.2" 45 | rustls = "0.23.32" 46 | url = "2.5.7" 47 | tokio-util = "0.7.15" 48 | tauri-plugin-store = "2" 49 | futures = "0.3.31" 50 | tauri-plugin-prevent-default = "4" 51 | tauri-plugin-log = "2" 52 | 53 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 54 | tauri-plugin-autostart = "2" 55 | tauri-plugin-single-instance = "2" 56 | tauri-plugin-updater = "2" 57 | tauri-plugin-window-state = "2" 58 | -------------------------------------------------------------------------------- /src/components/ConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalContent, 4 | ModalHeader, 5 | ModalBody, 6 | ModalFooter, 7 | } from "@heroui/modal"; 8 | import { Button } from "@heroui/button"; 9 | 10 | interface ConfirmModalProps { 11 | isOpen: boolean; 12 | onClose: () => void; 13 | onConfirm: () => void; 14 | title: string; 15 | message: string; 16 | confirmText?: string; 17 | cancelText?: string; 18 | confirmColor?: 19 | | "default" 20 | | "primary" 21 | | "secondary" 22 | | "success" 23 | | "warning" 24 | | "danger"; 25 | } 26 | 27 | const ConfirmModal = ({ 28 | isOpen, 29 | onClose, 30 | onConfirm, 31 | title, 32 | message, 33 | confirmText = "Confirm", 34 | cancelText = "Cancel", 35 | confirmColor = "danger", 36 | }: ConfirmModalProps) => { 37 | return ( 38 | 50 | 51 | {title} 52 | 53 |

{message}

54 |
55 | 56 | 59 | 62 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default ConfirmModal; 69 | -------------------------------------------------------------------------------- /src/screens/Setting.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "@heroui/switch"; 2 | import { useAutoStartStore } from "../stores/useAutoStartStore"; 3 | import { useEffect } from "react"; 4 | import TestDomain from "../components/Setting/TestDomain"; 5 | 6 | const Setting = () => { 7 | const { isAutoStartEnabled, isLoading, setIsAutoStartEnabled, load } = 8 | useAutoStartStore(); 9 | 10 | useEffect(() => { 11 | load(); 12 | }, []); 13 | 14 | const handleToggleAutoStart = async () => { 15 | await setIsAutoStartEnabled(!isAutoStartEnabled); 16 | }; 17 | 18 | return ( 19 |
20 |
21 |
22 |
23 | Setting 24 |
25 |
26 | 27 |
28 |
29 |
30 | Start on system launch 31 |

32 | Automatically open Better DNS Jumper when you 33 | log in to your computer. 34 |

35 |
36 | 41 |
42 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Setting; 50 | -------------------------------------------------------------------------------- /src/utils/interface.tsx: -------------------------------------------------------------------------------- 1 | import { Bluetooth } from "../components/icons/Bluetooth"; 2 | import { Ethernet } from "../components/icons/Ethernet"; 3 | import { Network } from "../components/icons/Network"; 4 | import { VirtualMachine } from "../components/icons/VirtualMachine"; 5 | import { VPN } from "../components/icons/VPN"; 6 | import { Wifi } from "../components/icons/Wifi"; 7 | 8 | export const getInterfaceIcon = (description: string) => { 9 | if (description.toLowerCase().includes("bluetooth")) { 10 | return ; 11 | } else if (description.toLowerCase().includes("virtual")) { 12 | return ; 13 | } else if ( 14 | description.toLowerCase().includes("wifi") || 15 | description.toLowerCase().includes("wireless") || 16 | description.toLowerCase().includes("wi-fi") 17 | ) { 18 | return ; 19 | } else if ( 20 | description.toLowerCase().includes("vpn") || 21 | description.toLowerCase().includes("tap-windows") 22 | ) { 23 | return ; 24 | } else if (description.toLowerCase().includes("ethernet")) { 25 | return ; 26 | } 27 | return ; 28 | }; 29 | 30 | export enum InterfaceType { 31 | Bluetooth = "bluetooth", 32 | Virtual = "virtual", 33 | Wifi = "wifi", 34 | Vpn = "vpn", 35 | Ethernet = "ethernet", 36 | } 37 | export const getInterfaceType = (description: string) => { 38 | if (description.toLowerCase().includes("bluetooth")) { 39 | return InterfaceType.Bluetooth; 40 | } else if (description.toLowerCase().includes("virtual")) { 41 | return InterfaceType.Virtual; 42 | } else if ( 43 | description.toLowerCase().includes("wifi") || 44 | description.toLowerCase().includes("wireless") || 45 | description.toLowerCase().includes("wi-fi") 46 | ) { 47 | return InterfaceType.Wifi; 48 | } else if ( 49 | description.toLowerCase().includes("vpn") || 50 | description.toLowerCase().includes("tap-windows") 51 | ) { 52 | return InterfaceType.Vpn; 53 | } else if (description.toLowerCase().includes("ethernet")) { 54 | return InterfaceType.Ethernet; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/Setting/TestDomain.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@heroui/input"; 2 | import { useEffect, useState } from "react"; 3 | import { useTestDomain } from "../../stores/tauriSettingStore"; 4 | import { Button } from "@heroui/button"; 5 | import { Save } from "../icons/Save"; 6 | 7 | const TestDomain = () => { 8 | const [testDomainValue, setTestDomainValue] = useState(""); 9 | 10 | const { 11 | data: testDomain, 12 | isLoading: isLoadingTestDomain, 13 | mutate: saveTestDomainMutation, 14 | isSaving: isSavingTestDomain, 15 | } = useTestDomain(); 16 | 17 | useEffect(() => { 18 | if (testDomain) { 19 | setTestDomainValue(testDomain); 20 | } 21 | }, [testDomain]); 22 | 23 | const handleSaveTestDomain = () => { 24 | saveTestDomainMutation(testDomainValue); 25 | }; 26 | 27 | return ( 28 |
29 |
30 | Test Domain 31 |

32 | This will be used to test the DNS server. 33 |

34 |
35 |
36 | setTestDomainValue(e.target.value)} 41 | placeholder="youtube.com" 42 | className="max-w-60" 43 | /> 44 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default TestDomain; 62 | -------------------------------------------------------------------------------- /src/hooks/useInterfaces.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, UseQueryOptions } from "@tanstack/react-query"; 2 | import { invoke } from "@tauri-apps/api/core"; 3 | 4 | export const useInterfaces = ( 5 | options?: Omit< 6 | UseQueryOptions, 7 | "queryKey" | "queryFn" 8 | > 9 | ) => { 10 | return useQuery({ 11 | queryKey: ["interfaces"], 12 | queryFn: () => invoke("get_interfaces"), 13 | ...options, 14 | }); 15 | }; 16 | 17 | export const useBestInterface = () => { 18 | return useQuery({ 19 | queryKey: ["best_interface"], 20 | queryFn: () => invoke("get_best_interface"), 21 | }); 22 | }; 23 | 24 | export const useChangeInterfaceState = () => { 25 | return useMutation({ 26 | mutationFn: (params: { interface_idx: number; enable: boolean }) => 27 | invoke("change_interface_state", { 28 | interface_idx: params.interface_idx, 29 | enable: params.enable, 30 | }), 31 | }); 32 | }; 33 | 34 | type Interface = { 35 | adapter: { 36 | description: string | null; 37 | device_id: number; 38 | guid: string | null; 39 | index: number; 40 | interface_index: number; 41 | mac_address: string | null; 42 | manufacturer: string | null; 43 | name: string | null; 44 | net_connection_id: string | null; 45 | net_enabled: boolean; 46 | config_manager_error_code: number | null; 47 | service_name: string | null; 48 | path: string | null; 49 | }; 50 | config: { 51 | default_ip_gateway: string[] | null; 52 | description: string | null; 53 | dhcp_enabled: boolean; 54 | dhcp_server: string | null; 55 | dns_host_name: string | null; 56 | dns_server_search_order: string[] | null; 57 | index: number; 58 | interface_index: number; 59 | ip_address: string[] | null; 60 | ip_connection_metric: number | null; 61 | ip_enabled: boolean; 62 | ip_subnet: string[] | null; 63 | mac_address: string | null; 64 | path: string | null; 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/data/defaultServers.ts: -------------------------------------------------------------------------------- 1 | import { SERVER } from "../types"; 2 | 3 | export const DEFAULT_SERVERS: SERVER[] = [ 4 | { 5 | type: "doh", 6 | key: "GOOGLE_DOH", 7 | name: "Google DoH", 8 | servers: ["https://dns.google/dns-query"], 9 | tags: ["Web"], 10 | }, 11 | { 12 | type: "doh", 13 | key: "CLOUDFLARE_DOH", 14 | name: "Cloudflare DoH", 15 | servers: ["https://cloudflare-dns.com/dns-query"], 16 | tags: ["Web"], 17 | }, 18 | { 19 | type: "doh", 20 | key: "QUAD9_DOH", 21 | name: "Quad9 DoH", 22 | servers: ["https://dns.quad9.net/dns-query"], 23 | tags: ["Security", "Privacy"], 24 | }, 25 | { 26 | type: "doh", 27 | key: "ADGUARD_DOH", 28 | name: "AdGuard DoH", 29 | servers: ["https://dns.adguard.com/dns-query"], 30 | tags: ["AdBlocking", "Privacy"], 31 | }, 32 | { 33 | type: "doh", 34 | key: "DYNX_ADBLOCK", 35 | name: "DynX AdBlock DoH", 36 | servers: ["https://dns.dynx.pro/dns-query"], 37 | tags: ["AdBlocking", "Privacy"], 38 | }, 39 | { 40 | type: "doh", 41 | key: "DYNX_ANTI_BAN", 42 | name: "DynX AntiBan DoH", 43 | servers: ["https://anti-ban.dynx.pro/dns-query"], 44 | tags: ["Bypass", "AntiBan", "Gaming"], 45 | }, 46 | { 47 | type: "dns", 48 | key: "GOOGLE", 49 | name: "Google DNS", 50 | servers: ["8.8.8.8", "8.8.4.4"], 51 | tags: ["General", "Web"], 52 | }, 53 | { 54 | type: "dns", 55 | key: "CLOUDFLARE", 56 | name: "Cloudflare DNS", 57 | servers: ["1.1.1.1", "1.0.0.1"], 58 | tags: ["General", "Web"], 59 | }, 60 | { 61 | type: "dns", 62 | key: "DYNX_ANTI_BAN", 63 | name: "DynX AntiBan DNS", 64 | servers: ["10.70.95.150", "10.70.95.162"], 65 | tags: ["Bypass", "AntiBan", "Gaming"], 66 | }, 67 | { 68 | type: "dns", 69 | key: "SHECAN", 70 | name: "Shecan DNS", 71 | servers: ["178.22.122.100", "185.51.200.2"], 72 | tags: ["Iran", "Gaming", "Web", "Ai"], 73 | }, 74 | { 75 | type: "dns", 76 | key: "ADGUARD", 77 | name: "AdGuard", 78 | servers: ["94.140.14.14", "94.140.15.15"], 79 | tags: ["Web", "Ad Blocker"], 80 | }, 81 | { 82 | type: "dns", 83 | key: "YANDEX", 84 | name: "Yandex DNS", 85 | servers: ["77.88.8.8", "77.88.8.1"], 86 | tags: ["Web"], 87 | }, 88 | ]; 89 | -------------------------------------------------------------------------------- /src/constants/dns-servers.ts: -------------------------------------------------------------------------------- 1 | import { SERVER } from "../types"; 2 | 3 | export const DNS_SERVERS: SERVER[] = [ 4 | { 5 | type: "dns", 6 | key: "GOOGLE", 7 | name: "Google DNS", 8 | servers: ["8.8.8.8", "8.8.4.4"], 9 | tags: ["General", "Web"], 10 | }, 11 | { 12 | type: "dns", 13 | key: "CLOUDFLARE", 14 | name: "Cloudflare DNS", 15 | servers: ["1.1.1.1", "1.0.0.1"], 16 | tags: ["General", "Web"], 17 | }, 18 | { 19 | type: "dns", 20 | key: "SHECAN", 21 | name: "Shecan DNS", 22 | servers: ["178.22.122.100", "185.51.200.2"], 23 | tags: ["Iran", "Gaming", "Web", "Ai"], 24 | }, 25 | { 26 | type: "dns", 27 | key: "US_DYN", 28 | name: "DynX AdBlocker", 29 | servers: ["216.146.35.35", "216.146.36.36"], 30 | tags: ["Web", "Ad Blocker", "Gaming"], 31 | }, 32 | { 33 | type: "dns", 34 | key: " DYNX_IRAN_ANTI_SANCTIONS", 35 | name: "DynX Iran Anti Sanctions", 36 | servers: ["10.70.95.150", "10.70.95.162"], 37 | tags: ["Bypass", "Ad Blocker", "Gaming"], 38 | }, 39 | { 40 | type: "dns", 41 | key: "ADGUARD", 42 | name: "AdGuard", 43 | servers: ["94.140.14.14", "94.140.15.15"], 44 | tags: ["Web", "Ad Blocker"], 45 | }, 46 | { 47 | type: "dns", 48 | key: "YANDEX", 49 | name: "Yandex DNS", 50 | servers: ["77.88.8.8", "77.88.8.1"], 51 | tags: ["Web"], 52 | }, 53 | ]; 54 | 55 | export const DOH_SERVERS: SERVER[] = [ 56 | { 57 | type: "doh", 58 | key: "DYNX", 59 | name: "DynX DoH", 60 | servers: ["https://dns.dynx.pro/dns-query"], 61 | tags: ["Web"], 62 | }, 63 | { 64 | type: "doh", 65 | key: "GOOGLE", 66 | name: "Google DoH", 67 | servers: ["https://dns.google/dns-query"], 68 | tags: ["Web"], 69 | }, 70 | { 71 | type: "doh", 72 | key: "CLOUDFLARE", 73 | name: "Cloudflare DoH", 74 | servers: ["https://cloudflare-dns.com/dns-query"], 75 | tags: ["Web"], 76 | }, 77 | { 78 | type: "doh", 79 | key: "HW_CFW", 80 | name: "HW CFW DoH", 81 | servers: ["https://doh.hoseinwave.ir/dns-query"], 82 | tags: ["Web"], 83 | }, 84 | { 85 | type: "doh", 86 | key: "HW_CFW_RAW_DOMAIN", 87 | name: "HW CFW DoH RAW DOMAIN", 88 | servers: ["https://doh-cf-workers.ho3einwave.workers.dev/dns-query"], 89 | tags: ["Web"], 90 | }, 91 | ]; 92 | -------------------------------------------------------------------------------- /src/hooks/useDns.ts: -------------------------------------------------------------------------------- 1 | import { MutationOptions, useMutation, useQuery } from "@tanstack/react-query"; 2 | import { invoke } from "@tauri-apps/api/core"; 3 | import { loadTestDomain } from "../stores/tauriSettingStore"; 4 | import { DEFAULT_SETTING } from "../data/defaultSetting"; 5 | 6 | export const useSetDns = ( 7 | params?: MutationOptions< 8 | void, 9 | Error, 10 | { path: string; dns_servers: string[]; dns_type: "doh" | "dns" } 11 | > 12 | ) => { 13 | return useMutation({ 14 | mutationFn: (params: { 15 | path: string; 16 | dns_servers: string[]; 17 | dns_type: "doh" | "dns"; 18 | }) => { 19 | return invoke("set_dns", params); 20 | }, 21 | ...params, 22 | }); 23 | }; 24 | 25 | export const useClearDns = ( 26 | params?: MutationOptions 27 | ) => { 28 | return useMutation({ 29 | mutationFn: (params: { path: string }) => { 30 | return invoke("clear_dns", params); 31 | }, 32 | 33 | ...params, 34 | }); 35 | }; 36 | 37 | export const useGetInterfaceDnsInfo = (interface_idx: number | null) => { 38 | return useQuery({ 39 | queryKey: ["interface_info", interface_idx], 40 | queryFn: () => { 41 | return invoke("get_interface_dns_info", { 42 | interface_idx, 43 | }); 44 | }, 45 | refetchInterval: 10000, 46 | enabled: interface_idx !== null, 47 | }); 48 | }; 49 | 50 | export const useClearDnsCache = ( 51 | params?: MutationOptions 52 | ) => { 53 | return useMutation({ 54 | mutationFn: () => { 55 | return invoke("clear_dns_cache"); 56 | }, 57 | ...params, 58 | }); 59 | }; 60 | 61 | export const useTestDohServer = ( 62 | params?: MutationOptions< 63 | DoHTestResult, 64 | Error, 65 | { server: string; domain: string } 66 | > 67 | ) => { 68 | return useMutation({ 69 | mutationFn: async (params: { server: string; domain: string }) => { 70 | const testDomain = await loadTestDomain(); 71 | return invoke("test_doh_server", { 72 | ...params, 73 | domain: testDomain || DEFAULT_SETTING.test_domain, 74 | }); 75 | }, 76 | ...params, 77 | }); 78 | }; 79 | 80 | export type DoHTestResult = { 81 | success: boolean; 82 | latency: number; 83 | error: string | null; 84 | }; 85 | 86 | export type InterfaceDnsInfo = { 87 | interface_index: number; 88 | dns_servers: string[]; 89 | interface_name: string; 90 | path: string | null; 91 | }; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/ho3einwave/better-dns-jumper/total?style=for-the-badge&color=blue) 2 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ho3einwave/better-dns-jumper/release.yml?style=for-the-badge) 3 | ![GitHub package.json dynamic](https://img.shields.io/github/package-json/version/ho3einwave/better-dns-jumper?style=for-the-badge) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/ho3einwave/better-dns-jumper?style=for-the-badge&logo=github&color=%23f7d000) 5 | 6 | 7 | # Better DNS Jumper 8 | 9 | A fast, modern DNS manager built with **Tauri (Rust + React)**. Switch DNS servers, manage network interfaces, and use DNS-over-HTTPS (DoH) through a clean, lightweight interface. 10 | 11 | ## Installation 12 | 13 | ### Download 14 | 15 | Grab the latest version from the **[Releases](https://github.com/Ho3einWave/better-dns-jumper/releases)** page. 16 | 17 | 18 | ## Related Projects 19 | 20 | ### [cf-doh-worker](https://github.com/Ho3einWave/cf-doh-worker) 21 | 22 | If you are looking for a **private, custom DNS-over-HTTPS (DoH) endpoint** to use with **Better DNS Jumper**'s DoH feature, check out the `cf-doh-worker` repository. 23 | 24 | This project is a very minimalist DoH proxy designed to run on **Cloudflare Workers**. It allows you to: 25 | * Quickly deploy your own private, highly-available DoH server. 26 | * Use a DoH endpoint under your own domain to potentially bypass restrictions on known public DoH providers. 27 | 28 | You can then configure the URL of your deployed Cloudflare Worker as a custom DoH server within the **Better DNS Jumper** application. 29 | 30 | 31 | ## Features 32 | 33 | * **DNS Protocols** 34 | 35 | * Traditional DNS (IPv4) 36 | * DNS-over-HTTPS with local proxy 37 | 38 | * **Network Management** 39 | 40 | * View and select interfaces 41 | * Auto-detect best interface 42 | * Set / clear DNS per interface 43 | 44 | * **DNS Servers** 45 | 46 | * Built-in popular servers (Google, Cloudflare, Quad9, AdGuard…) 47 | * Custom server support 48 | * DoH latency/availability testing 49 | 50 | * **Tools** 51 | 52 | * Clear DNS cache 53 | * Reset DNS settings 54 | * Auto-start 55 | * Auto-update 56 | 57 | * **UI** 58 | 59 | * Modern dark UI (React + HeroUI) 60 | * Smooth animations (Framer Motion) 61 | 62 | 63 | ### Build from Source 64 | 65 | ```bash 66 | git clone https://github.com/Ho3einWave/better-dns-jumper.git 67 | cd better-dns-jumper 68 | npm install # or bun install 69 | npm run tauri dev 70 | npm run tauri build 71 | ``` 72 | 73 | ## Usage 74 | 75 | 1. Launch the app (admin required) 76 | 2. Select a network interface (or use Auto) 77 | 3. Choose protocol: **DNS** or **DoH** 78 | 4. Pick a server 79 | 5. Toggle **Activate** to apply 80 | 6. Optional tools: clear cache, reset DNS, test DoH 81 | 82 | ## Technical Overview 83 | 84 | * **Frontend**: React + TypeScript + Tailwind + HeroUI 85 | * **Backend**: Rust (Tauri 2) 86 | * **DNS Engine**: Hickory DNS 87 | * **Windows Integration**: IP Helper API + WMI 88 | * **DoH Mode**: Runs a local DNS proxy (`127.0.0.2`) that forwards queries to the selected DoH server 89 | 90 | Project structure: 91 | 92 | ``` 93 | src/ # React frontend 94 | src-tauri/ # Rust backend 95 | ``` 96 | 97 | ## Roadmap 98 | 99 | - [ ] Improved error handling 100 | - [x] Clean exit & automatic DNS restore 101 | - [ ] Better logs & in-app log viewer 102 | - [ ] DNS-over-TLS / DNS-over-QUIC / DoH3 103 | - [ ] Reduce WMI usage 104 | - [ ] CLI support 105 | - [ ] Multi-language support 106 | - [ ] Syncable DNS profiles 107 | 108 | ## Contributing 109 | 110 | PRs are welcome. For major changes, open an issue first. 111 | 112 | ## License 113 | 114 | GPLv3 — see `LICENSE`. 115 | 116 | -------------------------------------------------------------------------------- /src/hooks/useUpdater.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { check, Update } from "@tauri-apps/plugin-updater"; 3 | 4 | interface UpdaterStore { 5 | isCheckingForUpdates: boolean; 6 | isDownloading: boolean; 7 | isReadyForInstall: boolean; 8 | isInstalling: boolean; 9 | isUpdateAvailable: boolean; 10 | isModalOpen: boolean; 11 | update: Update | null; 12 | downloaded: number; 13 | contentLength: number; 14 | checkForUpdates: () => Promise; 15 | downloadUpdate: () => Promise; 16 | installUpdate: () => Promise; 17 | downloadAndInstallUpdate: () => Promise; 18 | openModal: () => void; 19 | closeModal: () => void; 20 | } 21 | 22 | export const useUpdater = create((set, get) => ({ 23 | isCheckingForUpdates: false, 24 | isDownloading: false, 25 | isReadyForInstall: false, 26 | isInstalling: false, 27 | isUpdateAvailable: false, 28 | isModalOpen: false, 29 | downloaded: 0, 30 | contentLength: 0, 31 | update: null, 32 | checkForUpdates: async () => { 33 | set({ isCheckingForUpdates: true }); 34 | const result = await check(); 35 | if (result) { 36 | set({ 37 | isUpdateAvailable: true, 38 | update: result, 39 | isCheckingForUpdates: false, 40 | }); 41 | } else { 42 | set({ isCheckingForUpdates: false, isUpdateAvailable: false }); 43 | } 44 | }, 45 | downloadUpdate: async () => { 46 | const { update } = get(); 47 | if (!update) return; 48 | await update.download((event) => { 49 | switch (event.event) { 50 | case "Started": 51 | set({ 52 | isDownloading: true, 53 | downloaded: 0, 54 | contentLength: event.data.contentLength ?? 0, 55 | }); 56 | break; 57 | case "Progress": 58 | const downloaded = get().downloaded; 59 | set({ 60 | downloaded: downloaded + (event.data.chunkLength ?? 0), 61 | }); 62 | break; 63 | case "Finished": 64 | set({ isDownloading: false, isReadyForInstall: true }); 65 | break; 66 | } 67 | }); 68 | }, 69 | installUpdate: async () => { 70 | const { update, isReadyForInstall } = get(); 71 | if (!update || !isReadyForInstall) return; 72 | await update.install(); 73 | set({ isInstalling: true }); 74 | }, 75 | downloadAndInstallUpdate: async () => { 76 | const { update } = get(); 77 | if (!update) return; 78 | await update.downloadAndInstall((event) => { 79 | switch (event.event) { 80 | case "Started": 81 | set({ 82 | isDownloading: true, 83 | downloaded: 0, 84 | contentLength: event.data.contentLength ?? 0, 85 | }); 86 | break; 87 | case "Progress": 88 | const downloaded = get().downloaded; 89 | set({ 90 | downloaded: downloaded + (event.data.chunkLength ?? 0), 91 | }); 92 | break; 93 | case "Finished": 94 | set({ isDownloading: false, isInstalling: true }); 95 | break; 96 | } 97 | }); 98 | }, 99 | openModal: () => { 100 | set({ isModalOpen: true }); 101 | }, 102 | closeModal: () => { 103 | set({ isModalOpen: false }); 104 | }, 105 | })); 106 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/src/dns/dns_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::net_interfaces::general; 2 | use crate::utils::create_wmi_connection; 3 | use log::debug; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[link(name = "dnsapi")] 7 | extern "system" { 8 | fn DnsFlushResolverCache() -> i32; 9 | } 10 | 11 | pub fn get_interface_dns_info(interface_idx: u32) -> Result { 12 | let interface_info = general::get_interface_by_index(interface_idx); 13 | let wmi_con = create_wmi_connection().map_err(|e| format!("WMI connection failed: {}", e))?; 14 | 15 | let query = format!( 16 | "SELECT * FROM Win32_NetworkAdapterConfiguration WHERE InterfaceIndex = {}", 17 | interface_idx 18 | ); 19 | 20 | let result: Result, String> = wmi_con 21 | .raw_query(query) 22 | .map_err(|e| format!("WMI query failed: {}", e)); 23 | 24 | let interface_dns_info: Result = match result { 25 | Ok(result) => { 26 | if result.is_empty() { 27 | Err(format!("No interface found")) 28 | } else { 29 | let interface_info_wmi = result.first().cloned().unwrap_or_default(); 30 | let interface_info = match interface_info { 31 | Ok(interface_info) => interface_info, 32 | Err(e) => return Err(e), 33 | }; 34 | Ok(InterfaceDnsInfo { 35 | interface_index: interface_info_wmi.interface_index, 36 | dns_servers: interface_info_wmi.dns_server_search_order.clone(), 37 | interface_name: interface_info.adapter.name.unwrap_or_default(), 38 | path: interface_info_wmi.path, 39 | }) 40 | } 41 | } 42 | Err(e) => Err(e), 43 | }; 44 | 45 | return interface_dns_info; 46 | } 47 | 48 | // TODO: Change this method to use native windows api instead of wmi on windows 8+ or newer 49 | pub fn apply_dns_by_path(path: String, dns_servers: Vec) -> Result<(), String> { 50 | let wmi_con = create_wmi_connection().map_err(|e| format!("WMI connection failed: {}", e))?; 51 | 52 | let params = SetDNSServerParams { dns_servers }; 53 | 54 | let result: Result<(), wmi::WMIError> = wmi_con.exec_instance_method::( 55 | path, 56 | "SetDNSServerSearchOrder", 57 | params, 58 | ); 59 | 60 | match result { 61 | Ok(_) => Ok(()), 62 | Err(e) => Err(e.to_string()), 63 | } 64 | } 65 | 66 | pub fn clear_dns_by_path(path: String) -> Result<(), String> { 67 | let wmi_con = create_wmi_connection().map_err(|e| format!("WMI connection failed: {}", e))?; 68 | 69 | let params = SetDNSServerParams { 70 | dns_servers: vec![], 71 | }; 72 | 73 | let result: Result<(), wmi::WMIError> = wmi_con.exec_instance_method::( 74 | path, 75 | "SetDNSServerSearchOrder", 76 | params, 77 | ); 78 | 79 | match result { 80 | Ok(_) => Ok(()), 81 | Err(e) => Err(e.to_string()), 82 | } 83 | } 84 | 85 | pub fn clear_dns_cache() -> Result<(), String> { 86 | unsafe { 87 | let result = DnsFlushResolverCache(); 88 | 89 | debug!("Flushed DNS cache: {}", result); 90 | match result { 91 | 1 => Ok(()), 92 | _ => Err(format!("Failed to clear DNS cache")), 93 | } 94 | } 95 | } 96 | 97 | #[derive(Serialize, Debug)] 98 | struct SetDNSServerParams { 99 | #[serde(rename = "DNSServerSearchOrder")] 100 | dns_servers: Vec, 101 | } 102 | 103 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 104 | #[serde(rename = "Win32_NetworkAdapterConfiguration")] 105 | #[serde(rename_all = "PascalCase")] 106 | pub struct InterfaceInfoWmi { 107 | description: String, 108 | dns_server_search_order: Vec, 109 | interface_index: u32, 110 | #[serde(rename = "IPConnectionMetric")] 111 | ip_connection_metric: Option, 112 | #[serde(rename = "__Path")] 113 | pub path: Option, 114 | } 115 | 116 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 117 | pub struct InterfaceDnsInfo { 118 | interface_index: u32, 119 | dns_servers: Vec, 120 | interface_name: String, 121 | pub path: Option, 122 | } 123 | -------------------------------------------------------------------------------- /src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, Tab } from "@heroui/tabs"; 2 | import { Tooltip } from "@heroui/tooltip"; 3 | import { DNSServer } from "../components/icons/DNSServer"; 4 | import { Setting } from "../components/icons/Setting"; 5 | import { Key, useState, useEffect } from "react"; 6 | import { useNavigate, useLocation } from "react-router"; 7 | import { Lan } from "../components/icons/Lan"; 8 | import { Server } from "./icons/Server"; 9 | import { Update } from "./icons/Update"; 10 | import { useUpdater } from "../hooks/useUpdater"; 11 | 12 | const TABS = [ 13 | { 14 | key: "main", 15 | title: "DNS", 16 | icon: , 17 | path: "/", 18 | }, 19 | { 20 | key: "servers", 21 | title: "Servers", 22 | icon: , 23 | path: "/servers", 24 | }, 25 | { 26 | key: "network-interfaces", 27 | title: "Network Interfaces", 28 | icon: , 29 | path: "/network-interfaces", 30 | }, 31 | { 32 | key: "settings", 33 | title: "Settings", 34 | icon: , 35 | path: "/settings", 36 | }, 37 | ] as const; 38 | 39 | type TabKey = (typeof TABS)[number]["key"]; 40 | 41 | const Navigation = () => { 42 | const navigate = useNavigate(); 43 | const location = useLocation(); 44 | const { isUpdateAvailable, openModal } = useUpdater(); 45 | const [selectedKey, setSelectedKey] = useState("main"); 46 | 47 | // Update selected key based on current route 48 | useEffect(() => { 49 | const currentTab = TABS.find((tab) => tab.path === location.pathname); 50 | if (currentTab) { 51 | setSelectedKey(currentTab.key); 52 | } 53 | }, [location.pathname]); 54 | 55 | const handleTabChange = (key: Key) => { 56 | if (key === "update") { 57 | openModal(); 58 | return; 59 | } 60 | 61 | const tabKey = key as TabKey; 62 | const tab = TABS.find((t) => t.key === tabKey); 63 | if (tab) { 64 | setSelectedKey(tabKey); 65 | navigate(tab.path); 66 | } 67 | }; 68 | 69 | return ( 70 |
71 | 84 | {TABS.map((tab) => ( 85 | 93 | {tab.icon} 94 | 95 | } 96 | /> 97 | ))} 98 | {isUpdateAvailable && ( 99 | 107 |
108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 | } 116 | /> 117 | )} 118 |
119 |
120 | ); 121 | }; 122 | 123 | export default Navigation; 124 | -------------------------------------------------------------------------------- /src-tauri/src/commands/dns.rs: -------------------------------------------------------------------------------- 1 | use crate::dns::{dns_server, dns_utils}; 2 | use crate::net_interfaces::general; 3 | use crate::types::DoHTestResult; 4 | use crate::AppState; 5 | use log::{debug, error, info}; 6 | use tokio::sync::Mutex; 7 | use tokio::time::{self, Duration, Instant}; 8 | 9 | #[tauri::command(rename_all = "snake_case")] 10 | pub async fn test_doh_server(server: String, domain: String) -> Result { 11 | let server_url = 12 | url::Url::parse(&server).map_err(|e| format!("Failed to parse server: {}", e))?; 13 | 14 | let protocol = server_url.scheme(); 15 | if protocol != "https" { 16 | error!("Invalid protocol: {}", protocol); 17 | return Err(format!("Invalid protocol: {}", protocol)); 18 | } 19 | 20 | let resolver_domain = server_url.host().ok_or("Failed to get domain")?; 21 | let port = server_url.port().unwrap_or(443); 22 | let path = server_url.path(); 23 | let resolver = dns_server::DnsServer::create_dns_resolver( 24 | resolver_domain.to_string(), 25 | port, 26 | Some(path.to_string()), 27 | ); 28 | 29 | let resolver = match resolver { 30 | Ok(resolver) => resolver, 31 | Err(e) => { 32 | error!("Failed to create DNS resolver: {:?}", e); 33 | return Err(format!("Failed to create DNS resolver: {:?}", e)); 34 | } 35 | }; 36 | let timeout = Duration::from_secs(3); 37 | 38 | let start = Instant::now(); 39 | let result = time::timeout(timeout, resolver.lookup_ip(domain.to_string())).await; 40 | let elapsed = start.elapsed(); 41 | 42 | match result { 43 | Ok(Ok(lookup)) => { 44 | info!( 45 | "DNS lookup succeeded for {} via {} in {:?}", 46 | domain, server, elapsed 47 | ); 48 | lookup.iter().for_each(|item| { 49 | dbg!(&item); 50 | }); 51 | Ok(DoHTestResult { 52 | success: true, 53 | latency: elapsed.as_millis() as usize, 54 | error: None, 55 | }) 56 | } 57 | Ok(Err(e)) => { 58 | error!( 59 | "DNS lookup failed for {} via {} after {:?}: {}", 60 | domain, server, elapsed, e 61 | ); 62 | Err(format!("DNS lookup failed: {}", e)) 63 | } 64 | Err(_) => { 65 | error!( 66 | "DNS lookup timed out for {} via {} after {:?}", 67 | domain, server, elapsed 68 | ); 69 | Err(format!("DNS lookup timed out after {:?}", elapsed)) 70 | } 71 | } 72 | } 73 | 74 | #[tauri::command(rename_all = "snake_case")] 75 | pub fn get_interface_dns_info(interface_idx: u32) -> Result { 76 | let interface_idx = match interface_idx { 77 | 0 => general::get_best_interface_idx() 78 | .map_err(|e| format!("Failed to get best interface index: {}", e))?, 79 | _ => interface_idx, 80 | }; 81 | let result = dns_utils::get_interface_dns_info(interface_idx); 82 | return result; 83 | } 84 | 85 | #[tauri::command(rename_all = "snake_case")] 86 | pub async fn set_dns( 87 | app_state: tauri::State<'_, Mutex>, 88 | path: String, 89 | dns_servers: Vec, 90 | dns_type: String, 91 | ) -> Result<(), String> { 92 | debug!( 93 | "path: {}, dns_servers: {:?}, dns_type: {}", 94 | path, dns_servers, dns_type 95 | ); 96 | if dns_type == "doh" { 97 | let mut app_state = app_state.lock().await; 98 | app_state.dns_server.run(dns_servers[0].to_string()).await?; 99 | 100 | dns_utils::apply_dns_by_path(path, vec!["127.0.0.2".to_string()]) 101 | .map_err(|e| format!("Failed to apply dns by path: {}", e))?; 102 | 103 | return Ok(()); 104 | } else { 105 | let result = dns_utils::apply_dns_by_path(path, dns_servers); 106 | return result; 107 | } 108 | } 109 | 110 | #[tauri::command(rename_all = "snake_case")] 111 | pub async fn clear_dns( 112 | app_state: tauri::State<'_, Mutex>, 113 | path: String, 114 | ) -> Result<(), String> { 115 | debug!("getting app state"); 116 | let mut app_state = app_state.lock().await; 117 | debug!("shutting down dns server"); 118 | app_state.dns_server.shutdown().await?; 119 | debug!("dns server shutdown"); 120 | debug!("clearing dns for path: {}", path); 121 | let result = dns_utils::clear_dns_by_path(path); 122 | debug!("dns cleared"); 123 | return result; 124 | } 125 | 126 | #[tauri::command(rename_all = "snake_case")] 127 | pub fn clear_dns_cache() -> Result<(), String> { 128 | let result = dns_utils::clear_dns_cache(); 129 | return result; 130 | } 131 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod commands; 2 | mod dns; 3 | mod net_interfaces; 4 | mod types; 5 | mod utils; 6 | 7 | use dns::dns_server::DnsServer; 8 | use log::{debug, error, info}; 9 | use std::env::temp_dir; 10 | use tauri_plugin_log::TargetKind; 11 | 12 | use commands::dns::{clear_dns, clear_dns_cache, get_interface_dns_info, set_dns, test_doh_server}; 13 | use commands::net_interfaces::{change_interface_state, get_best_interface, get_interfaces}; 14 | use tauri::RunEvent; 15 | use tauri::{Manager, WindowEvent}; 16 | use tokio::sync::{oneshot, Mutex}; 17 | 18 | use crate::utils::clear_dns_on_exit; 19 | 20 | pub struct AppState { 21 | pub dns_server: DnsServer, 22 | } 23 | 24 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 25 | pub fn run() { 26 | tauri::Builder::default() 27 | .plugin( 28 | tauri_plugin_log::Builder::new() 29 | .target(tauri_plugin_log::Target::new(TargetKind::Folder { 30 | path: temp_dir().join("better-dns-jumper"), 31 | file_name: Some("better-dns-jumper".to_string()), 32 | })) 33 | .max_file_size(1024 * 1024 * 10) // 10MB 34 | .filter(|metadata| metadata.target().contains("better_dns_jumper_lib")) 35 | .build(), 36 | ) 37 | .plugin(tauri_plugin_updater::Builder::new().build()) 38 | .plugin(tauri_plugin_autostart::Builder::new().build()) 39 | .plugin(tauri_plugin_store::Builder::new().build()) 40 | .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { 41 | let main_window = app.get_webview_window("main"); 42 | match main_window { 43 | Some(window) => { 44 | debug!("Main window found"); 45 | let _ = window.set_focus(); 46 | } 47 | None => { 48 | error!("Failed to get main window"); 49 | } 50 | } 51 | })) 52 | .plugin(tauri_plugin_window_state::Builder::new().build()) 53 | .plugin(tauri_plugin_opener::init()) 54 | .plugin(prevent_default()) 55 | .invoke_handler(tauri::generate_handler![ 56 | get_best_interface, 57 | get_interfaces, 58 | set_dns, 59 | get_interface_dns_info, 60 | clear_dns, 61 | clear_dns_cache, 62 | test_doh_server, 63 | change_interface_state, 64 | ]) 65 | .manage(Mutex::new(AppState { 66 | dns_server: DnsServer::new(), 67 | })) 68 | .build(tauri::generate_context!()) 69 | .expect("error while building tauri application") 70 | .run(move |_app_handle, _event| match &_event { 71 | RunEvent::ExitRequested { .. } => { 72 | let app_handle = _app_handle.clone(); 73 | let (tx, rx) = oneshot::channel(); 74 | tokio::spawn(async move { 75 | let app_state = app_handle.state::>(); 76 | let mut guard = app_state.lock().await; 77 | if guard.dns_server.is_running().await { 78 | let result = 79 | guard.dns_server.shutdown().await.map_err(|e| { 80 | format!("Error while shutting down DNS server: {}", e) 81 | }); 82 | 83 | if result.is_err() { 84 | error!("Error while shutting down DNS server: {:?}", result.err()); 85 | } 86 | 87 | let result = clear_dns_on_exit() 88 | .map_err(|e| format!("Error while clearing DNS: {}", e)); 89 | let _ = tx.send(result); 90 | } else { 91 | let _ = tx.send(Ok(())); 92 | } 93 | }); 94 | 95 | let result = futures::executor::block_on(rx) 96 | .expect("error waiting for cleanup channel") 97 | .map_err(|e| format!("Error while clearing DNS: {}", e)); 98 | match result { 99 | Ok(_) => { 100 | debug!("DNS cleared successfully"); 101 | } 102 | Err(e) => { 103 | error!("Error while clearing DNS: {}", e); 104 | } 105 | } 106 | } 107 | RunEvent::WindowEvent { 108 | event: WindowEvent::CloseRequested { .. }, 109 | label, 110 | .. 111 | } => { 112 | info!("closing window... {}", label); 113 | } 114 | _ => (), 115 | }) 116 | } 117 | 118 | #[cfg(debug_assertions)] 119 | fn prevent_default() -> tauri::plugin::TauriPlugin { 120 | use tauri_plugin_prevent_default::Flags; 121 | 122 | tauri_plugin_prevent_default::Builder::new() 123 | .with_flags(Flags::all().difference(Flags::DEV_TOOLS | Flags::RELOAD)) 124 | .build() 125 | } 126 | 127 | #[cfg(not(debug_assertions))] 128 | fn prevent_default() -> tauri::plugin::TauriPlugin { 129 | tauri_plugin_prevent_default::init() 130 | } 131 | -------------------------------------------------------------------------------- /src-tauri/src/net_interfaces/general.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::create_wmi_connection; 2 | 3 | use crate::utils::ipv4_to_u32; 4 | use log::error; 5 | use serde::{Deserialize, Serialize}; 6 | use std::net::Ipv4Addr; 7 | use winapi::shared::{minwindef::DWORD, winerror::ERROR_SUCCESS}; 8 | 9 | use winapi::um::iphlpapi::GetBestInterface; 10 | 11 | pub fn get_best_interface_idx() -> Result { 12 | let dest_ip = Ipv4Addr::new(8, 8, 8, 8); 13 | let dest_ip_u32 = ipv4_to_u32(dest_ip); 14 | 15 | let mut if_index: DWORD = 0; 16 | 17 | let result = unsafe { GetBestInterface(dest_ip_u32, &mut if_index) }; 18 | 19 | if result != ERROR_SUCCESS { 20 | error!("Failed to get best interface index: {}", result); 21 | return Err(format!("Failed to get best interface index: {}", result)); 22 | } 23 | let interface_index: u32 = if_index.into(); 24 | 25 | return Ok(interface_index); 26 | } 27 | 28 | pub fn get_all_interfaces() -> Result, String> { 29 | let wmi_con = 30 | create_wmi_connection().map_err(|e| format!("Failed to create WMI connection: {}", e))?; 31 | 32 | let net_adapter_query = 33 | format!("SELECT * FROM Win32_NetworkAdapter WHERE NetEnabled = TRUE OR NetEnabled = FALSE"); 34 | 35 | let net_adapter_config_query = format!("SELECT * FROM Win32_NetworkAdapterConfiguration"); 36 | 37 | let net_adapter_result: Vec = wmi_con 38 | .raw_query(net_adapter_query) 39 | .map_err(|e| format!("Failed to get all interfaces: {}", e))?; 40 | 41 | let net_adapter_config_result: Vec = wmi_con 42 | .raw_query(net_adapter_config_query) 43 | .map_err(|e| format!("Failed to get all interfaces configuration: {}", e))?; 44 | 45 | let interfaces = net_adapter_result 46 | .iter() 47 | .map(|net_adapter| { 48 | let net_adapter_config = net_adapter_config_result.iter().find(|net_adapter_config| { 49 | net_adapter_config.interface_index == net_adapter.interface_index 50 | }); 51 | 52 | Interface { 53 | adapter: net_adapter.clone(), 54 | config: net_adapter_config.cloned(), 55 | } 56 | }) 57 | .collect(); 58 | return Ok(interfaces); 59 | } 60 | 61 | pub fn get_interface_by_index(index: u32) -> Result { 62 | let interfaces = get_all_interfaces()?; 63 | let interface = interfaces 64 | .iter() 65 | .find(|interface| interface.adapter.interface_index == index); 66 | if let Some(interface) = interface { 67 | return Ok(Interface { 68 | adapter: interface.adapter.clone(), 69 | config: interface.config.clone(), 70 | }); 71 | } else { 72 | error!("Interface with index {} not found", index); 73 | return Err(format!("Interface with index {} not found", index)); 74 | } 75 | } 76 | 77 | pub fn get_network_adapter_path_by_ifidx(index: u32) -> Result { 78 | let wmi_connection = 79 | create_wmi_connection().map_err(|e| format!("Failed to create WMI connection: {}", e))?; 80 | 81 | let query = format!( 82 | "SELECT * FROM Win32_NetworkAdapter WHERE InterfaceIndex = {}", 83 | index 84 | ); 85 | 86 | let result: Vec = wmi_connection 87 | .raw_query(query) 88 | .map_err(|e| format!("Failed to get network adapter path: {}", e))?; 89 | 90 | let path = result.first().cloned().unwrap_or_default().path; 91 | 92 | Ok(path.unwrap_or_default()) 93 | } 94 | 95 | pub fn change_interface_state(path: String, enable: bool) -> Result<(), String> { 96 | let wmi_con = 97 | create_wmi_connection().map_err(|e| format!("Failed to create WMI connection: {}", e))?; 98 | 99 | let method = if enable { "Enable" } else { "Disable" }; 100 | 101 | let result: Result<(), wmi::WMIError> = 102 | wmi_con.exec_instance_method::(path, method, ()); 103 | 104 | match result { 105 | Ok(_) => Ok(()), 106 | Err(e) => Err(e.to_string()), 107 | } 108 | } 109 | 110 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 111 | #[serde(rename = "Win32_NetworkAdapter")] 112 | #[serde(rename_all(deserialize = "PascalCase", serialize = "snake_case"))] 113 | pub struct NetworkAdapterWmi { 114 | pub description: Option, 115 | pub device_id: String, 116 | pub guid: Option, 117 | pub index: u32, 118 | pub interface_index: u32, 119 | pub mac_address: Option, 120 | pub manufacturer: Option, 121 | #[serde(rename(deserialize = "NetConnectionID", serialize = "name"))] 122 | pub name: Option, 123 | pub net_connection_id: Option, 124 | pub net_enabled: bool, 125 | pub config_manager_error_code: Option, 126 | pub service_name: Option, 127 | #[serde(rename(deserialize = "__Path", serialize = "path"))] 128 | pub path: Option, 129 | } 130 | 131 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 132 | #[serde(rename = "Win32_NetworkAdapterConfiguration")] 133 | #[serde(rename_all(deserialize = "PascalCase", serialize = "snake_case"))] 134 | pub struct NetworkAdapterConfigurationWmi { 135 | pub default_ip_gateway: Option>, 136 | pub description: Option, 137 | pub dhcp_enabled: bool, 138 | pub dhcp_server: Option, 139 | pub dns_host_name: Option, 140 | pub dns_server_search_order: Option>, 141 | pub index: u32, 142 | pub interface_index: u32, 143 | pub ip_address: Option>, 144 | pub ip_connection_metric: Option, 145 | pub ip_enabled: bool, 146 | pub ip_subnet: Option>, 147 | pub mac_address: Option, 148 | #[serde(rename(deserialize = "__Path", serialize = "path"))] 149 | pub path: Option, 150 | } 151 | 152 | #[derive(Debug, Clone, Serialize, Deserialize)] 153 | #[serde(rename_all(deserialize = "PascalCase", serialize = "snake_case"))] 154 | pub struct Interface { 155 | pub adapter: NetworkAdapterWmi, 156 | pub config: Option, 157 | } 158 | 159 | #[derive(Debug, Clone, Serialize, Deserialize)] 160 | pub struct Address { 161 | pub ip: String, 162 | pub subnet: Option, 163 | pub gateway: Option, 164 | } 165 | -------------------------------------------------------------------------------- /src/screens/NetworkInterfaces.tsx: -------------------------------------------------------------------------------- 1 | import { addToast } from "@heroui/toast"; 2 | import { CONFIG_MANAGER_ERROR_CODE_DISABLED } from "../constants/interface"; 3 | import { useChangeInterfaceState, useInterfaces } from "../hooks/useInterfaces"; 4 | import { Button } from "@heroui/button"; 5 | import { 6 | getInterfaceIcon, 7 | getInterfaceType, 8 | InterfaceType, 9 | } from "../utils/interface"; 10 | import { Connected } from "../components/icons/Connected"; 11 | import { Disconnect } from "../components/icons/Disconnect"; 12 | import { ScrollShadow } from "@heroui/scroll-shadow"; 13 | import InterfaceIp from "../components/InterfaceIp"; 14 | import { Input } from "@heroui/input"; 15 | import { useState } from "react"; 16 | 17 | const NetworkInterfaces = () => { 18 | const [search, setSearch] = useState(""); 19 | const { data: interfaces, refetch: refetchInterfaces } = useInterfaces({ 20 | refetchInterval: 5000, 21 | }); 22 | 23 | const { 24 | mutateAsync: changeInterfaceState, 25 | isPending: isChangingInterfaceState, 26 | } = useChangeInterfaceState(); 27 | 28 | const handleChangeInterfaceState = async ( 29 | interfaceIdx: number, 30 | enable: boolean 31 | ) => { 32 | addToast({ 33 | title: "Changing interface state...", 34 | color: "primary", 35 | promise: changeInterfaceStatePromise(interfaceIdx, enable), 36 | }); 37 | }; 38 | 39 | const changeInterfaceStatePromise = async ( 40 | interfaceIdx: number, 41 | enable: boolean 42 | ) => { 43 | await changeInterfaceState({ 44 | interface_idx: interfaceIdx, 45 | enable: enable, 46 | }); 47 | await refetchInterfaces(); 48 | }; 49 | 50 | const getTypePriority = (type: InterfaceType | undefined): number => { 51 | switch (type) { 52 | case InterfaceType.Vpn: 53 | return 0; 54 | case InterfaceType.Wifi: 55 | return 1; 56 | case InterfaceType.Ethernet: 57 | return 2; 58 | case InterfaceType.Virtual: 59 | return 3; 60 | case InterfaceType.Bluetooth: 61 | return 4; 62 | default: 63 | return 5; 64 | } 65 | }; 66 | 67 | const processedInterfaces = interfaces 68 | ?.sort((a, b) => { 69 | // First sort by net_enabled status 70 | const enabledDiff = 71 | Number(b.adapter.net_enabled) - Number(a.adapter.net_enabled); 72 | if (enabledDiff !== 0) { 73 | return enabledDiff; 74 | } 75 | 76 | // Then sort by type priority 77 | const typeA = getInterfaceType(a.adapter.description ?? ""); 78 | const typeB = getInterfaceType(b.adapter.description ?? ""); 79 | const priorityA = getTypePriority(typeA); 80 | const priorityB = getTypePriority(typeB); 81 | return priorityA - priorityB; 82 | }) 83 | .filter( 84 | (iface) => 85 | iface.adapter.name 86 | ?.toLowerCase() 87 | .includes(search.toLowerCase()) || 88 | iface.adapter.description 89 | ?.toLowerCase() 90 | .includes(search.toLowerCase()) || 91 | iface.config.ip_address?.some((ip) => ip.includes(search)) 92 | ); 93 | 94 | return ( 95 |
96 |
97 |
98 | setSearch(e.target.value)} 102 | /> 103 |
104 | 105 | {processedInterfaces?.map((iface) => { 106 | const isDisabled = 107 | iface.adapter.config_manager_error_code === 108 | CONFIG_MANAGER_ERROR_CODE_DISABLED; 109 | return ( 110 |
114 |
115 |
116 | 117 | {getInterfaceIcon( 118 | iface.adapter.description ?? "" 119 | )} 120 | 121 | {iface.adapter.name} 122 | 123 | #{iface.adapter.interface_index} 124 | 125 |
126 |
127 | {iface.adapter.description} 128 |
129 |
130 | {iface.config.ip_address?.map((ip) => ( 131 | 132 | ))} 133 |
134 |
135 |
136 | 157 |
158 |
159 | ); 160 | })} 161 |
162 |
163 |
164 | ); 165 | }; 166 | 167 | export default NetworkInterfaces; 168 | -------------------------------------------------------------------------------- /src/screens/Servers.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | import { useServerStore } from "../stores/useServersStore"; 3 | import { Button } from "@heroui/button"; 4 | import ServerModal from "../components/ServerModal"; 5 | import ConfirmModal from "../components/ConfirmModal"; 6 | import { PROTOCOLS, type SERVER } from "../types"; 7 | import { Chip } from "@heroui/chip"; 8 | import { Select, SelectItem } from "@heroui/select"; 9 | 10 | const Servers = () => { 11 | const { 12 | load, 13 | servers, 14 | addServer, 15 | removeServer, 16 | updateServer, 17 | resetServers, 18 | } = useServerStore(); 19 | 20 | const [activeTab, setActiveTab] = useState("all"); 21 | const [isModalOpen, setIsModalOpen] = useState(false); 22 | const [modalMode, setModalMode] = useState<"add" | "edit">("add"); 23 | const [editingServer, setEditingServer] = useState(null); 24 | const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); 25 | 26 | const filteredServers = useMemo(() => { 27 | if (activeTab === "all") { 28 | return servers; 29 | } else { 30 | return servers.filter((server) => server.type === activeTab); 31 | } 32 | }, [servers, activeTab]); 33 | 34 | useEffect(() => { 35 | load(); 36 | }, []); 37 | 38 | const handleResetServers = () => { 39 | setIsConfirmModalOpen(true); 40 | }; 41 | 42 | const handleConfirmReset = async () => { 43 | await resetServers(); 44 | load(); 45 | setIsConfirmModalOpen(false); 46 | }; 47 | 48 | const handleRemoveServer = async (key: string) => { 49 | await removeServer(key); 50 | load(); 51 | }; 52 | 53 | const handleOpenAddModal = () => { 54 | setModalMode("add"); 55 | setEditingServer(null); 56 | setIsModalOpen(true); 57 | }; 58 | 59 | const handleOpenEditModal = (server: SERVER) => { 60 | setModalMode("edit"); 61 | setEditingServer(server); 62 | setIsModalOpen(true); 63 | }; 64 | 65 | const handleCloseModal = () => { 66 | setIsModalOpen(false); 67 | setEditingServer(null); 68 | }; 69 | 70 | const handleSaveServer = async (server: SERVER) => { 71 | if (modalMode === "edit") { 72 | await updateServer(server); 73 | } else { 74 | await addServer(server); 75 | } 76 | load(); 77 | }; 78 | 79 | const getServerType = (type: string) => { 80 | return PROTOCOLS.find((p) => p.key === type)?.name; 81 | }; 82 | 83 | return ( 84 |
85 |
86 |
87 |
88 | Servers 89 |
90 |
91 |
92 | 107 |
108 | 116 | 124 |
125 |
126 | 127 |
128 | {filteredServers.map((server) => ( 129 |
133 |
134 |
135 | {server.name} 136 | 142 | {getServerType(server.type)} 143 | 144 |
145 |
146 | 147 | {server.servers.join(", ")} 148 | 149 |
150 |
151 | 152 |
153 | 161 | 171 |
172 |
173 | ))} 174 |
175 |
176 | 177 | 184 | 185 | setIsConfirmModalOpen(false)} 188 | onConfirm={handleConfirmReset} 189 | title="Restore Default Servers?" 190 | message="This will replace all your custom servers with the default server list. This action cannot be undone." 191 | confirmText="Restore" 192 | cancelText="Cancel" 193 | confirmColor="danger" 194 | /> 195 |
196 | ); 197 | }; 198 | 199 | export default Servers; 200 | -------------------------------------------------------------------------------- /src/components/Updater.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Button } from "@heroui/button"; 3 | import { 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalFooter, 8 | ModalHeader, 9 | } from "@heroui/modal"; 10 | import { useUpdater } from "../hooks/useUpdater"; 11 | import { openUrl } from "@tauri-apps/plugin-opener"; 12 | import { getReleaseUrl } from "../constants/updater"; 13 | 14 | const Updater = () => { 15 | const { 16 | isDownloading, 17 | isInstalling, 18 | isModalOpen, 19 | update, 20 | downloaded, 21 | contentLength, 22 | checkForUpdates, 23 | downloadAndInstallUpdate, 24 | closeModal, 25 | } = useUpdater(); 26 | 27 | useEffect(() => { 28 | // Check for updates when component mounts 29 | checkForUpdates(); 30 | }, [checkForUpdates]); 31 | 32 | const handleUpdate = async () => { 33 | await downloadAndInstallUpdate(); 34 | }; 35 | 36 | const handleViewRelease = async () => { 37 | if (update?.version) { 38 | await openUrl(getReleaseUrl(update.version)); 39 | } 40 | }; 41 | 42 | const downloadProgress = 43 | contentLength > 0 ? ((downloaded / contentLength) * 100).toFixed(1) : 0; 44 | 45 | return ( 46 | <> 47 | {/* Update Modal */} 48 | { 51 | if (!open) closeModal(); 52 | }} 53 | placement="center" 54 | backdrop="opaque" 55 | classNames={{ 56 | wrapper: "h-100vh overflow-y-hidden", 57 | }} 58 | > 59 | 60 | {() => ( 61 | <> 62 | 63 | Update Available 64 | 65 | 66 | {update && ( 67 |
68 |

69 | A new version of the app is 70 | available! 71 |

72 |
73 |
74 | 75 | Current Version: 76 | 77 | 78 | {update.currentVersion} 79 | 80 |
81 |
82 | 83 | New Version: 84 | 85 | 86 | {update.version} 87 | 88 |
89 |
90 | 91 | 100 | 101 | {isDownloading && ( 102 |
103 |
104 | Downloading... 105 | 106 | {downloadProgress}% 107 | 108 |
109 |
110 |
116 |
117 |

118 | {( 119 | downloaded / 120 | 1024 / 121 | 1024 122 | ).toFixed(2)}{" "} 123 | MB /{" "} 124 | {( 125 | contentLength / 126 | 1024 / 127 | 1024 128 | ).toFixed(2)}{" "} 129 | MB 130 |

131 |
132 | )} 133 | 134 | {isInstalling && ( 135 |
136 |

137 | Installing update... 138 |

139 |

140 | The app will restart 141 | automatically 142 |

143 |
144 | )} 145 |
146 | )} 147 | 148 | 149 | 157 | 169 | 170 | 171 | )} 172 | 173 | 174 | 175 | ); 176 | }; 177 | 178 | export default Updater; 179 | -------------------------------------------------------------------------------- /src-tauri/src/dns/dns_server.rs: -------------------------------------------------------------------------------- 1 | use hickory_proto::op::{Header, ResponseCode}; 2 | use hickory_proto::rr::{Record, RecordType}; 3 | use hickory_proto::runtime::TokioRuntimeProvider; 4 | use hickory_proto::xfer::Protocol; 5 | use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts}; 6 | use hickory_resolver::name_server::GenericConnector; 7 | use hickory_resolver::{Resolver, TokioResolver}; 8 | use hickory_server::authority::MessageResponseBuilder; 9 | use hickory_server::server::{ 10 | Request, RequestHandler, ResponseHandler, ResponseInfo, ServerFuture, 11 | }; 12 | use log::{debug, error, info}; 13 | use std::net::ToSocketAddrs; 14 | use std::sync::Arc; 15 | use tokio::net::UdpSocket; 16 | use tokio::sync::{oneshot, Mutex}; 17 | 18 | pub struct DnsServer { 19 | pub resolver: Option, 20 | pub server: Option>>>, 21 | pub socket: Option, 22 | pub shutdown_sender: Option>, 23 | } 24 | 25 | impl DnsServer { 26 | pub fn new() -> Self { 27 | Self { 28 | resolver: None, 29 | server: None, 30 | socket: None, 31 | shutdown_sender: None, 32 | } 33 | } 34 | 35 | pub async fn run(&mut self, server: String) -> Result<(), String> { 36 | let server_url = 37 | url::Url::parse(&server).map_err(|e| format!("Failed to parse server: {}", e))?; 38 | 39 | let protocol = server_url.scheme(); 40 | if protocol != "https" { 41 | error!("Invalid protocol: {}", protocol); 42 | return Err(format!("Invalid protocol: {}", protocol)); 43 | } 44 | 45 | let domain = server_url.host().ok_or("Failed to get domain")?; 46 | let port = server_url.port().unwrap_or(443); 47 | let path = server_url.path(); 48 | 49 | let resolver = 50 | DnsServer::create_dns_resolver(domain.to_string(), port, Some(path.to_string())) 51 | .map_err(|e| { 52 | error!("Failed to create DNS resolver: {}", e); 53 | format!("Failed to create DNS resolver: {}", e) 54 | })?; 55 | 56 | let socket = self.create_udp_socket().await?; 57 | 58 | debug!("created socket: {:?}", socket); 59 | 60 | let mut server = ServerFuture::new(DnsResolver::new(resolver)); 61 | 62 | debug!("created server"); 63 | 64 | server.register_socket(socket); 65 | 66 | let server = Arc::new(Mutex::new(server)); 67 | self.server = Some(server.clone()); 68 | 69 | let (shutdown_tx, shutdown_rx) = oneshot::channel(); 70 | self.shutdown_sender = Some(shutdown_tx); 71 | 72 | let server_clone = server.clone(); 73 | tokio::spawn(async move { 74 | debug!("Dns server blocking until done"); 75 | 76 | tokio::select! { 77 | result = async { 78 | let mut server_guard = server_clone.lock().await; 79 | server_guard.block_until_done().await 80 | } => { 81 | match result { 82 | Ok(_) => debug!("Dns server stopped (block_until_done completed)"), 83 | Err(err) => error!("Dns server stopped with error: {}", err), 84 | } 85 | } 86 | _ = shutdown_rx => { 87 | debug!("Dns server received shutdown signal"); 88 | // Acquire lock to call shutdown_gracefully 89 | // This will wait for the lock to be released by the cancelled branch above 90 | let mut server_guard = server_clone.lock().await; 91 | if let Err(_err) = server_guard.shutdown_gracefully().await { 92 | error!("Error during graceful shutdown: {:?}", _err); 93 | } 94 | // Continue to block until done (should complete quickly after shutdown) 95 | if let Err(_err) = server_guard.block_until_done().await { 96 | error!("Dns server stopped with error"); 97 | } 98 | debug!("Dns server stopped (after graceful shutdown)"); 99 | } 100 | } 101 | }); 102 | 103 | debug!("registered socket"); 104 | 105 | Ok(()) 106 | } 107 | 108 | pub async fn shutdown(&mut self) -> Result<(), String> { 109 | debug!("shutting down dns server"); 110 | // Send shutdown signal to the spawned task instead of trying to acquire the lock 111 | if let Some(shutdown_tx) = self.shutdown_sender.take() { 112 | debug!("sending shutdown signal"); 113 | if let Err(_) = shutdown_tx.send(()) { 114 | error!("shutdown signal receiver already dropped"); 115 | } else { 116 | debug!("shutdown signal sent successfully"); 117 | } 118 | } 119 | // Clear the server reference after shutdown 120 | self.server = None; 121 | debug!("dns server shutdown successfully"); 122 | Ok(()) 123 | } 124 | 125 | pub fn create_dns_resolver( 126 | domain: String, 127 | port: u16, 128 | http_endpoint: Option, 129 | ) -> Result { 130 | let mut config = ResolverConfig::new(); 131 | 132 | let mut socket_addr = (domain.clone(), port) 133 | .to_socket_addrs() 134 | .map_err(|e| format!("Failed to resolve domain: {}", e))?; 135 | 136 | let socket_addr = socket_addr 137 | .next() 138 | .ok_or(format!("Failed to resolve domain: {}", &domain))?; 139 | 140 | info!("DNS Server Resolved: {:?}", socket_addr); 141 | 142 | config.add_name_server(NameServerConfig { 143 | socket_addr, 144 | protocol: Protocol::Https, 145 | tls_dns_name: Some(domain), 146 | http_endpoint: http_endpoint, 147 | bind_addr: None, 148 | trust_negative_responses: true, 149 | }); 150 | 151 | let opts = ResolverOpts::default(); 152 | 153 | let connector = GenericConnector::::default(); 154 | 155 | let resolver = Resolver::builder_with_config(config, connector) 156 | .with_options(opts) 157 | .build(); 158 | 159 | dbg!(&resolver); 160 | 161 | Ok(resolver) 162 | } 163 | 164 | pub async fn create_udp_socket(&self) -> Result { 165 | let socket = UdpSocket::bind("127.0.0.2:53") 166 | .await 167 | .map_err(|e| format!("Failed to create UDP socket: {}", e))?; 168 | 169 | Ok(socket) 170 | } 171 | 172 | pub async fn is_running(&self) -> bool { 173 | self.server.is_some() 174 | } 175 | } 176 | 177 | pub struct DnsResolver { 178 | resolver: TokioResolver, 179 | } 180 | 181 | impl DnsResolver { 182 | pub fn new(resolver: TokioResolver) -> Self { 183 | Self { resolver } 184 | } 185 | } 186 | 187 | #[async_trait::async_trait] 188 | impl RequestHandler for DnsResolver { 189 | async fn handle_request( 190 | &self, 191 | request: &Request, 192 | mut response_handle: R, 193 | ) -> ResponseInfo { 194 | let resolver = &self.resolver; 195 | if let Some(query) = request.queries().first() { 196 | let name = query.name().to_ascii(); 197 | let record_type = query.query_type(); 198 | 199 | debug!("Received query: {} {:?}", name, record_type); 200 | 201 | // Perform DNS lookup through DoH resolver and convert to Vec 202 | let records_result: Result, _> = match record_type { 203 | RecordType::A | RecordType::AAAA => resolver 204 | .lookup_ip(name.clone()) 205 | .await 206 | .map(|lookup| lookup.as_lookup().record_iter().cloned().collect()), 207 | RecordType::TXT => resolver 208 | .txt_lookup(name.clone()) 209 | .await 210 | .map(|lookup| lookup.as_lookup().record_iter().cloned().collect()), 211 | RecordType::MX => resolver 212 | .mx_lookup(name.clone()) 213 | .await 214 | .map(|lookup| lookup.as_lookup().record_iter().cloned().collect()), 215 | _ => { 216 | error!("Unsupported record type: {:?}", record_type); 217 | let response = MessageResponseBuilder::from_message_request(request); 218 | let mut header = Header::response_from_request(request.header()); 219 | header.set_response_code(ResponseCode::NotImp); 220 | let result = response_handle 221 | .send_response(response.build_no_records(header)) 222 | .await; 223 | return match result { 224 | Err(e) => { 225 | error!("Error sending response: {}", e); 226 | let mut err_header = Header::response_from_request(request.header()); 227 | err_header.set_response_code(ResponseCode::ServFail); 228 | err_header.into() 229 | } 230 | Ok(info) => info, 231 | }; 232 | } 233 | }; 234 | 235 | // Build response 236 | let response = MessageResponseBuilder::from_message_request(request); 237 | let mut header = Header::response_from_request(request.header()); 238 | 239 | let result = if let Ok(records) = records_result { 240 | header.set_response_code(ResponseCode::NoError); 241 | response_handle 242 | .send_response(response.build(header, records.iter(), &[], &[], &[])) 243 | .await 244 | } else { 245 | header.set_response_code(ResponseCode::ServFail); 246 | response_handle 247 | .send_response(response.build_no_records(header)) 248 | .await 249 | }; 250 | 251 | match result { 252 | Err(e) => { 253 | error!("Error sending response: {}", e); 254 | let mut err_header = Header::response_from_request(request.header()); 255 | err_header.set_response_code(ResponseCode::ServFail); 256 | err_header.into() 257 | } 258 | Ok(info) => info, 259 | } 260 | } else { 261 | // No queries in request 262 | let response = MessageResponseBuilder::from_message_request(request); 263 | let mut header = Header::response_from_request(request.header()); 264 | header.set_response_code(ResponseCode::FormErr); 265 | let result = response_handle 266 | .send_response(response.build_no_records(header)) 267 | .await; 268 | 269 | match result { 270 | Err(e) => { 271 | error!("Error sending response: {}", e); 272 | let mut err_header = Header::response_from_request(request.header()); 273 | err_header.set_response_code(ResponseCode::ServFail); 274 | err_header.into() 275 | } 276 | Ok(info) => info, 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/components/ServerModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from "react"; 2 | import { 3 | Modal, 4 | ModalContent, 5 | ModalHeader, 6 | ModalBody, 7 | ModalFooter, 8 | } from "@heroui/modal"; 9 | import { Input } from "@heroui/input"; 10 | import { Select, SelectItem } from "@heroui/select"; 11 | import { Button } from "@heroui/button"; 12 | import type { SERVER } from "../types"; 13 | 14 | // Generate key from name: convert to uppercase, replace spaces/special chars with underscores 15 | const generateKeyFromName = (name: string): string => { 16 | return name 17 | .trim() 18 | .toUpperCase() 19 | .replace(/[^A-Z0-9]/g, "_") 20 | .replace(/_+/g, "_") 21 | .replace(/^_|_$/g, ""); 22 | }; 23 | 24 | // Validate IP address (IPv4) 25 | const isValidIP = (ip: string): boolean => { 26 | const ipRegex = 27 | /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 28 | return ipRegex.test(ip.trim()); 29 | }; 30 | 31 | // Validate URL (for DoH) 32 | const isValidURL = (url: string): boolean => { 33 | try { 34 | const parsed = new URL(url.trim()); 35 | return parsed.protocol === "https:"; 36 | } catch { 37 | return false; 38 | } 39 | }; 40 | 41 | // Validate servers based on type 42 | const validateServers = ( 43 | servers: string[], 44 | type: "dns" | "doh" 45 | ): { isValid: boolean; errors: string[] } => { 46 | const errors: string[] = []; 47 | 48 | if (servers.length === 0) { 49 | errors.push("At least one server is required"); 50 | return { isValid: false, errors }; 51 | } 52 | 53 | if (type === "doh" && servers.length > 1) { 54 | errors.push("DoH only accepts a single URL"); 55 | return { isValid: false, errors }; 56 | } 57 | 58 | if (type === "dns" && servers.length > 2) { 59 | errors.push("DNS servers can only have a maximum of 2 IP addresses"); 60 | return { isValid: false, errors }; 61 | } 62 | 63 | servers.forEach((server, index) => { 64 | if (type === "dns") { 65 | if (!isValidIP(server)) { 66 | errors.push(`Server ${index + 1} is not a valid IP address`); 67 | } 68 | } else if (type === "doh") { 69 | if (!isValidURL(server)) { 70 | errors.push("Not a valid HTTPS URL"); 71 | } 72 | } 73 | }); 74 | 75 | return { 76 | isValid: errors.length === 0, 77 | errors, 78 | }; 79 | }; 80 | 81 | interface ServerModalProps { 82 | isOpen: boolean; 83 | onClose: () => void; 84 | onSave: (server: SERVER) => Promise; 85 | server?: SERVER | null; 86 | mode: "add" | "edit"; 87 | } 88 | 89 | const ServerModal = ({ 90 | isOpen, 91 | onClose, 92 | onSave, 93 | server, 94 | mode, 95 | }: ServerModalProps) => { 96 | const [formData, setFormData] = useState<{ 97 | type: "dns" | "doh"; 98 | key: string; 99 | name: string; 100 | servers: string; 101 | tags: string; 102 | }>({ 103 | type: "dns", 104 | key: "", 105 | name: "", 106 | servers: "", 107 | tags: "", 108 | }); 109 | 110 | const [serverErrors, setServerErrors] = useState([]); 111 | 112 | // Generate key from name when in add mode 113 | const generatedKey = useMemo(() => { 114 | if (mode === "add" && formData.name.trim()) { 115 | return generateKeyFromName(formData.name); 116 | } 117 | return formData.key; 118 | }, [formData.name, formData.key, mode]); 119 | 120 | useEffect(() => { 121 | if (isOpen) { 122 | if (mode === "edit" && server) { 123 | // For DoH, only show the first server (single URL) 124 | const serverValue = 125 | server.type === "doh" 126 | ? server.servers[0] || "" 127 | : server.servers.join(", "); 128 | setFormData({ 129 | type: server.type, 130 | key: server.key, 131 | name: server.name, 132 | servers: serverValue, 133 | tags: server.tags.join(", "), 134 | }); 135 | setServerErrors([]); 136 | } else { 137 | setFormData({ 138 | type: "dns", 139 | key: "", 140 | name: "", 141 | servers: "", 142 | tags: "", 143 | }); 144 | setServerErrors([]); 145 | } 146 | } 147 | }, [isOpen, mode, server]); 148 | 149 | // Update key when name changes in add mode 150 | useEffect(() => { 151 | if (mode === "add" && formData.name.trim()) { 152 | const newKey = generateKeyFromName(formData.name); 153 | setFormData((prev) => ({ ...prev, key: newKey })); 154 | } 155 | }, [formData.name, mode]); 156 | 157 | const handleServersChange = (value: string) => { 158 | setFormData({ ...formData, servers: value }); 159 | 160 | // Validate servers in real-time 161 | // For DoH, treat as single value; for DNS, split by comma 162 | const serverList = 163 | formData.type === "doh" 164 | ? value.trim() 165 | ? [value.trim()] 166 | : [] 167 | : value 168 | .split(",") 169 | .map((s) => s.trim()) 170 | .filter((s) => s.length > 0); 171 | 172 | if (serverList.length > 0) { 173 | const validation = validateServers(serverList, formData.type); 174 | setServerErrors(validation.errors); 175 | } else { 176 | setServerErrors([]); 177 | } 178 | }; 179 | 180 | const handleTypeChange = (type: "dns" | "doh") => { 181 | setFormData({ ...formData, type }); 182 | 183 | // Re-validate servers when type changes 184 | // For DoH, treat as single value; for DNS, split by comma 185 | const serverList = 186 | type === "doh" 187 | ? formData.servers.trim() 188 | ? [formData.servers.trim()] 189 | : [] 190 | : formData.servers 191 | .split(",") 192 | .map((s) => s.trim()) 193 | .filter((s) => s.length > 0); 194 | 195 | if (serverList.length > 0) { 196 | const validation = validateServers(serverList, type); 197 | setServerErrors(validation.errors); 198 | } else { 199 | setServerErrors([]); 200 | } 201 | }; 202 | 203 | const handleSave = async () => { 204 | // For DoH, treat as single value; for DNS, split by comma 205 | const serverList = 206 | formData.type === "doh" 207 | ? formData.servers.trim() 208 | ? [formData.servers.trim()] 209 | : [] 210 | : formData.servers 211 | .split(",") 212 | .map((s) => s.trim()) 213 | .filter((s) => s.length > 0); 214 | 215 | // Validate before saving 216 | const validation = validateServers(serverList, formData.type); 217 | if (!validation.isValid) { 218 | setServerErrors(validation.errors); 219 | return; 220 | } 221 | 222 | const finalKey = mode === "add" ? generatedKey : formData.key; 223 | 224 | const serverData: SERVER = { 225 | type: formData.type, 226 | key: finalKey.trim(), 227 | name: formData.name.trim(), 228 | servers: serverList, 229 | tags: formData.tags 230 | .split(",") 231 | .map((t) => t.trim()) 232 | .filter((t) => t.length > 0), 233 | }; 234 | 235 | if ( 236 | !serverData.key || 237 | !serverData.name || 238 | serverData.servers.length === 0 239 | ) { 240 | return; 241 | } 242 | 243 | await onSave(serverData); 244 | onClose(); 245 | }; 246 | 247 | return ( 248 | 260 | 261 | 262 | {mode === "add" ? "Add Server" : "Edit Server"} 263 | 264 | 265 |
266 |
267 | 282 | 287 | setFormData({ ...formData, name: value }) 288 | } 289 | size="sm" 290 | placeholder="e.g., Google DNS" 291 | /> 292 |
293 | 0} 310 | errorMessage={serverErrors.join(", ")} 311 | /> 312 | 316 | setFormData({ ...formData, tags: value }) 317 | } 318 | size="sm" 319 | placeholder="Web, Gaming" 320 | description="Comma-separated list" 321 | /> 322 |
323 |
324 | 325 | 328 | 331 | 332 |
333 |
334 | ); 335 | }; 336 | 337 | export default ServerModal; 338 | -------------------------------------------------------------------------------- /src/screens/main.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from "react"; 2 | import ToggleButton from "../components/ToggleButton"; 3 | import { Select, SelectItem } from "@heroui/select"; 4 | import { Tooltip } from "@heroui/tooltip"; 5 | import { Button } from "@heroui/button"; 6 | import { useInterfaces } from "../hooks/useInterfaces"; 7 | import { 8 | useSetDns, 9 | useGetInterfaceDnsInfo, 10 | useClearDns, 11 | useClearDnsCache, 12 | useTestDohServer, 13 | type DoHTestResult, 14 | } from "../hooks/useDns"; 15 | import { DNSServer } from "../components/icons/DNSServer"; 16 | import { Network } from "../components/icons/Network"; 17 | import { Broom } from "../components/icons/Broom"; 18 | import { addToast } from "@heroui/toast"; 19 | import { Reset } from "../components/icons/Reset"; 20 | import { Texture } from "../components/icons/Texture"; 21 | import { Tab, Tabs } from "@heroui/tabs"; 22 | import { Test } from "../components/icons/Test"; 23 | import { PROTOCOLS, SERVER } from "../types"; 24 | import { useServerStore } from "../stores/useServersStore"; 25 | import { useDnsState } from "../hooks/useDnsState"; 26 | 27 | const Main = () => { 28 | const { servers, isLoading: isLoadingServers, load } = useServerStore(); 29 | 30 | const { 31 | isActive, 32 | toggleIsActive, 33 | dnsServer, 34 | setDnsServer, 35 | protocol, 36 | setProtocol, 37 | } = useDnsState(); 38 | const [IfIdx, setIfIdx] = useState(0); 39 | const [dohTestResults, setDohTestResults] = useState< 40 | Map 41 | >(new Map()); 42 | 43 | // Load servers on mount 44 | useEffect(() => { 45 | load(); 46 | }, [load]); 47 | 48 | // Get the appropriate server list based on selected protocol 49 | const serverList: SERVER[] = useMemo(() => { 50 | return servers.filter((server) => server.type === protocol); 51 | }, [servers, protocol]); 52 | 53 | // Set initial DNS server when servers are loaded or protocol changes 54 | useEffect(() => { 55 | if (!isLoadingServers && serverList.length > 0) { 56 | // If current server is not in the list, or no server is selected, select the first one 57 | if (!dnsServer || !serverList.find((s) => s.key === dnsServer)) { 58 | setDnsServer(serverList[0].key); 59 | } 60 | } 61 | }, [serverList, isLoadingServers, dnsServer]); 62 | 63 | const dnsServerData = serverList.find((server) => server.key === dnsServer); 64 | 65 | const { data: interfaces, isLoading: isLoadingInterfaces } = 66 | useInterfaces(); 67 | 68 | const { data: interfaceDnsInfo, refetch: refetchInterfaceDnsInfo } = 69 | useGetInterfaceDnsInfo(IfIdx); 70 | 71 | const { mutate: setDns } = useSetDns({ 72 | onSuccess: () => { 73 | refetchInterfaceDnsInfo(); 74 | }, 75 | }); 76 | const { mutate: clearDns } = useClearDns({ 77 | onSuccess: () => { 78 | refetchInterfaceDnsInfo(); 79 | }, 80 | }); 81 | 82 | const { mutate: testDohServer, isPending } = useTestDohServer({ 83 | onSuccess: (data, variables) => { 84 | // Find the server key from the server URL 85 | const dohServers = servers.filter((s) => s.type === "doh"); 86 | const serverKey = dohServers.find( 87 | (s) => s.servers[0] === variables.server 88 | )?.key; 89 | if (serverKey) { 90 | setDohTestResults((prev) => { 91 | const newMap = new Map(prev); 92 | newMap.set(serverKey, data); 93 | return newMap; 94 | }); 95 | } 96 | }, 97 | onError: (error, variables) => { 98 | // Find the server key from the server URL 99 | const dohServers = servers.filter((s) => s.type === "doh"); 100 | const serverKey = dohServers.find( 101 | (s) => s.servers[0] === variables.server 102 | )?.key; 103 | if (serverKey) { 104 | setDohTestResults((prev) => { 105 | const newMap = new Map(prev); 106 | newMap.set(serverKey, { 107 | success: false, 108 | latency: 0, 109 | error: error.message || "Test failed", 110 | }); 111 | return newMap; 112 | }); 113 | } 114 | }, 115 | }); 116 | 117 | // Test all DoH servers when switching to DoH tab 118 | useEffect(() => { 119 | if (protocol === "doh" && !isLoadingServers) { 120 | const dohServers = servers.filter((s) => s.type === "doh"); 121 | 122 | // Mark all DoH servers as testing 123 | setDohTestResults((prev) => { 124 | const newMap = new Map(prev); 125 | dohServers.forEach((server) => { 126 | if (!newMap.has(server.key)) { 127 | newMap.set(server.key, "testing"); 128 | } 129 | }); 130 | return newMap; 131 | }); 132 | 133 | // Test all DoH servers 134 | dohServers.forEach((server) => { 135 | testDohServer({ 136 | server: server.servers[0], 137 | domain: "google.com", 138 | }); 139 | }); 140 | } 141 | // eslint-disable-next-line react-hooks/exhaustive-deps 142 | }, [protocol, servers, isLoadingServers]); 143 | const { mutate: clearDnsCache } = useClearDnsCache({ 144 | onSuccess: () => { 145 | console.log("DNS cleared"); 146 | addToast({ 147 | title: "DNS cleared", 148 | color: "success", 149 | icon: , 150 | }); 151 | }, 152 | onError: (error) => { 153 | console.log( 154 | "[handleClearDnsCache] Error clearing DNS cache", 155 | error 156 | ); 157 | addToast({ 158 | title: "Error clearing DNS cache", 159 | color: "danger", 160 | icon: , 161 | }); 162 | }, 163 | }); 164 | 165 | const handleCopyToClipboard = async (text: string) => { 166 | try { 167 | await navigator.clipboard.writeText(text); 168 | addToast({ 169 | title: "Copied to clipboard", 170 | color: "success", 171 | }); 172 | } catch (error) { 173 | addToast({ 174 | title: "Failed to copy", 175 | color: "danger", 176 | }); 177 | } 178 | }; 179 | 180 | const renderDnsServers = () => { 181 | if (dnsServerData?.type === "doh") { 182 | return dnsServerData?.servers.map((server) => { 183 | const url = new URL(server); 184 | return ( 185 | 190 |
handleCopyToClipboard(server)} 193 | > 194 | {url.hostname} 195 |
196 |
197 | ); 198 | }); 199 | } else { 200 | return dnsServerData?.servers.join(", "); 201 | } 202 | }; 203 | const handleSetDns = () => { 204 | if (!dnsServerData) return; 205 | setDns({ 206 | path: interfaceDnsInfo?.path ?? "", 207 | dns_servers: dnsServerData?.servers, 208 | dns_type: dnsServerData?.type, 209 | }); 210 | }; 211 | const handleClearDns = () => { 212 | clearDns({ 213 | path: interfaceDnsInfo?.path ?? "", 214 | }); 215 | }; 216 | 217 | const handleToggle = () => { 218 | if (!isActive) { 219 | handleSetDns(); 220 | } else { 221 | handleClearDns(); 222 | } 223 | toggleIsActive(); 224 | }; 225 | 226 | const handleClearDnsCache = () => { 227 | clearDnsCache(); 228 | }; 229 | 230 | const handleResetDns = () => { 231 | clearDns({ 232 | path: interfaceDnsInfo?.path ?? "", 233 | }); 234 | }; 235 | 236 | const handleTestDohServer = () => { 237 | testDohServer({ 238 | server: dnsServerData?.servers[0] ?? "", 239 | domain: "google.com", 240 | }); 241 | }; 242 | 243 | return ( 244 |
245 |
246 | 247 |
248 |
249 | 291 | 359 | 360 | { 368 | setProtocol(key as "dns" | "doh"); 369 | // Reset to first server of the selected protocol 370 | const newServerList = servers.filter( 371 | (s) => s.type === key 372 | ); 373 | if (newServerList.length > 0) { 374 | setDnsServer(newServerList[0].key); 375 | } 376 | }} 377 | color="primary" 378 | isDisabled={ 379 | !interfaceDnsInfo?.path || isActive || isLoadingServers 380 | } 381 | > 382 | {PROTOCOLS.map((protocol) => ( 383 | 384 | ))} 385 | 386 | 387 |
388 |
389 |
390 | Server 391 | {dnsServerData?.type === "doh" ? "" : "s"}: 392 |
393 |
{renderDnsServers()}
394 |
395 | {/*
396 |
Tags:
397 |
{dnsServerData?.tags.join(", ")}
398 |
*/} 399 |
400 |
Interface:
401 |
402 | {IfIdx === 0 ? ( 403 | 404 | Auto 405 | 406 | ({interfaceDnsInfo?.interface_name}) 407 | 408 | 409 | ) : ( 410 | `${interfaceDnsInfo?.interface_name}` 411 | )} 412 |
413 |
414 | {(interfaceDnsInfo?.dns_servers.length ?? 0) > 0 && ( 415 |
416 |
Current DNS:
417 |
418 | {interfaceDnsInfo?.dns_servers.join(", ")} 419 |
420 |
421 | )} 422 |
423 |
424 | 429 | 432 | 433 | 438 | 445 | 446 | 451 | 459 | 460 | {new Array(4).fill(0).map((_, index) => ( 461 | 464 | ))} 465 |
466 |
467 |
468 | ); 469 | }; 470 | 471 | export default Main; 472 | --------------------------------------------------------------------------------