├── .cargo
└── config.toml
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── build.yaml
│ └── check.yaml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Cross.toml
├── LICENSE
├── README-CN.md
├── README.md
├── commit.nix
├── configs
├── default.json
├── example.yaml
├── fail_recursion.json
├── query_cache_policy.yaml
├── success_cidr.yaml
├── success_geoip.yaml
└── success_header.yaml
├── data
├── a.cn.zone
├── apple.txt
├── apple.txt.gz
├── china.txt
├── china.txt.gz
├── cn.mmdb
├── full.mmdb
├── ipcidr-test.txt
├── ipcn.txt
└── ipcn.txt.gz
├── dcompass
├── Cargo.toml
└── src
│ ├── main.rs
│ ├── parser.rs
│ ├── tests.rs
│ └── worker.rs
├── dmatcher
├── Cargo.toml
├── benches
│ ├── benchmark.rs
│ └── sample.txt
└── src
│ ├── domain.rs
│ └── lib.rs
├── droute
├── Cargo.toml
├── README.md
├── benches
│ ├── native_script.rs
│ └── rune_script.rs
├── src
│ ├── cache.rs
│ ├── lib.rs
│ ├── mock.rs
│ └── router
│ │ ├── mod.rs
│ │ ├── script
│ │ ├── mod.rs
│ │ ├── native.rs
│ │ ├── rune_scripting
│ │ │ ├── basis.rs
│ │ │ ├── message
│ │ │ │ ├── helper.rs
│ │ │ │ └── mod.rs
│ │ │ ├── mod.rs
│ │ │ ├── types.rs
│ │ │ └── utils.rs
│ │ └── utils
│ │ │ ├── blackhole.rs
│ │ │ ├── domain.rs
│ │ │ ├── geoip.rs
│ │ │ ├── ipcidr.rs
│ │ │ └── mod.rs
│ │ └── upstreams
│ │ ├── builder.rs
│ │ ├── error.rs
│ │ ├── mod.rs
│ │ └── upstream
│ │ ├── builder.rs
│ │ ├── mod.rs
│ │ └── qhandle
│ │ ├── https.rs
│ │ ├── mod.rs
│ │ ├── qos_governor.rs
│ │ ├── qos_none.rs
│ │ ├── tls
│ │ ├── mod.rs
│ │ ├── native_tls.rs
│ │ └── rustls.rs
│ │ └── udp.rs
└── tests
│ └── router.rs
├── flake.lock
├── flake.nix
└── rustfmt.toml
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | # [build]
2 | # rustflags = ["--cfg", "tokio_unstable"]
3 |
4 | # Rustc is now going to NOT statically link musl on default.
5 | # See also: https://github.com/rust-lang/compiler-team/issues/422#issuecomment-816579989
6 | # Moreover, cargo rustflags is NOT cumulative, so we have to add the above rustflags again here.
7 | # See also: https://github.com/rust-lang/cargo/issues/5376
8 | [target.'cfg(target_env = "musl")']
9 | rustflags = ["-C", "target-feature=+crt-static", "-C", "link-self-contained=yes"]
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] Concise description here."
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug 发生了什么**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce 如何重现**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Screenshots 截图**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Version & Platform (please complete the following information) 版本信息**
24 | - Variant: [e.g. `aarch64-unknown-linux-musl-cn`]
25 | - OS: [e.g. Windows 10 / macOS / Linux]
26 | - Version [e.g. build-20210109_1254]
27 |
28 | **Additional context 附加信息**
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEAT] Concise definition here"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: cargo
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "21:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: "Build dcompass on various targets"
2 | on:
3 | push:
4 | schedule:
5 | - cron: '0 1 * * *'
6 |
7 | jobs:
8 | cachix:
9 | if: ((startsWith(github.event.head_commit.message, 'build:') || (github.event_name == 'schedule'))) && (needs.create-release.outputs.log-num > 0)
10 | name: upload cachix
11 | needs: create-release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | # Nix Flakes doesn't work on shallow clones
17 | fetch-depth: 0
18 | - uses: cachix/install-nix-action@v20
19 |
20 | - uses: cachix/cachix-action@v12
21 | with:
22 | name: dcompass
23 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
24 | pushFilter: '(-source$|nixpkgs\.tar\.gz$|\.iso$|-squashfs.img$|crate-$)'
25 |
26 | # Run the general flake checks
27 | - run: nix flake check -vL
28 |
29 | create-release:
30 | if: (startsWith(github.event.head_commit.message, 'build:') || (github.event_name == 'schedule'))
31 | name: Create release
32 | runs-on: ubuntu-latest
33 | outputs:
34 | date: ${{ steps.current_time.outputs.formattedTime }}
35 | log-num: ${{ steps.get_log.outputs.log-num }}
36 | steps:
37 | - uses: actions/checkout@v2
38 | with:
39 | fetch-depth: 0
40 | - name: Get current time
41 | uses: 1466587594/get-current-time@v2
42 | id: current_time
43 | with:
44 | format: YYYYMMDD_HHmm
45 | utcOffset: "+08:00"
46 | - name: Get log
47 | id: get_log
48 | run: |
49 | echo "::set-output name=log-num::$(git --no-pager log --since yesterday --pretty=format:%h%x09%an%x09%ad%x09%s --date short | grep -c '')"
50 | - name: Create release
51 | id: create_release
52 | if: ${{steps.get_log.outputs.log-num > 0}}
53 | uses: actions/create-release@v1
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 | with:
57 | tag_name: build-${{ steps.current_time.outputs.formattedTime }}
58 | release_name: Automated build ${{ steps.current_time.outputs.formattedTime }}
59 |
60 | build-release:
61 | name: Build dcompass for ${{ matrix.target }}
62 | if: ((startsWith(github.event.head_commit.message, 'build:') || (github.event_name == 'schedule'))) && (needs.create-release.outputs.log-num > 0)
63 | needs: create-release
64 | strategy:
65 | fail-fast: false
66 | matrix:
67 | # armv5te-unknown-linux-musleabi being temporarily removed due to https://github.com/antifuchs/governor/issues/89
68 | # x86_64-unknown-freebsd removed for unknown issue on link. Potentially due to missing third party library in cross environment.
69 | target: [x86_64-unknown-linux-musl, x86_64-unknown-linux-gnu, armv7-unknown-linux-musleabihf, x86_64-pc-windows-gnu, x86_64-apple-darwin, aarch64-unknown-linux-musl, x86_64-unknown-netbsd, i686-unknown-linux-musl, armv5te-unknown-linux-musleabi, mips-unknown-linux-musl, mips64-unknown-linux-gnuabi64, mips64el-unknown-linux-gnuabi64, mipsel-unknown-linux-musl]
70 | include:
71 | - target: x86_64-unknown-netbsd
72 | os: ubuntu-latest
73 | - target: x86_64-unknown-linux-musl
74 | os: ubuntu-latest
75 | - target: x86_64-unknown-linux-gnu
76 | os: ubuntu-latest
77 | - target: i686-unknown-linux-musl
78 | os: ubuntu-latest
79 | - target: aarch64-unknown-linux-musl
80 | os: ubuntu-latest
81 | - target: armv7-unknown-linux-musleabihf
82 | os: ubuntu-latest
83 | - target: armv5te-unknown-linux-musleabi
84 | os: ubuntu-latest
85 | - target: x86_64-pc-windows-gnu
86 | os: ubuntu-latest
87 | - target: x86_64-apple-darwin
88 | os: macos-latest
89 | # - target: x86_64-unknown-freebsd
90 | # os: ubuntu-latest
91 | - target: mips-unknown-linux-musl
92 | os: ubuntu-latest
93 | - target: mips64-unknown-linux-gnuabi64
94 | os: ubuntu-latest
95 | - target: mips64el-unknown-linux-gnuabi64
96 | os: ubuntu-latest
97 | - target: mipsel-unknown-linux-musl
98 | os: ubuntu-latest
99 | # - target: i686-unknown-freebsd
100 | # os: ubuntu-latest
101 |
102 | runs-on: ${{ matrix.os }}
103 | steps:
104 |
105 | - name: Install Nix
106 | uses: cachix/install-nix-action@v20
107 |
108 | - name: Checkout
109 | uses: actions/checkout@v1
110 |
111 | - name: Update data files
112 | run: nix run .#update
113 |
114 | - name: Install musl tools
115 | if: contains(matrix.target, 'musl')
116 | run: sudo apt-get install musl-tools
117 |
118 | - name: Install i686 tools
119 | if: contains(matrix.target, 'i686')
120 | run: sudo apt-get install binutils-i686-linux-gnu
121 |
122 | - name: Install mipsel tools
123 | if: contains(matrix.target, 'mipsel')
124 | run: sudo apt-get install binutils-mipsel-linux-gnu
125 |
126 | - name: Install mips64el tools
127 | if: contains(matrix.target, 'mips64el')
128 | run: |
129 | sudo apt-get install binutils-mips64el-linux-gnuabi64
130 | # docker build --tag cross:mips64el-unknown-linux-muslabi64 -f Dockerfile.mips64el-unknown-linux-muslabi64 https://github.com/compassd/cross.git#master:docker
131 |
132 | - name: Install mips tools
133 | if: contains(matrix.target, 'mips-')
134 | run: sudo apt-get install binutils-mips-linux-gnu
135 |
136 | - name: Install mips64 tools
137 | if: contains(matrix.target, 'mips64-')
138 | run: |
139 | sudo apt-get install binutils-mips64-linux-gnuabi64
140 | # docker build --tag cross:mips64-unknown-linux-muslabi64 -f Dockerfile.mips64-unknown-linux-muslabi64 https://github.com/compassd/cross.git#master:docker
141 |
142 | - name: Install aarch64 tools
143 | if: contains(matrix.target, 'aarch64')
144 | run: sudo apt-get install binutils-aarch64-linux-gnu
145 |
146 | - name: Install arm tools
147 | if: contains(matrix.target, 'arm')
148 | run: sudo apt-get install binutils-arm-linux-gnueabihf
149 |
150 | - uses: actions-rs/toolchain@v1
151 | with:
152 | profile: minimal
153 | toolchain: stable
154 | target: ${{ matrix.target }}
155 |
156 | - uses: actions-rs/install@v0.1
157 | with:
158 | crate: cross
159 | version: latest
160 | use-tool-cache: true
161 |
162 | - name: Cargo update
163 | run: cargo update
164 |
165 | - uses: actions/cache@v2
166 | with:
167 | path: |
168 | ~/.cargo/registry
169 | ~/.cargo/git
170 | target
171 | key: ${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
172 |
173 | - name: Build full
174 | run: |
175 | cross build --manifest-path ./dcompass/Cargo.toml --release --locked --target ${{ matrix.target }} --features "geoip-maxmind"
176 | if [[ "${{ matrix.target }}" == *"windows"* ]]
177 | then
178 | cp ./target/${{ matrix.target }}/release/dcompass.exe ./dcompass-${{ matrix.target }}-full.exe
179 | else
180 | cp ./target/${{ matrix.target }}/release/dcompass ./dcompass-${{ matrix.target }}-full
181 | fi
182 | cross build --manifest-path ./dcompass/Cargo.toml --release --locked --target ${{ matrix.target }} --features "geoip-cn"
183 | if [[ "${{ matrix.target }}" == *"windows"* ]]
184 | then
185 | cp ./target/${{ matrix.target }}/release/dcompass.exe ./dcompass-${{ matrix.target }}.exe
186 | else
187 | cp ./target/${{ matrix.target }}/release/dcompass ./dcompass-${{ matrix.target }}
188 | fi
189 |
190 | - name: Strip x86
191 | if: contains(matrix.target, 'x86')
192 | shell: bash
193 | run: |
194 | if [[ "${{ matrix.target }}" == "x86_64-pc-windows-gnu" ]]
195 | then
196 | strip ./dcompass-${{ matrix.target }}-full.exe
197 | strip ./dcompass-${{ matrix.target }}.exe
198 | else
199 | strip ./dcompass-${{ matrix.target }}-full
200 | strip ./dcompass-${{ matrix.target }}
201 | fi
202 |
203 | - name: Strip arm
204 | if: contains(matrix.target, 'arm')
205 | shell: bash
206 | run: |
207 | arm-linux-gnueabihf-strip ./dcompass-${{ matrix.target }}-full
208 | arm-linux-gnueabihf-strip ./dcompass-${{ matrix.target }}
209 |
210 | - name: Strip mipsel
211 | if: contains(matrix.target, 'mipsel')
212 | shell: bash
213 | run: |
214 | mipsel-linux-gnu-strip ./dcompass-${{ matrix.target }}-full
215 | mipsel-linux-gnu-strip ./dcompass-${{ matrix.target }}
216 |
217 | - name: Strip mips64el
218 | if: contains(matrix.target, 'mips64el')
219 | shell: bash
220 | run: |
221 | mips64el-linux-gnuabi64-strip ./dcompass-${{ matrix.target }}-full
222 | mips64el-linux-gnuabi64-strip ./dcompass-${{ matrix.target }}
223 |
224 | - name: Strip mips
225 | if: contains(matrix.target, 'mips-')
226 | shell: bash
227 | run: |
228 | mips-linux-gnu-strip ./dcompass-${{ matrix.target }}-full
229 | mips-linux-gnu-strip ./dcompass-${{ matrix.target }}
230 |
231 | - name: Strip mips64
232 | if: contains(matrix.target, 'mips64-')
233 | shell: bash
234 | run: |
235 | mips64-linux-gnuabi64-strip ./dcompass-${{ matrix.target }}-full
236 | mips64-linux-gnuabi64-strip ./dcompass-${{ matrix.target }}
237 |
238 | - name: Strip i686
239 | if: contains(matrix.target, 'i686')
240 | shell: bash
241 | run: |
242 | i686-linux-gnu-strip ./dcompass-${{ matrix.target }}-full
243 | i686-linux-gnu-strip ./dcompass-${{ matrix.target }}
244 |
245 | - name: Strip aarch64
246 | if: contains(matrix.target, 'aarch64')
247 | shell: bash
248 | run: |
249 | aarch64-linux-gnu-strip ./dcompass-${{ matrix.target }}-full
250 | aarch64-linux-gnu-strip ./dcompass-${{ matrix.target }}
251 |
252 | # - name: Package
253 | # shell: bash
254 | # run: |
255 | # if [[ "${{ matrix.target }}" == "x86_64-pc-windows-gnu" ]]
256 | # then
257 | # upx ./dcompass-${{ matrix.target }}-full.exe || true
258 | # upx ./dcompass-${{ matrix.target }}.exe || true
259 | # else
260 | # upx ./dcompass-${{ matrix.target }}-full || true
261 | # upx ./dcompass-${{ matrix.target }} || true
262 | # fi
263 |
264 | - name: Echo body
265 | if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
266 | run: git --no-pager log --since yesterday --pretty=format:%h%x09%an%x09%ad%x09%s --date short > changelog.txt
267 |
268 | - name: Publish
269 | if: ${{ matrix.target == 'x86_64-unknown-linux-musl' }}
270 | uses: softprops/action-gh-release@v1
271 | with:
272 | files: 'dcompass*'
273 | body_path: changelog.txt
274 | tag_name: build-${{ needs.create-release.outputs.date }}
275 | env:
276 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
277 |
278 | - name: Publish (no notes)
279 | if: ${{ matrix.target != 'x86_64-unknown-linux-musl' }}
280 | uses: softprops/action-gh-release@v1
281 | with:
282 | files: 'dcompass*'
283 | tag_name: build-${{ needs.create-release.outputs.date }}
284 | env:
285 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
286 |
287 | remove-release:
288 | name: Clean up release(s)
289 | if: (startsWith(github.event.head_commit.message, 'build:') || (github.event_name == 'schedule'))
290 | needs: build-release
291 | runs-on: ubuntu-latest
292 | steps:
293 | - name: Clean-up releases
294 | uses: dev-drprasad/delete-older-releases@v0.1.0
295 | with:
296 | keep_latest: 7
297 | delete_tags: true
298 | env:
299 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
300 |
--------------------------------------------------------------------------------
/.github/workflows/check.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - '**'
5 | pull_request:
6 |
7 | name: Build, test, and bench
8 |
9 | jobs:
10 | cachix:
11 | name: upload cachix
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | # Nix Flakes doesn't work on shallow clones
17 | fetch-depth: 0
18 | - uses: cachix/install-nix-action@v20
19 |
20 | - uses: cachix/cachix-action@v12
21 | if: ${{ github.event_name == 'push' }}
22 | with:
23 | name: dcompass
24 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
25 | # Don't push source or .iso files as they are pointless to take up precious cache space.
26 | pushFilter: '(-source$|nixpkgs\.tar\.gz$|\.iso$|-squashfs.img$|crate-$)'
27 |
28 | # Run the general flake checks
29 | - run: nix flake check -vL
30 |
31 | build:
32 | name: Build all feature permutations
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v2
36 | - uses: actions/cache@v2
37 | with:
38 | path: |
39 | ~/.cargo/registry
40 | ~/.cargo/git
41 | target
42 | key: ${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}-check
43 | - uses: actions-rs/install@v0.1
44 | with:
45 | crate: cargo-all-features
46 | version: latest
47 | use-tool-cache: true
48 | - run: cargo build-all-features
49 | env:
50 | RUSTFLAGS: -D warnings
51 |
52 | test:
53 | name: Test Suite
54 | runs-on: ubuntu-latest
55 | steps:
56 | - uses: actions/checkout@v2
57 | - uses: actions-rs/install@v0.1
58 | with:
59 | crate: cargo-all-features
60 | version: latest
61 | use-tool-cache: true
62 | - run: cargo test-all-features
63 | env:
64 | RUSTFLAGS: -D warnings
65 |
66 | bench:
67 | name: Benchmark
68 | runs-on: ubuntu-latest
69 | steps:
70 | - uses: actions/checkout@v2
71 | - uses: actions/cache@v2
72 | with:
73 | path: |
74 | ~/.cargo/registry
75 | ~/.cargo/git
76 | target
77 | key: ${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}-bench
78 | - uses: actions-rs/toolchain@v1
79 | with:
80 | profile: minimal
81 | toolchain: stable
82 | override: true
83 | - run: cargo bench --no-run
84 | env:
85 | RUSTFLAGS: -D warnings
86 |
87 | clippy:
88 | name: Clippy
89 | runs-on: ubuntu-latest
90 | steps:
91 | - uses: actions/checkout@v2
92 | - uses: actions/cache@v2
93 | with:
94 | path: |
95 | ~/.cargo/registry
96 | ~/.cargo/git
97 | target
98 | key: ${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}-clippy
99 | - uses: actions-rs/toolchain@v1
100 | with:
101 | profile: minimal
102 | toolchain: stable
103 | override: true
104 | - run: rustup component add clippy
105 | - run: cargo clippy
106 | env:
107 | RUSTFLAGS: -D warnings
108 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | result
2 |
3 | # Generated by Cargo
4 | # will have compiled files and executables
5 | /target/
6 | **/target/
7 |
8 | # These are backup files generated by rustfmt
9 | **/*.rs.bk
10 |
11 |
12 | # Added by cargo
13 |
14 | /target
15 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = [
4 | "dcompass",
5 | "dmatcher",
6 | "droute",
7 | ]
8 |
9 | [profile.release]
10 | lto = true
11 | opt-level = 's'
12 | codegen-units = 1
13 | panic = "abort"
14 | # debug = 1
15 |
--------------------------------------------------------------------------------
/Cross.toml:
--------------------------------------------------------------------------------
1 | # Currently, cargo GitHub Action doesn't use the up to date (git) version of the cross, we have to specify the docker images manually.
2 | # Cross creates an internal list of supported docker images when builds and pulls the images according to the list. Here we add the newly-supported ones.
3 |
4 | [target.x86_64-unknown-freebsd]
5 | image = "rustembedded/cross:x86_64-unknown-freebsd"
6 |
7 | [target.mips64el-unknown-linux-muslabi64]
8 | image = "cross:mips64el-unknown-linux-muslabi64"
9 |
10 | [target.mips64-unknown-linux-muslabi64]
11 | image = "cross:mips64-unknown-linux-muslabi64"
12 |
--------------------------------------------------------------------------------
/README-CN.md:
--------------------------------------------------------------------------------
1 | # dcompass
2 | 
3 | 一个高性能的 DNS 服务器,支持插件式路由规则,DoT 以及 DoH
4 | [中文版](README-CN.md)
5 |
6 | # Why Do You Ever Need It
7 | 如果你对 [SmartDNS](https://github.com/pymumu/smartdns) 或 [Overture](https://github.com/shawn1m/overture) 的逻辑或速度不满,不妨尝试一下 `dcompass`
8 |
9 | # 特色
10 | - 高速 (实测约 2500 qps, 接近上游当前环境下的性能上限)
11 | - 无需畏惧网络环境的切换(如 4G 切换到 Wi-Fi )
12 | - 自由路由规则编写,简洁易维护的规则语法
13 | - 丰富的匹配器,作用器插件来实现大部分的需求
14 | - DoH/DoT/UDP 协议支持
15 | - 惰性 Cache 实现,在尽可能遵守 TTL 的前提下提高返回速度,保障恶劣网络环境下的使用体验
16 | - 可选不发送 SNI 来防止连接被切断
17 | - 原生跨平台实现,支持 Linux (ARM/x86)/Windows/macOS
18 | - 纯 Rust 实现,占用低且内存安全
19 |
20 | # 注意
21 | 目前程序处于活跃开发阶段,时刻可能发生不向后兼容的变动,请以 [example.yaml](configs/example.yaml) 为准。
22 |
23 | # 用法
24 | ```
25 | dcompass -c path/to/config.json # 或 YAML 配置文件
26 | ```
27 | 你也可以直接在配置文件 (config.yml) 相同的文件夹下直接运行 `dcompass`
28 |
29 | # 软件包
30 | 1. Github Action 会自动每天按照 main branch 和最新的 maxmind GeoIP 数据库对一些平台进行编译并上传到 [release page](https://github.com/LEXUGE/dcompass/releases)。如果是 Raspberry Pi 用户,建议尝试 `armv7-unknown-linux-musleabihf`, `armv5te-unknown-linux-musleabi`, `aarch64-unknown-linux-musl`。每个 target 都带有 `full`, `cn`, `min` 三个版本, `full` 包含 maxmind GeoIP2 database, `cn` 包含 GeoIP2-CN databse (只含有中国 IP), `min` 不内置数据库。
31 | 2. NixOS 打包文件在[这里](https://github.com/icebox-nix/netkit.nix). 同时,对于 NixOS 用户,我们提供了一个包含 systemd 服务的 NixOS module 来方便用户配置。
32 |
33 | # 配置(待翻译)
34 | **Please refer to the latest English version for up-to-date information.**
35 | 配置文件包含不同的 fields
36 | - `cache_size`: DNS Cache 的大小. Larger size implies higher cache capacity (use LRU algorithm as the backend).
37 | - `verbosity`: Log 等级. Possible values are `trace`, `debug`, `info`, `warn`, `error`, `off`.
38 | - `address`: 监听的地址。
39 | - `table`: A routing table composed of `rule` blocks. The table cannot be empty and should contains a single rule named with `start`. Each rule contains `tag`, `if`, `then`, and `else`. Latter two of which are tuples of the form `(action, next)`, which means take the action first and goto the next rule with the tag specified.
40 | - `upstreams`: A set of upstreams. `timeout` is the time in seconds to timeout, which takes no effect on method `Hybrid` (default to 5). `tag` is the name of the upstream. `methods` is the method for each upstream.
41 |
42 | Different actions:
43 | - `skip`: Do nothing.
44 | - `disable`: Set response with a SOA message to curb further query. It is often used accompanied with `qtype` matcher to disable certain types of queries.
45 | - `query(tag)`: Send query via upstream with specified tag.
46 |
47 | Different matchers: (More matchers to come, including `cidr`)
48 | - `any`: Matches anything.
49 | - `domain(list of file paths)`: Matches domain in specified domain lists
50 | - `qtype(list of record types)`: Matches record type specified.
51 | - `geoip(on: resp or src, codes: list of country codes, path: optional path to the mmdb database file)`: If there is one or more `A` or `AAAA` records at the current state and the first of which has got a country code in the list specified, then it matches, otherwise it always doesn't match.
52 |
53 | Different querying methods:
54 | - `https`: DNS over HTTPS querying methods. `no_sni` means don't send SNI (useful to counter censorship). `name` is the TLS certification name of the remote server. `addr` is the remote server address.
55 | - `tls`: DNS over TLS querying methods. `no_sni` means don't send SNI (useful to counter censorship). `name` is the TLS certification name of the remote server. `addr` is the remote server address.
56 | - `udp`: Typical UDP querying method. `addr` is the remote server address.
57 | - `hybrid`: Race multiple upstreams together. the value of which is a set of tags of upstreams. Note, you can include another `hybrid` inside the set as long as they don't form chain dependencies, which is prohibited and would be detected by `dcompass` in advance.
58 |
59 | 一个无需任何外部文件的防污染分流且开箱及用的配置文件 [example.yaml](configs/example.yaml)(只支持 `full` 和 `cn`, `min` 如需使用此配置需要自带 GeoIP database)。
60 |
61 | 使用 GeoIP 来防污染的路由表(table)样例
62 |
63 | ```yaml
64 | table:
65 | - tag: start
66 | if: any
67 | then:
68 | - query: domestic
69 | - check_secure
70 | - tag: check_secure
71 | if:
72 | geoip:
73 | on: resp
74 | codes:
75 | - CN
76 | else:
77 | - query: secure
78 | - end
79 | ```
80 |
81 | # 值得说明的细节
82 | - 如果一个数据包包含有多个 DNS 请求(实际几乎不可能),匹配器只会对多个 DNS 请求的第一个进行匹配。
83 | - Cache record 一旦存在,只有在 LRU 算法将其丢弃时才会被丢弃,否则即使过期,还是会被返回,并且后台会并发一个任务来尝试更新这个 cache。
84 |
85 | # Benchmark
86 | 模拟测试(忽略网络请求的时间):
87 | ```
88 | non_cache_resolve time: [10.624 us 10.650 us 10.679 us]
89 | change: [-0.9733% -0.0478% +0.8159%] (p = 0.93 > 0.05)
90 | No change in performance detected.
91 | Found 12 outliers among 100 measurements (12.00%)
92 | 1 (1.00%) low mild
93 | 6 (6.00%) high mild
94 | 5 (5.00%) high severe
95 |
96 | cached_resolve time: [10.712 us 10.748 us 10.785 us]
97 | change: [-5.2060% -4.1827% -3.1967%] (p = 0.00 < 0.05)
98 | Performance has improved.
99 | Found 10 outliers among 100 measurements (10.00%)
100 | 2 (2.00%) low mild
101 | 7 (7.00%) high mild
102 | 1 (1.00%) high severe
103 | ```
104 |
105 | 下面是实测,不具有统计学意义
106 | - On `i7-10710U`, dnsperf gets out `~760 qps` with `0.12s avg latency` and `0.27% ServFail` rate for a test of `15004` queries.
107 | - As a reference SmartDNS gets `~640 qps` for the same test on the same hardware.
108 |
109 | # 计划
110 | - [ ] 支持自由配置的 inbound server 选项,包括 `DoH`, `DoT`, `TCP`, 和 `UDP`。
111 | - [ ] IP-CIDR 匹配器,可用于 source IP 或 response IP
112 | - [x] GeoIP 匹配器,可用于 source IP 或 response IP
113 | - [ ] 支持自由返回结果的上游(upstream)
114 |
115 | # License
116 | All three components `dmatcher`, `droute`, `dcompass` are licensed under GPLv3+.
117 | `dcompass` and `droute` with `geoip` feature gate enabled include GeoLite2 data created by MaxMind, available from https://www.maxmind.com.
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dcompass
2 |
3 | 
4 | [](https://t.me/dcompass_channel)
5 | A high-performance programmable DNS component.
6 | [中文版(未更新)](README-CN.md)
7 |
8 | # Features
9 |
10 | - Fast (~50000 qps in wild where upstream perf is about the same)
11 | - Rust-like scripting with [rune](https://rune-rs.github.io)
12 | - Fearless hot switch between network environments
13 | - Written in pure Rust
14 |
15 | # Notice
16 | **[2022-09-19] More efficient and robust scripting with rune**
17 | Introducing dcompass v0.3.0. With the rune script engine, dcompass is about 2-6x faster than the previous version with unparalleled concurrency stability. However, existing configurations are no longer valid. Please see example configs to migrate.
18 |
19 | **[2022-06-22] All-new script engine**
20 | Introducing dcompass v0.2.0. With the new script engine, you can now access every bit, every record, and every section of every DNS message. Program dcompass into whatever you want! However, existing configurations are no longer valid. Please see examples to migrate.
21 |
22 | **[2021-9-16] Expression Engine and breaking changes**
23 | dcompass is now equipped with an expression engine which let you easily and freely compose logical expressions with existing matchers. This enables us to greatly improve config readablity and versatility. However, all existing config files involving if rule block are no longer working. Please see examples to migrate.
24 |
25 | **[2021-07-28] 2x faster and breaking changes**
26 | We adopted a brand new bare metal DNS library `domain` which allows us to manipulate DNS messages without much allocation. This adoption significantly improves the memory footprint and throughput of dcompass. Due to this major refactorization, DoT/TCP/zone protocol are temporarily unavailable, however, UDP and DoH connections are now blazing fast. We will gradually put back those protocols.
27 |
28 | # Usages
29 |
30 | ```
31 | dcompass -c path/to/config.json # Or YAML
32 | ```
33 |
34 | Or you can simply run `dcompass` from the folder where your configuration file named `config.yml` resides.
35 | You can also validate your configuration
36 |
37 | ```
38 | dcompass -c path/to/config.json -v
39 | ```
40 |
41 | # Quickstart
42 |
43 | See [example.yaml](configs/example.yaml)
44 |
45 | Below is a script using GeoIP to mitigate DNS pollution
46 |
47 | ```yaml
48 | script: |
49 | pub async fn route(upstreams, inited, ctx, query) {
50 | let resp = upstreams.send_default("domestic", query).await?;
51 |
52 | for ans in resp.answer? {
53 | match ans.rtype.to_str() {
54 | "A" if !inited.geoip.0.contains(ans.to_a()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
55 | "AAAA" if !inited.geoip.0.contains(ans.to_aaaa()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
56 | _ => continue,
57 | }
58 | }
59 | Ok(resp)
60 | }
61 |
62 | pub async fn init() {
63 | Ok(#{"geoip": Utils::GeoIp(GeoIp::create_default()?)})
64 | }
65 | ```
66 |
67 | And another script that adds EDNS Client Subnet record into the OPT pseudo-section:
68 |
69 | ```yaml
70 | script: |
71 | pub async fn route(upstreams, inited, ctx, query) {
72 | // Optionally remove all the existing OPT pseudo-section(s)
73 | // query.clear_opt();
74 |
75 | query.push_opt(ClientSubnet::new(u8(15), u8(0), IpAddr::from_str("23.62.93.233")?).to_opt_data())?;
76 |
77 | upstreams.send_default("ali", query).await
78 | }
79 | ```
80 |
81 | Or implement your simple xip.io service:
82 | ```yaml
83 | script: |
84 | pub async fn route(upstreams, inited, ctx, query) {
85 | let header = query.header;
86 | header.qr = true;
87 | query.header = header;
88 |
89 | let ip_str = query.first_question?.qname.to_str();
90 | let ip = IpAddr::from_str(ip_str.replace(".xip.io", ""))?;
91 |
92 | query.push_answer(DnsRecord::new(query.first_question?.qname, Class::from_str("IN")?, 3600, A::new(ip)?.to_rdata()))?;
93 |
94 | Ok(query)
95 | }
96 | ```
97 |
98 | # Configuration
99 |
100 | Configuration file contains different fields:
101 |
102 | - `verbosity`: Log level filter. Possible values are `trace`, `debug`, `info`, `warn`, `error`, `off`.
103 | - `address`: The address to bind on.
104 | - `script`: The routing script composed of `init` and `route` snippets. `init` is run once to prepare repeatedly used components like matchers in order to avoid overhead. `script` snippet is run for every incoming DNS request concurrently.
105 | - `upstreams`: A set of upstreams. `timeout` is the time in seconds to timeout, which takes no effect on method `Hybrid` (default to 5). `tag` is the name of the upstream. `methods` is the method for each upstream.
106 |
107 | Different utilities:
108 |
109 | - `blackhole(Message)`: Set response with a SOA message to curb further query. It is often used accompanied with `qtype` to disable certain types of queries.
110 | - `upstreams.send(tag, [optional] cache policy, Message)`: Send query via upstream with specified tag. Configure cache policy with one of the three levels: `disabled`, `standard`, `persistent`. See also [example](configs/query_cache_policy.yaml).
111 |
112 | Geo IP matcher:
113 |
114 | - `GeoIp::create_default() -> Result`: Create a new Geo IP matcher from builtin Geo IP database.
115 | - `GeoIp::from_path(path) -> Result`: Create a new GeoIp matcher from the Geo IP database file with the path given.
116 | - `geoip.contains(IP address, country code)`: whether the IPs belonged to the given country code contains the given IP address
117 |
118 | IP CIDR matcher:
119 |
120 | - `IpCidr::new()`: Create an empty IP CIDR matcher.
121 | - `ipcidr.add_file(path)`: Read IP CIDR rules from the given file and add them to the IP CIDR matcher.
122 | - `ipcidr.contains(IP address)`: whether the given IP address matches any rule in the IP CIDR matcher.
123 |
124 | Domain matcher:
125 |
126 | - `Domain::new()`: Create an empty domain matcher.
127 | - `domain.add_qname(domain)`: Add the given domain to the domain matcher's ruleset.
128 | - `domain.add_file(path)`: Read domains from the given file and add them to the domain matcher.
129 | - `domain.contains(domain)`: whether the given domain matches any rule in the domain matcher.
130 |
131 | Different querying methods:
132 |
133 | - `https`: DNS over HTTPS querying methods. `uri` is the remote server address in the form like `https://cloudflare-dns.com/dns-query`. `addr` is the server IP address (both IPv6 and IPv4) are accepted. HTTP and SOCKS5 proxies are also accepted on establishing connections via `proxy`, whose format is like `socks5://[user:[passwd]]@[ip:[port]]`.
134 | - `tls`: DNS over TLS querying methods. `sni` controls whether to send SNI (useful to counter censorship). `domain` is the TLS certification name of the remote server. `addr` is the remote server address. `max_reuse` controls the maximum number of recycling of each client instance.
135 | - `udp`: Typical UDP querying method. `addr` is the remote server address.
136 | - `hybrid`: Race multiple upstreams together. the value of which is a set of tags of upstreams. Note, you can include another `hybrid` inside the set as long as they don't form chain dependencies, which is prohibited and would be detected by `dcompass` in advance.
137 | - `zone`: [CURRENTLY UNSUPOORTED] use local DNS zone file to provide customized responses. See also [zone config example](configs/success_zone.yaml)
138 |
139 | See [example.yaml](configs/example.yaml) for a pre-configured out-of-box anti-pollution configuration (Only works with `full` or `cn` version, to use with `min`, please provide your own database).
140 |
141 | # Packages
142 |
143 | You can download binaries at [release page](https://github.com/LEXUGE/dcompass/releases).
144 |
145 | 1. GitHub Action build is set up `x86_64`, `i686`, `arm`, and `mips`. Check them out on release page!
146 | 2. NixOS package is available at this repo as a flake. Also, for NixOS users, a NixOS modules is provided with systemd services and easy-to-setup interfaces in the same repository where package is provided.
147 |
148 | ```
149 | └───packages
150 | ├───aarch64-linux
151 | │ ├───dcompass-cn: package 'dcompass-cn-git'
152 | │ └───dcompass-maxmind: package 'dcompass-maxmind-git'
153 | ├───i686-linux
154 | │ ├───dcompass-cn: package 'dcompass-cn-git'
155 | │ └───dcompass-maxmind: package 'dcompass-maxmind-git'
156 | ├───x86_64-darwin
157 | │ ├───dcompass-cn: package 'dcompass-cn-git'
158 | │ └───dcompass-maxmind: package 'dcompass-maxmind-git'
159 | └───x86_64-linux
160 | ├───dcompass-cn: package 'dcompass-cn-git'
161 | └───dcompass-maxmind: package 'dcompass-maxmind-git'
162 | ```
163 |
164 | cache is available at [cachix](https://dcompass.cachix.org), with public key `dcompass.cachix.org-1:uajJEJ1U9uy/y260jBIGgDwlyLqfL1sD5yaV/uWVlbk=` (`outputs.publicKey`).
165 |
166 | # Benchmark
167 |
168 | Mocked benchmark (server served on local loopback):
169 |
170 | ```
171 | Gnuplot not found, using plotters backend
172 | non_cache_resolve time: [20.548 us 20.883 us 21.282 us]
173 | change: [-33.128% -30.416% -27.511%] (p = 0.00 < 0.05)
174 | Performance has improved.
175 | Found 11 outliers among 100 measurements (11.00%)
176 | 6 (6.00%) high mild
177 | 5 (5.00%) high severe
178 |
179 | cached_resolve time: [2.6429 us 2.6493 us 2.6566 us]
180 | change: [-90.684% -90.585% -90.468%] (p = 0.00 < 0.05)
181 | Performance has improved.
182 | Found 2 outliers among 100 measurements (2.00%)
183 | 1 (1.00%) high mild
184 | 1 (1.00%) high severe
185 | ```
186 |
187 | # TODO-list
188 |
189 | - [ ] Support multiple inbound servers with different types like `DoH`, `DoT`, `TCP`, and `UDP`.
190 | - [ ] RESTful API and web dashboard
191 | - [x] Flexible DNS message editing API
192 | - [x] Script engine
193 | - [x] IP-CIDR matcher for both source address and response address
194 | - [x] GeoIP matcher for source address
195 |
196 | # License
197 |
198 | All three components `dmatcher`, `droute`, `dcompass` are licensed under GPLv3+.
199 | `dcompass` with `geoip` feature gate enabled includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.
200 |
--------------------------------------------------------------------------------
/commit.nix:
--------------------------------------------------------------------------------
1 | { pkgs, lib }:
2 | with pkgs;
3 |
4 | pkgs.mkShell {
5 | # this will make all the build inputs from hello and gnutar
6 | # available to the shell environment
7 | nativeBuildInputs = [
8 | shellcheck
9 | shfmt
10 | git
11 | coreutils
12 | findutils
13 | nixpkgs-fmt
14 |
15 | gcc
16 | # write rustfmt first to ensure we are using nightly rustfmt
17 | rust-bin.nightly."2024-01-01".rustfmt
18 | rust-bin.stable.latest.default
19 | binutils-unwrapped
20 |
21 | # perl
22 | # gnumake
23 | ];
24 |
25 | shellHook = ''
26 | set -e
27 |
28 | find . -path ./target -prune -false -o -type f -name '*.sh' -exec shellcheck {} +
29 | find . -path ./target -prune -false -o -type f -name '*.sh' -exec shfmt -w {} +
30 | find . -path ./target -prune -false -o -type f -name '*.nix' -exec nixpkgs-fmt {} +
31 | nix flake update
32 | cargo update
33 | cargo fmt -- --check
34 | cargo build
35 | cargo test
36 | cargo clippy
37 | cargo bench --no-run
38 |
39 | echo -n "Adding to git..."
40 | git add --all
41 | echo "Done."
42 |
43 | git status
44 | read -n 1 -s -r -p "Press any key to continue"
45 |
46 | echo "Commiting..."
47 | echo "Enter commit message: "
48 | read -r commitMessage
49 | git commit -m "$commitMessage"
50 | echo "Done."
51 |
52 | echo -n "Pushing..."
53 | git push
54 | echo "Done."
55 | '';
56 | }
57 |
--------------------------------------------------------------------------------
/configs/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbosity": "info",
3 | "address": "0.0.0.0:53",
4 | "script": "pub async fn route(upstreams, inited, ctx, query) { upstreams.send_default(\"cloudflare\", query).await }",
5 | "upstreams": {
6 | "cloudflare": {
7 | "https": {
8 | "timeout": 4,
9 | "uri": "https://cloudflare-dns.com/dns-query",
10 | "addr": "1.0.0.1",
11 | "sni": false
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/configs/example.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | verbosity: "info"
3 | address: 0.0.0.0:2053
4 | script: |
5 | pub async fn route(upstreams, inited, ctx, query) {
6 | // A few constants are predefined:
7 | // - query: the incoming query received
8 | // - ctx: the query context, e.g. client IP
9 | // - inited: the value returned by init()
10 | // - upstreams: the upstreams API
11 |
12 | if query.first_question?.qtype.to_str() == "AAAA" {
13 | return blackhole(query);
14 | }
15 |
16 | let resp = upstreams.send_default("domestic", query).await?;
17 |
18 | for ans in resp.answer? {
19 | match ans.rtype.to_str() {
20 | "A" if !inited.geoip.0.contains(ans.to_a()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
21 | "AAAA" if !inited.geoip.0.contains(ans.to_aaaa()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
22 | _ => continue,
23 | }
24 | }
25 | Ok(resp)
26 | }
27 |
28 | pub async fn init() {
29 | Ok(#{"geoip": Utils::GeoIp(GeoIp::create_default()?)})
30 | }
31 |
32 | upstreams:
33 | 114DNS:
34 | udp:
35 | addr: 114.114.114.114:53
36 |
37 | Ali:
38 | udp:
39 | addr: 223.6.6.6:53
40 |
41 | domestic:
42 | hybrid:
43 | - 114DNS
44 | - Ali
45 |
46 | cloudflare:
47 | https:
48 | uri: https://cloudflare-dns.com/dns-query
49 | ratelimit: 3000
50 | addr: 1.0.0.1
51 |
52 | quad9:
53 | https:
54 | uri: https://quad9.net/dns-query
55 | ratelimit: 3000
56 | addr: 9.9.9.9
57 |
58 | secure:
59 | hybrid:
60 | - cloudflare
61 | - quad9
62 |
--------------------------------------------------------------------------------
/configs/fail_recursion.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbosity": "off",
3 | "address": "0.0.0.0:2053",
4 | "script": "",
5 | "upstreams": {
6 | "114": {
7 | "udp": {
8 | "addr": "114.114.114.114:53",
9 | "timeout": 1
10 | }
11 | },
12 | "quad9": {
13 | "https": {
14 | "timeout": 2,
15 | "uri": "https://dns.quad9.net/dns-query",
16 | "addr": "9.9.9.9"
17 | }
18 | },
19 | "domestic": {
20 | "hybrid": [
21 | "114",
22 | "secure"
23 | ]
24 | },
25 | "secure": {
26 | "hybrid": [
27 | "quad9",
28 | "domestic"
29 | ]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/configs/query_cache_policy.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | verbosity: "off"
3 | address: 0.0.0.0:2053
4 | script: |
5 | pub async fn route(upstreams, inited, ctx, query) {
6 | if inited.domain.0.contains(query.first_question?.qname) {
7 | upstreams.send_default("domestic", query).await
8 | } else {
9 | upstreams.send("secure", CacheMode::Persistent, query).await
10 | }
11 | }
12 |
13 | pub async fn init() {
14 | let domain = Domain::new().add_file("../data/china.txt")?.seal();
15 | Ok(#{"domain": Utils::Domain(domain)})
16 | }
17 |
18 |
19 | upstreams:
20 | domestic:
21 | udp:
22 | addr: 223.5.5.6:53
23 | timeout: 1
24 | secure:
25 | https:
26 | timeout: 2
27 | uri: https://dns.quad9.net/dns-query
28 | addr: 9.9.9.9
29 |
--------------------------------------------------------------------------------
/configs/success_cidr.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | verbosity: "info"
3 | address: 0.0.0.0:2053
4 | script: |
5 | pub async fn route(upstreams, inited, ctx, query) {
6 | let resp = upstreams.send_default("domestic", query).await?;
7 |
8 | for ans in resp.answer? {
9 | match ans.rtype.to_str() {
10 | "A" if !inited.cidr.0.contains(ans.to_a()?.ip) => { return upstreams.send_default("secure", query).await; }
11 | "AAAA" if !inited.cidr.0.contains(ans.to_aaaa()?.ip) => { return upstreams.send_default("secure", query).await; }
12 | _ => continue,
13 | }
14 | }
15 |
16 | Ok(resp)
17 | }
18 |
19 | pub async fn init() {
20 | let cidr = IpCidr::new().add_file("../data/ipcn.txt")?.seal();
21 | Ok(#{"cidr": Utils::IpCidr(cidr)})
22 | }
23 |
24 | upstreams:
25 | domestic:
26 | udp:
27 | addr: 114.114.114.114:53
28 | timeout: 1
29 | secure:
30 | https:
31 | timeout: 2
32 | uri: https://dns.quad9.net/dns-query
33 | addr: 9.9.9.9
34 |
--------------------------------------------------------------------------------
/configs/success_geoip.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | verbosity: "off"
3 | address: 0.0.0.0:2053
4 | script: |
5 | pub async fn route(upstreams, inited, ctx, query) {
6 | let resp = upstreams.send_default("domestic", query).await?;
7 |
8 | for ans in resp.answer? {
9 | match ans.rtype.to_str() {
10 | "A" if !inited.geoip.0.contains(ans.to_a()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
11 | "AAAA" if !inited.geoip.0.contains(ans.to_aaaa()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
12 | _ => continue,
13 | }
14 | }
15 | Ok(resp)
16 | }
17 |
18 | pub async fn init() {
19 | Ok(#{"geoip": Utils::GeoIp(GeoIp::from_path("../data/full.mmdb").await?)})
20 | }
21 |
22 | upstreams:
23 | domestic:
24 | udp:
25 | addr: 114.114.114.114:53
26 | timeout: 1
27 | secure:
28 | https:
29 | timeout: 2
30 | uri: https://dns.quad9.net/dns-query
31 | addr: 9.9.9.9
32 |
--------------------------------------------------------------------------------
/configs/success_header.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | verbosity: "off"
3 | address: 0.0.0.0:2053
4 | script: |
5 | pub async fn route(upstreams, inited, ctx, query) {
6 | if query.header.opcode.to_str() == "QUERY" {
7 | upstreams.send_default("secure", query).await
8 | }
9 | }
10 |
11 | upstreams:
12 | secure:
13 | https:
14 | timeout: 2
15 | uri: https://dns.quad9.net/dns-query
16 | addr: 9.9.9.9
17 |
--------------------------------------------------------------------------------
/data/a.cn.zone:
--------------------------------------------------------------------------------
1 | ; replace the trust-dns.org with your own name
2 | @ IN SOA trust-dns.org. root.trust-dns.org. (
3 | 2021031306 ; Serial
4 | 28800 ; Refresh
5 | 7200 ; Retry
6 | 604800 ; Expire
7 | 86400) ; Minimum TTL
8 |
9 | NS bbb
10 |
11 | MX 1 alias
12 |
13 | ANAME www
14 |
15 | www A 127.0.0.1
16 | AAAA ::1
17 |
18 | bbb A 127.0.0.2
19 | this.has.dots A 127.0.0.3
20 |
21 | alias CNAME www
22 | alias-chain CNAME alias
23 |
24 | aname-chain ANAME alias
25 |
26 | ; _Service._Proto.Name TTL Class SRV Priority Weight Port Target
27 | server SRV 1 1 443 alias
28 |
29 | *.wildcard CNAME www
30 |
31 | no-service 86400 IN MX 0 .
32 |
--------------------------------------------------------------------------------
/data/apple.txt:
--------------------------------------------------------------------------------
1 | a1.mzstatic.com
2 | a2.mzstatic.com
3 | a3.mzstatic.com
4 | a4.mzstatic.com
5 | a5.mzstatic.com
6 | adcdownload.apple.com.akadns.net
7 | adcdownload.apple.com
8 | appldnld.apple.com
9 | appldnld.g.aaplimg.com
10 | apps.apple.com
11 | apps.mzstatic.com
12 | cdn-cn1.apple-mapkit.com
13 | cdn-cn2.apple-mapkit.com
14 | cdn-cn3.apple-mapkit.com
15 | cdn-cn4.apple-mapkit.com
16 | cdn.apple-mapkit.com
17 | cdn1.apple-mapkit.com
18 | cdn2.apple-mapkit.com
19 | cdn3.apple-mapkit.com
20 | cdn4.apple-mapkit.com
21 | cds-cdn.v.aaplimg.com
22 | cds.apple.com.akadns.net
23 | cds.apple.com
24 | cl1-cdn.origin-apple.com.akadns.net
25 | cl1.apple.com
26 | cl2-cn.apple.com
27 | cl2.apple.com.edgekey.net.globalredir.akadns.net
28 | cl2.apple.com
29 | cl3-cdn.origin-apple.com.akadns.net
30 | cl3.apple.com
31 | cl4-cdn.origin-apple.com.akadns.net
32 | cl4-cn.apple.com
33 | cl4.apple.com
34 | cl5-cdn.origin-apple.com.akadns.net
35 | cl5.apple.com
36 | clientflow.apple.com.akadns.net
37 | clientflow.apple.com
38 | configuration.apple.com.akadns.net
39 | configuration.apple.com
40 | cstat.apple.com
41 | dd-cdn.origin-apple.com.akadns.net
42 | download.developer.apple.com
43 | gs-loc-cn.apple.com
44 | gs-loc.apple.com
45 | gsp10-ssl-cn.ls.apple.com
46 | gsp11-cn.ls.apple.com
47 | gsp12-cn.ls.apple.com
48 | gsp13-cn.ls.apple.com
49 | gsp4-cn.ls.apple.com.edgekey.net.globalredir.akadns.net
50 | gsp4-cn.ls.apple.com.edgekey.net
51 | gsp4-cn.ls.apple.com
52 | gsp5-cn.ls.apple.com
53 | gspe19-cn-ssl.ls.apple.com
54 | gspe19-cn.ls-apple.com.akadns.net
55 | gspe19-cn.ls.apple.com
56 | gspe21-ssl.ls.apple.com
57 | gspe21.ls.apple.com
58 | gspe35-ssl.ls.apple.com
59 | iadsdk.apple.com
60 | icloud-cdn.icloud.com.akadns.net
61 | icloud.cdn-apple.com
62 | images.apple.com.akadns.net
63 | images.apple.com.edgekey.net.globalredir.akadns.net
64 | images.apple.com
65 | init-p01md-lb.push-apple.com.akadns.net
66 | init-p01md.apple.com
67 | init-p01st-lb.push-apple.com.akadns.net
68 | init-p01st.push.apple.com
69 | init-s01st-lb.push-apple.com.akadns.net
70 | init-s01st.push.apple.com
71 | iosapps.itunes.g.aaplimg.com
72 | iphone-ld.apple.com
73 | is1-ssl.mzstatic.com
74 | is1.mzstatic.com
75 | is2-ssl.mzstatic.com
76 | is2.mzstatic.com
77 | is3-ssl.mzstatic.com
78 | is3.mzstatic.com
79 | is4-ssl.mzstatic.com
80 | is4.mzstatic.com
81 | is5-ssl.mzstatic.com
82 | is5.mzstatic.com
83 | itunes-apple.com.akadns.net
84 | itunes.apple.com
85 | itunesconnect.apple.com
86 | mesu-cdn.apple.com.akadns.net
87 | mesu-china.apple.com.akadns.net
88 | mesu.apple.com
89 | music.apple.com
90 | ocsp-lb.apple.com.akadns.net
91 | ocsp.apple.com
92 | oscdn.apple.com
93 | oscdn.origin-apple.com.akadns.net
94 | pancake.apple.com
95 | pancake.cdn-apple.com.akadns.net
96 | phobos.apple.com
97 | prod-support.apple-support.akadns.net
98 | s.mzstatic.com
99 | s1.mzstatic.com
100 | s2.mzstatic.com
101 | s3.mzstatic.com
102 | s4.mzstatic.com
103 | s5.mzstatic.com
104 | stocks-sparkline-lb.apple.com.akadns.net
105 | store.apple.com.edgekey.net.globalredir.akadns.net
106 | store.apple.com.edgekey.net
107 | store.apple.com
108 | store.storeimages.apple.com.akadns.net
109 | store.storeimages.cdn-apple.com
110 | support-china.apple-support.akadns.net
111 | support.apple.com
112 | swcatalog-cdn.apple.com.akadns.net
113 | swcatalog.apple.com
114 | swcdn.apple.com
115 | swcdn.g.aaplimg.com
116 | swdist.apple.com.akadns.net
117 | swdist.apple.com
118 | swscan-cdn.apple.com.akadns.net
119 | swscan.apple.com
120 | updates-http.cdn-apple.com.akadns.net
121 | updates-http.cdn-apple.com
122 | valid.apple.com
123 | valid.origin-apple.com.akadns.net
124 | www.apple.com.edgekey.net.globalredir.akadns.net
125 | www.apple.com.edgekey.net
126 | www.apple.com
127 |
--------------------------------------------------------------------------------
/data/apple.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compassd/dcompass/68315d0052aa2887c6d6d995b46dd316cf49d571/data/apple.txt.gz
--------------------------------------------------------------------------------
/data/china.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compassd/dcompass/68315d0052aa2887c6d6d995b46dd316cf49d571/data/china.txt.gz
--------------------------------------------------------------------------------
/data/cn.mmdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compassd/dcompass/68315d0052aa2887c6d6d995b46dd316cf49d571/data/cn.mmdb
--------------------------------------------------------------------------------
/data/full.mmdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compassd/dcompass/68315d0052aa2887c6d6d995b46dd316cf49d571/data/full.mmdb
--------------------------------------------------------------------------------
/data/ipcidr-test.txt:
--------------------------------------------------------------------------------
1 | 223.255.252.0/23
2 |
--------------------------------------------------------------------------------
/data/ipcn.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compassd/dcompass/68315d0052aa2887c6d6d995b46dd316cf49d571/data/ipcn.txt.gz
--------------------------------------------------------------------------------
/dcompass/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dcompass"
3 | version = "0.3.0-alpha.1"
4 | authors = ["Harry Ying "]
5 | edition = "2021"
6 | description = "Rule-based high performance DNS server with multi-upstreams, DoT and DoH supports."
7 | repository = "https://github.com/LEXUGE/dcompass"
8 | license = "GPL-3.0"
9 |
10 | [features]
11 | geoip-cn = ["droute/geoip-cn"]
12 | geoip-maxmind = ["droute/geoip-maxmind"]
13 |
14 | [dependencies]
15 | # used by tokio-console
16 | # console-subscriber = "^0.1"
17 | # tokio = { version = "^1", features = ["rt-multi-thread", "net", "fs", "macros", "io-util", "signal", "sync", "tracing"]}
18 |
19 | compact_str = { version = "^0.6", features = ["serde"]}
20 | async-trait = "^0.1"
21 | domain = {version = "^0.7", features = ["bytes"]}
22 | futures = "^0.3"
23 | tokio = { version = "^1", features = ["rt-multi-thread", "net", "fs", "macros", "io-util", "signal", "sync"]}
24 | simple_logger = "^4"
25 | log = "^0.4"
26 | anyhow = "^1.0"
27 | serde = { version = "^1.0", features = ["derive", "rc"] }
28 | serde_yaml = "^0.9"
29 | dmatcher = {version = "^0.1", path = "../dmatcher"}
30 | structopt = "^0.3"
31 | bytes = "^1"
32 |
33 | # Use rustls on other platforms
34 | [target.'cfg(not(any(target_arch = "mips", target_arch = "mips64")))'.dependencies]
35 | droute = {version = "0.3.0-alpha.1", path = "../droute", features = ["doh-rustls", "dot-rustls"]}
36 |
37 | # Use native tls on MIPS
38 | [target.'cfg(any(target_arch = "mips", target_arch = "mips64"))'.dependencies]
39 | droute = {version = "0.3.0-alpha.1", path = "../droute", features = ["doh-native-tls", "dot-native-tls"]}
40 |
41 | # Both musl and msvc are not well-supoorted
42 | # Only allow on gnu or none env AND not on windows
43 | # [target.'cfg(all(any(target_env = "gnu", target_env = ""), not(target_os = "windows")))'.dependencies]
44 | # tikv-jemallocator = {version = "^0.4", features = ["background_threads"]}
45 |
46 | [dev-dependencies]
47 | tokio-test = "^0.4"
48 |
49 | [package.metadata.cargo-all-features]
50 | # If your crate has a large number of optional dependencies, skip them for speed
51 | skip_optional_dependencies = true
52 |
53 | skip_feature_sets = [
54 | ["geoip-maxmind", "geoip-cn"],
55 | ]
56 |
--------------------------------------------------------------------------------
/dcompass/src/main.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | // #[cfg(all(any(target_env = "gnu", target_env = ""), not(target_os = "windows")))]
17 | // use tikv_jemallocator::Jemalloc;
18 | //
19 | // #[cfg(all(any(target_env = "gnu", target_env = ""), not(target_os = "windows")))]
20 | // #[global_allocator]
21 | // static GLOBAL: Jemalloc = Jemalloc;
22 |
23 | mod parser;
24 | #[cfg(test)]
25 | mod tests;
26 | mod worker;
27 |
28 | use self::{parser::Parsed, worker::worker};
29 | use anyhow::{Context, Result};
30 | use bytes::BytesMut;
31 | use droute::{
32 | builders::{RouterBuilder, RuneScript},
33 | errors::ScriptError,
34 | AsyncTryInto, Router,
35 | };
36 | use log::*;
37 | use simple_logger::SimpleLogger;
38 | use std::{net::SocketAddr, path::PathBuf, result::Result as StdResult, sync::Arc, time::Duration};
39 | use structopt::StructOpt;
40 | use tokio::{
41 | fs::File,
42 | io::AsyncReadExt,
43 | net::UdpSocket,
44 | signal,
45 | sync::broadcast::{self, Sender},
46 | time::sleep,
47 | };
48 |
49 | #[derive(Debug, StructOpt)]
50 | #[structopt(
51 | name = "dcompass",
52 | about = "High-performance DNS server with freestyle routing scheme support and DoT/DoH functionalities built-in."
53 | )]
54 | struct DcompassOpts {
55 | /// Path to the configuration file. Use built-in if not provided.
56 | #[structopt(short, long, parse(from_os_str))]
57 | config: Option,
58 |
59 | /// Set this flag to validate the configuration file only.
60 | #[structopt(short, long, parse(from_flag))]
61 | validate: bool,
62 | }
63 |
64 | async fn init(p: Parsed) -> StdResult<(Router, SocketAddr, LevelFilter), ScriptError> {
65 | Ok((
66 | RouterBuilder::new(p.script, p.upstreams)
67 | .async_try_into()
68 | .await?,
69 | p.address,
70 | p.verbosity,
71 | ))
72 | }
73 |
74 | async fn serve(socket: Arc, router: Arc>, tx: &Sender<()>) {
75 | loop {
76 | // Size recommended by DNS Flag Day 2020: "This is practical for the server operators that know their environment, and the defaults in the DNS software should reflect the minimum safe size which is 1232."
77 | let mut buf = BytesMut::with_capacity(1024);
78 | buf.resize(1024, 0);
79 | // On windows, some applications may go away after they got their first response, resulting in a broken pipe, we should discard errors on receiving/sending messages.
80 | let (len, src) = match socket.recv_from(&mut buf).await {
81 | Ok(r) => r,
82 | Err(e) => {
83 | warn!("failed to receive query: {}", e);
84 | continue;
85 | }
86 | };
87 |
88 | buf.resize(len, 0);
89 |
90 | let router = router.clone();
91 | let socket = socket.clone();
92 | let mut shutdown = tx.subscribe();
93 | #[rustfmt::skip]
94 | tokio::spawn(async move {
95 | tokio::select! {
96 | biased; res = worker(router, socket, buf.freeze(), src) => {
97 | match res {
98 | Ok(_) => (),
99 | Err(e) => warn!("handling query failed: {}", e),
100 | }
101 | }
102 | _ = shutdown.recv() => {
103 | // If a shutdown signal is received, return from the spawned task.
104 | // This will result in the task terminating.
105 | log::warn!("worker shut down");
106 | }
107 | }
108 | });
109 | }
110 | }
111 |
112 | #[tokio::main]
113 | async fn main() -> Result<()> {
114 | // console_subscriber::init();
115 |
116 | let args: DcompassOpts = DcompassOpts::from_args();
117 |
118 | // If the config path is manually specified with `-c` flag, we use it and any error should fail early.
119 | // If there is no specified config but there is `config.yaml` under the path where user is invoking `dcompass` (not the absolute path of the binary), then we shall try that config. If the file exists but we failed to read, this should fail. Otherwise, we shall use the default anyway.
120 | let config = if let Some(config_path) = args.config {
121 | let display_path = config_path.as_path().display();
122 | let mut file = File::open(config_path.clone())
123 | .await
124 | .with_context(|| format!("Failed to open the file specified: {}", display_path))?;
125 | let mut config = String::new();
126 | file.read_to_string(&mut config)
127 | .await
128 | .with_context(|| format!("Failed to read from the file specified: {}", display_path))?;
129 | println!("Using the config file specified: {}", display_path);
130 | config
131 | } else {
132 | let mut config_path = std::env::current_dir()?;
133 | config_path.push("config.yaml");
134 | let display_path = config_path.as_path().display();
135 | match File::open(config_path.clone()).await {
136 | // We have found the config and successfully opened it.
137 | Ok(mut file) => {
138 | let mut config = String::new();
139 | file.read_to_string(&mut config).await.with_context(|| {
140 | format!("Failed to read from the file found: {}", display_path)
141 | })?;
142 | println!("Using the config under current path: {}", display_path);
143 | config
144 | }
145 | // No config found, using built-in.
146 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
147 | println!("No config found or specified, using built-in config.");
148 | include_str!("../../configs/default.json").to_owned()
149 | }
150 | // Found but unable to open. We shall exit as this is intended.
151 | Err(e) => {
152 | return Err(e).with_context(|| {
153 | format!("`config.yaml` found, but failed to open: {}", display_path)
154 | })
155 | }
156 | }
157 | };
158 |
159 | // Create whatever we need for get dcompass up and running.
160 | let (router, addr, verbosity) = init(
161 | serde_yaml::from_str(&config)
162 | .with_context(|| "Failed to parse the configuration file".to_string())?,
163 | )
164 | .await?;
165 |
166 | // If we are only required to validate the config, we shall be safe to exit now.
167 | if args.validate {
168 | println!("The configuration provided is valid.");
169 | return Ok(());
170 | }
171 |
172 | // Start logging
173 | SimpleLogger::new()
174 | // These modules are quite chatty, we want to disable it.
175 | .with_level(verbosity)
176 | .init()?;
177 |
178 | info!("dcompass ready!");
179 |
180 | let router = Arc::new(router);
181 | // Bind an UDP socket
182 | let socket = Arc::new(
183 | UdpSocket::bind(addr)
184 | .await
185 | .with_context(|| format!("failed to bind to {}", addr))?,
186 | );
187 |
188 | // Create a shutdown broadcast channel
189 | let (tx, _) = broadcast::channel::<()>(10);
190 |
191 | // We don't have to worry about incoming requests when shutting down, because when we initiate shutdown, the loop was already terminated
192 | #[rustfmt::skip]
193 | tokio::select! {
194 | _ = serve(socket, router, &tx) => (),
195 | _ = signal::ctrl_c() => {
196 | log::warn!("Ctrl-C received, shutting down");
197 | sleep(Duration::from_millis(500)).await;
198 | // Error implies that there is no receiver/active worker, we are done
199 | if tx.send(()).is_ok() {
200 | while tx.receiver_count() != 0 {
201 | log::warn!("waiting 5 seconds for workers to exit...");
202 | sleep(Duration::from_secs(5)).await
203 | }
204 | }
205 | log::warn!("gracefully shut down!");
206 | }
207 | };
208 | Ok(())
209 | }
210 |
--------------------------------------------------------------------------------
/dcompass/src/parser.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use droute::builders::*;
17 | use log::LevelFilter;
18 | use serde::Deserialize;
19 | use std::net::SocketAddr;
20 |
21 | #[derive(Deserialize, Clone)]
22 | #[serde(rename_all = "lowercase")]
23 | #[serde(remote = "LevelFilter")]
24 | enum LevelFilterDef {
25 | Off,
26 | Error,
27 | Warn,
28 | Info,
29 | Debug,
30 | Trace,
31 | }
32 |
33 | #[derive(Deserialize)]
34 | #[serde(deny_unknown_fields)]
35 | pub struct Parsed {
36 | pub script: RuneScriptBuilder,
37 | // We are not using UpstreamsBuilder because flatten ruins error location.
38 | #[serde(flatten)]
39 | pub upstreams: UpstreamsBuilder,
40 | pub address: SocketAddr,
41 | #[serde(with = "LevelFilterDef")]
42 | pub verbosity: LevelFilter,
43 | }
44 |
--------------------------------------------------------------------------------
/dcompass/src/tests.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use super::init;
17 | use droute::errors::*;
18 |
19 | #[tokio::test]
20 | async fn check_default() {
21 | init(serde_yaml::from_str(include_str!("../../configs/default.json")).unwrap())
22 | .await
23 | .unwrap();
24 | }
25 |
26 | #[tokio::test]
27 | async fn check_success_ipcidr() {
28 | assert_eq!(true, true);
29 | init(serde_yaml::from_str(include_str!("../../configs/success_cidr.yaml")).unwrap())
30 | .await
31 | .unwrap();
32 | }
33 |
34 | #[cfg(all(feature = "geoip-maxmind", not(feature = "geoip-cn")))]
35 | #[tokio::test]
36 | async fn check_example_maxmind() {
37 | assert_eq!(
38 | init(serde_yaml::from_str(include_str!("../../configs/example.yaml")).unwrap())
39 | .await
40 | .is_ok(),
41 | true
42 | );
43 | }
44 |
45 | #[cfg(all(feature = "geoip-cn", not(feature = "geoip-maxmind")))]
46 | #[tokio::test]
47 | async fn check_example_cn() {
48 | assert_eq!(
49 | init(serde_yaml::from_str(include_str!("../../configs/example.yaml")).unwrap())
50 | .await
51 | .is_ok(),
52 | true
53 | );
54 | }
55 |
56 | #[tokio::test]
57 | async fn check_success_query_cache_mode() {
58 | init(serde_yaml::from_str(include_str!("../../configs/query_cache_policy.yaml")).unwrap())
59 | .await
60 | .unwrap();
61 | assert_eq!(
62 | init(serde_yaml::from_str(include_str!("../../configs/query_cache_policy.yaml")).unwrap())
63 | .await
64 | .is_ok(),
65 | true
66 | );
67 | }
68 |
69 | #[tokio::test(flavor = "multi_thread")]
70 | async fn check_success_geoip() {
71 | assert_eq!(
72 | init(serde_yaml::from_str(include_str!("../../configs/success_geoip.yaml")).unwrap())
73 | .await
74 | .is_ok(),
75 | true
76 | );
77 | }
78 |
79 | #[tokio::test]
80 | async fn check_success_header_yaml() {
81 | assert_eq!(
82 | init(serde_yaml::from_str(include_str!("../../configs/success_header.yaml")).unwrap())
83 | .await
84 | .is_ok(),
85 | true
86 | );
87 | }
88 |
89 | #[tokio::test]
90 | async fn check_fail_recursion() {
91 | match init(serde_yaml::from_str(include_str!("../../configs/fail_recursion.json")).unwrap())
92 | .await
93 | .err()
94 | .unwrap()
95 | {
96 | ScriptError::UpstreamError(UpstreamError::HybridRecursion(_)) => {}
97 | e => panic!("Not the right error type: {}", e),
98 | };
99 | }
100 |
--------------------------------------------------------------------------------
/dcompass/src/worker.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use anyhow::Result;
17 | use bytes::Bytes;
18 | use domain::base::Message;
19 | use droute::{builders::RuneScript, QueryContext, Router};
20 | use log::*;
21 | use std::{net::SocketAddr, sync::Arc};
22 | use tokio::net::UdpSocket;
23 |
24 | /// Handle a single incoming packet
25 | pub async fn worker(
26 | router: Arc>,
27 | socket: Arc,
28 | buf: Bytes,
29 | src: SocketAddr,
30 | ) -> Result<()> {
31 | socket
32 | .send_to(
33 | router
34 | .resolve(
35 | Message::from_octets(buf)?,
36 | Some(QueryContext { ip: src.ip() }),
37 | )
38 | .await?
39 | .as_slice(),
40 | src,
41 | )
42 | .await
43 | .unwrap_or_else(|e| {
44 | warn!("failed to send back response: {}", e);
45 | 0
46 | });
47 |
48 | info!("response completed. Sent back to {} successfully.", src);
49 |
50 | Ok(())
51 | }
52 |
--------------------------------------------------------------------------------
/dmatcher/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "dmatcher"
3 | version = "0.1.11"
4 | authors = ["Harry Ying "]
5 | edition = "2021"
6 | description = "A simple domain matching algorithm, intended to be fast."
7 | repository = "https://github.com/LEXUGE/dmatcher"
8 | license = "GPL-3.0"
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [dependencies]
13 | domain = {version = "^0.7", features = ["bytes"]}
14 | bytes = "^1"
15 |
16 | [dev-dependencies]
17 | criterion = "^0.4"
18 |
19 | [[bench]]
20 | name = "benchmark"
21 | harness = false
22 |
--------------------------------------------------------------------------------
/dmatcher/benches/benchmark.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use bytes::Bytes;
17 | use criterion::{criterion_group, criterion_main, Criterion};
18 | use dmatcher::domain::Domain;
19 | use domain::base::Dname;
20 | use std::{fs::File, io::Read, str::FromStr};
21 |
22 | fn bench_match(c: &mut Criterion) {
23 | let mut file = File::open("./benches/sample.txt").unwrap();
24 | let mut contents = String::new();
25 | let mut matcher = Domain::new();
26 | file.read_to_string(&mut contents).unwrap();
27 | let domains: Vec> = contents
28 | .split('\n')
29 | .filter(|&x| !x.is_empty())
30 | .map(|x| Dname::from_str(x).unwrap())
31 | .collect();
32 |
33 | let test = Dname::from_str("store.www.baidu.com").unwrap();
34 | matcher.insert_multi(&domains);
35 | c.bench_function("match", |b| {
36 | b.iter(|| assert_eq!(matcher.matches(&test), true))
37 | });
38 | }
39 |
40 | criterion_group!(benches, bench_match);
41 | criterion_main!(benches);
42 |
--------------------------------------------------------------------------------
/dmatcher/src/domain.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | //! This is a simple domain matching algorithm to match domains against a set of user-defined domain rules.
17 | //!
18 | //! Features:
19 | //!
20 | //! - Super fast (187 ns per match for a 73300+ domain rule set)
21 | //! - No dependencies
22 | //!
23 |
24 | use bytes::Bytes;
25 | use domain::base::{name::OwnedLabel, Dname};
26 | use std::{collections::HashMap, sync::Arc};
27 |
28 | #[derive(PartialEq, Clone)]
29 | struct LevelNode {
30 | next_lvs: HashMap, LevelNode>,
31 | }
32 |
33 | impl LevelNode {
34 | fn new() -> Self {
35 | Self {
36 | next_lvs: HashMap::new(),
37 | }
38 | }
39 | }
40 |
41 | /// Domain matcher algorithm
42 | #[derive(Clone)]
43 | pub struct Domain {
44 | root: LevelNode,
45 | }
46 |
47 | impl Default for Domain {
48 | fn default() -> Self {
49 | Self::new()
50 | }
51 | }
52 |
53 | impl Domain {
54 | /// Create a matcher.
55 | pub fn new() -> Self {
56 | Self {
57 | root: LevelNode::new(),
58 | }
59 | }
60 |
61 | /// Pass in a string containing `\n` and get all domains inserted.
62 | pub fn insert_multi(&mut self, domain: &[Dname]) {
63 | // This gets rid of empty substrings for stability reasons. See also https://github.com/LEXUGE/dcompass/issues/33.
64 | domain.iter().for_each(|d| self.insert(d));
65 | }
66 |
67 | /// Pass in a domain and insert it into the matcher.
68 | /// This ignores any line containing chars other than A-Z, a-z, 1-9, and -.
69 | /// See also: https://tools.ietf.org/html/rfc1035
70 | pub fn insert(&mut self, domain: &Dname) {
71 | let mut ptr = &mut self.root;
72 | for lv in domain.iter().rev() {
73 | ptr = ptr
74 | .next_lvs
75 | .entry(Arc::new(lv.to_owned()))
76 | .or_insert_with(LevelNode::new);
77 | }
78 | }
79 |
80 | /// Match the domain against inserted domain rules. If `apple.com` is inserted, then `www.apple.com` and `stores.www.apple.com` is considered as matched while `apple.cn` is not.
81 | pub fn matches(&self, domain: &Dname) -> bool {
82 | let mut ptr = &self.root;
83 | for lv in domain.iter().rev() {
84 | // We have reached the end of our rule set, breaking
85 | if ptr.next_lvs.is_empty() {
86 | break;
87 | }
88 | // If not empty...
89 | ptr = match ptr.next_lvs.get(&lv.to_owned()) {
90 | Some(v) => v,
91 | None => return false,
92 | };
93 | }
94 |
95 | // If we exhausted our rule set, then this is a match
96 | // e.g. apps.apple.com is a match for apple.com
97 | ptr.next_lvs.is_empty()
98 | // Otherwise:
99 | // If there are still rules left but we have reached the end of our test case, then it is not a match.
100 | // e.g. apple.com is not a match for apps.apple.com
101 | }
102 | }
103 |
104 | #[cfg(test)]
105 | mod tests {
106 | use super::Domain;
107 | use domain::base::Dname;
108 | use std::str::FromStr;
109 |
110 | macro_rules! dname {
111 | ($s:expr) => {
112 | Dname::from_str($s).unwrap()
113 | };
114 | }
115 |
116 | #[test]
117 | fn matches() {
118 | let mut matcher = Domain::new();
119 | matcher.insert(&dname!("apple.com"));
120 | matcher.insert(&dname!("apple.cn"));
121 | assert_eq!(matcher.matches(&dname!("store.apple.com")), true);
122 | assert_eq!(matcher.matches(&dname!("store.apple.com.")), true);
123 | assert_eq!(matcher.matches(&dname!("baidu.com")), false);
124 | }
125 |
126 | #[test]
127 | fn matches_2() {
128 | let mut matcher = Domain::new();
129 | matcher.insert(&dname!("tejia.taobao.com"));
130 | matcher.insert(&dname!("temai.m.taobao.com"));
131 | matcher.insert(&dname!("tui.taobao.com"));
132 | assert_eq!(matcher.matches(&dname!("a.tui.taobao.com")), true);
133 | assert_eq!(matcher.matches(&dname!("tejia.taobao.com")), true);
134 | assert_eq!(matcher.matches(&dname!("m.taobao.com")), false);
135 | assert_eq!(matcher.matches(&dname!("taobao.com")), false);
136 | }
137 |
138 | #[test]
139 | fn insert_multi() {
140 | let mut matcher = Domain::new();
141 | matcher.insert_multi(&[dname!("apple.com"), dname!("apple.cn")]);
142 | assert_eq!(matcher.matches(&dname!("store.apple.cn")), true);
143 | assert_eq!(matcher.matches(&dname!("store.apple.com.")), true);
144 | assert_eq!(matcher.matches(&dname!("baidu.com")), false);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/dmatcher/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | #![deny(missing_docs)]
17 | #![deny(unsafe_code)]
18 | //! This is a library providing a set of domain and IP address matching algorithms.
19 |
20 | pub mod domain;
21 |
--------------------------------------------------------------------------------
/droute/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "droute"
3 | version = "0.3.0-alpha.1"
4 | authors = ["Harry Ying "]
5 | edition = "2021"
6 | description = "Routing mechanism lib for dcompass the DNS server."
7 | repository = "https://github.com/LEXUGE/dcompass"
8 | license = "GPL-3.0"
9 | readme = "README.md"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 | [features]
13 | default = ["rune-scripting"]
14 | doh-rustls = ["reqwest/rustls-tls", "rustls", "webpki-roots"]
15 | doh-native-tls = ["reqwest/native-tls-vendored", "native-tls"]
16 | dot-rustls = ["tokio-rustls", "rustls", "webpki-roots"]
17 | dot-native-tls = ["native-tls", "tokio-native-tls"]
18 | geoip-cn = []
19 | geoip-maxmind = []
20 | rune-scripting = ["rune"]
21 |
22 | [dependencies]
23 | # DNS-implementation related dependencies
24 | domain = {version = "^0.7", features = ["bytes"]}
25 | bytes = "^1"
26 |
27 | # geoip
28 | maxminddb = "^0.23"
29 |
30 | # doh
31 | reqwest = { version = "0.11", features = ["socks"], default-features = false}
32 | # doh-native-tls
33 | # we used vendored flag to make sure when used with tokio-native-tls, feature flags would merge and we can happily vendor openssl!
34 | native-tls = { version = "0.2", features = ["vendored"], optional = true}
35 | # doh-rustls
36 | rustls = {version = "^0.20", features = ["dangerous_configuration"], optional = true }
37 | webpki-roots = { version = "^0.22", optional = true }
38 |
39 | #dot
40 | tokio-native-tls = { version = "^0.3", optional = true }
41 | tokio-rustls = { version = "^0.23", optional = true }
42 |
43 | # TCP keepalive doesn't help us pool our connections, sadly
44 | socket2 = {version = "^0.4", features = ["all"]}
45 |
46 | # Async-aware dependencies
47 | futures = "^0.3"
48 | tokio = { version = "^1", features = ["rt-multi-thread", "net", "fs", "macros", "io-util"]}
49 |
50 | # Scripting backends
51 | rune = { version = "^0.12", optional = true }
52 |
53 | # Logic-related dependencies
54 | hex = "^0.4"
55 | compact_str = { version = "^0.6", features = ["serde"]}
56 | cidr-utils = { version = "^0.5", git = "https://github.com/compassd/cidr-utils", rev = "c5f5c2ef167b4de9856764fd6b3b84e784b98db2" }
57 | once_cell = "^1.7"
58 | dmatcher = {version = "^0.1", path = "../dmatcher"}
59 | log = "^0.4"
60 | serde = { version = "^1.0", features = ["derive", "rc"] }
61 | # CLru supports async, but it is not published yet.
62 | clru = "^0.6"
63 | thiserror = "^1.0"
64 | async-trait = "^0.1"
65 | deadpool = { version = "^0.9", features = ["managed", "rt_tokio_1"] }
66 |
67 | # (de)compression libs (TODO: can we rewrite it to make it async?)
68 | niffler = "^2"
69 |
70 | # macro helper
71 | paste = "^1"
72 |
73 | # Disable ratelimit on 32-bit platforms
74 | # Related issue: https://github.com/metrics-rs/quanta/pull/55
75 | [target.'cfg(target_pointer_width = "64")'.dependencies]
76 | # governor = {version = "0.3.3-dev", git = "https://github.com/antifuchs/governor"}
77 | governor = "^0.5"
78 |
79 | [dev-dependencies]
80 | tokio-test = "^0.4"
81 | criterion = { version = "^0.4", features = ["async_tokio"]}
82 |
83 | [[bench]]
84 | name = "native_script"
85 | harness = false
86 |
87 | [[bench]]
88 | name = "rune_script"
89 | required-features = ["rune-scripting"]
90 | harness = false
91 |
92 | [package.metadata.cargo-all-features]
93 | # If your crate has a large number of optional dependencies, skip them for speed
94 | skip_optional_dependencies = true
95 |
96 | skip_feature_sets = [
97 | ["doh-rustls", "doh-native-tls"],
98 | ["dot-rustls", "dot-native-tls"],
99 | ["geoip-maxmind", "geoip-cn"],
100 | ]
101 |
--------------------------------------------------------------------------------
/droute/README.md:
--------------------------------------------------------------------------------
1 | # droute
2 | `droute` is a simple, robust, pluggable DNS routing library. It supports DoT, DoH, upstream-racing, and customized routing schemes with plugins. It is also the backend for `dcompass`, a robust DNS server.
3 |
4 | # Feature gates
5 | It has following feature gates to be enabled on need:
6 | - `doh`: enable DNS over HTTPS upstream support
7 | - `dot`: enable DNS over TLS upstream support
8 | - `serde-cfg`: enable serde-aided structure serialization/deserialization
9 |
--------------------------------------------------------------------------------
/droute/benches/native_script.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use bytes::{Bytes, BytesMut};
17 | use criterion::{criterion_group, criterion_main, Criterion};
18 | use domain::{
19 | base::{Dname, Message, MessageBuilder, Rtype},
20 | rdata::A,
21 | };
22 | use droute::{
23 | builders::*, errors::*, mock::Server, AsyncTryInto, QueryContext, Router, ScriptBackend,
24 | ScriptBuilder, Upstreams,
25 | };
26 | use once_cell::sync::Lazy;
27 | use std::str::FromStr;
28 | use tokio::net::UdpSocket;
29 |
30 | // It is fine for us to have the same ID, because each query is sent from different source addr, meaning there is no collision on that
31 | static DUMMY_MSG: Lazy> = Lazy::new(|| {
32 | let name = Dname::::from_str("cloudflare-dns.com").unwrap();
33 | let mut builder = MessageBuilder::from_target(BytesMut::with_capacity(1232)).unwrap();
34 | let header = builder.header_mut();
35 | header.set_id(0);
36 | header.set_qr(true);
37 | let mut builder = builder.question();
38 | builder.push((&name, Rtype::A)).unwrap();
39 | let mut builder = builder.answer();
40 | builder
41 | .push((&name, 10, A::from_octets(1, 1, 1, 1)))
42 | .unwrap();
43 | Message::from_octets(BytesMut::from(builder.as_slice())).unwrap()
44 | });
45 |
46 | static QUERY: Lazy> = Lazy::new(|| {
47 | let name = Dname::::from_str("cloudflare-dns.com").unwrap();
48 | let mut builder = MessageBuilder::from_target(BytesMut::with_capacity(1232)).unwrap();
49 | builder.header_mut().set_id(0);
50 | let mut builder = builder.question();
51 | builder.push((&name, Rtype::A)).unwrap();
52 | builder.into_message()
53 | });
54 |
55 | async fn create_router(script_builder: impl ScriptBuilder) -> Router {
56 | RouterBuilder::new(
57 | script_builder,
58 | UpstreamsBuilder::new(4096).unwrap().add_upstream(
59 | "mock",
60 | UpstreamBuilder::Udp(UdpBuilder {
61 | addr: "127.0.0.1:53533".parse().unwrap(),
62 | max_pool_size: 256,
63 | timeout: 1,
64 | ratelimit: None,
65 | }),
66 | ),
67 | )
68 | .async_try_into()
69 | .await
70 | .unwrap()
71 | }
72 |
73 | fn bench_resolve(c: &mut Criterion) {
74 | let rt = tokio::runtime::Builder::new_multi_thread()
75 | .enable_all()
76 | .build()
77 | .unwrap();
78 |
79 | let socket = rt.block_on(UdpSocket::bind(&"127.0.0.1:53533")).unwrap();
80 | let server = Server::new(socket, vec![0; 1024], None);
81 | rt.spawn(server.run(DUMMY_MSG.clone()));
82 |
83 | let router = rt.block_on(create_router(NativeScriptBuilder::new(
84 | resolve_script_no_cache,
85 | )));
86 | let cached_router = rt.block_on(create_router(NativeScriptBuilder::new(resolve_script)));
87 |
88 | c.bench_function("native_non_cache_resolve", |b| {
89 | b.to_async(&rt).iter(|| async {
90 | assert_eq!(
91 | router
92 | .resolve(QUERY.clone(), None)
93 | .await
94 | .unwrap()
95 | .into_octets(),
96 | DUMMY_MSG.clone().into_octets()
97 | );
98 | })
99 | });
100 |
101 | c.bench_function("native_cached_resolve", |b| {
102 | b.to_async(&rt).iter(|| async {
103 | assert_eq!(
104 | cached_router
105 | .resolve(QUERY.clone(), None)
106 | .await
107 | .unwrap()
108 | .into_octets(),
109 | DUMMY_MSG.clone().into_octets()
110 | );
111 | })
112 | });
113 | }
114 |
115 | async fn resolve_script(
116 | upstreams: Upstreams,
117 | query: Message,
118 | _ctx: Option,
119 | ) -> Result, ScriptError> {
120 | Ok(upstreams
121 | .send(&"mock".into(), &droute::CacheMode::Standard, &query)
122 | .await?)
123 | }
124 |
125 | async fn resolve_script_no_cache(
126 | upstreams: Upstreams,
127 | query: Message,
128 | _ctx: Option,
129 | ) -> Result, ScriptError> {
130 | Ok(upstreams
131 | .send(&"mock".into(), &droute::CacheMode::Disabled, &query)
132 | .await?)
133 | }
134 |
135 | criterion_group!(benches, bench_resolve);
136 | criterion_main!(benches);
137 |
--------------------------------------------------------------------------------
/droute/benches/rune_script.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use bytes::{Bytes, BytesMut};
17 | use criterion::{criterion_group, criterion_main, Criterion};
18 | use domain::{
19 | base::{Dname, Message, MessageBuilder, Rtype},
20 | rdata::A,
21 | };
22 | use droute::{builders::*, mock::Server, AsyncTryInto, Router, ScriptBackend, ScriptBuilder};
23 | use once_cell::sync::Lazy;
24 | use std::str::FromStr;
25 | use tokio::net::UdpSocket;
26 |
27 | // It is fine for us to have the same ID, because each query is sent from different source addr, meaning there is no collision on that
28 | static DUMMY_MSG: Lazy> = Lazy::new(|| {
29 | let name = Dname::::from_str("cloudflare-dns.com").unwrap();
30 | let mut builder = MessageBuilder::from_target(BytesMut::with_capacity(1232)).unwrap();
31 | let header = builder.header_mut();
32 | header.set_id(0);
33 | header.set_qr(true);
34 | let mut builder = builder.question();
35 | builder.push((&name, Rtype::A)).unwrap();
36 | let mut builder = builder.answer();
37 | builder
38 | .push((&name, 10, A::from_octets(1, 1, 1, 1)))
39 | .unwrap();
40 | Message::from_octets(BytesMut::from(builder.as_slice())).unwrap()
41 | });
42 |
43 | static QUERY: Lazy> = Lazy::new(|| {
44 | let name = Dname::::from_str("cloudflare-dns.com").unwrap();
45 | let mut builder = MessageBuilder::from_target(BytesMut::with_capacity(1232)).unwrap();
46 | builder.header_mut().set_id(0);
47 | let mut builder = builder.question();
48 | builder.push((&name, Rtype::A)).unwrap();
49 | builder.into_message()
50 | });
51 |
52 | async fn create_router(script_builder: impl ScriptBuilder) -> Router {
53 | RouterBuilder::new(
54 | script_builder,
55 | UpstreamsBuilder::new(4096).unwrap().add_upstream(
56 | "mock",
57 | UpstreamBuilder::Udp(UdpBuilder {
58 | addr: "127.0.0.1:53533".parse().unwrap(),
59 | max_pool_size: 256,
60 | timeout: 1,
61 | ratelimit: None,
62 | }),
63 | ),
64 | )
65 | .async_try_into()
66 | .await
67 | .unwrap()
68 | }
69 |
70 | fn bench_resolve(c: &mut Criterion) {
71 | let rt = tokio::runtime::Builder::new_multi_thread()
72 | .enable_all()
73 | .build()
74 | .unwrap();
75 |
76 | let socket = rt.block_on(UdpSocket::bind(&"127.0.0.1:53533")).unwrap();
77 | let server = Server::new(socket, vec![0; 1024], None);
78 | rt.spawn(server.run(DUMMY_MSG.clone()));
79 |
80 | let router = rt.block_on(create_router(RuneScriptBuilder::new(
81 | r#"pub async fn route(upstreams, inited, ctx, query) { upstreams.send("mock", CacheMode::Disabled, query).await } pub async fn init() { Ok(#{}) }"#,
82 | )));
83 | let cached_router = rt.block_on(create_router(RuneScriptBuilder::new(
84 | r#"pub async fn route(upstreams, inited, ctx, query) { upstreams.send_default("mock", query).await } pub async fn init() { Ok(#{}) }"#,
85 | )));
86 |
87 | c.bench_function("rune_non_cache_resolve", |b| {
88 | b.to_async(&rt).iter(|| async {
89 | assert_eq!(
90 | router
91 | .resolve(QUERY.clone(), None)
92 | .await
93 | .unwrap()
94 | .into_octets(),
95 | DUMMY_MSG.clone().into_octets()
96 | );
97 | })
98 | });
99 |
100 | c.bench_function("rune_cached_resolve", |b| {
101 | b.to_async(&rt).iter(|| async {
102 | assert_eq!(
103 | cached_router
104 | .resolve(QUERY.clone(), None)
105 | .await
106 | .unwrap()
107 | .into_octets(),
108 | DUMMY_MSG.clone().into_octets()
109 | );
110 | })
111 | });
112 | }
113 |
114 | criterion_group!(benches, bench_resolve);
115 | criterion_main!(benches);
116 |
--------------------------------------------------------------------------------
/droute/src/cache.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2020 LEXUGE
2 | //
3 | // This program is free software: you can redistribute it and/or modify
4 | // it under the terms of the GNU General Public License as published by
5 | // the Free Software Foundation, either version 3 of the License, or
6 | // (at your option) any later version.
7 | //
8 | // This program is distributed in the hope that it will be useful,
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | // GNU General Public License for more details.
12 | //
13 | // You should have received a copy of the GNU General Public License
14 | // along with this program. If not, see .
15 |
16 | use self::RecordStatus::*;
17 | use crate::{Label, MAX_TTL};
18 | use bytes::Bytes;
19 | use clru::CLruCache;
20 | use domain::base::{name::ToDname, Message};
21 | use log::*;
22 | use std::{
23 | borrow::Borrow,
24 | hash::{Hash, Hasher},
25 | num::NonZeroUsize,
26 | sync::{Arc, Mutex},
27 | time::{Duration, Instant},
28 | };
29 |
30 | // Code to use (&A, &B) for accessing HashMap, clipped from https://stackoverflow.com/questions/45786717/how-to-implement-hashmap-with-two-keys/45795699#45795699.
31 | trait KeyPair {
32 | /// Obtains the first element of the pair.
33 | fn a(&self) -> &A;
34 | /// Obtains the second element of the pair.
35 | fn b(&self) -> &B;
36 | }
37 |
38 | impl<'a, A: ?Sized, B: ?Sized, C, D> Borrow + 'a> for (C, D)
39 | where
40 | A: Eq + Hash + 'a,
41 | B: Eq + Hash + 'a,
42 | C: Borrow + 'a,
43 | D: Borrow + 'a,
44 | {
45 | fn borrow(&self) -> &(dyn KeyPair + 'a) {
46 | self
47 | }
48 | }
49 |
50 | impl Hash for (dyn KeyPair + '_) {
51 | fn hash(&self, state: &mut H) {
52 | self.a().hash(state);
53 | self.b().hash(state);
54 | }
55 | }
56 |
57 | impl PartialEq for (dyn KeyPair + '_) {
58 | fn eq(&self, other: &Self) -> bool {
59 | self.a() == other.a() && self.b() == other.b()
60 | }
61 | }
62 |
63 | impl Eq for (dyn KeyPair + '_) {}
64 |
65 | impl KeyPair for (C, D)
66 | where
67 | C: Borrow,
68 | D: Borrow,
69 | {
70 | fn a(&self) -> &A {
71 | self.0.borrow()
72 | }
73 | fn b(&self) -> &B {
74 | self.1.borrow()
75 | }
76 | }
77 |
78 | #[derive(Clone)]
79 | pub struct CacheRecord {
80 | created_instant: Instant,
81 | content: T,
82 | ttl: Duration,
83 | }
84 |
85 | impl CacheRecord {
86 | pub fn new(content: T, ttl: Duration) -> Self {
87 | Self {
88 | created_instant: Instant::now(),
89 | content,
90 | ttl,
91 | }
92 | }
93 |
94 | pub fn get(&self) -> T {
95 | self.content.clone()
96 | }
97 |
98 | pub fn validate(&self) -> bool {
99 | Instant::now().saturating_duration_since(self.created_instant) <= self.ttl
100 | }
101 | }
102 |
103 | pub enum RecordStatus {
104 | Alive(T),
105 | Expired(T),
106 | }
107 |
108 | // A LRU cache for responses
109 | #[derive(Clone)]
110 | pub struct RespCache {
111 | #[allow(clippy::type_complexity)]
112 | cache: Arc>>>>,
113 | }
114 |
115 | impl RespCache {
116 | pub fn new(size: NonZeroUsize) -> Self {
117 | Self {
118 | cache: Arc::new(Mutex::new(CLruCache::new(size))),
119 | }
120 | }
121 |
122 | pub fn put(&self, tag: Label, query: &Message, msg: Message) {
123 | if msg.no_error() {
124 | // We are assured that it should parse and exist
125 | let ttl = Duration::from_secs(u64::from(
126 | query
127 | .answer()
128 | .ok()
129 | .and_then(|records| {
130 | records
131 | .filter(|r| r.is_ok())
132 | .map(|r| r.unwrap().ttl())
133 | .min()
134 | })
135 | .unwrap_or(MAX_TTL),
136 | ));
137 | self.cache.lock().unwrap().put(
138 | // We discard the first two bytes which are the places for ID
139 | (tag, query.as_octets().slice(2..)),
140 | // Clone should be cheap here
141 | CacheRecord::new(msg, ttl),
142 | );
143 | } else {
144 | info!("response errored, not caching erroneous upstream response.");
145 | };
146 | }
147 |
148 | pub fn get(&self, tag: &Label, msg: &Message) -> Option>> {
149 | let question = msg.first_question().unwrap();
150 | let qname = question.qname().to_bytes();
151 |
152 | match self
153 | .cache
154 | .lock()
155 | .unwrap()
156 | .get(&(tag, msg.as_octets().slice(2..)) as &dyn KeyPair