├── .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 | ![自动构建](https://github.com/LEXUGE/dcompass/workflows/Build%20dcompass%20on%20various%20targets/badge.svg) 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 | ![Automated build](https://github.com/LEXUGE/dcompass/workflows/Build%20dcompass%20on%20various%20targets/badge.svg) 4 | [![Join telegram channel](https://badges.aleen42.com/src/telegram.svg)](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) 157 | { 158 | Some(r) => { 159 | // Get record only once. 160 | if r.validate() { 161 | info!("cache hit for {}", qname); 162 | Some(Alive(r.get())) 163 | } else { 164 | info!("TTL passed for {}, returning expired record.", qname); 165 | Some(Expired(r.get())) 166 | } 167 | } 168 | Option::None => Option::None, 169 | } 170 | } 171 | } 172 | 173 | // Expire every hour 174 | // const ECS_CACHE_TTL: Duration = Duration::from_secs(60 * 60); 175 | 176 | // A LRU cache mapping local address to EDNS Client Subnet external IP addr 177 | // #[derive(Clone)] 178 | // pub struct EcsCache { 179 | // cache: Arc>>>, 180 | // } 181 | // 182 | // impl EcsCache { 183 | // pub fn new() -> Self { 184 | // Self { 185 | // cache: Arc::new(Mutex::new(None)), 186 | // } 187 | // } 188 | // 189 | // pub fn put(&self, external_ip: IpAddr) { 190 | // *self.cache.lock().unwrap() = Some(CacheRecord::new(external_ip, ECS_CACHE_TTL)); 191 | // } 192 | // 193 | // pub fn get(&self, ip: &IpAddr) -> Option> { 194 | // match &mut *self.cache.lock().unwrap() { 195 | // Some(r) => { 196 | // // Get record only once. 197 | // if r.validate() { 198 | // info!("ECS external IP cache hit for private IP {}", ip); 199 | // Some(Alive(r.get())) 200 | // } else { 201 | // info!( 202 | // "TTL passed for private IP {}, returning expired record.", 203 | // ip 204 | // ); 205 | // Some(Expired(r.get())) 206 | // } 207 | // } 208 | // Option::None => Option::None, 209 | // } 210 | // } 211 | // } 212 | // 213 | // impl Default for EcsCache { 214 | // fn default() -> Self { 215 | // Self::new() 216 | // } 217 | // } 218 | -------------------------------------------------------------------------------- /droute/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 | // The logging level usage is governed by following convention: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels 17 | // Trace - Only when I would be "tracing" the code and trying to find one part of a function specifically. 18 | // Debug - Information that is diagnostically helpful to people more than just developers (IT, sysadmins, etc.). 19 | // Info - Generally useful information to log (service start/stop, configuration assumptions, etc). Info I want to always have available but usually don't care about under normal circumstances. This is my out-of-the-box config level. 20 | // Warn - Anything that can potentially cause application oddities, but for which I am automatically recovering. (Such as switching from a primary to backup server, retrying an operation, missing secondary data, etc.) 21 | // Error - Any error which is fatal to the operation, but not the service or application (can't open a required file, missing data, etc.). These errors will force user (administrator, or direct user) intervention. These are usually reserved (in my apps) for incorrect connection strings, missing services, etc. 22 | // Fatal - Any error that is forcing a shutdown of the service or application to prevent data loss (or further data loss). I reserve these only for the most heinous errors and situations where there is guaranteed to have been data corruption or loss. 23 | 24 | #![deny(missing_docs)] 25 | #![deny(unsafe_code)] 26 | // Documentation 27 | //! This is the core library for dcompass. It implements configuration parsing scheme, DNS query routing rules, and upstream managements. 28 | pub(crate) mod cache; 29 | #[doc(hidden)] 30 | pub mod mock; 31 | mod router; 32 | 33 | #[cfg(all(feature = "doh-native-tls", feature = "doh-rustls"))] 34 | compile_error!("You should only choose one TLS backend for DNS over HTTPS implementation"); 35 | 36 | #[cfg(all(feature = "dot-native-tls", feature = "dot-rustls"))] 37 | compile_error!("You should only choose one TLS backend for DNS over TLS implementation"); 38 | 39 | use async_trait::async_trait; 40 | use compact_str::CompactString; 41 | 42 | /// All the builders 43 | // API guideline: when we are exporting, make sure we aggregate builders by pub using them in parent builder(s) modules. 44 | pub mod builders { 45 | pub use super::router::{script::builders::*, upstreams::builder::*, RouterBuilder}; 46 | } 47 | 48 | /// A collection of all errors in `droute` 49 | pub mod errors { 50 | pub use super::router::{ 51 | script::{MessageError, ScriptError}, 52 | upstreams::error::UpstreamError, 53 | }; 54 | } 55 | 56 | // All the major components 57 | pub use self::router::{ 58 | script::{native::NativeScript, utils, QueryContext, ScriptBackend, ScriptBuilder}, 59 | upstreams::{CacheMode, Upstream, Upstreams}, 60 | Router, 61 | }; 62 | 63 | // Maximum TTL as defined in https://tools.ietf.org/html/rfc2181, 2147483647 64 | // Setting this to a value of 1 day, in seconds 65 | const MAX_TTL: u32 = 86400_u32; 66 | 67 | // 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." 68 | // To better align our memory, we take 1024 here. 69 | const MAX_LEN: usize = 1024_usize; 70 | 71 | /// The type used for tag names in upstreams and routing script. 72 | pub type Label = CompactString; 73 | 74 | /// Async TryInto 75 | #[async_trait(?Send)] 76 | pub trait AsyncTryInto { 77 | /// Possible failures on conversion 78 | type Error; 79 | 80 | /// Convert `self` into `T` 81 | async fn async_try_into(self) -> Result; 82 | } 83 | 84 | /// A object that can be validated 85 | pub trait Validatable { 86 | /// The possible errors from the validation. 87 | type Error; 88 | /// Validate oneself. 89 | /// `used`: some of the tags used by other parts, which should be existed. 90 | fn validate(&self, used: Option<&Vec
().unwrap(); 117 | 118 | // DNS Primitives 119 | m.ty::().unwrap(); 120 | create_str_kit!(Dname, domain::base::Dname, m); 121 | 122 | m.ty::().unwrap(); 123 | // Rcode doesn't implment FromStr 124 | m.inst_fn("to_str", |this: &Rcode| this.0.to_string()) 125 | .unwrap(); 126 | 127 | m.inst_fn(Protocol::EQ, |this: &Rcode, other: &str| { 128 | this.0.to_string() == other 129 | }) 130 | .unwrap(); 131 | 132 | m.ty::().unwrap(); 133 | // OptRcode doesn't implment FromStr 134 | m.inst_fn("to_str", |this: &OptRcode| this.0.to_string()) 135 | .unwrap(); 136 | 137 | m.inst_fn(Protocol::EQ, |this: &OptRcode, other: &str| { 138 | this.0.to_string() == other 139 | }) 140 | .unwrap(); 141 | 142 | m.ty::().unwrap(); 143 | create_str_kit!(Opcode, domain::base::iana::opcode::Opcode, m); 144 | 145 | m.ty::().unwrap(); 146 | create_str_kit!(Rtype, domain::base::iana::rtype::Rtype, m); 147 | 148 | m.ty::().unwrap(); 149 | create_str_kit!(Class, domain::base::iana::class::Class, m); 150 | 151 | m.ty::().unwrap(); 152 | 153 | // Record Primitives 154 | m.ty::().unwrap(); 155 | m.ty::().unwrap(); 156 | m.ty::().unwrap(); 157 | m.ty::().unwrap(); 158 | m.ty::().unwrap(); 159 | m.ty::().unwrap(); 160 | m.ty::().unwrap(); 161 | m.ty::().unwrap(); 162 | m.ty::().unwrap(); 163 | m.ty::().unwrap(); 164 | 165 | // Iterators 166 | m.ty::().unwrap(); 167 | m.inst_fn(Protocol::INTO_ITER, OptRecordsIter::into_iterator) 168 | .unwrap(); 169 | m.inst_fn("into_iter", OptRecordsIter::into_iterator) 170 | .unwrap(); 171 | 172 | m.ty::().unwrap(); 173 | m.inst_fn(Protocol::INTO_ITER, DnsRecordsIter::into_iterator) 174 | .unwrap(); 175 | m.inst_fn("into_iter", DnsRecordsIter::into_iterator) 176 | .unwrap(); 177 | 178 | // Other types 179 | m.ty::().unwrap(); 180 | create_str_kit!(IpAddr, std::net::IpAddr, m); 181 | 182 | m 183 | }); 184 | -------------------------------------------------------------------------------- /droute/src/router/script/rune_scripting/utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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::types::*; 17 | use crate::{ 18 | errors::ScriptError, 19 | utils::{blackhole, Domain, GeoIp, IpCidr}, 20 | }; 21 | use once_cell::sync::Lazy; 22 | use rune::Module; 23 | use std::sync::Arc; 24 | 25 | #[derive(rune::Any, Clone)] 26 | pub enum Utils { 27 | #[rune(constructor)] 28 | Domain(#[rune(get)] SealedDomain), 29 | #[rune(constructor)] 30 | GeoIp(#[rune(get)] SealedGeoIp), 31 | #[rune(constructor)] 32 | IpCidr(#[rune(get)] SealedIpCidr), 33 | } 34 | 35 | #[derive(rune::Any, Clone)] 36 | pub struct SealedDomain(Arc); 37 | 38 | #[derive(rune::Any, Clone)] 39 | pub struct SealedGeoIp(Arc); 40 | 41 | #[derive(rune::Any, Clone)] 42 | pub struct SealedIpCidr(Arc); 43 | 44 | pub static UTILS_MODULE: Lazy = Lazy::new(|| { 45 | let mut m = Module::new(); 46 | 47 | m.ty::().unwrap(); 48 | 49 | // Blackhole 50 | { 51 | m.function( 52 | &["blackhole"], 53 | |msg: &Message| -> Result { Ok(blackhole(&msg.into())?.into()) }, 54 | ) 55 | .unwrap(); 56 | } 57 | 58 | // Domain list 59 | { 60 | m.ty::().unwrap(); 61 | m.ty::().unwrap(); 62 | 63 | m.function(&["Domain", "new"], Domain::new).unwrap(); 64 | m.inst_fn( 65 | "add_qname", 66 | |mut domain: Domain, qname: &str| -> Result { 67 | domain.add_qname(qname)?; 68 | Ok(domain) 69 | }, 70 | ) 71 | .unwrap(); 72 | m.inst_fn( 73 | "add_file", 74 | |mut domain: Domain, path: &str| -> Result { 75 | domain.add_file(path)?; 76 | Ok(domain) 77 | }, 78 | ) 79 | .unwrap(); 80 | 81 | m.inst_fn("seal", |domain: Domain| -> SealedDomain { 82 | SealedDomain(Arc::new(domain)) 83 | }) 84 | .unwrap(); 85 | 86 | m.inst_fn("contains", |domain: &SealedDomain, qname: &Dname| -> bool { 87 | domain.0.contains(&qname.into()) 88 | }) 89 | .unwrap(); 90 | } 91 | 92 | // GeoIP 93 | { 94 | m.ty::().unwrap(); 95 | m.ty::().unwrap(); 96 | 97 | m.function( 98 | &["GeoIp", "create_default"], 99 | || -> Result { 100 | Ok(SealedGeoIp(Arc::new(GeoIp::create_default()?))) 101 | }, 102 | ) 103 | .unwrap(); 104 | 105 | async fn geoip_from_path(path: &str) -> Result { 106 | Ok(SealedGeoIp(Arc::new(GeoIp::from_path(path).await?))) 107 | } 108 | 109 | m.async_function(&["GeoIp", "from_path"], geoip_from_path) 110 | .unwrap(); 111 | 112 | m.inst_fn( 113 | "contains", 114 | |geoip: &SealedGeoIp, ip: &IpAddr, code: &str| -> bool { 115 | geoip.0.contains(ip.into(), code) 116 | }, 117 | ) 118 | .unwrap(); 119 | } 120 | 121 | // IP CIDR 122 | { 123 | m.ty::().unwrap(); 124 | m.ty::().unwrap(); 125 | 126 | m.function(&["IpCidr", "new"], IpCidr::new).unwrap(); 127 | m.inst_fn( 128 | "add_file", 129 | |mut ipcidr: IpCidr, path: &str| -> Result { 130 | ipcidr.add_file(path)?; 131 | Ok(ipcidr) 132 | }, 133 | ) 134 | .unwrap(); 135 | 136 | m.inst_fn("seal", |cidr: IpCidr| -> SealedIpCidr { 137 | SealedIpCidr(Arc::new(cidr)) 138 | }) 139 | .unwrap(); 140 | 141 | m.inst_fn("contains", |ipcidr: &SealedIpCidr, ip: &IpAddr| -> bool { 142 | ipcidr.0.contains(ip.into()) 143 | }) 144 | .unwrap(); 145 | } 146 | 147 | m 148 | }); 149 | -------------------------------------------------------------------------------- /droute/src/router/script/utils/blackhole.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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::Result; 17 | use crate::MAX_TTL; 18 | use bytes::{Bytes, BytesMut}; 19 | use domain::{ 20 | base::{Dname, Message, MessageBuilder}, 21 | rdata::Soa, 22 | }; 23 | use once_cell::sync::Lazy; 24 | use std::str::FromStr; 25 | 26 | // Data from smartdns. https://github.com/pymumu/smartdns/blob/42b3e98b2a3ca90ea548f8cb5ed19a3da6011b74/src/dns_server.c#L651 27 | static SOA_RDATA: Lazy<(Dname, u32, Soa>)> = Lazy::new(|| { 28 | ( 29 | Dname::root_bytes(), 30 | MAX_TTL, 31 | Soa::new( 32 | Dname::from_str("a.gtld-servers.net").unwrap(), 33 | Dname::from_str("nstld.verisign-grs.com").unwrap(), 34 | 1800.into(), 35 | 1800, 36 | 900, 37 | 604800, 38 | 86400, 39 | ), 40 | ) 41 | }); 42 | 43 | /// Create a message that stops the requestor to send the query again. 44 | pub fn blackhole(query: &Message) -> Result> { 45 | // Is 50 a good number? 46 | let mut builder = MessageBuilder::from_target(BytesMut::with_capacity(50))? 47 | .start_answer(query, domain::base::iana::Rcode::NoError)? 48 | .additional(); 49 | 50 | builder.push(SOA_RDATA.clone())?; 51 | 52 | Ok(builder.into_message()) 53 | } 54 | -------------------------------------------------------------------------------- /droute/src/router/script/utils/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 | use super::Result; 17 | use bytes::Bytes; 18 | use dmatcher::domain::Domain as DomainAlg; 19 | use domain::base::{name::FromStrError, Dname}; 20 | use std::{path::PathBuf, str::FromStr}; 21 | 22 | /// The domain matcher 23 | #[derive(Clone)] 24 | #[cfg_attr(feature = "rune-scripting", derive(rune::Any))] 25 | pub struct Domain(DomainAlg); 26 | 27 | fn into_dnames(list: &str) -> std::result::Result>, FromStrError> { 28 | list.split('\n') 29 | .filter(|&x| { 30 | (!x.is_empty()) 31 | && (x.chars().all(|c| { 32 | char::is_ascii_alphabetic(&c) 33 | | char::is_ascii_digit(&c) 34 | | (c == '-') 35 | | (c == '.') 36 | })) 37 | }) 38 | .map(Dname::from_str) 39 | .collect() 40 | } 41 | 42 | impl Default for Domain { 43 | fn default() -> Self { 44 | Self::new() 45 | } 46 | } 47 | 48 | impl Domain { 49 | /// Create an empty `domain` matcher 50 | pub fn new() -> Self { 51 | Self(DomainAlg::new()) 52 | } 53 | 54 | /// Add a question name to the domain matcher's list 55 | pub fn add_qname(&mut self, s: impl AsRef) -> Result<()> { 56 | self.0.insert_multi(&into_dnames(s.as_ref())?); 57 | Ok(()) 58 | } 59 | 60 | /// Add all question names in a file to the domain matcher's list 61 | pub fn add_file(&mut self, path: impl AsRef) -> Result<()> { 62 | // from_str is Infallible 63 | let (mut file, _) = niffler::from_path(PathBuf::from_str(path.as_ref()).unwrap())?; 64 | let mut data = String::new(); 65 | file.read_to_string(&mut data)?; 66 | self.0.insert_multi(&into_dnames(&data)?); 67 | Ok(()) 68 | } 69 | 70 | /// Check if the question name matches any in the matcher. 71 | pub fn contains(&self, qname: &Dname) -> bool { 72 | self.0.matches(qname) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /droute/src/router/script/utils/geoip.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 super::Result; 17 | #[cfg(not(any(feature = "geoip-cn", feature = "geoip-maxmind")))] 18 | use super::UtilsError; 19 | use log::info; 20 | use maxminddb::{geoip2::Country, Reader}; 21 | use std::{net::IpAddr, path::PathBuf, str::FromStr, sync::Arc}; 22 | 23 | /// A matcher that matches if IP address in the record of the first A/AAAA response is in the list of countries. 24 | #[derive(Clone)] 25 | #[cfg_attr(feature = "rune-scripting", derive(rune::Any))] 26 | pub struct GeoIp { 27 | db: Arc>>, 28 | } 29 | 30 | // If both geoip-maxmind and geoip-cn are enabled, geoip-maxmind will be used 31 | fn get_builtin_db() -> Result> { 32 | #[cfg(feature = "geoip-maxmind")] 33 | return Ok(include_bytes!("../../../../../data/full.mmdb").to_vec()); 34 | #[cfg(all(feature = "geoip-cn", not(feature = "geoip-maxmind")))] 35 | return Ok(include_bytes!("../../../../../data/cn.mmdb").to_vec()); 36 | #[cfg(not(any(feature = "geoip-cn", feature = "geoip-maxmind")))] 37 | Err(UtilsError::NoBuiltInDb) 38 | } 39 | 40 | impl GeoIp { 41 | /// Create a geoip matcher from the database file with the given path 42 | pub async fn from_path(path: impl AsRef) -> Result { 43 | // Per std documentation, this is infallible 44 | let buf: Vec = tokio::fs::read(PathBuf::from_str(path.as_ref()).unwrap()).await?; 45 | Ok(Self { 46 | db: Arc::new(Reader::from_source(buf)?), 47 | }) 48 | } 49 | 50 | /// Create a geoip matcher from the database file with the given buffer 51 | #[cfg(test)] 52 | pub fn from_buf(buf: Vec) -> Result { 53 | Ok(Self { 54 | db: Arc::new(Reader::from_source(buf)?), 55 | }) 56 | } 57 | 58 | /// Create a geoip matcher from the buffer 59 | pub fn create_default() -> Result { 60 | let buf = get_builtin_db()?; 61 | Ok(Self { 62 | db: Arc::new(Reader::from_source(buf)?), 63 | }) 64 | } 65 | 66 | /// Whether the given country code contains the given IP address 67 | pub fn contains(&self, ip: IpAddr, code: &str) -> bool { 68 | let r = if let Ok(r) = self.db.lookup::(ip) { 69 | r 70 | } else { 71 | return false; 72 | }; 73 | 74 | r.country 75 | .and_then(|c| { 76 | c.iso_code.map(|n| { 77 | info!("IP `{}` has ISO country code `{}`", ip, n); 78 | n == code 79 | }) 80 | }) 81 | .unwrap_or(false) 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::GeoIp; 88 | use once_cell::sync::Lazy; 89 | 90 | // Starting from droute's crate root 91 | static DB: Lazy> = 92 | Lazy::new(|| include_bytes!("../../../../../data/full.mmdb").to_vec()); 93 | 94 | #[tokio::test] 95 | async fn builtin_db_not_china() { 96 | assert_eq!( 97 | GeoIp::from_buf(DB.clone()) 98 | .unwrap() 99 | .contains("1.1.1.1".parse().unwrap(), "CN"), 100 | false 101 | ) 102 | } 103 | 104 | #[tokio::test] 105 | async fn not_china() { 106 | assert_eq!( 107 | GeoIp::from_buf(DB.clone()) 108 | .unwrap() 109 | .contains("1.1.1.1".parse().unwrap(), "CN"), 110 | false 111 | ) 112 | } 113 | 114 | #[tokio::test] 115 | async fn mixed() { 116 | let geoip = GeoIp::from_buf(DB.clone()).unwrap(); 117 | assert_eq!(geoip.contains("180.101.49.12".parse().unwrap(), "CN"), true); 118 | assert_eq!(geoip.contains("69.162.81.155".parse().unwrap(), "US"), true) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /droute/src/router/script/utils/ipcidr.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use cidr_utils::{ 3 | cidr::{IpCidr as Cidr, IpCidrError}, 4 | utils::IpCidrCombiner as CidrCombiner, 5 | }; 6 | use std::{net::IpAddr, path::Path}; 7 | 8 | /// IP CIDR matcher. 9 | #[derive(Clone)] 10 | #[cfg_attr(feature = "rune-scripting", derive(rune::Any))] 11 | pub struct IpCidr { 12 | matcher: CidrCombiner, 13 | } 14 | 15 | impl IpCidr { 16 | /// Create a new empty `IpCidr` matcher 17 | pub fn new() -> Self { 18 | Self { 19 | matcher: CidrCombiner::new(), 20 | } 21 | } 22 | 23 | /// Add IP CIDRs from a files where each IP CIDR is seperated from one another by `\n`. 24 | pub fn add_file(&mut self, path: impl AsRef) -> Result<()> { 25 | let (mut file, _) = niffler::from_path(path)?; 26 | let mut data = String::new(); 27 | file.read_to_string(&mut data)?; 28 | // This gets rid of empty substrings for stability reasons. See also https://github.com/LEXUGE/dcompass/issues/33. 29 | data.split('\n').filter(|&x| !x.is_empty()).try_for_each( 30 | |x| -> std::result::Result<(), IpCidrError> { 31 | self.matcher.push(Cidr::from_str(x)?); 32 | Ok(()) 33 | }, 34 | )?; 35 | Ok(()) 36 | } 37 | 38 | /// Check if IP CIDR set contains the given IP address. 39 | pub fn contains(&self, ip: IpAddr) -> bool { 40 | self.matcher.contains(ip) 41 | } 42 | } 43 | 44 | impl Default for IpCidr { 45 | fn default() -> Self { 46 | Self::new() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /droute/src/router/script/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 | // proc-macro on non-inline modules are unstable 17 | 18 | mod blackhole; 19 | mod domain; 20 | mod geoip; 21 | mod ipcidr; 22 | 23 | pub use self::domain::Domain; 24 | pub use blackhole::blackhole; 25 | pub use geoip::GeoIp; 26 | pub use ipcidr::IpCidr; 27 | 28 | use ::domain::base::{name::FromStrError, octets::ParseError}; 29 | use maxminddb::MaxMindDBError; 30 | use thiserror::Error; 31 | 32 | /// A shorthand for returning utils error. 33 | pub type Result = std::result::Result; 34 | 35 | #[derive(Error, Debug)] 36 | /// All possible errors that may incur when using utils. 37 | pub enum UtilsError { 38 | /// Error forwarded from `std::io::Error`. 39 | #[error("An I/O error encountered. Check files provided for matcher(s) to ensure they exist and have the right permissions.")] 40 | IoError(#[from] std::io::Error), 41 | 42 | /// Error related to GeoIP usages. 43 | #[error("An error happened when using `geoip` matcher.")] 44 | GeoIpError(#[from] MaxMindDBError), 45 | 46 | /// Error related to IP CIDR. 47 | #[error("An error encountered in the IP CIDR matcher: {0}")] 48 | IpCidrError(#[from] cidr_utils::cidr::IpCidrError), 49 | 50 | /// No path to GeoIP database specified while no builtin database is provided. 51 | #[cfg(not(any(feature = "geoip-cn", feature = "geoip-maxmind")))] 52 | #[error("This build doesn't contain a built-in GeoIP database, please specify your own database or use other builds.")] 53 | NoBuiltInDb, 54 | 55 | /// Compression error 56 | #[error("Failed during decompression: {0}")] 57 | DecompError(#[from] niffler::Error), 58 | 59 | /// Failed to convert dname from string 60 | #[error(transparent)] 61 | FromStrError(#[from] FromStrError), 62 | 63 | /// Failed to parse the record 64 | #[error(transparent)] 65 | ParseError(#[from] ParseError), 66 | 67 | /// Short Buf 68 | #[error(transparent)] 69 | ShortBuf(#[from] ::domain::base::ShortBuf), 70 | } 71 | -------------------------------------------------------------------------------- /droute/src/router/upstreams/builder.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 | pub use super::upstream::builder::*; 17 | 18 | use super::{ 19 | error::{Result, UpstreamError}, 20 | QHandleError, Upstreams, 21 | }; 22 | use crate::{AsyncTryInto, Label, Upstream}; 23 | use async_trait::async_trait; 24 | use serde::{Deserialize, Serialize}; 25 | use std::{collections::HashMap, num::NonZeroUsize}; 26 | 27 | fn default_cache_size() -> NonZeroUsize { 28 | NonZeroUsize::new(2048).unwrap() 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Clone)] 32 | #[serde(rename_all = "lowercase")] 33 | /// The Builder for upstreams 34 | pub struct UpstreamsBuilder> { 35 | upstreams: HashMap, 36 | #[serde(default = "default_cache_size")] 37 | cache_size: NonZeroUsize, 38 | } 39 | 40 | impl> UpstreamsBuilder { 41 | /// Create an UpstreamsBuilder from a set of upstreams and the cache_size for all of them. 42 | pub fn from_map(upstreams: HashMap, U>, cache_size: NonZeroUsize) -> Self { 43 | Self { 44 | upstreams: upstreams.into_iter().map(|(k, v)| (k.into(), v)).collect(), 45 | cache_size, 46 | } 47 | } 48 | 49 | /// Create an empty UpstreamsBuilder with a given cache_size 50 | pub fn new(cache_size: usize) -> Option { 51 | std::num::NonZeroUsize::new(cache_size).map(|c| Self { 52 | upstreams: HashMap::new(), 53 | cache_size: c, 54 | }) 55 | } 56 | 57 | /// Add an upstream builder 58 | pub fn add_upstream(mut self, tag: impl Into