Compare commits
35 Commits
feat/gui
...
c8fa174ac0
| Author | SHA1 | Date | |
|---|---|---|---|
| c8fa174ac0 | |||
| 9624484b29 | |||
| eee73be58e | |||
| eb30037f53 | |||
| 2e1f05cadb | |||
| 09c1cd5ddc | |||
| bb162bccee | |||
| d1bf9163f7 | |||
| 7c11c7fbd7 | |||
| 8f4cbf7dc0 | |||
| bb3a33c103 | |||
| 0835df14ac | |||
| 6e5b64545e | |||
| f35dc56598 | |||
| 71674e3de8 | |||
| 4f2aef91c0 | |||
| 6950600bdd | |||
| a29a6190fb | |||
| b10402ee1e | |||
| dbf3df9ff9 | |||
| 6b3f88f6bb | |||
| 50569d2a20 | |||
| 664715f02b | |||
| 8e11f63479 | |||
| 19f67e8b77 | |||
| 8a0677caf2 | |||
| 5de0341ab4 | |||
| d3cdf6ea76 | |||
| 81f5568957 | |||
| 61f2d89ef1 | |||
| 429563eee9 | |||
| fc7360ea00 | |||
| 914e8b6d67 | |||
| 4169ede074 | |||
| d2b7080c96 |
+8
-1
@@ -23,4 +23,11 @@ rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
|
||||
rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
|
||||
|
||||
[target.mipsel-unknown-linux-musl]
|
||||
rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
|
||||
rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = ["-C", "link-arg=-framework", "-C", "link-arg=Cocoa", "-C", "link-arg=-framework", "-C", "link-arg=WebKit"]
|
||||
|
||||
[env]
|
||||
# Suppress int-conversion warnings in webview-sys for Apple Silicon
|
||||
CC_aarch64_apple_darwin = "clang -Wno-int-conversion"
|
||||
@@ -16,17 +16,28 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ windows-latest, ubuntu-latest, macOS-latest]
|
||||
include:
|
||||
- os: windows-latest
|
||||
- os: ubuntu-24.04
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install libgtk-dev libwebkit2gtk-4.0
|
||||
run: sudo apt update && sudo apt install libwebkit2gtk-4.0-dev
|
||||
- name: Install dependencies
|
||||
run: sudo apt update && sudo apt install libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
|
||||
- name: Update Rust
|
||||
run: rustup update stable
|
||||
|
||||
- name: Install ARM target (macOS)
|
||||
run: rustup target add aarch64-apple-darwin
|
||||
if: matrix.target == 'aarch64-apple-darwin'
|
||||
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
run: cargo build --verbose ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --all --verbose
|
||||
run: cargo test --all --verbose ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [ amd64, i686, armhf, armlf, arm64 ]
|
||||
arch: [ amd64, i686, armhf, armel, arm64 ]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -65,7 +65,9 @@ jobs:
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt upgrade && sudo apt install libwebkit2gtk-4.0-dev upx
|
||||
sudo apt update
|
||||
sudo apt upgrade
|
||||
sudo apt install libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev upx-ucl
|
||||
cargo install cross
|
||||
|
||||
- name: Build and package deb packages
|
||||
@@ -97,20 +99,14 @@ jobs:
|
||||
|
||||
- name: install dependencies
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: sudo apt update && sudo apt install --no-install-recommends libwebkit2gtk-4.0-dev upx
|
||||
run: sudo apt update && sudo apt install --no-install-recommends libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev upx-ucl
|
||||
|
||||
- name: Build release binaries
|
||||
run: cargo build --release
|
||||
|
||||
- name: Build Windows release binaries with Edge web-engine
|
||||
if: contains(matrix.os, 'windows')
|
||||
run: cargo build --release --features "edge" --target-dir edge
|
||||
|
||||
- name: windows
|
||||
if: contains(matrix.os, 'windows')
|
||||
run: |
|
||||
echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV
|
||||
echo "BIN_ARCH_EDGE=windows-amd64-edge" >> $GITHUB_ENV
|
||||
run: echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV
|
||||
|
||||
- name: linux
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
@@ -129,8 +125,6 @@ jobs:
|
||||
run: |
|
||||
echo "BIN_PATH=target/release/alfis.exe" >> $GITHUB_ENV
|
||||
echo "ZIP_NAME=alfis-${{env.BIN_ARCH}}-${{ needs.get_version.outputs.project_version }}.zip" >> $GITHUB_ENV
|
||||
echo "BIN_PATH_EDGE=edge/release/alfis.exe" >> $GITHUB_ENV
|
||||
echo "ZIP_NAME_EDGE=alfis-${{env.BIN_ARCH}}-${{ needs.get_version.outputs.project_version }}-edge.zip" >> $GITHUB_ENV
|
||||
|
||||
- name: Packaging
|
||||
uses: papeloto/action-zip@v1
|
||||
@@ -138,13 +132,6 @@ jobs:
|
||||
files: ${{ env.BIN_PATH }} alfis.toml README.md LICENSE adblock.txt
|
||||
dest: ${{ env.ZIP_NAME }}
|
||||
|
||||
- name: Packaging Edge binary
|
||||
if: contains(matrix.os, 'windows')
|
||||
uses: papeloto/action-zip@v1
|
||||
with:
|
||||
files: ${{ env.BIN_PATH_EDGE }} alfis.toml README.md LICENSE adblock.txt
|
||||
dest: ${{ env.ZIP_NAME_EDGE }}
|
||||
|
||||
- name: Upload zip
|
||||
id: upload-zip
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -155,15 +142,3 @@ jobs:
|
||||
asset_path: ${{ env.ZIP_NAME }}
|
||||
asset_name: ${{ env.ZIP_NAME }}
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload Edge binary
|
||||
if: contains(matrix.os, 'windows')
|
||||
id: upload-edge-binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: ${{ env.ZIP_NAME_EDGE }}
|
||||
asset_name: ${{ env.ZIP_NAME_EDGE }}
|
||||
asset_content_type: application/zip
|
||||
|
||||
Generated
+2709
-490
File diff suppressed because it is too large
Load Diff
+25
-20
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "alfis"
|
||||
version = "0.8.6"
|
||||
version = "0.8.9"
|
||||
authors = ["Revertron <alfis@revertron.com>"]
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
@@ -11,44 +11,50 @@ exclude = ["blockchain.db", "alfis.toml"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
getopts = "0.2.21"
|
||||
log = "0.4.22"
|
||||
getopts = "0.2.24"
|
||||
log = "0.4.28"
|
||||
simplelog = "0.12.2"
|
||||
toml = "0.8.19"
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
toml = "1.0.7"
|
||||
sha2 = "0.10.9"
|
||||
ed25519-dalek = "2.2.0"
|
||||
x25519-dalek = { version = "2.0.1", features = ["reusable_secrets"] }
|
||||
ecies-ed25519-ng = { git = "https://github.com/Revertron/ecies-ed25519-ng", rev = "554ca29", version = "0.5.3" }
|
||||
chacha20poly1305 = "0.10.1"
|
||||
blakeout = "0.3.0"
|
||||
num_cpus = "1.16.0"
|
||||
num_cpus = "1.17.0"
|
||||
byteorder = "1.5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
bincode = "1.3.3"
|
||||
bincode = { version = "2.0.1", features = ["serde"] }
|
||||
serde_cbor = "0.11.2"
|
||||
num-bigint = "0.4.6"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
time = "0.3.36"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
time = "0.3.44"
|
||||
rand = { package = "rand", version = "0.8.5" }
|
||||
sqlite = "0.36.0"
|
||||
uuid = { version = "1.11.0", features = ["serde", "v4"] }
|
||||
sqlite = "0.37.0"
|
||||
uuid = { version = "1.18.1", features = ["serde", "v4"] }
|
||||
mio = { version = "1.0.0", features = ["os-poll", "net"] }
|
||||
ureq = { version = "2.10", optional = true }
|
||||
lru = "0.12"
|
||||
derive_more = { version = "1.0.0", features = ["display", "error", "from"] }
|
||||
ureq = { version = "3.1.4", optional = true }
|
||||
lru = "0.16.2"
|
||||
derive_more = { version = "2.0.1", features = ["display", "error", "from"] }
|
||||
lazy_static = "1.5.0"
|
||||
spmc = "0.3.0"
|
||||
thread-priority = "1.2.0"
|
||||
thread-priority = "3.0.0"
|
||||
crossbeam-channel = "0.5.13"
|
||||
|
||||
# Optional dependencies regulated by features
|
||||
web-view = { git = "https://github.com/Boscop/web-view", features = [], optional = true }
|
||||
wry = { version = "0.53", optional = true }
|
||||
tao = { version = "0.34", optional = true }
|
||||
tray-icon = { version = "0.21.2", optional = true }
|
||||
tinyfiledialogs = { version = "3.9.1", optional = true }
|
||||
open = { version = "5.3.0", optional = true }
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["impl-default", "wincon", "shellscalingapi"] }
|
||||
windows-service = "0.7.0"
|
||||
windows-service = "0.8.0"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
@@ -69,7 +75,6 @@ ProductName="ALFIS"
|
||||
FileDescription="Alternative Free Identity System"
|
||||
|
||||
[features]
|
||||
webgui = ["web-view", "tinyfiledialogs", "open"]
|
||||
edge = ["webgui", "web-view/edge"]
|
||||
webgui = ["wry", "tao", "tray-icon", "tinyfiledialogs", "open"]
|
||||
doh = ["ureq"]
|
||||
default = ["webgui", "doh"]
|
||||
|
||||
@@ -8,9 +8,9 @@ This project represents a minimal blockchain without cryptocurrency, capable of
|
||||
Not so clear? Hold on.
|
||||
|
||||
## This software provides:
|
||||
- Very small and [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) synchronized database of domain names.
|
||||
The consistency of this database is based on [blockchain](https://en.wikipedia.org/wiki/Blockchain) technology, that prevents retroactive changing of data, and has strict cryptographical consensus.
|
||||
- DNS server with cache, like you have in your Internet-router. It resolves the domains from database and forwards all regular DNS-requests to some other resolver - your router, Google DNS, Cloudflare DNS, or [AdGuard DNS](https://dns.adguard.com/) (if you want to block ads and trackers).
|
||||
- Tiny and [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) synchronized database of domain names.
|
||||
The consistency of this database is based on [blockchain](https://en.wikipedia.org/wiki/Blockchain) technology that prevents retroactive changing of data, and has strict cryptographical consensus.
|
||||
- DNS server with cache and enhanced security features. It resolves the domains from database and forwards all regular DNS-requests to some other resolver – your router, Google DNS, Cloudflare DNS, or [AdGuard DNS](https://dns.adguard.com/) (if you want to block ads and trackers).
|
||||
- Other systems need you to organize and run several DNS-servers to resolve their domains and regular domains, we have both in one.
|
||||
Moreover, ALFIS can forward requests of regular domains to [DNS-over-HTTPS](https://en.wikipedia.org/wiki/DNS_over_HTTPS) server. The security and privacy is right here.
|
||||
- Convenient graphical user interface to create domains in this alternative domain system. If you want just to use it like a DNS-server you can run it with `-n` flag or just build/download the variant without GUI.
|
||||
@@ -22,7 +22,7 @@ Moreover, ALFIS can forward requests of regular domains to [DNS-over-HTTPS](http
|
||||
|
||||

|
||||
|
||||
## How it works?
|
||||
## How does it work?
|
||||
Every node connects to its siblings and synchronizes the domain database.
|
||||
This DB consists of cryptographically bound blocks, that contain encrypted domain names, contacts, and some info, if you wish.
|
||||
There are 10 domain zones available to get domain in:
|
||||
@@ -46,7 +46,7 @@ You don't need any additional steps to build Alfis, just stick to the MSVC versi
|
||||
|
||||
If you see an error about missing `VCRUNTIME140.dll` when running alfis you will need to install [VC Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) from Microsoft.
|
||||
|
||||
If you want to use modern browser engine from Edge instead of old from IE, you need to build with this command: `cargo build --release --features "edge"` (or use corresponding build from [releases](https://github.com/Revertron/Alfis/releases)).
|
||||
The GUI version uses WebView2 (Edge-based rendering engine), which is included by default on Windows 10/11. If you're on an older system, you may need to install [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/).
|
||||
|
||||
###  On Windows (MINGW64)
|
||||
If you'd rather use Gnu version of Rust you can build Alfis by these steps:
|
||||
@@ -58,8 +58,8 @@ cargo build
|
||||
```
|
||||
|
||||
###  On Linux
|
||||
If you are building on Linux you must ensure that you have `libwebkitgtk` library installed.
|
||||
You can do it by issuing this command: `sudo apt install libwebkit2gtk-4.0-dev` (on Debian/Ubuntu and derivatives).
|
||||
If you are building on Linux, you must ensure that you have `libwebkitgtk` and `libxdo` libraries installed (for UI and tray icon respectively).
|
||||
You can do it by issuing this command: `sudo apt install libwebkit2gtk-4.1-dev libxdo-dev` (on Debian/Ubuntu and derivatives).
|
||||
|
||||
####  On Arch Linux
|
||||
|
||||
@@ -93,7 +93,7 @@ Beware of NetworkManager, it can change your resolvers at will.
|
||||
gpg --fetch-keys https://deb.revertron.com/key.txt
|
||||
gpg --export F244E16645D86D62 | sudo tee /usr/local/apt-keys/alfis.gpg > /dev/null
|
||||
```
|
||||
2. Add repository path to sources list
|
||||
2. Add a repository path to sources list
|
||||
```
|
||||
echo 'deb [signed-by=/usr/local/apt-keys/alfis.gpg] https://deb.revertron.com/ debian alfis' | sudo tee /etc/apt/sources.list.d/alfis.list
|
||||
```
|
||||
@@ -133,18 +133,18 @@ docker run --rm --name alfis -p 53:53/tcp -p 53:53/udp cofob/alfis
|
||||
|
||||
### GUI version Windows/Linux/macOS (if you want to create and change domains)
|
||||
If you want to create and manage your own domains on blockchain, you will need a version with GUI.
|
||||
You can download it from [releases](https://github.com/Revertron/Alfis/releases) section, choose appropriate OS and architecture version.
|
||||
You can download it from [releases](https://github.com/Revertron/Alfis/releases) section, choose the appropriate OS and architecture version.
|
||||
It needs to be without `nogui` suffix.
|
||||
|
||||
Just unzip that archive in some directory and run `alfis` (or `alfis.exe`) binary.
|
||||
By default, it searches for config file, named `alfis.toml` in current working directory, and creates/changes `blockchain.db` file in the same directory.
|
||||
By default, it searches for a config file, named `alfis.toml` in current working directory, and creates/changes `blockchain.db` file in the same directory.
|
||||
If you want it to load config from another file you can command it so: `alfis -c /etc/alfis.conf`.
|
||||
|
||||
## Roadmap
|
||||
1. Stabilize blockchain functions (domain transfer, info & contacts in UI), bug hunting and fixing.
|
||||
2. Change DNS server/proxy to own resource saving implementation (using trust-dns-proto for RR parsing).
|
||||
1. Stabilize blockchain functions (domain transfer, info & contacts in UI), bug hunting and fixing. ✅
|
||||
2. ~~Change DNS server/proxy to own resource saving implementation (using trust-dns-proto for RR parsing).~~
|
||||
3. P2P traffic encryption (ECDH). ✅
|
||||
4. Web-GUI to manage you node from browser.
|
||||
4. ~~Web-GUI to manage your node from browser.~~
|
||||
|
||||
## Remarkable contributions
|
||||
* [@umasterov](https://github.com/umasterov) contributed fantastic logo for this project.
|
||||
|
||||
+464
-690
File diff suppressed because it is too large
Load Diff
+15
-4
@@ -1,4 +1,4 @@
|
||||
# The hash of first block in a chain to know with which nodes to work
|
||||
# The hash of the first block in a chain to know with which nodes to work
|
||||
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
|
||||
# Paths to your key files to load automatically
|
||||
key_files = ["key1.toml", "key2.toml", "key3.toml", "key4.toml", "key5.toml"]
|
||||
@@ -10,7 +10,7 @@ check_blocks = 8
|
||||
# All bootstrap nodes
|
||||
peers = ["peer-v4.alfis.name:4244", "peer-v6.alfis.name:4244", "peer-ygg.alfis.name:4244"]
|
||||
# Your node will listen on that address for other nodes to connect
|
||||
listen = "[::]:42440"
|
||||
listen = "[::]:4244"
|
||||
# Set true if you want your IP to participate in peer-exchange, or false otherwise
|
||||
public = true
|
||||
# Allow connections to/from Yggdrasil only (https://yggdrasil-network.github.io)
|
||||
@@ -19,9 +19,15 @@ yggdrasil_only = false
|
||||
# DNS resolver options
|
||||
[dns]
|
||||
# Your DNS resolver will be listening on this address and port (Usual port is 53)
|
||||
listen = "127.0.0.1:5311"
|
||||
listen = "127.0.0.3:53"
|
||||
# How many threads to spawn by DNS server
|
||||
threads = 10
|
||||
|
||||
# DNS cache memory limit in megabytes (default: 100)
|
||||
# Prevents unbounded cache growth in high-load environments
|
||||
# Set to 0 for unlimited cache (not recommended for production)
|
||||
cache_memory_limit_mb = 100
|
||||
|
||||
# AdGuard DNS servers to filter ads and trackers
|
||||
forwarders = ["https://dns.adguard.com/dns-query"]
|
||||
#forwarders = ["94.140.14.14:53", "94.140.15.15:53"]
|
||||
@@ -32,6 +38,11 @@ forwarders = ["https://dns.adguard.com/dns-query"]
|
||||
# Bootstrap DNS-servers to resolve domains of DoH providers
|
||||
bootstraps = ["9.9.9.9:53", "94.140.14.14:53"]
|
||||
|
||||
# Enable DNS 0x20 encoding for cache poisoning protection
|
||||
# Recommended: true (default)
|
||||
# Set false only if upstream resolvers don't preserve case (very rare)
|
||||
enable_0x20 = true
|
||||
|
||||
# Hosts file support (resolve local names or block ads)
|
||||
#hosts = ["system", "adblock.txt"]
|
||||
|
||||
@@ -39,5 +50,5 @@ bootstraps = ["9.9.9.9:53", "94.140.14.14:53"]
|
||||
[mining]
|
||||
# How many CPU threads to spawn for mining, zero = number of CPU cores
|
||||
threads = 0
|
||||
# Set lower priority for mining threads
|
||||
# Set a lower priority for mining threads
|
||||
lower = true
|
||||
@@ -35,10 +35,10 @@ elif [ $PKGARCH = "i686" ]; then TARGET='i686-unknown-linux-musl'
|
||||
elif [ $PKGARCH = "mipsel" ]; then TARGET='mipsel-unknown-linux-musl'
|
||||
elif [ $PKGARCH = "mips" ]; then TARGET='mips-unknown-linux-musl'
|
||||
elif [ $PKGARCH = "armhf" ]; then TARGET='armv7-unknown-linux-musleabihf'
|
||||
elif [ $PKGARCH = "armlf" ]; then TARGET='arm-unknown-linux-musleabi'
|
||||
elif [ $PKGARCH = "armel" ]; then TARGET='arm-unknown-linux-musleabi'
|
||||
elif [ $PKGARCH = "arm64" ]; then TARGET='aarch64-unknown-linux-musl'
|
||||
else
|
||||
echo "Specify PKGARCH=amd64,i686,mips,mipsel,armhf,armlf,arm64"
|
||||
echo "Specify PKGARCH=amd64,i686,mips,mipsel,armhf,armel,arm64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@ extern crate serde_json;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use bincode::config;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blockchain::hash_utils::{hash_difficulty, key_hash_difficulty};
|
||||
@@ -94,7 +94,7 @@ impl Block {
|
||||
|
||||
/// Serializes block to bincode format for hashing.
|
||||
pub fn as_bytes_compact(&self) -> Vec<u8> {
|
||||
bincode::serialize(&self).unwrap()
|
||||
bincode::serde::encode_to_vec(&self, config::legacy()).unwrap()
|
||||
}
|
||||
|
||||
/// Checks if this block is superior to the other
|
||||
|
||||
+146
-24
@@ -1,10 +1,12 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace, warn};
|
||||
|
||||
use crate::blockchain::transaction::DomainData;
|
||||
use crate::commons::rtt_tracker::RttTracker;
|
||||
use crate::dns::filter::DnsFilter;
|
||||
use crate::dns::protocol::{DnsPacket, DnsQuestion, DnsRecord, QueryType, ResultCode, TransientTtl};
|
||||
use crate::Context;
|
||||
@@ -14,12 +16,16 @@ const NAME_SERVER: &str = "ns.alfis.name";
|
||||
const SERVER_ADMIN: &str = "admin.alfis.name";
|
||||
|
||||
pub struct BlockchainFilter {
|
||||
context: Arc<Mutex<Context>>
|
||||
context: Arc<Mutex<Context>>,
|
||||
ns_tracker: Arc<RttTracker<IpAddr>>,
|
||||
}
|
||||
|
||||
impl BlockchainFilter {
|
||||
pub fn new(context: Arc<Mutex<Context>>) -> Self {
|
||||
BlockchainFilter { context }
|
||||
BlockchainFilter {
|
||||
context,
|
||||
ns_tracker: Arc::new(RttTracker::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_soa_record(zone: String, serial: u32, packet: &mut DnsPacket) {
|
||||
@@ -44,36 +50,53 @@ impl BlockchainFilter {
|
||||
have_zone
|
||||
}
|
||||
|
||||
fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &Vec<IpAddr>) -> Option<DnsPacket> {
|
||||
let port = 10000 + (rand::random::<u16>() % 50000);
|
||||
let mut dns_client = DnsNetworkClient::new(port);
|
||||
fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &[IpAddr], tracker: &RttTracker<IpAddr>) -> Option<DnsPacket> {
|
||||
let mut dns_client = DnsNetworkClient::new();
|
||||
dns_client.run().unwrap();
|
||||
let timeout = std::time::Duration::from_secs(2);
|
||||
|
||||
for server in servers {
|
||||
let addr = SocketAddr::new(server.to_owned(), 53);
|
||||
if let Ok(res) = dns_client.send_udp_query(qname, qtype, addr, false) {
|
||||
dns_client.stop();
|
||||
return Some(res);
|
||||
let ordered = tracker.select_ordered(servers);
|
||||
|
||||
for server in &ordered {
|
||||
let addr = SocketAddr::new(*server, 53);
|
||||
let start = Instant::now();
|
||||
match dns_client.send_udp_query(qname, qtype, addr, false, timeout) {
|
||||
Ok(res) => {
|
||||
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
||||
tracker.record_success(server, elapsed);
|
||||
dns_client.stop();
|
||||
return Some(res);
|
||||
}
|
||||
Err(_) => {
|
||||
tracker.record_failure(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
dns_client.stop();
|
||||
None
|
||||
}
|
||||
|
||||
fn create_packet(&self, qname: &str, qtype: QueryType, zone: String, answers: Vec<DnsRecord>) -> Option<DnsPacket> {
|
||||
fn create_packet(&self, qname: &str, qtype: QueryType, zone: String, answers: Vec<DnsRecord>, ns_records: Vec<DnsRecord>, glue_records: Vec<DnsRecord>) -> Option<DnsPacket> {
|
||||
if !answers.is_empty() {
|
||||
// Create DnsPacket
|
||||
// Create DnsPacket with answers
|
||||
let mut packet = DnsPacket::new();
|
||||
packet.header.authoritative_answer = true;
|
||||
packet.questions.push(DnsQuestion::new(String::from(qname), qtype));
|
||||
for answer in answers {
|
||||
packet.answers.push(answer);
|
||||
}
|
||||
packet.authorities.push(DnsRecord::NS { domain: zone, host: String::from(NAME_SERVER), ttl: TransientTtl(600) });
|
||||
// Add NS records to authority section
|
||||
for ns_record in ns_records {
|
||||
packet.authorities.push(ns_record);
|
||||
}
|
||||
// Add GLUE records to additional section (resources)
|
||||
for glue_record in glue_records {
|
||||
packet.resources.push(glue_record);
|
||||
}
|
||||
//trace!("Returning packet: {:?}", &packet);
|
||||
Some(packet)
|
||||
} else {
|
||||
// Create DnsPacket
|
||||
// Create DnsPacket without answers
|
||||
let mut packet = DnsPacket::new();
|
||||
packet.header.authoritative_answer = true;
|
||||
packet.header.rescode = ResultCode::NXDOMAIN;
|
||||
@@ -85,7 +108,7 @@ impl BlockchainFilter {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData) -> (bool, Option<DnsPacket>) {
|
||||
fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData, recursive: bool, tracker: &RttTracker<IpAddr>) -> (bool, Option<DnsPacket>) {
|
||||
// First we search for NS records, collecting nameserver domains
|
||||
let mut hosts = Vec::new();
|
||||
for record in data.records.iter() {
|
||||
@@ -103,7 +126,27 @@ impl BlockchainFilter {
|
||||
return (false, None);
|
||||
}
|
||||
|
||||
// Searching glue records
|
||||
// If non-recursive, return a referral response with NS and GLUE records
|
||||
if !recursive {
|
||||
trace!("Non-recursive query for delegated domain {}, returning referral", qname);
|
||||
let ns_records = BlockchainFilter::get_ns_records(data, top_domain);
|
||||
let glue_records = BlockchainFilter::get_glue_records(data, top_domain, &hosts);
|
||||
|
||||
let mut packet = DnsPacket::new();
|
||||
packet.header.authoritative_answer = false; // Not authoritative for the answer, but for the zone
|
||||
packet.questions.push(DnsQuestion::new(String::from(qname), qtype));
|
||||
// Add NS records to authority section
|
||||
for ns_record in ns_records {
|
||||
packet.authorities.push(ns_record);
|
||||
}
|
||||
// Add GLUE records to additional section (resources)
|
||||
for glue_record in glue_records {
|
||||
packet.resources.push(glue_record);
|
||||
}
|
||||
return (true, Some(packet));
|
||||
}
|
||||
|
||||
// For recursive queries, search for glue records to query external servers
|
||||
let mut servers = Vec::new();
|
||||
for record in data.records.iter() {
|
||||
match &record {
|
||||
@@ -129,7 +172,7 @@ impl BlockchainFilter {
|
||||
|
||||
if !servers.is_empty() {
|
||||
trace!("Found NS servers for domain {}: {:?}", &qname, &servers);
|
||||
let answer = BlockchainFilter::lookup_from_ns(qname, qtype, &servers);
|
||||
let answer = BlockchainFilter::lookup_from_ns(qname, qtype, &servers, tracker);
|
||||
if let Some(packet) = &answer {
|
||||
trace!("Resolved {:?} from NS: {:?}", (qname, qtype), &packet.answers);
|
||||
}
|
||||
@@ -138,13 +181,76 @@ impl BlockchainFilter {
|
||||
|
||||
(false, None)
|
||||
}
|
||||
|
||||
/// Extract NS records from domain data and return them
|
||||
fn get_ns_records(data: &DomainData, top_domain: &str) -> Vec<DnsRecord> {
|
||||
data.records.iter()
|
||||
.filter_map(|record| {
|
||||
if let DnsRecord::NS { domain, host, ttl } = record {
|
||||
if domain == "@" {
|
||||
return Some(DnsRecord::NS {
|
||||
domain: String::from(top_domain),
|
||||
host: host.clone(),
|
||||
ttl: *ttl
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract GLUE records (A/AAAA records for NS hosts within the same domain)
|
||||
fn get_glue_records(data: &DomainData, top_domain: &str, ns_hosts: &[String]) -> Vec<DnsRecord> {
|
||||
let mut glue_records = Vec::new();
|
||||
|
||||
for record in data.records.iter() {
|
||||
match record {
|
||||
DnsRecord::A { domain, addr, ttl } => {
|
||||
let full_domain = if domain == "@" {
|
||||
String::from(top_domain)
|
||||
} else {
|
||||
format!("{}.{}", domain, top_domain)
|
||||
};
|
||||
|
||||
if ns_hosts.iter().any(|ns| ns == &full_domain) {
|
||||
glue_records.push(DnsRecord::A {
|
||||
domain: full_domain,
|
||||
addr: addr.clone(),
|
||||
ttl: *ttl
|
||||
});
|
||||
}
|
||||
}
|
||||
DnsRecord::AAAA { domain, addr, ttl } => {
|
||||
let full_domain = if domain == "@" {
|
||||
String::from(top_domain)
|
||||
} else {
|
||||
format!("{}.{}", domain, top_domain)
|
||||
};
|
||||
|
||||
if ns_hosts.iter().any(|ns| ns == &full_domain) {
|
||||
glue_records.push(DnsRecord::AAAA {
|
||||
domain: full_domain,
|
||||
addr: addr.clone(),
|
||||
ttl: *ttl
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
glue_records
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsFilter for BlockchainFilter {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket> {
|
||||
// Lowercase for case-insensitive lookup (blockchain stores domains as lowercase)
|
||||
let qname_lower = qname.to_lowercase();
|
||||
let top_domain;
|
||||
let subdomain;
|
||||
let parts: Vec<&str> = qname.rsplitn(3, '.').collect();
|
||||
let parts: Vec<&str> = qname_lower.rsplitn(3, '.').collect();
|
||||
match parts.len() {
|
||||
1 => {
|
||||
let mut packet = DnsPacket::new();
|
||||
@@ -192,9 +298,12 @@ impl DnsFilter for BlockchainFilter {
|
||||
};
|
||||
|
||||
// Check if this domain has NS records and needs to resolve all records through them
|
||||
let (has_ns, result) = Self::resolve_by_ns(qname, qtype, &top_domain, &data);
|
||||
if has_ns {
|
||||
return result;
|
||||
// But skip this if we're querying for NS records themselves - return them directly
|
||||
if qtype != QueryType::NS {
|
||||
let (has_ns, result) = Self::resolve_by_ns(qname, qtype, &top_domain, &data, recursive, &self.ns_tracker);
|
||||
if has_ns {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
let mut answers: Vec<DnsRecord> = Vec::new();
|
||||
@@ -237,7 +346,7 @@ impl DnsFilter for BlockchainFilter {
|
||||
let mut domain_exists = !answers.is_empty() || subdomain.is_empty();
|
||||
if answers.is_empty() {
|
||||
// If there are no records found we search for *.domain.tld record
|
||||
for mut record in data.records {
|
||||
for mut record in data.records.iter_mut() {
|
||||
let record_domain = record.get_domain().unwrap_or(String::new());
|
||||
if record.get_querytype() == qtype && record_domain == "*" {
|
||||
match &mut record {
|
||||
@@ -263,7 +372,20 @@ impl DnsFilter for BlockchainFilter {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut packet) = self.create_packet(qname, qtype, zone, answers) {
|
||||
// Extract NS records and GLUE records for the response
|
||||
let ns_records = BlockchainFilter::get_ns_records(&data, &top_domain);
|
||||
let ns_hosts: Vec<String> = ns_records.iter()
|
||||
.filter_map(|record| {
|
||||
if let DnsRecord::NS { host, .. } = record {
|
||||
Some(host.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let glue_records = BlockchainFilter::get_glue_records(&data, &top_domain, &ns_hosts);
|
||||
|
||||
if let Some(mut packet) = self.create_packet(qname, qtype, zone, answers, ns_records, glue_records) {
|
||||
if domain_exists && packet.answers.is_empty() {
|
||||
packet.header.rescode = ResultCode::NOERROR;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub const UI_REFRESH_DELAY_MS: u128 = 500;
|
||||
pub const LOG_REFRESH_DELAY_SEC: u64 = 60;
|
||||
|
||||
pub const POLL_TIMEOUT: Option<Duration> = Some(Duration::from_millis(200));
|
||||
pub const WAIT_FOR_INTERNET: Duration = Duration::from_secs(10);
|
||||
pub const WAIT_FOR_INTERNET: Duration = Duration::from_secs(5);
|
||||
/// We start syncing blocks only when we got 4 and more connected nodes
|
||||
pub const MIN_CONNECTED_NODES_START_SYNC: usize = 4;
|
||||
pub const MAX_READ_BLOCK_TIME: u128 = 100;
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::dns::protocol::DnsRecord;
|
||||
|
||||
pub mod constants;
|
||||
pub mod eventbus;
|
||||
pub mod rtt_tracker;
|
||||
pub mod simplebus;
|
||||
|
||||
/// Convert bytes array to HEX format
|
||||
@@ -128,6 +129,7 @@ pub fn is_yggdrasil_record(record: &DnsRecord) -> bool {
|
||||
DnsRecord::SRV { .. } => {}
|
||||
DnsRecord::OPT { .. } => {}
|
||||
DnsRecord::TLSA { .. } => {}
|
||||
DnsRecord::HTTPS { .. } => {}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
/// Unbound-style RTT band width in milliseconds.
|
||||
/// Servers within min_rtt + BAND are considered equally good.
|
||||
const RTT_BAND_MS: f64 = 100.0;
|
||||
/// EWMA smoothing factor: 87.5% history, 12.5% new measurement.
|
||||
const EWMA_WEIGHT: f64 = 7.0 / 8.0;
|
||||
/// Penalty RTT assigned on timeout/failure (ms).
|
||||
const TIMEOUT_PENALTY_MS: f64 = 5000.0;
|
||||
/// Stats older than this are expired so the server gets re-probed.
|
||||
const STATS_EXPIRE_SECS: u64 = 900;
|
||||
|
||||
struct RttStats {
|
||||
rtt: f64,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
/// Adaptive server selection using Unbound-style RTT banding.
|
||||
///
|
||||
/// Tracks smoothed RTT per key and selects servers by grouping them into
|
||||
/// a "preferred" band (within `RTT_BAND_MS` of the fastest known server)
|
||||
/// and a "fallback" group. Unknown or expired servers are treated as
|
||||
/// preferred so they get probed.
|
||||
pub struct RttTracker<K: Eq + Hash + Clone> {
|
||||
stats: Mutex<HashMap<K, RttStats>>,
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash + Clone> RttTracker<K> {
|
||||
pub fn new() -> Self {
|
||||
RttTracker {
|
||||
stats: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `keys` reordered for adaptive selection.
|
||||
///
|
||||
/// - Keys with no stats or expired stats go to the preferred group (to be probed).
|
||||
/// - Known keys within `min_rtt + RTT_BAND_MS` go to the preferred group.
|
||||
/// - The rest are fallback.
|
||||
/// - Each group is shuffled; preferred comes first.
|
||||
pub fn select_ordered(&self, keys: &[K]) -> Vec<K> {
|
||||
let now = Instant::now();
|
||||
let stats = self.stats.lock().unwrap();
|
||||
|
||||
let mut known: Vec<(K, f64)> = Vec::new();
|
||||
let mut unknown: Vec<K> = Vec::new();
|
||||
for key in keys {
|
||||
match stats.get(key) {
|
||||
Some(s) if now.duration_since(s.last_update).as_secs() < STATS_EXPIRE_SECS => {
|
||||
known.push((key.clone(), s.rtt));
|
||||
}
|
||||
_ => {
|
||||
unknown.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(stats);
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
if known.is_empty() {
|
||||
unknown.shuffle(&mut rng);
|
||||
return unknown;
|
||||
}
|
||||
|
||||
let min_rtt = known.iter().map(|(_, rtt)| *rtt).fold(f64::INFINITY, f64::min);
|
||||
let band_threshold = min_rtt + RTT_BAND_MS;
|
||||
|
||||
let mut preferred: Vec<K> = Vec::new();
|
||||
let mut fallback: Vec<K> = Vec::new();
|
||||
for (key, rtt) in known {
|
||||
if rtt <= band_threshold {
|
||||
preferred.push(key);
|
||||
} else {
|
||||
fallback.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
preferred.extend(unknown);
|
||||
preferred.shuffle(&mut rng);
|
||||
fallback.shuffle(&mut rng);
|
||||
preferred.extend(fallback);
|
||||
preferred
|
||||
}
|
||||
|
||||
/// Record a successful query with the measured RTT in milliseconds.
|
||||
pub fn record_success(&self, key: &K, rtt_ms: f64) {
|
||||
self.update(key, rtt_ms);
|
||||
}
|
||||
|
||||
/// Record a failed/timed-out query, applying a penalty RTT.
|
||||
pub fn record_failure(&self, key: &K) {
|
||||
self.update(key, TIMEOUT_PENALTY_MS);
|
||||
}
|
||||
|
||||
fn update(&self, key: &K, rtt_ms: f64) {
|
||||
let mut stats = self.stats.lock().unwrap();
|
||||
let entry = stats.entry(key.clone()).or_insert(RttStats {
|
||||
rtt: rtt_ms,
|
||||
last_update: Instant::now(),
|
||||
});
|
||||
entry.rtt = entry.rtt * EWMA_WEIGHT + rtt_ms * (1.0 - EWMA_WEIGHT);
|
||||
entry.last_update = Instant::now();
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,14 @@ pub struct Chacha {
|
||||
|
||||
impl Chacha {
|
||||
pub fn new(key: &[u8], nonce: &[u8]) -> Self {
|
||||
let key = Key::from_slice(key);
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
let nonce = Nonce::clone_from_slice(nonce);
|
||||
// Convert slices to fixed-size arrays, then to GenericArray
|
||||
let key_array: [u8; 32] = key.try_into().expect("Key must be 32 bytes");
|
||||
let key = Key::from(key_array);
|
||||
let cipher = ChaCha20Poly1305::new(&key);
|
||||
|
||||
let nonce_array: [u8; 12] = nonce.try_into().expect("Nonce must be 12 bytes");
|
||||
let nonce = Nonce::from(nonce_array);
|
||||
|
||||
Chacha { cipher, nonce }
|
||||
}
|
||||
|
||||
@@ -30,7 +35,7 @@ impl Chacha {
|
||||
}
|
||||
|
||||
pub fn get_nonce(&self) -> &[u8] {
|
||||
&self.nonce.as_slice()
|
||||
self.nonce.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -136,7 +136,7 @@ pub trait PacketBuffer {
|
||||
outstr.push_str(delim);
|
||||
|
||||
let str_buffer = self.get_range(pos, len as usize)?;
|
||||
outstr.push_str(&String::from_utf8_lossy(str_buffer).to_lowercase());
|
||||
outstr.push_str(&String::from_utf8_lossy(str_buffer));
|
||||
|
||||
delim = ".";
|
||||
|
||||
@@ -226,7 +226,7 @@ where T: Read {
|
||||
}
|
||||
|
||||
impl<'a, T> StreamPacketBuffer<'a, T> where T: Read + 'a {
|
||||
pub fn new(stream: &'a mut T) -> StreamPacketBuffer<'_, T> {
|
||||
pub fn new(stream: &'a mut T) -> StreamPacketBuffer<'a, T> {
|
||||
StreamPacketBuffer {
|
||||
stream,
|
||||
buffer: Vec::new(),
|
||||
@@ -300,14 +300,16 @@ impl<'a, T> PacketBuffer for StreamPacketBuffer<'a, T> where T: Read + 'a {
|
||||
}
|
||||
}
|
||||
|
||||
const BUF_SIZE: usize = 4096;
|
||||
|
||||
pub struct BytePacketBuffer {
|
||||
pub buf: [u8; 512],
|
||||
pub buf: [u8; BUF_SIZE],
|
||||
pub pos: usize
|
||||
}
|
||||
|
||||
impl BytePacketBuffer {
|
||||
pub fn new() -> BytePacketBuffer {
|
||||
BytePacketBuffer { buf: [0; 512], pos: 0 }
|
||||
BytePacketBuffer { buf: [0; BUF_SIZE], pos: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +321,7 @@ impl Default for BytePacketBuffer {
|
||||
|
||||
impl PacketBuffer for BytePacketBuffer {
|
||||
fn read(&mut self) -> Result<u8> {
|
||||
if self.pos >= 512 {
|
||||
if self.pos >= BUF_SIZE {
|
||||
return Err(BufferError::EndOfBuffer);
|
||||
}
|
||||
let res = self.buf[self.pos];
|
||||
@@ -329,21 +331,21 @@ impl PacketBuffer for BytePacketBuffer {
|
||||
}
|
||||
|
||||
fn get(&mut self, pos: usize) -> Result<u8> {
|
||||
if pos >= 512 {
|
||||
if pos >= BUF_SIZE {
|
||||
return Err(BufferError::EndOfBuffer);
|
||||
}
|
||||
Ok(self.buf[pos])
|
||||
}
|
||||
|
||||
fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
|
||||
if start + len >= 512 {
|
||||
if start + len >= BUF_SIZE {
|
||||
return Err(BufferError::EndOfBuffer);
|
||||
}
|
||||
Ok(&self.buf[start..start + len as usize])
|
||||
}
|
||||
|
||||
fn write(&mut self, val: u8) -> Result<()> {
|
||||
if self.pos >= 512 {
|
||||
if self.pos >= BUF_SIZE {
|
||||
return Err(BufferError::EndOfBuffer);
|
||||
}
|
||||
self.buf[self.pos] = val;
|
||||
|
||||
+302
-18
@@ -2,16 +2,82 @@
|
||||
|
||||
extern crate serde;
|
||||
use std::clone::Clone;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use lru::LruCache;
|
||||
|
||||
use chrono::*;
|
||||
use derive_more::{Display, Error, From};
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dns::protocol::{DnsPacket, DnsRecord, QueryType, ResultCode};
|
||||
|
||||
/// Estimate the memory size of a DNS record in bytes
|
||||
fn estimate_dns_record_size(record: &DnsRecord) -> usize {
|
||||
match record {
|
||||
DnsRecord::A { domain, .. } => 56 + domain.len(),
|
||||
DnsRecord::AAAA { domain, .. } => 68 + domain.len(),
|
||||
DnsRecord::NS { domain, host, .. } |
|
||||
DnsRecord::CNAME { domain, host, .. } => 64 + domain.len() + host.len(),
|
||||
DnsRecord::MX { domain, host, .. } => 72 + domain.len() + host.len(),
|
||||
DnsRecord::SRV { domain, host, .. } => 80 + domain.len() + host.len(),
|
||||
DnsRecord::SOA { domain, m_name, r_name, .. } =>
|
||||
120 + domain.len() + m_name.len() + r_name.len(),
|
||||
DnsRecord::TXT { domain, data, .. } => 64 + domain.len() + data.len(),
|
||||
DnsRecord::PTR { domain, data, .. } => 64 + domain.len() + data.len(),
|
||||
DnsRecord::TLSA { domain, data, .. } => 80 + domain.len() + data.len(),
|
||||
DnsRecord::HTTPS { domain, target, params, .. } =>
|
||||
88 + domain.len() + target.len() + params.len(),
|
||||
DnsRecord::UNKNOWN { domain, .. } => 64 + domain.len(),
|
||||
DnsRecord::OPT { data, .. } => 48 + data.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimate the memory size of a domain entry in bytes
|
||||
fn estimate_domain_entry_size(entry: &DomainEntry) -> usize {
|
||||
let mut size = 0;
|
||||
|
||||
// Base struct sizes
|
||||
size += std::mem::size_of::<DomainEntry>(); // ~56 bytes
|
||||
size += std::mem::size_of::<Arc<DomainEntry>>(); // 16 bytes
|
||||
|
||||
// Domain string: 24 byte header + actual chars
|
||||
size += 24 + entry.domain.len();
|
||||
|
||||
// HashMap base overhead
|
||||
size += 24;
|
||||
size += entry.record_types.len() * 32; // Bucket overhead per entry
|
||||
|
||||
// Calculate size of each RecordSet
|
||||
for (_qtype, record_set) in &entry.record_types {
|
||||
size += std::mem::size_of::<QueryType>(); // 2 bytes
|
||||
|
||||
match record_set {
|
||||
RecordSet::NoRecords { .. } => {
|
||||
size += 56; // Enum variant + timestamp + ttl
|
||||
}
|
||||
RecordSet::Records { records, .. } => {
|
||||
size += 56; // Base enum variant
|
||||
size += 24; // HashSet base
|
||||
size += records.len() * 16; // Bucket overhead per record
|
||||
|
||||
// Sum up all record sizes
|
||||
for record_entry in records {
|
||||
size += estimate_dns_record_size(&record_entry.record);
|
||||
size += 32; // DateTime<Local> overhead
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, From, Error)]
|
||||
pub enum CacheError {
|
||||
Io(std::io::Error),
|
||||
@@ -132,7 +198,7 @@ impl DomainEntry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_queryresult(&self, qtype: QueryType, result_vec: &mut Vec<DnsRecord>) {
|
||||
pub fn fill_queryresult(&self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>) {
|
||||
let now = Local::now();
|
||||
|
||||
let current_set = match self.record_types.get(&qtype) {
|
||||
@@ -149,21 +215,71 @@ impl DomainEntry {
|
||||
}
|
||||
|
||||
if entry.record.get_querytype() == qtype {
|
||||
result_vec.push(entry.record.clone());
|
||||
let mut record = entry.record.clone();
|
||||
// Preserve the original query case in the response
|
||||
record.set_domain(qname.to_string());
|
||||
result_vec.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cache {
|
||||
domain_entries: BTreeMap<String, Arc<DomainEntry>>
|
||||
domain_entries: LruCache<String, Arc<DomainEntry>>,
|
||||
current_memory_bytes: usize,
|
||||
max_memory_bytes: usize
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn new() -> Cache {
|
||||
Cache { domain_entries: BTreeMap::new() }
|
||||
Cache::with_memory_limit(0)
|
||||
}
|
||||
|
||||
pub fn with_memory_limit(limit_mb: usize) -> Cache {
|
||||
let max_memory_bytes = if limit_mb == 0 {
|
||||
usize::MAX
|
||||
} else {
|
||||
limit_mb * 1024 * 1024
|
||||
};
|
||||
|
||||
// Estimate capacity: assume ~1KB per entry
|
||||
let estimated_capacity = if limit_mb == 0 {
|
||||
100_000 // Default capacity for unlimited
|
||||
} else {
|
||||
limit_mb * 1000
|
||||
};
|
||||
|
||||
Cache {
|
||||
domain_entries: LruCache::new(NonZeroUsize::new(estimated_capacity).unwrap()),
|
||||
current_memory_bytes: 0,
|
||||
max_memory_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
fn evict_to_limit(&mut self) -> usize {
|
||||
if self.max_memory_bytes == usize::MAX {
|
||||
return 0; // Unlimited
|
||||
}
|
||||
|
||||
let mut evicted = 0;
|
||||
let target_memory = (self.max_memory_bytes * 90) / 100; // Evict to 90%
|
||||
|
||||
while self.current_memory_bytes > target_memory {
|
||||
if let Some((_, entry)) = self.domain_entries.pop_lru() {
|
||||
let size = estimate_domain_entry_size(&entry);
|
||||
self.current_memory_bytes = self.current_memory_bytes.saturating_sub(size);
|
||||
evicted += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if evicted > 0 {
|
||||
info!("Evicted {} DNS cache entries (memory: {} bytes)", evicted, self.current_memory_bytes);
|
||||
}
|
||||
|
||||
evicted
|
||||
}
|
||||
|
||||
fn get_cache_state(&mut self, qname: &str, qtype: QueryType) -> CacheState {
|
||||
@@ -174,17 +290,21 @@ impl Cache {
|
||||
}
|
||||
|
||||
fn fill_queryresult(&mut self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>, increment_stats: bool) {
|
||||
if let Some(domain_entry) = self.domain_entries.get_mut(qname).and_then(Arc::get_mut) {
|
||||
// DNS is case-insensitive, so lowercase for cache lookup
|
||||
let qname_lower = qname.to_lowercase();
|
||||
if let Some(domain_entry) = self.domain_entries.get_mut(&qname_lower).and_then(Arc::get_mut) {
|
||||
if increment_stats {
|
||||
domain_entry.hits += 1
|
||||
}
|
||||
|
||||
domain_entry.fill_queryresult(qtype, result_vec);
|
||||
domain_entry.fill_queryresult(qname, qtype, result_vec);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup(&mut self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
|
||||
match self.get_cache_state(qname, qtype) {
|
||||
// DNS is case-insensitive, so lowercase for cache lookup
|
||||
let qname_lower = qname.to_lowercase();
|
||||
match self.get_cache_state(&qname_lower, qtype) {
|
||||
CacheState::PositiveCache => {
|
||||
let mut qr = DnsPacket::new();
|
||||
self.fill_queryresult(qname, qtype, &mut qr.answers, true);
|
||||
@@ -208,38 +328,90 @@ impl Cache {
|
||||
Some(x) => x,
|
||||
None => continue
|
||||
};
|
||||
// Store with a lowercase key for case-insensitive lookups
|
||||
let domain_lower = domain.to_lowercase();
|
||||
|
||||
if let Some(ref mut rs) = self.domain_entries.get_mut(&domain).and_then(Arc::get_mut) {
|
||||
// Try to update existing entry
|
||||
if let Some(ref mut rs) = self.domain_entries.get_mut(&domain_lower).and_then(Arc::get_mut) {
|
||||
let old_size = estimate_domain_entry_size(rs);
|
||||
rs.store_record(rec);
|
||||
let new_size = estimate_domain_entry_size(rs);
|
||||
|
||||
self.current_memory_bytes = self.current_memory_bytes
|
||||
.saturating_sub(old_size)
|
||||
.saturating_add(new_size);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut rs = DomainEntry::new(domain.clone());
|
||||
// Insert new entry
|
||||
let mut rs = DomainEntry::new(domain_lower.clone());
|
||||
rs.store_record(rec);
|
||||
self.domain_entries.insert(domain.clone(), Arc::new(rs));
|
||||
let entry_size = estimate_domain_entry_size(&rs);
|
||||
|
||||
// Check if eviction needed
|
||||
if self.current_memory_bytes + entry_size > self.max_memory_bytes {
|
||||
self.evict_to_limit();
|
||||
}
|
||||
|
||||
self.domain_entries.put(domain_lower, Arc::new(rs));
|
||||
self.current_memory_bytes = self.current_memory_bytes.saturating_add(entry_size);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_nxdomain(&mut self, qname: &str, qtype: QueryType, ttl: u32) {
|
||||
if let Some(ref mut rs) = self.domain_entries.get_mut(qname).and_then(Arc::get_mut) {
|
||||
// Store with lowercase key for case-insensitive lookups
|
||||
let qname_lower = qname.to_lowercase();
|
||||
|
||||
// Try to update existing entry
|
||||
if let Some(ref mut rs) = self.domain_entries.get_mut(&qname_lower).and_then(Arc::get_mut) {
|
||||
let old_size = estimate_domain_entry_size(rs);
|
||||
rs.store_nxdomain(qtype, ttl);
|
||||
let new_size = estimate_domain_entry_size(rs);
|
||||
|
||||
self.current_memory_bytes = self.current_memory_bytes
|
||||
.saturating_sub(old_size)
|
||||
.saturating_add(new_size);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rs = DomainEntry::new(qname.to_string());
|
||||
// Insert new entry
|
||||
let mut rs = DomainEntry::new(qname_lower.clone());
|
||||
rs.store_nxdomain(qtype, ttl);
|
||||
self.domain_entries.insert(qname.to_string(), Arc::new(rs));
|
||||
let entry_size = estimate_domain_entry_size(&rs);
|
||||
|
||||
// Check if eviction needed
|
||||
if self.current_memory_bytes + entry_size > self.max_memory_bytes {
|
||||
self.evict_to_limit();
|
||||
}
|
||||
|
||||
self.domain_entries.put(qname_lower, Arc::new(rs));
|
||||
self.current_memory_bytes = self.current_memory_bytes.saturating_add(entry_size);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SynchronizedCache {
|
||||
pub cache: RwLock<Cache>
|
||||
}
|
||||
|
||||
impl SynchronizedCache {
|
||||
pub fn new() -> SynchronizedCache {
|
||||
SynchronizedCache { cache: RwLock::new(Cache::new()) }
|
||||
SynchronizedCache::with_memory_limit(0)
|
||||
}
|
||||
|
||||
pub fn with_memory_limit(limit_mb: usize) -> SynchronizedCache {
|
||||
SynchronizedCache {
|
||||
cache: RwLock::new(Cache::with_memory_limit(limit_mb))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_memory_usage(&self) -> Result<usize> {
|
||||
let cache = self.cache.read().map_err(|_| CacheError::PoisonedLock)?;
|
||||
Ok(cache.current_memory_bytes)
|
||||
}
|
||||
|
||||
pub fn get_entry_count(&self) -> Result<usize> {
|
||||
let cache = self.cache.read().map_err(|_| CacheError::PoisonedLock)?;
|
||||
Ok(cache.domain_entries.len())
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<Vec<Arc<DomainEntry>>> {
|
||||
@@ -247,7 +419,7 @@ impl SynchronizedCache {
|
||||
|
||||
let mut list = Vec::new();
|
||||
|
||||
for rs in cache.domain_entries.values() {
|
||||
for (_, rs) in cache.domain_entries.iter() {
|
||||
list.push(rs.clone());
|
||||
}
|
||||
|
||||
@@ -379,4 +551,116 @@ mod tests {
|
||||
assert_eq!(1, cache.domain_entries.get(&"www.microsoft.com".to_string()).unwrap().updates);
|
||||
assert_eq!(1, cache.domain_entries.get(&"www.microsoft.com".to_string()).unwrap().hits);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_limited_cache() {
|
||||
let mut cache = Cache::with_memory_limit(1); // 1MB limit
|
||||
|
||||
// Add many records until limit is hit
|
||||
for i in 0..5000 {
|
||||
let domain = format!("test{}.com", i);
|
||||
let records = vec![DnsRecord::A {
|
||||
domain: domain.clone(),
|
||||
addr: "127.0.0.1".parse().unwrap(),
|
||||
ttl: TransientTtl(3600)
|
||||
}];
|
||||
cache.store(&records);
|
||||
}
|
||||
|
||||
// Verify memory stayed under limit (with some tolerance)
|
||||
let limit_bytes = 1024 * 1024;
|
||||
let tolerance_bytes = limit_bytes * 110 / 100; // 110% tolerance
|
||||
assert!(
|
||||
cache.current_memory_bytes <= tolerance_bytes,
|
||||
"Cache memory {} bytes exceeds limit with tolerance {} bytes",
|
||||
cache.current_memory_bytes, tolerance_bytes
|
||||
);
|
||||
|
||||
// Verify cache still works and has been evicted
|
||||
assert!(cache.domain_entries.len() < 5000, "Cache should have evicted entries");
|
||||
assert!(cache.domain_entries.len() > 0, "Cache should not be empty");
|
||||
|
||||
// Most recent entries should still be present
|
||||
assert!(cache.lookup("test4999.com", QueryType::A).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unlimited_cache() {
|
||||
let mut cache = Cache::with_memory_limit(0); // Unlimited
|
||||
|
||||
for i in 0..1000 {
|
||||
let domain = format!("test{}.com", i);
|
||||
let records = vec![DnsRecord::A {
|
||||
domain: domain.clone(),
|
||||
addr: "127.0.0.1".parse().unwrap(),
|
||||
ttl: TransientTtl(3600)
|
||||
}];
|
||||
cache.store(&records);
|
||||
}
|
||||
|
||||
// All entries should be present
|
||||
assert_eq!(cache.domain_entries.len(), 1000);
|
||||
assert_eq!(cache.max_memory_bytes, usize::MAX);
|
||||
|
||||
// Verify lookups work for all entries
|
||||
assert!(cache.lookup("test0.com", QueryType::A).is_some());
|
||||
assert!(cache.lookup("test500.com", QueryType::A).is_some());
|
||||
assert!(cache.lookup("test999.com", QueryType::A).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lru_eviction_order() {
|
||||
let mut cache = Cache::with_memory_limit(1); // Small limit to trigger eviction
|
||||
|
||||
// Add initial batch of records
|
||||
for i in 0..100 {
|
||||
cache.store(&[DnsRecord::A {
|
||||
domain: format!("domain{}.com", i),
|
||||
addr: "127.0.0.1".parse().unwrap(),
|
||||
ttl: TransientTtl(3600)
|
||||
}]);
|
||||
}
|
||||
|
||||
// Access domain50 to make it recently used
|
||||
let _ = cache.lookup("domain50.com", QueryType::A);
|
||||
|
||||
// Add more records to trigger eviction
|
||||
for i in 100..200 {
|
||||
cache.store(&[DnsRecord::A {
|
||||
domain: format!("domain{}.com", i),
|
||||
addr: "127.0.0.1".parse().unwrap(),
|
||||
ttl: TransientTtl(3600)
|
||||
}]);
|
||||
}
|
||||
|
||||
// Most recently added entries should be present
|
||||
assert!(cache.lookup("domain199.com", QueryType::A).is_some());
|
||||
|
||||
// Verify cache is respecting memory limit
|
||||
let limit_bytes = 1024 * 1024;
|
||||
let tolerance_bytes = limit_bytes * 110 / 100;
|
||||
assert!(cache.current_memory_bytes <= tolerance_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nxdomain_memory_tracking() {
|
||||
let mut cache = Cache::with_memory_limit(1); // 1MB limit
|
||||
|
||||
// Store many NXDOMAIN responses
|
||||
for i in 0..1000 {
|
||||
let domain = format!("nonexistent{}.com", i);
|
||||
cache.store_nxdomain(&domain, QueryType::A, 3600);
|
||||
}
|
||||
|
||||
// Verify memory tracking works for NXDOMAIN
|
||||
assert!(cache.current_memory_bytes > 0);
|
||||
assert!(cache.current_memory_bytes <= 1024 * 1024 * 110 / 100);
|
||||
|
||||
// Verify NXDOMAIN responses work
|
||||
if let Some(packet) = cache.lookup("nonexistent999.com", QueryType::A) {
|
||||
assert_eq!(ResultCode::NXDOMAIN, packet.header.rescode);
|
||||
} else {
|
||||
panic!("NXDOMAIN entry should be cached");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+201
-93
@@ -4,18 +4,18 @@ use std::io::Write;
|
||||
#[cfg(feature = "doh")]
|
||||
use std::io::Read;
|
||||
use std::marker::{Send, Sync};
|
||||
use std::net::{SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
|
||||
#[cfg(feature = "doh")]
|
||||
use std::net::IpAddr;
|
||||
#[cfg(feature = "doh")]
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool, AtomicU16};
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
#[cfg(feature = "doh")]
|
||||
use std::sync::RwLock;
|
||||
use std::thread::{sleep, Builder};
|
||||
use std::time::Duration as SleepDuration;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::*;
|
||||
use derive_more::{Display, Error, From};
|
||||
@@ -32,6 +32,18 @@ use crate::dns::protocol::{DnsPacket, DnsQuestion, QueryType};
|
||||
use crate::dns::protocol::DnsRecord;
|
||||
#[cfg(feature = "doh")]
|
||||
use lru::LruCache;
|
||||
#[cfg(feature = "doh")]
|
||||
use ureq::Agent;
|
||||
#[cfg(feature = "doh")]
|
||||
use ureq::config::Config;
|
||||
#[cfg(feature = "doh")]
|
||||
use ureq::http::Uri;
|
||||
#[cfg(feature = "doh")]
|
||||
use ureq::unversioned::resolver::{ArrayVec, ResolvedSocketAddrs, Resolver};
|
||||
#[cfg(feature = "doh")]
|
||||
use ureq::unversioned::transport::{DefaultConnector, NextTimeout};
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Display, From, Error)]
|
||||
pub enum ClientError {
|
||||
@@ -65,7 +77,10 @@ pub struct DnsNetworkClient {
|
||||
total_failed: AtomicUsize,
|
||||
|
||||
/// Counter for assigning packet ids
|
||||
seq: AtomicUsize,
|
||||
seq: AtomicU16,
|
||||
|
||||
/// Enable DNS 0x20 encoding for additional security
|
||||
enable_0x20: bool,
|
||||
|
||||
/// The requesting socket for IPv4
|
||||
socket_ipv4: UdpSocket,
|
||||
@@ -86,6 +101,8 @@ pub struct DnsNetworkClient {
|
||||
struct PendingQuery {
|
||||
seq: u16,
|
||||
timestamp: DateTime<Local>,
|
||||
/// The query name with 0x20 encoding applied (for validation)
|
||||
query_name: String,
|
||||
tx: Sender<Option<DnsPacket>>
|
||||
}
|
||||
|
||||
@@ -94,18 +111,43 @@ unsafe impl Send for DnsNetworkClient {}
|
||||
unsafe impl Sync for DnsNetworkClient {}
|
||||
|
||||
impl DnsNetworkClient {
|
||||
pub fn new(port: u16) -> DnsNetworkClient {
|
||||
pub fn new() -> DnsNetworkClient {
|
||||
Self::new_with_0x20(true)
|
||||
}
|
||||
|
||||
pub fn new_with_0x20(enable_0x20: bool) -> DnsNetworkClient {
|
||||
let socket_ipv4 = UdpSocket::bind("0.0.0.0:0").expect("Error binding IPv4");
|
||||
let socket_ipv6 = UdpSocket::bind("[::]:0").expect("Error binding IPv6");
|
||||
|
||||
DnsNetworkClient {
|
||||
total_sent: AtomicUsize::new(0),
|
||||
total_failed: AtomicUsize::new(0),
|
||||
seq: AtomicUsize::new(0),
|
||||
socket_ipv4: UdpSocket::bind(format!("0.0.0.0:{}", port)).expect("Error binding IPv4"),
|
||||
socket_ipv6: UdpSocket::bind(format!("[::]:{}", port + 1)).expect("Error binding IPv6"),
|
||||
seq: AtomicU16::new(rand::random::<u16>()),
|
||||
enable_0x20,
|
||||
socket_ipv4,
|
||||
socket_ipv6,
|
||||
pending_queries: Arc::new(Mutex::new(Vec::new())),
|
||||
stopped: Arc::new(AtomicBool::new(false))
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply DNS 0x20 encoding (random case) to domain name for additional entropy
|
||||
/// This helps prevent cache poisoning by adding ~10-15 bits of entropy per query
|
||||
fn apply_0x20_encoding(domain: &str) -> String {
|
||||
domain.chars().map(|c| {
|
||||
if c.is_ascii_alphabetic() {
|
||||
// Randomly uppercase or lowercase each letter
|
||||
if rand::random::<bool>() {
|
||||
c.to_ascii_uppercase()
|
||||
} else {
|
||||
c.to_ascii_lowercase()
|
||||
}
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Send a DNS query using TCP transport
|
||||
///
|
||||
/// This is much simpler than using UDP, since the kernel will take care of
|
||||
@@ -146,14 +188,21 @@ impl DnsNetworkClient {
|
||||
|
||||
/// Send a DNS query using UDP transport
|
||||
///
|
||||
/// This will construct a query packet, and fire it off to the specified server.
|
||||
/// This will construct a query packet and fire it off to the specified server.
|
||||
/// The query is sent from the callee thread, but responses are read on a
|
||||
/// worker thread, and returned to this thread through a channel. Thus this
|
||||
/// method is thread safe, and can be used from any number of threads in
|
||||
/// worker thread and returned to this thread through a channel. Thus, this
|
||||
/// method is thread-safe and can be used from any number of threads in
|
||||
/// parallel.
|
||||
pub fn send_udp_query<A: ToSocketAddrs>(&self, qname: &str, qtype: QueryType, server: A, recursive: bool) -> Result<DnsPacket> {
|
||||
pub fn send_udp_query<A: ToSocketAddrs>(&self, qname: &str, qtype: QueryType, server: A, recursive: bool, timeout: Duration) -> Result<DnsPacket> {
|
||||
let _ = self.total_sent.fetch_add(1, Ordering::Release);
|
||||
|
||||
// Apply DNS 0x20 encoding if enabled (random case for additional entropy)
|
||||
let query_name = if self.enable_0x20 {
|
||||
Self::apply_0x20_encoding(qname)
|
||||
} else {
|
||||
qname.to_string()
|
||||
};
|
||||
|
||||
// Prepare request
|
||||
let mut packet = DnsPacket::new();
|
||||
|
||||
@@ -165,18 +214,19 @@ impl DnsNetworkClient {
|
||||
packet.header.questions = 1;
|
||||
packet.header.recursion_desired = recursive;
|
||||
|
||||
packet.questions.push(DnsQuestion::new(qname.to_string(), qtype));
|
||||
packet.questions.push(DnsQuestion::new(query_name.clone(), qtype));
|
||||
|
||||
// Create a return channel, and add a `PendingQuery` to the list of lookups in progress
|
||||
// Create a return channel and add a `PendingQuery` to the list of lookups in progress
|
||||
let (tx, rx) = channel();
|
||||
{
|
||||
let mut pending_queries = self.pending_queries.lock().map_err(|_| ClientError::PoisonedLock)?;
|
||||
pending_queries.push(PendingQuery { seq: packet.header.id, timestamp: Local::now(), tx });
|
||||
pending_queries.push(PendingQuery { seq: packet.header.id, timestamp: Local::now(), query_name, tx });
|
||||
}
|
||||
|
||||
// Send query
|
||||
// Send a query
|
||||
let mut req_buffer = BytePacketBuffer::new();
|
||||
packet.write(&mut req_buffer, 512)?;
|
||||
let len = req_buffer.buf.len();
|
||||
packet.write(&mut req_buffer, len)?;
|
||||
let addr: SocketAddr = server.to_socket_addrs()?.next().expect("Wrong resolver address");
|
||||
match addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
@@ -187,8 +237,8 @@ impl DnsNetworkClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for response
|
||||
match rx.recv() {
|
||||
// Wait for response with timeout
|
||||
match rx.recv_timeout(timeout) {
|
||||
Ok(Some(qr)) => Ok(qr),
|
||||
Ok(None) => {
|
||||
let _ = self.total_failed.fetch_add(1, Ordering::Release);
|
||||
@@ -196,7 +246,7 @@ impl DnsNetworkClient {
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = self.total_failed.fetch_add(1, Ordering::Release);
|
||||
Err(ClientError::LookupFailed)
|
||||
Err(ClientError::TimeOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,16 +264,17 @@ impl DnsClient for DnsNetworkClient {
|
||||
/// The run method launches a worker thread. Unless this thread is running, no
|
||||
/// responses will ever be generated, and clients will just block indefinitely.
|
||||
fn run(&self) -> Result<()> {
|
||||
let timeout = Some(std::time::Duration::from_millis(500));
|
||||
// Start the thread for handling incoming responses
|
||||
{
|
||||
let socket_copy = self.socket_ipv4.try_clone()?;
|
||||
let _ = socket_copy.set_read_timeout(timeout);
|
||||
let _ = socket_copy.set_read_timeout(Some(Duration::from_millis(500)));
|
||||
let pending_queries_lock = self.pending_queries.clone();
|
||||
let stopped = Arc::clone(&self.stopped);
|
||||
|
||||
let match_case = self.enable_0x20;
|
||||
|
||||
Builder::new()
|
||||
.name("DnsNetworkClient-worker-thread".into())
|
||||
.name("DnsNetworkClient-worker-thread-v4".into())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if stopped.load(Ordering::SeqCst) {
|
||||
@@ -232,7 +283,9 @@ impl DnsClient for DnsNetworkClient {
|
||||
|
||||
// Read data into a buffer
|
||||
let mut res_buffer = BytePacketBuffer::new();
|
||||
match socket_copy.recv_from(&mut res_buffer.buf) {
|
||||
let recv_result = socket_copy.recv_from(&mut res_buffer.buf);
|
||||
|
||||
match recv_result {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
continue;
|
||||
@@ -248,13 +301,24 @@ impl DnsClient for DnsNetworkClient {
|
||||
}
|
||||
};
|
||||
|
||||
// Acquire a lock on the pending_queries list, and search for a
|
||||
// Acquire a lock on the pending_queries list and search for a
|
||||
// matching PendingQuery to which to deliver the response.
|
||||
if let Ok(mut pending_queries) = pending_queries_lock.lock() {
|
||||
let mut matched_query = None;
|
||||
for (i, pending_query) in pending_queries.iter().enumerate() {
|
||||
if pending_query.seq == packet.header.id {
|
||||
// Matching query found, send the response
|
||||
// Validate 0x20 encoding - response must match query case exactly
|
||||
if !packet.questions.is_empty() {
|
||||
let response_name = &packet.questions[0].name;
|
||||
if (match_case && response_name != &pending_query.query_name)
|
||||
|| (pending_query.query_name.to_lowercase() != response_name.to_lowercase()) {
|
||||
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
|
||||
pending_query.query_name, response_name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Matching query found with correct case, send the response
|
||||
let _ = pending_query.tx.send(Some(packet.clone()));
|
||||
|
||||
// Mark this index for removal from list
|
||||
@@ -267,7 +331,7 @@ impl DnsClient for DnsNetworkClient {
|
||||
if let Some(idx) = matched_query {
|
||||
pending_queries.remove(idx);
|
||||
} else {
|
||||
println!("Discarding response for: {:?}", packet.questions[0]);
|
||||
trace!("Discarding unsolicited response for: {:?}", packet.questions.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,12 +341,14 @@ impl DnsClient for DnsNetworkClient {
|
||||
// Start the same thread for IPv6
|
||||
{
|
||||
let socket_copy = self.socket_ipv6.try_clone()?;
|
||||
let _ = socket_copy.set_read_timeout(timeout);
|
||||
let _ = socket_copy.set_read_timeout(Some(Duration::from_millis(500)));
|
||||
let pending_queries_lock = self.pending_queries.clone();
|
||||
let stopped = Arc::clone(&self.stopped);
|
||||
|
||||
let match_case = self.enable_0x20;
|
||||
|
||||
Builder::new()
|
||||
.name("DnsNetworkClient-worker-thread".into())
|
||||
.name("DnsNetworkClient-worker-thread-v6".into())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if stopped.load(Ordering::SeqCst) {
|
||||
@@ -291,7 +357,9 @@ impl DnsClient for DnsNetworkClient {
|
||||
|
||||
// Read data into a buffer
|
||||
let mut res_buffer = BytePacketBuffer::new();
|
||||
match socket_copy.recv_from(&mut res_buffer.buf) {
|
||||
let recv_result = socket_copy.recv_from(&mut res_buffer.buf);
|
||||
|
||||
match recv_result {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
continue;
|
||||
@@ -313,7 +381,18 @@ impl DnsClient for DnsNetworkClient {
|
||||
let mut matched_query = None;
|
||||
for (i, pending_query) in pending_queries.iter().enumerate() {
|
||||
if pending_query.seq == packet.header.id {
|
||||
// Matching query found, send the response
|
||||
// Validate 0x20 encoding - response must match query case exactly
|
||||
if !packet.questions.is_empty() {
|
||||
let response_name = &packet.questions[0].name;
|
||||
if (match_case && response_name != &pending_query.query_name)
|
||||
|| (pending_query.query_name.to_lowercase() != response_name.to_lowercase()) {
|
||||
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
|
||||
pending_query.query_name, response_name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Matching query found with correct case, send the response
|
||||
let _ = pending_query.tx.send(Some(packet.clone()));
|
||||
|
||||
// Mark this index for removal from list
|
||||
@@ -326,7 +405,7 @@ impl DnsClient for DnsNetworkClient {
|
||||
if let Some(idx) = matched_query {
|
||||
pending_queries.remove(idx);
|
||||
} else {
|
||||
println!("Discarding response for: {:?}", packet.questions[0]);
|
||||
trace!("Discarding unsolicited response for: {:?}", packet.questions.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,7 +420,6 @@ impl DnsClient for DnsNetworkClient {
|
||||
Builder::new()
|
||||
.name("DnsNetworkClient-timeout-thread".into())
|
||||
.spawn(move || {
|
||||
let timeout = Duration::seconds(5);
|
||||
loop {
|
||||
if stopped.load(Ordering::SeqCst) {
|
||||
break;
|
||||
@@ -349,7 +427,7 @@ impl DnsClient for DnsNetworkClient {
|
||||
if let Ok(mut pending_queries) = pending_queries_lock.lock() {
|
||||
let mut finished_queries = Vec::new();
|
||||
for (i, pending_query) in pending_queries.iter().enumerate() {
|
||||
let expires = pending_query.timestamp + timeout;
|
||||
let expires = pending_query.timestamp + DEFAULT_TIMEOUT;
|
||||
if expires < Local::now() {
|
||||
let _ = pending_query.tx.send(None);
|
||||
finished_queries.push(i);
|
||||
@@ -362,7 +440,7 @@ impl DnsClient for DnsNetworkClient {
|
||||
}
|
||||
}
|
||||
|
||||
sleep(SleepDuration::from_millis(100));
|
||||
sleep(Duration::from_millis(100));
|
||||
}
|
||||
})?;
|
||||
}
|
||||
@@ -375,7 +453,7 @@ impl DnsClient for DnsNetworkClient {
|
||||
}
|
||||
|
||||
fn send_query(&self, qname: &str, qtype: QueryType, server: &str, recursive: bool) -> Result<DnsPacket> {
|
||||
let packet = self.send_udp_query(qname, qtype, server, recursive)?;
|
||||
let packet = self.send_udp_query(qname, qtype, server, recursive, DEFAULT_TIMEOUT)?;
|
||||
if !packet.header.truncated_message {
|
||||
return Ok(packet);
|
||||
}
|
||||
@@ -387,9 +465,9 @@ impl DnsClient for DnsNetworkClient {
|
||||
|
||||
#[cfg(feature = "doh")]
|
||||
pub struct HttpsDnsClient {
|
||||
agent: ureq::Agent,
|
||||
agent: Agent,
|
||||
/// Counter for assigning packet ids
|
||||
seq: AtomicUsize,
|
||||
seq: AtomicU16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "doh")]
|
||||
@@ -402,60 +480,88 @@ impl HttpsDnsClient {
|
||||
.collect::<Vec<SocketAddr>>();
|
||||
trace!("Using bootstraps: {:?}", &servers);
|
||||
|
||||
let agent_config = Agent::config_builder()
|
||||
.user_agent(&client_name)
|
||||
.timeout_global(Some(Duration::from_secs(5)))
|
||||
.max_idle_connections_per_host(8)
|
||||
.max_idle_connections(16)
|
||||
.max_idle_age(Duration::from_secs(300))
|
||||
.build();
|
||||
let agent = Agent::with_parts(agent_config, DefaultConnector::default(), BootstrapResolver::new(servers));
|
||||
Self { agent, seq: AtomicU16::new(rand::random::<u16>()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "doh")]
|
||||
#[derive(Debug)]
|
||||
struct BootstrapResolver {
|
||||
servers: Vec<SocketAddr>,
|
||||
cache: RwLock<LruCache<String, Vec<SocketAddr>>>
|
||||
}
|
||||
|
||||
#[cfg(feature = "doh")]
|
||||
impl BootstrapResolver {
|
||||
pub fn new(servers: Vec<SocketAddr>) -> Self {
|
||||
let cache: LruCache<String, Vec<SocketAddr>> = LruCache::new(NonZeroUsize::new(10).unwrap());
|
||||
let cache = RwLock::new(cache);
|
||||
Self { servers, cache }
|
||||
}
|
||||
}
|
||||
|
||||
let agent = ureq::AgentBuilder::new()
|
||||
.user_agent(&client_name)
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.max_idle_connections_per_host(4)
|
||||
.max_idle_connections(16)
|
||||
.resolver(move |addr: &str| {
|
||||
let addr = match addr.find(':') {
|
||||
Some(index) => addr[0..index].to_string(),
|
||||
None => addr.to_string()
|
||||
};
|
||||
trace!("Resolving {}", addr);
|
||||
if let Some(addrs) = cache.write().unwrap().get(&addr) {
|
||||
trace!("Found bootstrap ip in cache");
|
||||
return Ok(addrs.clone());
|
||||
}
|
||||
#[cfg(feature = "doh")]
|
||||
impl Resolver for BootstrapResolver {
|
||||
fn resolve(&self, uri: &Uri, _config: &Config, timeout: NextTimeout) -> std::result::Result<ResolvedSocketAddrs, ureq::Error> {
|
||||
let domain = uri.host().unwrap_or("localhost");
|
||||
let port = uri.port_u16().unwrap_or(443);
|
||||
let addr = match domain.find(':') {
|
||||
Some(index) => domain[0..index].to_string(),
|
||||
None => domain.to_string()
|
||||
};
|
||||
let timeout_duration = Duration::from_millis(timeout.after.as_millis() as u64);
|
||||
trace!("Resolving {}", addr);
|
||||
if let Some(addrs) = self.cache.write().unwrap().get(&addr) {
|
||||
trace!("Found bootstrap ip in cache");
|
||||
let mut results: ResolvedSocketAddrs = ArrayVec::from_fn(|_| SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0));
|
||||
for addr in addrs {
|
||||
results.push(addr.to_owned());
|
||||
}
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
let port = 10000 + (rand::random::<u16>() % 50000);
|
||||
let mut dns_client = DnsNetworkClient::new(port);
|
||||
dns_client.run().unwrap();
|
||||
let mut dns_client = DnsNetworkClient::new();
|
||||
dns_client.run().unwrap();
|
||||
|
||||
let mut result: Vec<IpAddr> = Vec::new();
|
||||
for server in &servers {
|
||||
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true) {
|
||||
for answer in &res.answers {
|
||||
if let DnsRecord::A { addr, .. } = answer {
|
||||
result.push(IpAddr::V4(*addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::AAAA, server, true) {
|
||||
for answer in &res.answers {
|
||||
if let DnsRecord::AAAA { addr, .. } = answer {
|
||||
result.push(IpAddr::V6(*addr))
|
||||
}
|
||||
}
|
||||
let mut result: Vec<IpAddr> = Vec::new();
|
||||
let mut results: ResolvedSocketAddrs = ArrayVec::from_fn(|_| SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0));
|
||||
for server in &self.servers {
|
||||
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true, timeout_duration) {
|
||||
for answer in &res.answers {
|
||||
if let DnsRecord::A { addr, .. } = answer {
|
||||
results.push(SocketAddr::new(IpAddr::V4(*addr), port));
|
||||
result.push(IpAddr::V4(*addr))
|
||||
}
|
||||
}
|
||||
dns_client.stop();
|
||||
}
|
||||
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::AAAA, server, true, timeout_duration) {
|
||||
for answer in &res.answers {
|
||||
if let DnsRecord::AAAA { addr, .. } = answer {
|
||||
results.push(SocketAddr::new(IpAddr::V6(*addr), port));
|
||||
result.push(IpAddr::V6(*addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dns_client.stop();
|
||||
|
||||
result.sort();
|
||||
result.dedup();
|
||||
let addrs = result
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, 443))
|
||||
.collect::<Vec<_>>();
|
||||
trace!("Resolved addresses: {:?}", &addrs);
|
||||
cache.write().unwrap().put(addr, addrs.clone());
|
||||
Ok(addrs)
|
||||
})
|
||||
.build();
|
||||
Self { agent, seq: AtomicUsize::new(1) }
|
||||
result.sort();
|
||||
result.dedup();
|
||||
let addrs = result
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect::<Vec<_>>();
|
||||
trace!("Resolved addresses: {:?}", &addrs);
|
||||
self.cache.write().unwrap().put(addr, addrs.clone());
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,20 +603,20 @@ impl DnsClient for HttpsDnsClient {
|
||||
|
||||
let response = self.agent
|
||||
.post(doh_url)
|
||||
.set("Content-Type", "application/dns-message")
|
||||
.send_bytes(req_buffer.buffer.as_slice());
|
||||
.header("Content-Type", "application/dns-message")
|
||||
.send(req_buffer.buffer.as_slice());
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
match response.status() {
|
||||
match response.status().as_u16() {
|
||||
200 => {
|
||||
match response.header("Content-Length") {
|
||||
match response.headers().get("Content-Length") {
|
||||
None => warn!("No 'Content-Length' header in DoH response!"),
|
||||
Some(str) => {
|
||||
match str.parse::<usize>() {
|
||||
match str.to_str().unwrap_or("0").parse::<usize>() {
|
||||
Ok(size) => {
|
||||
let mut bytes: Vec<u8> = Vec::with_capacity(size);
|
||||
response.into_reader()
|
||||
response.into_body().into_reader()
|
||||
.take(4096)
|
||||
.read_to_end(&mut bytes)?;
|
||||
let mut buffer = VectorPacketBuffer::new();
|
||||
@@ -580,10 +686,11 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
pub fn test_udp_client() {
|
||||
let client = DnsNetworkClient::new(31456);
|
||||
// Disable 0x20 for testing against public DNS servers that may not preserve case
|
||||
let client = DnsNetworkClient::new_with_0x20(false);
|
||||
client.run().unwrap();
|
||||
|
||||
let res = client.send_udp_query("google.com", QueryType::A, ("8.8.8.8", 53), true).unwrap();
|
||||
let res = client.send_udp_query("google.com", QueryType::A, ("8.8.8.8", 53), true, DEFAULT_TIMEOUT).unwrap();
|
||||
|
||||
assert_eq!(res.questions[0].name, "google.com");
|
||||
assert!(res.answers.len() > 0);
|
||||
@@ -598,7 +705,8 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
pub fn test_tcp_client() {
|
||||
let client = DnsNetworkClient::new(31458);
|
||||
// Disable 0x20 for testing against public DNS servers
|
||||
let client = DnsNetworkClient::new_with_0x20(false);
|
||||
let res = client.send_tcp_query("google.com", QueryType::A, ("8.8.8.8", 53), true).unwrap();
|
||||
|
||||
assert_eq!(res.questions[0].name, "google.com");
|
||||
|
||||
+12
-8
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use derive_more::{Display, Error, From};
|
||||
|
||||
use crate::commons::rtt_tracker::RttTracker;
|
||||
use crate::dns::authority::Authority;
|
||||
use crate::dns::cache::SynchronizedCache;
|
||||
use crate::dns::client::{DnsClient, DnsNetworkClient};
|
||||
@@ -56,18 +57,19 @@ pub struct ServerContext {
|
||||
pub enable_tcp: bool,
|
||||
pub enable_api: bool,
|
||||
pub statistics: ServerStatistics,
|
||||
pub zones_dir: &'static str
|
||||
pub zones_dir: &'static str,
|
||||
pub forwarder_tracker: Arc<RttTracker<String>>,
|
||||
}
|
||||
|
||||
impl Default for ServerContext {
|
||||
fn default() -> Self {
|
||||
ServerContext::new(String::from("0.0.0.0:53"), Vec::new())
|
||||
ServerContext::new(String::from("0.0.0.0:53"), Vec::new(), true, 100)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerContext {
|
||||
#[allow(unused_variables)]
|
||||
pub fn new(dns_listen: String, bootstraps: Vec<String>) -> ServerContext {
|
||||
pub fn new(dns_listen: String, bootstraps: Vec<String>, enable_0x20: bool, cache_limit_mb: usize) -> ServerContext {
|
||||
#[cfg(not(feature = "doh"))]
|
||||
let doh_client = None;
|
||||
#[cfg(feature = "doh")]
|
||||
@@ -75,9 +77,9 @@ impl ServerContext {
|
||||
|
||||
ServerContext {
|
||||
authority: Authority::new(),
|
||||
cache: SynchronizedCache::new(),
|
||||
cache: SynchronizedCache::with_memory_limit(cache_limit_mb),
|
||||
filters: Vec::new(),
|
||||
old_client: Box::new(DnsNetworkClient::new(10000 + (rand::random::<u16>() % 50000))),
|
||||
old_client: Box::new(DnsNetworkClient::new_with_0x20(enable_0x20)),
|
||||
doh_client,
|
||||
dns_listen,
|
||||
api_port: 5380,
|
||||
@@ -87,7 +89,8 @@ impl ServerContext {
|
||||
enable_tcp: true,
|
||||
enable_api: false,
|
||||
statistics: ServerStatistics { tcp_query_count: AtomicUsize::new(0), udp_query_count: AtomicUsize::new(0) },
|
||||
zones_dir: "zones"
|
||||
zones_dir: "zones",
|
||||
forwarder_tracker: Arc::new(RttTracker::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +132,7 @@ pub mod tests {
|
||||
pub fn create_test_context(callback: Box<StubCallback>) -> Arc<ServerContext> {
|
||||
Arc::new(ServerContext {
|
||||
authority: Authority::new(),
|
||||
cache: SynchronizedCache::new(),
|
||||
cache: SynchronizedCache::with_memory_limit(0), // Unlimited for tests
|
||||
filters: Vec::new(),
|
||||
old_client: Box::new(DnsStubClient::new(callback)),
|
||||
doh_client: Some(Box::new(HttpsDnsClient::new(Vec::new()))),
|
||||
@@ -141,7 +144,8 @@ pub mod tests {
|
||||
enable_tcp: true,
|
||||
enable_api: false,
|
||||
statistics: ServerStatistics { tcp_query_count: AtomicUsize::new(0), udp_query_count: AtomicUsize::new(0) },
|
||||
zones_dir: "zones"
|
||||
zones_dir: "zones",
|
||||
forwarder_tracker: Arc::new(RttTracker::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
use crate::dns::protocol::{DnsPacket, QueryType};
|
||||
|
||||
pub trait DnsFilter {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket>;
|
||||
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket>;
|
||||
}
|
||||
|
||||
pub struct DummyFilter {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl DnsFilter for DummyFilter {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket> {
|
||||
None
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -53,7 +53,7 @@ impl HostsFilter {
|
||||
}
|
||||
|
||||
impl DnsFilter for HostsFilter {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
|
||||
fn lookup(&self, qname: &str, qtype: QueryType, _recursive: bool) -> Option<DnsPacket> {
|
||||
let mut packet = DnsPacket::new();
|
||||
if let Some(list) = self.hosts.get(qname) {
|
||||
for addr in list {
|
||||
|
||||
+143
-7
@@ -38,6 +38,7 @@ pub enum QueryType {
|
||||
SRV, // 33
|
||||
OPT, // 41
|
||||
TLSA, // 52
|
||||
HTTPS, // 65
|
||||
}
|
||||
|
||||
impl QueryType {
|
||||
@@ -55,6 +56,7 @@ impl QueryType {
|
||||
QueryType::SRV => 33,
|
||||
QueryType::OPT => 41,
|
||||
QueryType::TLSA => 52,
|
||||
QueryType::HTTPS => 65,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +73,7 @@ impl QueryType {
|
||||
33 => QueryType::SRV,
|
||||
41 => QueryType::OPT,
|
||||
52 => QueryType::TLSA,
|
||||
65 => QueryType::HTTPS,
|
||||
_ => QueryType::UNKNOWN(num),
|
||||
}
|
||||
}
|
||||
@@ -172,6 +175,48 @@ pub enum DnsRecord {
|
||||
data: Vec<u8>,
|
||||
ttl: TransientTtl
|
||||
}, // 52
|
||||
HTTPS {
|
||||
domain: String,
|
||||
priority: u16,
|
||||
target: String,
|
||||
params: Vec<u8>,
|
||||
ttl: TransientTtl
|
||||
}, // 65
|
||||
}
|
||||
|
||||
/// Read an uncompressed domain name (does not follow compression pointers)
|
||||
/// Used for HTTPS/SVCB records per RFC 9460
|
||||
fn read_uncompressed_name<T: PacketBuffer>(buffer: &mut T) -> Result<String> {
|
||||
let mut outstr = String::new();
|
||||
let mut delim = "";
|
||||
|
||||
loop {
|
||||
let len = buffer.read()? as usize;
|
||||
|
||||
// Check for compression pointer (RFC 9460: HTTPS TargetName must be uncompressed)
|
||||
// If we encounter one, this is an error - but we'll just stop
|
||||
if (len & 0xC0) > 0 {
|
||||
// This shouldn't happen for HTTPS records per RFC 9460
|
||||
// Skip the second byte of the pointer
|
||||
buffer.read()?;
|
||||
break;
|
||||
}
|
||||
|
||||
// Names are terminated by an empty label of length 0
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
outstr.push_str(delim);
|
||||
|
||||
for _ in 0..len {
|
||||
outstr.push(buffer.read()? as char);
|
||||
}
|
||||
|
||||
delim = ".";
|
||||
}
|
||||
|
||||
Ok(outstr)
|
||||
}
|
||||
|
||||
impl DnsRecord {
|
||||
@@ -267,11 +312,17 @@ impl DnsRecord {
|
||||
}
|
||||
QueryType::TXT => {
|
||||
let mut txt = String::new();
|
||||
let end_pos = buffer.pos() + data_len as usize;
|
||||
|
||||
let cur_pos = buffer.pos();
|
||||
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, data_len as usize)?));
|
||||
|
||||
buffer.step(data_len as usize)?;
|
||||
// TXT RDATA consists of one or more <length><text> segments (RFC 1035 3.3.14)
|
||||
while buffer.pos() < end_pos {
|
||||
let seg_len = buffer.read()? as usize;
|
||||
if seg_len > 0 {
|
||||
let cur_pos = buffer.pos();
|
||||
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, seg_len)?));
|
||||
buffer.step(seg_len)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DnsRecord::TXT { domain, data: txt, ttl: TransientTtl(ttl) })
|
||||
}
|
||||
@@ -294,6 +345,34 @@ impl DnsRecord {
|
||||
buffer.step(data_len as usize)?;
|
||||
Ok(DnsRecord::TLSA { domain, certificate_usage, selector, matching_type, data, ttl: TransientTtl(ttl) })
|
||||
}
|
||||
QueryType::HTTPS => {
|
||||
// Track the start position of the data section
|
||||
let data_start_pos = buffer.pos();
|
||||
|
||||
let priority = buffer.read_u16()?;
|
||||
|
||||
// Read TargetName without compression (RFC 9460 requirement)
|
||||
let target = read_uncompressed_name(buffer)?;
|
||||
|
||||
// Calculate remaining bytes for SvcParams based on data_len
|
||||
let bytes_consumed = buffer.pos() - data_start_pos;
|
||||
let params_len = if data_len as usize > bytes_consumed {
|
||||
data_len as usize - bytes_consumed
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let params = if params_len > 0 {
|
||||
let cur_pos = buffer.pos();
|
||||
let p = buffer.get_range(cur_pos, params_len)?.to_vec();
|
||||
buffer.step(params_len)?;
|
||||
p
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok(DnsRecord::HTTPS { domain, priority, target, params, ttl: TransientTtl(ttl) })
|
||||
}
|
||||
QueryType::UNKNOWN(_) => {
|
||||
buffer.step(data_len as usize)?;
|
||||
|
||||
@@ -452,6 +531,38 @@ impl DnsRecord {
|
||||
buffer.write_u8(*b)?;
|
||||
}
|
||||
}
|
||||
DnsRecord::HTTPS { ref domain, priority, ref target, ref params, ttl: TransientTtl(ttl) } => {
|
||||
buffer.write_qname(domain)?;
|
||||
buffer.write_u16(QueryType::HTTPS.to_num())?;
|
||||
buffer.write_u16(1)?;
|
||||
buffer.write_u32(ttl)?;
|
||||
|
||||
let pos = buffer.pos();
|
||||
buffer.write_u16(0)?;
|
||||
|
||||
buffer.write_u16(priority)?;
|
||||
|
||||
// Write TargetName WITHOUT compression (RFC 9460 requirement)
|
||||
let split_str = target.split('.').collect::<Vec<&str>>();
|
||||
for label in split_str.iter() {
|
||||
if label.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let len = label.len();
|
||||
buffer.write_u8(len as u8)?;
|
||||
for b in label.as_bytes() {
|
||||
buffer.write_u8(*b)?;
|
||||
}
|
||||
}
|
||||
buffer.write_u8(0)?; // Terminate with null label
|
||||
|
||||
for b in params {
|
||||
buffer.write_u8(*b)?;
|
||||
}
|
||||
|
||||
let size = buffer.pos() - (pos + 2);
|
||||
buffer.set_u16(pos, size as u16)?;
|
||||
}
|
||||
DnsRecord::OPT { packet_len, flags, ref data } => {
|
||||
buffer.write_u8(0)?;
|
||||
buffer.write_u16(QueryType::OPT.to_num())?;
|
||||
@@ -485,6 +596,7 @@ impl DnsRecord {
|
||||
DnsRecord::TXT { .. } => QueryType::TXT,
|
||||
DnsRecord::OPT { .. } => QueryType::OPT,
|
||||
DnsRecord::TLSA { .. } => QueryType::TLSA,
|
||||
DnsRecord::HTTPS { .. } => QueryType::HTTPS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,11 +612,30 @@ impl DnsRecord {
|
||||
| DnsRecord::UNKNOWN { ref domain, .. }
|
||||
| DnsRecord::SOA { ref domain, .. }
|
||||
| DnsRecord::TXT { ref domain, .. }
|
||||
| DnsRecord::TLSA { ref domain, .. } => Some(domain.clone()),
|
||||
| DnsRecord::TLSA { ref domain, .. }
|
||||
| DnsRecord::HTTPS { ref domain, .. } => Some(domain.clone()),
|
||||
DnsRecord::OPT { .. } => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_domain(&mut self, new_domain: String) {
|
||||
match self {
|
||||
DnsRecord::A { ref mut domain, .. }
|
||||
| DnsRecord::AAAA { ref mut domain, .. }
|
||||
| DnsRecord::NS { ref mut domain, .. }
|
||||
| DnsRecord::CNAME { ref mut domain, .. }
|
||||
| DnsRecord::SRV { ref mut domain, .. }
|
||||
| DnsRecord::PTR { ref mut domain, .. }
|
||||
| DnsRecord::MX { ref mut domain, .. }
|
||||
| DnsRecord::UNKNOWN { ref mut domain, .. }
|
||||
| DnsRecord::SOA { ref mut domain, .. }
|
||||
| DnsRecord::TXT { ref mut domain, .. }
|
||||
| DnsRecord::TLSA { ref mut domain, .. }
|
||||
| DnsRecord::HTTPS { ref mut domain, .. } => *domain = new_domain,
|
||||
DnsRecord::OPT { .. } => {} // OPT records don't have a domain field
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> Option<String> {
|
||||
match *self {
|
||||
DnsRecord::A { ref addr, .. } => Some(addr.to_string()),
|
||||
@@ -526,6 +657,10 @@ impl DnsRecord {
|
||||
let data = crate::commons::to_hex(data);
|
||||
Some(format!("{} {} {} {} {}", domain, certificate_usage, selector, matching_type, &data))
|
||||
},
|
||||
DnsRecord::HTTPS { ref target, priority, ref params, .. } => {
|
||||
let params_hex = crate::commons::to_hex(params);
|
||||
Some(format!("{} {} {}", priority, target, params_hex))
|
||||
},
|
||||
DnsRecord::OPT { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -541,8 +676,9 @@ impl DnsRecord {
|
||||
| DnsRecord::MX { ttl: TransientTtl(ttl), .. }
|
||||
| DnsRecord::UNKNOWN { ttl: TransientTtl(ttl), .. }
|
||||
| DnsRecord::SOA { ttl: TransientTtl(ttl), .. }
|
||||
| DnsRecord::TXT { ttl: TransientTtl(ttl), .. } => ttl,
|
||||
| DnsRecord::TLSA { ttl: TransientTtl(ttl), .. } => ttl,
|
||||
| DnsRecord::TXT { ttl: TransientTtl(ttl), .. }
|
||||
| DnsRecord::TLSA { ttl: TransientTtl(ttl), .. }
|
||||
| DnsRecord::HTTPS { ttl: TransientTtl(ttl), .. } => ttl,
|
||||
DnsRecord::OPT { .. } => 0
|
||||
}
|
||||
}
|
||||
|
||||
+94
-33
@@ -2,10 +2,10 @@
|
||||
//! incoming queries
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::vec::Vec;
|
||||
|
||||
use derive_more::{Display, Error, From};
|
||||
use rand::seq::IteratorRandom;
|
||||
|
||||
use crate::dns::context::ServerContext;
|
||||
use crate::dns::protocol::{DnsPacket, QueryType, ResultCode};
|
||||
@@ -53,7 +53,7 @@ pub trait DnsResolver {
|
||||
}
|
||||
|
||||
for filter in context.filters.iter() {
|
||||
if let Some(packet) = filter.lookup(qname, qtype) {
|
||||
if let Some(packet) = filter.lookup(qname, qtype, recursive) {
|
||||
context.cache.store(&packet.answers)?;
|
||||
return Ok(packet);
|
||||
}
|
||||
@@ -85,27 +85,51 @@ impl DnsResolver for ForwardingDnsResolver {
|
||||
}
|
||||
|
||||
fn perform(&mut self, qname: &str, qtype: QueryType) -> Result<DnsPacket> {
|
||||
let mut random = rand::thread_rng();
|
||||
let upstream = self.upstreams.iter().choose(&mut random).unwrap();
|
||||
let result = match self.context.cache.lookup(qname, qtype) {
|
||||
None => {
|
||||
if is_url(upstream) {
|
||||
if let Some(client) = &self.context.doh_client {
|
||||
client.send_query(qname, qtype, upstream, true)?
|
||||
} else {
|
||||
log::error!("This build doesn't support DoH");
|
||||
return Err(ResolveError::NoServerFound);
|
||||
}
|
||||
if let Some(packet) = self.context.cache.lookup(qname, qtype) {
|
||||
return Ok(packet);
|
||||
}
|
||||
|
||||
let ordered = self.context.forwarder_tracker.select_ordered(&self.upstreams);
|
||||
let mut last_err = ResolveError::NoServerFound;
|
||||
|
||||
for upstream in &ordered {
|
||||
let start = Instant::now();
|
||||
let query_result = if is_url(upstream) {
|
||||
if let Some(client) = &self.context.doh_client {
|
||||
client.send_query(qname, qtype, upstream, true)
|
||||
} else {
|
||||
self.context.old_client.send_query(qname, qtype, upstream, true)?
|
||||
log::error!("This build doesn't support DoH");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Some(packet) => packet
|
||||
};
|
||||
} else {
|
||||
self.context.old_client.send_query(qname, qtype, upstream, true)
|
||||
};
|
||||
|
||||
self.context.cache.store(&result.answers)?;
|
||||
match query_result {
|
||||
Ok(mut result) => {
|
||||
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
||||
self.context.forwarder_tracker.record_success(upstream, elapsed);
|
||||
self.context.cache.store(&result.answers)?;
|
||||
|
||||
Ok(result)
|
||||
// Fix domain names in answers to match original query case
|
||||
let qname_lower = qname.to_lowercase();
|
||||
for answer in &mut result.answers {
|
||||
if let Some(domain) = answer.get_domain() {
|
||||
if domain.to_lowercase() == qname_lower {
|
||||
answer.set_domain(qname.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
Err(e) => {
|
||||
self.context.forwarder_tracker.record_failure(upstream);
|
||||
last_err = e.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +193,18 @@ impl DnsResolver for RecursiveDnsResolver {
|
||||
let _ = self.context.cache.store(&response.answers);
|
||||
let _ = self.context.cache.store(&response.authorities);
|
||||
let _ = self.context.cache.store(&response.resources);
|
||||
return Ok(response);
|
||||
|
||||
// Fix domain names in answers to match original query case
|
||||
let qname_lower = qname.to_lowercase();
|
||||
let mut fixed_response = response;
|
||||
for answer in &mut fixed_response.answers {
|
||||
if let Some(domain) = answer.get_domain() {
|
||||
if domain.to_lowercase() == qname_lower {
|
||||
answer.set_domain(qname.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(fixed_response);
|
||||
}
|
||||
|
||||
if response.header.rescode == ResultCode::NXDOMAIN {
|
||||
@@ -194,7 +229,19 @@ impl DnsResolver for RecursiveDnsResolver {
|
||||
// If not, we'll have to resolve the ip of a NS record
|
||||
let new_ns_name = match response.get_unresolved_ns(qname) {
|
||||
Some(x) => x,
|
||||
None => return Ok(response)
|
||||
None => {
|
||||
// Fix domain names before returning
|
||||
let qname_lower = qname.to_lowercase();
|
||||
let mut fixed_response = response;
|
||||
for answer in &mut fixed_response.answers {
|
||||
if let Some(domain) = answer.get_domain() {
|
||||
if domain.to_lowercase() == qname_lower {
|
||||
answer.set_domain(qname.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(fixed_response);
|
||||
}
|
||||
};
|
||||
|
||||
// Recursively resolve the NS
|
||||
@@ -204,7 +251,17 @@ impl DnsResolver for RecursiveDnsResolver {
|
||||
if let Some(new_ns) = recursive_response.get_random_a() {
|
||||
ns = new_ns.clone();
|
||||
} else {
|
||||
return Ok(response);
|
||||
// Fix domain names before returning
|
||||
let qname_lower = qname.to_lowercase();
|
||||
let mut fixed_response = response;
|
||||
for answer in &mut fixed_response.answers {
|
||||
if let Some(domain) = answer.get_domain() {
|
||||
if domain.to_lowercase() == qname_lower {
|
||||
answer.set_domain(qname.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(fixed_response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +300,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
match Arc::get_mut(&mut context) {
|
||||
Some(mut ctx) => {
|
||||
Some(ctx) => {
|
||||
ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
|
||||
}
|
||||
None => panic!()
|
||||
@@ -251,7 +308,7 @@ mod tests {
|
||||
|
||||
let mut resolver = context.create_resolver(Arc::clone(&context));
|
||||
|
||||
// First verify that we get a match back
|
||||
// First, verify that we get a match back
|
||||
{
|
||||
let res = match resolver.resolve("google.com", QueryType::A, true) {
|
||||
Ok(x) => x,
|
||||
@@ -268,7 +325,7 @@ mod tests {
|
||||
}
|
||||
};
|
||||
|
||||
// Do the same lookup again, and verify that it's present in the cache
|
||||
// Do the same lookup again and verify that it's present in the cache
|
||||
// and that the counter has been updated
|
||||
{
|
||||
let res = match resolver.resolve("google.com", QueryType::A, true) {
|
||||
@@ -548,19 +605,23 @@ mod tests {
|
||||
|
||||
assert_eq!(3, list.len());
|
||||
|
||||
// Check statistics for google entry
|
||||
assert_eq!("google.com", list[1].domain);
|
||||
// Find entries by domain name (LRU order may vary)
|
||||
let google_entry = list.iter().find(|e| e.domain == "google.com").expect("google.com entry");
|
||||
let ns1_entry = list.iter().find(|e| e.domain == "ns1.google.com").expect("ns1.google.com entry");
|
||||
let foobar_entry = list.iter().find(|e| e.domain == "foobar.google.com").expect("foobar.google.com NXDOMAIN entry");
|
||||
|
||||
// Should have a NS record and an A record for a total of 2 record types
|
||||
assert_eq!(2, list[1].record_types.len());
|
||||
// google.com should have a NS record and an A record for a total of 2 record types
|
||||
assert_eq!(2, google_entry.record_types.len());
|
||||
|
||||
// Should have been hit two times for NS google.com and once for
|
||||
// A google.com
|
||||
assert_eq!(3, list[1].hits);
|
||||
assert_eq!(3, google_entry.hits);
|
||||
|
||||
assert_eq!("ns1.google.com", list[2].domain);
|
||||
assert_eq!(1, list[2].record_types.len());
|
||||
assert_eq!(2, list[2].hits);
|
||||
assert_eq!(1, ns1_entry.record_types.len());
|
||||
assert_eq!(2, ns1_entry.hits);
|
||||
|
||||
// foobar.google.com should be a cached NXDOMAIN with 0 hits
|
||||
assert_eq!(0, foobar_entry.hits);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -107,7 +107,7 @@ pub fn execute_query(context: Arc<ServerContext>, request: &DnsPacket) -> DnsPac
|
||||
|
||||
let question = &request.questions[0];
|
||||
packet.questions.push(question.clone());
|
||||
log::trace!("Resolving: {}, type {:?}", &question.name, &question.qtype);
|
||||
debug!("Resolving: {}, type {:?}", &question.name, &question.qtype);
|
||||
|
||||
let mut resolver = context.create_resolver(Arc::clone(&context));
|
||||
let res_code = match resolver.resolve(&question.name, question.qtype, request.header.recursion_desired) {
|
||||
@@ -246,6 +246,10 @@ impl DnsServer for DnsUdpServer {
|
||||
debug!("UDP service loop has finished");
|
||||
break;
|
||||
}
|
||||
if code == 10054 {
|
||||
// Ignore
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug!("Failed to read from UDP socket: {:?}", err);
|
||||
continue;
|
||||
@@ -441,7 +445,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
match Arc::get_mut(&mut context) {
|
||||
Some(mut ctx) => {
|
||||
Some(ctx) => {
|
||||
ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
|
||||
}
|
||||
None => panic!()
|
||||
@@ -460,7 +464,7 @@ mod tests {
|
||||
}
|
||||
};
|
||||
|
||||
// A successful resolve, that also resolves a CNAME without recursive lookup
|
||||
// A successful resolve that also resolves a CNAME without recursive lookup
|
||||
{
|
||||
let res = execute_query(Arc::clone(&context), &build_query("www.facebook.com", QueryType::CNAME));
|
||||
assert_eq!(2, res.answers.len());
|
||||
@@ -480,7 +484,7 @@ mod tests {
|
||||
}
|
||||
};
|
||||
|
||||
// A successful resolve, that also resolves a CNAME through recursive lookup
|
||||
// A successful resolve that also resolves a CNAME through recursive lookup
|
||||
{
|
||||
let res = execute_query(Arc::clone(&context), &build_query("www.microsoft.com", QueryType::CNAME));
|
||||
dbg!(&res);
|
||||
@@ -503,7 +507,7 @@ mod tests {
|
||||
|
||||
// Disable recursive resolves to generate a failure
|
||||
match Arc::get_mut(&mut context) {
|
||||
Some(mut ctx) => {
|
||||
Some(ctx) => {
|
||||
ctx.allow_recursive = false;
|
||||
}
|
||||
None => panic!()
|
||||
@@ -531,7 +535,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
match Arc::get_mut(&mut context2) {
|
||||
Some(mut ctx) => {
|
||||
Some(ctx) => {
|
||||
ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
|
||||
}
|
||||
None => panic!()
|
||||
|
||||
+7
-2
@@ -33,9 +33,14 @@ pub fn start_dns_server(context: &Arc<Mutex<Context>>, settings: &Settings) -> b
|
||||
result
|
||||
}
|
||||
|
||||
/// Creates DNS-context with all needed settings
|
||||
/// Creates DNS-context with all necessary settings
|
||||
fn create_server_context(context: Arc<Mutex<Context>>, settings: &Settings) -> Arc<ServerContext> {
|
||||
let mut server_context = ServerContext::new(settings.dns.listen.clone(), settings.dns.bootstraps.clone());
|
||||
let mut server_context = ServerContext::new(
|
||||
settings.dns.listen.clone(),
|
||||
settings.dns.bootstraps.clone(),
|
||||
settings.dns.enable_0x20,
|
||||
settings.dns.cache_memory_limit_mb
|
||||
);
|
||||
server_context.allow_recursive = true;
|
||||
server_context.resolve_strategy = match settings.dns.forwarders.is_empty() {
|
||||
true => ResolveStrategy::Recursive,
|
||||
|
||||
+10
-8
@@ -59,6 +59,7 @@ fn main() {
|
||||
opts.optflag("v", "version", "Print version and exit");
|
||||
opts.optflag("d", "debug", "Show debug messages, more than usual");
|
||||
opts.optflag("t", "trace", "Show trace messages, more than debug");
|
||||
opts.optflag("", "hide", "Hide UI, show only tray icon.");
|
||||
opts.optflag("b", "blocks", "List blocks from DB and exit");
|
||||
opts.optflag("g", "generate", "Generate new config file. Generated config will be printed to console.");
|
||||
#[cfg(windows)]
|
||||
@@ -251,7 +252,7 @@ fn main() {
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "webgui")]
|
||||
web_ui::run_interface(Arc::clone(&context), miner);
|
||||
web_ui::run_interface(Arc::clone(&context), miner, opt_matches.opt_present("hide"));
|
||||
}
|
||||
|
||||
// Without explicitly detaching the console cmd won't redraw it's prompt.
|
||||
@@ -297,6 +298,12 @@ fn load_keys(settings: &Settings) -> Vec<Keystore> {
|
||||
}
|
||||
|
||||
pub fn start_services(settings: &Settings, context: &Arc<Mutex<Context>>) -> (bool, Arc<Mutex<Miner>>, JoinHandle<()>) {
|
||||
let dns_server_ok = if settings.dns.threads > 0 {
|
||||
dns_utils::start_dns_server(&context, &settings)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if let Ok(mut context) = context.lock() {
|
||||
context.chain.check_chain(settings.check_blocks);
|
||||
match context.chain.get_block(1) {
|
||||
@@ -309,12 +316,6 @@ pub fn start_services(settings: &Settings, context: &Arc<Mutex<Context>>) -> (bo
|
||||
}
|
||||
}
|
||||
|
||||
let dns_server_ok = if settings.dns.threads > 0 {
|
||||
dns_utils::start_dns_server(&context, &settings)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let mut miner_obj = Miner::new(Arc::clone(&context));
|
||||
miner_obj.start_mining_thread();
|
||||
let miner: Arc<Mutex<Miner>> = Arc::new(Mutex::new(miner_obj));
|
||||
@@ -339,8 +340,9 @@ fn setup_logger(opt_matches: &Matches, console_attached: bool) {
|
||||
}
|
||||
let mut builder = ConfigBuilder::new();
|
||||
let config = builder.add_filter_ignore_str("mio::poll")
|
||||
.add_filter_ignore_str("rustls::client")
|
||||
.add_filter_ignore_str("rustls::")
|
||||
.add_filter_ignore_str("ureq::")
|
||||
.add_filter_ignore_str("ureq_proto::")
|
||||
.set_thread_level(LevelFilter::Error)
|
||||
.set_location_level(LevelFilter::Off)
|
||||
.set_target_level(LevelFilter::Error)
|
||||
|
||||
+130
-8
@@ -29,7 +29,22 @@ impl Settings {
|
||||
Ok(mut file) => {
|
||||
let mut text = String::new();
|
||||
file.read_to_string(&mut text).unwrap();
|
||||
if let Ok(settings) = toml::from_str(&text) {
|
||||
if let Ok(mut settings) = toml::from_str::<Settings>(&text) {
|
||||
// Migrate incorrect test port 42440 to correct port 4244 for public nodes
|
||||
if settings.net.public && settings.net.listen.contains(":42440") {
|
||||
warn!("Migrating incorrect port 42440 to 4244 in net.listen configuration");
|
||||
|
||||
// Update the in-memory settings
|
||||
settings.net.listen = settings.net.listen.replace(":42440", ":4244");
|
||||
|
||||
// Try to save the corrected configuration back to file
|
||||
if let Err(e) = Self::save_migration(filename, &text) {
|
||||
warn!("Could not save migrated config to {}: {}", filename, e);
|
||||
info!("Please manually update net.listen from :42440 to :4244 in your config");
|
||||
} else {
|
||||
info!("Successfully migrated config file {} (port 42440 → 4244)", filename);
|
||||
}
|
||||
}
|
||||
return Some(settings);
|
||||
}
|
||||
None
|
||||
@@ -38,6 +53,15 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
fn save_migration(filename: &str, original_text: &str) -> Result<(), std::io::Error> {
|
||||
use std::io::Write;
|
||||
// Simple text replacement preserves all comments and formatting
|
||||
let migrated_text = original_text.replace(":42440", ":4244");
|
||||
let mut file = File::create(filename)?;
|
||||
file.write_all(migrated_text.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_origin(&self) -> Bytes {
|
||||
if self.origin.eq("") {
|
||||
return Bytes::zero32();
|
||||
@@ -70,17 +94,25 @@ pub struct Dns {
|
||||
#[serde(default = "default_dns_bootstraps")]
|
||||
pub bootstraps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub hosts: Vec<String>
|
||||
pub hosts: Vec<String>,
|
||||
/// Enable DNS 0x20 encoding (random case) for additional security against cache poisoning
|
||||
#[serde(default = "default_dns_0x20")]
|
||||
pub enable_0x20: bool,
|
||||
/// DNS cache memory limit in megabytes (default: 100MB, 0 = unlimited)
|
||||
#[serde(default = "default_cache_memory_limit_mb")]
|
||||
pub cache_memory_limit_mb: usize
|
||||
}
|
||||
|
||||
impl Default for Dns {
|
||||
fn default() -> Self {
|
||||
Dns {
|
||||
listen: String::from("127.0.0.1:53"),
|
||||
threads: 20,
|
||||
listen: default_listen_dns(),
|
||||
threads: 10,
|
||||
forwarders: vec![String::from("94.140.14.14:53"), String::from("94.140.15.15:53")],
|
||||
bootstraps: default_dns_bootstraps(),
|
||||
hosts: Vec::new()
|
||||
hosts: Vec::new(),
|
||||
enable_0x20: default_dns_0x20(),
|
||||
cache_memory_limit_mb: default_cache_memory_limit_mb()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +140,7 @@ pub struct Net {
|
||||
impl Default for Net {
|
||||
fn default() -> Self {
|
||||
Net {
|
||||
peers: vec![String::from("test-ip4.alfis.name:4244"), String::from("test-ip6.alfis.name:4244")],
|
||||
peers: vec![String::from("peer-v4.alfis.name:4244"), String::from("peer-v6.alfis.name:4244")],
|
||||
listen: String::from("[::]:4244"),
|
||||
public: true,
|
||||
yggdrasil_only: false
|
||||
@@ -121,11 +153,11 @@ fn default_listen() -> String {
|
||||
}
|
||||
|
||||
fn default_listen_dns() -> String {
|
||||
String::from("0.0.0.0:53")
|
||||
String::from("127.0.0.3:53")
|
||||
}
|
||||
|
||||
fn default_threads() -> usize {
|
||||
100
|
||||
10
|
||||
}
|
||||
|
||||
fn default_check_blocks() -> u64 {
|
||||
@@ -144,4 +176,94 @@ fn default_key_files() -> Vec<String> {
|
||||
|
||||
fn default_dns_bootstraps() -> Vec<String> {
|
||||
vec![String::from("9.9.9.9:53"), String::from("94.140.14.14:53")]
|
||||
}
|
||||
|
||||
fn default_dns_0x20() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_cache_memory_limit_mb() -> usize {
|
||||
100 // 100 MB default
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_port_migration_for_public_nodes() {
|
||||
// Create a temporary config file with incorrect port
|
||||
let test_file = "test_migration_unit.toml";
|
||||
let config_content = r#"
|
||||
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
|
||||
key_files = ["key1.toml"]
|
||||
|
||||
[net]
|
||||
# Comment should be preserved
|
||||
listen = "[::]:42440"
|
||||
public = true
|
||||
"#;
|
||||
|
||||
// Write test config
|
||||
let mut file = File::create(test_file).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
drop(file);
|
||||
|
||||
// Load the config (should trigger migration)
|
||||
let settings = Settings::load(test_file).unwrap();
|
||||
|
||||
// Verify the setting in memory is correct
|
||||
assert_eq!(settings.net.listen, "[::]:4244");
|
||||
|
||||
// Read the file to verify it was actually modified
|
||||
let mut file = File::open(test_file).unwrap();
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
|
||||
// Verify file was migrated
|
||||
assert!(content.contains(":4244"));
|
||||
assert!(!content.contains(":42440"));
|
||||
// Verify comment was preserved
|
||||
assert!(content.contains("# Comment should be preserved"));
|
||||
|
||||
// Cleanup
|
||||
std::fs::remove_file(test_file).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_migration_for_private_nodes() {
|
||||
// Create a temporary config file with incorrect port but public = false
|
||||
let test_file = "test_no_migration_unit.toml";
|
||||
let config_content = r#"
|
||||
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
|
||||
key_files = ["key1.toml"]
|
||||
|
||||
[net]
|
||||
listen = "[::]:42440"
|
||||
public = false
|
||||
"#;
|
||||
|
||||
// Write test config
|
||||
let mut file = File::create(test_file).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
drop(file);
|
||||
|
||||
// Load the config (should NOT trigger migration because public = false)
|
||||
let settings = Settings::load(test_file).unwrap();
|
||||
|
||||
// Verify the setting remains unchanged
|
||||
assert_eq!(settings.net.listen, "[::]:42440");
|
||||
|
||||
// Read the file to verify it was NOT modified
|
||||
let mut file = File::open(test_file).unwrap();
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
|
||||
// Verify file was NOT migrated (still has 42440)
|
||||
assert!(content.contains(":42440"));
|
||||
|
||||
// Cleanup
|
||||
std::fs::remove_file(test_file).ok();
|
||||
}
|
||||
}
|
||||
+441
-314
@@ -2,11 +2,11 @@ extern crate open;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate tinyfiledialogs as tfd;
|
||||
extern crate web_view;
|
||||
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
use alfis::blockchain::transaction::DomainData;
|
||||
use alfis::blockchain::types::MineResult;
|
||||
@@ -17,229 +17,217 @@ use alfis::event::Event;
|
||||
use alfis::eventbus::{post, register};
|
||||
use alfis::miner::Miner;
|
||||
use alfis::{keystore, Block, Bytes, Context, Keystore, Transaction};
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use chrono::{Local, Utc};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use image::GenericImageView;
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace, warn, LevelFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use web_view::Content;
|
||||
use Cmd::*;
|
||||
|
||||
use self::web_view::{Handle, WebView};
|
||||
use tao::{
|
||||
event::{Event as TaoEvent, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy},
|
||||
window::WindowBuilder,
|
||||
};
|
||||
use tao::dpi::PhysicalPosition;
|
||||
use tray_icon::menu::{Menu, MenuEvent, MenuItem};
|
||||
use tray_icon::{TrayIconBuilder, TrayIconEvent};
|
||||
use wry::WebViewBuilder;
|
||||
|
||||
pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>) {
|
||||
pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hide: bool) {
|
||||
let file_content = include_str!("webview/index.html");
|
||||
let mut styles = inline_style(include_str!("webview/bulma.css"));
|
||||
styles.push_str(&inline_style(include_str!("webview/styles.css")));
|
||||
styles.push_str(&inline_style(include_str!("webview/busy_indicator.css")));
|
||||
let scripts = inline_script(include_str!("webview/scripts.js"));
|
||||
|
||||
let html = Content::Html(file_content.to_owned().replace("{styles}", &styles).replace("{scripts}", &scripts));
|
||||
let html = file_content.to_owned().replace("{styles}", &styles).replace("{scripts}", &scripts);
|
||||
let title = format!("ALFIS {}", env!("CARGO_PKG_VERSION"));
|
||||
let mut interface = web_view::builder()
|
||||
.title(&title)
|
||||
.content(html)
|
||||
.size(1023, 720)
|
||||
.min_size(773, 350)
|
||||
.resizable(true)
|
||||
.debug(false)
|
||||
.user_data(())
|
||||
.invoke_handler(|web_view, arg| {
|
||||
debug!("Command {}", arg);
|
||||
match serde_json::from_str(arg).unwrap() {
|
||||
Loaded => { action_loaded(&context, web_view); }
|
||||
LoadKey => { action_load_key(&context, web_view); }
|
||||
CreateKey => { keystore::create_key(Arc::clone(&context)); }
|
||||
SaveKey => { action_save_key(&context); }
|
||||
SelectKey { index } => { action_select_key(&context, web_view, index); }
|
||||
CheckRecord { data } => { action_check_record(web_view, data); }
|
||||
CheckDomain { name } => { action_check_domain(&context, web_view, name); }
|
||||
MineDomain { name, data, signing, encryption, renewal } => {
|
||||
action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data, signing, encryption, renewal);
|
||||
}
|
||||
TransferDomain { name, owner} => { info!("Transferring '{name}' to '{owner}'"); }
|
||||
StopMining => { post(Event::ActionStopMining); }
|
||||
Open { link } => {
|
||||
if open::that(&link).is_err() {
|
||||
show_warning(web_view, "Something wrong, I can't open the link 😢");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
|
||||
// Create event loop and window
|
||||
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
|
||||
|
||||
// Create tray menu
|
||||
let tray_menu = Menu::new();
|
||||
let show_item = MenuItem::new("Show Window", true, None);
|
||||
let quit_item = MenuItem::new("Quit", true, None);
|
||||
tray_menu.append(&show_item).unwrap();
|
||||
tray_menu.append(&quit_item).unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
let icon = tray_icon::Icon::from_resource(1, None).unwrap();
|
||||
// Create tray icon
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let icon = load_icon_from_png();
|
||||
|
||||
let tray_icon = TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu))
|
||||
.with_tooltip(&title)
|
||||
.with_icon(icon)
|
||||
.with_menu_on_left_click(false)
|
||||
.build()
|
||||
.expect("Error building GUI");
|
||||
.unwrap();
|
||||
|
||||
run_interface_loop(&mut interface);
|
||||
}
|
||||
let window_size = tao::dpi::LogicalSize::new(1024, 720);
|
||||
// Get primary monitor and calculate center position
|
||||
let position = match event_loop.primary_monitor() {
|
||||
Some(monitor) => {
|
||||
let monitor_size = monitor.size();
|
||||
let monitor_position = monitor.position();
|
||||
|
||||
/// Indefinitely loops through WebView steps
|
||||
fn run_interface_loop(interface: &mut WebView<()>) {
|
||||
// We use this ugly loop to lower CPU usage a lot.
|
||||
// If we use .run() or only .step() in a loop without sleeps it will try
|
||||
// to support 60FPS and uses more CPU than it should.
|
||||
let pause = Duration::from_millis(25);
|
||||
let mut start = Instant::now();
|
||||
loop {
|
||||
match interface.step() {
|
||||
None => {
|
||||
info!("Interface closed, exiting");
|
||||
post(Event::ActionQuit);
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
break;
|
||||
}
|
||||
Some(result) => {
|
||||
match result {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
error!("Something wrong with webview, exiting");
|
||||
break;
|
||||
let scaled = window_size.to_physical::<i32>(monitor.scale_factor());
|
||||
let center_x = monitor_position.x + (monitor_size.width as i32 - scaled.width) / 2;
|
||||
let center_y = monitor_position.y + (monitor_size.height as i32 - scaled.height) / 2;
|
||||
|
||||
Some(PhysicalPosition::new(center_x, center_y))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut builder = WindowBuilder::new()
|
||||
.with_title(&title)
|
||||
.with_inner_size(window_size)
|
||||
.with_min_inner_size(tao::dpi::LogicalSize::new(773, 350))
|
||||
.with_resizable(true)
|
||||
.with_visible(!hide);
|
||||
|
||||
if let Some(position) = position {
|
||||
builder = builder.with_position(position);
|
||||
}
|
||||
|
||||
let window = builder.build(&event_loop)
|
||||
.expect("Failed to create the window");
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use winapi::um::shellscalingapi::SetProcessDpiAwareness;
|
||||
unsafe {
|
||||
SetProcessDpiAwareness(2);
|
||||
}
|
||||
use tao::platform::windows::IconExtWindows;
|
||||
use tao::window::Icon;
|
||||
let icon = Icon::from_resource(1, None).unwrap();
|
||||
window.set_window_icon(Some(icon));
|
||||
}
|
||||
|
||||
// Clone for the IPC handler
|
||||
let context_ipc = Arc::clone(&context);
|
||||
let miner_ipc = Arc::clone(&miner);
|
||||
let proxy = event_loop.create_proxy();
|
||||
let proxy_ipc = proxy.clone();
|
||||
|
||||
// Create webview
|
||||
let builder = WebViewBuilder::new()
|
||||
.with_transparent(false)
|
||||
.with_visible(true)
|
||||
.with_devtools(cfg!(debug_assertions))
|
||||
.with_html(html) // Using test HTML to verify wry works
|
||||
.with_ipc_handler(move |request| {
|
||||
let body = request.body();
|
||||
debug!("Command {}", body);
|
||||
|
||||
match serde_json::from_str(body) {
|
||||
Ok(cmd) => {
|
||||
match cmd {
|
||||
Loaded => {
|
||||
let _ = proxy_ipc.send_event(UserEvent::Loaded);
|
||||
}
|
||||
LoadKey => {
|
||||
action_load_key(&context_ipc, &proxy_ipc);
|
||||
}
|
||||
CreateKey => {
|
||||
keystore::create_key(Arc::clone(&context_ipc));
|
||||
}
|
||||
SaveKey => {
|
||||
action_save_key(&context_ipc);
|
||||
}
|
||||
SelectKey { index } => {
|
||||
action_select_key(&context_ipc, &proxy_ipc, index);
|
||||
}
|
||||
CheckRecord { data } => {
|
||||
let result = check_record(&data);
|
||||
let _ = proxy_ipc.send_event(UserEvent::EvalJs(format!("recordOkay({})", result)));
|
||||
}
|
||||
CheckDomain { name } => {
|
||||
let available = check_domain_available(&context_ipc, &name);
|
||||
let _ = proxy_ipc.send_event(UserEvent::EvalJs(format!("domainAvailable({})", available)));
|
||||
}
|
||||
MineDomain { name, data, signing, encryption, renewal } => {
|
||||
action_create_domain(Arc::clone(&context_ipc), Arc::clone(&miner_ipc), &proxy_ipc, name, data, signing, encryption, renewal);
|
||||
}
|
||||
TransferDomain { name, owner } => {
|
||||
info!("Transferring '{name}' to '{owner}'");
|
||||
}
|
||||
StopMining => {
|
||||
post(Event::ActionStopMining);
|
||||
}
|
||||
Open { link } => {
|
||||
if open::that(&link).is_err() {
|
||||
let _ = proxy_ipc.send_event(UserEvent::ShowWarning("Something wrong, I can't open the link 😢".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if start.elapsed().as_millis() > 1 {
|
||||
thread::sleep(pause);
|
||||
start = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_check_record(web_view: &mut WebView<()>, data: String) {
|
||||
match serde_json::from_str::<DnsRecord>(&data) {
|
||||
Ok(record) => {
|
||||
if let Some(string) = record.get_data() {
|
||||
if string.len() > MAX_DATA_LEN {
|
||||
web_view.eval("recordOkay(false)").expect("Error evaluating!");
|
||||
} else {
|
||||
web_view.eval("recordOkay(true)").expect("Error evaluating!");
|
||||
Err(e) => {
|
||||
error!("Error parsing command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
web_view.eval("recordOkay(false)").expect("Error evaluating!");
|
||||
dbg!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fn action_check_domain(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>, name: String) {
|
||||
let c = context.lock().unwrap();
|
||||
if let Some(keystore) = c.get_keystore() {
|
||||
let name = name.to_lowercase();
|
||||
let available = match c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()) {
|
||||
MineResult::Fine => true,
|
||||
_ => false
|
||||
};
|
||||
web_view.eval(&format!("domainAvailable({})", available)).expect("Error evaluating!");
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let webview = builder.build(&window).unwrap();
|
||||
#[cfg(target_os = "linux")]
|
||||
let webview = {
|
||||
use tao::platform::unix::WindowExtUnix;
|
||||
use wry::WebViewBuilderExtUnix;
|
||||
let vbox = window.default_vbox().unwrap();
|
||||
builder.build_gtk(vbox).expect("Failed to build webview gtk object")
|
||||
};
|
||||
// Disabling context menu on the page in release build
|
||||
#[cfg(not(debug_assertions))]
|
||||
let _ = webview.evaluate_script("document.addEventListener('contextmenu', e => e.preventDefault());");
|
||||
|
||||
fn action_save_key(context: &Arc<Mutex<Context>>) {
|
||||
if !context.lock().unwrap().has_keys() {
|
||||
return;
|
||||
}
|
||||
let result = tfd::save_file_dialog_with_filter("Save keys file", "", &["*.toml"], "Key files (*.toml)");
|
||||
match result {
|
||||
None => {}
|
||||
Some(mut new_path) => {
|
||||
if !new_path.ends_with(".toml") {
|
||||
new_path.push_str(".toml");
|
||||
}
|
||||
let path = new_path.clone();
|
||||
if let Some(keystore) = context.lock().unwrap().get_keystore_mut() {
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
keystore.save(&new_path, "");
|
||||
info!("Key file saved to {}", &path);
|
||||
post(Event::KeySaved { path, public, hash });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let webview = Arc::new(Mutex::new(webview));
|
||||
let webview_clone = Arc::clone(&webview);
|
||||
|
||||
fn action_select_key(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>, index: usize) {
|
||||
if context.lock().unwrap().select_key_by_index(index) {
|
||||
let (path, public, hash) = {
|
||||
let keystore = context.lock().unwrap().get_keystore().cloned().unwrap();
|
||||
let path = keystore.get_path().to_owned();
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
(path, public, hash)
|
||||
};
|
||||
post(Event::KeyLoaded { path, public, hash });
|
||||
web_view.eval(&format!("keySelected({})", index)).expect("Error evaluating!");
|
||||
}
|
||||
}
|
||||
|
||||
fn action_load_key(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
let result = tfd::open_file_dialog("Open keys file", "", Some((&["*.key", "*.toml"], "Key files")));
|
||||
match result {
|
||||
None => {}
|
||||
Some(file_name) => {
|
||||
match Keystore::from_file(&file_name, "") {
|
||||
None => {
|
||||
error!("Error loading keystore '{}'!", &file_name);
|
||||
show_warning(web_view, "Error loading key!<br>Key cannot be loaded or its difficulty is not enough.");
|
||||
event_fail(web_view, &format!("Error loading key from \\'{}\\'!", &file_name));
|
||||
}
|
||||
Some(keystore) => {
|
||||
info!("Loaded keystore with keys: {:?}, {:?}", &keystore.get_public(), &keystore.get_encryption_public());
|
||||
let path = keystore.get_path().to_owned();
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
post(Event::KeyLoaded { path, public, hash });
|
||||
|
||||
if !context.lock().unwrap().select_key_by_public(&keystore.get_public()) {
|
||||
context.lock().unwrap().add_keystore(keystore);
|
||||
} else {
|
||||
warn!("This key is already loaded!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
info!("Interface loaded");
|
||||
web_view.eval("showMiningIndicator(false, false);").expect("Error evaluating!");
|
||||
let handle: Handle<()> = web_view.handle();
|
||||
// Setup event bus listener
|
||||
let proxy_events = proxy.clone();
|
||||
let threads = context.lock().unwrap().settings.mining.threads;
|
||||
let threads = match threads {
|
||||
0 => num_cpus::get(),
|
||||
_ => threads
|
||||
};
|
||||
let status = Arc::new(Mutex::new(UiStatus::new(threads)));
|
||||
let context_copy = Arc::clone(context);
|
||||
let c = context.lock().unwrap();
|
||||
let connected_nodes = Arc::new(AtomicUsize::new(0));
|
||||
let nodes_copy = Arc::clone(&connected_nodes);
|
||||
|
||||
register(move |_uuid, e| {
|
||||
//debug!("Got event from bus {:?}", &e);
|
||||
let status = Arc::clone(&status);
|
||||
let handle = handle.clone();
|
||||
let context_copy = Arc::clone(&context_copy);
|
||||
let _ = thread::Builder::new().name(String::from("webui")).spawn(move || {
|
||||
let proxy = proxy_events.clone();
|
||||
let nodes_copy = Arc::clone(&nodes_copy);
|
||||
|
||||
thread::Builder::new().name(String::from("webui")).spawn(move || {
|
||||
let mut status = status.lock().unwrap();
|
||||
let mut context = context_copy.lock().unwrap();
|
||||
let eval = match e {
|
||||
Event::KeyCreated { path, public, hash } => {
|
||||
load_domains(&mut context, &handle);
|
||||
send_keys_to_ui(&context, &handle);
|
||||
event_handle_luck(&handle, "Key successfully created! Don\\'t forget to save it!");
|
||||
let _ = proxy.send_event(UserEvent::LoadDomains);
|
||||
let _ = proxy.send_event(UserEvent::SendKeysToUi);
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('luck', '{}', 'Key successfully created! Don\\'t forget to save it!');", Local::now().format("%d.%m.%y %X"))));
|
||||
let mut s = format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash);
|
||||
s.push_str(" showSuccess('New key mined successfully! Save it to a safe place!')");
|
||||
s
|
||||
}
|
||||
Event::KeyLoaded { path, public, hash } |
|
||||
Event::KeySaved { path, public, hash } => {
|
||||
load_domains(&mut context, &handle);
|
||||
send_keys_to_ui(&context, &handle);
|
||||
let _ = proxy.send_event(UserEvent::LoadDomains);
|
||||
let _ = proxy.send_event(UserEvent::SendKeysToUi);
|
||||
format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash)
|
||||
}
|
||||
Event::MinerStarted | Event::KeyGeneratorStarted => {
|
||||
status.mining = true;
|
||||
status.max_diff = 0;
|
||||
event_handle_info(&handle, "Mining started");
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining started');", Local::now().format("%d.%m.%y %X"))));
|
||||
String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);")
|
||||
}
|
||||
Event::MinerStopped { success, full } => {
|
||||
@@ -253,12 +241,12 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
if full {
|
||||
match success {
|
||||
true => {
|
||||
load_domains(&mut context, &handle);
|
||||
event_handle_luck(&handle, "Mining is successful!");
|
||||
let _ = proxy.send_event(UserEvent::LoadDomains);
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('luck', '{}', 'Mining is successful!');", Local::now().format("%d.%m.%y %X"))));
|
||||
s.push_str(" showSuccess('Block successfully mined!')");
|
||||
}
|
||||
false => {
|
||||
event_handle_info(&handle, "Mining finished without result.");
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining finished without result.');", Local::now().format("%d.%m.%y %X"))));
|
||||
s.push_str(" showWarning('Mining unsuccessful, sorry.')");
|
||||
}
|
||||
}
|
||||
@@ -288,7 +276,7 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
status.syncing = true;
|
||||
status.synced_blocks = have;
|
||||
if height != status.sync_height {
|
||||
event_handle_info(&handle, "Syncing started...");
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Syncing started...');", Local::now().format("%d.%m.%y %X"))));
|
||||
status.sync_height = height;
|
||||
}
|
||||
if status.mining {
|
||||
@@ -298,8 +286,8 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
}
|
||||
}
|
||||
Event::SyncFinished => {
|
||||
load_domains(&mut context, &handle);
|
||||
event_handle_info(&handle, "Syncing finished.");
|
||||
let _ = proxy.send_event(UserEvent::LoadDomains);
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Syncing finished.');", Local::now().format("%d.%m.%y %X"))));
|
||||
status.syncing = false;
|
||||
if status.mining {
|
||||
String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);")
|
||||
@@ -308,6 +296,7 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
}
|
||||
}
|
||||
Event::NetworkStatus { blocks, domains, keys, nodes } => {
|
||||
nodes_copy.store(nodes, Ordering::SeqCst);
|
||||
if status.mining || status.syncing || nodes < 3 {
|
||||
format!("setStats({}, {}, {}, {});", blocks, domains, keys, nodes)
|
||||
} else {
|
||||
@@ -316,49 +305,251 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
|
||||
}
|
||||
Event::BlockchainChanged { index } => {
|
||||
debug!("Current blockchain height is {}", index);
|
||||
event_handle_info(&handle, &format!("Blockchain changed, current block count is {} now.", index));
|
||||
String::new() // Nothing
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Blockchain changed, current block count is {} now.');", Local::now().format("%d.%m.%y %X"), index)));
|
||||
String::new()
|
||||
}
|
||||
Event::Error { text } => format!("showError('{}')", &text),
|
||||
_ => String::new()
|
||||
};
|
||||
|
||||
if !eval.is_empty() {
|
||||
handle.dispatch(move |web_view| {
|
||||
web_view.eval(&eval.replace("\\", "\\\\"))
|
||||
}).expect("Error dispatching!");
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(eval));
|
||||
}
|
||||
});
|
||||
}).ok();
|
||||
true
|
||||
});
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
TrayIconEvent::set_event_handler(Some(move |event| {
|
||||
let _ = proxy.send_event(UserEvent::TrayIconEvent(event));
|
||||
}));
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
MenuEvent::set_event_handler(Some(move |event| {
|
||||
let _ = proxy.send_event(UserEvent::MenuEvent(event));
|
||||
}));
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
// Run event loop
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
TaoEvent::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => {
|
||||
window.set_visible(false);
|
||||
}
|
||||
TaoEvent::UserEvent(user_event) => {
|
||||
let wv = webview_clone.lock().unwrap();
|
||||
match user_event {
|
||||
UserEvent::EvalJs(js) => {
|
||||
let js_escaped = js.replace("\\", "\\\\");
|
||||
if let Err(e) = wv.evaluate_script(&js_escaped) {
|
||||
error!("Error evaluating JavaScript: {}", e);
|
||||
}
|
||||
}
|
||||
UserEvent::Loaded => {
|
||||
action_loaded(&context, &wv, &proxy);
|
||||
}
|
||||
UserEvent::LoadDomains => {
|
||||
load_domains(&mut context.lock().unwrap(), &wv);
|
||||
}
|
||||
UserEvent::SendKeysToUi => {
|
||||
send_keys_to_ui(&context.lock().unwrap(), &wv);
|
||||
}
|
||||
UserEvent::ShowWarning(text) => {
|
||||
show_warning(&wv, &text);
|
||||
}
|
||||
UserEvent::TrayIconEvent(event) => {
|
||||
match event {
|
||||
TrayIconEvent::DoubleClick { button, .. } => {
|
||||
if button == tray_icon::MouseButton::Left {
|
||||
window.set_visible(true);
|
||||
window.set_focus();
|
||||
}
|
||||
}
|
||||
TrayIconEvent::Enter { .. } => {
|
||||
let nodes = connected_nodes.load(Ordering::SeqCst);
|
||||
let title = format!("ALFIS {}\nConnected: {nodes}", env!("CARGO_PKG_VERSION"));
|
||||
let _ = tray_icon.set_tooltip(Some(title));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
UserEvent::MenuEvent(event) => {
|
||||
if event.id == show_item.id() {
|
||||
window.set_visible(true);
|
||||
} else if event.id == quit_item.id() {
|
||||
info!("Interface closed, exiting");
|
||||
post(Event::ActionQuit);
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum UserEvent {
|
||||
EvalJs(String),
|
||||
Loaded,
|
||||
LoadDomains,
|
||||
SendKeysToUi,
|
||||
ShowWarning(String),
|
||||
TrayIconEvent(TrayIconEvent),
|
||||
MenuEvent(MenuEvent)
|
||||
}
|
||||
|
||||
/// Load icon from embedded in binary PNG file. Only needed in Linux/macOS builds.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn load_icon_from_png() -> tray_icon::Icon {
|
||||
// Include PNG in binary
|
||||
const ICON_BYTES: &[u8] = include_bytes!("../img/logo/alfis_icon32.png");
|
||||
|
||||
// decode image by crate `image`
|
||||
let image = image::load_from_memory(ICON_BYTES)
|
||||
.expect("Error loading image from png");
|
||||
let rgba = image.to_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
|
||||
// Convert to format for tray_icon
|
||||
tray_icon::Icon::from_rgba(rgba.into_vec(), width, height)
|
||||
.expect("Error loading icon")
|
||||
}
|
||||
|
||||
fn check_record(data: &str) -> bool {
|
||||
match serde_json::from_str::<DnsRecord>(data) {
|
||||
Ok(record) => {
|
||||
if let Some(string) = record.get_data() {
|
||||
string.len() <= MAX_DATA_LEN
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false
|
||||
}
|
||||
}
|
||||
|
||||
fn check_domain_available(context: &Arc<Mutex<Context>>, name: &str) -> bool {
|
||||
let c = context.lock().unwrap();
|
||||
if let Some(keystore) = c.get_keystore() {
|
||||
let name = name.to_lowercase();
|
||||
matches!(c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()), MineResult::Fine)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn action_save_key(context: &Arc<Mutex<Context>>) {
|
||||
if !context.lock().unwrap().has_keys() {
|
||||
return;
|
||||
}
|
||||
let result = tfd::save_file_dialog_with_filter("Save keys file", "", &["*.toml"], "Key files (*.toml)");
|
||||
match result {
|
||||
None => {}
|
||||
Some(mut new_path) => {
|
||||
if !new_path.ends_with(".toml") {
|
||||
new_path.push_str(".toml");
|
||||
}
|
||||
let path = new_path.clone();
|
||||
if let Some(keystore) = context.lock().unwrap().get_keystore_mut() {
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
keystore.save(&new_path, "");
|
||||
info!("Key file saved to {}", &path);
|
||||
post(Event::KeySaved { path, public, hash });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_select_key(context: &Arc<Mutex<Context>>, proxy: &EventLoopProxy<UserEvent>, index: usize) {
|
||||
if context.lock().unwrap().select_key_by_index(index) {
|
||||
let (path, public, hash) = {
|
||||
let keystore = context.lock().unwrap().get_keystore().cloned().unwrap();
|
||||
let path = keystore.get_path().to_owned();
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
(path, public, hash)
|
||||
};
|
||||
post(Event::KeyLoaded { path, public, hash });
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("keySelected({})", index)));
|
||||
}
|
||||
}
|
||||
|
||||
fn action_load_key(context: &Arc<Mutex<Context>>, proxy: &EventLoopProxy<UserEvent>) {
|
||||
let result = tfd::open_file_dialog("Open keys file", "", Some((&["*.key", "*.toml"], "Key files")));
|
||||
match result {
|
||||
None => {}
|
||||
Some(file_name) => {
|
||||
match Keystore::from_file(&file_name, "") {
|
||||
None => {
|
||||
error!("Error loading keystore '{}'!", &file_name);
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("Error loading key!<br>Key cannot be loaded or its difficulty is not enough.".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('fail', '{}', 'Error loading key from \\\\'{}\\\\!');", Local::now().format("%d.%m.%y %X"), &file_name)));
|
||||
}
|
||||
Some(keystore) => {
|
||||
info!("Loaded keystore with keys: {:?}, {:?}", &keystore.get_public(), &keystore.get_encryption_public());
|
||||
let path = keystore.get_path().to_owned();
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
post(Event::KeyLoaded { path, public, hash });
|
||||
|
||||
if !context.lock().unwrap().select_key_by_public(&keystore.get_public()) {
|
||||
context.lock().unwrap().add_keystore(keystore);
|
||||
} else {
|
||||
warn!("This key is already loaded!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn action_loaded(context: &Arc<Mutex<Context>>, webview: &wry::WebView, proxy: &EventLoopProxy<UserEvent>) {
|
||||
info!("Interface loaded");
|
||||
let _ = webview.evaluate_script("showMiningIndicator(false, false);");
|
||||
|
||||
let c = context.lock().unwrap();
|
||||
|
||||
if let Some(keystore) = c.get_keystore() {
|
||||
let path = keystore.get_path().to_owned();
|
||||
let public = keystore.get_public().to_string();
|
||||
let hash = keystore.get_hash().to_string();
|
||||
post(Event::KeyLoaded { path, public, hash });
|
||||
}
|
||||
|
||||
let index = c.chain.get_height();
|
||||
if index > 0 {
|
||||
post(Event::BlockchainChanged { index });
|
||||
}
|
||||
|
||||
let zones = c.chain.get_zones();
|
||||
info!("Loaded zones: {:?}", &zones);
|
||||
if let Ok(zones) = serde_json::to_string(&zones) {
|
||||
let _ = web_view.eval(&format!("zonesChanged('{}');", &zones));
|
||||
let _ = webview.evaluate_script(&format!("zonesChanged('{}');", &zones));
|
||||
}
|
||||
send_keys_to_ui(&c, &web_view.handle());
|
||||
|
||||
drop(c);
|
||||
let _ = proxy.send_event(UserEvent::SendKeysToUi);
|
||||
|
||||
let c = context.lock().unwrap();
|
||||
let command = format!("setStats({}, {}, {}, {});", c.chain.get_height(), c.chain.get_domains_count(), c.chain.get_users_count(), 0);
|
||||
if let Err(e) = web_view.eval(&command) {
|
||||
if let Err(e) = webview.evaluate_script(&command) {
|
||||
error!("Error evaluating stats: {}", e);
|
||||
}
|
||||
event_info(web_view, "Application loaded");
|
||||
let _ = webview.evaluate_script(&format!("addEvent('info', '{}', 'Application loaded');", Local::now().format("%d.%m.%y %X")));
|
||||
}
|
||||
|
||||
fn load_domains(context: &mut MutexGuard<Context>, handle: &Handle<()>) {
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval("clearMyDomains();")
|
||||
});
|
||||
fn load_domains(context: &mut MutexGuard<Context>, webview: &wry::WebView) {
|
||||
let _ = webview.evaluate_script("clearMyDomains();");
|
||||
let domains = context.chain.get_my_domains(context.get_keystore());
|
||||
let mut domains = domains.iter().map(|(_, d)| d).collect::<Vec<_>>();
|
||||
domains.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
@@ -366,16 +557,12 @@ fn load_domains(context: &mut MutexGuard<Context>, handle: &Handle<()>) {
|
||||
let d = serde_json::to_string(&data).unwrap();
|
||||
let d = d.replace("'", "\\'").replace("\\n", "\\\\n").replace("\"", "\\\"");
|
||||
let command = format!("addMyDomain('{}', {}, {}, '{}');", &domain, timestamp, timestamp + DOMAIN_LIFETIME, &d);
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval(&command)
|
||||
});
|
||||
let _ = webview.evaluate_script(&command);
|
||||
}
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval("refreshMyDomains();")
|
||||
});
|
||||
let _ = webview.evaluate_script("refreshMyDomains();");
|
||||
}
|
||||
|
||||
fn send_keys_to_ui(context: &MutexGuard<Context>, handle: &Handle<()>) {
|
||||
fn send_keys_to_ui(context: &MutexGuard<Context>, webview: &wry::WebView) {
|
||||
let keys = {
|
||||
let mut keys = Vec::new();
|
||||
for key in context.get_keystores() {
|
||||
@@ -387,94 +574,101 @@ fn send_keys_to_ui(context: &MutexGuard<Context>, handle: &Handle<()>) {
|
||||
};
|
||||
if !keys.is_empty() {
|
||||
let index = context.get_active_key_index();
|
||||
let _ = handle.dispatch(move |web_view| {
|
||||
let command = format!("keysChanged('{}'); keySelected({});", serde_json::to_string(&keys).unwrap(), index);
|
||||
web_view.eval(&command)
|
||||
});
|
||||
let command = format!("keysChanged('{}'); keySelected({});", serde_json::to_string(&keys).unwrap(), index);
|
||||
let _ = webview.evaluate_script(&command);
|
||||
}
|
||||
}
|
||||
|
||||
fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, web_view: &mut WebView<()>, name: String, data: String, signing: String, encryption: String, renewal: bool) {
|
||||
fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, proxy: &EventLoopProxy<UserEvent>, name: String, data: String, signing: String, encryption: String, renewal: bool) {
|
||||
debug!("Creating domain with data: {}", &data);
|
||||
let c = Arc::clone(&context);
|
||||
let context = context.lock().unwrap();
|
||||
if !context.has_keys() {
|
||||
show_warning(web_view, "You don't have keys loaded!<br>Load or mine the keys and try again.");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let context_guard = context.lock().unwrap();
|
||||
|
||||
if !context_guard.has_keys() {
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("You don't have keys loaded!<br>Load or mine the keys and try again.".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
return;
|
||||
}
|
||||
if context.chain.is_waiting_signers() {
|
||||
show_warning(web_view, "Waiting for last full block to be signed. Try again later.");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
|
||||
if context_guard.chain.is_waiting_signers() {
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("Waiting for last full block to be signed. Try again later.".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
info!("Waiting for last full block to be signed. Try again later.");
|
||||
return;
|
||||
}
|
||||
let keystore = context.get_keystore().unwrap().clone();
|
||||
|
||||
let keystore = context_guard.get_keystore().unwrap().clone();
|
||||
let pub_key = keystore.get_public();
|
||||
let data = match serde_json::from_str::<DomainData>(&data) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
show_warning(web_view, "Something wrong with domain data. I cannot mine it.");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("Something wrong with domain data. I cannot mine it.".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
warn!("Error parsing data: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Parsed domain data:\n{:#?}", &data);
|
||||
|
||||
if data.records.len() > MAX_RECORDS {
|
||||
show_warning(web_view, "Too many records. Mining more than 30 records not allowed.");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("Too many records. Mining more than 30 records not allowed.".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if yggdrasil only quality of zone is not violated
|
||||
let zones = context.chain.get_zones();
|
||||
let zones = context_guard.chain.get_zones();
|
||||
for z in zones {
|
||||
if z.name == data.zone && z.yggdrasil {
|
||||
for record in &data.records {
|
||||
if !is_yggdrasil_record(record) {
|
||||
show_warning(web_view, &format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone));
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning(format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone)));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (signing, encryption) = if signing.is_empty() || encryption.is_empty() {
|
||||
(keystore.get_public(), keystore.get_encryption_public())
|
||||
} else {
|
||||
(Bytes::new(from_hex(&signing).unwrap()), Bytes::new(from_hex(&encryption).unwrap()))
|
||||
};
|
||||
match context.chain.can_mine_domain(context.chain.get_height(), &name, &pub_key) {
|
||||
|
||||
match context_guard.chain.can_mine_domain(context_guard.chain.get_height(), &name, &pub_key) {
|
||||
MineResult::Fine => {
|
||||
drop(context);
|
||||
drop(context_guard);
|
||||
create_domain(c, miner, CLASS_DOMAIN, &name, data, DOMAIN_DIFFICULTY, &keystore, signing, encryption, renewal);
|
||||
let _ = web_view.eval("domainMiningStarted();");
|
||||
event_info(web_view, &format!("Mining of domain \\'{}\\' has started", &name));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningStarted();".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining of domain \\\\'{}\\\\' has started');", Local::now().format("%d.%m.%y %X"), &name)));
|
||||
}
|
||||
MineResult::WrongName => {
|
||||
show_warning(web_view, "You can't mine this domain!");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine this domain!".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
}
|
||||
MineResult::WrongData => {
|
||||
show_warning(web_view, "You have an error in records!");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("You have an error in records!".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
}
|
||||
MineResult::WrongKey => {
|
||||
show_warning(web_view, "You can't mine with current key!");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine with current key!".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
}
|
||||
MineResult::WrongZone => {
|
||||
show_warning(web_view, "You can't mine domain in this zone!");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine domain in this zone!".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
}
|
||||
MineResult::NotOwned => {
|
||||
show_warning(web_view, "This domain is already taken, and it is not yours!");
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning("This domain is already taken, and it is not yours!".to_string()));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
}
|
||||
MineResult::Cooldown { time } => {
|
||||
event_info(web_view, &format!("You have cooldown {}!", format_cooldown(time)));
|
||||
show_warning(web_view, &format!("You have cooldown {}!", format_cooldown(time)));
|
||||
let _ = web_view.eval("domainMiningUnavailable();");
|
||||
let cooldown = format_cooldown(time);
|
||||
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'You have cooldown {}!');", Local::now().format("%d.%m.%y %X"), &cooldown)));
|
||||
let _ = proxy.send_event(UserEvent::ShowWarning(format!("You have cooldown {}!", cooldown)));
|
||||
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,80 +684,13 @@ fn format_cooldown(time: i64) -> String {
|
||||
format!("{} hours", minutes / 60)
|
||||
}
|
||||
|
||||
fn show_warning(web_view: &mut WebView<()>, text: &str) {
|
||||
fn show_warning(webview: &wry::WebView, text: &str) {
|
||||
let str = text.replace('\'', "\\'");
|
||||
match web_view.eval(&format!("showWarning('{}');", &str)) {
|
||||
Ok(_) => {}
|
||||
Err(_) => { warn!("Error showing warning!"); }
|
||||
if let Err(e) = webview.evaluate_script(&format!("showWarning('{}');", &str)) {
|
||||
warn!("Error showing warning: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn show_success(web_view: &mut WebView<()>, text: &str) {
|
||||
let str = text.replace('\'', "\\'");
|
||||
match web_view.eval(&format!("showSuccess('{}');", &str)) {
|
||||
Ok(_) => {}
|
||||
Err(_) => { warn!("Error showing success!"); }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_info(web_view: &mut WebView<()>, message: &str) {
|
||||
let _ = web_view.eval(&format_event_now("info", message));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_warn(web_view: &mut WebView<()>, message: &str) {
|
||||
let _ = web_view.eval(&format_event_now("warn", message));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_fail(web_view: &mut WebView<()>, message: &str) {
|
||||
let _ = web_view.eval(&format_event_now("fail", message));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_handle_info(handle: &Handle<()>, message: &str) {
|
||||
let message = message.to_owned();
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval(&format_event_now("info", &message))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_handle_warn(handle: &Handle<()>, message: &str) {
|
||||
let message = message.to_owned();
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval(&format_event_now("warn", &message))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_handle_fail(handle: &Handle<()>, message: &str) {
|
||||
let message = message.to_owned();
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval(&format_event_now("fail", &message))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn event_handle_luck(handle: &Handle<()>, message: &str) {
|
||||
let message = message.to_owned();
|
||||
let _ = handle.dispatch(move |web_view|{
|
||||
web_view.eval(&format_event_now("luck", &message))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn format_event(kind: &str, time: DateTime<Local>, message: &str) -> String {
|
||||
format!("addEvent('{}', '{}', '{}');", kind, time.format("%d.%m.%y %X"), message)
|
||||
}
|
||||
|
||||
fn format_event_now(kind: &str, message: &str) -> String {
|
||||
let time = Local::now();
|
||||
format!("addEvent('{}', '{}', '{}');", kind, time.format("%d.%m.%y %X"), message)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, class: &str, name: &str, mut data: DomainData, difficulty: u32, keystore: &Keystore, signing: Bytes, encryption: Bytes, renewal: bool) {
|
||||
let name = name.to_owned();
|
||||
@@ -611,7 +738,7 @@ struct UiStatus {
|
||||
|
||||
impl UiStatus {
|
||||
fn new(threads: usize) -> Self {
|
||||
let speed =vec![0; threads];
|
||||
let speed = vec![0; threads];
|
||||
UiStatus { mining: false, syncing: false, synced_blocks: 0, sync_height: 0, max_diff: 0, speed }
|
||||
}
|
||||
|
||||
|
||||
Vendored
+16335
-6395
File diff suppressed because it is too large
Load Diff
+12
-7
@@ -47,13 +47,13 @@ function refreshRecordsList() {
|
||||
|
||||
function makeRecord(value, index, array) {
|
||||
let data = value.addr;
|
||||
if (value.type == "MX") {
|
||||
if (value.type === "MX") {
|
||||
data = value.priority + " " + value.host;
|
||||
} else if (value.type == "CNAME" || value.type == "NS") {
|
||||
} else if (value.type === "CNAME" || value.type === "NS") {
|
||||
data = value.host;
|
||||
} else if (value.type == "TXT" || value.type == "TLSA") {
|
||||
} else if (value.type === "TXT" || value.type === "TLSA") {
|
||||
data = value.data.toString();
|
||||
} else if (value.type == "SRV") {
|
||||
} else if (value.type === "SRV") {
|
||||
data = value.priority + " " + value.weight + " " + value.port + " " + value.host;
|
||||
}
|
||||
|
||||
@@ -212,12 +212,17 @@ function editDomain(domain, event) {
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
// Workaround for Arch Linux Webkit
|
||||
// https://github.com/Boscop/web-view/issues/212#issuecomment-671055663
|
||||
// Compatibility shim for wry IPC
|
||||
if (typeof window.external == 'undefined' || typeof window.external.invoke == 'undefined') {
|
||||
window.external = {
|
||||
invoke: function(x) {
|
||||
window.webkit.messageHandlers.external.postMessage(x);
|
||||
// wry uses window.ipc.postMessage
|
||||
if (typeof window.ipc !== 'undefined') {
|
||||
window.ipc.postMessage(x);
|
||||
} else if (typeof window.webkit !== 'undefined' && typeof window.webkit.messageHandlers !== 'undefined') {
|
||||
// Fallback for older webkit
|
||||
window.webkit.messageHandlers.external.postMessage(x);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -85,7 +85,7 @@ fn run_service_logic() -> Result<()> {
|
||||
let (_dns_server_ok, _miner, _network) = start_services(&settings, &context);
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
// Poll shutdown event.
|
||||
match shutdown_rx.recv_timeout(Duration::from_secs(1)) {
|
||||
// Break the loop either upon stop or channel disconnect
|
||||
|
||||
Reference in New Issue
Block a user