45 Commits

Author SHA1 Message Date
Sweetbread 071567fa73 Fix NixOS GUI issues
Fixes #380, #414
2026-04-13 09:27:21 +03:00
Revertron 489f16e462 Added forgotten file, fixed CI. 2026-04-06 02:40:45 +02:00
Revertron f4c8b1fe42 Updated the CI for releases. 2026-04-06 02:28:56 +02:00
Revertron b01ade19b1 Fixed new sync. 2026-04-06 02:18:23 +02:00
Revertron 9bea173f21 Initial sync made a lot faster. 2026-04-05 12:13:30 +02:00
Revertron ef573436d9 Updated dependencies. 2026-04-04 16:24:21 +02:00
Revertron d86fb6916f Disabled 0x20 encoding for NS queries. 2026-04-04 16:19:58 +02:00
Revertron 5044064f6c Fixed 0x20 encoding in cache. 2026-04-04 15:49:16 +02:00
Revertron c90458eaaf Fixed #411 (very big DNS result over DoH). 2026-04-04 15:37:25 +02:00
Revertron cfb3cf6cf8 Added parsing of bare IPs for forwarders in config. 2026-03-29 18:55:59 +02:00
Revertron c8fa174ac0 Added adaptive RTT-based server selection for forwarders too. 2026-03-29 18:43:55 +02:00
Revertron 9624484b29 Added adaptive NS server selection with RTT banding. 2026-03-29 18:15:06 +02:00
Revertron eee73be58e Fixed old test. 2026-03-20 11:38:19 +01:00
Revertron eb30037f53 Fixed bug with TXT parsing. 2026-03-20 11:13:00 +01:00
Revertron 2e1f05cadb Implemented memory limit for DNS cache. 2026-01-05 16:50:20 +01:00
Revertron 09c1cd5ddc Updated styles, adding dark theme. 2026-01-05 15:43:30 +01:00
Revertron bb162bccee Fixed Windows build. 2025-10-29 16:54:29 +01:00
Revertron d1bf9163f7 Merge pull request #397 from Revertron/feature/tray-icon
Added tray icon and ability to start ALFIS with hidden UI, shown by tray icon actions.
2025-10-29 16:41:39 +01:00
Revertron 7c11c7fbd7 Tested and fixed tray icon on Linux. 2025-10-29 16:39:44 +01:00
Revertron 8f4cbf7dc0 Added tray icon and ability to run UI hidden, but shown by tray icon actions. 2025-10-29 16:01:41 +01:00
Revertron bb3a33c103 Tuned HTTPs client for DoH. 2025-10-29 15:59:19 +01:00
Revertron 0835df14ac Disabled DevTools & context menu in release build. 2025-10-28 23:08:49 +01:00
Revertron 6e5b64545e Made some ureq trace logs silent. 2025-10-28 22:55:20 +01:00
Revertron f35dc56598 Made DNS server start earlier. Made Windows service more robust. 2025-10-28 22:16:56 +01:00
Revertron 71674e3de8 Fixes for CI, for future releases. 2025-10-28 16:31:05 +01:00
Revertron 4f2aef91c0 Added support for HTTPS (65) DNS record type. 2025-10-28 15:25:59 +01:00
Revertron 6950600bdd On some Linux machines ALFIS is unable to get primary monitor (what do you expect from Linux?). Fixed that. 2025-10-28 13:24:34 +01:00
Revertron a29a6190fb Fix DNS domain name case preservation and DNS 0x20 encoding issues.
Fixed DNS 0x20 encoding bug in worker threads and removed automatic lowercasing in DNS buffer parsing to preserve case from authoritative sources. Implemented case-insensitive lookups for cache and blockchain while ensuring restoration of the original client query case in all response paths instead of returning randomized DNS 0x20 case from upstream servers.
2025-10-28 13:11:56 +01:00
Revertron b10402ee1e Updated README. 2025-10-28 00:25:32 +01:00
Revertron dbf3df9ff9 Updated CI for release building. 2025-10-27 22:55:59 +01:00
Revertron 6b3f88f6bb Updated dependencies. Updated adblock.txt. 2025-10-27 22:44:24 +01:00
Revertron 50569d2a20 Merge pull request #395 from Revertron/move_to_wry
Migrated UI from unsupported webview crate to wry.
2025-10-27 22:39:44 +01:00
Revertron 664715f02b CI fixes. 2025-10-27 22:18:36 +01:00
Revertron 8e11f63479 Fixed centering of the window. 2025-10-27 22:09:11 +01:00
Revertron 19f67e8b77 Moved from webview crate to "wry" to fix Linux's inability to maintain compatibility. 2025-10-27 20:36:37 +01:00
Revertron 8a0677caf2 Added automatic config migration for incorrect test port 42440 → 4244
Automatically fixes net.listen port for public nodes that have the incorrect
test port 42440. Migration preserves comments and formatting by using text
replacement instead of TOML re-serialization.
2025-10-27 14:56:32 +01:00
Revertron 5de0341ab4 Enhanced DNS security with ephemeral ports and DNS 0x20 encoding
Significantly improve DNS client security against cache poisoning attacks through multiple defense layers:

Security Improvements:
- Bind UDP sockets to OS-assigned ephemeral ports (0.0.0.0:0) instead of predictable random ports, eliminating port-based attack vectors
- Implement DNS 0x20 encoding with strict case validation, adding 10-15 bits of entropy per query by randomizing domain name case
- Randomize transaction ID starting point using AtomicU16 for better entropy distribution

Attack difficulty increased from ~16 bits (65K attempts) to ~42-47 bits
(4.4-140 trillion attempts), making spoofing 1,000x to 32,000x harder.

Configuration:
- Add 'enable_0x20' option to DNS settings (default: true)
- Users can disable for compatibility with legacy resolvers if needed
- Feature is configurable via alfis.toml
2025-10-27 14:39:47 +01:00
Revertron d3cdf6ea76 Fixed warnings in some tests. 2025-10-27 01:37:38 +01:00
Revertron 81f5568957 Updated all dependencies. 2025-10-27 01:22:02 +01:00
Revertron 61f2d89ef1 Fixed GLUE records return on NS requests. 2025-10-23 22:48:48 +02:00
Revertron 429563eee9 Another try to build for macOS. 2025-10-23 21:46:16 +02:00
Revertron fc7360ea00 Another try to build for macOS. 2025-10-23 21:43:22 +02:00
Revertron 914e8b6d67 Another try to build for macOS. 2025-10-23 21:35:45 +02:00
Revertron 4169ede074 Added DNS timeouts here and there.
Fixed macOS and Ubuntu pipelines.
2025-10-23 21:26:03 +02:00
Revertron d2b7080c96 Many DNS fixes! 2025-10-22 22:55:58 +02:00
41 changed files with 22102 additions and 8395 deletions
+8 -1
View File
@@ -23,4 +23,11 @@ rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"] rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
[target.mipsel-unknown-linux-musl] [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 -5
View File
@@ -16,17 +16,28 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ windows-latest, ubuntu-latest, macOS-latest] include:
- os: windows-latest
- os: ubuntu-24.04
- os: macos-latest
target: aarch64-apple-darwin
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install libgtk-dev libwebkit2gtk-4.0 - name: Install dependencies
run: sudo apt update && sudo apt install libwebkit2gtk-4.0-dev run: sudo apt update && sudo apt install libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev
if: contains(matrix.os, 'ubuntu') if: contains(matrix.os, 'ubuntu')
- name: Update Rust - name: Update Rust
run: rustup update stable run: rustup update stable
- name: Install ARM target (macOS)
run: rustup target add aarch64-apple-darwin
if: matrix.target == 'aarch64-apple-darwin'
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
- name: Run tests - name: Run tests
run: cargo test --all --verbose run: cargo test --all --verbose ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
+71 -97
View File
@@ -17,7 +17,7 @@ jobs:
project_version: ${{ env.VERSION }} project_version: ${{ env.VERSION }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Getting version - name: Getting version
run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
@@ -25,55 +25,50 @@ jobs:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: get_version needs: get_version
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Create Release - name: Create Release
id: create_release
uses: actions/create-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.get_version.outputs.project_version }} VERSION: ${{ needs.get_version.outputs.project_version }}
with: run: |
tag_name: ${{ env.VERSION }} gh release create "$VERSION" \
release_name: ${{ env.VERSION }} --title "$VERSION" \
body: | --draft \
## New --notes "## New
* Added new features. * Added new features.
## Bug Fixes & Improvements ## Bug Fixes & Improvements
* Various fixes and stability improvements. * Various fixes and stability improvements.
## Documentation & others ## Documentation & others
* Updated documentation. * Updated documentation."
draft: true
prerelease: false
linux-nogui: linux-nogui:
name: Create and upload builds name: Build Linux nogui (${{ matrix.arch }})
needs: [ create_release, get_version ] needs: [create_release, get_version]
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
arch: [ amd64, i686, armhf, armlf, arm64 ] arch: [amd64, i686, armhf, armel, arm64]
defaults: defaults:
run: run:
shell: bash shell: bash
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: install dependencies - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: | run: |
sudo apt update && sudo apt upgrade && sudo apt install libwebkit2gtk-4.0-dev upx sudo apt-get update
cargo install cross sudo apt-get install -y upx-ucl
cargo install cross --git https://github.com/cross-rs/cross
- name: Build and package deb packages - name: Build and package deb packages
run: PKGARCH=${{ matrix.arch }} contrib/deb/generate.sh run: PKGARCH=${{ matrix.arch }} contrib/deb/generate.sh
- name: Upload bins & debs - name: Upload bins & debs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
tag_name="${{ needs.get_version.outputs.project_version }}" tag_name="${{ needs.get_version.outputs.project_version }}"
ls -lh ./bin/ ls -lh ./bin/
@@ -81,89 +76,68 @@ jobs:
gh release upload "$tag_name" *.deb --clobber gh release upload "$tag_name" *.deb --clobber
build-and-upload-gui-zips: build-and-upload-gui-zips:
name: Create and upload builds name: Build GUI (${{ matrix.name }})
needs: [ create_release, get_version ] needs: [create_release, get_version]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ windows-latest, ubuntu-latest, macOS-latest ] include:
- os: ubuntu-latest
name: linux-amd64
bin_path: ./target/release/alfis
archive_cmd: zip
- os: windows-latest
name: windows-amd64
bin_path: target/release/alfis.exe
archive_cmd: 7z
- os: macos-latest
name: darwin-arm64
bin_path: ./target/release/alfis
archive_cmd: zip
- os: macos-latest
name: darwin-amd64
target: x86_64-apple-darwin
bin_path: ./target/x86_64-apple-darwin/release/alfis
archive_cmd: zip
defaults: defaults:
run: run:
shell: bash shell: bash
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: install dependencies - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target || '' }}
- name: Install Linux dependencies
if: contains(matrix.os, 'ubuntu') if: contains(matrix.os, 'ubuntu')
run: sudo apt update && sudo apt install --no-install-recommends libwebkit2gtk-4.0-dev upx run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libxdo-dev \
libsoup-3.0-dev \
libayatana-appindicator3-dev
- name: Build release binaries - name: Build release binaries
run: cargo build --release run: cargo build --release ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
- name: Build Windows release binaries with Edge web-engine - name: Package zip
if: contains(matrix.os, 'windows') env:
run: cargo build --release --features "edge" --target-dir edge ZIP_NAME: alfis-${{ matrix.name }}-${{ needs.get_version.outputs.project_version }}.zip
- name: windows
if: contains(matrix.os, 'windows')
run: | run: |
echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV if [ "${{ matrix.archive_cmd }}" = "7z" ]; then
echo "BIN_ARCH_EDGE=windows-amd64-edge" >> $GITHUB_ENV 7z a "$ZIP_NAME" "${{ matrix.bin_path }}" alfis.toml README.md LICENSE adblock.txt
else
- name: linux zip "$ZIP_NAME" "${{ matrix.bin_path }}" alfis.toml README.md LICENSE adblock.txt
if: contains(matrix.os, 'ubuntu') fi
run: echo "BIN_ARCH=linux-amd64" >> $GITHUB_ENV
- name: macos
if: contains(matrix.os, 'mac')
run: echo "BIN_ARCH=darwin-amd64" >> $GITHUB_ENV
- name: Fill variables
run: |
echo "BIN_PATH=./target/release/alfis" >> $GITHUB_ENV
echo "ZIP_NAME=alfis-${{env.BIN_ARCH}}-${{ needs.get_version.outputs.project_version }}.zip" >> $GITHUB_ENV
- name: Windows variables
if: contains(matrix.os, 'windows')
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
with:
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 - name: Upload zip
id: upload-zip
uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.get_version.outputs.project_version }}
with: ZIP_NAME: alfis-${{ matrix.name }}-${{ needs.get_version.outputs.project_version }}.zip
upload_url: ${{ needs.create_release.outputs.upload_url }} run: gh release upload "$TAG" "$ZIP_NAME" --clobber
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
+2722 -481
View File
File diff suppressed because it is too large Load Diff
+26 -21
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "alfis" name = "alfis"
version = "0.8.6" version = "0.8.9"
authors = ["Revertron <alfis@revertron.com>"] authors = ["Revertron <alfis@revertron.com>"]
edition = "2021" edition = "2021"
build = "build.rs" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
getopts = "0.2.21" getopts = "0.2.24"
log = "0.4.22" log = "0.4.28"
simplelog = "0.12.2" simplelog = "0.12.2"
toml = "0.8.19" toml = "1.0.7"
sha2 = "0.10.8" sha2 = "0.11.0"
ed25519-dalek = "2.1.1" ed25519-dalek = "2.2.0"
x25519-dalek = { version = "2.0.1", features = ["reusable_secrets"] } 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" } ecies-ed25519-ng = { git = "https://github.com/Revertron/ecies-ed25519-ng", rev = "554ca29", version = "0.5.3" }
chacha20poly1305 = "0.10.1" chacha20poly1305 = "0.10.1"
blakeout = "0.3.0" blakeout = "0.3.0"
num_cpus = "1.16.0" num_cpus = "1.17.0"
byteorder = "1.5.0" byteorder = "1.5.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
bincode = "1.3.3" bincode = { version = "2.0.1", features = ["serde"] }
serde_cbor = "0.11.2" serde_cbor = "0.11.2"
num-bigint = "0.4.6" num-bigint = "0.4.6"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.42", features = ["serde"] }
time = "0.3.36" time = "0.3.44"
rand = { package = "rand", version = "0.8.5" } rand = { package = "rand", version = "0.8.5" }
sqlite = "0.36.0" sqlite = "0.37.0"
uuid = { version = "1.11.0", features = ["serde", "v4"] } uuid = { version = "1.18.1", features = ["serde", "v4"] }
mio = { version = "1.0.0", features = ["os-poll", "net"] } mio = { version = "1.0.0", features = ["os-poll", "net"] }
ureq = { version = "2.10", optional = true } ureq = { version = "3.1.4", optional = true }
lru = "0.12" lru = "0.16.2"
derive_more = { version = "1.0.0", features = ["display", "error", "from"] } derive_more = { version = "2.0.1", features = ["display", "error", "from"] }
lazy_static = "1.5.0" lazy_static = "1.5.0"
spmc = "0.3.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 # Optional dependencies regulated by features
web-view = { git = "https://github.com/Boscop/web-view", features = [], optional = true } wry = { version = "0.55.0", optional = true }
tao = { version = "0.35.0", optional = true }
tray-icon = { version = "0.22.0", optional = true }
tinyfiledialogs = { version = "3.9.1", optional = true } tinyfiledialogs = { version = "3.9.1", optional = true }
open = { version = "5.3.0", 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] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["impl-default", "wincon", "shellscalingapi"] } winapi = { version = "0.3.9", features = ["impl-default", "wincon", "shellscalingapi"] }
windows-service = "0.7.0" windows-service = "0.8.0"
[build-dependencies] [build-dependencies]
winres = "0.1.12" winres = "0.1.12"
@@ -59,7 +65,7 @@ lto = true
strip = true # Automatically strip symbols from the binary. strip = true # Automatically strip symbols from the binary.
[profile.dev] [profile.dev]
opt-level = 2 opt-level = 1
[profile.test] [profile.test]
opt-level = 2 opt-level = 2
@@ -69,7 +75,6 @@ ProductName="ALFIS"
FileDescription="Alternative Free Identity System" FileDescription="Alternative Free Identity System"
[features] [features]
webgui = ["web-view", "tinyfiledialogs", "open"] webgui = ["wry", "tao", "tray-icon", "tinyfiledialogs", "open"]
edge = ["webgui", "web-view/edge"]
doh = ["ureq"] doh = ["ureq"]
default = ["webgui", "doh"] default = ["webgui", "doh"]
+13 -13
View File
@@ -8,9 +8,9 @@ This project represents a minimal blockchain without cryptocurrency, capable of
Not so clear? Hold on. Not so clear? Hold on.
## This software provides: ## This software provides:
- Very small and [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) synchronized database of domain names. - 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. 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). - 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. - 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. 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. - 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
![Screenshot](img/domains.png) ![Screenshot](img/domains.png)
## How it works? ## How does it work?
Every node connects to its siblings and synchronizes the domain database. 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. 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: 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 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/).
### ![Windows Logo](/img/windows.svg) On Windows (MINGW64) ### ![Windows Logo](/img/windows.svg) On Windows (MINGW64)
If you'd rather use Gnu version of Rust you can build Alfis by these steps: If you'd rather use Gnu version of Rust you can build Alfis by these steps:
@@ -58,8 +58,8 @@ cargo build
``` ```
### ![Linux Logo](/img/linux.svg) On Linux ### ![Linux Logo](/img/linux.svg) On Linux
If you are building on Linux you must ensure that you have `libwebkitgtk` library installed. 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.0-dev` (on Debian/Ubuntu and derivatives). You can do it by issuing this command: `sudo apt install libwebkit2gtk-4.1-dev libxdo-dev` (on Debian/Ubuntu and derivatives).
#### ![Arch Linux Logo](/img/archlinux.svg) On Arch Linux #### ![Arch Linux Logo](/img/archlinux.svg) 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 --fetch-keys https://deb.revertron.com/key.txt
gpg --export F244E16645D86D62 | sudo tee /usr/local/apt-keys/alfis.gpg > /dev/null 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 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) ### 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. 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. It needs to be without `nogui` suffix.
Just unzip that archive in some directory and run `alfis` (or `alfis.exe`) binary. 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`. If you want it to load config from another file you can command it so: `alfis -c /etc/alfis.conf`.
## Roadmap ## Roadmap
1. Stabilize blockchain functions (domain transfer, info & contacts in UI), bug hunting and fixing. 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). 2. ~~Change DNS server/proxy to own resource saving implementation (using trust-dns-proto for RR parsing).~~
3. P2P traffic encryption (ECDH). ✅ 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 ## Remarkable contributions
* [@umasterov](https://github.com/umasterov) contributed fantastic logo for this project. * [@umasterov](https://github.com/umasterov) contributed fantastic logo for this project.
+464 -690
View File
File diff suppressed because it is too large Load Diff
+15 -4
View File
@@ -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" origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
# Paths to your key files to load automatically # Paths to your key files to load automatically
key_files = ["key1.toml", "key2.toml", "key3.toml", "key4.toml", "key5.toml"] key_files = ["key1.toml", "key2.toml", "key3.toml", "key4.toml", "key5.toml"]
@@ -10,7 +10,7 @@ check_blocks = 8
# All bootstrap nodes # All bootstrap nodes
peers = ["peer-v4.alfis.name:4244", "peer-v6.alfis.name:4244", "peer-ygg.alfis.name:4244"] 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 # 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 # Set true if you want your IP to participate in peer-exchange, or false otherwise
public = true public = true
# Allow connections to/from Yggdrasil only (https://yggdrasil-network.github.io) # Allow connections to/from Yggdrasil only (https://yggdrasil-network.github.io)
@@ -19,9 +19,15 @@ yggdrasil_only = false
# DNS resolver options # DNS resolver options
[dns] [dns]
# Your DNS resolver will be listening on this address and port (Usual port is 53) # 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 # How many threads to spawn by DNS server
threads = 10 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 # AdGuard DNS servers to filter ads and trackers
forwarders = ["https://dns.adguard.com/dns-query"] forwarders = ["https://dns.adguard.com/dns-query"]
#forwarders = ["94.140.14.14:53", "94.140.15.15:53"] #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 # Bootstrap DNS-servers to resolve domains of DoH providers
bootstraps = ["9.9.9.9:53", "94.140.14.14:53"] 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 file support (resolve local names or block ads)
#hosts = ["system", "adblock.txt"] #hosts = ["system", "adblock.txt"]
@@ -39,5 +50,5 @@ bootstraps = ["9.9.9.9:53", "94.140.14.14:53"]
[mining] [mining]
# How many CPU threads to spawn for mining, zero = number of CPU cores # How many CPU threads to spawn for mining, zero = number of CPU cores
threads = 0 threads = 0
# Set lower priority for mining threads # Set a lower priority for mining threads
lower = true lower = true
+2 -2
View File
@@ -35,10 +35,10 @@ elif [ $PKGARCH = "i686" ]; then TARGET='i686-unknown-linux-musl'
elif [ $PKGARCH = "mipsel" ]; then TARGET='mipsel-unknown-linux-musl' elif [ $PKGARCH = "mipsel" ]; then TARGET='mipsel-unknown-linux-musl'
elif [ $PKGARCH = "mips" ]; then TARGET='mips-unknown-linux-musl' elif [ $PKGARCH = "mips" ]; then TARGET='mips-unknown-linux-musl'
elif [ $PKGARCH = "armhf" ]; then TARGET='armv7-unknown-linux-musleabihf' 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' elif [ $PKGARCH = "arm64" ]; then TARGET='aarch64-unknown-linux-musl'
else else
echo "Specify PKGARCH=amd64,i686,mips,mipsel,armhf,armlf,arm64" echo "Specify PKGARCH=amd64,i686,mips,mipsel,armhf,armel,arm64"
exit 1 exit 1
fi fi
Binary file not shown.
+30 -33
View File
@@ -18,18 +18,30 @@
"i686-windows" "i686-windows"
"x86_64-windows" "x86_64-windows"
]; ];
in flake-utils.lib.eachSystem systems (system: in flake-utils.lib.eachSystem systems (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
lib = pkgs.lib;
naersk-lib = naersk.lib.${system}; naersk-lib = naersk.lib.${system};
isLinux = pkgs.stdenv.hostPlatform.isLinux;
alfis = { webgui ? true, doh ? true, edge ? false }: guiBuildInputs = lib.optionals isLinux (with pkgs; [
gtk3
webkitgtk_4_1
xdotool
libayatana-appindicator
]);
guiNativeBuildInputs = [ pkgs.pkg-config ]
++ lib.optionals isLinux [ pkgs.makeWrapper pkgs.wrapGAppsHook ];
guiRuntimeTools = lib.optionals isLinux [ pkgs.kdePackages.kdialog ];
guiRuntimeLibPath = lib.optionalString isLinux (lib.makeLibraryPath guiBuildInputs);
alfis = { webgui ? true, doh ? true }:
let let
features = builtins.concatStringsSep " " (builtins.concatMap features = builtins.concatStringsSep " " (builtins.concatMap
({ option, features }: pkgs.lib.optionals option features) [ ({ option, features }: lib.optionals option features) [
{ {
option = webgui; option = webgui;
features = [ "webgui" ]; features = [ "webgui" ];
@@ -38,55 +50,40 @@
option = doh; option = doh;
features = [ "doh" ]; features = [ "doh" ];
} }
{
option = edge;
features = [ "edge" ];
}
]); ]);
in naersk-lib.buildPackage { in naersk-lib.buildPackage {
pname = "alfis"; pname = "alfis";
nativeBuildInputs = with pkgs; [ pkg-config webkitgtk kdialog ]; root = ./.;
dontWrapQtApps = true; nativeBuildInputs = guiNativeBuildInputs;
buildInputs = guiBuildInputs;
cargoBuildOptions = opts: cargoBuildOptions = opts:
opts ++ [ "--no-default-features" ] opts ++ [ "--no-default-features" ]
++ [ "--features" ''"${features}"'' ]; ++ lib.optionals (features != "") [ "--features" features ];
root = ./.; preFixup = lib.optionalString isLinux ''
gappsWrapperArgs+=(--prefix PATH : "${lib.makeBinPath guiRuntimeTools}")
gappsWrapperArgs+=(--prefix LD_LIBRARY_PATH : "${guiRuntimeLibPath}")
'';
}; };
isWindows = builtins.elem system [ "i686-windows" "x86_64-windows" ];
in rec { in rec {
packages = { packages = {
alfis = alfis { alfis = alfis {
webgui = true; webgui = true;
doh = true; doh = true;
edge = false;
}; };
alfisWithoutGUI = alfis { alfisWithoutGUI = alfis {
webgui = false; webgui = false;
doh = true; doh = true;
edge = false;
};
} // pkgs.lib.optionalAttrs isWindows {
alfisEdge = alfis {
webgui = false;
doh = true;
edge = true;
}; };
}; };
defaultPackage = packages.alfis; defaultPackage = packages.alfis;
apps = with flake-utils.lib; apps = with flake-utils.lib; {
{ alfis = mkApp { drv = packages.alfis; };
alfis = mkApp { drv = packages.alfis; }; alfisWithoutGUI = mkApp { drv = packages.alfisWithoutGUI; };
alfisWithoutGUI = mkApp { drv = packages.alfisWithoutGUI; }; };
} // pkgs.lib.optionalAttrs isWindows {
alfisEdge = mkApp { drv = packages.alfisEdge; };
};
defaultApp = apps.alfis; defaultApp = apps.alfis;
devShell = import ./shell.nix { inherit pkgs; }; devShell = import ./shell.nix { inherit pkgs; };
}); });
} }
+17 -2
View File
@@ -1,6 +1,21 @@
{ pkgs ? import <nixpkgs> { } }: { pkgs ? import <nixpkgs> { } }:
let
runtimeLibs = with pkgs; [
gtk3
webkitgtk_4_1
xdotool
libayatana-appindicator
];
packages = with pkgs; [
cargo
rustc
pkg-config
kdePackages.kdialog
] ++ runtimeLibs;
in
pkgs.mkShell { pkgs.mkShell {
buildInputs = buildInputs = packages;
[ pkgs.cargo pkgs.rustc pkgs.webkitgtk pkgs.pkg-config pkgs.kdialog ]; LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath runtimeLibs;
} }
+2 -2
View File
@@ -3,7 +3,7 @@ extern crate serde_json;
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Debug; use std::fmt::Debug;
use bincode::config;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::blockchain::hash_utils::{hash_difficulty, key_hash_difficulty}; use crate::blockchain::hash_utils::{hash_difficulty, key_hash_difficulty};
@@ -94,7 +94,7 @@ impl Block {
/// Serializes block to bincode format for hashing. /// Serializes block to bincode format for hashing.
pub fn as_bytes_compact(&self) -> Vec<u8> { 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 /// Checks if this block is superior to the other
+19 -4
View File
@@ -975,13 +975,23 @@ impl Chain {
return false; return false;
} }
// If this signers' public key has already locked/signed that block we return error // If this signers' public key has already locked/signed that block we return error
// Use signing_keys cache to avoid repeated DB loads for the same blocks
let mut cache = self.signers.borrow_mut();
for i in (full_block.index + 1)..block.index { for i in (full_block.index + 1)..block.index {
let signer = self.get_block(i).expect("Error in DB!"); let pub_key = if let Some(cached) = cache.signing_keys.get(&i) {
if signer.pub_key == block.pub_key { cached.clone()
} else {
let signer = self.get_block(i).expect("Error in DB!");
cache.signing_keys.insert(i, signer.pub_key.clone());
signer.pub_key
};
if pub_key == block.pub_key {
warn!("Ignoring block {} from '{:?}', already signed by this key", block.index, &block.pub_key); warn!("Ignoring block {} from '{:?}', already signed by this key", block.index, &block.pub_key);
return false; return false;
} }
} }
// Cache this block's pub_key too for future checks
cache.signing_keys.insert(block.index, block.pub_key.clone());
true true
} }
@@ -1066,6 +1076,7 @@ impl Chain {
let mut signers = self.signers.borrow_mut(); let mut signers = self.signers.borrow_mut();
signers.index = block.index; signers.index = block.index;
signers.signers = result.clone(); signers.signers = result.clone();
signers.signing_keys.clear();
result result
} }
@@ -1087,12 +1098,15 @@ impl Chain {
struct SignersCache { struct SignersCache {
index: u64, index: u64,
signers: Vec<Bytes> signers: Vec<Bytes>,
/// Cache of block_index → pub_key for signing blocks since last full_block.
/// Avoids repeated DB loads in is_good_signer_for_block duplicate check.
signing_keys: HashMap<u64, Bytes>,
} }
impl SignersCache { impl SignersCache {
pub fn new() -> RefCell<SignersCache> { pub fn new() -> RefCell<SignersCache> {
let cache = SignersCache { index: 0, signers: Vec::new() }; let cache = SignersCache { index: 0, signers: Vec::new(), signing_keys: HashMap::new() };
RefCell::new(cache) RefCell::new(cache)
} }
@@ -1103,6 +1117,7 @@ impl SignersCache {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.index = 0; self.index = 0;
self.signers.clear(); self.signers.clear();
self.signing_keys.clear();
} }
} }
+147 -24
View File
@@ -1,10 +1,12 @@
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant;
#[allow(unused_imports)] #[allow(unused_imports)]
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use crate::blockchain::transaction::DomainData; use crate::blockchain::transaction::DomainData;
use crate::commons::rtt_tracker::RttTracker;
use crate::dns::filter::DnsFilter; use crate::dns::filter::DnsFilter;
use crate::dns::protocol::{DnsPacket, DnsQuestion, DnsRecord, QueryType, ResultCode, TransientTtl}; use crate::dns::protocol::{DnsPacket, DnsQuestion, DnsRecord, QueryType, ResultCode, TransientTtl};
use crate::Context; use crate::Context;
@@ -14,12 +16,16 @@ const NAME_SERVER: &str = "ns.alfis.name";
const SERVER_ADMIN: &str = "admin.alfis.name"; const SERVER_ADMIN: &str = "admin.alfis.name";
pub struct BlockchainFilter { pub struct BlockchainFilter {
context: Arc<Mutex<Context>> context: Arc<Mutex<Context>>,
ns_tracker: Arc<RttTracker<IpAddr>>,
} }
impl BlockchainFilter { impl BlockchainFilter {
pub fn new(context: Arc<Mutex<Context>>) -> Self { 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) { fn add_soa_record(zone: String, serial: u32, packet: &mut DnsPacket) {
@@ -44,36 +50,54 @@ impl BlockchainFilter {
have_zone have_zone
} }
fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &Vec<IpAddr>) -> Option<DnsPacket> { fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &[IpAddr], tracker: &RttTracker<IpAddr>) -> Option<DnsPacket> {
let port = 10000 + (rand::random::<u16>() % 50000); // Disable 0x20 encoding for NS queries - external NS servers may not preserve case
let mut dns_client = DnsNetworkClient::new(port); let mut dns_client = DnsNetworkClient::new_with_0x20(false);
dns_client.run().unwrap(); dns_client.run().unwrap();
let timeout = std::time::Duration::from_secs(2);
for server in servers { let ordered = tracker.select_ordered(servers);
let addr = SocketAddr::new(server.to_owned(), 53);
if let Ok(res) = dns_client.send_udp_query(qname, qtype, addr, false) { for server in &ordered {
dns_client.stop(); let addr = SocketAddr::new(*server, 53);
return Some(res); 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(); dns_client.stop();
None 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() { if !answers.is_empty() {
// Create DnsPacket // Create DnsPacket with answers
let mut packet = DnsPacket::new(); let mut packet = DnsPacket::new();
packet.header.authoritative_answer = true; packet.header.authoritative_answer = true;
packet.questions.push(DnsQuestion::new(String::from(qname), qtype)); packet.questions.push(DnsQuestion::new(String::from(qname), qtype));
for answer in answers { for answer in answers {
packet.answers.push(answer); 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); //trace!("Returning packet: {:?}", &packet);
Some(packet) Some(packet)
} else { } else {
// Create DnsPacket // Create DnsPacket without answers
let mut packet = DnsPacket::new(); let mut packet = DnsPacket::new();
packet.header.authoritative_answer = true; packet.header.authoritative_answer = true;
packet.header.rescode = ResultCode::NXDOMAIN; packet.header.rescode = ResultCode::NXDOMAIN;
@@ -85,7 +109,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 // First we search for NS records, collecting nameserver domains
let mut hosts = Vec::new(); let mut hosts = Vec::new();
for record in data.records.iter() { for record in data.records.iter() {
@@ -103,7 +127,27 @@ impl BlockchainFilter {
return (false, None); 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(); let mut servers = Vec::new();
for record in data.records.iter() { for record in data.records.iter() {
match &record { match &record {
@@ -129,7 +173,7 @@ impl BlockchainFilter {
if !servers.is_empty() { if !servers.is_empty() {
trace!("Found NS servers for domain {}: {:?}", &qname, &servers); 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 { if let Some(packet) = &answer {
trace!("Resolved {:?} from NS: {:?}", (qname, qtype), &packet.answers); trace!("Resolved {:?} from NS: {:?}", (qname, qtype), &packet.answers);
} }
@@ -138,13 +182,76 @@ impl BlockchainFilter {
(false, None) (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 { 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 top_domain;
let subdomain; let subdomain;
let parts: Vec<&str> = qname.rsplitn(3, '.').collect(); let parts: Vec<&str> = qname_lower.rsplitn(3, '.').collect();
match parts.len() { match parts.len() {
1 => { 1 => {
let mut packet = DnsPacket::new(); let mut packet = DnsPacket::new();
@@ -192,9 +299,12 @@ impl DnsFilter for BlockchainFilter {
}; };
// Check if this domain has NS records and needs to resolve all records through them // 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); // But skip this if we're querying for NS records themselves - return them directly
if has_ns { if qtype != QueryType::NS {
return result; 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(); let mut answers: Vec<DnsRecord> = Vec::new();
@@ -237,7 +347,7 @@ impl DnsFilter for BlockchainFilter {
let mut domain_exists = !answers.is_empty() || subdomain.is_empty(); let mut domain_exists = !answers.is_empty() || subdomain.is_empty();
if answers.is_empty() { if answers.is_empty() {
// If there are no records found we search for *.domain.tld record // 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()); let record_domain = record.get_domain().unwrap_or(String::new());
if record.get_querytype() == qtype && record_domain == "*" { if record.get_querytype() == qtype && record_domain == "*" {
match &mut record { match &mut record {
@@ -263,7 +373,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() { if domain_exists && packet.answers.is_empty() {
packet.header.rescode = ResultCode::NOERROR; packet.header.rescode = ResultCode::NOERROR;
} }
+1 -1
View File
@@ -45,7 +45,7 @@ pub const UI_REFRESH_DELAY_MS: u128 = 500;
pub const LOG_REFRESH_DELAY_SEC: u64 = 60; pub const LOG_REFRESH_DELAY_SEC: u64 = 60;
pub const POLL_TIMEOUT: Option<Duration> = Some(Duration::from_millis(200)); 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 /// We start syncing blocks only when we got 4 and more connected nodes
pub const MIN_CONNECTED_NODES_START_SYNC: usize = 4; pub const MIN_CONNECTED_NODES_START_SYNC: usize = 4;
pub const MAX_READ_BLOCK_TIME: u128 = 100; pub const MAX_READ_BLOCK_TIME: u128 = 100;
+2
View File
@@ -10,6 +10,7 @@ use crate::dns::protocol::DnsRecord;
pub mod constants; pub mod constants;
pub mod eventbus; pub mod eventbus;
pub mod rtt_tracker;
pub mod simplebus; pub mod simplebus;
/// Convert bytes array to HEX format /// Convert bytes array to HEX format
@@ -128,6 +129,7 @@ pub fn is_yggdrasil_record(record: &DnsRecord) -> bool {
DnsRecord::SRV { .. } => {} DnsRecord::SRV { .. } => {}
DnsRecord::OPT { .. } => {} DnsRecord::OPT { .. } => {}
DnsRecord::TLSA { .. } => {} DnsRecord::TLSA { .. } => {}
DnsRecord::HTTPS { .. } => {}
} }
true true
} }
+110
View File
@@ -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();
}
}
+9 -4
View File
@@ -15,9 +15,14 @@ pub struct Chacha {
impl Chacha { impl Chacha {
pub fn new(key: &[u8], nonce: &[u8]) -> Self { pub fn new(key: &[u8], nonce: &[u8]) -> Self {
let key = Key::from_slice(key); // Convert slices to fixed-size arrays, then to GenericArray
let cipher = ChaCha20Poly1305::new(key); let key_array: [u8; 32] = key.try_into().expect("Key must be 32 bytes");
let nonce = Nonce::clone_from_slice(nonce); 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 } Chacha { cipher, nonce }
} }
@@ -30,7 +35,7 @@ impl Chacha {
} }
pub fn get_nonce(&self) -> &[u8] { pub fn get_nonce(&self) -> &[u8] {
&self.nonce.as_slice() self.nonce.as_ref()
} }
} }
+19 -8
View File
@@ -136,7 +136,7 @@ pub trait PacketBuffer {
outstr.push_str(delim); outstr.push_str(delim);
let str_buffer = self.get_range(pos, len as usize)?; 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 = "."; delim = ".";
@@ -166,6 +166,9 @@ impl VectorPacketBuffer {
impl PacketBuffer for VectorPacketBuffer { impl PacketBuffer for VectorPacketBuffer {
fn read(&mut self) -> Result<u8> { fn read(&mut self) -> Result<u8> {
if self.pos >= self.buffer.len() {
return Err(BufferError::EndOfBuffer);
}
let res = self.buffer[self.pos]; let res = self.buffer[self.pos];
self.pos += 1; self.pos += 1;
@@ -173,10 +176,16 @@ impl PacketBuffer for VectorPacketBuffer {
} }
fn get(&mut self, pos: usize) -> Result<u8> { fn get(&mut self, pos: usize) -> Result<u8> {
if pos >= self.buffer.len() {
return Err(BufferError::EndOfBuffer);
}
Ok(self.buffer[pos]) Ok(self.buffer[pos])
} }
fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> { fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
if start + len > self.buffer.len() {
return Err(BufferError::EndOfBuffer);
}
Ok(&self.buffer[start..start + len as usize]) Ok(&self.buffer[start..start + len as usize])
} }
@@ -226,7 +235,7 @@ where T: Read {
} }
impl<'a, T> StreamPacketBuffer<'a, T> where T: Read + 'a { 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 { StreamPacketBuffer {
stream, stream,
buffer: Vec::new(), buffer: Vec::new(),
@@ -300,14 +309,16 @@ impl<'a, T> PacketBuffer for StreamPacketBuffer<'a, T> where T: Read + 'a {
} }
} }
const BUF_SIZE: usize = 4096;
pub struct BytePacketBuffer { pub struct BytePacketBuffer {
pub buf: [u8; 512], pub buf: [u8; BUF_SIZE],
pub pos: usize pub pos: usize
} }
impl BytePacketBuffer { impl BytePacketBuffer {
pub fn new() -> BytePacketBuffer { pub fn new() -> BytePacketBuffer {
BytePacketBuffer { buf: [0; 512], pos: 0 } BytePacketBuffer { buf: [0; BUF_SIZE], pos: 0 }
} }
} }
@@ -319,7 +330,7 @@ impl Default for BytePacketBuffer {
impl PacketBuffer for BytePacketBuffer { impl PacketBuffer for BytePacketBuffer {
fn read(&mut self) -> Result<u8> { fn read(&mut self) -> Result<u8> {
if self.pos >= 512 { if self.pos >= BUF_SIZE {
return Err(BufferError::EndOfBuffer); return Err(BufferError::EndOfBuffer);
} }
let res = self.buf[self.pos]; let res = self.buf[self.pos];
@@ -329,21 +340,21 @@ impl PacketBuffer for BytePacketBuffer {
} }
fn get(&mut self, pos: usize) -> Result<u8> { fn get(&mut self, pos: usize) -> Result<u8> {
if pos >= 512 { if pos >= BUF_SIZE {
return Err(BufferError::EndOfBuffer); return Err(BufferError::EndOfBuffer);
} }
Ok(self.buf[pos]) Ok(self.buf[pos])
} }
fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> { fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
if start + len >= 512 { if start + len >= BUF_SIZE {
return Err(BufferError::EndOfBuffer); return Err(BufferError::EndOfBuffer);
} }
Ok(&self.buf[start..start + len as usize]) Ok(&self.buf[start..start + len as usize])
} }
fn write(&mut self, val: u8) -> Result<()> { fn write(&mut self, val: u8) -> Result<()> {
if self.pos >= 512 { if self.pos >= BUF_SIZE {
return Err(BufferError::EndOfBuffer); return Err(BufferError::EndOfBuffer);
} }
self.buf[self.pos] = val; self.buf[self.pos] = val;
+302 -18
View File
@@ -2,16 +2,82 @@
extern crate serde; extern crate serde;
use std::clone::Clone; use std::clone::Clone;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use lru::LruCache;
use chrono::*; use chrono::*;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::dns::protocol::{DnsPacket, DnsRecord, QueryType, ResultCode}; 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)] #[derive(Debug, Display, From, Error)]
pub enum CacheError { pub enum CacheError {
Io(std::io::Error), 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 now = Local::now();
let current_set = match self.record_types.get(&qtype) { let current_set = match self.record_types.get(&qtype) {
@@ -149,21 +215,71 @@ impl DomainEntry {
} }
if entry.record.get_querytype() == qtype { 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 { 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 { impl Cache {
pub fn new() -> 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 { 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) { 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 { if increment_stats {
domain_entry.hits += 1 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> { 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 => { CacheState::PositiveCache => {
let mut qr = DnsPacket::new(); let mut qr = DnsPacket::new();
self.fill_queryresult(qname, qtype, &mut qr.answers, true); self.fill_queryresult(qname, qtype, &mut qr.answers, true);
@@ -208,38 +328,90 @@ impl Cache {
Some(x) => x, Some(x) => x,
None => continue 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); 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; continue;
} }
let mut rs = DomainEntry::new(domain.clone()); // Insert new entry
let mut rs = DomainEntry::new(domain_lower.clone());
rs.store_record(rec); 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) { 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); 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; 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); 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 struct SynchronizedCache {
pub cache: RwLock<Cache> pub cache: RwLock<Cache>
} }
impl SynchronizedCache { impl SynchronizedCache {
pub fn new() -> 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>>> { pub fn list(&self) -> Result<Vec<Arc<DomainEntry>>> {
@@ -247,7 +419,7 @@ impl SynchronizedCache {
let mut list = Vec::new(); let mut list = Vec::new();
for rs in cache.domain_entries.values() { for (_, rs) in cache.domain_entries.iter() {
list.push(rs.clone()); 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().updates);
assert_eq!(1, cache.domain_entries.get(&"www.microsoft.com".to_string()).unwrap().hits); 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");
}
}
} }
+202 -94
View File
@@ -4,18 +4,18 @@ use std::io::Write;
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
use std::io::Read; use std::io::Read;
use std::marker::{Send, Sync}; use std::marker::{Send, Sync};
use std::net::{SocketAddr, TcpStream, ToSocketAddrs, UdpSocket}; use std::net::{Ipv4Addr, SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
use std::net::IpAddr; use std::net::IpAddr;
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
use std::num::NonZeroUsize; 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::mpsc::{channel, Sender};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
use std::sync::RwLock; use std::sync::RwLock;
use std::thread::{sleep, Builder}; use std::thread::{sleep, Builder};
use std::time::Duration as SleepDuration; use std::time::Duration;
use chrono::*; use chrono::*;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
@@ -32,6 +32,18 @@ use crate::dns::protocol::{DnsPacket, DnsQuestion, QueryType};
use crate::dns::protocol::DnsRecord; use crate::dns::protocol::DnsRecord;
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
use lru::LruCache; 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)] #[derive(Debug, Display, From, Error)]
pub enum ClientError { pub enum ClientError {
@@ -65,7 +77,10 @@ pub struct DnsNetworkClient {
total_failed: AtomicUsize, total_failed: AtomicUsize,
/// Counter for assigning packet ids /// Counter for assigning packet ids
seq: AtomicUsize, seq: AtomicU16,
/// Enable DNS 0x20 encoding for additional security
enable_0x20: bool,
/// The requesting socket for IPv4 /// The requesting socket for IPv4
socket_ipv4: UdpSocket, socket_ipv4: UdpSocket,
@@ -86,6 +101,8 @@ pub struct DnsNetworkClient {
struct PendingQuery { struct PendingQuery {
seq: u16, seq: u16,
timestamp: DateTime<Local>, timestamp: DateTime<Local>,
/// The query name with 0x20 encoding applied (for validation)
query_name: String,
tx: Sender<Option<DnsPacket>> tx: Sender<Option<DnsPacket>>
} }
@@ -94,18 +111,43 @@ unsafe impl Send for DnsNetworkClient {}
unsafe impl Sync for DnsNetworkClient {} unsafe impl Sync for DnsNetworkClient {}
impl 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 { DnsNetworkClient {
total_sent: AtomicUsize::new(0), total_sent: AtomicUsize::new(0),
total_failed: AtomicUsize::new(0), total_failed: AtomicUsize::new(0),
seq: AtomicUsize::new(0), seq: AtomicU16::new(rand::random::<u16>()),
socket_ipv4: UdpSocket::bind(format!("0.0.0.0:{}", port)).expect("Error binding IPv4"), enable_0x20,
socket_ipv6: UdpSocket::bind(format!("[::]:{}", port + 1)).expect("Error binding IPv6"), socket_ipv4,
socket_ipv6,
pending_queries: Arc::new(Mutex::new(Vec::new())), pending_queries: Arc::new(Mutex::new(Vec::new())),
stopped: Arc::new(AtomicBool::new(false)) 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 /// Send a DNS query using TCP transport
/// ///
/// This is much simpler than using UDP, since the kernel will take care of /// 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 /// 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 /// 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 /// 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 /// method is thread-safe and can be used from any number of threads in
/// parallel. /// 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); 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 // Prepare request
let mut packet = DnsPacket::new(); let mut packet = DnsPacket::new();
@@ -165,18 +214,19 @@ impl DnsNetworkClient {
packet.header.questions = 1; packet.header.questions = 1;
packet.header.recursion_desired = recursive; 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 (tx, rx) = channel();
{ {
let mut pending_queries = self.pending_queries.lock().map_err(|_| ClientError::PoisonedLock)?; 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(); 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"); let addr: SocketAddr = server.to_socket_addrs()?.next().expect("Wrong resolver address");
match addr { match addr {
SocketAddr::V4(addr) => { SocketAddr::V4(addr) => {
@@ -187,8 +237,8 @@ impl DnsNetworkClient {
} }
} }
// Wait for response // Wait for response with timeout
match rx.recv() { match rx.recv_timeout(timeout) {
Ok(Some(qr)) => Ok(qr), Ok(Some(qr)) => Ok(qr),
Ok(None) => { Ok(None) => {
let _ = self.total_failed.fetch_add(1, Ordering::Release); let _ = self.total_failed.fetch_add(1, Ordering::Release);
@@ -196,7 +246,7 @@ impl DnsNetworkClient {
} }
Err(_) => { Err(_) => {
let _ = self.total_failed.fetch_add(1, Ordering::Release); 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 /// The run method launches a worker thread. Unless this thread is running, no
/// responses will ever be generated, and clients will just block indefinitely. /// responses will ever be generated, and clients will just block indefinitely.
fn run(&self) -> Result<()> { fn run(&self) -> Result<()> {
let timeout = Some(std::time::Duration::from_millis(500));
// Start the thread for handling incoming responses // Start the thread for handling incoming responses
{ {
let socket_copy = self.socket_ipv4.try_clone()?; 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 pending_queries_lock = self.pending_queries.clone();
let stopped = Arc::clone(&self.stopped); let stopped = Arc::clone(&self.stopped);
let match_case = self.enable_0x20;
Builder::new() Builder::new()
.name("DnsNetworkClient-worker-thread".into()) .name("DnsNetworkClient-worker-thread-v4".into())
.spawn(move || { .spawn(move || {
loop { loop {
if stopped.load(Ordering::SeqCst) { if stopped.load(Ordering::SeqCst) {
@@ -232,7 +283,9 @@ impl DnsClient for DnsNetworkClient {
// Read data into a buffer // Read data into a buffer
let mut res_buffer = BytePacketBuffer::new(); 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(_) => {} Ok(_) => {}
Err(_) => { Err(_) => {
continue; 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. // matching PendingQuery to which to deliver the response.
if let Ok(mut pending_queries) = pending_queries_lock.lock() { if let Ok(mut pending_queries) = pending_queries_lock.lock() {
let mut matched_query = None; let mut matched_query = None;
for (i, pending_query) in pending_queries.iter().enumerate() { for (i, pending_query) in pending_queries.iter().enumerate() {
if pending_query.seq == packet.header.id { 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())); let _ = pending_query.tx.send(Some(packet.clone()));
// Mark this index for removal from list // Mark this index for removal from list
@@ -267,7 +331,7 @@ impl DnsClient for DnsNetworkClient {
if let Some(idx) = matched_query { if let Some(idx) = matched_query {
pending_queries.remove(idx); pending_queries.remove(idx);
} else { } 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 // Start the same thread for IPv6
{ {
let socket_copy = self.socket_ipv6.try_clone()?; 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 pending_queries_lock = self.pending_queries.clone();
let stopped = Arc::clone(&self.stopped); let stopped = Arc::clone(&self.stopped);
let match_case = self.enable_0x20;
Builder::new() Builder::new()
.name("DnsNetworkClient-worker-thread".into()) .name("DnsNetworkClient-worker-thread-v6".into())
.spawn(move || { .spawn(move || {
loop { loop {
if stopped.load(Ordering::SeqCst) { if stopped.load(Ordering::SeqCst) {
@@ -291,7 +357,9 @@ impl DnsClient for DnsNetworkClient {
// Read data into a buffer // Read data into a buffer
let mut res_buffer = BytePacketBuffer::new(); 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(_) => {} Ok(_) => {}
Err(_) => { Err(_) => {
continue; continue;
@@ -313,7 +381,18 @@ impl DnsClient for DnsNetworkClient {
let mut matched_query = None; let mut matched_query = None;
for (i, pending_query) in pending_queries.iter().enumerate() { for (i, pending_query) in pending_queries.iter().enumerate() {
if pending_query.seq == packet.header.id { 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())); let _ = pending_query.tx.send(Some(packet.clone()));
// Mark this index for removal from list // Mark this index for removal from list
@@ -326,7 +405,7 @@ impl DnsClient for DnsNetworkClient {
if let Some(idx) = matched_query { if let Some(idx) = matched_query {
pending_queries.remove(idx); pending_queries.remove(idx);
} else { } 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() Builder::new()
.name("DnsNetworkClient-timeout-thread".into()) .name("DnsNetworkClient-timeout-thread".into())
.spawn(move || { .spawn(move || {
let timeout = Duration::seconds(5);
loop { loop {
if stopped.load(Ordering::SeqCst) { if stopped.load(Ordering::SeqCst) {
break; break;
@@ -349,7 +427,7 @@ impl DnsClient for DnsNetworkClient {
if let Ok(mut pending_queries) = pending_queries_lock.lock() { if let Ok(mut pending_queries) = pending_queries_lock.lock() {
let mut finished_queries = Vec::new(); let mut finished_queries = Vec::new();
for (i, pending_query) in pending_queries.iter().enumerate() { 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() { if expires < Local::now() {
let _ = pending_query.tx.send(None); let _ = pending_query.tx.send(None);
finished_queries.push(i); 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> { 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 { if !packet.header.truncated_message {
return Ok(packet); return Ok(packet);
} }
@@ -387,9 +465,9 @@ impl DnsClient for DnsNetworkClient {
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
pub struct HttpsDnsClient { pub struct HttpsDnsClient {
agent: ureq::Agent, agent: Agent,
/// Counter for assigning packet ids /// Counter for assigning packet ids
seq: AtomicUsize, seq: AtomicU16,
} }
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
@@ -402,60 +480,88 @@ impl HttpsDnsClient {
.collect::<Vec<SocketAddr>>(); .collect::<Vec<SocketAddr>>();
trace!("Using bootstraps: {:?}", &servers); 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: LruCache<String, Vec<SocketAddr>> = LruCache::new(NonZeroUsize::new(10).unwrap());
let cache = RwLock::new(cache); let cache = RwLock::new(cache);
Self { servers, cache }
}
}
let agent = ureq::AgentBuilder::new() #[cfg(feature = "doh")]
.user_agent(&client_name) impl Resolver for BootstrapResolver {
.timeout(std::time::Duration::from_secs(3)) fn resolve(&self, uri: &Uri, _config: &Config, timeout: NextTimeout) -> std::result::Result<ResolvedSocketAddrs, ureq::Error> {
.max_idle_connections_per_host(4) let domain = uri.host().unwrap_or("localhost");
.max_idle_connections(16) let port = uri.port_u16().unwrap_or(443);
.resolver(move |addr: &str| { let addr = match domain.find(':') {
let addr = match addr.find(':') { Some(index) => domain[0..index].to_string(),
Some(index) => addr[0..index].to_string(), None => domain.to_string()
None => addr.to_string() };
}; let timeout_duration = Duration::from_millis(timeout.after.as_millis() as u64);
trace!("Resolving {}", addr); trace!("Resolving {}", addr);
if let Some(addrs) = cache.write().unwrap().get(&addr) { if let Some(addrs) = self.cache.write().unwrap().get(&addr) {
trace!("Found bootstrap ip in cache"); trace!("Found bootstrap ip in cache");
return Ok(addrs.clone()); 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();
let mut dns_client = DnsNetworkClient::new(port); dns_client.run().unwrap();
dns_client.run().unwrap();
let mut result: Vec<IpAddr> = Vec::new(); let mut result: Vec<IpAddr> = Vec::new();
for server in &servers { let mut results: ResolvedSocketAddrs = ArrayVec::from_fn(|_| SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0));
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true) { for server in &self.servers {
for answer in &res.answers { if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true, timeout_duration) {
if let DnsRecord::A { addr, .. } = answer { for answer in &res.answers {
result.push(IpAddr::V4(*addr)) if let DnsRecord::A { addr, .. } = answer {
} results.push(SocketAddr::new(IpAddr::V4(*addr), port));
} 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))
}
}
} }
} }
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.sort();
result.dedup(); result.dedup();
let addrs = result let addrs = result
.into_iter() .into_iter()
.map(|ip| SocketAddr::new(ip, 443)) .map(|ip| SocketAddr::new(ip, port))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
trace!("Resolved addresses: {:?}", &addrs); trace!("Resolved addresses: {:?}", &addrs);
cache.write().unwrap().put(addr, addrs.clone()); self.cache.write().unwrap().put(addr, addrs.clone());
Ok(addrs) Ok(results)
})
.build();
Self { agent, seq: AtomicUsize::new(1) }
} }
} }
@@ -497,21 +603,21 @@ impl DnsClient for HttpsDnsClient {
let response = self.agent let response = self.agent
.post(doh_url) .post(doh_url)
.set("Content-Type", "application/dns-message") .header("Content-Type", "application/dns-message")
.send_bytes(req_buffer.buffer.as_slice()); .send(req_buffer.buffer.as_slice());
match response { match response {
Ok(response) => { Ok(response) => {
match response.status() { match response.status().as_u16() {
200 => { 200 => {
match response.header("Content-Length") { match response.headers().get("Content-Length") {
None => warn!("No 'Content-Length' header in DoH response!"), None => warn!("No 'Content-Length' header in DoH response!"),
Some(str) => { Some(str) => {
match str.parse::<usize>() { match str.to_str().unwrap_or("0").parse::<usize>() {
Ok(size) => { Ok(size) => {
let mut bytes: Vec<u8> = Vec::with_capacity(size); let mut bytes: Vec<u8> = Vec::with_capacity(size);
response.into_reader() response.into_body().into_reader()
.take(4096) .take(65535)
.read_to_end(&mut bytes)?; .read_to_end(&mut bytes)?;
let mut buffer = VectorPacketBuffer::new(); let mut buffer = VectorPacketBuffer::new();
buffer.buffer.extend_from_slice(bytes.as_slice()); buffer.buffer.extend_from_slice(bytes.as_slice());
@@ -580,10 +686,11 @@ pub mod tests {
#[test] #[test]
pub fn test_udp_client() { 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(); 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_eq!(res.questions[0].name, "google.com");
assert!(res.answers.len() > 0); assert!(res.answers.len() > 0);
@@ -598,7 +705,8 @@ pub mod tests {
#[test] #[test]
pub fn test_tcp_client() { 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(); 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"); assert_eq!(res.questions[0].name, "google.com");
+12 -8
View File
@@ -5,6 +5,7 @@ use std::sync::Arc;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use crate::commons::rtt_tracker::RttTracker;
use crate::dns::authority::Authority; use crate::dns::authority::Authority;
use crate::dns::cache::SynchronizedCache; use crate::dns::cache::SynchronizedCache;
use crate::dns::client::{DnsClient, DnsNetworkClient}; use crate::dns::client::{DnsClient, DnsNetworkClient};
@@ -56,18 +57,19 @@ pub struct ServerContext {
pub enable_tcp: bool, pub enable_tcp: bool,
pub enable_api: bool, pub enable_api: bool,
pub statistics: ServerStatistics, pub statistics: ServerStatistics,
pub zones_dir: &'static str pub zones_dir: &'static str,
pub forwarder_tracker: Arc<RttTracker<String>>,
} }
impl Default for ServerContext { impl Default for ServerContext {
fn default() -> Self { 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 { impl ServerContext {
#[allow(unused_variables)] #[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"))] #[cfg(not(feature = "doh"))]
let doh_client = None; let doh_client = None;
#[cfg(feature = "doh")] #[cfg(feature = "doh")]
@@ -75,9 +77,9 @@ impl ServerContext {
ServerContext { ServerContext {
authority: Authority::new(), authority: Authority::new(),
cache: SynchronizedCache::new(), cache: SynchronizedCache::with_memory_limit(cache_limit_mb),
filters: Vec::new(), 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, doh_client,
dns_listen, dns_listen,
api_port: 5380, api_port: 5380,
@@ -87,7 +89,8 @@ impl ServerContext {
enable_tcp: true, enable_tcp: true,
enable_api: false, enable_api: false,
statistics: ServerStatistics { tcp_query_count: AtomicUsize::new(0), udp_query_count: AtomicUsize::new(0) }, 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> { pub fn create_test_context(callback: Box<StubCallback>) -> Arc<ServerContext> {
Arc::new(ServerContext { Arc::new(ServerContext {
authority: Authority::new(), authority: Authority::new(),
cache: SynchronizedCache::new(), cache: SynchronizedCache::with_memory_limit(0), // Unlimited for tests
filters: Vec::new(), filters: Vec::new(),
old_client: Box::new(DnsStubClient::new(callback)), old_client: Box::new(DnsStubClient::new(callback)),
doh_client: Some(Box::new(HttpsDnsClient::new(Vec::new()))), doh_client: Some(Box::new(HttpsDnsClient::new(Vec::new()))),
@@ -141,7 +144,8 @@ pub mod tests {
enable_tcp: true, enable_tcp: true,
enable_api: false, enable_api: false,
statistics: ServerStatistics { tcp_query_count: AtomicUsize::new(0), udp_query_count: AtomicUsize::new(0) }, 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
View File
@@ -1,14 +1,14 @@
use crate::dns::protocol::{DnsPacket, QueryType}; use crate::dns::protocol::{DnsPacket, QueryType};
pub trait DnsFilter { 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 {} pub struct DummyFilter {}
#[allow(unused_variables)] #[allow(unused_variables)]
impl DnsFilter for DummyFilter { 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 None
} }
} }
+1 -1
View File
@@ -53,7 +53,7 @@ impl HostsFilter {
} }
impl DnsFilter for 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(); let mut packet = DnsPacket::new();
if let Some(list) = self.hosts.get(qname) { if let Some(list) = self.hosts.get(qname) {
for addr in list { for addr in list {
+143 -7
View File
@@ -38,6 +38,7 @@ pub enum QueryType {
SRV, // 33 SRV, // 33
OPT, // 41 OPT, // 41
TLSA, // 52 TLSA, // 52
HTTPS, // 65
} }
impl QueryType { impl QueryType {
@@ -55,6 +56,7 @@ impl QueryType {
QueryType::SRV => 33, QueryType::SRV => 33,
QueryType::OPT => 41, QueryType::OPT => 41,
QueryType::TLSA => 52, QueryType::TLSA => 52,
QueryType::HTTPS => 65,
} }
} }
@@ -71,6 +73,7 @@ impl QueryType {
33 => QueryType::SRV, 33 => QueryType::SRV,
41 => QueryType::OPT, 41 => QueryType::OPT,
52 => QueryType::TLSA, 52 => QueryType::TLSA,
65 => QueryType::HTTPS,
_ => QueryType::UNKNOWN(num), _ => QueryType::UNKNOWN(num),
} }
} }
@@ -172,6 +175,48 @@ pub enum DnsRecord {
data: Vec<u8>, data: Vec<u8>,
ttl: TransientTtl ttl: TransientTtl
}, // 52 }, // 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 { impl DnsRecord {
@@ -267,11 +312,17 @@ impl DnsRecord {
} }
QueryType::TXT => { QueryType::TXT => {
let mut txt = String::new(); let mut txt = String::new();
let end_pos = buffer.pos() + data_len as usize;
let cur_pos = buffer.pos(); // TXT RDATA consists of one or more <length><text> segments (RFC 1035 3.3.14)
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, data_len as usize)?)); while buffer.pos() < end_pos {
let seg_len = buffer.read()? as usize;
buffer.step(data_len 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) }) Ok(DnsRecord::TXT { domain, data: txt, ttl: TransientTtl(ttl) })
} }
@@ -294,6 +345,34 @@ impl DnsRecord {
buffer.step(data_len as usize)?; buffer.step(data_len as usize)?;
Ok(DnsRecord::TLSA { domain, certificate_usage, selector, matching_type, data, ttl: TransientTtl(ttl) }) 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(_) => { QueryType::UNKNOWN(_) => {
buffer.step(data_len as usize)?; buffer.step(data_len as usize)?;
@@ -452,6 +531,38 @@ impl DnsRecord {
buffer.write_u8(*b)?; 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 } => { DnsRecord::OPT { packet_len, flags, ref data } => {
buffer.write_u8(0)?; buffer.write_u8(0)?;
buffer.write_u16(QueryType::OPT.to_num())?; buffer.write_u16(QueryType::OPT.to_num())?;
@@ -485,6 +596,7 @@ impl DnsRecord {
DnsRecord::TXT { .. } => QueryType::TXT, DnsRecord::TXT { .. } => QueryType::TXT,
DnsRecord::OPT { .. } => QueryType::OPT, DnsRecord::OPT { .. } => QueryType::OPT,
DnsRecord::TLSA { .. } => QueryType::TLSA, DnsRecord::TLSA { .. } => QueryType::TLSA,
DnsRecord::HTTPS { .. } => QueryType::HTTPS,
} }
} }
@@ -500,11 +612,30 @@ impl DnsRecord {
| DnsRecord::UNKNOWN { ref domain, .. } | DnsRecord::UNKNOWN { ref domain, .. }
| DnsRecord::SOA { ref domain, .. } | DnsRecord::SOA { ref domain, .. }
| DnsRecord::TXT { 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 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> { pub fn get_data(&self) -> Option<String> {
match *self { match *self {
DnsRecord::A { ref addr, .. } => Some(addr.to_string()), DnsRecord::A { ref addr, .. } => Some(addr.to_string()),
@@ -526,6 +657,10 @@ impl DnsRecord {
let data = crate::commons::to_hex(data); let data = crate::commons::to_hex(data);
Some(format!("{} {} {} {} {}", domain, certificate_usage, selector, matching_type, &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, DnsRecord::OPT { .. } => None,
} }
} }
@@ -541,8 +676,9 @@ impl DnsRecord {
| DnsRecord::MX { ttl: TransientTtl(ttl), .. } | DnsRecord::MX { ttl: TransientTtl(ttl), .. }
| DnsRecord::UNKNOWN { ttl: TransientTtl(ttl), .. } | DnsRecord::UNKNOWN { ttl: TransientTtl(ttl), .. }
| DnsRecord::SOA { ttl: TransientTtl(ttl), .. } | DnsRecord::SOA { ttl: TransientTtl(ttl), .. }
| DnsRecord::TXT { ttl: TransientTtl(ttl), .. } => ttl, | DnsRecord::TXT { ttl: TransientTtl(ttl), .. }
| DnsRecord::TLSA { ttl: TransientTtl(ttl), .. } => ttl, | DnsRecord::TLSA { ttl: TransientTtl(ttl), .. }
| DnsRecord::HTTPS { ttl: TransientTtl(ttl), .. } => ttl,
DnsRecord::OPT { .. } => 0 DnsRecord::OPT { .. } => 0
} }
} }
+98 -33
View File
@@ -2,10 +2,10 @@
//! incoming queries //! incoming queries
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
use std::vec::Vec; use std::vec::Vec;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use rand::seq::IteratorRandom;
use crate::dns::context::ServerContext; use crate::dns::context::ServerContext;
use crate::dns::protocol::{DnsPacket, QueryType, ResultCode}; use crate::dns::protocol::{DnsPacket, QueryType, ResultCode};
@@ -53,7 +53,7 @@ pub trait DnsResolver {
} }
for filter in context.filters.iter() { 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)?; context.cache.store(&packet.answers)?;
return Ok(packet); return Ok(packet);
} }
@@ -85,27 +85,55 @@ impl DnsResolver for ForwardingDnsResolver {
} }
fn perform(&mut self, qname: &str, qtype: QueryType) -> Result<DnsPacket> { fn perform(&mut self, qname: &str, qtype: QueryType) -> Result<DnsPacket> {
let mut random = rand::thread_rng(); if let Some(packet) = self.context.cache.lookup(qname, qtype) {
let upstream = self.upstreams.iter().choose(&mut random).unwrap(); return Ok(packet);
let result = match self.context.cache.lookup(qname, qtype) { }
None => {
if is_url(upstream) { let ordered = self.context.forwarder_tracker.select_ordered(&self.upstreams);
if let Some(client) = &self.context.doh_client { let mut last_err = ResolveError::NoServerFound;
client.send_query(qname, qtype, upstream, true)?
} else { for upstream in &ordered {
log::error!("This build doesn't support DoH"); let start = Instant::now();
return Err(ResolveError::NoServerFound); let query_result = if is_url(upstream) {
} if let Some(client) = &self.context.doh_client {
client.send_query(qname, qtype, upstream, true)
} else { } else {
self.context.old_client.send_query(qname, qtype, upstream, true)? log::error!("This build doesn't support DoH");
continue;
} }
}, } else {
Some(packet) => packet 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);
Ok(result) // Fix domain names to match original query case before caching
let qname_lower = qname.to_lowercase();
for rec in result.answers.iter_mut()
.chain(result.authorities.iter_mut())
.chain(result.resources.iter_mut())
{
if let Some(domain) = rec.get_domain() {
if domain.to_lowercase() == qname_lower {
rec.set_domain(qname.to_string());
}
}
}
self.context.cache.store(&result.answers)?;
return Ok(result);
}
Err(e) => {
self.context.forwarder_tracker.record_failure(upstream);
last_err = e.into();
}
}
}
Err(last_err)
} }
} }
@@ -169,7 +197,18 @@ impl DnsResolver for RecursiveDnsResolver {
let _ = self.context.cache.store(&response.answers); let _ = self.context.cache.store(&response.answers);
let _ = self.context.cache.store(&response.authorities); let _ = self.context.cache.store(&response.authorities);
let _ = self.context.cache.store(&response.resources); 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 { if response.header.rescode == ResultCode::NXDOMAIN {
@@ -194,7 +233,19 @@ impl DnsResolver for RecursiveDnsResolver {
// If not, we'll have to resolve the ip of a NS record // If not, we'll have to resolve the ip of a NS record
let new_ns_name = match response.get_unresolved_ns(qname) { let new_ns_name = match response.get_unresolved_ns(qname) {
Some(x) => x, 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 // Recursively resolve the NS
@@ -204,7 +255,17 @@ impl DnsResolver for RecursiveDnsResolver {
if let Some(new_ns) = recursive_response.get_random_a() { if let Some(new_ns) = recursive_response.get_random_a() {
ns = new_ns.clone(); ns = new_ns.clone();
} else { } 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 +304,7 @@ mod tests {
})); }));
match Arc::get_mut(&mut context) { 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")] }; ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
} }
None => panic!() None => panic!()
@@ -251,7 +312,7 @@ mod tests {
let mut resolver = context.create_resolver(Arc::clone(&context)); 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) { let res = match resolver.resolve("google.com", QueryType::A, true) {
Ok(x) => x, Ok(x) => x,
@@ -268,7 +329,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 // and that the counter has been updated
{ {
let res = match resolver.resolve("google.com", QueryType::A, true) { let res = match resolver.resolve("google.com", QueryType::A, true) {
@@ -548,19 +609,23 @@ mod tests {
assert_eq!(3, list.len()); assert_eq!(3, list.len());
// Check statistics for google entry // Find entries by domain name (LRU order may vary)
assert_eq!("google.com", list[1].domain); 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 // google.com should have a NS record and an A record for a total of 2 record types
assert_eq!(2, list[1].record_types.len()); assert_eq!(2, google_entry.record_types.len());
// Should have been hit two times for NS google.com and once for // Should have been hit two times for NS google.com and once for
// A google.com // 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, ns1_entry.record_types.len());
assert_eq!(1, list[2].record_types.len()); assert_eq!(2, ns1_entry.hits);
assert_eq!(2, list[2].hits);
// foobar.google.com should be a cached NXDOMAIN with 0 hits
assert_eq!(0, foobar_entry.hits);
}; };
} }
} }
+10 -6
View File
@@ -107,7 +107,7 @@ pub fn execute_query(context: Arc<ServerContext>, request: &DnsPacket) -> DnsPac
let question = &request.questions[0]; let question = &request.questions[0];
packet.questions.push(question.clone()); 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 mut resolver = context.create_resolver(Arc::clone(&context));
let res_code = match resolver.resolve(&question.name, question.qtype, request.header.recursion_desired) { 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"); debug!("UDP service loop has finished");
break; break;
} }
if code == 10054 {
// Ignore
continue;
}
} }
debug!("Failed to read from UDP socket: {:?}", err); debug!("Failed to read from UDP socket: {:?}", err);
continue; continue;
@@ -441,7 +445,7 @@ mod tests {
})); }));
match Arc::get_mut(&mut context) { 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")] }; ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
} }
None => panic!() 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)); let res = execute_query(Arc::clone(&context), &build_query("www.facebook.com", QueryType::CNAME));
assert_eq!(2, res.answers.len()); 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)); let res = execute_query(Arc::clone(&context), &build_query("www.microsoft.com", QueryType::CNAME));
dbg!(&res); dbg!(&res);
@@ -503,7 +507,7 @@ mod tests {
// Disable recursive resolves to generate a failure // Disable recursive resolves to generate a failure
match Arc::get_mut(&mut context) { match Arc::get_mut(&mut context) {
Some(mut ctx) => { Some(ctx) => {
ctx.allow_recursive = false; ctx.allow_recursive = false;
} }
None => panic!() None => panic!()
@@ -531,7 +535,7 @@ mod tests {
})); }));
match Arc::get_mut(&mut context2) { 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")] }; ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
} }
None => panic!() None => panic!()
+20 -3
View File
@@ -33,13 +33,30 @@ pub fn start_dns_server(context: &Arc<Mutex<Context>>, settings: &Settings) -> b
result 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> { 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.allow_recursive = true;
server_context.resolve_strategy = match settings.dns.forwarders.is_empty() { server_context.resolve_strategy = match settings.dns.forwarders.is_empty() {
true => ResolveStrategy::Recursive, true => ResolveStrategy::Recursive,
false => ResolveStrategy::Forward { upstreams: settings.dns.forwarders.clone() } false => {
let upstreams = settings.dns.forwarders.iter().map(|s| {
if s.starts_with("https://") || s.parse::<std::net::SocketAddr>().is_ok() {
s.clone()
} else if let Ok(ip) = s.parse::<std::net::IpAddr>() {
std::net::SocketAddr::new(ip, 53).to_string()
} else {
warn!("Cannot parse forwarder address: {}", s);
s.clone()
}
}).collect();
ResolveStrategy::Forward { upstreams }
}
}; };
// Add host filters // Add host filters
for host in &settings.dns.hosts { for host in &settings.dns.hosts {
+24 -8
View File
@@ -59,6 +59,7 @@ fn main() {
opts.optflag("v", "version", "Print version and exit"); opts.optflag("v", "version", "Print version and exit");
opts.optflag("d", "debug", "Show debug messages, more than usual"); opts.optflag("d", "debug", "Show debug messages, more than usual");
opts.optflag("t", "trace", "Show trace messages, more than debug"); 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("b", "blocks", "List blocks from DB and exit");
opts.optflag("g", "generate", "Generate new config file. Generated config will be printed to console."); opts.optflag("g", "generate", "Generate new config file. Generated config will be printed to console.");
#[cfg(windows)] #[cfg(windows)]
@@ -147,6 +148,20 @@ fn main() {
no_gui = true; no_gui = true;
} }
#[cfg(all(feature = "webgui", target_os = "linux"))]
if !no_gui {
let running_via_sudo = env::var_os("SUDO_UID").is_some();
let has_graphical_session = env::var_os("DISPLAY").is_some() || env::var_os("WAYLAND_DISPLAY").is_some();
if running_via_sudo {
warn!(target: LOG_TARGET_MAIN, "Running GUI via sudo is not supported on Linux, starting without GUI");
no_gui = true;
} else if !has_graphical_session {
warn!(target: LOG_TARGET_MAIN, "No graphical session detected, starting without GUI");
no_gui = true;
}
}
#[cfg(windows)] #[cfg(windows)]
if opt_matches.opt_present("service") { if opt_matches.opt_present("service") {
let appdata = env::var("PROGRAMDATA").expect("Failed to get APPDATA directory"); let appdata = env::var("PROGRAMDATA").expect("Failed to get APPDATA directory");
@@ -251,7 +266,7 @@ fn main() {
}); });
} }
#[cfg(feature = "webgui")] #[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. // Without explicitly detaching the console cmd won't redraw it's prompt.
@@ -297,6 +312,12 @@ fn load_keys(settings: &Settings) -> Vec<Keystore> {
} }
pub fn start_services(settings: &Settings, context: &Arc<Mutex<Context>>) -> (bool, Arc<Mutex<Miner>>, JoinHandle<()>) { 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() { if let Ok(mut context) = context.lock() {
context.chain.check_chain(settings.check_blocks); context.chain.check_chain(settings.check_blocks);
match context.chain.get_block(1) { match context.chain.get_block(1) {
@@ -309,12 +330,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)); let mut miner_obj = Miner::new(Arc::clone(&context));
miner_obj.start_mining_thread(); miner_obj.start_mining_thread();
let miner: Arc<Mutex<Miner>> = Arc::new(Mutex::new(miner_obj)); let miner: Arc<Mutex<Miner>> = Arc::new(Mutex::new(miner_obj));
@@ -339,8 +354,9 @@ fn setup_logger(opt_matches: &Matches, console_attached: bool) {
} }
let mut builder = ConfigBuilder::new(); let mut builder = ConfigBuilder::new();
let config = builder.add_filter_ignore_str("mio::poll") 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::")
.add_filter_ignore_str("ureq_proto::")
.set_thread_level(LevelFilter::Error) .set_thread_level(LevelFilter::Error)
.set_location_level(LevelFilter::Off) .set_location_level(LevelFilter::Off)
.set_target_level(LevelFilter::Error) .set_target_level(LevelFilter::Error)
+1
View File
@@ -3,6 +3,7 @@ pub mod network;
pub mod peer; pub mod peer;
pub mod peers; pub mod peers;
pub mod state; pub mod state;
pub mod version;
pub use message::Message; pub use message::Message;
pub use network::Network; pub use network::Network;
+482 -90
View File
@@ -10,17 +10,22 @@ use std::sync::{Arc, mpsc, Mutex};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{io, thread}; use std::{io, thread};
use crossbeam_channel::{bounded, Receiver, Sender};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
#[allow(unused_imports)] #[allow(unused_imports)]
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
use mio::event::Event; use mio::event::Event;
use mio::net::{TcpListener, TcpStream}; use mio::net::{TcpListener, TcpStream};
use mio::{Events, Interest, Poll, Registry, Token}; use mio::{Events, Interest, Poll, Registry, Token};
use crate::commons::rtt_tracker::RttTracker;
use crate::p2p::version::Version;
use rand::{random, Rng, RngCore}; use rand::{random, Rng, RngCore};
use rand::prelude::thread_rng; use rand::prelude::thread_rng;
use x25519_dalek::{PublicKey, ReusableSecret}; use x25519_dalek::{PublicKey, ReusableSecret};
use crate::blockchain::types::BlockQuality; use crate::blockchain::types::BlockQuality;
use crate::blockchain::hash_utils::{check_block_hash, check_block_signature, hash_difficulty};
use crate::commons::*; use crate::commons::*;
use crate::crypto::Chacha; use crate::crypto::Chacha;
use crate::eventbus::{post, register}; use crate::eventbus::{post, register};
@@ -29,6 +34,20 @@ use crate::{Block, Bytes, Context};
const SERVER: Token = Token(0); const SERVER: Token = Token(0);
/// Job sent to validation worker threads
struct ValidationJob {
token: Token,
block: Block,
}
/// Result from validation worker threads after CPU-intensive checks
enum PreValidationResult {
/// Block passed hash and signature checks, needs DB validation
NeedsDbValidation(Token, Block),
/// Block failed basic validation
Invalid(Token, Block),
}
pub struct Network { pub struct Network {
context: Arc<Mutex<Context>>, context: Arc<Mutex<Context>>,
secret_key: ReusableSecret, secret_key: ReusableSecret,
@@ -37,7 +56,14 @@ pub struct Network {
// States of peer connections, and some data to send when sockets become writable // States of peer connections, and some data to send when sockets become writable
peers: Peers, peers: Peers,
// Orphan blocks from future // Orphan blocks from future
future_blocks: HashMap<u64, Block> future_blocks: HashMap<u64, Block>,
// Validation thread pool channels
validation_sender: Sender<ValidationJob>,
validation_receiver: Receiver<PreValidationResult>,
// Track pending block requests: block_index -> (request_time, peer_token)
pending_requests: HashMap<u64, (Instant, Token)>,
// Track peer response times for adaptive selection
peer_rtt: RttTracker<Token>,
} }
impl Network { impl Network {
@@ -47,7 +73,88 @@ impl Network {
let secret_key = ReusableSecret::random_from_rng(&mut thread_rng); let secret_key = ReusableSecret::random_from_rng(&mut thread_rng);
let public_key = PublicKey::from(&secret_key); let public_key = PublicKey::from(&secret_key);
let peers = Peers::new(); let peers = Peers::new();
Network { context, secret_key, public_key, token: Token(1), peers, future_blocks: HashMap::new() }
// Create validation thread pool
let cpus = num_cpus::get();
let num_workers = cpus.min((cpus / 2).max(1)); // At most half cpus
info!("Starting {num_workers} validation threads");
let channel_capacity = (num_workers * 4).max(100);
let (job_sender, job_receiver) = bounded::<ValidationJob>(channel_capacity);
let (result_sender, result_receiver) = bounded::<PreValidationResult>(channel_capacity);
// Spawn validation worker threads
for i in 0..num_workers {
let job_rx = job_receiver.clone();
let result_tx = result_sender.clone();
thread::Builder::new()
.name(format!("block-validator-{}", i))
.spawn(move || {
Self::validation_worker(job_rx, result_tx);
})
.expect("Failed to spawn validation worker thread");
}
// Drop the extra senders/receivers we don't need
drop(result_sender);
Network {
context,
secret_key,
public_key,
token: Token(1),
peers,
future_blocks: HashMap::new(),
validation_sender: job_sender,
validation_receiver: result_receiver,
pending_requests: HashMap::new(),
peer_rtt: RttTracker::new(),
}
}
/// Worker thread that performs CPU-intensive block validation
fn validation_worker(
job_receiver: Receiver<ValidationJob>,
result_sender: Sender<PreValidationResult>,
) {
loop {
match job_receiver.recv() {
Ok(job) => {
let ValidationJob { token, block } = job;
// Perform CPU-intensive validation without holding any locks
// These checks don't require database access
// Check 1: Verify block hash
if !check_block_hash(&block) {
debug!("Block {} failed hash validation", block.index);
let _ = result_sender.send(PreValidationResult::Invalid(token, block));
continue;
}
// Check 2: Verify block signature
if !check_block_signature(&block) {
debug!("Block {} failed signature validation", block.index);
let _ = result_sender.send(PreValidationResult::Invalid(token, block));
continue;
}
// Check 3: Verify hash difficulty matches claimed difficulty
if hash_difficulty(&block.hash) < block.difficulty {
debug!("Block {} hash difficulty doesn't match claimed difficulty", block.index);
let _ = result_sender.send(PreValidationResult::Invalid(token, block));
continue;
}
// Block passed CPU-intensive checks, send for DB validation
let _ = result_sender.send(PreValidationResult::NeedsDbValidation(token, block));
}
Err(_) => {
// Channel closed, exit worker thread
break;
}
}
}
} }
pub fn start(&mut self) { pub fn start(&mut self) {
@@ -175,6 +282,9 @@ impl Network {
}; };
let _ = debug_send.send(format!("Handle connection event: {:?} for peer {}", &event, &peer)); let _ = debug_send.send(format!("Handle connection event: {:?} for peer {}", &event, &peer));
if !self.handle_connection_event(poll.registry(), event, &mut seen_blocks, &mut buffer) { if !self.handle_connection_event(poll.registry(), event, &mut seen_blocks, &mut buffer) {
// Record failure and remove pending requests for this peer
self.peer_rtt.record_failure(&token);
self.pending_requests.retain(|_index, (_time, t)| *t != token);
let _ = self.peers.close_peer(poll.registry(), &token); let _ = self.peers.close_peer(poll.registry(), &token);
let blocks = self.context.lock().unwrap().chain.get_height(); let blocks = self.context.lock().unwrap().chain.get_height();
let keys = self.context.lock().unwrap().chain.get_users_count(); let keys = self.context.lock().unwrap().chain.get_users_count();
@@ -185,6 +295,11 @@ impl Network {
} }
} }
let _ = debug_send.send(String::from("After events iter")); let _ = debug_send.send(String::from("After events iter"));
// Process validation results from worker threads
let _ = debug_send.send(String::from("Process validation results"));
self.process_validation_results();
if last_events_time.elapsed().as_secs() > MAX_IDLE_SECONDS { if last_events_time.elapsed().as_secs() > MAX_IDLE_SECONDS {
if self.peers.get_peers_count() > 0 { if self.peers.get_peers_count() > 0 {
warn!("Something is wrong with swarm connections, closing all."); warn!("Something is wrong with swarm connections, closing all.");
@@ -239,8 +354,20 @@ impl Network {
(blocks, max_height, context.chain.get_last_hash()) (blocks, max_height, context.chain.get_last_hash())
}; };
// Periodic sync maintenance: gap retries and idle peer kicks
if height < max_height {
self.sync_maintain(height, max_height);
} else if height >= max_height && !self.future_blocks.is_empty() {
// We've caught up but have stale future_blocks — clean them up
self.future_blocks.clear();
self.pending_requests.clear();
post(crate::event::Event::SyncFinished);
}
let _ = debug_send.send(String::from("Peers update")); let _ = debug_send.send(String::from("Peers update"));
let have_blocks: HashSet<u64> = self.future_blocks.values().map(|block| block.index).collect(); let mut have_blocks: HashSet<u64> = self.future_blocks.keys().copied().collect();
// Also include blocks that are pending validation to avoid re-requesting them
have_blocks.extend(self.pending_requests.keys().copied());
self.peers.update(poll.registry(), hash, height, max_height, have_blocks); self.peers.update(poll.registry(), hash, height, max_height, have_blocks);
ui_timer = Instant::now(); ui_timer = Instant::now();
} }
@@ -454,14 +581,23 @@ impl Network {
peer.set_state(State::idle()); peer.set_state(State::idle());
} }
State::Idle { from } => { State::Idle { from } => {
debug!("Odd version of pings for {}", peer.get_addr().ip()); if peer.has_queued_messages() {
if from.elapsed().as_secs() >= 120 { // Send ONE queued message at a time to avoid flooding remote peer
let data: Vec<u8> = { if let Some(queued_data) = peer.pop_message() {
let c = self.context.lock().unwrap(); if let Ok(data) = encode_bytes(&queued_data, peer.get_cipher()) {
let message = Message::ping(c.chain.get_height(), c.chain.get_last_hash()); send_message(peer.get_stream(), &data).unwrap_or_else(|e| warn!("Error sending queued message {}", e));
encode_message(&message, peer.get_cipher()).unwrap() }
}; }
send_message(peer.get_stream(), &data).unwrap_or_else(|e| warn!("Error sending ping {}", e)); } else {
debug!("Odd version of pings for {}", peer.get_addr().ip());
if from.elapsed().as_secs() >= 120 {
let data: Vec<u8> = {
let c = self.context.lock().unwrap();
let message = Message::ping(c.chain.get_height(), c.chain.get_last_hash());
encode_message(&message, peer.get_cipher()).unwrap()
};
send_message(peer.get_stream(), &data).unwrap_or_else(|e| warn!("Error sending ping {}", e));
}
} }
} }
State::Error => {} State::Error => {}
@@ -504,6 +640,7 @@ impl Network {
} else if origin.eq(my_origin) { } else if origin.eq(my_origin) {
let peer = self.peers.get_mut_peer(token).unwrap(); let peer = self.peers.get_mut_peer(token).unwrap();
debug!("Incoming v{} on {}", &app_version, peer.get_addr().ip()); debug!("Incoming v{} on {}", &app_version, peer.get_addr().ip());
peer.set_version(Version::parse(&app_version));
let app_version = self.context.lock().unwrap().app_version.clone(); let app_version = self.context.lock().unwrap().app_version.clone();
if version == my_version { if version == my_version {
peer.set_public(public); peer.set_public(public);
@@ -538,6 +675,7 @@ impl Network {
let peer = self.peers.get_mut_peer(token).unwrap(); let peer = self.peers.get_mut_peer(token).unwrap();
// TODO check rand_id whether we have this peers connection already // TODO check rand_id whether we have this peers connection already
debug!("Outgoing v{} on {}", &app_version, peer.get_addr().ip()); debug!("Outgoing v{} on {}", &app_version, peer.get_addr().ip());
peer.set_version(Version::parse(&app_version));
peer.set_height(height); peer.set_height(height);
peer.set_active(true); peer.set_active(true);
peer.set_public(public); peer.set_public(public);
@@ -564,10 +702,20 @@ impl Network {
return State::message(Message::pong(my_height, my_hash)); return State::message(Message::pong(my_height, my_hash));
} }
if peer.is_higher(my_height) { if peer.is_higher(my_height) {
let mut context = self.context.lock().unwrap(); let max_height = {
context.chain.update_max_height(height); let mut context = self.context.lock().unwrap();
info!("Peer is higher, requesting block {} from {}", my_height + 1, peer.get_addr().ip()); context.chain.update_max_height(height);
State::message(Message::GetBlock { index: my_height + 1 }) context.chain.get_max_height()
};
// Start pipeline: queue a block request, then send pong
if let Some(idx) = self.next_block_to_request(my_height, max_height) {
let peer = self.peers.get_mut_peer(token).unwrap();
if peer.can_send() {
peer.queue_message(Message::GetBlock { index: idx });
self.pending_requests.insert(idx, (Instant::now(), *token));
}
}
State::message(Message::pong(my_height, my_hash))
} else if my_height == height && hash.ne(&my_hash) { } else if my_height == height && hash.ne(&my_hash) {
info!("Hashes are different, requesting block {} from {}", my_height, peer.get_addr().ip()); info!("Hashes are different, requesting block {} from {}", my_height, peer.get_addr().ip());
info!("My hash: {:?}, their hash: {:?}", &my_hash, &hash); info!("My hash: {:?}, their hash: {:?}", &my_hash, &hash);
@@ -585,10 +733,17 @@ impl Network {
return State::idle(); return State::idle();
} }
if peer.is_higher(my_height) { if peer.is_higher(my_height) {
let mut context = self.context.lock().unwrap(); let max_height = {
context.chain.update_max_height(height); let mut context = self.context.lock().unwrap();
info!("Peer is higher, requesting block {} from {}", my_height + 1, peer.get_addr().ip()); context.chain.update_max_height(height);
State::message(Message::GetBlock { index: my_height + 1 }) context.chain.get_max_height()
};
// Start pipeline: request next needed block from this peer
if let Some(idx) = self.next_block_to_request(my_height, max_height) {
self.pending_requests.insert(idx, (Instant::now(), *token));
return State::message(Message::GetBlock { index: idx });
}
State::idle()
} else if my_height == height && hash.ne(&my_hash) { } else if my_height == height && hash.ne(&my_hash) {
info!("Hashes are different, requesting block {} from {}", my_height, peer.get_addr().ip()); info!("Hashes are different, requesting block {} from {}", my_height, peer.get_addr().ip());
info!("My hash: {:?}, their hash: {:?}", &my_hash, &hash); info!("My hash: {:?}, their hash: {:?}", &my_hash, &hash);
@@ -636,12 +791,46 @@ impl Network {
if index != block.index { if index != block.index {
return State::Banned; return State::Banned;
} }
debug!("Received block {} with hash {:?}", block.index, &block.hash); let peer_addr = self.peers.get_peer(token).map_or("unknown".to_string(), |p| p.get_addr().ip().to_string());
if !seen_blocks.contains(&block.hash) { debug!("Received block {} with hash {:?} from {}", block.index, &block.hash, &peer_addr);
self.handle_block(token, block, seen_blocks) // Record RTT but keep in pending_requests until validation completes
} else { // (prevents re-requesting while block is in validation channel)
State::idle() if let Some((request_time, peer_token)) = self.pending_requests.get(&block.index) {
let rtt_ms = request_time.elapsed().as_secs_f64() * 1000.0;
self.peer_rtt.record_success(peer_token, rtt_ms);
} }
if !seen_blocks.contains(&block.hash) {
seen_blocks.insert(block.hash.clone());
// Send block to validation worker threads for parallel processing
match self.validation_sender.try_send(ValidationJob {
token: *token,
block,
}) {
Ok(_) => {},
Err(crossbeam_channel::TrySendError::Full(job)) => {
debug!("Validation queue full, deferring block {}", job.block.index);
self.future_blocks.insert(job.block.index, job.block);
},
Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
warn!("Validation worker threads have stopped");
},
}
}
// Pipeline: immediately request the next needed block from this peer
let (my_height, max_height) = {
let c = self.context.lock().unwrap();
(c.chain.get_height(), c.chain.get_max_height())
};
if my_height < max_height {
if let Some(next_idx) = self.next_block_to_request(my_height, max_height) {
let peer_addr = self.peers.get_peer(token).map_or("unknown".to_string(), |p| p.get_addr().ip().to_string());
debug!("Requesting block {next_idx} from {}", &peer_addr);
self.pending_requests.insert(next_idx, (Instant::now(), *token));
return State::message(Message::GetBlock { index: next_idx });
}
}
State::idle()
} }
Message::Twin => State::Twin, Message::Twin => State::Twin,
Message::Loop => State::Loop Message::Loop => State::Loop
@@ -649,79 +838,282 @@ impl Network {
answer answer
} }
fn handle_block(&mut self, token: &Token, block: Block, seen_blocks: &mut HashSet<Bytes>) -> State { /// Find the next block that needs to be requested within the sync window.
seen_blocks.insert(block.hash.clone()); /// Returns None if all blocks in the window are already requested or received.
let peers_count = self.peers.get_peers_active_count(); fn next_block_to_request(&self, my_height: u64, max_height: u64) -> Option<u64> {
let peer = self.peers.get_mut_peer(token).unwrap(); const SYNC_WINDOW: u64 = 500;
peer.set_received_block(block.index); // Don't pipeline until genesis block is added — other blocks would just
trace!("New block from {}", &peer.get_addr()); // queue as "future" and spam warnings since last_block is None.
let window = if my_height == 0 { 1 } else { SYNC_WINDOW };
let end = max_height.min(my_height + window);
for idx in (my_height + 1)..=end {
if !self.future_blocks.contains_key(&idx) && !self.pending_requests.contains_key(&idx) {
return Some(idx);
}
}
None
}
let mut context = self.context.lock().unwrap(); /// Periodic sync maintenance: retry gap blocks, kick idle peers into the pipeline.
let max_height = context.chain.get_max_height(); fn sync_maintain(&mut self, my_height: u64, max_height: u64) {
match context.chain.check_new_block(&block) { const GAP_TIMEOUT_MSECS: u128 = 1500;
BlockQuality::Good => {
let mut next_index = block.index + 1; let now = Instant::now();
context.chain.add_block(block);
// If we have some consequent blocks in a bucket of 'future blocks', we add them // Record failures for timed-out requests before cleanup
while let Some(block) = self.future_blocks.remove(&next_index) { let timed_out: Vec<Token> = self.pending_requests.iter()
if context.chain.check_new_block(&block) == BlockQuality::Good { .filter(|(_, (t, _))| t.elapsed().as_millis() >= GAP_TIMEOUT_MSECS)
debug!("Added block {} from future blocks", next_index); .map(|(_, (_, tok))| *tok)
context.chain.add_block(block); .collect();
} else { for tok in &timed_out {
warn!("Block {} in future blocks is bad!", block.index); self.peer_rtt.record_failure(tok);
}
// Clean up stale requests: timed out or peer no longer active
self.pending_requests.retain(|_index, (request_time, token)| {
if request_time.elapsed().as_millis() >= GAP_TIMEOUT_MSECS * 2 {
return false;
}
match self.peers.get_peer(token) {
Some(peer) => peer.active(),
None => false,
}
});
let raw_peers = self.peers.get_active_peer_tokens();
if raw_peers.is_empty() {
return;
}
let active_peers = self.peer_rtt.select_ordered(&raw_peers);
// Retry gap blocks with shorter timeout — push to FRONT of fastest peer's queue
let min_future_block = self.future_blocks.keys().min().copied();
if let Some(min_future) = min_future_block {
// active_peers is already ranked by RTT (fastest first)
let mut gap_peer_idx = 0;
for block_index in (my_height + 1)..min_future {
if self.future_blocks.contains_key(&block_index) {
continue;
}
if let Some((req_time, old_token)) = self.pending_requests.get(&block_index) {
if req_time.elapsed().as_millis() < GAP_TIMEOUT_MSECS {
continue;
}
self.peer_rtt.record_failure(old_token);
// Skip the failed peer
if let Some(pos) = active_peers.iter().position(|t| t == old_token) {
if pos == gap_peer_idx {
gap_peer_idx = (gap_peer_idx + 1) % active_peers.len();
}
}
debug!("Gap block {} timed out, re-requesting (have future from {})", block_index, min_future);
}
// Find a peer that can accept a message (old nodes only allow 1 in flight)
let mut sent = false;
for _ in 0..active_peers.len() {
let peer_token = active_peers[gap_peer_idx % active_peers.len()];
gap_peer_idx = (gap_peer_idx + 1) % active_peers.len();
if let Some(peer) = self.peers.get_mut_peer(&peer_token) {
if !peer.can_send() {
continue;
}
peer.queue_priority_message(Message::GetBlock { index: block_index });
self.pending_requests.insert(block_index, (now, peer_token));
debug!("Requesting gap block {} from {} (priority)", block_index, peer.get_addr().ip());
sent = true;
break; break;
} }
next_index += 1;
} }
let my_height = context.chain.get_height(); if !sent {
post(crate::event::Event::BlockchainChanged { index: my_height }); break; // All peers busy, try next cycle
// If it was the last block to sync }
if my_height == max_height { }
post(crate::event::Event::SyncFinished); }
self.future_blocks.clear();
} else { // Kick idle peers into the pipeline (peers with no pending requests)
let event = crate::event::Event::Syncing { have: my_height, height: max(max_height, my_height) }; for t in &active_peers {
post(event); let has_pending = self.pending_requests.values().any(|(_, tk)| tk == t);
} if has_pending {
let domains = context.chain.get_domains_count(); continue;
let keys = context.chain.get_users_count(); }
post(crate::event::Event::NetworkStatus { blocks: my_height, domains, keys, nodes: peers_count }); if let Some(peer) = self.peers.get_peer(t) {
} if !peer.get_state().is_idle() || peer.has_queued_messages() {
BlockQuality::Twin => { debug!("Ignoring duplicate block {}", block.index); } continue;
BlockQuality::Future => { }
debug!("Got future block {}", block.index); }
self.future_blocks.insert(block.index, block); if let Some(idx) = self.next_block_to_request(my_height, max_height) {
} if let Some(peer) = self.peers.get_mut_peer(t) {
BlockQuality::Bad => { peer.queue_message(Message::GetBlock { index: idx });
// TODO save bad public keys to banned table self.pending_requests.insert(idx, (now, *t));
debug!("Ignoring bad block from {}:\n{:?}", peer.get_addr(), &block); debug!("Kicking idle peer {} with block {}", peer.get_addr().ip(), idx);
let height = context.chain.get_height(); }
if height + 1 == block.index { }
context.chain.update_max_height(height); }
post(crate::event::Event::SyncFinished); }
}
return State::Banned; /// Process validation results from worker threads and add validated blocks to chain
} fn process_validation_results(&mut self) {
BlockQuality::Rewind => { // Process all available validation results without blocking
debug!("Got some orphan block, requesting its parent"); while let Ok(result) = self.validation_receiver.try_recv() {
return State::message(Message::GetBlock { index: block.index - 1 }); match result {
} PreValidationResult::NeedsDbValidation(token, block) => {
BlockQuality::Fork => { // CPU-intensive validation passed, now do DB-dependent validation
debug!("Got forked block {} with hash {:?}", block.index, block.hash); let peers_count = self.peers.get_peers_active_count();
// If we are very much behind of blockchain
let lagged = block.index == context.chain.get_height() && block.index + LIMITED_CONFIDENCE_DEPTH <= max_height; // Update peer state
let our_block = context.chain.get_block(block.index).unwrap(); if let Some(peer) = self.peers.get_mut_peer(&token) {
if block.is_better_than(&our_block) || lagged { peer.set_received_block(block.index);
context.chain.replace_block(block).expect("Error replacing block with fork"); trace!("Validated block {} from {}", block.index, peer.get_addr());
let index = context.chain.get_height(); } else {
post(crate::event::Event::BlockchainChanged { index }); // Peer disconnected, but we can still process the block
} else { trace!("Validated block {} from disconnected peer", block.index);
debug!("Fork in not better than our block, dropping."); }
return State::message(Message::block(our_block.index, our_block.as_bytes()));
// Lock context only for DB operations
let mut context = self.context.lock().unwrap();
let max_height = context.chain.get_max_height();
// Do remaining DB-dependent validation and add to chain
match context.chain.check_new_block(&block) {
BlockQuality::Good => {
let block_index = block.index;
let mut next_index = block.index + 1;
context.chain.add_block(block);
// Clean up pending request for this block
self.pending_requests.remove(&block_index);
// Process future blocks that are now ready
while let Some(block) = self.future_blocks.remove(&next_index) {
if context.chain.check_new_block(&block) == BlockQuality::Good {
debug!("Added block {} from future blocks", next_index);
context.chain.add_block(block);
self.pending_requests.remove(&next_index);
} else {
warn!("Block {} in future blocks is bad!", block.index);
break;
}
next_index += 1;
}
let my_height = context.chain.get_height();
post(crate::event::Event::BlockchainChanged { index: my_height });
// Check if sync is finished
if my_height >= max_height {
post(crate::event::Event::SyncFinished);
self.future_blocks.clear();
} else {
let event = crate::event::Event::Syncing {
have: my_height,
height: max(max_height, my_height)
};
post(event);
}
let domains = context.chain.get_domains_count();
let keys = context.chain.get_users_count();
post(crate::event::Event::NetworkStatus {
blocks: my_height,
domains,
keys,
nodes: peers_count
});
}
BlockQuality::Twin => {
debug!("Ignoring duplicate block {}", block.index);
}
BlockQuality::Future => {
debug!("Got future block {}", block.index);
let block_index = block.index;
self.future_blocks.insert(block.index, block);
// Clean up pending request since we have this block now
self.pending_requests.remove(&block_index);
}
BlockQuality::Bad => {
debug!("Block {} failed DB validation", block.index);
if let Some(peer) = self.peers.get_mut_peer(&token) {
debug!("Banning peer {} for bad block", peer.get_addr());
// Mark peer for banning
peer.set_state(State::Banned);
}
let height = context.chain.get_height();
if height + 1 == block.index {
context.chain.update_max_height(height);
post(crate::event::Event::SyncFinished);
}
}
BlockQuality::Rewind => {
debug!("Got orphan block {}, requesting parent", block.index);
// Save the block so it can be processed after the rewind resolves
let block_index = block.index;
self.future_blocks.insert(block.index, block);
self.pending_requests.remove(&block_index);
if let Some(peer) = self.peers.get_mut_peer(&token) {
peer.set_state(State::message(Message::GetBlock {
index: block_index - 1
}));
}
}
BlockQuality::Fork => {
debug!("Got forked block {} with hash {:?}", block.index, block.hash);
let lagged = block.index == context.chain.get_height()
&& block.index + LIMITED_CONFIDENCE_DEPTH <= max_height;
if let Some(our_block) = context.chain.get_block(block.index) {
if block.is_better_than(&our_block) || lagged {
let fork_index = block.index;
context.chain.replace_block(block)
.expect("Error replacing block with fork");
let mut next_index = fork_index + 1;
// Process future blocks that may now be valid after fork switch
while let Some(fb) = self.future_blocks.remove(&next_index) {
if context.chain.check_new_block(&fb) == BlockQuality::Good {
debug!("Added block {} from future blocks after fork", next_index);
context.chain.add_block(fb);
self.pending_requests.remove(&next_index);
} else {
debug!("Future block {} not good after fork", next_index);
break;
}
next_index += 1;
}
let my_height = context.chain.get_height();
post(crate::event::Event::BlockchainChanged { index: my_height });
if my_height >= max_height {
post(crate::event::Event::SyncFinished);
self.future_blocks.clear();
} else {
post(crate::event::Event::Syncing {
have: my_height,
height: max(max_height, my_height)
});
}
} else {
debug!("Fork is not better than our block, dropping");
if let Some(peer) = self.peers.get_mut_peer(&token) {
peer.set_state(State::message(Message::block(
our_block.index,
our_block.as_bytes()
)));
}
}
}
}
}
// Context lock is dropped here
}
PreValidationResult::Invalid(token, block) => {
// Block failed CPU-intensive validation
debug!("Block {} failed pre-validation (hash/signature)", block.index);
if let Some(peer) = self.peers.get_mut_peer(&token) {
debug!("Banning peer {} for invalid block", peer.get_addr());
peer.set_state(State::Banned);
}
} }
} }
} }
State::idle()
} }
/// Gets new token from old token, mutating the last /// Gets new token from old token, mutating the last
+55 -2
View File
@@ -1,3 +1,4 @@
use std::collections::VecDeque;
use std::fmt::Display; use std::fmt::Display;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::time::Instant; use std::time::Instant;
@@ -5,13 +6,16 @@ use std::time::Instant;
use mio::net::TcpStream; use mio::net::TcpStream;
use crate::crypto::Chacha; use crate::crypto::Chacha;
use crate::p2p::message::Message;
use crate::p2p::State; use crate::p2p::State;
use crate::p2p::version::Version;
#[derive(Debug)] #[derive(Debug)]
pub struct Peer { pub struct Peer {
addr: SocketAddr, addr: SocketAddr,
stream: TcpStream, stream: TcpStream,
state: State, state: State,
outgoing: VecDeque<Vec<u8>>,
id: String, id: String,
height: u64, height: u64,
inbound: bool, inbound: bool,
@@ -21,7 +25,8 @@ pub struct Peer {
reconnects: u32, reconnects: u32,
received_block: u64, received_block: u64,
sent_height: u64, sent_height: u64,
cipher: Option<Chacha> cipher: Option<Chacha>,
version: Version,
} }
impl Peer { impl Peer {
@@ -30,6 +35,7 @@ impl Peer {
addr, addr,
stream, stream,
state, state,
outgoing: VecDeque::new(),
id: String::new(), id: String::new(),
height: 0, height: 0,
inbound, inbound,
@@ -39,7 +45,8 @@ impl Peer {
reconnects: 0, reconnects: 0,
received_block: 0, received_block: 0,
sent_height: 0, sent_height: 0,
cipher: None cipher: None,
version: Version::default(),
} }
} }
@@ -78,6 +85,52 @@ impl Peer {
self.state = state; self.state = state;
} }
/// Queue a message for sending. Does not change peer state.
pub fn queue_message(&mut self, msg: Message) {
let data = serde_cbor::to_vec(&msg).unwrap();
self.outgoing.push_back(data);
}
/// Queue a high-priority message at the front (e.g. gap block requests)
pub fn queue_priority_message(&mut self, msg: Message) {
let data = serde_cbor::to_vec(&msg).unwrap();
self.outgoing.push_front(data);
}
pub fn has_queued_messages(&self) -> bool {
!self.outgoing.is_empty()
}
pub fn queued_count(&self) -> usize {
self.outgoing.len()
}
pub fn pop_message(&mut self) -> Option<Vec<u8>> {
self.outgoing.pop_front()
}
pub fn set_version(&mut self, version: Version) {
self.version = version;
}
pub fn get_version(&self) -> &Version {
&self.version
}
/// Old nodes (< 0.8.9) overwrite outgoing messages, can only have 1 in flight
pub fn supports_queue(&self) -> bool {
self.version >= Version { major: 0, minor: 8, patch: 9 }
}
/// Check if we can send another message to this peer
pub fn can_send(&self) -> bool {
if self.supports_queue() {
true
} else {
self.outgoing.is_empty()
}
}
pub fn get_id(&self) -> &str { pub fn get_id(&self) -> &str {
&self.id &self.id
} }
+13 -3
View File
@@ -202,6 +202,13 @@ impl Peers {
count count
} }
pub fn get_active_peer_tokens(&self) -> Vec<Token> {
self.peers.iter()
.filter(|(_, peer)| peer.active())
.map(|(token, _)| *token)
.collect()
}
pub fn is_tween_connect(&self, id: &str) -> bool { pub fn is_tween_connect(&self, id: &str) -> bool {
for (_, peer) in self.peers.iter() { for (_, peer) in self.peers.iter() {
if peer.active() && peer.get_id() == id { if peer.active() && peer.get_id() == id {
@@ -255,7 +262,10 @@ impl Peers {
let mut stale_tokens = Vec::new(); let mut stale_tokens = Vec::new();
for (token, peer) in self.peers.iter_mut() { for (token, peer) in self.peers.iter_mut() {
if let State::Idle { from } = peer.get_state() { if let State::Idle { from } = peer.get_state() {
if from.elapsed().as_secs() >= PING_PERIOD + random_time { if peer.has_queued_messages() {
let stream = peer.get_stream();
registry.reregister(stream, *token, Interest::WRITABLE).unwrap();
} else if from.elapsed().as_secs() >= PING_PERIOD + random_time {
// Sometimes we check for new peers instead of pinging // Sometimes we check for new peers instead of pinging
let message = if nodes < MAX_NODES && random::<bool>() { let message = if nodes < MAX_NODES && random::<bool>() {
Message::GetPeers Message::GetPeers
@@ -273,7 +283,7 @@ impl Peers {
stale_tokens.push((token.clone(), peer.get_addr())); stale_tokens.push((token.clone(), peer.get_addr()));
continue; continue;
} }
if matches!(peer.get_state(), State::Message {..}) { if matches!(peer.get_state(), State::Message {..}) || peer.has_queued_messages() {
let stream = peer.get_stream(); let stream = peer.get_stream();
registry.reregister(stream, *token, Interest::WRITABLE).unwrap(); registry.reregister(stream, *token, Interest::WRITABLE).unwrap();
} }
@@ -288,7 +298,7 @@ impl Peers {
self.ignored.retain(|_addr, time| { time.elapsed().as_secs() < 600 }); self.ignored.retain(|_addr, time| { time.elapsed().as_secs() < 600 });
// If someone has more blocks we sync // If someone has more blocks we sync
if nodes >= MIN_CONNECTED_NODES_START_SYNC && height < max_height { if nodes >= MIN_CONNECTED_NODES_START_SYNC && height < max_height && have_blocks.is_empty() {
// Give some opportunity to get more peers instead of requests for blocks only // Give some opportunity to get more peers instead of requests for blocks only
let request_blocks = nodes >= 10 || random::<bool>(); let request_blocks = nodes >= 10 || random::<bool>();
if request_blocks { if request_blocks {
+59
View File
@@ -0,0 +1,59 @@
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Version {
pub major: u16,
pub minor: u16,
pub patch: u16,
}
impl Version {
pub fn parse(s: &str) -> Version {
let parts: Vec<&str> = s.split('.').collect();
Version {
major: parts.get(0).and_then(|s| s.parse().ok()).unwrap_or(0),
minor: parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
patch: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
}
}
}
impl Default for Version {
fn default() -> Self {
Version { major: 0, minor: 0, patch: 0 }
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.major.cmp(&other.major)
.then(self.minor.cmp(&other.minor))
.then(self.patch.cmp(&other.patch))
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_comparison() {
assert!(Version::parse("0.8.9") >= Version::parse("0.8.9"));
assert!(Version::parse("0.8.10") > Version::parse("0.8.9"));
assert!(Version::parse("0.8.8") < Version::parse("0.8.9"));
assert!(Version::parse("0.9.0") > Version::parse("0.8.99"));
assert!(Version::parse("1.0.0") > Version::parse("0.99.99"));
}
}
+130 -8
View File
@@ -29,7 +29,22 @@ impl Settings {
Ok(mut file) => { Ok(mut file) => {
let mut text = String::new(); let mut text = String::new();
file.read_to_string(&mut text).unwrap(); 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); return Some(settings);
} }
None 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 { pub fn get_origin(&self) -> Bytes {
if self.origin.eq("") { if self.origin.eq("") {
return Bytes::zero32(); return Bytes::zero32();
@@ -70,17 +94,25 @@ pub struct Dns {
#[serde(default = "default_dns_bootstraps")] #[serde(default = "default_dns_bootstraps")]
pub bootstraps: Vec<String>, pub bootstraps: Vec<String>,
#[serde(default)] #[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 { impl Default for Dns {
fn default() -> Self { fn default() -> Self {
Dns { Dns {
listen: String::from("127.0.0.1:53"), listen: default_listen_dns(),
threads: 20, threads: 10,
forwarders: vec![String::from("94.140.14.14:53"), String::from("94.140.15.15:53")], forwarders: vec![String::from("94.140.14.14:53"), String::from("94.140.15.15:53")],
bootstraps: default_dns_bootstraps(), 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 { impl Default for Net {
fn default() -> Self { fn default() -> Self {
Net { 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"), listen: String::from("[::]:4244"),
public: true, public: true,
yggdrasil_only: false yggdrasil_only: false
@@ -121,11 +153,11 @@ fn default_listen() -> String {
} }
fn default_listen_dns() -> String { fn default_listen_dns() -> String {
String::from("0.0.0.0:53") String::from("127.0.0.3:53")
} }
fn default_threads() -> usize { fn default_threads() -> usize {
100 10
} }
fn default_check_blocks() -> u64 { fn default_check_blocks() -> u64 {
@@ -144,4 +176,94 @@ fn default_key_files() -> Vec<String> {
fn default_dns_bootstraps() -> Vec<String> { fn default_dns_bootstraps() -> Vec<String> {
vec![String::from("9.9.9.9:53"), String::from("94.140.14.14:53")] 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();
}
} }
+479 -317
View File
@@ -2,11 +2,12 @@ extern crate open;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
extern crate tinyfiledialogs as tfd; extern crate tinyfiledialogs as tfd;
extern crate web_view;
use std::panic::{self, AssertUnwindSafe};
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::Duration;
use alfis::blockchain::transaction::DomainData; use alfis::blockchain::transaction::DomainData;
use alfis::blockchain::types::MineResult; use alfis::blockchain::types::MineResult;
@@ -17,229 +18,212 @@ use alfis::event::Event;
use alfis::eventbus::{post, register}; use alfis::eventbus::{post, register};
use alfis::miner::Miner; use alfis::miner::Miner;
use alfis::{keystore, Block, Bytes, Context, Keystore, Transaction}; 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)] #[allow(unused_imports)]
use log::{debug, error, info, trace, warn, LevelFilter}; use log::{debug, error, info, trace, warn, LevelFilter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use web_view::Content;
use Cmd::*; 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 file_content = include_str!("webview/index.html");
let mut styles = inline_style(include_str!("webview/bulma.css")); 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/styles.css")));
styles.push_str(&inline_style(include_str!("webview/busy_indicator.css"))); styles.push_str(&inline_style(include_str!("webview/busy_indicator.css")));
let scripts = inline_script(include_str!("webview/scripts.js")); 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 title = format!("ALFIS {}", env!("CARGO_PKG_VERSION"));
let mut interface = web_view::builder()
.title(&title) // Create event loop and window
.content(html) let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
.size(1023, 720)
.min_size(773, 350) // Create tray menu
.resizable(true) let tray_menu = Menu::new();
.debug(false) let show_item = MenuItem::new("Show Window", true, None);
.user_data(()) let quit_item = MenuItem::new("Quit", true, None);
.invoke_handler(|web_view, arg| { tray_menu.append(&show_item).unwrap();
debug!("Command {}", arg); tray_menu.append(&quit_item).unwrap();
match serde_json::from_str(arg).unwrap() {
Loaded => { action_loaded(&context, web_view); } #[cfg(windows)]
LoadKey => { action_load_key(&context, web_view); } let icon = tray_icon::Icon::from_resource(1, None).unwrap();
CreateKey => { keystore::create_key(Arc::clone(&context)); } // Create tray icon
SaveKey => { action_save_key(&context); } #[cfg(not(target_os = "windows"))]
SelectKey { index } => { action_select_key(&context, web_view, index); } let icon = load_icon_from_png();
CheckRecord { data } => { action_check_record(web_view, data); }
CheckDomain { name } => { action_check_domain(&context, web_view, name); } let tray_icon = build_tray_icon(&title, tray_menu, icon);
MineDomain { name, data, signing, encryption, renewal } => { let tray_available = tray_icon.is_some();
action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data, signing, encryption, renewal);
} let window_size = tao::dpi::LogicalSize::new(1024, 720);
TransferDomain { name, owner} => { info!("Transferring '{name}' to '{owner}'"); } // Get primary monitor and calculate center position
StopMining => { post(Event::ActionStopMining); } let position = match event_loop.primary_monitor() {
Open { link } => { Some(monitor) => {
if open::that(&link).is_err() { let monitor_size = monitor.size();
show_warning(web_view, "Something wrong, I can't open the link 😢"); let monitor_position = monitor.position();
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 || !tray_available);
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()));
}
}
} }
} }
} Err(e) => {
Ok(()) error!("Error parsing command: {}", e);
})
.build()
.expect("Error building GUI");
run_interface_loop(&mut interface);
}
/// 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;
}
} }
} }
} });
if start.elapsed().as_millis() > 1 {
thread::sleep(pause);
start = Instant::now();
}
}
}
fn action_check_record(web_view: &mut WebView<()>, data: String) { #[cfg(not(target_os = "linux"))]
match serde_json::from_str::<DnsRecord>(&data) { let webview = builder.build(&window).unwrap();
Ok(record) => { #[cfg(target_os = "linux")]
if let Some(string) = record.get_data() { let webview = {
if string.len() > MAX_DATA_LEN { use tao::platform::unix::WindowExtUnix;
web_view.eval("recordOkay(false)").expect("Error evaluating!"); use wry::WebViewBuilderExtUnix;
} else { let vbox = window.default_vbox().unwrap();
web_view.eval("recordOkay(true)").expect("Error evaluating!"); builder.build_gtk(vbox).expect("Failed to build webview gtk object")
} };
} // Disabling context menu on the page in release build
} #[cfg(not(debug_assertions))]
Err(e) => { let _ = webview.evaluate_script("document.addEventListener('contextmenu', e => e.preventDefault());");
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 webview = Arc::new(Mutex::new(webview));
let c = context.lock().unwrap(); let webview_clone = Arc::clone(&webview);
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!");
}
}
fn action_save_key(context: &Arc<Mutex<Context>>) { // Setup event bus listener
if !context.lock().unwrap().has_keys() { let proxy_events = proxy.clone();
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>>, 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();
let threads = context.lock().unwrap().settings.mining.threads; let threads = context.lock().unwrap().settings.mining.threads;
let threads = match threads { let threads = match threads {
0 => num_cpus::get(), 0 => num_cpus::get(),
_ => threads _ => threads
}; };
let status = Arc::new(Mutex::new(UiStatus::new(threads))); let status = Arc::new(Mutex::new(UiStatus::new(threads)));
let context_copy = Arc::clone(context); let connected_nodes = Arc::new(AtomicUsize::new(0));
let c = context.lock().unwrap(); let nodes_copy = Arc::clone(&connected_nodes);
register(move |_uuid, e| { register(move |_uuid, e| {
//debug!("Got event from bus {:?}", &e);
let status = Arc::clone(&status); let status = Arc::clone(&status);
let handle = handle.clone(); let proxy = proxy_events.clone();
let context_copy = Arc::clone(&context_copy); let nodes_copy = Arc::clone(&nodes_copy);
let _ = thread::Builder::new().name(String::from("webui")).spawn(move || {
thread::Builder::new().name(String::from("webui")).spawn(move || {
let mut status = status.lock().unwrap(); let mut status = status.lock().unwrap();
let mut context = context_copy.lock().unwrap();
let eval = match e { let eval = match e {
Event::KeyCreated { path, public, hash } => { Event::KeyCreated { path, public, hash } => {
load_domains(&mut context, &handle); let _ = proxy.send_event(UserEvent::LoadDomains);
send_keys_to_ui(&context, &handle); let _ = proxy.send_event(UserEvent::SendKeysToUi);
event_handle_luck(&handle, "Key successfully created! Don\\'t forget to save it!"); 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); let mut s = format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash);
s.push_str(" showSuccess('New key mined successfully! Save it to a safe place!')"); s.push_str(" showSuccess('New key mined successfully! Save it to a safe place!')");
s s
} }
Event::KeyLoaded { path, public, hash } | Event::KeyLoaded { path, public, hash } |
Event::KeySaved { path, public, hash } => { Event::KeySaved { path, public, hash } => {
load_domains(&mut context, &handle); let _ = proxy.send_event(UserEvent::LoadDomains);
send_keys_to_ui(&context, &handle); let _ = proxy.send_event(UserEvent::SendKeysToUi);
format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash) format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash)
} }
Event::MinerStarted | Event::KeyGeneratorStarted => { Event::MinerStarted | Event::KeyGeneratorStarted => {
status.mining = true; status.mining = true;
status.max_diff = 0; 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);") String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);")
} }
Event::MinerStopped { success, full } => { Event::MinerStopped { success, full } => {
@@ -253,12 +237,12 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
if full { if full {
match success { match success {
true => { true => {
load_domains(&mut context, &handle); let _ = proxy.send_event(UserEvent::LoadDomains);
event_handle_luck(&handle, "Mining is successful!"); 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!')"); s.push_str(" showSuccess('Block successfully mined!')");
} }
false => { 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.')"); s.push_str(" showWarning('Mining unsuccessful, sorry.')");
} }
} }
@@ -288,7 +272,7 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
status.syncing = true; status.syncing = true;
status.synced_blocks = have; status.synced_blocks = have;
if height != status.sync_height { 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; status.sync_height = height;
} }
if status.mining { if status.mining {
@@ -298,8 +282,8 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
} }
} }
Event::SyncFinished => { Event::SyncFinished => {
load_domains(&mut context, &handle); let _ = proxy.send_event(UserEvent::LoadDomains);
event_handle_info(&handle, "Syncing finished."); let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Syncing finished.');", Local::now().format("%d.%m.%y %X"))));
status.syncing = false; status.syncing = false;
if status.mining { if status.mining {
String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);") String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);")
@@ -308,6 +292,7 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
} }
} }
Event::NetworkStatus { blocks, domains, keys, nodes } => { Event::NetworkStatus { blocks, domains, keys, nodes } => {
nodes_copy.store(nodes, Ordering::SeqCst);
if status.mining || status.syncing || nodes < 3 { if status.mining || status.syncing || nodes < 3 {
format!("setStats({}, {}, {}, {});", blocks, domains, keys, nodes) format!("setStats({}, {}, {}, {});", blocks, domains, keys, nodes)
} else { } else {
@@ -316,49 +301,290 @@ fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
} }
Event::BlockchainChanged { index } => { Event::BlockchainChanged { index } => {
debug!("Current blockchain height is {}", index); debug!("Current blockchain height is {}", index);
event_handle_info(&handle, &format!("Blockchain changed, current block count is {} now.", index)); 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() // Nothing String::new()
} }
Event::Error { text } => format!("showError('{}')", &text), Event::Error { text } => format!("showError('{}')", &text),
_ => String::new() _ => String::new()
}; };
if !eval.is_empty() { if !eval.is_empty() {
handle.dispatch(move |web_view| { let _ = proxy.send_event(UserEvent::EvalJs(eval));
web_view.eval(&eval.replace("\\", "\\\\"))
}).expect("Error dispatching!");
} }
}); }).ok();
true true
}); });
if tray_available {
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,
..
} => {
if tray_available {
window.set_visible(false);
} else {
info!("Interface closed, exiting");
post(Event::ActionQuit);
thread::sleep(Duration::from_millis(100));
*control_flow = ControlFlow::Exit;
}
}
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) => {
if let Some(tray_icon) = tray_icon.as_ref() {
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;
}
}
}
}
_ => {}
}
});
}
fn build_tray_icon(title: &str, tray_menu: Menu, icon: tray_icon::Icon) -> Option<tray_icon::TrayIcon> {
let previous_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let result = panic::catch_unwind(AssertUnwindSafe(|| {
TrayIconBuilder::new()
.with_menu(Box::new(tray_menu))
.with_tooltip(title)
.with_icon(icon)
.with_menu_on_left_click(false)
.build()
}));
panic::set_hook(previous_hook);
match result {
Ok(Ok(tray_icon)) => Some(tray_icon),
Ok(Err(error)) => {
warn!("Tray icon is unavailable: {error}");
None
}
Err(_) => {
warn!("Tray icon is unavailable: failed to load appindicator library, continuing without tray support");
None
}
}
}
#[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() { if let Some(keystore) = c.get_keystore() {
let path = keystore.get_path().to_owned(); let path = keystore.get_path().to_owned();
let public = keystore.get_public().to_string(); let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string(); let hash = keystore.get_hash().to_string();
post(Event::KeyLoaded { path, public, hash }); post(Event::KeyLoaded { path, public, hash });
} }
let index = c.chain.get_height(); let index = c.chain.get_height();
if index > 0 { if index > 0 {
post(Event::BlockchainChanged { index }); post(Event::BlockchainChanged { index });
} }
let zones = c.chain.get_zones(); let zones = c.chain.get_zones();
info!("Loaded zones: {:?}", &zones); info!("Loaded zones: {:?}", &zones);
if let Ok(zones) = serde_json::to_string(&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); 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); 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<()>) { fn load_domains(context: &mut MutexGuard<Context>, webview: &wry::WebView) {
let _ = handle.dispatch(move |web_view|{ let _ = webview.evaluate_script("clearMyDomains();");
web_view.eval("clearMyDomains();")
});
let domains = context.chain.get_my_domains(context.get_keystore()); let domains = context.chain.get_my_domains(context.get_keystore());
let mut domains = domains.iter().map(|(_, d)| d).collect::<Vec<_>>(); let mut domains = domains.iter().map(|(_, d)| d).collect::<Vec<_>>();
domains.sort_by(|a, b| a.0.cmp(&b.0)); domains.sort_by(|a, b| a.0.cmp(&b.0));
@@ -366,16 +592,12 @@ fn load_domains(context: &mut MutexGuard<Context>, handle: &Handle<()>) {
let d = serde_json::to_string(&data).unwrap(); let d = serde_json::to_string(&data).unwrap();
let d = d.replace("'", "\\'").replace("\\n", "\\\\n").replace("\"", "\\\""); let d = d.replace("'", "\\'").replace("\\n", "\\\\n").replace("\"", "\\\"");
let command = format!("addMyDomain('{}', {}, {}, '{}');", &domain, timestamp, timestamp + DOMAIN_LIFETIME, &d); let command = format!("addMyDomain('{}', {}, {}, '{}');", &domain, timestamp, timestamp + DOMAIN_LIFETIME, &d);
let _ = handle.dispatch(move |web_view|{ let _ = webview.evaluate_script(&command);
web_view.eval(&command)
});
} }
let _ = handle.dispatch(move |web_view|{ let _ = webview.evaluate_script("refreshMyDomains();");
web_view.eval("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 keys = {
let mut keys = Vec::new(); let mut keys = Vec::new();
for key in context.get_keystores() { for key in context.get_keystores() {
@@ -387,94 +609,101 @@ fn send_keys_to_ui(context: &MutexGuard<Context>, handle: &Handle<()>) {
}; };
if !keys.is_empty() { if !keys.is_empty() {
let index = context.get_active_key_index(); 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);
let command = format!("keysChanged('{}'); keySelected({});", serde_json::to_string(&keys).unwrap(), index); let _ = webview.evaluate_script(&command);
web_view.eval(&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); debug!("Creating domain with data: {}", &data);
let c = Arc::clone(&context); let c = Arc::clone(&context);
let context = context.lock().unwrap(); let context_guard = 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."); if !context_guard.has_keys() {
let _ = web_view.eval("domainMiningUnavailable();"); 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; return;
} }
if context.chain.is_waiting_signers() {
show_warning(web_view, "Waiting for last full block to be signed. Try again later."); if context_guard.chain.is_waiting_signers() {
let _ = web_view.eval("domainMiningUnavailable();"); 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."); info!("Waiting for last full block to be signed. Try again later.");
return; return;
} }
let keystore = context.get_keystore().unwrap().clone();
let keystore = context_guard.get_keystore().unwrap().clone();
let pub_key = keystore.get_public(); let pub_key = keystore.get_public();
let data = match serde_json::from_str::<DomainData>(&data) { let data = match serde_json::from_str::<DomainData>(&data) {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
show_warning(web_view, "Something wrong with domain data. I cannot mine it."); let _ = proxy.send_event(UserEvent::ShowWarning("Something wrong with domain data. I cannot mine it.".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
warn!("Error parsing data: {}", e); warn!("Error parsing data: {}", e);
return; return;
} }
}; };
info!("Parsed domain data:\n{:#?}", &data); info!("Parsed domain data:\n{:#?}", &data);
if data.records.len() > MAX_RECORDS { if data.records.len() > MAX_RECORDS {
show_warning(web_view, "Too many records. Mining more than 30 records not allowed."); let _ = proxy.send_event(UserEvent::ShowWarning("Too many records. Mining more than 30 records not allowed.".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
return; return;
} }
// Check if yggdrasil only quality of zone is not violated // 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 { for z in zones {
if z.name == data.zone && z.yggdrasil { if z.name == data.zone && z.yggdrasil {
for record in &data.records { for record in &data.records {
if !is_yggdrasil_record(record) { if !is_yggdrasil_record(record) {
show_warning(web_view, &format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone)); let _ = proxy.send_event(UserEvent::ShowWarning(format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone)));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
return; return;
} }
} }
} }
} }
let (signing, encryption) = if signing.is_empty() || encryption.is_empty() { let (signing, encryption) = if signing.is_empty() || encryption.is_empty() {
(keystore.get_public(), keystore.get_encryption_public()) (keystore.get_public(), keystore.get_encryption_public())
} else { } else {
(Bytes::new(from_hex(&signing).unwrap()), Bytes::new(from_hex(&encryption).unwrap())) (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 => { MineResult::Fine => {
drop(context); drop(context_guard);
create_domain(c, miner, CLASS_DOMAIN, &name, data, DOMAIN_DIFFICULTY, &keystore, signing, encryption, renewal); create_domain(c, miner, CLASS_DOMAIN, &name, data, DOMAIN_DIFFICULTY, &keystore, signing, encryption, renewal);
let _ = web_view.eval("domainMiningStarted();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningStarted();".to_string()));
event_info(web_view, &format!("Mining of domain \\'{}\\' has started", &name)); let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining of domain \\\\'{}\\\\' has started');", Local::now().format("%d.%m.%y %X"), &name)));
} }
MineResult::WrongName => { MineResult::WrongName => {
show_warning(web_view, "You can't mine this domain!"); let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine this domain!".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
} }
MineResult::WrongData => { MineResult::WrongData => {
show_warning(web_view, "You have an error in records!"); let _ = proxy.send_event(UserEvent::ShowWarning("You have an error in records!".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
} }
MineResult::WrongKey => { MineResult::WrongKey => {
show_warning(web_view, "You can't mine with current key!"); let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine with current key!".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
} }
MineResult::WrongZone => { MineResult::WrongZone => {
show_warning(web_view, "You can't mine domain in this zone!"); let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine domain in this zone!".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
} }
MineResult::NotOwned => { MineResult::NotOwned => {
show_warning(web_view, "This domain is already taken, and it is not yours!"); let _ = proxy.send_event(UserEvent::ShowWarning("This domain is already taken, and it is not yours!".to_string()));
let _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
} }
MineResult::Cooldown { time } => { MineResult::Cooldown { time } => {
event_info(web_view, &format!("You have cooldown {}!", format_cooldown(time))); let cooldown = format_cooldown(time);
show_warning(web_view, &format!("You have 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 _ = web_view.eval("domainMiningUnavailable();"); let _ = proxy.send_event(UserEvent::ShowWarning(format!("You have cooldown {}!", cooldown)));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
} }
} }
} }
@@ -490,80 +719,13 @@ fn format_cooldown(time: i64) -> String {
format!("{} hours", minutes / 60) 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('\'', "\\'"); let str = text.replace('\'', "\\'");
match web_view.eval(&format!("showWarning('{}');", &str)) { if let Err(e) = webview.evaluate_script(&format!("showWarning('{}');", &str)) {
Ok(_) => {} warn!("Error showing warning: {}", e);
Err(_) => { warn!("Error showing warning!"); }
} }
} }
#[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)] #[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) { 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(); let name = name.to_owned();
@@ -611,7 +773,7 @@ struct UiStatus {
impl UiStatus { impl UiStatus {
fn new(threads: usize) -> Self { 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 } UiStatus { mining: false, syncing: false, synced_blocks: 0, sync_height: 0, max_diff: 0, speed }
} }
+16335 -6395
View File
File diff suppressed because it is too large Load Diff
+12 -7
View File
@@ -47,13 +47,13 @@ function refreshRecordsList() {
function makeRecord(value, index, array) { function makeRecord(value, index, array) {
let data = value.addr; let data = value.addr;
if (value.type == "MX") { if (value.type === "MX") {
data = value.priority + " " + value.host; data = value.priority + " " + value.host;
} else if (value.type == "CNAME" || value.type == "NS") { } else if (value.type === "CNAME" || value.type === "NS") {
data = value.host; data = value.host;
} else if (value.type == "TXT" || value.type == "TLSA") { } else if (value.type === "TXT" || value.type === "TLSA") {
data = value.data.toString(); data = value.data.toString();
} else if (value.type == "SRV") { } else if (value.type === "SRV") {
data = value.priority + " " + value.weight + " " + value.port + " " + value.host; data = value.priority + " " + value.weight + " " + value.port + " " + value.host;
} }
@@ -212,12 +212,17 @@ function editDomain(domain, event) {
} }
function onLoad() { function onLoad() {
// Workaround for Arch Linux Webkit // Compatibility shim for wry IPC
// https://github.com/Boscop/web-view/issues/212#issuecomment-671055663
if (typeof window.external == 'undefined' || typeof window.external.invoke == 'undefined') { if (typeof window.external == 'undefined' || typeof window.external.invoke == 'undefined') {
window.external = { window.external = {
invoke: function(x) { 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
View File
@@ -85,7 +85,7 @@ fn run_service_logic() -> Result<()> {
let (_dns_server_ok, _miner, _network) = start_services(&settings, &context); let (_dns_server_ok, _miner, _network) = start_services(&settings, &context);
loop { loop {
thread::sleep(Duration::from_secs(1)); thread::sleep(Duration::from_millis(50));
// Poll shutdown event. // Poll shutdown event.
match shutdown_rx.recv_timeout(Duration::from_secs(1)) { match shutdown_rx.recv_timeout(Duration::from_secs(1)) {
// Break the loop either upon stop or channel disconnect // Break the loop either upon stop or channel disconnect