5 Commits

Author SHA1 Message Date
Sweetbread 10a6813a8b wip 2025-10-01 22:57:48 +03:00
Sweetbread 886f42e062 ui(credentails): loading key 2025-10-01 15:31:21 +03:00
Sweetbread f06e6f9121 wip 2025-10-01 15:04:46 +03:00
Sweetbread 6240208c17 wip 2025-10-01 15:04:46 +03:00
Sweetbread 9b1dc672a9 Update shell 2025-10-01 15:04:46 +03:00
38 changed files with 9052 additions and 21423 deletions
-7
View File
@@ -24,10 +24,3 @@ rustflags = ["-Ctarget-feature=+crt-static", "-Clink-arg=-s"]
[target.mipsel-unknown-linux-musl]
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"
+5 -16
View File
@@ -16,28 +16,17 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
- os: ubuntu-24.04
- os: macos-latest
target: aarch64-apple-darwin
os: [ windows-latest, ubuntu-latest, macOS-latest]
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev
- name: Install libgtk-dev libwebkit2gtk-4.0
run: sudo apt update && sudo apt install libwebkit2gtk-4.0-dev
if: contains(matrix.os, 'ubuntu')
- name: Update Rust
run: rustup update stable
- name: Install ARM target (macOS)
run: rustup target add aarch64-apple-darwin
if: matrix.target == 'aarch64-apple-darwin'
- name: Build
run: cargo build --verbose ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
run: cargo build --verbose
- name: Run tests
run: cargo test --all --verbose ${{ matrix.target && format('--target {0}', matrix.target) || '' }}
run: cargo test --all --verbose
+31 -6
View File
@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [ amd64, i686, armhf, armel, arm64 ]
arch: [ amd64, i686, armhf, armlf, arm64 ]
defaults:
run:
shell: bash
@@ -65,9 +65,7 @@ jobs:
- name: install dependencies
run: |
sudo apt update
sudo apt upgrade
sudo apt install libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev upx-ucl
sudo apt update && sudo apt upgrade && sudo apt install libwebkit2gtk-4.0-dev upx
cargo install cross
- name: Build and package deb packages
@@ -99,14 +97,20 @@ jobs:
- name: install dependencies
if: contains(matrix.os, 'ubuntu')
run: sudo apt update && sudo apt install --no-install-recommends libwebkit2gtk-4.1-dev libxdo-dev libsoup-3.0-dev upx-ucl
run: sudo apt update && sudo apt install --no-install-recommends libwebkit2gtk-4.0-dev upx
- name: Build release binaries
run: cargo build --release
- name: Build Windows release binaries with Edge web-engine
if: contains(matrix.os, 'windows')
run: cargo build --release --features "edge" --target-dir edge
- name: windows
if: contains(matrix.os, 'windows')
run: echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV
run: |
echo "BIN_ARCH=windows-amd64" >> $GITHUB_ENV
echo "BIN_ARCH_EDGE=windows-amd64-edge" >> $GITHUB_ENV
- name: linux
if: contains(matrix.os, 'ubuntu')
@@ -125,6 +129,8 @@ jobs:
run: |
echo "BIN_PATH=target/release/alfis.exe" >> $GITHUB_ENV
echo "ZIP_NAME=alfis-${{env.BIN_ARCH}}-${{ needs.get_version.outputs.project_version }}.zip" >> $GITHUB_ENV
echo "BIN_PATH_EDGE=edge/release/alfis.exe" >> $GITHUB_ENV
echo "ZIP_NAME_EDGE=alfis-${{env.BIN_ARCH}}-${{ needs.get_version.outputs.project_version }}-edge.zip" >> $GITHUB_ENV
- name: Packaging
uses: papeloto/action-zip@v1
@@ -132,6 +138,13 @@ jobs:
files: ${{ env.BIN_PATH }} alfis.toml README.md LICENSE adblock.txt
dest: ${{ env.ZIP_NAME }}
- name: Packaging Edge binary
if: contains(matrix.os, 'windows')
uses: papeloto/action-zip@v1
with:
files: ${{ env.BIN_PATH_EDGE }} alfis.toml README.md LICENSE adblock.txt
dest: ${{ env.ZIP_NAME_EDGE }}
- name: Upload zip
id: upload-zip
uses: actions/upload-release-asset@v1
@@ -142,3 +155,15 @@ jobs:
asset_path: ${{ env.ZIP_NAME }}
asset_name: ${{ env.ZIP_NAME }}
asset_content_type: application/zip
- name: Upload Edge binary
if: contains(matrix.os, 'windows')
id: upload-edge-binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_path: ${{ env.ZIP_NAME_EDGE }}
asset_name: ${{ env.ZIP_NAME_EDGE }}
asset_content_type: application/zip
Generated
+562 -2528
View File
File diff suppressed because it is too large Load Diff
+24 -26
View File
@@ -1,6 +1,6 @@
[package]
name = "alfis"
version = "0.8.9"
version = "0.8.6"
authors = ["Revertron <alfis@revertron.com>"]
edition = "2021"
build = "build.rs"
@@ -11,50 +11,46 @@ exclude = ["blockchain.db", "alfis.toml"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
getopts = "0.2.24"
log = "0.4.28"
getopts = "0.2.21"
log = "0.4.22"
simplelog = "0.12.2"
toml = "1.0.7"
sha2 = "0.10.9"
ed25519-dalek = "2.2.0"
toml = "0.8.19"
sha2 = "0.10.8"
ed25519-dalek = "2.1.1"
x25519-dalek = { version = "2.0.1", features = ["reusable_secrets"] }
ecies-ed25519-ng = { git = "https://github.com/Revertron/ecies-ed25519-ng", rev = "554ca29", version = "0.5.3" }
chacha20poly1305 = "0.10.1"
blakeout = "0.3.0"
num_cpus = "1.17.0"
num_cpus = "1.16.0"
byteorder = "1.5.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = { version = "2.0.1", features = ["serde"] }
bincode = "1.3.3"
serde_cbor = "0.11.2"
num-bigint = "0.4.6"
chrono = { version = "0.4.42", features = ["serde"] }
time = "0.3.44"
chrono = { version = "0.4.38", features = ["serde"] }
time = "0.3.36"
rand = { package = "rand", version = "0.8.5" }
sqlite = "0.37.0"
uuid = { version = "1.18.1", features = ["serde", "v4"] }
sqlite = "0.36.0"
uuid = { version = "1.11.0", features = ["serde", "v4"] }
mio = { version = "1.0.0", features = ["os-poll", "net"] }
ureq = { version = "3.1.4", optional = true }
lru = "0.16.2"
derive_more = { version = "2.0.1", features = ["display", "error", "from"] }
ureq = { version = "2.10", optional = true }
lru = "0.12"
derive_more = { version = "1.0.0", features = ["display", "error", "from"] }
lazy_static = "1.5.0"
spmc = "0.3.0"
thread-priority = "3.0.0"
crossbeam-channel = "0.5.13"
thread-priority = "1.2.0"
# Optional dependencies regulated by features
wry = { version = "0.53", optional = true }
tao = { version = "0.34", optional = true }
tray-icon = { version = "0.21.2", optional = true }
# web-view = { git = "https://github.com/Boscop/web-view", features = [], optional = true }
gtk = { package = "gtk4", version = "0.9.6", features = ["v4_16"], optional = true }
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_5"], optional = true }
tinyfiledialogs = { version = "3.9.1", optional = true }
open = { version = "5.3.0", optional = true }
[target.'cfg(not(target_os = "windows"))'.dependencies]
image = { version = "0.25", default-features = false, features = ["png"] }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["impl-default", "wincon", "shellscalingapi"] }
windows-service = "0.8.0"
windows-service = "0.7.0"
[build-dependencies]
winres = "0.1.12"
@@ -75,6 +71,8 @@ ProductName="ALFIS"
FileDescription="Alternative Free Identity System"
[features]
webgui = ["wry", "tao", "tray-icon", "tinyfiledialogs", "open"]
gui = ["gtk", "adw"]
# webgui = ["web-view", "tinyfiledialogs", "open"]
# edge = ["webgui", "web-view/edge"]
doh = ["ureq"]
default = ["webgui", "doh"]
default = ["gui", "doh"]
+13 -13
View File
@@ -8,9 +8,9 @@ This project represents a minimal blockchain without cryptocurrency, capable of
Not so clear? Hold on.
## This software provides:
- Tiny and [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) synchronized database of domain names.
The consistency of this database is based on [blockchain](https://en.wikipedia.org/wiki/Blockchain) technology that prevents retroactive changing of data, and has strict cryptographical consensus.
- DNS server with cache and enhanced security features. It resolves the domains from database and forwards all regular DNS-requests to some other resolver your router, Google DNS, Cloudflare DNS, or [AdGuard DNS](https://dns.adguard.com/) (if you want to block ads and trackers).
- Very small and [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) synchronized database of domain names.
The consistency of this database is based on [blockchain](https://en.wikipedia.org/wiki/Blockchain) technology, that prevents retroactive changing of data, and has strict cryptographical consensus.
- DNS server with cache, like you have in your Internet-router. It resolves the domains from database and forwards all regular DNS-requests to some other resolver - your router, Google DNS, Cloudflare DNS, or [AdGuard DNS](https://dns.adguard.com/) (if you want to block ads and trackers).
- Other systems need you to organize and run several DNS-servers to resolve their domains and regular domains, we have both in one.
Moreover, ALFIS can forward requests of regular domains to [DNS-over-HTTPS](https://en.wikipedia.org/wiki/DNS_over_HTTPS) server. The security and privacy is right here.
- Convenient graphical user interface to create domains in this alternative domain system. If you want just to use it like a DNS-server you can run it with `-n` flag or just build/download the variant without GUI.
@@ -22,7 +22,7 @@ Moreover, ALFIS can forward requests of regular domains to [DNS-over-HTTPS](http
![Screenshot](img/domains.png)
## How does it work?
## How it works?
Every node connects to its siblings and synchronizes the domain database.
This DB consists of cryptographically bound blocks, that contain encrypted domain names, contacts, and some info, if you wish.
There are 10 domain zones available to get domain in:
@@ -46,7 +46,7 @@ You don't need any additional steps to build Alfis, just stick to the MSVC versi
If you see an error about missing `VCRUNTIME140.dll` when running alfis you will need to install [VC Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) from Microsoft.
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/).
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)).
### ![Windows Logo](/img/windows.svg) On Windows (MINGW64)
If you'd rather use Gnu version of Rust you can build Alfis by these steps:
@@ -58,8 +58,8 @@ cargo build
```
### ![Linux Logo](/img/linux.svg) On Linux
If you are building on Linux, you must ensure that you have `libwebkitgtk` and `libxdo` libraries installed (for UI and tray icon respectively).
You can do it by issuing this command: `sudo apt install libwebkit2gtk-4.1-dev libxdo-dev` (on Debian/Ubuntu and derivatives).
If you are building on Linux you must ensure that you have `libwebkitgtk` library installed.
You can do it by issuing this command: `sudo apt install libwebkit2gtk-4.0-dev` (on Debian/Ubuntu and derivatives).
#### ![Arch Linux Logo](/img/archlinux.svg) On Arch Linux
@@ -93,7 +93,7 @@ Beware of NetworkManager, it can change your resolvers at will.
gpg --fetch-keys https://deb.revertron.com/key.txt
gpg --export F244E16645D86D62 | sudo tee /usr/local/apt-keys/alfis.gpg > /dev/null
```
2. Add a repository path to sources list
2. Add repository path to sources list
```
echo 'deb [signed-by=/usr/local/apt-keys/alfis.gpg] https://deb.revertron.com/ debian alfis' | sudo tee /etc/apt/sources.list.d/alfis.list
```
@@ -133,18 +133,18 @@ docker run --rm --name alfis -p 53:53/tcp -p 53:53/udp cofob/alfis
### GUI version Windows/Linux/macOS (if you want to create and change domains)
If you want to create and manage your own domains on blockchain, you will need a version with GUI.
You can download it from [releases](https://github.com/Revertron/Alfis/releases) section, choose the appropriate OS and architecture version.
You can download it from [releases](https://github.com/Revertron/Alfis/releases) section, choose appropriate OS and architecture version.
It needs to be without `nogui` suffix.
Just unzip that archive in some directory and run `alfis` (or `alfis.exe`) binary.
By default, it searches for a config file, named `alfis.toml` in current working directory, and creates/changes `blockchain.db` file in the same directory.
By default, it searches for config file, named `alfis.toml` in current working directory, and creates/changes `blockchain.db` file in the same directory.
If you want it to load config from another file you can command it so: `alfis -c /etc/alfis.conf`.
## Roadmap
1. Stabilize blockchain functions (domain transfer, info & contacts in UI), bug hunting and fixing.
2. ~~Change DNS server/proxy to own resource saving implementation (using trust-dns-proto for RR parsing).~~
1. Stabilize blockchain functions (domain transfer, info & contacts in UI), bug hunting and fixing.
2. Change DNS server/proxy to own resource saving implementation (using trust-dns-proto for RR parsing).
3. P2P traffic encryption (ECDH). ✅
4. ~~Web-GUI to manage your node from browser.~~
4. Web-GUI to manage you node from browser.
## Remarkable contributions
* [@umasterov](https://github.com/umasterov) contributed fantastic logo for this project.
+690 -464
View File
File diff suppressed because it is too large Load Diff
+4 -15
View File
@@ -1,4 +1,4 @@
# The hash of the first block in a chain to know with which nodes to work
# The hash of first block in a chain to know with which nodes to work
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
# Paths to your key files to load automatically
key_files = ["key1.toml", "key2.toml", "key3.toml", "key4.toml", "key5.toml"]
@@ -10,7 +10,7 @@ check_blocks = 8
# All bootstrap nodes
peers = ["peer-v4.alfis.name:4244", "peer-v6.alfis.name:4244", "peer-ygg.alfis.name:4244"]
# Your node will listen on that address for other nodes to connect
listen = "[::]:4244"
listen = "[::]:42440"
# Set true if you want your IP to participate in peer-exchange, or false otherwise
public = true
# Allow connections to/from Yggdrasil only (https://yggdrasil-network.github.io)
@@ -19,15 +19,9 @@ yggdrasil_only = false
# DNS resolver options
[dns]
# Your DNS resolver will be listening on this address and port (Usual port is 53)
listen = "127.0.0.3:53"
listen = "127.0.0.1:5311"
# How many threads to spawn by DNS server
threads = 10
# DNS cache memory limit in megabytes (default: 100)
# Prevents unbounded cache growth in high-load environments
# Set to 0 for unlimited cache (not recommended for production)
cache_memory_limit_mb = 100
# AdGuard DNS servers to filter ads and trackers
forwarders = ["https://dns.adguard.com/dns-query"]
#forwarders = ["94.140.14.14:53", "94.140.15.15:53"]
@@ -38,11 +32,6 @@ forwarders = ["https://dns.adguard.com/dns-query"]
# Bootstrap DNS-servers to resolve domains of DoH providers
bootstraps = ["9.9.9.9:53", "94.140.14.14:53"]
# Enable DNS 0x20 encoding for cache poisoning protection
# Recommended: true (default)
# Set false only if upstream resolvers don't preserve case (very rare)
enable_0x20 = true
# Hosts file support (resolve local names or block ads)
#hosts = ["system", "adblock.txt"]
@@ -50,5 +39,5 @@ enable_0x20 = true
[mining]
# How many CPU threads to spawn for mining, zero = number of CPU cores
threads = 0
# Set a lower priority for mining threads
# Set lower priority for mining threads
lower = true
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -35,10 +35,10 @@ elif [ $PKGARCH = "i686" ]; then TARGET='i686-unknown-linux-musl'
elif [ $PKGARCH = "mipsel" ]; then TARGET='mipsel-unknown-linux-musl'
elif [ $PKGARCH = "mips" ]; then TARGET='mips-unknown-linux-musl'
elif [ $PKGARCH = "armhf" ]; then TARGET='armv7-unknown-linux-musleabihf'
elif [ $PKGARCH = "armel" ]; then TARGET='arm-unknown-linux-musleabi'
elif [ $PKGARCH = "armlf" ]; then TARGET='arm-unknown-linux-musleabi'
elif [ $PKGARCH = "arm64" ]; then TARGET='aarch64-unknown-linux-musl'
else
echo "Specify PKGARCH=amd64,i686,mips,mipsel,armhf,armel,arm64"
echo "Specify PKGARCH=amd64,i686,mips,mipsel,armhf,armlf,arm64"
exit 1
fi
Binary file not shown.
+9 -2
View File
@@ -1,6 +1,13 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
buildInputs =
[ pkgs.cargo pkgs.rustc pkgs.webkitgtk pkgs.pkg-config pkgs.kdialog ];
buildInputs = with pkgs; [
cargo
rustc
rust-analyzer
pkg-config
libsForQt5.kdialog
libadwaita
gtk4
];
}
+2 -2
View File
@@ -3,7 +3,7 @@ extern crate serde_json;
use std::cell::RefCell;
use std::fmt::Debug;
use bincode::config;
use serde::{Deserialize, Serialize};
use crate::blockchain::hash_utils::{hash_difficulty, key_hash_difficulty};
@@ -94,7 +94,7 @@ impl Block {
/// Serializes block to bincode format for hashing.
pub fn as_bytes_compact(&self) -> Vec<u8> {
bincode::serde::encode_to_vec(&self, config::legacy()).unwrap()
bincode::serialize(&self).unwrap()
}
/// Checks if this block is superior to the other
+20 -142
View File
@@ -1,12 +1,10 @@
use std::net::{IpAddr, SocketAddr};
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use crate::blockchain::transaction::DomainData;
use crate::commons::rtt_tracker::RttTracker;
use crate::dns::filter::DnsFilter;
use crate::dns::protocol::{DnsPacket, DnsQuestion, DnsRecord, QueryType, ResultCode, TransientTtl};
use crate::Context;
@@ -16,16 +14,12 @@ const NAME_SERVER: &str = "ns.alfis.name";
const SERVER_ADMIN: &str = "admin.alfis.name";
pub struct BlockchainFilter {
context: Arc<Mutex<Context>>,
ns_tracker: Arc<RttTracker<IpAddr>>,
context: Arc<Mutex<Context>>
}
impl BlockchainFilter {
pub fn new(context: Arc<Mutex<Context>>) -> Self {
BlockchainFilter {
context,
ns_tracker: Arc::new(RttTracker::new()),
}
BlockchainFilter { context }
}
fn add_soa_record(zone: String, serial: u32, packet: &mut DnsPacket) {
@@ -50,53 +44,36 @@ impl BlockchainFilter {
have_zone
}
fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &[IpAddr], tracker: &RttTracker<IpAddr>) -> Option<DnsPacket> {
let mut dns_client = DnsNetworkClient::new();
fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &Vec<IpAddr>) -> Option<DnsPacket> {
let port = 10000 + (rand::random::<u16>() % 50000);
let mut dns_client = DnsNetworkClient::new(port);
dns_client.run().unwrap();
let timeout = std::time::Duration::from_secs(2);
let ordered = tracker.select_ordered(servers);
for server in &ordered {
let addr = SocketAddr::new(*server, 53);
let start = Instant::now();
match dns_client.send_udp_query(qname, qtype, addr, false, timeout) {
Ok(res) => {
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
tracker.record_success(server, elapsed);
for server in servers {
let addr = SocketAddr::new(server.to_owned(), 53);
if let Ok(res) = dns_client.send_udp_query(qname, qtype, addr, false) {
dns_client.stop();
return Some(res);
}
Err(_) => {
tracker.record_failure(server);
}
}
}
dns_client.stop();
None
}
fn create_packet(&self, qname: &str, qtype: QueryType, zone: String, answers: Vec<DnsRecord>, ns_records: Vec<DnsRecord>, glue_records: Vec<DnsRecord>) -> Option<DnsPacket> {
fn create_packet(&self, qname: &str, qtype: QueryType, zone: String, answers: Vec<DnsRecord>) -> Option<DnsPacket> {
if !answers.is_empty() {
// Create DnsPacket with answers
// Create DnsPacket
let mut packet = DnsPacket::new();
packet.header.authoritative_answer = true;
packet.questions.push(DnsQuestion::new(String::from(qname), qtype));
for answer in answers {
packet.answers.push(answer);
}
// 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);
}
packet.authorities.push(DnsRecord::NS { domain: zone, host: String::from(NAME_SERVER), ttl: TransientTtl(600) });
//trace!("Returning packet: {:?}", &packet);
Some(packet)
} else {
// Create DnsPacket without answers
// Create DnsPacket
let mut packet = DnsPacket::new();
packet.header.authoritative_answer = true;
packet.header.rescode = ResultCode::NXDOMAIN;
@@ -108,7 +85,7 @@ impl BlockchainFilter {
}
}
fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData, recursive: bool, tracker: &RttTracker<IpAddr>) -> (bool, Option<DnsPacket>) {
fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData) -> (bool, Option<DnsPacket>) {
// First we search for NS records, collecting nameserver domains
let mut hosts = Vec::new();
for record in data.records.iter() {
@@ -126,27 +103,7 @@ impl BlockchainFilter {
return (false, None);
}
// 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
// Searching glue records
let mut servers = Vec::new();
for record in data.records.iter() {
match &record {
@@ -172,7 +129,7 @@ impl BlockchainFilter {
if !servers.is_empty() {
trace!("Found NS servers for domain {}: {:?}", &qname, &servers);
let answer = BlockchainFilter::lookup_from_ns(qname, qtype, &servers, tracker);
let answer = BlockchainFilter::lookup_from_ns(qname, qtype, &servers);
if let Some(packet) = &answer {
trace!("Resolved {:?} from NS: {:?}", (qname, qtype), &packet.answers);
}
@@ -181,76 +138,13 @@ impl BlockchainFilter {
(false, None)
}
/// Extract NS records from domain data and return them
fn get_ns_records(data: &DomainData, top_domain: &str) -> Vec<DnsRecord> {
data.records.iter()
.filter_map(|record| {
if let DnsRecord::NS { domain, host, ttl } = record {
if domain == "@" {
return Some(DnsRecord::NS {
domain: String::from(top_domain),
host: host.clone(),
ttl: *ttl
});
}
}
None
})
.collect()
}
/// Extract GLUE records (A/AAAA records for NS hosts within the same domain)
fn get_glue_records(data: &DomainData, top_domain: &str, ns_hosts: &[String]) -> Vec<DnsRecord> {
let mut glue_records = Vec::new();
for record in data.records.iter() {
match record {
DnsRecord::A { domain, addr, ttl } => {
let full_domain = if domain == "@" {
String::from(top_domain)
} else {
format!("{}.{}", domain, top_domain)
};
if ns_hosts.iter().any(|ns| ns == &full_domain) {
glue_records.push(DnsRecord::A {
domain: full_domain,
addr: addr.clone(),
ttl: *ttl
});
}
}
DnsRecord::AAAA { domain, addr, ttl } => {
let full_domain = if domain == "@" {
String::from(top_domain)
} else {
format!("{}.{}", domain, top_domain)
};
if ns_hosts.iter().any(|ns| ns == &full_domain) {
glue_records.push(DnsRecord::AAAA {
domain: full_domain,
addr: addr.clone(),
ttl: *ttl
});
}
}
_ => {}
}
}
glue_records
}
}
impl DnsFilter for BlockchainFilter {
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket> {
// Lowercase for case-insensitive lookup (blockchain stores domains as lowercase)
let qname_lower = qname.to_lowercase();
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
let top_domain;
let subdomain;
let parts: Vec<&str> = qname_lower.rsplitn(3, '.').collect();
let parts: Vec<&str> = qname.rsplitn(3, '.').collect();
match parts.len() {
1 => {
let mut packet = DnsPacket::new();
@@ -298,13 +192,10 @@ impl DnsFilter for BlockchainFilter {
};
// Check if this domain has NS records and needs to resolve all records through them
// 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);
let (has_ns, result) = Self::resolve_by_ns(qname, qtype, &top_domain, &data);
if has_ns {
return result;
}
}
let mut answers: Vec<DnsRecord> = Vec::new();
let mut cname: Option<DnsRecord> = None;
@@ -346,7 +237,7 @@ impl DnsFilter for BlockchainFilter {
let mut domain_exists = !answers.is_empty() || subdomain.is_empty();
if answers.is_empty() {
// If there are no records found we search for *.domain.tld record
for mut record in data.records.iter_mut() {
for mut record in data.records {
let record_domain = record.get_domain().unwrap_or(String::new());
if record.get_querytype() == qtype && record_domain == "*" {
match &mut record {
@@ -372,20 +263,7 @@ impl DnsFilter for BlockchainFilter {
}
}
// 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 let Some(mut packet) = self.create_packet(qname, qtype, zone, answers) {
if domain_exists && packet.answers.is_empty() {
packet.header.rescode = ResultCode::NOERROR;
}
+1 -1
View File
@@ -45,7 +45,7 @@ pub const UI_REFRESH_DELAY_MS: u128 = 500;
pub const LOG_REFRESH_DELAY_SEC: u64 = 60;
pub const POLL_TIMEOUT: Option<Duration> = Some(Duration::from_millis(200));
pub const WAIT_FOR_INTERNET: Duration = Duration::from_secs(5);
pub const WAIT_FOR_INTERNET: Duration = Duration::from_secs(10);
/// We start syncing blocks only when we got 4 and more connected nodes
pub const MIN_CONNECTED_NODES_START_SYNC: usize = 4;
pub const MAX_READ_BLOCK_TIME: u128 = 100;
-2
View File
@@ -10,7 +10,6 @@ use crate::dns::protocol::DnsRecord;
pub mod constants;
pub mod eventbus;
pub mod rtt_tracker;
pub mod simplebus;
/// Convert bytes array to HEX format
@@ -129,7 +128,6 @@ pub fn is_yggdrasil_record(record: &DnsRecord) -> bool {
DnsRecord::SRV { .. } => {}
DnsRecord::OPT { .. } => {}
DnsRecord::TLSA { .. } => {}
DnsRecord::HTTPS { .. } => {}
}
true
}
-110
View File
@@ -1,110 +0,0 @@
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();
}
}
+4 -9
View File
@@ -15,14 +15,9 @@ pub struct Chacha {
impl Chacha {
pub fn new(key: &[u8], nonce: &[u8]) -> Self {
// Convert slices to fixed-size arrays, then to GenericArray
let key_array: [u8; 32] = key.try_into().expect("Key must be 32 bytes");
let key = Key::from(key_array);
let cipher = ChaCha20Poly1305::new(&key);
let nonce_array: [u8; 12] = nonce.try_into().expect("Nonce must be 12 bytes");
let nonce = Nonce::from(nonce_array);
let key = Key::from_slice(key);
let cipher = ChaCha20Poly1305::new(key);
let nonce = Nonce::clone_from_slice(nonce);
Chacha { cipher, nonce }
}
@@ -35,7 +30,7 @@ impl Chacha {
}
pub fn get_nonce(&self) -> &[u8] {
self.nonce.as_ref()
&self.nonce.as_slice()
}
}
+8 -10
View File
@@ -136,7 +136,7 @@ pub trait PacketBuffer {
outstr.push_str(delim);
let str_buffer = self.get_range(pos, len as usize)?;
outstr.push_str(&String::from_utf8_lossy(str_buffer));
outstr.push_str(&String::from_utf8_lossy(str_buffer).to_lowercase());
delim = ".";
@@ -226,7 +226,7 @@ where T: Read {
}
impl<'a, T> StreamPacketBuffer<'a, T> where T: Read + 'a {
pub fn new(stream: &'a mut T) -> StreamPacketBuffer<'a, T> {
pub fn new(stream: &'a mut T) -> StreamPacketBuffer<'_, T> {
StreamPacketBuffer {
stream,
buffer: Vec::new(),
@@ -300,16 +300,14 @@ impl<'a, T> PacketBuffer for StreamPacketBuffer<'a, T> where T: Read + 'a {
}
}
const BUF_SIZE: usize = 4096;
pub struct BytePacketBuffer {
pub buf: [u8; BUF_SIZE],
pub buf: [u8; 512],
pub pos: usize
}
impl BytePacketBuffer {
pub fn new() -> BytePacketBuffer {
BytePacketBuffer { buf: [0; BUF_SIZE], pos: 0 }
BytePacketBuffer { buf: [0; 512], pos: 0 }
}
}
@@ -321,7 +319,7 @@ impl Default for BytePacketBuffer {
impl PacketBuffer for BytePacketBuffer {
fn read(&mut self) -> Result<u8> {
if self.pos >= BUF_SIZE {
if self.pos >= 512 {
return Err(BufferError::EndOfBuffer);
}
let res = self.buf[self.pos];
@@ -331,21 +329,21 @@ impl PacketBuffer for BytePacketBuffer {
}
fn get(&mut self, pos: usize) -> Result<u8> {
if pos >= BUF_SIZE {
if pos >= 512 {
return Err(BufferError::EndOfBuffer);
}
Ok(self.buf[pos])
}
fn get_range(&mut self, start: usize, len: usize) -> Result<&[u8]> {
if start + len >= BUF_SIZE {
if start + len >= 512 {
return Err(BufferError::EndOfBuffer);
}
Ok(&self.buf[start..start + len as usize])
}
fn write(&mut self, val: u8) -> Result<()> {
if self.pos >= BUF_SIZE {
if self.pos >= 512 {
return Err(BufferError::EndOfBuffer);
}
self.buf[self.pos] = val;
+18 -302
View File
@@ -2,82 +2,16 @@
extern crate serde;
use std::clone::Clone;
use std::collections::{HashMap, HashSet};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::{Arc, RwLock};
use lru::LruCache;
use chrono::*;
use derive_more::{Display, Error, From};
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};
use crate::dns::protocol::{DnsPacket, DnsRecord, QueryType, ResultCode};
/// Estimate the memory size of a DNS record in bytes
fn estimate_dns_record_size(record: &DnsRecord) -> usize {
match record {
DnsRecord::A { domain, .. } => 56 + domain.len(),
DnsRecord::AAAA { domain, .. } => 68 + domain.len(),
DnsRecord::NS { domain, host, .. } |
DnsRecord::CNAME { domain, host, .. } => 64 + domain.len() + host.len(),
DnsRecord::MX { domain, host, .. } => 72 + domain.len() + host.len(),
DnsRecord::SRV { domain, host, .. } => 80 + domain.len() + host.len(),
DnsRecord::SOA { domain, m_name, r_name, .. } =>
120 + domain.len() + m_name.len() + r_name.len(),
DnsRecord::TXT { domain, data, .. } => 64 + domain.len() + data.len(),
DnsRecord::PTR { domain, data, .. } => 64 + domain.len() + data.len(),
DnsRecord::TLSA { domain, data, .. } => 80 + domain.len() + data.len(),
DnsRecord::HTTPS { domain, target, params, .. } =>
88 + domain.len() + target.len() + params.len(),
DnsRecord::UNKNOWN { domain, .. } => 64 + domain.len(),
DnsRecord::OPT { data, .. } => 48 + data.len(),
}
}
/// Estimate the memory size of a domain entry in bytes
fn estimate_domain_entry_size(entry: &DomainEntry) -> usize {
let mut size = 0;
// Base struct sizes
size += std::mem::size_of::<DomainEntry>(); // ~56 bytes
size += std::mem::size_of::<Arc<DomainEntry>>(); // 16 bytes
// Domain string: 24 byte header + actual chars
size += 24 + entry.domain.len();
// HashMap base overhead
size += 24;
size += entry.record_types.len() * 32; // Bucket overhead per entry
// Calculate size of each RecordSet
for (_qtype, record_set) in &entry.record_types {
size += std::mem::size_of::<QueryType>(); // 2 bytes
match record_set {
RecordSet::NoRecords { .. } => {
size += 56; // Enum variant + timestamp + ttl
}
RecordSet::Records { records, .. } => {
size += 56; // Base enum variant
size += 24; // HashSet base
size += records.len() * 16; // Bucket overhead per record
// Sum up all record sizes
for record_entry in records {
size += estimate_dns_record_size(&record_entry.record);
size += 32; // DateTime<Local> overhead
}
}
}
}
size
}
#[derive(Debug, Display, From, Error)]
pub enum CacheError {
Io(std::io::Error),
@@ -198,7 +132,7 @@ impl DomainEntry {
}
}
pub fn fill_queryresult(&self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>) {
pub fn fill_queryresult(&self, qtype: QueryType, result_vec: &mut Vec<DnsRecord>) {
let now = Local::now();
let current_set = match self.record_types.get(&qtype) {
@@ -215,71 +149,21 @@ impl DomainEntry {
}
if entry.record.get_querytype() == qtype {
let mut record = entry.record.clone();
// Preserve the original query case in the response
record.set_domain(qname.to_string());
result_vec.push(record);
result_vec.push(entry.record.clone());
}
}
}
}
}
#[derive(Default)]
pub struct Cache {
domain_entries: LruCache<String, Arc<DomainEntry>>,
current_memory_bytes: usize,
max_memory_bytes: usize
domain_entries: BTreeMap<String, Arc<DomainEntry>>
}
impl Cache {
pub fn new() -> Cache {
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
Cache { domain_entries: BTreeMap::new() }
}
fn get_cache_state(&mut self, qname: &str, qtype: QueryType) -> CacheState {
@@ -290,21 +174,17 @@ impl Cache {
}
fn fill_queryresult(&mut self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>, increment_stats: bool) {
// 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 let Some(domain_entry) = self.domain_entries.get_mut(qname).and_then(Arc::get_mut) {
if increment_stats {
domain_entry.hits += 1
}
domain_entry.fill_queryresult(qname, qtype, result_vec);
domain_entry.fill_queryresult(qtype, result_vec);
}
}
pub fn lookup(&mut self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
// DNS is case-insensitive, so lowercase for cache lookup
let qname_lower = qname.to_lowercase();
match self.get_cache_state(&qname_lower, qtype) {
match self.get_cache_state(qname, qtype) {
CacheState::PositiveCache => {
let mut qr = DnsPacket::new();
self.fill_queryresult(qname, qtype, &mut qr.answers, true);
@@ -328,90 +208,38 @@ impl Cache {
Some(x) => x,
None => continue
};
// Store with a lowercase key for case-insensitive lookups
let domain_lower = domain.to_lowercase();
// 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);
if let Some(ref mut rs) = self.domain_entries.get_mut(&domain).and_then(Arc::get_mut) {
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;
}
// Insert new entry
let mut rs = DomainEntry::new(domain_lower.clone());
let mut rs = DomainEntry::new(domain.clone());
rs.store_record(rec);
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);
self.domain_entries.insert(domain.clone(), Arc::new(rs));
}
}
pub fn store_nxdomain(&mut self, qname: &str, qtype: QueryType, ttl: u32) {
// 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);
if let Some(ref mut rs) = self.domain_entries.get_mut(qname).and_then(Arc::get_mut) {
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;
}
// Insert new entry
let mut rs = DomainEntry::new(qname_lower.clone());
let mut rs = DomainEntry::new(qname.to_string());
rs.store_nxdomain(qtype, ttl);
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);
self.domain_entries.insert(qname.to_string(), Arc::new(rs));
}
}
#[derive(Default)]
pub struct SynchronizedCache {
pub cache: RwLock<Cache>
}
impl SynchronizedCache {
pub fn new() -> SynchronizedCache {
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())
SynchronizedCache { cache: RwLock::new(Cache::new()) }
}
pub fn list(&self) -> Result<Vec<Arc<DomainEntry>>> {
@@ -419,7 +247,7 @@ impl SynchronizedCache {
let mut list = Vec::new();
for (_, rs) in cache.domain_entries.iter() {
for rs in cache.domain_entries.values() {
list.push(rs.clone());
}
@@ -551,116 +379,4 @@ mod tests {
assert_eq!(1, cache.domain_entries.get(&"www.microsoft.com".to_string()).unwrap().updates);
assert_eq!(1, cache.domain_entries.get(&"www.microsoft.com".to_string()).unwrap().hits);
}
#[test]
fn test_memory_limited_cache() {
let mut cache = Cache::with_memory_limit(1); // 1MB limit
// Add many records until limit is hit
for i in 0..5000 {
let domain = format!("test{}.com", i);
let records = vec![DnsRecord::A {
domain: domain.clone(),
addr: "127.0.0.1".parse().unwrap(),
ttl: TransientTtl(3600)
}];
cache.store(&records);
}
// Verify memory stayed under limit (with some tolerance)
let limit_bytes = 1024 * 1024;
let tolerance_bytes = limit_bytes * 110 / 100; // 110% tolerance
assert!(
cache.current_memory_bytes <= tolerance_bytes,
"Cache memory {} bytes exceeds limit with tolerance {} bytes",
cache.current_memory_bytes, tolerance_bytes
);
// Verify cache still works and has been evicted
assert!(cache.domain_entries.len() < 5000, "Cache should have evicted entries");
assert!(cache.domain_entries.len() > 0, "Cache should not be empty");
// Most recent entries should still be present
assert!(cache.lookup("test4999.com", QueryType::A).is_some());
}
#[test]
fn test_unlimited_cache() {
let mut cache = Cache::with_memory_limit(0); // Unlimited
for i in 0..1000 {
let domain = format!("test{}.com", i);
let records = vec![DnsRecord::A {
domain: domain.clone(),
addr: "127.0.0.1".parse().unwrap(),
ttl: TransientTtl(3600)
}];
cache.store(&records);
}
// All entries should be present
assert_eq!(cache.domain_entries.len(), 1000);
assert_eq!(cache.max_memory_bytes, usize::MAX);
// Verify lookups work for all entries
assert!(cache.lookup("test0.com", QueryType::A).is_some());
assert!(cache.lookup("test500.com", QueryType::A).is_some());
assert!(cache.lookup("test999.com", QueryType::A).is_some());
}
#[test]
fn test_lru_eviction_order() {
let mut cache = Cache::with_memory_limit(1); // Small limit to trigger eviction
// Add initial batch of records
for i in 0..100 {
cache.store(&[DnsRecord::A {
domain: format!("domain{}.com", i),
addr: "127.0.0.1".parse().unwrap(),
ttl: TransientTtl(3600)
}]);
}
// Access domain50 to make it recently used
let _ = cache.lookup("domain50.com", QueryType::A);
// Add more records to trigger eviction
for i in 100..200 {
cache.store(&[DnsRecord::A {
domain: format!("domain{}.com", i),
addr: "127.0.0.1".parse().unwrap(),
ttl: TransientTtl(3600)
}]);
}
// Most recently added entries should be present
assert!(cache.lookup("domain199.com", QueryType::A).is_some());
// Verify cache is respecting memory limit
let limit_bytes = 1024 * 1024;
let tolerance_bytes = limit_bytes * 110 / 100;
assert!(cache.current_memory_bytes <= tolerance_bytes);
}
#[test]
fn test_nxdomain_memory_tracking() {
let mut cache = Cache::with_memory_limit(1); // 1MB limit
// Store many NXDOMAIN responses
for i in 0..1000 {
let domain = format!("nonexistent{}.com", i);
cache.store_nxdomain(&domain, QueryType::A, 3600);
}
// Verify memory tracking works for NXDOMAIN
assert!(cache.current_memory_bytes > 0);
assert!(cache.current_memory_bytes <= 1024 * 1024 * 110 / 100);
// Verify NXDOMAIN responses work
if let Some(packet) = cache.lookup("nonexistent999.com", QueryType::A) {
assert_eq!(ResultCode::NXDOMAIN, packet.header.rescode);
} else {
panic!("NXDOMAIN entry should be cached");
}
}
}
+122 -230
View File
@@ -4,18 +4,18 @@ use std::io::Write;
#[cfg(feature = "doh")]
use std::io::Read;
use std::marker::{Send, Sync};
use std::net::{Ipv4Addr, SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
use std::net::{SocketAddr, TcpStream, ToSocketAddrs, UdpSocket};
#[cfg(feature = "doh")]
use std::net::IpAddr;
#[cfg(feature = "doh")]
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool, AtomicU16};
use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool};
use std::sync::mpsc::{channel, Sender};
use std::sync::{Arc, Mutex};
#[cfg(feature = "doh")]
use std::sync::RwLock;
use std::thread::{sleep, Builder};
use std::time::Duration;
use std::time::Duration as SleepDuration;
use chrono::*;
use derive_more::{Display, Error, From};
@@ -32,18 +32,6 @@ use crate::dns::protocol::{DnsPacket, DnsQuestion, QueryType};
use crate::dns::protocol::DnsRecord;
#[cfg(feature = "doh")]
use lru::LruCache;
#[cfg(feature = "doh")]
use ureq::Agent;
#[cfg(feature = "doh")]
use ureq::config::Config;
#[cfg(feature = "doh")]
use ureq::http::Uri;
#[cfg(feature = "doh")]
use ureq::unversioned::resolver::{ArrayVec, ResolvedSocketAddrs, Resolver};
#[cfg(feature = "doh")]
use ureq::unversioned::transport::{DefaultConnector, NextTimeout};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Display, From, Error)]
pub enum ClientError {
@@ -77,10 +65,7 @@ pub struct DnsNetworkClient {
total_failed: AtomicUsize,
/// Counter for assigning packet ids
seq: AtomicU16,
/// Enable DNS 0x20 encoding for additional security
enable_0x20: bool,
seq: AtomicUsize,
/// The requesting socket for IPv4
socket_ipv4: UdpSocket,
@@ -101,8 +86,6 @@ pub struct DnsNetworkClient {
struct PendingQuery {
seq: u16,
timestamp: DateTime<Local>,
/// The query name with 0x20 encoding applied (for validation)
query_name: String,
tx: Sender<Option<DnsPacket>>
}
@@ -111,43 +94,18 @@ unsafe impl Send for DnsNetworkClient {}
unsafe impl Sync for DnsNetworkClient {}
impl 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");
pub fn new(port: u16) -> DnsNetworkClient {
DnsNetworkClient {
total_sent: AtomicUsize::new(0),
total_failed: AtomicUsize::new(0),
seq: AtomicU16::new(rand::random::<u16>()),
enable_0x20,
socket_ipv4,
socket_ipv6,
seq: AtomicUsize::new(0),
socket_ipv4: UdpSocket::bind(format!("0.0.0.0:{}", port)).expect("Error binding IPv4"),
socket_ipv6: UdpSocket::bind(format!("[::]:{}", port + 1)).expect("Error binding IPv6"),
pending_queries: Arc::new(Mutex::new(Vec::new())),
stopped: Arc::new(AtomicBool::new(false))
}
}
/// Apply DNS 0x20 encoding (random case) to domain name for additional entropy
/// This helps prevent cache poisoning by adding ~10-15 bits of entropy per query
fn apply_0x20_encoding(domain: &str) -> String {
domain.chars().map(|c| {
if c.is_ascii_alphabetic() {
// Randomly uppercase or lowercase each letter
if rand::random::<bool>() {
c.to_ascii_uppercase()
} else {
c.to_ascii_lowercase()
}
} else {
c
}
}).collect()
}
/// Send a DNS query using TCP transport
///
/// This is much simpler than using UDP, since the kernel will take care of
@@ -188,21 +146,14 @@ impl DnsNetworkClient {
/// Send a DNS query using UDP transport
///
/// This will construct a query packet and fire it off to the specified server.
/// This will construct a query packet, and fire it off to the specified server.
/// The query is sent from the callee thread, but responses are read on a
/// worker thread and returned to this thread through a channel. Thus, this
/// method is thread-safe and can be used from any number of threads in
/// worker thread, and returned to this thread through a channel. Thus this
/// method is thread safe, and can be used from any number of threads in
/// parallel.
pub fn send_udp_query<A: ToSocketAddrs>(&self, qname: &str, qtype: QueryType, server: A, recursive: bool, timeout: Duration) -> Result<DnsPacket> {
pub fn send_udp_query<A: ToSocketAddrs>(&self, qname: &str, qtype: QueryType, server: A, recursive: bool) -> Result<DnsPacket> {
let _ = self.total_sent.fetch_add(1, Ordering::Release);
// Apply DNS 0x20 encoding if enabled (random case for additional entropy)
let query_name = if self.enable_0x20 {
Self::apply_0x20_encoding(qname)
} else {
qname.to_string()
};
// Prepare request
let mut packet = DnsPacket::new();
@@ -214,19 +165,18 @@ impl DnsNetworkClient {
packet.header.questions = 1;
packet.header.recursion_desired = recursive;
packet.questions.push(DnsQuestion::new(query_name.clone(), qtype));
packet.questions.push(DnsQuestion::new(qname.to_string(), qtype));
// Create a return channel and add a `PendingQuery` to the list of lookups in progress
// Create a return channel, and add a `PendingQuery` to the list of lookups in progress
let (tx, rx) = channel();
{
let mut pending_queries = self.pending_queries.lock().map_err(|_| ClientError::PoisonedLock)?;
pending_queries.push(PendingQuery { seq: packet.header.id, timestamp: Local::now(), query_name, tx });
pending_queries.push(PendingQuery { seq: packet.header.id, timestamp: Local::now(), tx });
}
// Send a query
// Send query
let mut req_buffer = BytePacketBuffer::new();
let len = req_buffer.buf.len();
packet.write(&mut req_buffer, len)?;
packet.write(&mut req_buffer, 512)?;
let addr: SocketAddr = server.to_socket_addrs()?.next().expect("Wrong resolver address");
match addr {
SocketAddr::V4(addr) => {
@@ -237,8 +187,8 @@ impl DnsNetworkClient {
}
}
// Wait for response with timeout
match rx.recv_timeout(timeout) {
// Wait for response
match rx.recv() {
Ok(Some(qr)) => Ok(qr),
Ok(None) => {
let _ = self.total_failed.fetch_add(1, Ordering::Release);
@@ -246,7 +196,7 @@ impl DnsNetworkClient {
}
Err(_) => {
let _ = self.total_failed.fetch_add(1, Ordering::Release);
Err(ClientError::TimeOut)
Err(ClientError::LookupFailed)
}
}
}
@@ -264,17 +214,16 @@ impl DnsClient for DnsNetworkClient {
/// The run method launches a worker thread. Unless this thread is running, no
/// responses will ever be generated, and clients will just block indefinitely.
fn run(&self) -> Result<()> {
let timeout = Some(std::time::Duration::from_millis(500));
// Start the thread for handling incoming responses
{
let socket_copy = self.socket_ipv4.try_clone()?;
let _ = socket_copy.set_read_timeout(Some(Duration::from_millis(500)));
let _ = socket_copy.set_read_timeout(timeout);
let pending_queries_lock = self.pending_queries.clone();
let stopped = Arc::clone(&self.stopped);
let match_case = self.enable_0x20;
Builder::new()
.name("DnsNetworkClient-worker-thread-v4".into())
.name("DnsNetworkClient-worker-thread".into())
.spawn(move || {
loop {
if stopped.load(Ordering::SeqCst) {
@@ -283,83 +232,7 @@ impl DnsClient for DnsNetworkClient {
// Read data into a buffer
let mut res_buffer = BytePacketBuffer::new();
let recv_result = socket_copy.recv_from(&mut res_buffer.buf);
match recv_result {
Ok(_) => {}
Err(_) => {
continue;
}
}
// Construct a DnsPacket from buffer, skipping the packet if parsing failed
let packet = match DnsPacket::from_buffer(&mut res_buffer) {
Ok(packet) => packet,
Err(err) => {
println!("DnsNetworkClient failed to parse packet with error: {:?}", err);
continue;
}
};
// Acquire a lock on the pending_queries list and search for a
// matching PendingQuery to which to deliver the response.
if let Ok(mut pending_queries) = pending_queries_lock.lock() {
let mut matched_query = None;
for (i, pending_query) in pending_queries.iter().enumerate() {
if pending_query.seq == packet.header.id {
// Validate 0x20 encoding - response must match query case exactly
if !packet.questions.is_empty() {
let response_name = &packet.questions[0].name;
if (match_case && response_name != &pending_query.query_name)
|| (pending_query.query_name.to_lowercase() != response_name.to_lowercase()) {
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
pending_query.query_name, response_name);
continue;
}
}
// Matching query found with correct case, send the response
let _ = pending_query.tx.send(Some(packet.clone()));
// Mark this index for removal from list
matched_query = Some(i);
break;
}
}
if let Some(idx) = matched_query {
pending_queries.remove(idx);
} else {
trace!("Discarding unsolicited response for: {:?}", packet.questions.get(0));
}
}
}
})?;
}
// Start the same thread for IPv6
{
let socket_copy = self.socket_ipv6.try_clone()?;
let _ = socket_copy.set_read_timeout(Some(Duration::from_millis(500)));
let pending_queries_lock = self.pending_queries.clone();
let stopped = Arc::clone(&self.stopped);
let match_case = self.enable_0x20;
Builder::new()
.name("DnsNetworkClient-worker-thread-v6".into())
.spawn(move || {
loop {
if stopped.load(Ordering::SeqCst) {
break;
}
// Read data into a buffer
let mut res_buffer = BytePacketBuffer::new();
let recv_result = socket_copy.recv_from(&mut res_buffer.buf);
match recv_result {
match socket_copy.recv_from(&mut res_buffer.buf) {
Ok(_) => {}
Err(_) => {
continue;
@@ -381,18 +254,7 @@ impl DnsClient for DnsNetworkClient {
let mut matched_query = None;
for (i, pending_query) in pending_queries.iter().enumerate() {
if pending_query.seq == packet.header.id {
// 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
// Matching query found, send the response
let _ = pending_query.tx.send(Some(packet.clone()));
// Mark this index for removal from list
@@ -405,7 +267,66 @@ impl DnsClient for DnsNetworkClient {
if let Some(idx) = matched_query {
pending_queries.remove(idx);
} else {
trace!("Discarding unsolicited response for: {:?}", packet.questions.get(0));
println!("Discarding response for: {:?}", packet.questions[0]);
}
}
}
})?;
}
// Start the same thread for IPv6
{
let socket_copy = self.socket_ipv6.try_clone()?;
let _ = socket_copy.set_read_timeout(timeout);
let pending_queries_lock = self.pending_queries.clone();
let stopped = Arc::clone(&self.stopped);
Builder::new()
.name("DnsNetworkClient-worker-thread".into())
.spawn(move || {
loop {
if stopped.load(Ordering::SeqCst) {
break;
}
// Read data into a buffer
let mut res_buffer = BytePacketBuffer::new();
match socket_copy.recv_from(&mut res_buffer.buf) {
Ok(_) => {}
Err(_) => {
continue;
}
}
// Construct a DnsPacket from buffer, skipping the packet if parsing failed
let packet = match DnsPacket::from_buffer(&mut res_buffer) {
Ok(packet) => packet,
Err(err) => {
println!("DnsNetworkClient failed to parse packet with error: {:?}", err);
continue;
}
};
// Acquire a lock on the pending_queries list, and search for a
// matching PendingQuery to which to deliver the response.
if let Ok(mut pending_queries) = pending_queries_lock.lock() {
let mut matched_query = None;
for (i, pending_query) in pending_queries.iter().enumerate() {
if pending_query.seq == packet.header.id {
// Matching query found, send the response
let _ = pending_query.tx.send(Some(packet.clone()));
// Mark this index for removal from list
matched_query = Some(i);
break;
}
}
if let Some(idx) = matched_query {
pending_queries.remove(idx);
} else {
println!("Discarding response for: {:?}", packet.questions[0]);
}
}
}
@@ -420,6 +341,7 @@ impl DnsClient for DnsNetworkClient {
Builder::new()
.name("DnsNetworkClient-timeout-thread".into())
.spawn(move || {
let timeout = Duration::seconds(5);
loop {
if stopped.load(Ordering::SeqCst) {
break;
@@ -427,7 +349,7 @@ impl DnsClient for DnsNetworkClient {
if let Ok(mut pending_queries) = pending_queries_lock.lock() {
let mut finished_queries = Vec::new();
for (i, pending_query) in pending_queries.iter().enumerate() {
let expires = pending_query.timestamp + DEFAULT_TIMEOUT;
let expires = pending_query.timestamp + timeout;
if expires < Local::now() {
let _ = pending_query.tx.send(None);
finished_queries.push(i);
@@ -440,7 +362,7 @@ impl DnsClient for DnsNetworkClient {
}
}
sleep(Duration::from_millis(100));
sleep(SleepDuration::from_millis(100));
}
})?;
}
@@ -453,7 +375,7 @@ impl DnsClient for DnsNetworkClient {
}
fn send_query(&self, qname: &str, qtype: QueryType, server: &str, recursive: bool) -> Result<DnsPacket> {
let packet = self.send_udp_query(qname, qtype, server, recursive, DEFAULT_TIMEOUT)?;
let packet = self.send_udp_query(qname, qtype, server, recursive)?;
if !packet.header.truncated_message {
return Ok(packet);
}
@@ -465,9 +387,9 @@ impl DnsClient for DnsNetworkClient {
#[cfg(feature = "doh")]
pub struct HttpsDnsClient {
agent: Agent,
agent: ureq::Agent,
/// Counter for assigning packet ids
seq: AtomicU16,
seq: AtomicUsize,
}
#[cfg(feature = "doh")]
@@ -480,72 +402,41 @@ impl HttpsDnsClient {
.collect::<Vec<SocketAddr>>();
trace!("Using bootstraps: {:?}", &servers);
let agent_config = Agent::config_builder()
.user_agent(&client_name)
.timeout_global(Some(Duration::from_secs(5)))
.max_idle_connections_per_host(8)
.max_idle_connections(16)
.max_idle_age(Duration::from_secs(300))
.build();
let agent = Agent::with_parts(agent_config, DefaultConnector::default(), BootstrapResolver::new(servers));
Self { agent, seq: AtomicU16::new(rand::random::<u16>()) }
}
}
#[cfg(feature = "doh")]
#[derive(Debug)]
struct BootstrapResolver {
servers: Vec<SocketAddr>,
cache: RwLock<LruCache<String, Vec<SocketAddr>>>
}
#[cfg(feature = "doh")]
impl BootstrapResolver {
pub fn new(servers: Vec<SocketAddr>) -> Self {
let cache: LruCache<String, Vec<SocketAddr>> = LruCache::new(NonZeroUsize::new(10).unwrap());
let cache = RwLock::new(cache);
Self { servers, cache }
}
}
#[cfg(feature = "doh")]
impl Resolver for BootstrapResolver {
fn resolve(&self, uri: &Uri, _config: &Config, timeout: NextTimeout) -> std::result::Result<ResolvedSocketAddrs, ureq::Error> {
let domain = uri.host().unwrap_or("localhost");
let port = uri.port_u16().unwrap_or(443);
let addr = match domain.find(':') {
Some(index) => domain[0..index].to_string(),
None => domain.to_string()
let agent = ureq::AgentBuilder::new()
.user_agent(&client_name)
.timeout(std::time::Duration::from_secs(3))
.max_idle_connections_per_host(4)
.max_idle_connections(16)
.resolver(move |addr: &str| {
let addr = match addr.find(':') {
Some(index) => addr[0..index].to_string(),
None => addr.to_string()
};
let timeout_duration = Duration::from_millis(timeout.after.as_millis() as u64);
trace!("Resolving {}", addr);
if let Some(addrs) = self.cache.write().unwrap().get(&addr) {
if let Some(addrs) = cache.write().unwrap().get(&addr) {
trace!("Found bootstrap ip in cache");
let mut results: ResolvedSocketAddrs = ArrayVec::from_fn(|_| SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0));
for addr in addrs {
results.push(addr.to_owned());
}
return Ok(results);
return Ok(addrs.clone());
}
let mut dns_client = DnsNetworkClient::new();
let port = 10000 + (rand::random::<u16>() % 50000);
let mut dns_client = DnsNetworkClient::new(port);
dns_client.run().unwrap();
let mut result: Vec<IpAddr> = Vec::new();
let mut results: ResolvedSocketAddrs = ArrayVec::from_fn(|_| SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0));
for server in &self.servers {
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true, timeout_duration) {
for server in &servers {
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::A, server, true) {
for answer in &res.answers {
if let DnsRecord::A { addr, .. } = answer {
results.push(SocketAddr::new(IpAddr::V4(*addr), port));
result.push(IpAddr::V4(*addr))
}
}
}
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::AAAA, server, true, timeout_duration) {
if let Ok(res) = dns_client.send_udp_query(&addr, QueryType::AAAA, server, true) {
for answer in &res.answers {
if let DnsRecord::AAAA { addr, .. } = answer {
results.push(SocketAddr::new(IpAddr::V6(*addr), port));
result.push(IpAddr::V6(*addr))
}
}
@@ -557,11 +448,14 @@ impl Resolver for BootstrapResolver {
result.dedup();
let addrs = result
.into_iter()
.map(|ip| SocketAddr::new(ip, port))
.map(|ip| SocketAddr::new(ip, 443))
.collect::<Vec<_>>();
trace!("Resolved addresses: {:?}", &addrs);
self.cache.write().unwrap().put(addr, addrs.clone());
Ok(results)
cache.write().unwrap().put(addr, addrs.clone());
Ok(addrs)
})
.build();
Self { agent, seq: AtomicUsize::new(1) }
}
}
@@ -603,20 +497,20 @@ impl DnsClient for HttpsDnsClient {
let response = self.agent
.post(doh_url)
.header("Content-Type", "application/dns-message")
.send(req_buffer.buffer.as_slice());
.set("Content-Type", "application/dns-message")
.send_bytes(req_buffer.buffer.as_slice());
match response {
Ok(response) => {
match response.status().as_u16() {
match response.status() {
200 => {
match response.headers().get("Content-Length") {
match response.header("Content-Length") {
None => warn!("No 'Content-Length' header in DoH response!"),
Some(str) => {
match str.to_str().unwrap_or("0").parse::<usize>() {
match str.parse::<usize>() {
Ok(size) => {
let mut bytes: Vec<u8> = Vec::with_capacity(size);
response.into_body().into_reader()
response.into_reader()
.take(4096)
.read_to_end(&mut bytes)?;
let mut buffer = VectorPacketBuffer::new();
@@ -686,11 +580,10 @@ pub mod tests {
#[test]
pub fn test_udp_client() {
// Disable 0x20 for testing against public DNS servers that may not preserve case
let client = DnsNetworkClient::new_with_0x20(false);
let client = DnsNetworkClient::new(31456);
client.run().unwrap();
let res = client.send_udp_query("google.com", QueryType::A, ("8.8.8.8", 53), true, DEFAULT_TIMEOUT).unwrap();
let res = client.send_udp_query("google.com", QueryType::A, ("8.8.8.8", 53), true).unwrap();
assert_eq!(res.questions[0].name, "google.com");
assert!(res.answers.len() > 0);
@@ -705,8 +598,7 @@ pub mod tests {
#[test]
pub fn test_tcp_client() {
// Disable 0x20 for testing against public DNS servers
let client = DnsNetworkClient::new_with_0x20(false);
let client = DnsNetworkClient::new(31458);
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");
+8 -12
View File
@@ -5,7 +5,6 @@ use std::sync::Arc;
use derive_more::{Display, Error, From};
use crate::commons::rtt_tracker::RttTracker;
use crate::dns::authority::Authority;
use crate::dns::cache::SynchronizedCache;
use crate::dns::client::{DnsClient, DnsNetworkClient};
@@ -57,19 +56,18 @@ pub struct ServerContext {
pub enable_tcp: bool,
pub enable_api: bool,
pub statistics: ServerStatistics,
pub zones_dir: &'static str,
pub forwarder_tracker: Arc<RttTracker<String>>,
pub zones_dir: &'static str
}
impl Default for ServerContext {
fn default() -> Self {
ServerContext::new(String::from("0.0.0.0:53"), Vec::new(), true, 100)
ServerContext::new(String::from("0.0.0.0:53"), Vec::new())
}
}
impl ServerContext {
#[allow(unused_variables)]
pub fn new(dns_listen: String, bootstraps: Vec<String>, enable_0x20: bool, cache_limit_mb: usize) -> ServerContext {
pub fn new(dns_listen: String, bootstraps: Vec<String>) -> ServerContext {
#[cfg(not(feature = "doh"))]
let doh_client = None;
#[cfg(feature = "doh")]
@@ -77,9 +75,9 @@ impl ServerContext {
ServerContext {
authority: Authority::new(),
cache: SynchronizedCache::with_memory_limit(cache_limit_mb),
cache: SynchronizedCache::new(),
filters: Vec::new(),
old_client: Box::new(DnsNetworkClient::new_with_0x20(enable_0x20)),
old_client: Box::new(DnsNetworkClient::new(10000 + (rand::random::<u16>() % 50000))),
doh_client,
dns_listen,
api_port: 5380,
@@ -89,8 +87,7 @@ impl ServerContext {
enable_tcp: true,
enable_api: false,
statistics: ServerStatistics { tcp_query_count: AtomicUsize::new(0), udp_query_count: AtomicUsize::new(0) },
zones_dir: "zones",
forwarder_tracker: Arc::new(RttTracker::new()),
zones_dir: "zones"
}
}
@@ -132,7 +129,7 @@ pub mod tests {
pub fn create_test_context(callback: Box<StubCallback>) -> Arc<ServerContext> {
Arc::new(ServerContext {
authority: Authority::new(),
cache: SynchronizedCache::with_memory_limit(0), // Unlimited for tests
cache: SynchronizedCache::new(),
filters: Vec::new(),
old_client: Box::new(DnsStubClient::new(callback)),
doh_client: Some(Box::new(HttpsDnsClient::new(Vec::new()))),
@@ -144,8 +141,7 @@ pub mod tests {
enable_tcp: true,
enable_api: false,
statistics: ServerStatistics { tcp_query_count: AtomicUsize::new(0), udp_query_count: AtomicUsize::new(0) },
zones_dir: "zones",
forwarder_tracker: Arc::new(RttTracker::new()),
zones_dir: "zones"
})
}
}
+2 -2
View File
@@ -1,14 +1,14 @@
use crate::dns::protocol::{DnsPacket, QueryType};
pub trait DnsFilter {
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket>;
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket>;
}
pub struct DummyFilter {}
#[allow(unused_variables)]
impl DnsFilter for DummyFilter {
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket> {
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
None
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ impl HostsFilter {
}
impl DnsFilter for HostsFilter {
fn lookup(&self, qname: &str, qtype: QueryType, _recursive: bool) -> Option<DnsPacket> {
fn lookup(&self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
let mut packet = DnsPacket::new();
if let Some(list) = self.hosts.get(qname) {
for addr in list {
+6 -142
View File
@@ -38,7 +38,6 @@ pub enum QueryType {
SRV, // 33
OPT, // 41
TLSA, // 52
HTTPS, // 65
}
impl QueryType {
@@ -56,7 +55,6 @@ impl QueryType {
QueryType::SRV => 33,
QueryType::OPT => 41,
QueryType::TLSA => 52,
QueryType::HTTPS => 65,
}
}
@@ -73,7 +71,6 @@ impl QueryType {
33 => QueryType::SRV,
41 => QueryType::OPT,
52 => QueryType::TLSA,
65 => QueryType::HTTPS,
_ => QueryType::UNKNOWN(num),
}
}
@@ -175,48 +172,6 @@ pub enum DnsRecord {
data: Vec<u8>,
ttl: TransientTtl
}, // 52
HTTPS {
domain: String,
priority: u16,
target: String,
params: Vec<u8>,
ttl: TransientTtl
}, // 65
}
/// Read an uncompressed domain name (does not follow compression pointers)
/// Used for HTTPS/SVCB records per RFC 9460
fn read_uncompressed_name<T: PacketBuffer>(buffer: &mut T) -> Result<String> {
let mut outstr = String::new();
let mut delim = "";
loop {
let len = buffer.read()? as usize;
// Check for compression pointer (RFC 9460: HTTPS TargetName must be uncompressed)
// If we encounter one, this is an error - but we'll just stop
if (len & 0xC0) > 0 {
// This shouldn't happen for HTTPS records per RFC 9460
// Skip the second byte of the pointer
buffer.read()?;
break;
}
// Names are terminated by an empty label of length 0
if len == 0 {
break;
}
outstr.push_str(delim);
for _ in 0..len {
outstr.push(buffer.read()? as char);
}
delim = ".";
}
Ok(outstr)
}
impl DnsRecord {
@@ -312,17 +267,11 @@ impl DnsRecord {
}
QueryType::TXT => {
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();
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, seg_len)?));
buffer.step(seg_len)?;
}
}
txt.push_str(&String::from_utf8_lossy(buffer.get_range(cur_pos, data_len as usize)?));
buffer.step(data_len as usize)?;
Ok(DnsRecord::TXT { domain, data: txt, ttl: TransientTtl(ttl) })
}
@@ -345,34 +294,6 @@ impl DnsRecord {
buffer.step(data_len as usize)?;
Ok(DnsRecord::TLSA { domain, certificate_usage, selector, matching_type, data, ttl: TransientTtl(ttl) })
}
QueryType::HTTPS => {
// Track the start position of the data section
let data_start_pos = buffer.pos();
let priority = buffer.read_u16()?;
// Read TargetName without compression (RFC 9460 requirement)
let target = read_uncompressed_name(buffer)?;
// Calculate remaining bytes for SvcParams based on data_len
let bytes_consumed = buffer.pos() - data_start_pos;
let params_len = if data_len as usize > bytes_consumed {
data_len as usize - bytes_consumed
} else {
0
};
let params = if params_len > 0 {
let cur_pos = buffer.pos();
let p = buffer.get_range(cur_pos, params_len)?.to_vec();
buffer.step(params_len)?;
p
} else {
Vec::new()
};
Ok(DnsRecord::HTTPS { domain, priority, target, params, ttl: TransientTtl(ttl) })
}
QueryType::UNKNOWN(_) => {
buffer.step(data_len as usize)?;
@@ -531,38 +452,6 @@ impl DnsRecord {
buffer.write_u8(*b)?;
}
}
DnsRecord::HTTPS { ref domain, priority, ref target, ref params, ttl: TransientTtl(ttl) } => {
buffer.write_qname(domain)?;
buffer.write_u16(QueryType::HTTPS.to_num())?;
buffer.write_u16(1)?;
buffer.write_u32(ttl)?;
let pos = buffer.pos();
buffer.write_u16(0)?;
buffer.write_u16(priority)?;
// Write TargetName WITHOUT compression (RFC 9460 requirement)
let split_str = target.split('.').collect::<Vec<&str>>();
for label in split_str.iter() {
if label.is_empty() {
continue;
}
let len = label.len();
buffer.write_u8(len as u8)?;
for b in label.as_bytes() {
buffer.write_u8(*b)?;
}
}
buffer.write_u8(0)?; // Terminate with null label
for b in params {
buffer.write_u8(*b)?;
}
let size = buffer.pos() - (pos + 2);
buffer.set_u16(pos, size as u16)?;
}
DnsRecord::OPT { packet_len, flags, ref data } => {
buffer.write_u8(0)?;
buffer.write_u16(QueryType::OPT.to_num())?;
@@ -596,7 +485,6 @@ impl DnsRecord {
DnsRecord::TXT { .. } => QueryType::TXT,
DnsRecord::OPT { .. } => QueryType::OPT,
DnsRecord::TLSA { .. } => QueryType::TLSA,
DnsRecord::HTTPS { .. } => QueryType::HTTPS,
}
}
@@ -612,30 +500,11 @@ impl DnsRecord {
| DnsRecord::UNKNOWN { ref domain, .. }
| DnsRecord::SOA { ref domain, .. }
| DnsRecord::TXT { ref domain, .. }
| DnsRecord::TLSA { ref domain, .. }
| DnsRecord::HTTPS { ref domain, .. } => Some(domain.clone()),
| DnsRecord::TLSA { ref domain, .. } => Some(domain.clone()),
DnsRecord::OPT { .. } => None
}
}
pub fn set_domain(&mut self, new_domain: String) {
match self {
DnsRecord::A { ref mut domain, .. }
| DnsRecord::AAAA { ref mut domain, .. }
| DnsRecord::NS { ref mut domain, .. }
| DnsRecord::CNAME { ref mut domain, .. }
| DnsRecord::SRV { ref mut domain, .. }
| DnsRecord::PTR { ref mut domain, .. }
| DnsRecord::MX { ref mut domain, .. }
| DnsRecord::UNKNOWN { ref mut domain, .. }
| DnsRecord::SOA { ref mut domain, .. }
| DnsRecord::TXT { ref mut domain, .. }
| DnsRecord::TLSA { ref mut domain, .. }
| DnsRecord::HTTPS { ref mut domain, .. } => *domain = new_domain,
DnsRecord::OPT { .. } => {} // OPT records don't have a domain field
}
}
pub fn get_data(&self) -> Option<String> {
match *self {
DnsRecord::A { ref addr, .. } => Some(addr.to_string()),
@@ -657,10 +526,6 @@ impl DnsRecord {
let data = crate::commons::to_hex(data);
Some(format!("{} {} {} {} {}", domain, certificate_usage, selector, matching_type, &data))
},
DnsRecord::HTTPS { ref target, priority, ref params, .. } => {
let params_hex = crate::commons::to_hex(params);
Some(format!("{} {} {}", priority, target, params_hex))
},
DnsRecord::OPT { .. } => None,
}
}
@@ -676,9 +541,8 @@ impl DnsRecord {
| DnsRecord::MX { ttl: TransientTtl(ttl), .. }
| DnsRecord::UNKNOWN { ttl: TransientTtl(ttl), .. }
| DnsRecord::SOA { ttl: TransientTtl(ttl), .. }
| DnsRecord::TXT { ttl: TransientTtl(ttl), .. }
| DnsRecord::TLSA { ttl: TransientTtl(ttl), .. }
| DnsRecord::HTTPS { ttl: TransientTtl(ttl), .. } => ttl,
| DnsRecord::TXT { ttl: TransientTtl(ttl), .. } => ttl,
| DnsRecord::TLSA { ttl: TransientTtl(ttl), .. } => ttl,
DnsRecord::OPT { .. } => 0
}
}
+28 -89
View File
@@ -2,10 +2,10 @@
//! incoming queries
use std::sync::Arc;
use std::time::Instant;
use std::vec::Vec;
use derive_more::{Display, Error, From};
use rand::seq::IteratorRandom;
use crate::dns::context::ServerContext;
use crate::dns::protocol::{DnsPacket, QueryType, ResultCode};
@@ -53,7 +53,7 @@ pub trait DnsResolver {
}
for filter in context.filters.iter() {
if let Some(packet) = filter.lookup(qname, qtype, recursive) {
if let Some(packet) = filter.lookup(qname, qtype) {
context.cache.store(&packet.answers)?;
return Ok(packet);
}
@@ -85,51 +85,27 @@ impl DnsResolver for ForwardingDnsResolver {
}
fn perform(&mut self, qname: &str, qtype: QueryType) -> Result<DnsPacket> {
if let Some(packet) = self.context.cache.lookup(qname, qtype) {
return Ok(packet);
}
let ordered = self.context.forwarder_tracker.select_ordered(&self.upstreams);
let mut last_err = ResolveError::NoServerFound;
for upstream in &ordered {
let start = Instant::now();
let query_result = if is_url(upstream) {
let mut random = rand::thread_rng();
let upstream = self.upstreams.iter().choose(&mut random).unwrap();
let result = match self.context.cache.lookup(qname, qtype) {
None => {
if is_url(upstream) {
if let Some(client) = &self.context.doh_client {
client.send_query(qname, qtype, upstream, true)
client.send_query(qname, qtype, upstream, true)?
} else {
log::error!("This build doesn't support DoH");
continue;
return Err(ResolveError::NoServerFound);
}
} 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)?;
// 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)
Ok(result)
}
}
@@ -193,18 +169,7 @@ impl DnsResolver for RecursiveDnsResolver {
let _ = self.context.cache.store(&response.answers);
let _ = self.context.cache.store(&response.authorities);
let _ = self.context.cache.store(&response.resources);
// 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);
return Ok(response);
}
if response.header.rescode == ResultCode::NXDOMAIN {
@@ -229,19 +194,7 @@ impl DnsResolver for RecursiveDnsResolver {
// If not, we'll have to resolve the ip of a NS record
let new_ns_name = match response.get_unresolved_ns(qname) {
Some(x) => x,
None => {
// 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);
}
None => return Ok(response)
};
// Recursively resolve the NS
@@ -251,17 +204,7 @@ impl DnsResolver for RecursiveDnsResolver {
if let Some(new_ns) = recursive_response.get_random_a() {
ns = new_ns.clone();
} else {
// 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);
return Ok(response);
}
}
}
@@ -300,7 +243,7 @@ mod tests {
}));
match Arc::get_mut(&mut context) {
Some(ctx) => {
Some(mut ctx) => {
ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
}
None => panic!()
@@ -308,7 +251,7 @@ mod tests {
let mut resolver = context.create_resolver(Arc::clone(&context));
// First, verify that we get a match back
// First verify that we get a match back
{
let res = match resolver.resolve("google.com", QueryType::A, true) {
Ok(x) => x,
@@ -325,7 +268,7 @@ mod tests {
}
};
// Do the same lookup again and verify that it's present in the cache
// Do the same lookup again, and verify that it's present in the cache
// and that the counter has been updated
{
let res = match resolver.resolve("google.com", QueryType::A, true) {
@@ -605,23 +548,19 @@ mod tests {
assert_eq!(3, list.len());
// Find entries by domain name (LRU order may vary)
let google_entry = list.iter().find(|e| e.domain == "google.com").expect("google.com entry");
let ns1_entry = list.iter().find(|e| e.domain == "ns1.google.com").expect("ns1.google.com entry");
let foobar_entry = list.iter().find(|e| e.domain == "foobar.google.com").expect("foobar.google.com NXDOMAIN entry");
// Check statistics for google entry
assert_eq!("google.com", list[1].domain);
// google.com should have a NS record and an A record for a total of 2 record types
assert_eq!(2, google_entry.record_types.len());
// Should have a NS record and an A record for a total of 2 record types
assert_eq!(2, list[1].record_types.len());
// Should have been hit two times for NS google.com and once for
// A google.com
assert_eq!(3, google_entry.hits);
assert_eq!(3, list[1].hits);
assert_eq!(1, ns1_entry.record_types.len());
assert_eq!(2, ns1_entry.hits);
// foobar.google.com should be a cached NXDOMAIN with 0 hits
assert_eq!(0, foobar_entry.hits);
assert_eq!("ns1.google.com", list[2].domain);
assert_eq!(1, list[2].record_types.len());
assert_eq!(2, list[2].hits);
};
}
}
+6 -10
View File
@@ -107,7 +107,7 @@ pub fn execute_query(context: Arc<ServerContext>, request: &DnsPacket) -> DnsPac
let question = &request.questions[0];
packet.questions.push(question.clone());
debug!("Resolving: {}, type {:?}", &question.name, &question.qtype);
log::trace!("Resolving: {}, type {:?}", &question.name, &question.qtype);
let mut resolver = context.create_resolver(Arc::clone(&context));
let res_code = match resolver.resolve(&question.name, question.qtype, request.header.recursion_desired) {
@@ -246,10 +246,6 @@ impl DnsServer for DnsUdpServer {
debug!("UDP service loop has finished");
break;
}
if code == 10054 {
// Ignore
continue;
}
}
debug!("Failed to read from UDP socket: {:?}", err);
continue;
@@ -445,7 +441,7 @@ mod tests {
}));
match Arc::get_mut(&mut context) {
Some(ctx) => {
Some(mut ctx) => {
ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
}
None => panic!()
@@ -464,7 +460,7 @@ mod tests {
}
};
// A successful resolve that also resolves a CNAME without recursive lookup
// A successful resolve, that also resolves a CNAME without recursive lookup
{
let res = execute_query(Arc::clone(&context), &build_query("www.facebook.com", QueryType::CNAME));
assert_eq!(2, res.answers.len());
@@ -484,7 +480,7 @@ mod tests {
}
};
// A successful resolve that also resolves a CNAME through recursive lookup
// A successful resolve, that also resolves a CNAME through recursive lookup
{
let res = execute_query(Arc::clone(&context), &build_query("www.microsoft.com", QueryType::CNAME));
dbg!(&res);
@@ -507,7 +503,7 @@ mod tests {
// Disable recursive resolves to generate a failure
match Arc::get_mut(&mut context) {
Some(ctx) => {
Some(mut ctx) => {
ctx.allow_recursive = false;
}
None => panic!()
@@ -535,7 +531,7 @@ mod tests {
}));
match Arc::get_mut(&mut context2) {
Some(ctx) => {
Some(mut ctx) => {
ctx.resolve_strategy = ResolveStrategy::Forward { upstreams: vec![String::from("127.0.0.1:53")] };
}
None => panic!()
+2 -7
View File
@@ -33,14 +33,9 @@ pub fn start_dns_server(context: &Arc<Mutex<Context>>, settings: &Settings) -> b
result
}
/// Creates DNS-context with all necessary settings
/// Creates DNS-context with all needed settings
fn create_server_context(context: Arc<Mutex<Context>>, settings: &Settings) -> Arc<ServerContext> {
let mut server_context = ServerContext::new(
settings.dns.listen.clone(),
settings.dns.bootstraps.clone(),
settings.dns.enable_0x20,
settings.dns.cache_memory_limit_mb
);
let mut server_context = ServerContext::new(settings.dns.listen.clone(), settings.dns.bootstraps.clone());
server_context.allow_recursive = true;
server_context.resolve_strategy = match settings.dns.forwarders.is_empty() {
true => ResolveStrategy::Recursive,
+14 -14
View File
@@ -29,8 +29,9 @@ use alfis::{dns_utils, Block, Bytes, Chain, Context, Keystore, Miner, Network, S
#[cfg(windows)]
use crate::win_service::start_service;
#[cfg(feature = "webgui")]
mod web_ui;
// #[cfg(feature = "webgui")]
// mod web_ui;
mod ui;
#[cfg(windows)]
mod win_service;
@@ -59,7 +60,6 @@ fn main() {
opts.optflag("v", "version", "Print version and exit");
opts.optflag("d", "debug", "Show debug messages, more than usual");
opts.optflag("t", "trace", "Show trace messages, more than debug");
opts.optflag("", "hide", "Hide UI, show only tray icon.");
opts.optflag("b", "blocks", "List blocks from DB and exit");
opts.optflag("g", "generate", "Generate new config file. Generated config will be printed to console.");
#[cfg(windows)]
@@ -143,7 +143,7 @@ fn main() {
};
let mut no_gui = opt_matches.opt_present("n");
if !cfg!(feature = "webgui")
if !cfg!(feature = "gui")
{
no_gui = true;
}
@@ -251,8 +251,9 @@ fn main() {
post(Event::Error { text: String::from("Error starting DNS-server. Please, check that it&rsquo;s port is not busy.") });
});
}
#[cfg(feature = "webgui")]
web_ui::run_interface(Arc::clone(&context), miner, opt_matches.opt_present("hide"));
// #[cfg(feature = "webgui")]
// web_ui::run_interface(Arc::clone(&context), miner);
ui::run_ui(Arc::clone(&context), miner);
}
// Without explicitly detaching the console cmd won't redraw it's prompt.
@@ -298,12 +299,6 @@ fn load_keys(settings: &Settings) -> Vec<Keystore> {
}
pub fn start_services(settings: &Settings, context: &Arc<Mutex<Context>>) -> (bool, Arc<Mutex<Miner>>, JoinHandle<()>) {
let dns_server_ok = if settings.dns.threads > 0 {
dns_utils::start_dns_server(&context, &settings)
} else {
true
};
if let Ok(mut context) = context.lock() {
context.chain.check_chain(settings.check_blocks);
match context.chain.get_block(1) {
@@ -316,6 +311,12 @@ pub fn start_services(settings: &Settings, context: &Arc<Mutex<Context>>) -> (bo
}
}
let dns_server_ok = if settings.dns.threads > 0 {
dns_utils::start_dns_server(&context, &settings)
} else {
true
};
let mut miner_obj = Miner::new(Arc::clone(&context));
miner_obj.start_mining_thread();
let miner: Arc<Mutex<Miner>> = Arc::new(Mutex::new(miner_obj));
@@ -340,9 +341,8 @@ fn setup_logger(opt_matches: &Matches, console_attached: bool) {
}
let mut builder = ConfigBuilder::new();
let config = builder.add_filter_ignore_str("mio::poll")
.add_filter_ignore_str("rustls::")
.add_filter_ignore_str("rustls::client")
.add_filter_ignore_str("ureq::")
.add_filter_ignore_str("ureq_proto::")
.set_thread_level(LevelFilter::Error)
.set_location_level(LevelFilter::Off)
.set_target_level(LevelFilter::Error)
+8 -130
View File
@@ -29,22 +29,7 @@ impl Settings {
Ok(mut file) => {
let mut text = String::new();
file.read_to_string(&mut text).unwrap();
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);
}
}
if let Ok(settings) = toml::from_str(&text) {
return Some(settings);
}
None
@@ -53,15 +38,6 @@ impl Settings {
}
}
fn save_migration(filename: &str, original_text: &str) -> Result<(), std::io::Error> {
use std::io::Write;
// Simple text replacement preserves all comments and formatting
let migrated_text = original_text.replace(":42440", ":4244");
let mut file = File::create(filename)?;
file.write_all(migrated_text.as_bytes())?;
Ok(())
}
pub fn get_origin(&self) -> Bytes {
if self.origin.eq("") {
return Bytes::zero32();
@@ -94,25 +70,17 @@ pub struct Dns {
#[serde(default = "default_dns_bootstraps")]
pub bootstraps: Vec<String>,
#[serde(default)]
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
pub hosts: Vec<String>
}
impl Default for Dns {
fn default() -> Self {
Dns {
listen: default_listen_dns(),
threads: 10,
listen: String::from("127.0.0.1:53"),
threads: 20,
forwarders: vec![String::from("94.140.14.14:53"), String::from("94.140.15.15:53")],
bootstraps: default_dns_bootstraps(),
hosts: Vec::new(),
enable_0x20: default_dns_0x20(),
cache_memory_limit_mb: default_cache_memory_limit_mb()
hosts: Vec::new()
}
}
}
@@ -140,7 +108,7 @@ pub struct Net {
impl Default for Net {
fn default() -> Self {
Net {
peers: vec![String::from("peer-v4.alfis.name:4244"), String::from("peer-v6.alfis.name:4244")],
peers: vec![String::from("test-ip4.alfis.name:4244"), String::from("test-ip6.alfis.name:4244")],
listen: String::from("[::]:4244"),
public: true,
yggdrasil_only: false
@@ -153,11 +121,11 @@ fn default_listen() -> String {
}
fn default_listen_dns() -> String {
String::from("127.0.0.3:53")
String::from("0.0.0.0:53")
}
fn default_threads() -> usize {
10
100
}
fn default_check_blocks() -> u64 {
@@ -177,93 +145,3 @@ fn default_key_files() -> Vec<String> {
fn default_dns_bootstraps() -> Vec<String> {
vec![String::from("9.9.9.9:53"), String::from("94.140.14.14:53")]
}
fn default_dns_0x20() -> bool {
true
}
fn default_cache_memory_limit_mb() -> usize {
100 // 100 MB default
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_port_migration_for_public_nodes() {
// Create a temporary config file with incorrect port
let test_file = "test_migration_unit.toml";
let config_content = r#"
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
key_files = ["key1.toml"]
[net]
# Comment should be preserved
listen = "[::]:42440"
public = true
"#;
// Write test config
let mut file = File::create(test_file).unwrap();
file.write_all(config_content.as_bytes()).unwrap();
drop(file);
// Load the config (should trigger migration)
let settings = Settings::load(test_file).unwrap();
// Verify the setting in memory is correct
assert_eq!(settings.net.listen, "[::]:4244");
// Read the file to verify it was actually modified
let mut file = File::open(test_file).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
// Verify file was migrated
assert!(content.contains(":4244"));
assert!(!content.contains(":42440"));
// Verify comment was preserved
assert!(content.contains("# Comment should be preserved"));
// Cleanup
std::fs::remove_file(test_file).ok();
}
#[test]
fn test_no_migration_for_private_nodes() {
// Create a temporary config file with incorrect port but public = false
let test_file = "test_no_migration_unit.toml";
let config_content = r#"
origin = "0000001D2A77D63477172678502E51DE7F346061FF7EB188A2445ECA3FC0780E"
key_files = ["key1.toml"]
[net]
listen = "[::]:42440"
public = false
"#;
// Write test config
let mut file = File::create(test_file).unwrap();
file.write_all(config_content.as_bytes()).unwrap();
drop(file);
// Load the config (should NOT trigger migration because public = false)
let settings = Settings::load(test_file).unwrap();
// Verify the setting remains unchanged
assert_eq!(settings.net.listen, "[::]:42440");
// Read the file to verify it was NOT modified
let mut file = File::open(test_file).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
// Verify file was NOT migrated (still has 42440)
assert!(content.contains(":42440"));
// Cleanup
std::fs::remove_file(test_file).ok();
}
}
+107
View File
@@ -0,0 +1,107 @@
use std::{
sync::Arc,
rc::Rc, cell::RefCell
};
use alfis::Keystore;
use crate::{Mutex, Context, ui::KeyData};
use log::{error, info, warn};
use gtk::{
prelude::*,
ApplicationWindow,
Button,
Label,
Box,
Orientation,
Entry,
FileDialog
};
pub fn menu(window: &ApplicationWindow, parent: &gtk::Box, context: &Arc<Mutex<Context>>) {
let key_data = Rc::new(RefCell::from(KeyData{hash: String::new(), path: String::new(), public: String::new()}));
let keys = Box::new(Orientation::Horizontal, 8);
keys.set_hexpand(true);
parent.append(&keys);
let key_hash = Entry::builder()
.placeholder_text("No key loaded")
.editable(false)
.tooltip_text("If you load or mine a keypair the public key will be displayed here")
.hexpand(true)
.focusable(false)
.build();
keys.append(&key_hash);
let load_btn = Button::with_label("Load key");
let window = window.clone();
let context = context.clone();
load_btn.connect_clicked(move |_| {
open_file(&window, &key_hash, &context, key_data.clone());
});
keys.append(&load_btn);
let save_btn = Button::builder()
.label("Save key")
.sensitive(false)
.build();
keys.append(&save_btn);
let mine_btn = Button::with_label("Mine new key");
mine_btn.add_css_class("suggested-action");
keys.append(&mine_btn);
parent.append(&Label::new(Some("To mine domains you need a strong pair of signing keys and a pair of ecryption keys.")));
}
fn open_file(window: &ApplicationWindow, key_hash: &Entry ,context: &Arc<Mutex<Context>>, keydata: Rc<RefCell<KeyData>>) {
let filter = gtk::FileFilter::new();
filter.add_pattern("*.key");
filter.add_pattern("*.toml");
filter.set_name(Some("Key files"));
let fc = FileDialog::builder()
.title("Select key")
.default_filter(&filter)
.build();
let context = context.clone();
let key_hash = key_hash.clone();
fc.open(Some(window), None::<gtk::gio::Cancellable>.as_ref(), move |result| {
match result {
Ok(file) => {
match Keystore::from_file(file.path().unwrap().to_str().unwrap(), "") {
None => {
error!("Error loading keystore '{}'!", &file.path().unwrap().to_str().unwrap());
// show_warning(web_view, "Error loading key!<br>Key cannot be loaded or its difficulty is not enough."); # TODO: display warnings
// 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();
let _ = &key_hash.set_text(public.as_str());
let key_data = KeyData { path, public, hash };
*keydata.borrow_mut() = key_data;
if !context.lock().unwrap().select_key_by_public(&keystore.get_public()) {
context.lock().unwrap().add_keystore(keystore);
} else {
warn!("This key is already loaded!");
}
}
}
}
Err(e) => {
if !e.matches(gtk::gio::IOErrorEnum::Cancelled) {
error!("Error selecting file: {}", e);
}
}
}
});
}
+242
View File
@@ -0,0 +1,242 @@
use std::{
sync::{Arc, MutexGuard},
rc::Rc, cell::RefCell
};
use alfis::{
blockchain::{
types::MineResult,
transaction::DomainData
},
crypto::CryptoBox,
Keystore,
Block,
Bytes,
Transaction,
from_hex,
is_yggdrasil_record,
CLASS_DOMAIN,
DOMAIN_LIFETIME,
DOMAIN_DIFFICULTY,
MAX_RECORDS
};
use crate::{Mutex, Context, Miner, ui::KeyData};
use log::{debug, error, info, warn};
use chrono::Utc;
use gtk::{
prelude::*,
AlertDialog,
ApplicationWindow,
Box,
Button,
Entry,
FileDialog,
Label,
Orientation,
Overlay,
Popover
};
struct Domain<'a> {
domain: &'a str,
timestamp: u32,
d: &'a str,
}
pub fn menu(app: &adw::Application, window: &ApplicationWindow, parent: &gtk::Box, context: &Arc<Mutex<Context>>, miner: &Arc<Mutex<Miner>>) {
// let new_domain_btn = Button::with_label("New domain");
let new_domain_btn = Button::from_icon_name("list-add");
new_domain_btn.add_css_class("suggested-action");
new_domain_btn.set_halign(gtk::Align::End);
new_domain_btn.set_hexpand(false);
parent.append(&new_domain_btn);
// let new_domain_child = Box::new(Orientation::Vertical, 16);
// let new_domain_desc = Box::new(Orientation::Horizontal, 8);
// new_domain_child.append(&new_domain_desc);
// let domain_entry = Entry::builder()
// .placeholder_text("example")
// .hexpand(true)
// .build();
// new_domain_desc.append(&domain_entry);
// let new_domain_pop = Popover::builder()
// .autohide(true)
// .child(&new_domain_child)
// .build();
// parent.append(&new_domain_pop);
// let _ = new_domain_btn.connect_clicked(move |_| { new_domain_pop.popup(); });
// let win = ApplicationWindow::builder()
// .application(app)
// .title("Мое модальное окно")
// .default_width(400)
// .default_height(300)
// .modal(true) // Это делает окно модальным
// .build();
// win.set_child(Some(&Label::new(Some("Meow"))));
// // Устанавливаем родительское окно (допустим, `parent_window` - это ссылка на главное окно)
// parent.append(&win);
// // let tmp_modal = AlertDialog::builder()
// // .message("This is the test message")
// // .modal(true)
// // .build();
// let tmp_layout = Overlay::builder()
// .child(&Label::new(Some("This is the test message")))
// .build();
// parent.append(&tmp_layout);
// // tmp_modal.show(Some(window));
}
fn load_domains(context: &mut MutexGuard<Context>) {
// let _ = handle.dispatch(move |web_view|{
// web_view.eval("clearMyDomains();")
// });
let domains = context.chain.get_my_domains(context.get_keystore());
let mut domains = domains.iter().map(|(_, d)| d).collect::<Vec<_>>();
domains.sort_by(|a, b| a.0.cmp(&b.0));
for (domain, timestamp, data) in domains {
let d = serde_json::to_string(&data).unwrap();
let d = d.replace("'", "\\'").replace("\\n", "\\\\n").replace("\"", "\\\"");
let command = format!("addMyDomain('{}', {}, {}, '{}');", &domain, timestamp, timestamp + DOMAIN_LIFETIME, &d);
// let _ = handle.dispatch(move |web_view|{
// web_view.eval(&command)
// });
}
// let _ = handle.dispatch(move |web_view|{
// web_view.eval("refreshMyDomains();")
// });
}
fn action_create_domain(
context: Arc<Mutex<Context>>,
miner: Arc<Mutex<Miner>>,
name: String,
data: String,
signing: String,
encryption: String,
renewal: bool
) {
debug!("Creating domain with data: {}", &data);
let c = Arc::clone(&context);
let context = context.lock().unwrap();
if !context.has_keys() {
// show_warning(web_view, "You don't have keys loaded!<br>Load or mine the keys and try again.");
// let _ = web_view.eval("domainMiningUnavailable();");
return;
}
if context.chain.is_waiting_signers() {
// show_warning(web_view, "Waiting for last full block to be signed. Try again later.");
// let _ = web_view.eval("domainMiningUnavailable();");
info!("Waiting for last full block to be signed. Try again later.");
return;
}
let keystore = context.get_keystore().unwrap().clone();
let pub_key = keystore.get_public();
let data = match serde_json::from_str::<DomainData>(&data) {
Ok(data) => data,
Err(e) => {
// show_warning(web_view, "Something wrong with domain data. I cannot mine it.");
// let _ = web_view.eval("domainMiningUnavailable();");
warn!("Error parsing data: {}", e);
return;
}
};
info!("Parsed domain data:\n{:#?}", &data);
if data.records.len() > MAX_RECORDS {
// show_warning(web_view, "Too many records. Mining more than 30 records not allowed.");
// let _ = web_view.eval("domainMiningUnavailable();");
return;
}
// Check if yggdrasil only quality of zone is not violated
let zones = context.chain.get_zones();
for z in zones {
if z.name == data.zone && z.yggdrasil {
for record in &data.records {
if !is_yggdrasil_record(record) {
// show_warning(web_view, &format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone));
// let _ = web_view.eval("domainMiningUnavailable();");
return;
}
}
}
}
let (signing, encryption) = if signing.is_empty() || encryption.is_empty() {
(keystore.get_public(), keystore.get_encryption_public())
} else {
(Bytes::new(from_hex(&signing).unwrap()), Bytes::new(from_hex(&encryption).unwrap()))
};
match context.chain.can_mine_domain(context.chain.get_height(), &name, &pub_key) {
MineResult::Fine => {
drop(context);
create_domain(c, miner, CLASS_DOMAIN, &name, data, DOMAIN_DIFFICULTY, &keystore, signing, encryption, renewal);
// let _ = web_view.eval("domainMiningStarted();");
// event_info(web_view, &format!("Mining of domain \\'{}\\' has started", &name));
}
MineResult::WrongName => {
// show_warning(web_view, "You can't mine this domain!");
// let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::WrongData => {
// show_warning(web_view, "You have an error in records!");
// let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::WrongKey => {
// show_warning(web_view, "You can't mine with current key!");
// let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::WrongZone => {
// show_warning(web_view, "You can't mine domain in this zone!");
// let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::NotOwned => {
// show_warning(web_view, "This domain is already taken, and it is not yours!");
// let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::Cooldown { time } => {
// event_info(web_view, &format!("You have cooldown {}!", format_cooldown(time)));
// show_warning(web_view, &format!("You have cooldown {}!", format_cooldown(time)));
// let _ = web_view.eval("domainMiningUnavailable();");
}
}
}
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 encrypted = CryptoBox::encrypt(encryption.as_slice(), name.as_bytes()).expect("Error encrypting domain name!");
data.encrypted = Bytes::from_bytes(&encrypted);
let data = serde_json::to_string(&data).unwrap();
let (signing, encryption) = if signing.is_empty() || encryption.is_empty() {
(keystore.get_public(), keystore.get_encryption_public())
} else {
(signing, encryption)
};
let transaction = Transaction::from_str(name, class.to_owned(), data, signing, encryption);
// If this domain is already in blockchain we approve slightly smaller difficulty
let height = context.lock().unwrap().chain.get_height();
let discount = context.lock().unwrap().chain.get_identity_discount(&transaction.identity, renewal, height, Utc::now().timestamp());
let block = Block::new(Some(transaction), keystore.get_public(), Bytes::default(), difficulty - discount);
miner.lock().unwrap().add_block(block, keystore.clone());
}
+84
View File
@@ -0,0 +1,84 @@
#[warn(unused_imports)]
mod credentails;
mod domains;
use alfis::{
blockchain::{
transaction::DomainData,
types::MineResult
},
commons::*,
crypto::CryptoBox,
dns::protocol::DnsRecord,
event::Event,
eventbus::{post, register},
miner::Miner,
keystore, Block, Bytes, Context, Keystore, Transaction
};
use std::{
sync::{Arc, Mutex, MutexGuard},
thread,
time::{Duration, Instant},
rc::Rc, cell::RefCell
};
use gtk::prelude::*;
use gtk::{
glib,
ApplicationWindow,
Button,
Label,
HeaderBar,
Box,
Orientation,
Notebook,
Entry,
FileDialog
};
#[derive(Clone, Debug)]
pub struct KeyData {
pub path: String,
pub public: String,
pub hash: String,
}
#[warn(unused_variables)]
pub fn run_ui(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>) -> glib::ExitCode {
let application = adw::Application::builder()
.application_id("ru.Revertron.Alfis")
.build();
let context = context.clone();
let miner = miner.clone();
application.connect_activate(move |app| {
let window = ApplicationWindow::builder()
.application(app)
.title(format!("ALFIS {}", env!("CARGO_PKG_VERSION")))
.default_width(1023)
.default_height(720)
.resizable(false)
.build();
let ab = HeaderBar::new();
window.set_titlebar(Some(&ab));
let nb = Notebook::new();
window.set_child(Some(&nb));
let credentails = Box::new(Orientation::Vertical, 4);
credentails::menu(&window, &credentails, &context);
let domains = Box::new(Orientation::Vertical, 4);
domains::menu(app, &window, &domains, &context, &miner);
nb.append_page(&credentails, Some(&Label::new(Some("Credentials"))));
nb.append_page(&domains, Some(&Label::new(Some("Domains"))));
window.present();
});
application.run()
}
+308 -435
View File
@@ -2,11 +2,11 @@ extern crate open;
extern crate serde;
extern crate serde_json;
extern crate tinyfiledialogs as tfd;
extern crate web_view;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::Duration;
use std::time::{Duration, Instant};
use alfis::blockchain::transaction::DomainData;
use alfis::blockchain::types::MineResult;
@@ -17,217 +17,229 @@ use alfis::event::Event;
use alfis::eventbus::{post, register};
use alfis::miner::Miner;
use alfis::{keystore, Block, Bytes, Context, Keystore, Transaction};
use chrono::{Local, Utc};
#[cfg(not(target_os = "windows"))]
use image::GenericImageView;
use chrono::{DateTime, Local, Utc};
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn, LevelFilter};
use serde::{Deserialize, Serialize};
use web_view::Content;
use Cmd::*;
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;
use self::web_view::{Handle, WebView};
pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hide: bool) {
pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>) {
let file_content = include_str!("webview/index.html");
let mut styles = inline_style(include_str!("webview/bulma.css"));
styles.push_str(&inline_style(include_str!("webview/styles.css")));
styles.push_str(&inline_style(include_str!("webview/busy_indicator.css")));
let scripts = inline_script(include_str!("webview/scripts.js"));
let html = file_content.to_owned().replace("{styles}", &styles).replace("{scripts}", &scripts);
let html = Content::Html(file_content.to_owned().replace("{styles}", &styles).replace("{scripts}", &scripts));
let title = format!("ALFIS {}", env!("CARGO_PKG_VERSION"));
// Create event loop and window
let event_loop = EventLoopBuilder::<UserEvent>::with_user_event().build();
// Create tray menu
let tray_menu = Menu::new();
let show_item = MenuItem::new("Show Window", true, None);
let quit_item = MenuItem::new("Quit", true, None);
tray_menu.append(&show_item).unwrap();
tray_menu.append(&quit_item).unwrap();
#[cfg(windows)]
let icon = tray_icon::Icon::from_resource(1, None).unwrap();
// Create tray icon
#[cfg(not(target_os = "windows"))]
let icon = load_icon_from_png();
let tray_icon = TrayIconBuilder::new()
.with_menu(Box::new(tray_menu))
.with_tooltip(&title)
.with_icon(icon)
.with_menu_on_left_click(false)
.build()
.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)));
}
let mut interface = web_view::builder()
.title(&title)
.content(html)
.size(1023, 720)
.min_size(773, 350)
.resizable(true)
.debug(false)
.user_data(())
.invoke_handler(|web_view, arg| {
debug!("Command {}", arg);
match serde_json::from_str(arg).unwrap() {
Loaded => { action_loaded(&context, web_view); }
LoadKey => { action_load_key(&context, web_view); }
CreateKey => { keystore::create_key(Arc::clone(&context)); }
SaveKey => { action_save_key(&context); }
SelectKey { index } => { action_select_key(&context, web_view, index); }
CheckRecord { data } => { action_check_record(web_view, data); }
CheckDomain { name } => { action_check_domain(&context, web_view, name); }
MineDomain { name, data, signing, encryption, renewal } => {
action_create_domain(Arc::clone(&context_ipc), Arc::clone(&miner_ipc), &proxy_ipc, name, data, signing, encryption, renewal);
}
TransferDomain { name, owner } => {
info!("Transferring '{name}' to '{owner}'");
}
StopMining => {
post(Event::ActionStopMining);
action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data, signing, encryption, renewal);
}
TransferDomain { name, owner} => { info!("Transferring '{name}' to '{owner}'"); }
StopMining => { post(Event::ActionStopMining); }
Open { link } => {
if open::that(&link).is_err() {
let _ = proxy_ipc.send_event(UserEvent::ShowWarning("Something wrong, I can't open the link 😢".to_string()));
show_warning(web_view, "Something wrong, I can't open the link 😢");
}
}
}
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) => {
error!("Error parsing command: {}", e);
web_view.eval("recordOkay(false)").expect("Error evaluating!");
dbg!(e);
}
}
});
}
#[cfg(not(target_os = "linux"))]
let webview = builder.build(&window).unwrap();
#[cfg(target_os = "linux")]
let webview = {
use tao::platform::unix::WindowExtUnix;
use wry::WebViewBuilderExtUnix;
let vbox = window.default_vbox().unwrap();
builder.build_gtk(vbox).expect("Failed to build webview gtk object")
fn action_check_domain(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>, name: String) {
let c = context.lock().unwrap();
if let Some(keystore) = c.get_keystore() {
let name = name.to_lowercase();
let available = match c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()) {
MineResult::Fine => true,
_ => false
};
// Disabling context menu on the page in release build
#[cfg(not(debug_assertions))]
let _ = webview.evaluate_script("document.addEventListener('contextmenu', e => e.preventDefault());");
web_view.eval(&format!("domainAvailable({})", available)).expect("Error evaluating!");
}
}
let webview = Arc::new(Mutex::new(webview));
let webview_clone = Arc::clone(&webview);
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 });
}
}
}
}
// Setup event bus listener
let proxy_events = proxy.clone();
fn action_select_key(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>, index: usize) {
if context.lock().unwrap().select_key_by_index(index) {
let (path, public, hash) = {
let keystore = context.lock().unwrap().get_keystore().cloned().unwrap();
let path = keystore.get_path().to_owned();
let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string();
(path, public, hash)
};
post(Event::KeyLoaded { path, public, hash });
web_view.eval(&format!("keySelected({})", index)).expect("Error evaluating!");
}
}
fn action_load_key(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
let result = tfd::open_file_dialog("Open keys file", "", Some((&["*.key", "*.toml"], "Key files")));
match result {
None => {}
Some(file_name) => {
match Keystore::from_file(&file_name, "") {
None => {
error!("Error loading keystore '{}'!", &file_name);
show_warning(web_view, "Error loading key!<br>Key cannot be loaded or its difficulty is not enough.");
event_fail(web_view, &format!("Error loading key from \\'{}\\'!", &file_name));
}
Some(keystore) => {
info!("Loaded keystore with keys: {:?}, {:?}", &keystore.get_public(), &keystore.get_encryption_public());
let path = keystore.get_path().to_owned();
let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string();
post(Event::KeyLoaded { path, public, hash });
if !context.lock().unwrap().select_key_by_public(&keystore.get_public()) {
context.lock().unwrap().add_keystore(keystore);
} else {
warn!("This key is already loaded!");
}
}
}
}
}
}
fn action_loaded(context: &Arc<Mutex<Context>>, web_view: &mut WebView<()>) {
info!("Interface loaded");
web_view.eval("showMiningIndicator(false, false);").expect("Error evaluating!");
let handle: Handle<()> = web_view.handle();
let threads = context.lock().unwrap().settings.mining.threads;
let threads = match threads {
0 => num_cpus::get(),
_ => threads
};
let status = Arc::new(Mutex::new(UiStatus::new(threads)));
let connected_nodes = Arc::new(AtomicUsize::new(0));
let nodes_copy = Arc::clone(&connected_nodes);
let context_copy = Arc::clone(context);
let c = context.lock().unwrap();
register(move |_uuid, e| {
//debug!("Got event from bus {:?}", &e);
let status = Arc::clone(&status);
let proxy = proxy_events.clone();
let nodes_copy = Arc::clone(&nodes_copy);
thread::Builder::new().name(String::from("webui")).spawn(move || {
let handle = handle.clone();
let context_copy = Arc::clone(&context_copy);
let _ = thread::Builder::new().name(String::from("webui")).spawn(move || {
let mut status = status.lock().unwrap();
let mut context = context_copy.lock().unwrap();
let eval = match e {
Event::KeyCreated { path, public, hash } => {
let _ = proxy.send_event(UserEvent::LoadDomains);
let _ = proxy.send_event(UserEvent::SendKeysToUi);
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('luck', '{}', 'Key successfully created! Don\\'t forget to save it!');", Local::now().format("%d.%m.%y %X"))));
load_domains(&mut context, &handle);
send_keys_to_ui(&context, &handle);
event_handle_luck(&handle, "Key successfully created! Don\\'t forget to save it!");
let mut s = format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash);
s.push_str(" showSuccess('New key mined successfully! Save it to a safe place!')");
s
}
Event::KeyLoaded { path, public, hash } |
Event::KeySaved { path, public, hash } => {
let _ = proxy.send_event(UserEvent::LoadDomains);
let _ = proxy.send_event(UserEvent::SendKeysToUi);
load_domains(&mut context, &handle);
send_keys_to_ui(&context, &handle);
format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash)
}
Event::MinerStarted | Event::KeyGeneratorStarted => {
status.mining = true;
status.max_diff = 0;
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining started');", Local::now().format("%d.%m.%y %X"))));
event_handle_info(&handle, "Mining started");
String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);")
}
Event::MinerStopped { success, full } => {
@@ -241,12 +253,12 @@ pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hid
if full {
match success {
true => {
let _ = proxy.send_event(UserEvent::LoadDomains);
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('luck', '{}', 'Mining is successful!');", Local::now().format("%d.%m.%y %X"))));
load_domains(&mut context, &handle);
event_handle_luck(&handle, "Mining is successful!");
s.push_str(" showSuccess('Block successfully mined!')");
}
false => {
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining finished without result.');", Local::now().format("%d.%m.%y %X"))));
event_handle_info(&handle, "Mining finished without result.");
s.push_str(" showWarning('Mining unsuccessful, sorry.')");
}
}
@@ -276,7 +288,7 @@ pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hid
status.syncing = true;
status.synced_blocks = have;
if height != status.sync_height {
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Syncing started...');", Local::now().format("%d.%m.%y %X"))));
event_handle_info(&handle, "Syncing started...");
status.sync_height = height;
}
if status.mining {
@@ -286,8 +298,8 @@ pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hid
}
}
Event::SyncFinished => {
let _ = proxy.send_event(UserEvent::LoadDomains);
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Syncing finished.');", Local::now().format("%d.%m.%y %X"))));
load_domains(&mut context, &handle);
event_handle_info(&handle, "Syncing finished.");
status.syncing = false;
if status.mining {
String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);")
@@ -296,7 +308,6 @@ pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hid
}
}
Event::NetworkStatus { blocks, domains, keys, nodes } => {
nodes_copy.store(nodes, Ordering::SeqCst);
if status.mining || status.syncing || nodes < 3 {
format!("setStats({}, {}, {}, {});", blocks, domains, keys, nodes)
} else {
@@ -305,251 +316,49 @@ pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, hid
}
Event::BlockchainChanged { index } => {
debug!("Current blockchain height is {}", 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()
event_handle_info(&handle, &format!("Blockchain changed, current block count is {} now.", index));
String::new() // Nothing
}
Event::Error { text } => format!("showError('{}')", &text),
_ => String::new()
};
if !eval.is_empty() {
let _ = proxy.send_event(UserEvent::EvalJs(eval));
handle.dispatch(move |web_view| {
web_view.eval(&eval.replace("\\", "\\\\"))
}).expect("Error dispatching!");
}
}).ok();
});
true
});
let proxy = event_loop.create_proxy();
TrayIconEvent::set_event_handler(Some(move |event| {
let _ = proxy.send_event(UserEvent::TrayIconEvent(event));
}));
let proxy = event_loop.create_proxy();
MenuEvent::set_event_handler(Some(move |event| {
let _ = proxy.send_event(UserEvent::MenuEvent(event));
}));
let proxy = event_loop.create_proxy();
// Run event loop
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
TaoEvent::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
window.set_visible(false);
}
TaoEvent::UserEvent(user_event) => {
let wv = webview_clone.lock().unwrap();
match user_event {
UserEvent::EvalJs(js) => {
let js_escaped = js.replace("\\", "\\\\");
if let Err(e) = wv.evaluate_script(&js_escaped) {
error!("Error evaluating JavaScript: {}", e);
}
}
UserEvent::Loaded => {
action_loaded(&context, &wv, &proxy);
}
UserEvent::LoadDomains => {
load_domains(&mut context.lock().unwrap(), &wv);
}
UserEvent::SendKeysToUi => {
send_keys_to_ui(&context.lock().unwrap(), &wv);
}
UserEvent::ShowWarning(text) => {
show_warning(&wv, &text);
}
UserEvent::TrayIconEvent(event) => {
match event {
TrayIconEvent::DoubleClick { button, .. } => {
if button == tray_icon::MouseButton::Left {
window.set_visible(true);
window.set_focus();
}
}
TrayIconEvent::Enter { .. } => {
let nodes = connected_nodes.load(Ordering::SeqCst);
let title = format!("ALFIS {}\nConnected: {nodes}", env!("CARGO_PKG_VERSION"));
let _ = tray_icon.set_tooltip(Some(title));
}
_ => {}
}
}
UserEvent::MenuEvent(event) => {
if event.id == show_item.id() {
window.set_visible(true);
} else if event.id == quit_item.id() {
info!("Interface closed, exiting");
post(Event::ActionQuit);
thread::sleep(Duration::from_millis(100));
*control_flow = ControlFlow::Exit;
}
}
}
}
_ => {}
}
});
}
#[derive(Debug)]
enum UserEvent {
EvalJs(String),
Loaded,
LoadDomains,
SendKeysToUi,
ShowWarning(String),
TrayIconEvent(TrayIconEvent),
MenuEvent(MenuEvent)
}
/// Load icon from embedded in binary PNG file. Only needed in Linux/macOS builds.
#[cfg(not(target_os = "windows"))]
fn load_icon_from_png() -> tray_icon::Icon {
// Include PNG in binary
const ICON_BYTES: &[u8] = include_bytes!("../img/logo/alfis_icon32.png");
// decode image by crate `image`
let image = image::load_from_memory(ICON_BYTES)
.expect("Error loading image from png");
let rgba = image.to_rgba8();
let (width, height) = image.dimensions();
// Convert to format for tray_icon
tray_icon::Icon::from_rgba(rgba.into_vec(), width, height)
.expect("Error loading icon")
}
fn check_record(data: &str) -> bool {
match serde_json::from_str::<DnsRecord>(data) {
Ok(record) => {
if let Some(string) = record.get_data() {
string.len() <= MAX_DATA_LEN
} else {
false
}
}
Err(_) => false
}
}
fn check_domain_available(context: &Arc<Mutex<Context>>, name: &str) -> bool {
let c = context.lock().unwrap();
if let Some(keystore) = c.get_keystore() {
let name = name.to_lowercase();
matches!(c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()), MineResult::Fine)
} else {
false
}
}
fn action_save_key(context: &Arc<Mutex<Context>>) {
if !context.lock().unwrap().has_keys() {
return;
}
let result = tfd::save_file_dialog_with_filter("Save keys file", "", &["*.toml"], "Key files (*.toml)");
match result {
None => {}
Some(mut new_path) => {
if !new_path.ends_with(".toml") {
new_path.push_str(".toml");
}
let path = new_path.clone();
if let Some(keystore) = context.lock().unwrap().get_keystore_mut() {
let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string();
keystore.save(&new_path, "");
info!("Key file saved to {}", &path);
post(Event::KeySaved { path, public, hash });
}
}
}
}
fn action_select_key(context: &Arc<Mutex<Context>>, proxy: &EventLoopProxy<UserEvent>, index: usize) {
if context.lock().unwrap().select_key_by_index(index) {
let (path, public, hash) = {
let keystore = context.lock().unwrap().get_keystore().cloned().unwrap();
let path = keystore.get_path().to_owned();
let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string();
(path, public, hash)
};
post(Event::KeyLoaded { path, public, hash });
let _ = proxy.send_event(UserEvent::EvalJs(format!("keySelected({})", index)));
}
}
fn action_load_key(context: &Arc<Mutex<Context>>, proxy: &EventLoopProxy<UserEvent>) {
let result = tfd::open_file_dialog("Open keys file", "", Some((&["*.key", "*.toml"], "Key files")));
match result {
None => {}
Some(file_name) => {
match Keystore::from_file(&file_name, "") {
None => {
error!("Error loading keystore '{}'!", &file_name);
let _ = proxy.send_event(UserEvent::ShowWarning("Error loading key!<br>Key cannot be loaded or its difficulty is not enough.".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('fail', '{}', 'Error loading key from \\\\'{}\\\\!');", Local::now().format("%d.%m.%y %X"), &file_name)));
}
Some(keystore) => {
info!("Loaded keystore with keys: {:?}, {:?}", &keystore.get_public(), &keystore.get_encryption_public());
let path = keystore.get_path().to_owned();
let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string();
post(Event::KeyLoaded { path, public, hash });
if !context.lock().unwrap().select_key_by_public(&keystore.get_public()) {
context.lock().unwrap().add_keystore(keystore);
} else {
warn!("This key is already loaded!");
}
}
}
}
}
}
fn action_loaded(context: &Arc<Mutex<Context>>, webview: &wry::WebView, proxy: &EventLoopProxy<UserEvent>) {
info!("Interface loaded");
let _ = webview.evaluate_script("showMiningIndicator(false, false);");
let c = context.lock().unwrap();
if let Some(keystore) = c.get_keystore() {
let path = keystore.get_path().to_owned();
let public = keystore.get_public().to_string();
let hash = keystore.get_hash().to_string();
post(Event::KeyLoaded { path, public, hash });
}
let index = c.chain.get_height();
if index > 0 {
post(Event::BlockchainChanged { index });
}
let zones = c.chain.get_zones();
info!("Loaded zones: {:?}", &zones);
if let Ok(zones) = serde_json::to_string(&zones) {
let _ = webview.evaluate_script(&format!("zonesChanged('{}');", &zones));
let _ = web_view.eval(&format!("zonesChanged('{}');", &zones));
}
drop(c);
let _ = proxy.send_event(UserEvent::SendKeysToUi);
let c = context.lock().unwrap();
send_keys_to_ui(&c, &web_view.handle());
let command = format!("setStats({}, {}, {}, {});", c.chain.get_height(), c.chain.get_domains_count(), c.chain.get_users_count(), 0);
if let Err(e) = webview.evaluate_script(&command) {
if let Err(e) = web_view.eval(&command) {
error!("Error evaluating stats: {}", e);
}
let _ = webview.evaluate_script(&format!("addEvent('info', '{}', 'Application loaded');", Local::now().format("%d.%m.%y %X")));
event_info(web_view, "Application loaded");
}
fn load_domains(context: &mut MutexGuard<Context>, webview: &wry::WebView) {
let _ = webview.evaluate_script("clearMyDomains();");
fn load_domains(context: &mut MutexGuard<Context>, handle: &Handle<()>) {
let _ = handle.dispatch(move |web_view|{
web_view.eval("clearMyDomains();")
});
let domains = context.chain.get_my_domains(context.get_keystore());
let mut domains = domains.iter().map(|(_, d)| d).collect::<Vec<_>>();
domains.sort_by(|a, b| a.0.cmp(&b.0));
@@ -557,12 +366,16 @@ fn load_domains(context: &mut MutexGuard<Context>, webview: &wry::WebView) {
let d = serde_json::to_string(&data).unwrap();
let d = d.replace("'", "\\'").replace("\\n", "\\\\n").replace("\"", "\\\"");
let command = format!("addMyDomain('{}', {}, {}, '{}');", &domain, timestamp, timestamp + DOMAIN_LIFETIME, &d);
let _ = webview.evaluate_script(&command);
let _ = handle.dispatch(move |web_view|{
web_view.eval(&command)
});
}
let _ = webview.evaluate_script("refreshMyDomains();");
let _ = handle.dispatch(move |web_view|{
web_view.eval("refreshMyDomains();")
});
}
fn send_keys_to_ui(context: &MutexGuard<Context>, webview: &wry::WebView) {
fn send_keys_to_ui(context: &MutexGuard<Context>, handle: &Handle<()>) {
let keys = {
let mut keys = Vec::new();
for key in context.get_keystores() {
@@ -574,101 +387,94 @@ fn send_keys_to_ui(context: &MutexGuard<Context>, webview: &wry::WebView) {
};
if !keys.is_empty() {
let index = context.get_active_key_index();
let _ = handle.dispatch(move |web_view| {
let command = format!("keysChanged('{}'); keySelected({});", serde_json::to_string(&keys).unwrap(), index);
let _ = webview.evaluate_script(&command);
web_view.eval(&command)
});
}
}
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) {
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) {
debug!("Creating domain with data: {}", &data);
let c = Arc::clone(&context);
let context_guard = context.lock().unwrap();
if !context_guard.has_keys() {
let _ = proxy.send_event(UserEvent::ShowWarning("You don't have keys loaded!<br>Load or mine the keys and try again.".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
let context = context.lock().unwrap();
if !context.has_keys() {
show_warning(web_view, "You don't have keys loaded!<br>Load or mine the keys and try again.");
let _ = web_view.eval("domainMiningUnavailable();");
return;
}
if context_guard.chain.is_waiting_signers() {
let _ = proxy.send_event(UserEvent::ShowWarning("Waiting for last full block to be signed. Try again later.".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
if context.chain.is_waiting_signers() {
show_warning(web_view, "Waiting for last full block to be signed. Try again later.");
let _ = web_view.eval("domainMiningUnavailable();");
info!("Waiting for last full block to be signed. Try again later.");
return;
}
let keystore = context_guard.get_keystore().unwrap().clone();
let keystore = context.get_keystore().unwrap().clone();
let pub_key = keystore.get_public();
let data = match serde_json::from_str::<DomainData>(&data) {
Ok(data) => data,
Err(e) => {
let _ = proxy.send_event(UserEvent::ShowWarning("Something wrong with domain data. I cannot mine it.".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "Something wrong with domain data. I cannot mine it.");
let _ = web_view.eval("domainMiningUnavailable();");
warn!("Error parsing data: {}", e);
return;
}
};
info!("Parsed domain data:\n{:#?}", &data);
if data.records.len() > MAX_RECORDS {
let _ = proxy.send_event(UserEvent::ShowWarning("Too many records. Mining more than 30 records not allowed.".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "Too many records. Mining more than 30 records not allowed.");
let _ = web_view.eval("domainMiningUnavailable();");
return;
}
// Check if yggdrasil only quality of zone is not violated
let zones = context_guard.chain.get_zones();
let zones = context.chain.get_zones();
for z in zones {
if z.name == data.zone && z.yggdrasil {
for record in &data.records {
if !is_yggdrasil_record(record) {
let _ = proxy.send_event(UserEvent::ShowWarning(format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone)));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, &format!("Zone {} is Yggdrasil only, you cannot use IPs from clearnet!", &data.zone));
let _ = web_view.eval("domainMiningUnavailable();");
return;
}
}
}
}
let (signing, encryption) = if signing.is_empty() || encryption.is_empty() {
(keystore.get_public(), keystore.get_encryption_public())
} else {
(Bytes::new(from_hex(&signing).unwrap()), Bytes::new(from_hex(&encryption).unwrap()))
};
match context_guard.chain.can_mine_domain(context_guard.chain.get_height(), &name, &pub_key) {
match context.chain.can_mine_domain(context.chain.get_height(), &name, &pub_key) {
MineResult::Fine => {
drop(context_guard);
drop(context);
create_domain(c, miner, CLASS_DOMAIN, &name, data, DOMAIN_DIFFICULTY, &keystore, signing, encryption, renewal);
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningStarted();".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'Mining of domain \\\\'{}\\\\' has started');", Local::now().format("%d.%m.%y %X"), &name)));
let _ = web_view.eval("domainMiningStarted();");
event_info(web_view, &format!("Mining of domain \\'{}\\' has started", &name));
}
MineResult::WrongName => {
let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine this domain!".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "You can't mine this domain!");
let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::WrongData => {
let _ = proxy.send_event(UserEvent::ShowWarning("You have an error in records!".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "You have an error in records!");
let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::WrongKey => {
let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine with current key!".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "You can't mine with current key!");
let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::WrongZone => {
let _ = proxy.send_event(UserEvent::ShowWarning("You can't mine domain in this zone!".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "You can't mine domain in this zone!");
let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::NotOwned => {
let _ = proxy.send_event(UserEvent::ShowWarning("This domain is already taken, and it is not yours!".to_string()));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
show_warning(web_view, "This domain is already taken, and it is not yours!");
let _ = web_view.eval("domainMiningUnavailable();");
}
MineResult::Cooldown { time } => {
let cooldown = format_cooldown(time);
let _ = proxy.send_event(UserEvent::EvalJs(format!("addEvent('info', '{}', 'You have cooldown {}!');", Local::now().format("%d.%m.%y %X"), &cooldown)));
let _ = proxy.send_event(UserEvent::ShowWarning(format!("You have cooldown {}!", cooldown)));
let _ = proxy.send_event(UserEvent::EvalJs("domainMiningUnavailable();".to_string()));
event_info(web_view, &format!("You have cooldown {}!", format_cooldown(time)));
show_warning(web_view, &format!("You have cooldown {}!", format_cooldown(time)));
let _ = web_view.eval("domainMiningUnavailable();");
}
}
}
@@ -684,13 +490,80 @@ fn format_cooldown(time: i64) -> String {
format!("{} hours", minutes / 60)
}
fn show_warning(webview: &wry::WebView, text: &str) {
fn show_warning(web_view: &mut WebView<()>, text: &str) {
let str = text.replace('\'', "\\'");
if let Err(e) = webview.evaluate_script(&format!("showWarning('{}');", &str)) {
warn!("Error showing warning: {}", e);
match web_view.eval(&format!("showWarning('{}');", &str)) {
Ok(_) => {}
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)]
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();
@@ -738,7 +611,7 @@ struct UiStatus {
impl UiStatus {
fn new(threads: usize) -> Self {
let speed = vec![0; threads];
let speed =vec![0; threads];
UiStatus { mining: false, syncing: false, synced_blocks: 0, sync_height: 0, max_diff: 0, speed }
}
+6663 -16603
View File
File diff suppressed because it is too large Load Diff
+6 -11
View File
@@ -47,13 +47,13 @@ function refreshRecordsList() {
function makeRecord(value, index, array) {
let data = value.addr;
if (value.type === "MX") {
if (value.type == "MX") {
data = value.priority + " " + value.host;
} else if (value.type === "CNAME" || value.type === "NS") {
} else if (value.type == "CNAME" || value.type == "NS") {
data = value.host;
} else if (value.type === "TXT" || value.type === "TLSA") {
} else if (value.type == "TXT" || value.type == "TLSA") {
data = value.data.toString();
} else if (value.type === "SRV") {
} else if (value.type == "SRV") {
data = value.priority + " " + value.weight + " " + value.port + " " + value.host;
}
@@ -212,18 +212,13 @@ function editDomain(domain, event) {
}
function onLoad() {
// Compatibility shim for wry IPC
// Workaround for Arch Linux Webkit
// https://github.com/Boscop/web-view/issues/212#issuecomment-671055663
if (typeof window.external == 'undefined' || typeof window.external.invoke == 'undefined') {
window.external = {
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);
}
}
};
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -85,7 +85,7 @@ fn run_service_logic() -> Result<()> {
let (_dns_server_ok, _miner, _network) = start_services(&settings, &context);
loop {
thread::sleep(Duration::from_millis(50));
thread::sleep(Duration::from_secs(1));
// Poll shutdown event.
match shutdown_rx.recv_timeout(Duration::from_secs(1)) {
// Break the loop either upon stop or channel disconnect