Compare commits
35 Commits
feat/gui
...
c8fa174ac0
| Author | SHA1 | Date | |
|---|---|---|---|
| c8fa174ac0 | |||
| 9624484b29 | |||
| eee73be58e | |||
| eb30037f53 | |||
| 2e1f05cadb | |||
| 09c1cd5ddc | |||
| bb162bccee | |||
| d1bf9163f7 | |||
| 7c11c7fbd7 | |||
| 8f4cbf7dc0 | |||
| bb3a33c103 | |||
| 0835df14ac | |||
| 6e5b64545e | |||
| f35dc56598 | |||
| 71674e3de8 | |||
| 4f2aef91c0 | |||
| 6950600bdd | |||
| a29a6190fb | |||
| b10402ee1e | |||
| dbf3df9ff9 | |||
| 6b3f88f6bb | |||
| 50569d2a20 | |||
| 664715f02b | |||
| 8e11f63479 | |||
| 19f67e8b77 | |||
| 8a0677caf2 | |||
| 5de0341ab4 | |||
| d3cdf6ea76 | |||
| 81f5568957 | |||
| 61f2d89ef1 | |||
| 429563eee9 | |||
| fc7360ea00 | |||
| 914e8b6d67 | |||
| 4169ede074 | |||
| d2b7080c96 |
@@ -24,3 +24,10 @@ 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,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) || '' }}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
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
|
||||||
@@ -65,7 +65,9 @@ jobs:
|
|||||||
|
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update && sudo apt upgrade && sudo apt install libwebkit2gtk-4.0-dev upx
|
sudo apt update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo apt install libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev upx-ucl
|
||||||
cargo install cross
|
cargo install cross
|
||||||
|
|
||||||
- name: Build and package deb packages
|
- name: Build and package deb packages
|
||||||
@@ -97,20 +99,14 @@ jobs:
|
|||||||
|
|
||||||
- name: install dependencies
|
- name: install 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 update && sudo apt install --no-install-recommends libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev upx-ucl
|
||||||
|
|
||||||
- name: Build release binaries
|
- name: Build release binaries
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
- name: Build Windows release binaries with Edge web-engine
|
|
||||||
if: contains(matrix.os, 'windows')
|
|
||||||
run: cargo build --release --features "edge" --target-dir edge
|
|
||||||
|
|
||||||
- name: windows
|
- name: windows
|
||||||
if: contains(matrix.os, 'windows')
|
if: contains(matrix.os, 'windows')
|
||||||
run: |
|
run: echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV
|
||||||
echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV
|
|
||||||
echo "BIN_ARCH_EDGE=windows-amd64-edge" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: linux
|
- name: linux
|
||||||
if: contains(matrix.os, 'ubuntu')
|
if: contains(matrix.os, 'ubuntu')
|
||||||
@@ -129,8 +125,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "BIN_PATH=target/release/alfis.exe" >> $GITHUB_ENV
|
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 "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
|
- name: Packaging
|
||||||
uses: papeloto/action-zip@v1
|
uses: papeloto/action-zip@v1
|
||||||
@@ -138,13 +132,6 @@ jobs:
|
|||||||
files: ${{ env.BIN_PATH }} alfis.toml README.md LICENSE adblock.txt
|
files: ${{ env.BIN_PATH }} alfis.toml README.md LICENSE adblock.txt
|
||||||
dest: ${{ env.ZIP_NAME }}
|
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
|
id: upload-zip
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
@@ -155,15 +142,3 @@ jobs:
|
|||||||
asset_path: ${{ env.ZIP_NAME }}
|
asset_path: ${{ env.ZIP_NAME }}
|
||||||
asset_name: ${{ env.ZIP_NAME }}
|
asset_name: ${{ env.ZIP_NAME }}
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|
||||||
- name: Upload Edge binary
|
|
||||||
if: contains(matrix.os, 'windows')
|
|
||||||
id: upload-edge-binary
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ${{ env.ZIP_NAME_EDGE }}
|
|
||||||
asset_name: ${{ env.ZIP_NAME_EDGE }}
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|||||||
Generated
+2709
-490
File diff suppressed because it is too large
Load Diff
+25
-20
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[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.10.9"
|
||||||
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.53", optional = true }
|
||||||
|
tao = { version = "0.34", optional = true }
|
||||||
|
tray-icon = { version = "0.21.2", 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"
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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/).
|
||||||
|
|
||||||
###  On Windows (MINGW64)
|
###  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
|
|||||||
```
|
```
|
||||||
|
|
||||||
###  On Linux
|
###  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).
|
||||||
|
|
||||||
####  On Arch Linux
|
####  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
File diff suppressed because it is too large
Load Diff
+15
-4
@@ -1,4 +1,4 @@
|
|||||||
# The hash of first block in a chain to know with which nodes to work
|
# The hash of the first block in a chain to know with which nodes to work
|
||||||
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
|
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
|
||||||
@@ -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.
@@ -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
|
||||||
|
|||||||
+142
-20
@@ -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,53 @@ 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);
|
let mut dns_client = DnsNetworkClient::new();
|
||||||
let mut dns_client = DnsNetworkClient::new(port);
|
|
||||||
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 {
|
||||||
|
let addr = SocketAddr::new(*server, 53);
|
||||||
|
let start = Instant::now();
|
||||||
|
match dns_client.send_udp_query(qname, qtype, addr, false, timeout) {
|
||||||
|
Ok(res) => {
|
||||||
|
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
tracker.record_success(server, elapsed);
|
||||||
dns_client.stop();
|
dns_client.stop();
|
||||||
return Some(res);
|
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 +108,7 @@ impl BlockchainFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData) -> (bool, Option<DnsPacket>) {
|
fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData, recursive: bool, tracker: &RttTracker<IpAddr>) -> (bool, Option<DnsPacket>) {
|
||||||
// First we search for NS records, collecting nameserver domains
|
// 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 +126,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 +172,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 +181,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,10 +298,13 @@ 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 qtype != QueryType::NS {
|
||||||
|
let (has_ns, result) = Self::resolve_by_ns(qname, qtype, &top_domain, &data, recursive, &self.ns_tracker);
|
||||||
if has_ns {
|
if has_ns {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut answers: Vec<DnsRecord> = Vec::new();
|
let mut answers: Vec<DnsRecord> = Vec::new();
|
||||||
let mut cname: Option<DnsRecord> = None;
|
let mut cname: Option<DnsRecord> = None;
|
||||||
@@ -237,7 +346,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 +372,20 @@ impl DnsFilter for BlockchainFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut packet) = self.create_packet(qname, qtype, zone, answers) {
|
// Extract NS records and GLUE records for the response
|
||||||
|
let ns_records = BlockchainFilter::get_ns_records(&data, &top_domain);
|
||||||
|
let ns_hosts: Vec<String> = ns_records.iter()
|
||||||
|
.filter_map(|record| {
|
||||||
|
if let DnsRecord::NS { host, .. } = record {
|
||||||
|
Some(host.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let glue_records = BlockchainFilter::get_glue_records(&data, &top_domain, &ns_hosts);
|
||||||
|
|
||||||
|
if let Some(mut packet) = self.create_packet(qname, qtype, zone, answers, ns_records, glue_records) {
|
||||||
if domain_exists && packet.answers.is_empty() {
|
if domain_exists && packet.answers.is_empty() {
|
||||||
packet.header.rescode = ResultCode::NOERROR;
|
packet.header.rescode = ResultCode::NOERROR;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pub const UI_REFRESH_DELAY_MS: u128 = 500;
|
|||||||
pub const LOG_REFRESH_DELAY_SEC: u64 = 60;
|
pub const 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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
|
/// Unbound-style RTT band width in milliseconds.
|
||||||
|
/// Servers within min_rtt + BAND are considered equally good.
|
||||||
|
const RTT_BAND_MS: f64 = 100.0;
|
||||||
|
/// EWMA smoothing factor: 87.5% history, 12.5% new measurement.
|
||||||
|
const EWMA_WEIGHT: f64 = 7.0 / 8.0;
|
||||||
|
/// Penalty RTT assigned on timeout/failure (ms).
|
||||||
|
const TIMEOUT_PENALTY_MS: f64 = 5000.0;
|
||||||
|
/// Stats older than this are expired so the server gets re-probed.
|
||||||
|
const STATS_EXPIRE_SECS: u64 = 900;
|
||||||
|
|
||||||
|
struct RttStats {
|
||||||
|
rtt: f64,
|
||||||
|
last_update: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adaptive server selection using Unbound-style RTT banding.
|
||||||
|
///
|
||||||
|
/// Tracks smoothed RTT per key and selects servers by grouping them into
|
||||||
|
/// a "preferred" band (within `RTT_BAND_MS` of the fastest known server)
|
||||||
|
/// and a "fallback" group. Unknown or expired servers are treated as
|
||||||
|
/// preferred so they get probed.
|
||||||
|
pub struct RttTracker<K: Eq + Hash + Clone> {
|
||||||
|
stats: Mutex<HashMap<K, RttStats>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Eq + Hash + Clone> RttTracker<K> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
RttTracker {
|
||||||
|
stats: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `keys` reordered for adaptive selection.
|
||||||
|
///
|
||||||
|
/// - Keys with no stats or expired stats go to the preferred group (to be probed).
|
||||||
|
/// - Known keys within `min_rtt + RTT_BAND_MS` go to the preferred group.
|
||||||
|
/// - The rest are fallback.
|
||||||
|
/// - Each group is shuffled; preferred comes first.
|
||||||
|
pub fn select_ordered(&self, keys: &[K]) -> Vec<K> {
|
||||||
|
let now = Instant::now();
|
||||||
|
let stats = self.stats.lock().unwrap();
|
||||||
|
|
||||||
|
let mut known: Vec<(K, f64)> = Vec::new();
|
||||||
|
let mut unknown: Vec<K> = Vec::new();
|
||||||
|
for key in keys {
|
||||||
|
match stats.get(key) {
|
||||||
|
Some(s) if now.duration_since(s.last_update).as_secs() < STATS_EXPIRE_SECS => {
|
||||||
|
known.push((key.clone(), s.rtt));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
unknown.push(key.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(stats);
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
if known.is_empty() {
|
||||||
|
unknown.shuffle(&mut rng);
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_rtt = known.iter().map(|(_, rtt)| *rtt).fold(f64::INFINITY, f64::min);
|
||||||
|
let band_threshold = min_rtt + RTT_BAND_MS;
|
||||||
|
|
||||||
|
let mut preferred: Vec<K> = Vec::new();
|
||||||
|
let mut fallback: Vec<K> = Vec::new();
|
||||||
|
for (key, rtt) in known {
|
||||||
|
if rtt <= band_threshold {
|
||||||
|
preferred.push(key);
|
||||||
|
} else {
|
||||||
|
fallback.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preferred.extend(unknown);
|
||||||
|
preferred.shuffle(&mut rng);
|
||||||
|
fallback.shuffle(&mut rng);
|
||||||
|
preferred.extend(fallback);
|
||||||
|
preferred
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a successful query with the measured RTT in milliseconds.
|
||||||
|
pub fn record_success(&self, key: &K, rtt_ms: f64) {
|
||||||
|
self.update(key, rtt_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a failed/timed-out query, applying a penalty RTT.
|
||||||
|
pub fn record_failure(&self, key: &K) {
|
||||||
|
self.update(key, TIMEOUT_PENALTY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&self, key: &K, rtt_ms: f64) {
|
||||||
|
let mut stats = self.stats.lock().unwrap();
|
||||||
|
let entry = stats.entry(key.clone()).or_insert(RttStats {
|
||||||
|
rtt: rtt_ms,
|
||||||
|
last_update: Instant::now(),
|
||||||
|
});
|
||||||
|
entry.rtt = entry.rtt * EWMA_WEIGHT + rtt_ms * (1.0 - EWMA_WEIGHT);
|
||||||
|
entry.last_update = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,14 @@ pub struct Chacha {
|
|||||||
|
|
||||||
impl Chacha {
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -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 = ".";
|
||||||
|
|
||||||
@@ -226,7 +226,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 +300,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 +321,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 +331,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
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-69
@@ -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,41 +480,72 @@ 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 {
|
||||||
|
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true, timeout_duration) {
|
||||||
for answer in &res.answers {
|
for answer in &res.answers {
|
||||||
if let DnsRecord::A { addr, .. } = answer {
|
if let DnsRecord::A { addr, .. } = answer {
|
||||||
|
results.push(SocketAddr::new(IpAddr::V4(*addr), port));
|
||||||
result.push(IpAddr::V4(*addr))
|
result.push(IpAddr::V4(*addr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::AAAA, server, true) {
|
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::AAAA, server, true, timeout_duration) {
|
||||||
for answer in &res.answers {
|
for answer in &res.answers {
|
||||||
if let DnsRecord::AAAA { addr, .. } = answer {
|
if let DnsRecord::AAAA { addr, .. } = answer {
|
||||||
|
results.push(SocketAddr::new(IpAddr::V6(*addr), port));
|
||||||
result.push(IpAddr::V6(*addr))
|
result.push(IpAddr::V6(*addr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,14 +557,11 @@ impl HttpsDnsClient {
|
|||||||
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,20 +603,20 @@ 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(4096)
|
||||||
.read_to_end(&mut bytes)?;
|
.read_to_end(&mut bytes)?;
|
||||||
let mut buffer = VectorPacketBuffer::new();
|
let mut buffer = VectorPacketBuffer::new();
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
+142
-6
@@ -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;
|
||||||
|
|
||||||
|
// TXT RDATA consists of one or more <length><text> segments (RFC 1035 3.3.14)
|
||||||
|
while buffer.pos() < end_pos {
|
||||||
|
let seg_len = buffer.read()? as usize;
|
||||||
|
if seg_len > 0 {
|
||||||
let cur_pos = buffer.pos();
|
let cur_pos = buffer.pos();
|
||||||
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, data_len as usize)?));
|
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, seg_len)?));
|
||||||
|
buffer.step(seg_len)?;
|
||||||
buffer.step(data_len as usize)?;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-28
@@ -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,51 @@ 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);
|
||||||
|
let mut last_err = ResolveError::NoServerFound;
|
||||||
|
|
||||||
|
for upstream in &ordered {
|
||||||
|
let start = Instant::now();
|
||||||
|
let query_result = if is_url(upstream) {
|
||||||
if let Some(client) = &self.context.doh_client {
|
if let Some(client) = &self.context.doh_client {
|
||||||
client.send_query(qname, qtype, upstream, true)?
|
client.send_query(qname, qtype, upstream, true)
|
||||||
} else {
|
} else {
|
||||||
log::error!("This build doesn't support DoH");
|
log::error!("This build doesn't support DoH");
|
||||||
return Err(ResolveError::NoServerFound);
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.context.old_client.send_query(qname, qtype, upstream, true)?
|
self.context.old_client.send_query(qname, qtype, upstream, true)
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(packet) => packet
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match query_result {
|
||||||
|
Ok(mut result) => {
|
||||||
|
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
self.context.forwarder_tracker.record_success(upstream, elapsed);
|
||||||
self.context.cache.store(&result.answers)?;
|
self.context.cache.store(&result.answers)?;
|
||||||
|
|
||||||
Ok(result)
|
// Fix domain names in answers to match original query case
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
for answer in &mut result.answers {
|
||||||
|
if let Some(domain) = answer.get_domain() {
|
||||||
|
if domain.to_lowercase() == qname_lower {
|
||||||
|
answer.set_domain(qname.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.context.forwarder_tracker.record_failure(upstream);
|
||||||
|
last_err = e.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +193,18 @@ impl DnsResolver for RecursiveDnsResolver {
|
|||||||
let _ = self.context.cache.store(&response.answers);
|
let _ = self.context.cache.store(&response.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 +229,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 +251,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 +300,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 +308,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 +325,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Do the same lookup again, and verify that it's present in the cache
|
// Do the same lookup again and verify that it's present in the cache
|
||||||
// and that the counter has been updated
|
// 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 +605,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
@@ -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!()
|
||||||
|
|||||||
+7
-2
@@ -33,9 +33,14 @@ 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,
|
||||||
|
|||||||
+10
-8
@@ -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)]
|
||||||
@@ -251,7 +252,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 +298,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 +316,6 @@ pub fn start_services(settings: &Settings, context: &Arc<Mutex<Context>>) -> (bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dns_server_ok = if settings.dns.threads > 0 {
|
|
||||||
dns_utils::start_dns_server(&context, &settings)
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut miner_obj = Miner::new(Arc::clone(&context));
|
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 +340,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)
|
||||||
|
|||||||
+130
-8
@@ -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 {
|
||||||
@@ -145,3 +177,93 @@ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+436
-309
@@ -2,11 +2,11 @@ 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::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 +17,217 @@ 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 = TrayIconBuilder::new()
|
||||||
MineDomain { name, data, signing, encryption, renewal } => {
|
.with_menu(Box::new(tray_menu))
|
||||||
action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data, signing, encryption, renewal);
|
.with_tooltip(&title)
|
||||||
|
.with_icon(icon)
|
||||||
|
.with_menu_on_left_click(false)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let window_size = tao::dpi::LogicalSize::new(1024, 720);
|
||||||
|
// Get primary monitor and calculate center position
|
||||||
|
let position = match event_loop.primary_monitor() {
|
||||||
|
Some(monitor) => {
|
||||||
|
let monitor_size = monitor.size();
|
||||||
|
let monitor_position = monitor.position();
|
||||||
|
|
||||||
|
let scaled = window_size.to_physical::<i32>(monitor.scale_factor());
|
||||||
|
let center_x = monitor_position.x + (monitor_size.width as i32 - scaled.width) / 2;
|
||||||
|
let center_y = monitor_position.y + (monitor_size.height as i32 - scaled.height) / 2;
|
||||||
|
|
||||||
|
Some(PhysicalPosition::new(center_x, center_y))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = WindowBuilder::new()
|
||||||
|
.with_title(&title)
|
||||||
|
.with_inner_size(window_size)
|
||||||
|
.with_min_inner_size(tao::dpi::LogicalSize::new(773, 350))
|
||||||
|
.with_resizable(true)
|
||||||
|
.with_visible(!hide);
|
||||||
|
|
||||||
|
if let Some(position) = position {
|
||||||
|
builder = builder.with_position(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = builder.build(&event_loop)
|
||||||
|
.expect("Failed to create the window");
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use winapi::um::shellscalingapi::SetProcessDpiAwareness;
|
||||||
|
unsafe {
|
||||||
|
SetProcessDpiAwareness(2);
|
||||||
|
}
|
||||||
|
use tao::platform::windows::IconExtWindows;
|
||||||
|
use tao::window::Icon;
|
||||||
|
let icon = Icon::from_resource(1, None).unwrap();
|
||||||
|
window.set_window_icon(Some(icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone for the IPC handler
|
||||||
|
let context_ipc = Arc::clone(&context);
|
||||||
|
let miner_ipc = Arc::clone(&miner);
|
||||||
|
let proxy = event_loop.create_proxy();
|
||||||
|
let proxy_ipc = proxy.clone();
|
||||||
|
|
||||||
|
// Create webview
|
||||||
|
let builder = WebViewBuilder::new()
|
||||||
|
.with_transparent(false)
|
||||||
|
.with_visible(true)
|
||||||
|
.with_devtools(cfg!(debug_assertions))
|
||||||
|
.with_html(html) // Using test HTML to verify wry works
|
||||||
|
.with_ipc_handler(move |request| {
|
||||||
|
let body = request.body();
|
||||||
|
debug!("Command {}", body);
|
||||||
|
|
||||||
|
match serde_json::from_str(body) {
|
||||||
|
Ok(cmd) => {
|
||||||
|
match cmd {
|
||||||
|
Loaded => {
|
||||||
|
let _ = proxy_ipc.send_event(UserEvent::Loaded);
|
||||||
|
}
|
||||||
|
LoadKey => {
|
||||||
|
action_load_key(&context_ipc, &proxy_ipc);
|
||||||
|
}
|
||||||
|
CreateKey => {
|
||||||
|
keystore::create_key(Arc::clone(&context_ipc));
|
||||||
|
}
|
||||||
|
SaveKey => {
|
||||||
|
action_save_key(&context_ipc);
|
||||||
|
}
|
||||||
|
SelectKey { index } => {
|
||||||
|
action_select_key(&context_ipc, &proxy_ipc, index);
|
||||||
|
}
|
||||||
|
CheckRecord { data } => {
|
||||||
|
let result = check_record(&data);
|
||||||
|
let _ = proxy_ipc.send_event(UserEvent::EvalJs(format!("recordOkay({})", result)));
|
||||||
|
}
|
||||||
|
CheckDomain { name } => {
|
||||||
|
let available = check_domain_available(&context_ipc, &name);
|
||||||
|
let _ = proxy_ipc.send_event(UserEvent::EvalJs(format!("domainAvailable({})", available)));
|
||||||
|
}
|
||||||
|
MineDomain { name, data, signing, encryption, renewal } => {
|
||||||
|
action_create_domain(Arc::clone(&context_ipc), Arc::clone(&miner_ipc), &proxy_ipc, name, data, signing, encryption, renewal);
|
||||||
|
}
|
||||||
|
TransferDomain { name, owner } => {
|
||||||
|
info!("Transferring '{name}' to '{owner}'");
|
||||||
|
}
|
||||||
|
StopMining => {
|
||||||
|
post(Event::ActionStopMining);
|
||||||
}
|
}
|
||||||
TransferDomain { name, owner} => { info!("Transferring '{name}' to '{owner}'"); }
|
|
||||||
StopMining => { post(Event::ActionStopMining); }
|
|
||||||
Open { link } => {
|
Open { link } => {
|
||||||
if open::that(&link).is_err() {
|
if open::that(&link).is_err() {
|
||||||
show_warning(web_view, "Something wrong, I can't open the link 😢");
|
let _ = proxy_ipc.send_event(UserEvent::ShowWarning("Something wrong, I can't open the link 😢".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.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) {
|
|
||||||
match serde_json::from_str::<DnsRecord>(&data) {
|
|
||||||
Ok(record) => {
|
|
||||||
if let Some(string) = record.get_data() {
|
|
||||||
if string.len() > MAX_DATA_LEN {
|
|
||||||
web_view.eval("recordOkay(false)").expect("Error evaluating!");
|
|
||||||
} else {
|
|
||||||
web_view.eval("recordOkay(true)").expect("Error evaluating!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
web_view.eval("recordOkay(false)").expect("Error evaluating!");
|
error!("Error parsing command: {}", e);
|
||||||
dbg!(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
fn action_check_domain(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>, name: String) {
|
#[cfg(not(target_os = "linux"))]
|
||||||
let c = context.lock().unwrap();
|
let webview = builder.build(&window).unwrap();
|
||||||
if let Some(keystore) = c.get_keystore() {
|
#[cfg(target_os = "linux")]
|
||||||
let name = name.to_lowercase();
|
let webview = {
|
||||||
let available = match c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()) {
|
use tao::platform::unix::WindowExtUnix;
|
||||||
MineResult::Fine => true,
|
use wry::WebViewBuilderExtUnix;
|
||||||
_ => false
|
let vbox = window.default_vbox().unwrap();
|
||||||
|
builder.build_gtk(vbox).expect("Failed to build webview gtk object")
|
||||||
};
|
};
|
||||||
web_view.eval(&format!("domainAvailable({})", available)).expect("Error evaluating!");
|
// Disabling context menu on the page in release build
|
||||||
}
|
#[cfg(not(debug_assertions))]
|
||||||
}
|
let _ = webview.evaluate_script("document.addEventListener('contextmenu', e => e.preventDefault());");
|
||||||
|
|
||||||
fn action_save_key(context: &Arc<Mutex<Context>>) {
|
let webview = Arc::new(Mutex::new(webview));
|
||||||
if !context.lock().unwrap().has_keys() {
|
let webview_clone = Arc::clone(&webview);
|
||||||
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) {
|
// Setup event bus listener
|
||||||
if context.lock().unwrap().select_key_by_index(index) {
|
let proxy_events = proxy.clone();
|
||||||
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 +241,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 +276,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 +286,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 +296,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 +305,251 @@ 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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let proxy = event_loop.create_proxy();
|
||||||
|
TrayIconEvent::set_event_handler(Some(move |event| {
|
||||||
|
let _ = proxy.send_event(UserEvent::TrayIconEvent(event));
|
||||||
|
}));
|
||||||
|
|
||||||
|
let proxy = event_loop.create_proxy();
|
||||||
|
MenuEvent::set_event_handler(Some(move |event| {
|
||||||
|
let _ = proxy.send_event(UserEvent::MenuEvent(event));
|
||||||
|
}));
|
||||||
|
|
||||||
|
let proxy = event_loop.create_proxy();
|
||||||
|
|
||||||
|
// Run event loop
|
||||||
|
event_loop.run(move |event, _, control_flow| {
|
||||||
|
*control_flow = ControlFlow::Wait;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
TaoEvent::WindowEvent {
|
||||||
|
event: WindowEvent::CloseRequested,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
window.set_visible(false);
|
||||||
|
}
|
||||||
|
TaoEvent::UserEvent(user_event) => {
|
||||||
|
let wv = webview_clone.lock().unwrap();
|
||||||
|
match user_event {
|
||||||
|
UserEvent::EvalJs(js) => {
|
||||||
|
let js_escaped = js.replace("\\", "\\\\");
|
||||||
|
if let Err(e) = wv.evaluate_script(&js_escaped) {
|
||||||
|
error!("Error evaluating JavaScript: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserEvent::Loaded => {
|
||||||
|
action_loaded(&context, &wv, &proxy);
|
||||||
|
}
|
||||||
|
UserEvent::LoadDomains => {
|
||||||
|
load_domains(&mut context.lock().unwrap(), &wv);
|
||||||
|
}
|
||||||
|
UserEvent::SendKeysToUi => {
|
||||||
|
send_keys_to_ui(&context.lock().unwrap(), &wv);
|
||||||
|
}
|
||||||
|
UserEvent::ShowWarning(text) => {
|
||||||
|
show_warning(&wv, &text);
|
||||||
|
}
|
||||||
|
UserEvent::TrayIconEvent(event) => {
|
||||||
|
match event {
|
||||||
|
TrayIconEvent::DoubleClick { button, .. } => {
|
||||||
|
if button == tray_icon::MouseButton::Left {
|
||||||
|
window.set_visible(true);
|
||||||
|
window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TrayIconEvent::Enter { .. } => {
|
||||||
|
let nodes = connected_nodes.load(Ordering::SeqCst);
|
||||||
|
let title = format!("ALFIS {}\nConnected: {nodes}", env!("CARGO_PKG_VERSION"));
|
||||||
|
let _ = tray_icon.set_tooltip(Some(title));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserEvent::MenuEvent(event) => {
|
||||||
|
if event.id == show_item.id() {
|
||||||
|
window.set_visible(true);
|
||||||
|
} else if event.id == quit_item.id() {
|
||||||
|
info!("Interface closed, exiting");
|
||||||
|
post(Event::ActionQuit);
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum UserEvent {
|
||||||
|
EvalJs(String),
|
||||||
|
Loaded,
|
||||||
|
LoadDomains,
|
||||||
|
SendKeysToUi,
|
||||||
|
ShowWarning(String),
|
||||||
|
TrayIconEvent(TrayIconEvent),
|
||||||
|
MenuEvent(MenuEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load icon from embedded in binary PNG file. Only needed in Linux/macOS builds.
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn load_icon_from_png() -> tray_icon::Icon {
|
||||||
|
// Include PNG in binary
|
||||||
|
const ICON_BYTES: &[u8] = include_bytes!("../img/logo/alfis_icon32.png");
|
||||||
|
|
||||||
|
// decode image by crate `image`
|
||||||
|
let image = image::load_from_memory(ICON_BYTES)
|
||||||
|
.expect("Error loading image from png");
|
||||||
|
let rgba = image.to_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
|
||||||
|
// Convert to format for tray_icon
|
||||||
|
tray_icon::Icon::from_rgba(rgba.into_vec(), width, height)
|
||||||
|
.expect("Error loading icon")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_record(data: &str) -> bool {
|
||||||
|
match serde_json::from_str::<DnsRecord>(data) {
|
||||||
|
Ok(record) => {
|
||||||
|
if let Some(string) = record.get_data() {
|
||||||
|
string.len() <= MAX_DATA_LEN
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_domain_available(context: &Arc<Mutex<Context>>, name: &str) -> bool {
|
||||||
|
let c = context.lock().unwrap();
|
||||||
|
if let Some(keystore) = c.get_keystore() {
|
||||||
|
let name = name.to_lowercase();
|
||||||
|
matches!(c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()), MineResult::Fine)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_save_key(context: &Arc<Mutex<Context>>) {
|
||||||
|
if !context.lock().unwrap().has_keys() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let result = tfd::save_file_dialog_with_filter("Save keys file", "", &["*.toml"], "Key files (*.toml)");
|
||||||
|
match result {
|
||||||
|
None => {}
|
||||||
|
Some(mut new_path) => {
|
||||||
|
if !new_path.ends_with(".toml") {
|
||||||
|
new_path.push_str(".toml");
|
||||||
|
}
|
||||||
|
let path = new_path.clone();
|
||||||
|
if let Some(keystore) = context.lock().unwrap().get_keystore_mut() {
|
||||||
|
let public = keystore.get_public().to_string();
|
||||||
|
let hash = keystore.get_hash().to_string();
|
||||||
|
keystore.save(&new_path, "");
|
||||||
|
info!("Key file saved to {}", &path);
|
||||||
|
post(Event::KeySaved { path, public, hash });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_select_key(context: &Arc<Mutex<Context>>, proxy: &EventLoopProxy<UserEvent>, index: usize) {
|
||||||
|
if context.lock().unwrap().select_key_by_index(index) {
|
||||||
|
let (path, public, hash) = {
|
||||||
|
let keystore = context.lock().unwrap().get_keystore().cloned().unwrap();
|
||||||
|
let path = keystore.get_path().to_owned();
|
||||||
|
let public = keystore.get_public().to_string();
|
||||||
|
let hash = keystore.get_hash().to_string();
|
||||||
|
(path, public, hash)
|
||||||
|
};
|
||||||
|
post(Event::KeyLoaded { path, public, hash });
|
||||||
|
let _ = proxy.send_event(UserEvent::EvalJs(format!("keySelected({})", index)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_load_key(context: &Arc<Mutex<Context>>, proxy: &EventLoopProxy<UserEvent>) {
|
||||||
|
let result = tfd::open_file_dialog("Open keys file", "", Some((&["*.key", "*.toml"], "Key files")));
|
||||||
|
match result {
|
||||||
|
None => {}
|
||||||
|
Some(file_name) => {
|
||||||
|
match Keystore::from_file(&file_name, "") {
|
||||||
|
None => {
|
||||||
|
error!("Error loading keystore '{}'!", &file_name);
|
||||||
|
let _ = proxy.send_event(UserEvent::ShowWarning("Error loading key!<br>Key cannot be loaded or its difficulty is not enough.".to_string()));
|
||||||
|
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('fail', '{}', 'Error loading key from \\\\'{}\\\\!');", Local::now().format("%d.%m.%y %X"), &file_name)));
|
||||||
|
}
|
||||||
|
Some(keystore) => {
|
||||||
|
info!("Loaded keystore with keys: {:?}, {:?}", &keystore.get_public(), &keystore.get_encryption_public());
|
||||||
|
let path = keystore.get_path().to_owned();
|
||||||
|
let public = keystore.get_public().to_string();
|
||||||
|
let hash = keystore.get_hash().to_string();
|
||||||
|
post(Event::KeyLoaded { path, public, hash });
|
||||||
|
|
||||||
|
if !context.lock().unwrap().select_key_by_public(&keystore.get_public()) {
|
||||||
|
context.lock().unwrap().add_keystore(keystore);
|
||||||
|
} else {
|
||||||
|
warn!("This key is already loaded!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_loaded(context: &Arc<Mutex<Context>>, webview: &wry::WebView, proxy: &EventLoopProxy<UserEvent>) {
|
||||||
|
info!("Interface loaded");
|
||||||
|
let _ = webview.evaluate_script("showMiningIndicator(false, false);");
|
||||||
|
|
||||||
|
let c = context.lock().unwrap();
|
||||||
|
|
||||||
if let Some(keystore) = c.get_keystore() {
|
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 +557,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 +574,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);
|
||||||
web_view.eval(&command)
|
let _ = webview.evaluate_script(&command);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, web_view: &mut WebView<()>, name: String, data: String, signing: String, encryption: String, renewal: bool) {
|
fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, proxy: &EventLoopProxy<UserEvent>, name: String, data: String, signing: String, encryption: String, renewal: bool) {
|
||||||
debug!("Creating domain with data: {}", &data);
|
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 +684,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 +738,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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+16330
-6390
File diff suppressed because it is too large
Load Diff
+11
-6
@@ -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,13 +212,18 @@ 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) {
|
||||||
|
// 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);
|
window.webkit.messageHandlers.external.postMessage(x);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -85,7 +85,7 @@ fn run_service_logic() -> Result<()> {
|
|||||||
let (_dns_server_ok, _miner, _network) = start_services(&settings, &context);
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user