Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21e9c8d83b | ||
|
|
2c501b9fe4 | ||
|
|
e92acf8214 | ||
|
|
65fd8c7d4d | ||
|
|
e78cf2ba82 | ||
|
|
d4d796d3b1 |
108
.github/workflows/ci.yml
vendored
Normal file
108
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build in ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
env:
|
||||
NAME: updns
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
|
||||
- name: Cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --release
|
||||
|
||||
- name: Cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release
|
||||
|
||||
# -------------- Relese --------------
|
||||
|
||||
- name: Get release version (windows)
|
||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows')
|
||||
id: GITHUB_RELEASE
|
||||
shell: bash
|
||||
run: echo ::set-output name=TAG::${GITHUB_REF:10}
|
||||
|
||||
- name: Package zip (linux)
|
||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
cd ./target/release/
|
||||
zip ${{ env.NAME }}-${GITHUB_REF:10}-linux.zip ${{ env.NAME }}
|
||||
|
||||
- name: Package zip (osx)
|
||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'macos')
|
||||
run: |
|
||||
cd ./target/release/
|
||||
zip ${{ env.NAME }}-${GITHUB_REF:10}-osx.zip ${{ env.NAME }}
|
||||
|
||||
- name: Package zip (windows)
|
||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'windows')
|
||||
run: |
|
||||
cd ./target/release/
|
||||
Compress-Archive -CompressionLevel Optimal -Force -Path ${{ env.NAME }}.exe -DestinationPath ${{ env.NAME }}-${{ steps.GITHUB_RELEASE.outputs.TAG }}-windows.zip
|
||||
|
||||
- name: GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: ./target/release/*.zip
|
||||
|
||||
- name: Cargo publish
|
||||
uses: actions-rs/cargo@v1
|
||||
if: startsWith(github.ref, 'refs/tags/') && startsWith(matrix.os, 'ubuntu')
|
||||
with:
|
||||
command: publish
|
||||
args: --token ${{ secrets.CARGO_TOKEN }} -v
|
||||
|
||||
|
||||
62
.travis.yml
62
.travis.yml
@@ -1,62 +0,0 @@
|
||||
|
||||
language: rust
|
||||
services: docker
|
||||
sudo: required
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=updns
|
||||
|
||||
matrix:
|
||||
include:
|
||||
|
||||
- env: TARGET=linux
|
||||
os: linux
|
||||
|
||||
- env: TARGET=osx
|
||||
os: osx
|
||||
|
||||
- env: TARGET=windows
|
||||
os: windows
|
||||
|
||||
before_install:
|
||||
- set -e
|
||||
- rustup component add rustfmt
|
||||
|
||||
script:
|
||||
- cargo fmt --all -- --check
|
||||
- cargo test --release
|
||||
- cargo build --release
|
||||
|
||||
after_script: set +e
|
||||
|
||||
before_deploy:
|
||||
- cd ./target/release/
|
||||
- test -r $CRATE_NAME && zip $CRATE_NAME-$TRAVIS_TAG-$TARGET.zip $CRATE_NAME || mv $CRATE_NAME.exe $CRATE_NAME-$TRAVIS_TAG-$TARGET.exe
|
||||
- cd ../../
|
||||
|
||||
deploy:
|
||||
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: $GITHUB_TOKEN
|
||||
file_glob: true
|
||||
file: ./target/release/$CRATE_NAME-$TRAVIS_TAG-$TARGET.*
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
||||
- provider: cargo
|
||||
token: $CARGO_TOKEN
|
||||
on:
|
||||
condition: $TARGET = linux
|
||||
tags: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
- master
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -2,7 +2,7 @@
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "ace"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -455,13 +455,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -522,7 +522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.6"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -540,11 +540,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "0.2.2"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bytes 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"futures-core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -552,12 +553,12 @@ dependencies = [
|
||||
"mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pin-project-lite 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-macros 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "0.2.0"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -576,15 +577,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "updns"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"ace 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ace 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -626,7 +628,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum ace 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42d6302f03bbae8544fb07d9ab94bfbe2a5dbaadc857749d9fad959b55f13069"
|
||||
"checksum ace 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efcf59867ff84f1cae9358064c6bd2a17b856d3a2bcd1cb4660c60c6f4d21c0a"
|
||||
"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
|
||||
"checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee"
|
||||
"checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba"
|
||||
@@ -683,7 +685,7 @@ dependencies = [
|
||||
"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
|
||||
"checksum redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d"
|
||||
"checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd"
|
||||
"checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
|
||||
"checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716"
|
||||
"checksum rust-argon2 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf"
|
||||
"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
|
||||
@@ -691,10 +693,10 @@ dependencies = [
|
||||
"checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
|
||||
"checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92"
|
||||
"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f"
|
||||
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
|
||||
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
|
||||
"checksum tokio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2e765bf9f550bd9b8a970633ca3b56b8120c4b6c5dcbe26a93744cb02fee4b17"
|
||||
"checksum tokio-macros 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d5795a71419535c6dcecc9b6ca95bdd3c2d6142f7e8343d7beb9923f129aa87e"
|
||||
"checksum tokio 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ffa2fdcfa937b20cb3c822a635ceecd5fc1a27a6a474527e5516aa24b8c8820a"
|
||||
"checksum tokio-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "50a61f268a3db2acee8dcab514efc813dc6dbe8a00e86076f935f94304b59a7a"
|
||||
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "updns"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2018"
|
||||
|
||||
authors = ["wyhaya <wyhaya@gmail.com>"]
|
||||
@@ -18,10 +18,11 @@ keywords = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
ace = "0.0.3"
|
||||
ace = "0.1.0"
|
||||
dirs = "2.0.2"
|
||||
futures = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.3.1"
|
||||
log = { version = "0.4.8", features = ["max_level_trace", "release_max_level_info"] }
|
||||
regex = "1.3.3"
|
||||
time = "0.1.42"
|
||||
tokio = {version = "0.2.2", features = ["fs", "io-util", "macros", "net", "stream", "time"]}
|
||||
tokio = { version = "0.2.9", features = ["fs", "io-util", "macros", "net", "stream", "time"] }
|
||||
|
||||
39
README.md
39
README.md
@@ -1,14 +1,14 @@
|
||||
|
||||
|
||||
# updns
|
||||
|
||||
[](https://github.com/wyhaya/updns/actions)
|
||||
[](https://travis-ci.org/wyhaya/updns)
|
||||
[](https://crates.io/crates/updns)
|
||||
[](https://github.com/wyhaya/updns/blob/master/LICENSE)
|
||||
|
||||
---
|
||||
|
||||
updns is a simple DNS proxy server developed using `Rust`. You can intercept any domain name and return the ip you need.
|
||||
updns is a simple DNS proxy server developed using `Rust`. You can intercept any domain name and return the ip you need
|
||||
|
||||
## Install
|
||||
|
||||
@@ -20,7 +20,7 @@ Or use `cargo` to install
|
||||
cargo install updns
|
||||
```
|
||||
|
||||
## Start to use
|
||||
## Start to use 🚀
|
||||
|
||||
```bash
|
||||
updns
|
||||
@@ -28,9 +28,7 @@ updns
|
||||
updns -c /your/hosts
|
||||
```
|
||||
|
||||
You may use `sudo` to run this command because you will use the `53` port, make sure you have sufficient permissions.
|
||||
|
||||
Now change your local DNS server to `127.0.0.1` 🚀
|
||||
You may use `sudo` to run this command because you will use the `53` port
|
||||
|
||||
## Running in docker
|
||||
|
||||
@@ -53,43 +51,42 @@ Usage:
|
||||
Command:
|
||||
add Add a DNS record
|
||||
ls Print all configured DNS records
|
||||
config Call vim to edit the configuration file
|
||||
config Call 'vim' to edit the configuration file
|
||||
path Print related directories
|
||||
help Print help information
|
||||
version Print version information
|
||||
|
||||
Option:
|
||||
-c Specify a config file
|
||||
-w Check the interval of the configuration file
|
||||
-i Check the interval time of the configuration file
|
||||
format: 1ms, 1s, 1m, 1h, 1d
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
You can use `updns config` command and then call `vim` quick edit, or use `updns path` find the updns's installation directory and edit the `config` file
|
||||
You can use `updns config` command and then call `vim` edit, or find `~/.updns/config` edit
|
||||
|
||||
You can specify standard domains, or utilize [regular expressions](https://rustexp.lpil.uk "rustexp") for dynamic matching,
|
||||
You can update the config file at any time, updns will listen for file changes
|
||||
You can specify standard domains, or utilize [regular expressions](https://rustexp.lpil.uk "rustexp") for dynamic matching
|
||||
|
||||
> Regular expression starts with `~`
|
||||
|
||||
```ini
|
||||
bind 0.0.0.0:53 # Binding address
|
||||
proxy 8.8.8.8:53 # Proxy address
|
||||
timeout 2000 # Proxy timeout (ms)
|
||||
bind 0.0.0.0:53 # Binding address
|
||||
proxy 8.8.8.8:53 # Proxy address
|
||||
timeout 2s # Proxy timeout (format: 1ms, 1s, 1m, 1h, 1d)
|
||||
|
||||
# Domain matching
|
||||
example.com 1.1.1.1
|
||||
*.example.com 2.2.2.2
|
||||
^\w+\.example\.[a-z]+$ 3.3.3.3
|
||||
example.com 1.1.1.1
|
||||
*.example.com 2.2.2.2
|
||||
~^\w+\.example\.[a-z]+$ 3.3.3.3
|
||||
|
||||
# IPv6
|
||||
test.com ::
|
||||
|
||||
# Import from other file
|
||||
import /other/hosts
|
||||
```
|
||||
|
||||
## Todo
|
||||
|
||||
* Dynamically update port bindings
|
||||
|
||||
## Reference
|
||||
|
||||
[Building a DNS server in Rust](https://github.com/EmilHernvall/dnsguide)
|
||||
|
||||
145
src/config.rs
145
src/config.rs
@@ -1,4 +1,6 @@
|
||||
use crate::matcher::Matcher;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -6,6 +8,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
result,
|
||||
slice::Iter,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
@@ -17,6 +20,37 @@ lazy_static! {
|
||||
static ref COMMENT_REGEX: Regex = Regex::new("#.*$").unwrap();
|
||||
}
|
||||
|
||||
// Parse time format into Duration
|
||||
pub fn try_parse_duration(text: &str) -> result::Result<Duration, ()> {
|
||||
let numbers = "0123456789.".chars().collect::<Vec<char>>();
|
||||
let i = text
|
||||
.chars()
|
||||
.position(|ch| !numbers.contains(&ch))
|
||||
.ok_or_else(|| ())?;
|
||||
|
||||
let time = &text[..i];
|
||||
let unit = &text[i..];
|
||||
|
||||
if time.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
let n = time.parse::<f64>().map_err(|_| ())?;
|
||||
let ms = match unit {
|
||||
"d" => Ok(24_f64 * 60_f64 * 60_f64 * 1000_f64 * n),
|
||||
"h" => Ok(60_f64 * 60_f64 * 1000_f64 * n),
|
||||
"m" => Ok(60_f64 * 1000_f64 * n),
|
||||
"s" => Ok(1000_f64 * n),
|
||||
"ms" => Ok(n),
|
||||
_ => Err(()),
|
||||
}? as u64;
|
||||
|
||||
if ms == 0 {
|
||||
Err(())
|
||||
} else {
|
||||
Ok(Duration::from_millis(ms))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Invalid {
|
||||
pub line: usize,
|
||||
@@ -34,7 +68,7 @@ pub enum InvalidType {
|
||||
}
|
||||
|
||||
impl InvalidType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
pub fn description(&self) -> &str {
|
||||
match self {
|
||||
InvalidType::SocketAddr => "Cannot parse socket address",
|
||||
InvalidType::IpAddr => "Cannot parse ip address",
|
||||
@@ -47,7 +81,7 @@ impl InvalidType {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Hosts {
|
||||
record: Vec<(Host, IpAddr)>,
|
||||
record: Vec<(Matcher, IpAddr)>,
|
||||
}
|
||||
|
||||
impl Hosts {
|
||||
@@ -55,7 +89,7 @@ impl Hosts {
|
||||
Hosts { record: Vec::new() }
|
||||
}
|
||||
|
||||
fn push(&mut self, record: (Host, IpAddr)) {
|
||||
fn push(&mut self, record: (Matcher, IpAddr)) {
|
||||
self.record.push(record);
|
||||
}
|
||||
|
||||
@@ -65,7 +99,7 @@ impl Hosts {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&mut self) -> Iter<(Host, IpAddr)> {
|
||||
pub fn iter(&mut self) -> Iter<(Matcher, IpAddr)> {
|
||||
self.record.iter()
|
||||
}
|
||||
|
||||
@@ -79,67 +113,12 @@ impl Hosts {
|
||||
}
|
||||
}
|
||||
|
||||
// domain match
|
||||
const TEXT: &str = "abcdefghijklmnopqrstuvwxyz0123456789-.";
|
||||
const WILDCARD: &str = "abcdefghijklmnopqrstuvwxyz0123456789-.*";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Host(MatchMode);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MatchMode {
|
||||
Text(String),
|
||||
Regex(Regex),
|
||||
}
|
||||
|
||||
impl Host {
|
||||
fn new(domain: &str) -> result::Result<Host, regex::Error> {
|
||||
// example.com
|
||||
if Self::is_text(domain) {
|
||||
return Ok(Host(MatchMode::Text(domain.to_string())));
|
||||
}
|
||||
|
||||
// *.example.com
|
||||
if Self::is_wildcard(domain) {
|
||||
let s = format!("^{}$", domain.replace(".", r"\.").replace("*", r"[^.]+"));
|
||||
return Ok(Host(MatchMode::Regex(Regex::new(&s)?)));
|
||||
}
|
||||
|
||||
// use regex
|
||||
Ok(Host(MatchMode::Regex(Regex::new(domain)?)))
|
||||
}
|
||||
|
||||
fn is_text(domain: &str) -> bool {
|
||||
domain.chars().all(|item| TEXT.chars().any(|c| item == c))
|
||||
}
|
||||
|
||||
fn is_wildcard(domain: &str) -> bool {
|
||||
domain
|
||||
.chars()
|
||||
.all(|item| WILDCARD.chars().any(|c| item == c))
|
||||
}
|
||||
|
||||
pub fn is_match(&self, domain: &str) -> bool {
|
||||
match &self.0 {
|
||||
MatchMode::Text(text) => text == domain,
|
||||
MatchMode::Regex(reg) => reg.is_match(domain),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
match &self.0 {
|
||||
MatchMode::Text(text) => text,
|
||||
MatchMode::Regex(reg) => reg.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub bind: Vec<SocketAddr>,
|
||||
pub proxy: Vec<SocketAddr>,
|
||||
pub hosts: Hosts,
|
||||
pub timeout: Option<u64>,
|
||||
pub timeout: Option<Duration>,
|
||||
pub invalid: Vec<Invalid>,
|
||||
}
|
||||
|
||||
@@ -223,17 +202,17 @@ impl Parser {
|
||||
// match host
|
||||
// example.com 0.0.0.0
|
||||
// 0.0.0.0 example.com
|
||||
fn record(left: &str, right: &str) -> result::Result<(Host, IpAddr), InvalidType> {
|
||||
fn record(left: &str, right: &str) -> result::Result<(Matcher, IpAddr), InvalidType> {
|
||||
// ip domain
|
||||
if let Ok(ip) = right.parse() {
|
||||
return Host::new(left)
|
||||
return Matcher::new(left)
|
||||
.map(|host| (host, ip))
|
||||
.map_err(|_| InvalidType::Regex);
|
||||
}
|
||||
|
||||
// domain ip
|
||||
if let Ok(ip) = left.parse() {
|
||||
return Host::new(right)
|
||||
return Matcher::new(right)
|
||||
.map(|host| (host, ip))
|
||||
.map_err(|_| InvalidType::Regex);
|
||||
}
|
||||
@@ -282,7 +261,7 @@ impl Parser {
|
||||
Ok(addr) => config.proxy.push(addr),
|
||||
Err(_) => invalid!(InvalidType::SocketAddr),
|
||||
},
|
||||
"timeout" => match value.parse::<u64>() {
|
||||
"timeout" => match try_parse_duration(value) {
|
||||
Ok(timeout) => config.timeout = Some(timeout),
|
||||
Err(_) => invalid!(InvalidType::Timeout),
|
||||
},
|
||||
@@ -304,42 +283,6 @@ impl Parser {
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_host {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create() {}
|
||||
|
||||
#[test]
|
||||
fn test_text() {
|
||||
let host = Host::new("example.com").unwrap();
|
||||
assert!(host.is_match("example.com"));
|
||||
assert!(!host.is_match("-example.com"));
|
||||
assert!(!host.is_match("example.com.cn"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard() {
|
||||
let host = Host::new("*.example.com").unwrap();
|
||||
assert!(host.is_match("test.example.com"));
|
||||
assert!(!host.is_match("test.example.test"));
|
||||
assert!(!host.is_match("test.test.com"));
|
||||
|
||||
let host = Host::new("*.example.*").unwrap();
|
||||
assert!(host.is_match("test.example.test"));
|
||||
assert!(!host.is_match("example.com"));
|
||||
assert!(!host.is_match("test.test.test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_regex() {
|
||||
let host = Host::new("^example.com$").unwrap();
|
||||
assert!(host.is_match("example.com"));
|
||||
assert!(!host.is_match("test.example.com"));
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
26
src/logger.rs
Normal file
26
src/logger.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use log::{LevelFilter, Metadata, Record, SetLoggerError};
|
||||
|
||||
static LOGGER: Logger = Logger;
|
||||
|
||||
pub fn init() -> Result<(), SetLoggerError> {
|
||||
log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Trace))
|
||||
}
|
||||
|
||||
struct Logger;
|
||||
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, _: &Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
println!(
|
||||
"[{}] {:<5} {}",
|
||||
time::now().strftime("%F %T").unwrap(),
|
||||
record.level(),
|
||||
record.args()
|
||||
);
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
73
src/main.rs
73
src/main.rs
@@ -1,8 +1,7 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod config;
|
||||
mod lib;
|
||||
mod logger;
|
||||
mod matcher;
|
||||
mod watch;
|
||||
|
||||
use ace::App;
|
||||
@@ -10,6 +9,7 @@ use config::{Config, Hosts, Invalid, Parser};
|
||||
use dirs;
|
||||
use futures::prelude::*;
|
||||
use lib::*;
|
||||
use log::*;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
env,
|
||||
@@ -29,13 +29,13 @@ const CONFIG_FILE: [&str; 2] = [".updns", "config"];
|
||||
|
||||
const DEFAULT_BIND: &str = "0.0.0.0:53";
|
||||
const DEFAULT_PROXY: [&str; 2] = ["8.8.8.8:53", "1.1.1.1:53"];
|
||||
const DEFAULT_TIMEOUT: u64 = 2000;
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_millis(2000);
|
||||
|
||||
const WATCH_INTERVAL: u64 = 5000;
|
||||
const WATCH_INTERVAL: Duration = Duration::from_millis(5000);
|
||||
|
||||
static mut PROXY: Vec<SocketAddr> = Vec::new();
|
||||
static mut HOSTS: Option<Hosts> = None;
|
||||
static mut TIMEOUT: u64 = DEFAULT_TIMEOUT;
|
||||
static mut TIMEOUT: Duration = DEFAULT_TIMEOUT;
|
||||
|
||||
macro_rules! exit {
|
||||
($($arg:tt)*) => {
|
||||
@@ -45,27 +45,11 @@ macro_rules! exit {
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => {
|
||||
eprint!("{} ERROR ", time::now().strftime("[%Y-%m-%d %H:%M:%S]").unwrap());
|
||||
eprintln!($($arg)*);
|
||||
};
|
||||
}
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => {
|
||||
print!("{} INFO ", time::now().strftime("[%Y-%m-%d %H:%M:%S]").unwrap());
|
||||
println!($($arg)*);
|
||||
};
|
||||
}
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => {
|
||||
print!("{} WARN ", time::now().strftime("[%Y-%m-%d %H:%M:%S]").unwrap());
|
||||
println!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let _ = logger::init();
|
||||
|
||||
let app = App::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
||||
.cmd("add", "Add a DNS record")
|
||||
.cmd("ls", "Print all configured DNS records")
|
||||
@@ -74,12 +58,18 @@ async fn main() {
|
||||
.cmd("help", "Print help information")
|
||||
.cmd("version", "Print version information")
|
||||
.opt("-c", "Specify a config file")
|
||||
.opt("-w", "Check the interval of the configuration file (ms)");
|
||||
.opt(
|
||||
"-i",
|
||||
vec![
|
||||
"Check the interval time of the configuration file",
|
||||
"format: 1ms, 1s, 1m, 1h, 1d",
|
||||
],
|
||||
);
|
||||
|
||||
let config_path = match app.value("-c") {
|
||||
Some(values) => {
|
||||
if values.is_empty() {
|
||||
exit!("'-c' value: [CONFIG]");
|
||||
exit!("'-c' missing a value: [FILE]");
|
||||
}
|
||||
PathBuf::from(values[0])
|
||||
}
|
||||
@@ -90,14 +80,17 @@ async fn main() {
|
||||
};
|
||||
|
||||
// Check profile interval
|
||||
let watch_interval = match app.value("-w") {
|
||||
let watch_interval = match app.value("-i") {
|
||||
Some(values) => {
|
||||
if values.is_empty() {
|
||||
exit!("'-w' value: [ms]");
|
||||
exit!("'-i' missing a value: : [1ms, 1s, 1m, 1h, 1d]");
|
||||
}
|
||||
values[0]
|
||||
.parse::<u64>()
|
||||
.unwrap_or_else(|_| exit!("Cannot resolve '{}' to number", &values[0]))
|
||||
config::try_parse_duration(values[0]).unwrap_or_else(|_| {
|
||||
exit!(
|
||||
"Cannot resolve '{}' to interval time, format: 1ms, 1s, 1m, 1h, 1d",
|
||||
&values[0]
|
||||
)
|
||||
})
|
||||
}
|
||||
None => WATCH_INTERVAL,
|
||||
};
|
||||
@@ -136,11 +129,11 @@ async fn main() {
|
||||
let n = config
|
||||
.hosts
|
||||
.iter()
|
||||
.map(|(r, _)| r.as_str().len())
|
||||
.map(|(m, _)| m.to_string().len())
|
||||
.fold(0, |a, b| a.max(b));
|
||||
|
||||
for (host, ip) in config.hosts.iter() {
|
||||
println!("{:domain$} {}", host.as_str(), ip, domain = n);
|
||||
println!("{:domain$} {}", host.to_string(), ip, domain = n);
|
||||
}
|
||||
}
|
||||
"config" => {
|
||||
@@ -165,9 +158,9 @@ async fn main() {
|
||||
config_path.display()
|
||||
);
|
||||
}
|
||||
"help" => app.help(),
|
||||
"version" => app.version(),
|
||||
_ => app.error_try("help"),
|
||||
"help" => app.print_help(),
|
||||
"version" => app.print_version(),
|
||||
_ => app.print_error_try("help"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -194,7 +187,7 @@ async fn main() {
|
||||
watch_config(config_path, watch_interval).await;
|
||||
}
|
||||
|
||||
fn update_config(mut proxy: Vec<SocketAddr>, hosts: Hosts, timeout: Option<u64>) {
|
||||
fn update_config(mut proxy: Vec<SocketAddr>, hosts: Hosts, timeout: Option<Duration>) {
|
||||
if proxy.is_empty() {
|
||||
proxy = DEFAULT_PROXY
|
||||
.iter()
|
||||
@@ -227,13 +220,13 @@ fn output_invalid(errors: &[Invalid]) {
|
||||
error!(
|
||||
"[line:{}] {} `{}`",
|
||||
invalid.line,
|
||||
invalid.kind.as_str(),
|
||||
invalid.kind.description(),
|
||||
invalid.source
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn watch_config(p: PathBuf, t: u64) {
|
||||
async fn watch_config(p: PathBuf, t: Duration) {
|
||||
let mut watch = Watch::new(&p, t).await;
|
||||
|
||||
while let Some(_) = watch.next().await {
|
||||
@@ -287,7 +280,7 @@ async fn proxy(buf: &[u8]) -> Result<Vec<u8>> {
|
||||
for addr in proxy.iter() {
|
||||
let mut socket = UdpSocket::bind(("0.0.0.0", 0)).await?;
|
||||
|
||||
let data: Result<Vec<u8>> = timeout(Duration::from_millis(unsafe { TIMEOUT }), async {
|
||||
let data: Result<Vec<u8>> = timeout(unsafe { TIMEOUT }, async {
|
||||
socket.send_to(&buf, addr).await?;
|
||||
let mut res = [0; 512];
|
||||
let len = socket.recv(&mut res).await?;
|
||||
|
||||
184
src/matcher.rs
Normal file
184
src/matcher.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use regex::{Error, Regex};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Matcher(MatchMode);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MatchMode {
|
||||
Static(String),
|
||||
Wildcard(WildcardMatch),
|
||||
Regex(Regex),
|
||||
}
|
||||
|
||||
const REGEX_WORD: char = '~';
|
||||
const WILDCARD: char = '*';
|
||||
|
||||
impl Matcher {
|
||||
pub fn new(raw: &str) -> Result<Self, Error> {
|
||||
// Use regex: ~^example\.com$
|
||||
if raw.starts_with(REGEX_WORD) {
|
||||
let reg = raw.replacen(REGEX_WORD, "", 1);
|
||||
let mode = MatchMode::Regex(Regex::new(®)?);
|
||||
return Ok(Matcher(mode));
|
||||
}
|
||||
|
||||
// Use wildcard match: *.example.com
|
||||
let find = raw.chars().any(|c| c == WILDCARD);
|
||||
if find {
|
||||
let mode = MatchMode::Wildcard(WildcardMatch::new(raw));
|
||||
return Ok(Matcher(mode));
|
||||
}
|
||||
|
||||
// Plain Text: example.com
|
||||
Ok(Matcher(MatchMode::Static(raw.to_string())))
|
||||
}
|
||||
|
||||
pub fn is_match(&self, domain: &str) -> bool {
|
||||
match &self.0 {
|
||||
MatchMode::Static(raw) => raw == domain,
|
||||
MatchMode::Wildcard(raw) => raw.is_match(domain),
|
||||
MatchMode::Regex(raw) => raw.is_match(domain),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WildcardMatch {
|
||||
chars: Vec<char>,
|
||||
}
|
||||
|
||||
impl WildcardMatch {
|
||||
fn new(raw: &str) -> Self {
|
||||
let mut chars = Vec::with_capacity(raw.len());
|
||||
for c in raw.chars() {
|
||||
chars.push(c);
|
||||
}
|
||||
Self { chars }
|
||||
}
|
||||
|
||||
fn is_match(&self, text: &str) -> bool {
|
||||
let mut chars = text.chars();
|
||||
let mut dot = false;
|
||||
|
||||
for cur in &self.chars {
|
||||
match cur {
|
||||
'*' => {
|
||||
match chars.next() {
|
||||
Some(c) => {
|
||||
if c == '.' {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
while let Some(n) = chars.next() {
|
||||
if n == '.' {
|
||||
dot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
word => {
|
||||
if dot {
|
||||
if word == &'.' {
|
||||
dot = false;
|
||||
continue;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
match chars.next() {
|
||||
Some(c) => {
|
||||
if word != &c {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if dot {
|
||||
return false;
|
||||
}
|
||||
chars.next().is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Matcher {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self.0 {
|
||||
MatchMode::Static(raw) => write!(f, "{}", raw),
|
||||
MatchMode::Wildcard(raw) => {
|
||||
let mut s = String::new();
|
||||
for ch in raw.chars.clone() {
|
||||
s.push(ch);
|
||||
}
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
MatchMode::Regex(raw) => write!(f, "~{}", raw.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_matcher {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create() {}
|
||||
|
||||
#[test]
|
||||
fn test_text() {
|
||||
let matcher = Matcher::new("example.com").unwrap();
|
||||
assert!(matcher.is_match("example.com"));
|
||||
assert!(!matcher.is_match("-example.com"));
|
||||
assert!(!matcher.is_match("example.com.cn"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard() {
|
||||
let matcher = Matcher::new("*").unwrap();
|
||||
assert!(matcher.is_match("localhost"));
|
||||
assert!(!matcher.is_match(".localhost"));
|
||||
assert!(!matcher.is_match("localhost."));
|
||||
assert!(!matcher.is_match("local.host"));
|
||||
|
||||
let matcher = Matcher::new("*.com").unwrap();
|
||||
assert!(matcher.is_match("test.com"));
|
||||
assert!(matcher.is_match("example.com"));
|
||||
assert!(!matcher.is_match("test.test"));
|
||||
assert!(!matcher.is_match(".test.com"));
|
||||
assert!(!matcher.is_match("test.com."));
|
||||
assert!(!matcher.is_match("test.test.com"));
|
||||
|
||||
let matcher = Matcher::new("*.*").unwrap();
|
||||
assert!(matcher.is_match("test.test"));
|
||||
assert!(!matcher.is_match(".test.test"));
|
||||
assert!(!matcher.is_match("test.test."));
|
||||
assert!(!matcher.is_match("test.test.test"));
|
||||
|
||||
let matcher = Matcher::new("*.example.com").unwrap();
|
||||
assert!(matcher.is_match("test.example.com"));
|
||||
assert!(matcher.is_match("example.example.com"));
|
||||
assert!(!matcher.is_match("test.example.com.com"));
|
||||
assert!(!matcher.is_match("test.test.example.com"));
|
||||
|
||||
let matcher = Matcher::new("*.example.*").unwrap();
|
||||
assert!(matcher.is_match("test.example.com"));
|
||||
assert!(matcher.is_match("example.example.com"));
|
||||
assert!(!matcher.is_match("test.test.example.test"));
|
||||
assert!(!matcher.is_match("test.example.test.test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_regex() {
|
||||
let matcher = Matcher::new("~^example.com$").unwrap();
|
||||
assert!(matcher.is_match("example.com"));
|
||||
assert!(!matcher.is_match("test.example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ pub struct Watch {
|
||||
}
|
||||
|
||||
impl Watch {
|
||||
pub async fn new<P: AsRef<Path>>(path: P, duration: u64) -> Watch {
|
||||
pub async fn new<P: AsRef<Path>>(path: P, duration: Duration) -> Watch {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
Watch {
|
||||
path: path.clone(),
|
||||
state: None,
|
||||
modified: Self::modified(path).await,
|
||||
timer: interval(Duration::from_millis(duration)),
|
||||
timer: interval(duration),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user