Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d28f861075 | |||
| 861559ccca | |||
| b9542793ee | |||
| 432eb59198 | |||
| b21ee5187d | |||
| 2726400f5d | |||
| e6ab6fa20d |
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1 +1,24 @@
|
|||||||
|
# Rust build artifacts
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Arch package build artifacts
|
||||||
|
pkg/arch/src/
|
||||||
|
pkg/arch/pkg/
|
||||||
|
pkg/arch/*.pkg.tar.*
|
||||||
|
pkg/arch/doh-forwarder/
|
||||||
|
|
||||||
|
|
||||||
|
# Editor / IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|||||||
249
Cargo.lock
generated
249
Cargo.lock
generated
@@ -126,32 +126,6 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg_aliases"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpufeatures"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "critical-section"
|
name = "critical-section"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -284,10 +258,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -298,24 +270,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"js-sys",
|
|
||||||
"libc",
|
|
||||||
"r-efi 6.0.0",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -344,11 +302,10 @@ dependencies = [
|
|||||||
"idna",
|
"idna",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.9.4",
|
"rand",
|
||||||
"ring",
|
"ring",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tokio",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@@ -419,22 +376,6 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyper-rustls"
|
|
||||||
version = "0.27.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
|
||||||
dependencies = [
|
|
||||||
"http",
|
|
||||||
"hyper",
|
|
||||||
"hyper-util",
|
|
||||||
"rustls",
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls",
|
|
||||||
"tower-service",
|
|
||||||
"webpki-roots",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -627,12 +568,6 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru-slab"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -758,62 +693,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.11.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls",
|
|
||||||
"socket2",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.11.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"getrandom 0.4.3",
|
|
||||||
"lru-slab",
|
|
||||||
"rand 0.10.2",
|
|
||||||
"rand_pcg",
|
|
||||||
"ring",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"slab",
|
|
||||||
"thiserror",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.5.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
|
||||||
dependencies = [
|
|
||||||
"cfg_aliases",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"socket2",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.46"
|
version = "1.0.46"
|
||||||
@@ -829,12 +708,6 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "r-efi"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -842,18 +715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core 0.9.5",
|
"rand_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
|
||||||
dependencies = [
|
|
||||||
"chacha20",
|
|
||||||
"getrandom 0.4.3",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -863,7 +725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core 0.9.5",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -875,21 +737,6 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_pcg"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -929,21 +776,16 @@ dependencies = [
|
|||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls",
|
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -951,7 +793,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -968,47 +809,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "2.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls"
|
|
||||||
version = "0.23.41"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"rustls-webpki",
|
|
||||||
"subtle",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-pki-types"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
|
||||||
dependencies = [
|
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-webpki"
|
|
||||||
version = "0.103.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
|
||||||
dependencies = [
|
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"untrusted",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -1155,12 +955,6 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "subtle"
|
|
||||||
version = "2.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.118"
|
version = "2.0.118"
|
||||||
@@ -1274,16 +1068,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-rustls"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
|
||||||
dependencies = [
|
|
||||||
"rustls",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
@@ -1586,25 +1370,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-time"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webpki-roots"
|
|
||||||
version = "1.0.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1778,12 +1543,6 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zeroize"
|
|
||||||
version = "1.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ license = "MIT"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
hickory-proto = "0.25"
|
hickory-proto = { version = "0.25", default-features = false, features = ["std"] }
|
||||||
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }
|
reqwest = { version = "0.12", default-features = false }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ Before pushing, verify:
|
|||||||
- [ ] No `unwrap()` outside of tests
|
- [ ] No `unwrap()` outside of tests
|
||||||
- [ ] Config values are externalized (not hardcoded)
|
- [ ] Config values are externalized (not hardcoded)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Intra Comparison](docs/intra-comparison.md) — Architectural comparison
|
||||||
|
with Jigsaw's Intra, explaining why DNS forwarding alone can't bypass
|
||||||
|
censorship and what's needed (TLS splitting, TUN interception, etc.)
|
||||||
|
|
||||||
## Milestone Tracking
|
## Milestone Tracking
|
||||||
|
|
||||||
See [docs/plan.md](docs/plan.md) for the full project plan and milestone checklist.
|
See [docs/plan.md](docs/plan.md) for the full project plan and milestone checklist.
|
||||||
@@ -91,6 +97,7 @@ See [docs/plan.md](docs/plan.md) for the full project plan and milestone checkli
|
|||||||
| M5 — Observability | 🔲 Not started |
|
| M5 — Observability | 🔲 Not started |
|
||||||
| M6 — Blocking & Filtering | 🔲 Not started |
|
| M6 — Blocking & Filtering | 🔲 Not started |
|
||||||
| M7 — Production Hardening | 🔲 Not started |
|
| M7 — Production Hardening | 🔲 Not started |
|
||||||
|
| M8 — Censorship Resistance | 🔲 Not started |
|
||||||
|
|
||||||
## Useful Commands
|
## Useful Commands
|
||||||
|
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Mohammadreza Khani
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
153
README.md
153
README.md
@@ -2,14 +2,26 @@
|
|||||||
|
|
||||||
A lightweight DNS-over-HTTPS (DoH) forwarder written in Rust.
|
A lightweight DNS-over-HTTPS (DoH) forwarder written in Rust.
|
||||||
|
|
||||||
Accepts DNS queries over HTTPS ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484)) and forwards them to upstream DoH resolvers (Cloudflare, Google, Quad9, or custom).
|
Accepts DNS queries over traditional DNS (UDP/TCP) and HTTP DoH ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484)), then forwards them upstream via DoH (HTTPS POST) to configurable resolvers.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
client["DNS Client"] -- "UDP/TCP or HTTPS" --> forwarder["doh-forwarder"]
|
||||||
|
forwarder -- "HTTPS POST (DoH)" --> upstream["Upstream Resolvers\n(Cloudflare, Google, ...)"]
|
||||||
|
upstream -- "DNS response" --> forwarder
|
||||||
|
forwarder -- "DNS response" --> client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
See [docs/story.md](docs/story.md) for the full context — why this exists, what already works, and where this project is headed.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- **Dual-protocol listener** — traditional DNS (UDP/TCP) + HTTP DoH endpoints
|
||||||
- **RFC 8484 compliant** — GET (`?dns=base64url`) and POST (`application/dns-message`)
|
- **RFC 8484 compliant** — GET (`?dns=base64url`) and POST (`application/dns-message`)
|
||||||
- **Multiple upstreams** — configure several DoH resolvers
|
- **Multiple upstreams** — configure several DoH resolvers
|
||||||
- **Selection strategies** — round-robin or failover
|
- **Selection strategies** — round-robin or failover with automatic retry
|
||||||
- **In-memory cache** — TTL-aware with LRU eviction (configurable)
|
|
||||||
- **Environment overrides** — override config via env vars
|
- **Environment overrides** — override config via env vars
|
||||||
- **Graceful shutdown** — handles SIGINT and SIGTERM
|
- **Graceful shutdown** — handles SIGINT and SIGTERM
|
||||||
- **Health endpoint** — `GET /health`
|
- **Health endpoint** — `GET /health`
|
||||||
@@ -20,7 +32,7 @@ Accepts DNS queries over HTTPS ([RFC 8484](https://www.rfc-editor.org/rfc/rfc848
|
|||||||
# Build
|
# Build
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Run with default config
|
# Run with default config (DoH on :8800, DNS on :53530)
|
||||||
./target/release/doh-forwarder
|
./target/release/doh-forwarder
|
||||||
|
|
||||||
# Run with a custom config
|
# Run with a custom config
|
||||||
@@ -29,94 +41,111 @@ cargo build --release
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Create a TOML config file (see [config/default.toml](config/default.toml)):
|
See [config/default.toml](config/default.toml):
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
listen = "0.0.0.0:3000"
|
listen = "0.0.0.0:8800"
|
||||||
|
|
||||||
|
[dns]
|
||||||
|
listen = "0.0.0.0:53530"
|
||||||
|
|
||||||
[upstream]
|
[upstream]
|
||||||
resolvers = [
|
resolvers = [
|
||||||
{ name = "cloudflare", url = "https://cloudflare-dns.com/dns-query" },
|
{ name = "cloudflare", url = "https://cloudflare-dns.com/dns-query" },
|
||||||
{ name = "google", url = "https://dns.google/dns-query" },
|
|
||||||
{ name = "quad9", url = "https://dns.quad9.net/dns-query" },
|
|
||||||
]
|
]
|
||||||
strategy = "round-robin" # or "failover"
|
strategy = "round-robin" # or "failover"
|
||||||
|
|
||||||
[cache]
|
|
||||||
enabled = true
|
|
||||||
max_entries = 10000
|
|
||||||
max_ttl_secs = 3600
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variable Overrides
|
### Environment Variable Overrides
|
||||||
|
|
||||||
| Variable | Description | Example |
|
| Variable | Description | Example |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DOH_LISTEN` | Override server listen address | `0.0.0.0:8080` |
|
| `DOH_LISTEN` | Override DoH server listen address | `0.0.0.0:8080` |
|
||||||
| `DOH_UPSTREAM_STRATEGY` | Override upstream strategy | `failover` |
|
| `DOH_UPSTREAM_STRATEGY` | Override upstream strategy | `failover` |
|
||||||
| `DOH_UPSTREAM_RESOLVERS` | Comma-separated `name=url` pairs | `google=https://dns.google/dns-query,cf=https://cloudflare-dns.com/dns-query` |
|
| `DOH_UPSTREAM_RESOLVERS` | Comma-separated `name=url` pairs | `google=https://dns.google/dns-query,cf=https://cloudflare-dns.com/dns-query` |
|
||||||
| `RUST_LOG` | Log level filter | `debug`, `doh_forwarder=trace` |
|
| `RUST_LOG` | Log level filter | `debug`, `doh_forwarder=trace` |
|
||||||
|
|
||||||
## API
|
## Usage
|
||||||
|
|
||||||
### POST `/dns-query`
|
### Point your system DNS at the forwarder
|
||||||
|
|
||||||
Send a raw DNS wire-format query in the request body.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using a pre-built DNS query (hex-encoded)
|
# /etc/resolv.conf
|
||||||
curl -X POST http://localhost:3000/dns-query \
|
nameserver 127.0.0.1
|
||||||
|
# Then run the forwarder with [dns] listen = "127.0.0.1:53"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use `resolvectl` with systemd-resolved:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
resolvectl dns eth0 127.0.0.1
|
||||||
|
resolvectl dns-over-tls no # we handle encryption upstream
|
||||||
|
```
|
||||||
|
|
||||||
|
### DoH API
|
||||||
|
|
||||||
|
**POST `/dns-query`** — raw DNS wire-format in body:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8800/dns-query \
|
||||||
-H "Content-Type: application/dns-message" \
|
-H "Content-Type: application/dns-message" \
|
||||||
-H "Accept: application/dns-message" \
|
-H "Accept: application/dns-message" \
|
||||||
--data-binary @query.bin
|
--data-binary @query.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
### GET `/dns-query`
|
**GET `/dns-query`** — base64url-encoded query parameter:
|
||||||
|
|
||||||
Send a base64url-encoded DNS query as a query parameter.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Encode a DNS query and send via GET
|
curl "http://localhost:8800/dns-query?dns=<base64url-encoded-query>"
|
||||||
QUERY=$(python3 -c "
|
|
||||||
import base64, struct
|
|
||||||
# Minimal A record query for example.com
|
|
||||||
q = b'\\x12\\x34' # ID
|
|
||||||
q += b'\\x01\\x00' # flags
|
|
||||||
q += b'\\x00\\x01' # 1 question
|
|
||||||
q += b'\\x00\\x00' # 0 answers
|
|
||||||
q += b'\\x00\\x00' # 0 authority
|
|
||||||
q += b'\\x00\\x00' # 0 additional
|
|
||||||
q += b'\\x07example\\x03com\\x00' # name
|
|
||||||
q += b'\\x00\\x01' # type A
|
|
||||||
q += b'\\x00\\x01' # class IN
|
|
||||||
print(base64.urlsafe_b64encode(q).rstrip(b'=').decode())
|
|
||||||
")
|
|
||||||
curl "http://localhost:3000/dns-query?dns=$QUERY"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### GET `/health`
|
**GET `/health`** — returns `ok`:
|
||||||
|
|
||||||
Returns `ok` if the server is running.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3000/health
|
curl http://localhost:8800/health
|
||||||
# ok
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upstream Strategies
|
### Upstream Strategies
|
||||||
|
|
||||||
| Strategy | Behavior |
|
| Strategy | Behavior |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `round-robin` | Distributes queries across resolvers in order |
|
| `round-robin` | Distributes queries across resolvers in rotation |
|
||||||
| `failover` | Tries resolvers in order; moves to the next on failure |
|
| `failover` | Tries resolvers in order; moves to the next on failure |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
doh-forwarder/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── config/
|
||||||
|
│ └── default.toml # Default configuration
|
||||||
|
├── docs/
|
||||||
|
│ ├── plan.md # Project plan & milestones
|
||||||
|
│ └── story.md # Why this project exists
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # Entrypoint, server setup, graceful shutdown
|
||||||
|
│ ├── lib.rs # Public API re-exports
|
||||||
|
│ ├── config.rs # TOML config + env overrides
|
||||||
|
│ ├── routes.rs # HTTP route handlers (/dns-query, /health)
|
||||||
|
│ ├── dns/
|
||||||
|
│ │ ├── mod.rs
|
||||||
|
│ │ ├── query.rs # DNS query parsing
|
||||||
|
│ │ ├── response.rs # DNS response building (SERVFAIL)
|
||||||
|
│ │ └── listener.rs # UDP/TCP DNS listener
|
||||||
|
│ └── upstream/
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ └── doh_client.rs # DoH forwarding client (retry, strategies)
|
||||||
|
├── tests/
|
||||||
|
│ └── integration.rs # HTTP integration tests
|
||||||
|
└── DEVELOP.md # Development rules & milestone tracking
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
See [DEVELOP.md](DEVELOP.md) for development rules, coding standards, and milestone tracking.
|
||||||
# Prerequisites
|
|
||||||
rustup show # Rust 1.80+
|
|
||||||
|
|
||||||
|
```bash
|
||||||
# Run with auto-reload
|
# Run with auto-reload
|
||||||
cargo install cargo-watch
|
cargo install cargo-watch
|
||||||
cargo watch -x run
|
cargo watch -x run
|
||||||
@@ -129,32 +158,6 @@ cargo clippy -- -D warnings
|
|||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
```
|
```
|
||||||
|
|
||||||
See [DEVELOP.md](DEVELOP.md) for full development rules and milestone tracking.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
doh-forwarder/
|
|
||||||
├── Cargo.toml
|
|
||||||
├── config/
|
|
||||||
│ └── default.toml # Default configuration
|
|
||||||
├── src/
|
|
||||||
│ ├── main.rs # Entrypoint, server setup
|
|
||||||
│ ├── lib.rs # Public API re-exports
|
|
||||||
│ ├── config.rs # TOML config + env overrides
|
|
||||||
│ ├── cache.rs # DNS response cache (TTL + LRU)
|
|
||||||
│ ├── dns/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ ├── query.rs # DNS query parsing
|
|
||||||
│ │ └── response.rs # DNS response building
|
|
||||||
│ ├── upstream/
|
|
||||||
│ │ ├── mod.rs
|
|
||||||
│ │ └── doh_client.rs # DoH forwarding client
|
|
||||||
│ └── routes.rs # HTTP route handlers
|
|
||||||
└── tests/
|
|
||||||
└── integration.rs # Integration tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
286
docs/intra-comparison.md
Normal file
286
docs/intra-comparison.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# doh-forwarder vs Intra — Architectural Comparison
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document compares **doh-forwarder** (this project) with
|
||||||
|
[Intra](https://github.com/Jigsaw-Code/Intra) by Jigsaw (Google) to
|
||||||
|
understand why Intra can unblock YouTube and other censored sites while
|
||||||
|
doh-forwarder can only resolve their DNS.
|
||||||
|
|
||||||
|
## The Core Problem
|
||||||
|
|
||||||
|
DNS resolution is only the first layer of censorship. Even when DNS is
|
||||||
|
encrypted and returns the correct IP, the **actual HTTPS connection** to
|
||||||
|
the target server can be blocked by Deep Packet Inspection (DPI) at the
|
||||||
|
TCP/TLS level.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Browser
|
||||||
|
participant DNS as doh-forwarder
|
||||||
|
participant DPI as ISP Firewall
|
||||||
|
participant YT as YouTube
|
||||||
|
|
||||||
|
Note over Browser,YT: doh-forwarder flow (DNS only)
|
||||||
|
Browser->>DNS: youtube.com?
|
||||||
|
DNS-->>Browser: 142.250.80.46 ✅
|
||||||
|
Browser->>DPI: HTTPS to 142.250.80.46:443
|
||||||
|
DPI-->>Browser: ❌ BLOCKED (SNI inspection)
|
||||||
|
|
||||||
|
Note over Browser,YT: Intra flow (full traffic)
|
||||||
|
Browser->>DPI: HTTPS to 142.250.80.46:443 (via TUN)
|
||||||
|
DPI->>DPI: Split/retry Client Hello
|
||||||
|
DPI-->>YT: ✅ PASSED
|
||||||
|
YT-->>Browser: Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architectural Differences
|
||||||
|
|
||||||
|
### doh-forwarder — DNS-only forwarder
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph clients["DNS Clients"]
|
||||||
|
browser["Browser / App"]
|
||||||
|
os["OS DNS Client"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph forwarder["doh-forwarder"]
|
||||||
|
doh["Axum HTTP Server\n(port 8800)"]
|
||||||
|
dns["DnsListener\n(UDP + TCP)"]
|
||||||
|
client["DohClient\n(reqwest HTTP)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph upstreams["Upstream DoH Resolvers"]
|
||||||
|
cf["Cloudflare"]
|
||||||
|
google["Google"]
|
||||||
|
quad9["Quad9"]
|
||||||
|
end
|
||||||
|
|
||||||
|
browser -- "HTTPS (POST/GET)" --> doh
|
||||||
|
os -- "UDP/TCP :53" --> dns
|
||||||
|
doh --> client
|
||||||
|
dns --> client
|
||||||
|
client -- "HTTPS POST" --> cf
|
||||||
|
client -- "HTTPS POST" --> google
|
||||||
|
client -- "HTTPS POST" --> quad9
|
||||||
|
```
|
||||||
|
|
||||||
|
- Accepts DNS queries on port 53 (UDP/TCP) and port 8800 (DoH)
|
||||||
|
- Forwards queries to upstream DoH resolvers over HTTPS
|
||||||
|
- Returns DNS responses to clients
|
||||||
|
- **Does not touch any non-DNS traffic**
|
||||||
|
|
||||||
|
### Intra — Full traffic interception + circumvention
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph tun["TUN Device (captures ALL traffic)"]
|
||||||
|
direction TB
|
||||||
|
dns["DNS Interception\n→ DoH query via HTTP POST"]
|
||||||
|
tcp["intraStreamDialer\n→ DialWithSplitRetry()\n→ TLS hello splitting"]
|
||||||
|
end
|
||||||
|
|
||||||
|
dns_packets["DNS packets\n(port 53)"] --> dns
|
||||||
|
tcp_packets["TCP packets\n(port 443)"] --> tcp
|
||||||
|
|
||||||
|
tcp --> success{"Success?"}
|
||||||
|
success -- "Yes" --> normal["Normal TLS handshake"]
|
||||||
|
success -- "No (blocked)" --> retry["Close + retry with\nsplit Client Hello"]
|
||||||
|
retry --> normal
|
||||||
|
```
|
||||||
|
|
||||||
|
- Creates a **TUN device** that captures all device traffic
|
||||||
|
- Intercepts DNS packets → sends via DoH (like doh-forwarder)
|
||||||
|
- Intercepts TCP connections to port 443 → applies circumvention
|
||||||
|
- Uses **TLS Client Hello splitting** to evade DPI
|
||||||
|
|
||||||
|
## Feature Comparison
|
||||||
|
|
||||||
|
| Feature | doh-forwarder | Intra |
|
||||||
|
|---------|:------------:|:-----:|
|
||||||
|
| DNS-over-HTTPS forwarding | ✅ | ✅ |
|
||||||
|
| Multiple upstream resolvers | ✅ | ✅ |
|
||||||
|
| Round-robin / failover | ✅ | ❌ |
|
||||||
|
| UDP + TCP DNS listener | ✅ | ✅ (via TUN) |
|
||||||
|
| HTTP DoH endpoint | ✅ | ❌ |
|
||||||
|
| TUN device (all traffic) | ❌ | ✅ |
|
||||||
|
| TCP stream interception | ❌ | ✅ |
|
||||||
|
| TLS Client Hello splitting | ❌ | ✅ |
|
||||||
|
| Retry on censorship detection | ❌ | ✅ |
|
||||||
|
| SNI-based error reporting | ❌ | ✅ |
|
||||||
|
| EDNS padding | ❌ | ✅ |
|
||||||
|
| IP map with confirmation tracking | ❌ | ✅ |
|
||||||
|
| Servfail hangover (rate limiting) | ❌ | ✅ |
|
||||||
|
| Written in | Rust | Go + Java |
|
||||||
|
| Platform | Linux (server) | Android (VPN) |
|
||||||
|
|
||||||
|
## The Missing Piece: TLS Client Hello Splitting
|
||||||
|
|
||||||
|
The critical technique Intra uses to bypass censorship is **TCP segment
|
||||||
|
splitting** on the TLS Client Hello. Here's how it works:
|
||||||
|
|
||||||
|
### How DPI censorship works
|
||||||
|
|
||||||
|
When your browser connects to `https://www.youtube.com`, the first thing
|
||||||
|
it sends is a **TLS Client Hello** packet. This packet contains the
|
||||||
|
**SNI (Server Name Indication)** field in plaintext — the domain name
|
||||||
|
`www.youtube.com`. A DPI system reads this field and blocks the
|
||||||
|
connection if the domain is on a blocklist.
|
||||||
|
|
||||||
|
### How splitting defeats DPI
|
||||||
|
|
||||||
|
Intra's `splitHello()` function splits the TLS Client Hello into **two
|
||||||
|
TCP segments** at a random offset (6–64 bytes):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant DPI as DPI Firewall
|
||||||
|
participant Server
|
||||||
|
|
||||||
|
Note over Client,Server: Normal (BLOCKED)
|
||||||
|
Client->>DPI: [TLS Header] [SNI: youtube.com ... full hello]
|
||||||
|
DPI->>DPI: Read SNI → youtube.com
|
||||||
|
DPI-->>Client: ❌ BLOCKED
|
||||||
|
|
||||||
|
Note over Client,Server: Split (PASSES THROUGH)
|
||||||
|
Client->>DPI: Segment 1: [TLS Header] [first N bytes...]
|
||||||
|
Client->>DPI: Segment 2: [remaining... SNI: youtube.com ...]
|
||||||
|
DPI->>DPI: Can't reassemble fast enough
|
||||||
|
DPI-->>Server: ✅ PASSED
|
||||||
|
Server-->>Client: Response
|
||||||
|
```
|
||||||
|
|
||||||
|
For TLS Client Hello specifically, Intra also **properly fragments the
|
||||||
|
TLS record header** — it adjusts the record length field in each segment
|
||||||
|
so the TLS layer itself remains valid:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// First segment: adjust record length to split point
|
||||||
|
pkt := hello[:splitLen]
|
||||||
|
binary.BigEndian.PutUint16(pkt[3:5], uint16(recordSplitLen))
|
||||||
|
|
||||||
|
// Second segment: copy TLS header, set remaining length
|
||||||
|
pkt = hello[splitLen-5:]
|
||||||
|
copy(pkt, hello[:5])
|
||||||
|
binary.BigEndian.PutUint16(pkt[3:5], recordLen-uint16(recordSplitLen))
|
||||||
|
```
|
||||||
|
|
||||||
|
### The retry mechanism
|
||||||
|
|
||||||
|
`DialWithSplitRetry()` in `retrier.go`:
|
||||||
|
|
||||||
|
1. Opens a normal TCP connection to the target
|
||||||
|
2. Sends the TLS Client Hello
|
||||||
|
3. Sets a read deadline (1200ms + 2×RTT)
|
||||||
|
4. If the connection is dropped or times out → **censorship detected**
|
||||||
|
5. Closes the connection
|
||||||
|
6. Opens a **new** TCP connection
|
||||||
|
7. Replays the Client Hello as **split segments**
|
||||||
|
8. If this succeeds → the connection proceeds normally
|
||||||
|
|
||||||
|
## Why doh-forwarder Can't Unblock YouTube
|
||||||
|
|
||||||
|
doh-forwarder operates at the **DNS layer only**. It has no visibility
|
||||||
|
into or control over:
|
||||||
|
|
||||||
|
- TCP connections to resolved IPs
|
||||||
|
- TLS handshakes
|
||||||
|
- HTTP requests
|
||||||
|
|
||||||
|
The traffic flow is:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Browser
|
||||||
|
participant OS as OS DNS
|
||||||
|
participant DF as doh-forwarder
|
||||||
|
participant DPI as ISP DPI
|
||||||
|
participant YT as YouTube (142.250.80.46)
|
||||||
|
|
||||||
|
Browser->>OS: youtube.com?
|
||||||
|
OS->>DF: DNS query (port 53)
|
||||||
|
DF-->>OS: 142.250.80.46 ✅
|
||||||
|
OS-->>Browser: 142.250.80.46
|
||||||
|
|
||||||
|
Note over Browser,YT: Steps 4–7 are outside doh-forwarder's scope
|
||||||
|
Browser->>DPI: TCP SYN to 142.250.80.46:443
|
||||||
|
Browser->>DPI: TLS ClientHello (SNI: youtube.com)
|
||||||
|
DPI->>DPI: Read SNI → block
|
||||||
|
DPI-->>Browser: ❌ Connection reset
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 4–7 happen entirely outside doh-forwarder's scope.
|
||||||
|
|
||||||
|
## What Would Be Needed
|
||||||
|
|
||||||
|
To achieve what Intra does, doh-forwarder would need to evolve from a
|
||||||
|
DNS forwarder into a **traffic interception and circumvention proxy**:
|
||||||
|
|
||||||
|
### Option A: SOCKS5 Proxy with Split Transport (M6 path)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
browser["Browser"] -- "SOCKS5" --> proxy["doh-forwarder\n(SOCKS5 proxy)"]
|
||||||
|
proxy -- "split TCP" --> target["Target Server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure browser to use SOCKS5 proxy
|
||||||
|
- Proxy applies TLS splitting on outbound connections
|
||||||
|
- Lighter than full TUN, requires browser config
|
||||||
|
|
||||||
|
### Option B: Transparent Proxy with TUN (Intra-like)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
all["All Traffic"] -- "TUN" --> forwarder["doh-forwarder"]
|
||||||
|
forwarder -- "DNS: DoH" --> dns["DNS Resolver"]
|
||||||
|
forwarder -- "TCP: split" --> target["Target Server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Zero browser configuration
|
||||||
|
- Requires root/CAP_NET_ADMIN for TUN
|
||||||
|
- Significantly more complex
|
||||||
|
|
||||||
|
### Option C: Hybrid
|
||||||
|
|
||||||
|
- DNS forwarding via DoH (current functionality)
|
||||||
|
- Optional SOCKS5 proxy for TCP circumvention
|
||||||
|
- Optional TUN mode for transparent interception
|
||||||
|
- User chooses based on their environment
|
||||||
|
|
||||||
|
## Lessons from Intra's Codebase
|
||||||
|
|
||||||
|
### 1. IP Map with Confirmation Tracking
|
||||||
|
|
||||||
|
Intra maintains an `ipmap` that tracks which IPs of a DoH resolver
|
||||||
|
actually work. If a confirmed IP starts failing, it's disconfirmed and
|
||||||
|
alternatives are tried. This provides resilience against partial IP
|
||||||
|
blocking.
|
||||||
|
|
||||||
|
### 2. EDNS Padding
|
||||||
|
|
||||||
|
Intra pads DNS queries to standard sizes to prevent traffic analysis
|
||||||
|
that could identify specific domains by query size.
|
||||||
|
|
||||||
|
### 3. Servfail Hangover
|
||||||
|
|
||||||
|
When a DoH server returns errors, Intra enters a 10-second "hangover"
|
||||||
|
where it stops sending queries (returns SERVFAIL immediately). This
|
||||||
|
rate-limits queries to misconfigured or blocked servers.
|
||||||
|
|
||||||
|
### 4. SNI Reporter
|
||||||
|
|
||||||
|
Intra reports which SNIs are being censored (via the Choir library) to
|
||||||
|
contribute to censorship monitoring without revealing individual user
|
||||||
|
activity.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Intra source code](https://github.com/Jigsaw-Code/Intra)
|
||||||
|
- [Outline SDK](https://github.com/Jigsaw-Code/outline-sdk) — the
|
||||||
|
transport library Intra uses
|
||||||
|
- [RFC 8484 — DNS over HTTPS](https://tools.ietf.org/html/rfc8484)
|
||||||
|
- [TLS Client Hello splitting](https://gfw.report/talks/syria2016/en/)
|
||||||
|
— research on DPI evasion
|
||||||
52
docs/plan.md
52
docs/plan.md
@@ -17,17 +17,36 @@ A lightweight dual-protocol DNS forwarder written in Rust. It accepts DNS querie
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
flowchart LR
|
||||||
│ DNS Client │─────▶│ DoH Forwarder │─────▶│ Upstream DoH │
|
subgraph clients["Clients"]
|
||||||
│ (browser, │ HTTPS│ (this project) │ HTTPS│ (1.1.1.1, │
|
browser["Browser / App"]
|
||||||
│ app) │◀─────│ /dns-query │◀─────│ 8.8.8.8, …) │
|
os["OS DNS Client"]
|
||||||
└─────────────┘ │ /health │ └─────────────────┘
|
end
|
||||||
│ └──────────────────┘
|
|
||||||
│ UDP/TCP ▲
|
subgraph forwarder["DoH Forwarder"]
|
||||||
│ (port 5353) │
|
doh["/dns-query (HTTP)"]
|
||||||
└──────────────────────┘
|
health["/health"]
|
||||||
System DNS (resolv.conf)
|
dns["DNS Listener (UDP/TCP)"]
|
||||||
|
upstream["Upstream Client"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph upstreams["Upstream DoH Resolvers"]
|
||||||
|
cf["Cloudflare (1.1.1.1)"]
|
||||||
|
google["Google (8.8.8.8)"]
|
||||||
|
other["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
browser -- "HTTPS (RFC 8484)" --> doh
|
||||||
|
os -- "UDP/TCP :5353" --> dns
|
||||||
|
doh --> upstream
|
||||||
|
dns --> upstream
|
||||||
|
upstream -- "HTTPS POST" --> cf
|
||||||
|
upstream -- "HTTPS POST" --> google
|
||||||
|
upstream -- "HTTPS POST" --> other
|
||||||
|
cf -- "DNS response" --> upstream
|
||||||
|
google -- "DNS response" --> upstream
|
||||||
|
other -- "DNS response" --> upstream
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
@@ -101,6 +120,17 @@ A lightweight dual-protocol DNS forwarder written in Rust. It accepts DNS querie
|
|||||||
- [ ] Helm chart
|
- [ ] Helm chart
|
||||||
- [ ] CI pipeline (lint, test, build, image push)
|
- [ ] CI pipeline (lint, test, build, image push)
|
||||||
|
|
||||||
|
### M8 — Censorship Resistance (the real differentiator)
|
||||||
|
|
||||||
|
This is where the project goes beyond what `cloudflared` offers. See
|
||||||
|
[story.md](story.md) for the full rationale.
|
||||||
|
|
||||||
|
- [ ] SOCKS5 proxy support for upstream DoH connections
|
||||||
|
- [ ] HTTP CONNECT proxy support for upstream DoH connections
|
||||||
|
- [ ] Configurable upstream transport (direct / SOCKS5 / WireGuard tunnel)
|
||||||
|
- [ ] Upstream health probing (latency + availability)
|
||||||
|
- [ ] Auto-switch away from unreachable resolvers
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
169
docs/story.md
Normal file
169
docs/story.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# The Story — Why This Project Exists
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
In countries with internet censorship (Iran, China, Russia, and others), DNS is
|
||||||
|
one of the first things controlled. ISPs intercept plain DNS queries (UDP/TCP
|
||||||
|
port 53) and:
|
||||||
|
|
||||||
|
- Return fake IP addresses for blocked domains
|
||||||
|
- Log every domain you look up
|
||||||
|
- Inject ads or redirect to government portals
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant ISP as ISP DNS
|
||||||
|
participant Real as Real Server
|
||||||
|
|
||||||
|
User->>ISP: youtube.com?
|
||||||
|
ISP-->>User: 1.2.3.4 (FAKE IP)
|
||||||
|
User->>Real: HTTPS to 1.2.3.4
|
||||||
|
Real-->>User: ❌ Connection failed / redirected
|
||||||
|
```
|
||||||
|
|
||||||
|
DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) solve this by encrypting DNS
|
||||||
|
queries, making them invisible to the ISP's DNS layer.
|
||||||
|
|
||||||
|
## What Already Exists
|
||||||
|
|
||||||
|
This is not a new problem. Mature tools already solve local DNS → DoH forwarding:
|
||||||
|
|
||||||
|
| Tool | What it does | Maintained by |
|
||||||
|
|---|---|---|
|
||||||
|
| `cloudflared` | Local DNS → DoH proxy (`cloudflared proxy-dns`) | Cloudflare |
|
||||||
|
| `dnscrypt-proxy` | DoH + DoT + anonymized relays, blocklists | Frank Denis |
|
||||||
|
| `stubby` | Local DNS → DoT proxy | getdns team |
|
||||||
|
| `systemd-resolved` | Built into systemd, supports DoT | systemd team |
|
||||||
|
| `AdGuard Home` | DNS proxy + DoH/DoT + web UI + blocklists | AdGuard |
|
||||||
|
|
||||||
|
For simple "local DNS → DoH" forwarding, **`cloudflared proxy-dns` does exactly
|
||||||
|
what this project does in one command:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cloudflared proxy-dns --port 5353 --upstream https://cloudflare-dns.com/dns-query
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Real Obstacle
|
||||||
|
|
||||||
|
The harder problem — and the one most relevant to censored regions — is that
|
||||||
|
**upstream DoH/DoT endpoints themselves get blocked:**
|
||||||
|
|
||||||
|
| Technique | What gets blocked | How |
|
||||||
|
|---|---|---|
|
||||||
|
| SNI inspection | `cloudflare-dns.com`, `dns.google` | TLS ClientHello reveals the domain |
|
||||||
|
| IP blocklist | Known resolver IPs (1.1.1.1, 8.8.8.8) | Direct IP blocking |
|
||||||
|
| TLS fingerprinting | DoH/DoT traffic patterns | JA3/JA4 fingerprint analysis |
|
||||||
|
| Deep packet inspection | HTTPS traffic to known DNS resolvers | Pattern matching on domain + path |
|
||||||
|
|
||||||
|
A local forwarder that sends `POST https://cloudflare-dns.com/dns-query` will
|
||||||
|
work in most of the world, but **will likely fail in Iran** because the
|
||||||
|
connection to Cloudflare itself is blocked.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant FW as Firewall / DPI
|
||||||
|
participant CF as Cloudflare DoH
|
||||||
|
|
||||||
|
Client->>FW: POST https://cloudflare-dns.com/dns-query
|
||||||
|
FW->>FW: Inspect SNI: cloudflare-dns.com
|
||||||
|
FW-->>Client: ❌ BLOCKED (connection reset)
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Actually Works in Censored Environments
|
||||||
|
|
||||||
|
Tools that solve the **upstream blocking** problem:
|
||||||
|
|
||||||
|
1. **Warp / WireGuard** — tunnel all traffic (including DNS) through an
|
||||||
|
encrypted VPN. The ISP sees WireGuard traffic but can't inspect it.
|
||||||
|
|
||||||
|
2. **GoodbyeDPI / zapret** — packet manipulation to evade DPI without a VPN.
|
||||||
|
Modifies TCP flags, splits packets, fakes SNI.
|
||||||
|
|
||||||
|
3. **dnscrypt-proxy with anonymized relays** — DNS queries go through
|
||||||
|
intermediate relays. The resolver never sees your IP, and the ISP never
|
||||||
|
sees the resolver's IP.
|
||||||
|
|
||||||
|
4. **Tor** — route DNS through Tor exit nodes. Slow but highly resistant.
|
||||||
|
|
||||||
|
5. **Encrypted Client Hello (ECH)** — hides the SNI field in TLS handshakes.
|
||||||
|
Still emerging; not widely supported.
|
||||||
|
|
||||||
|
## So Is This Project Necessary?
|
||||||
|
|
||||||
|
**Honestly: no, not for its current functionality.**
|
||||||
|
|
||||||
|
If the goal is "use encrypted DNS from Linux in a censored region," install
|
||||||
|
`cloudflared` or `dnscrypt-proxy` + a Warp/WireGuard tunnel. These are
|
||||||
|
battle-tested, maintained by teams, and packaged in every distro.
|
||||||
|
|
||||||
|
## Where This Project Becomes Useful
|
||||||
|
|
||||||
|
This project becomes **genuinely valuable** if it evolves beyond a basic
|
||||||
|
forwarder. Directions that matter:
|
||||||
|
|
||||||
|
### 1. Upstream obfuscation (the real differentiator)
|
||||||
|
|
||||||
|
Route upstream DoH traffic through a proxy (SOCKS5, HTTP CONNECT, or
|
||||||
|
WireGuard tunnel) instead of direct HTTPS. This bypasses SNI and IP
|
||||||
|
blocking:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
client["DNS Client"] -- "UDP/TCP" --> forwarder["doh-forwarder"]
|
||||||
|
forwarder -- "SOCKS5/CONNECT" --> proxy["Proxy Server"]
|
||||||
|
proxy -- "HTTPS" --> doh["DoH Resolver"]
|
||||||
|
doh -- "DNS response" --> proxy
|
||||||
|
proxy -- "DNS response" --> forwarder
|
||||||
|
forwarder -- "DNS response" --> client
|
||||||
|
```
|
||||||
|
|
||||||
|
No existing lightweight Rust tool does this well. `dnscrypt-proxy` has
|
||||||
|
anonymized relays but no SOCKS5 support for DoH.
|
||||||
|
|
||||||
|
### 2. Pluggable transport layer
|
||||||
|
|
||||||
|
Support multiple upstream transports:
|
||||||
|
- **Direct DoH** (current — works in free internet)
|
||||||
|
- **DoH over SOCKS5** (works behind blocks)
|
||||||
|
- **DoH over WireGuard tunnel** (works behind aggressive DPI)
|
||||||
|
- **DNS-over-TLS** (for resolvers that support it)
|
||||||
|
|
||||||
|
The user picks the transport based on their environment.
|
||||||
|
|
||||||
|
### 3. Smart upstream selection
|
||||||
|
|
||||||
|
Don't just round-robin — **probe** upstreams for latency and availability.
|
||||||
|
Auto-switch when a resolver becomes unreachable. This matters when resolvers
|
||||||
|
get blocked intermittently.
|
||||||
|
|
||||||
|
### 4. Built-in blocklist support
|
||||||
|
|
||||||
|
Ad-blocking and malware-blocking at the DNS level. `dnscrypt-proxy` and
|
||||||
|
`AdGuard Home` do this, but a lightweight Rust binary with this built-in
|
||||||
|
is appealing for embedded/resource-constrained systems.
|
||||||
|
|
||||||
|
### 5. Learning
|
||||||
|
|
||||||
|
Even if you never deploy it, building this teaches:
|
||||||
|
- DNS wire format (RFC 1035)
|
||||||
|
- DoH protocol (RFC 8484)
|
||||||
|
- Rust async (Tokio, Axum)
|
||||||
|
- Network programming
|
||||||
|
- Error handling patterns
|
||||||
|
|
||||||
|
That alone justifies the project.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
|---|---|
|
||||||
|
| Does this solve local DNS → DoH? | Yes, but `cloudflared` already does this |
|
||||||
|
| Does this bypass censorship? | No — upstream DoH endpoints get blocked |
|
||||||
|
| Is the project pointless? | No — it's a foundation for something better |
|
||||||
|
| What makes it worth building? | SOCKS5/proxy upstream support, pluggable transports, learning |
|
||||||
|
|
||||||
|
The project is a **starting point**, not a finished solution. The first 80%
|
||||||
|
(local DNS → DoH forwarding) is solved by existing tools. The last 20%
|
||||||
|
(bypassing upstream blocking) is where this project can differentiate.
|
||||||
@@ -4,28 +4,30 @@ pkgver=0.1.0
|
|||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A lightweight DNS-over-HTTPS forwarder"
|
pkgdesc="A lightweight DNS-over-HTTPS forwarder"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
url="https://gitea.logi.camp/LogiCrew/doh-forwarder"
|
url="https://git.logicamp.dev/mohamad/doh-forwarder"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('gcc-libs')
|
depends=('gcc-libs' 'openssl')
|
||||||
makedepends=('rust' 'cargo')
|
makedepends=('rust' 'cargo')
|
||||||
backup=('etc/doh-forwarder/config.toml')
|
backup=('etc/doh-forwarder/config.toml')
|
||||||
source=("$pkgname-$pkgver.tar.gz::https://git.logicamp.dev/LogiCrew/doh-forwarder/archive/v${pkgver}.tar.gz"
|
source=("$pkgname::git+https://git.logicamp.dev/mohamad/doh-forwarder.git#tag=v${pkgver}"
|
||||||
'doh-forwarder.service'
|
'doh-forwarder.service'
|
||||||
'doh-forwarder.sysusers'
|
'doh-forwarder.sysusers'
|
||||||
|
'doh-forwarder.tmpfiles'
|
||||||
'doh-forwarder.conf')
|
'doh-forwarder.conf')
|
||||||
sha256sums=('SKIP'
|
sha256sums=('SKIP'
|
||||||
|
'SKIP'
|
||||||
'SKIP'
|
'SKIP'
|
||||||
'SKIP'
|
'SKIP'
|
||||||
'SKIP')
|
'SKIP')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$pkgname"
|
cd "$pkgname"
|
||||||
cargo build --release --locked
|
cargo build --release
|
||||||
}
|
}
|
||||||
|
|
||||||
check() {
|
check() {
|
||||||
cd "$pkgname"
|
cd "$pkgname"
|
||||||
cargo test --release --locked
|
cargo test --release
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
@@ -40,6 +42,7 @@ package() {
|
|||||||
# Systemd
|
# Systemd
|
||||||
install -Dm644 "$srcdir/doh-forwarder.service" "$pkgdir/usr/lib/systemd/system/doh-forwarder.service"
|
install -Dm644 "$srcdir/doh-forwarder.service" "$pkgdir/usr/lib/systemd/system/doh-forwarder.service"
|
||||||
install -Dm644 "$srcdir/doh-forwarder.sysusers" "$pkgdir/usr/lib/sysusers.d/doh-forwarder.conf"
|
install -Dm644 "$srcdir/doh-forwarder.sysusers" "$pkgdir/usr/lib/sysusers.d/doh-forwarder.conf"
|
||||||
|
install -Dm644 "$srcdir/doh-forwarder.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/doh-forwarder.conf"
|
||||||
|
|
||||||
# License
|
# License
|
||||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
|||||||
2
pkg/arch/doh-forwarder.tmpfiles
Normal file
2
pkg/arch/doh-forwarder.tmpfiles
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Type Path Mode User Group Age Argument
|
||||||
|
d /var/lib/doh-forwarder 0755 doh-forwarder doh-forwarder -
|
||||||
3
pkg/arch/doh-forwarder/FETCH_HEAD
Normal file
3
pkg/arch/doh-forwarder/FETCH_HEAD
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
b21ee5187d80c051928d1589287c815e6960cd0d not-for-merge branch 'main' of https://git.logicamp.dev/mohamad/doh-forwarder
|
||||||
|
b21ee5187d80c051928d1589287c815e6960cd0d not-for-merge tag 'v0.1.0' of https://git.logicamp.dev/mohamad/doh-forwarder
|
||||||
|
4c62b1436ae3b6770add63cce66312c478b1ef9e not-for-merge tag 'v0.1.0-rc.1' of https://git.logicamp.dev/mohamad/doh-forwarder
|
||||||
1
pkg/arch/doh-forwarder/HEAD
Normal file
1
pkg/arch/doh-forwarder/HEAD
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ref: refs/heads/main
|
||||||
9
pkg/arch/doh-forwarder/config
Normal file
9
pkg/arch/doh-forwarder/config
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = https://git.logicamp.dev/mohamad/doh-forwarder.git
|
||||||
|
tagOpt = --no-tags
|
||||||
|
fetch = +refs/*:refs/*
|
||||||
|
mirror = true
|
||||||
1
pkg/arch/doh-forwarder/description
Normal file
1
pkg/arch/doh-forwarder/description
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
15
pkg/arch/doh-forwarder/hooks/applypatch-msg.sample
Executable file
15
pkg/arch/doh-forwarder/hooks/applypatch-msg.sample
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to check the commit log message taken by
|
||||||
|
# applypatch from an e-mail message.
|
||||||
|
#
|
||||||
|
# The hook should exit with non-zero status after issuing an
|
||||||
|
# appropriate message if it wants to stop the commit. The hook is
|
||||||
|
# allowed to edit the commit message file.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "applypatch-msg".
|
||||||
|
|
||||||
|
. git-sh-setup
|
||||||
|
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||||
|
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||||
|
:
|
||||||
74
pkg/arch/doh-forwarder/hooks/commit-msg.sample
Executable file
74
pkg/arch/doh-forwarder/hooks/commit-msg.sample
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to check the commit log message.
|
||||||
|
# Called by "git commit" with one argument, the name of the file
|
||||||
|
# that has the commit message. The hook should exit with non-zero
|
||||||
|
# status after issuing an appropriate message if it wants to stop the
|
||||||
|
# commit. The hook is allowed to edit the commit message file.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "commit-msg".
|
||||||
|
|
||||||
|
# Uncomment the below to add a Signed-off-by line to the message.
|
||||||
|
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||||
|
# hook is more suited to it.
|
||||||
|
#
|
||||||
|
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||||
|
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||||
|
|
||||||
|
# This example catches duplicate Signed-off-by lines and messages that
|
||||||
|
# would confuse 'git am'.
|
||||||
|
|
||||||
|
ret=0
|
||||||
|
|
||||||
|
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||||
|
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||||
|
echo >&2 Duplicate Signed-off-by lines.
|
||||||
|
ret=1
|
||||||
|
}
|
||||||
|
|
||||||
|
comment_re="$(
|
||||||
|
{
|
||||||
|
git config --get-regexp "^core\.comment(char|string)\$" ||
|
||||||
|
echo '#'
|
||||||
|
} | sed -n -e '
|
||||||
|
${
|
||||||
|
s/^[^ ]* //
|
||||||
|
s|[][*./\]|\\&|g
|
||||||
|
s/^auto$/[#;@!$%^&|:]/
|
||||||
|
p
|
||||||
|
}'
|
||||||
|
)"
|
||||||
|
scissors_line="^${comment_re} -\{8,\} >8 -\{8,\}\$"
|
||||||
|
comment_line="^${comment_re}.*"
|
||||||
|
blank_line='^[ ]*$'
|
||||||
|
# Disallow lines starting with "diff -" or "Index: " in the body of the
|
||||||
|
# message. Stop looking if we see a scissors line.
|
||||||
|
line="$(sed -n -e "
|
||||||
|
# Skip comments and blank lines at the start of the file.
|
||||||
|
/${scissors_line}/q
|
||||||
|
/${comment_line}/d
|
||||||
|
/${blank_line}/d
|
||||||
|
# The first paragraph will become the subject header so
|
||||||
|
# does not need to be checked.
|
||||||
|
: subject
|
||||||
|
n
|
||||||
|
/${scissors_line}/q
|
||||||
|
/${blank_line}/!b subject
|
||||||
|
# Check the body of the message for problematic
|
||||||
|
# prefixes.
|
||||||
|
: body
|
||||||
|
n
|
||||||
|
/${scissors_line}/q
|
||||||
|
/${comment_line}/b body
|
||||||
|
/^diff -/{p;q;}
|
||||||
|
/^Index: /{p;q;}
|
||||||
|
b body
|
||||||
|
" "$1")"
|
||||||
|
if test -n "$line"
|
||||||
|
then
|
||||||
|
echo >&2 "Message contains a diff that will confuse 'git am'."
|
||||||
|
echo >&2 "To fix this indent the diff."
|
||||||
|
ret=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $ret
|
||||||
168
pkg/arch/doh-forwarder/hooks/fsmonitor-watchman.sample
Executable file
168
pkg/arch/doh-forwarder/hooks/fsmonitor-watchman.sample
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use IPC::Open2;
|
||||||
|
|
||||||
|
# An example hook script to integrate Watchman
|
||||||
|
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||||
|
# new and modified files.
|
||||||
|
#
|
||||||
|
# The hook is passed a version (currently 2) and last update token
|
||||||
|
# formatted as a string and outputs to stdout a new update token and
|
||||||
|
# all files that have been modified since the update token. Paths must
|
||||||
|
# be relative to the root of the working tree and separated by a single NUL.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "query-watchman" and set
|
||||||
|
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||||
|
#
|
||||||
|
my ($version, $last_update_token) = @ARGV;
|
||||||
|
|
||||||
|
# Uncomment for debugging
|
||||||
|
# print STDERR "$0 $version $last_update_token\n";
|
||||||
|
|
||||||
|
# Check the hook interface version
|
||||||
|
if ($version ne 2) {
|
||||||
|
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||||
|
"Falling back to scanning...\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $git_work_tree = get_working_dir();
|
||||||
|
|
||||||
|
my $json_pkg;
|
||||||
|
eval {
|
||||||
|
require JSON::XS;
|
||||||
|
$json_pkg = "JSON::XS";
|
||||||
|
1;
|
||||||
|
} or do {
|
||||||
|
require JSON::PP;
|
||||||
|
$json_pkg = "JSON::PP";
|
||||||
|
};
|
||||||
|
|
||||||
|
launch_watchman();
|
||||||
|
|
||||||
|
sub launch_watchman {
|
||||||
|
my $o = watchman_query();
|
||||||
|
if (is_work_tree_watched($o)) {
|
||||||
|
output_result($o->{clock}, @{$o->{files}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub output_result {
|
||||||
|
my ($clockid, @files) = @_;
|
||||||
|
|
||||||
|
# Uncomment for debugging watchman output
|
||||||
|
# open (my $fh, ">", ".git/watchman-output.out");
|
||||||
|
# binmode $fh, ":utf8";
|
||||||
|
# print $fh "$clockid\n@files\n";
|
||||||
|
# close $fh;
|
||||||
|
|
||||||
|
binmode STDOUT, ":utf8";
|
||||||
|
print $clockid;
|
||||||
|
print "\0";
|
||||||
|
local $, = "\0";
|
||||||
|
print @files;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub watchman_clock {
|
||||||
|
my $response = qx/watchman clock "$git_work_tree"/;
|
||||||
|
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||||
|
"Falling back to scanning...\n" if $? != 0;
|
||||||
|
|
||||||
|
return $json_pkg->new->utf8->decode($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub watchman_query {
|
||||||
|
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||||
|
or die "open2() failed: $!\n" .
|
||||||
|
"Falling back to scanning...\n";
|
||||||
|
|
||||||
|
# In the query expression below we're asking for names of files that
|
||||||
|
# changed since $last_update_token but not from the .git folder.
|
||||||
|
#
|
||||||
|
# To accomplish this, we're using the "since" generator to use the
|
||||||
|
# recency index to select candidate nodes and "fields" to limit the
|
||||||
|
# output to file names only. Then we're using the "expression" term to
|
||||||
|
# further constrain the results.
|
||||||
|
my $last_update_line = "";
|
||||||
|
if (substr($last_update_token, 0, 1) eq "c") {
|
||||||
|
$last_update_token = "\"$last_update_token\"";
|
||||||
|
$last_update_line = qq[\n"since": $last_update_token,];
|
||||||
|
}
|
||||||
|
my $query = <<" END";
|
||||||
|
["query", "$git_work_tree", {$last_update_line
|
||||||
|
"fields": ["name"],
|
||||||
|
"expression": ["not", ["dirname", ".git"]]
|
||||||
|
}]
|
||||||
|
END
|
||||||
|
|
||||||
|
# Uncomment for debugging the watchman query
|
||||||
|
# open (my $fh, ">", ".git/watchman-query.json");
|
||||||
|
# print $fh $query;
|
||||||
|
# close $fh;
|
||||||
|
|
||||||
|
print CHLD_IN $query;
|
||||||
|
close CHLD_IN;
|
||||||
|
my $response = do {local $/; <CHLD_OUT>};
|
||||||
|
|
||||||
|
# Uncomment for debugging the watch response
|
||||||
|
# open ($fh, ">", ".git/watchman-response.json");
|
||||||
|
# print $fh $response;
|
||||||
|
# close $fh;
|
||||||
|
|
||||||
|
die "Watchman: command returned no output.\n" .
|
||||||
|
"Falling back to scanning...\n" if $response eq "";
|
||||||
|
die "Watchman: command returned invalid output: $response\n" .
|
||||||
|
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||||
|
|
||||||
|
return $json_pkg->new->utf8->decode($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub is_work_tree_watched {
|
||||||
|
my ($output) = @_;
|
||||||
|
my $error = $output->{error};
|
||||||
|
if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||||
|
my $response = qx/watchman watch "$git_work_tree"/;
|
||||||
|
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||||
|
"Falling back to scanning...\n" if $? != 0;
|
||||||
|
$output = $json_pkg->new->utf8->decode($response);
|
||||||
|
$error = $output->{error};
|
||||||
|
die "Watchman: $error.\n" .
|
||||||
|
"Falling back to scanning...\n" if $error;
|
||||||
|
|
||||||
|
# Uncomment for debugging watchman output
|
||||||
|
# open (my $fh, ">", ".git/watchman-output.out");
|
||||||
|
# close $fh;
|
||||||
|
|
||||||
|
# Watchman will always return all files on the first query so
|
||||||
|
# return the fast "everything is dirty" flag to git and do the
|
||||||
|
# Watchman query just to get it over with now so we won't pay
|
||||||
|
# the cost in git to look up each individual file.
|
||||||
|
my $o = watchman_clock();
|
||||||
|
$error = $o->{error};
|
||||||
|
|
||||||
|
die "Watchman: $error.\n" .
|
||||||
|
"Falling back to scanning...\n" if $error;
|
||||||
|
|
||||||
|
output_result($o->{clock}, ("/"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
die "Watchman: $error.\n" .
|
||||||
|
"Falling back to scanning...\n" if $error;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_working_dir {
|
||||||
|
my $working_dir;
|
||||||
|
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||||
|
$working_dir = Win32::GetCwd();
|
||||||
|
$working_dir =~ tr/\\/\//;
|
||||||
|
} else {
|
||||||
|
require Cwd;
|
||||||
|
$working_dir = Cwd::cwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $working_dir;
|
||||||
|
}
|
||||||
8
pkg/arch/doh-forwarder/hooks/post-update.sample
Executable file
8
pkg/arch/doh-forwarder/hooks/post-update.sample
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to prepare a packed repository for use over
|
||||||
|
# dumb transports.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "post-update".
|
||||||
|
|
||||||
|
exec git update-server-info
|
||||||
14
pkg/arch/doh-forwarder/hooks/pre-applypatch.sample
Executable file
14
pkg/arch/doh-forwarder/hooks/pre-applypatch.sample
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to verify what is about to be committed
|
||||||
|
# by applypatch from an e-mail message.
|
||||||
|
#
|
||||||
|
# The hook should exit with non-zero status after issuing an
|
||||||
|
# appropriate message if it wants to stop the commit.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "pre-applypatch".
|
||||||
|
|
||||||
|
. git-sh-setup
|
||||||
|
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||||
|
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||||
|
:
|
||||||
49
pkg/arch/doh-forwarder/hooks/pre-commit.sample
Executable file
49
pkg/arch/doh-forwarder/hooks/pre-commit.sample
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to verify what is about to be committed.
|
||||||
|
# Called by "git commit" with no arguments. The hook should
|
||||||
|
# exit with non-zero status after issuing an appropriate message if
|
||||||
|
# it wants to stop the commit.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "pre-commit".
|
||||||
|
|
||||||
|
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
against=HEAD
|
||||||
|
else
|
||||||
|
# Initial commit: diff against an empty tree object
|
||||||
|
against=$(git hash-object -t tree /dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If you want to allow non-ASCII filenames set this variable to true.
|
||||||
|
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||||
|
|
||||||
|
# Redirect output to stderr.
|
||||||
|
exec 1>&2
|
||||||
|
|
||||||
|
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||||
|
# them from being added to the repository. We exploit the fact that the
|
||||||
|
# printable range starts at the space character and ends with tilde.
|
||||||
|
if [ "$allownonascii" != "true" ] &&
|
||||||
|
# Note that the use of brackets around a tr range is ok here, (it's
|
||||||
|
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||||
|
# the square bracket bytes happen to fall in the designated range.
|
||||||
|
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||||
|
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||||
|
then
|
||||||
|
cat <<\EOF
|
||||||
|
Error: Attempt to add a non-ASCII file name.
|
||||||
|
|
||||||
|
This can cause problems if you want to work with people on other platforms.
|
||||||
|
|
||||||
|
To be portable it is advisable to rename the file.
|
||||||
|
|
||||||
|
If you know what you are doing you can disable this check using:
|
||||||
|
|
||||||
|
git config hooks.allownonascii true
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If there are whitespace errors, print the offending file names and fail.
|
||||||
|
exec git diff-index --check --cached $against --
|
||||||
13
pkg/arch/doh-forwarder/hooks/pre-merge-commit.sample
Executable file
13
pkg/arch/doh-forwarder/hooks/pre-merge-commit.sample
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to verify what is about to be committed.
|
||||||
|
# Called by "git merge" with no arguments. The hook should
|
||||||
|
# exit with non-zero status after issuing an appropriate message to
|
||||||
|
# stderr if it wants to stop the merge commit.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "pre-merge-commit".
|
||||||
|
|
||||||
|
. git-sh-setup
|
||||||
|
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||||
|
exec "$GIT_DIR/hooks/pre-commit"
|
||||||
|
:
|
||||||
53
pkg/arch/doh-forwarder/hooks/pre-push.sample
Executable file
53
pkg/arch/doh-forwarder/hooks/pre-push.sample
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# An example hook script to verify what is about to be pushed. Called by "git
|
||||||
|
# push" after it has checked the remote status, but before anything has been
|
||||||
|
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||||
|
#
|
||||||
|
# This hook is called with the following parameters:
|
||||||
|
#
|
||||||
|
# $1 -- Name of the remote to which the push is being done
|
||||||
|
# $2 -- URL to which the push is being done
|
||||||
|
#
|
||||||
|
# If pushing without using a named remote those arguments will be equal.
|
||||||
|
#
|
||||||
|
# Information about the commits which are being pushed is supplied as lines to
|
||||||
|
# the standard input in the form:
|
||||||
|
#
|
||||||
|
# <local ref> <local oid> <remote ref> <remote oid>
|
||||||
|
#
|
||||||
|
# This sample shows how to prevent push of commits where the log message starts
|
||||||
|
# with "WIP" (work in progress).
|
||||||
|
|
||||||
|
remote="$1"
|
||||||
|
url="$2"
|
||||||
|
|
||||||
|
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||||
|
|
||||||
|
while read local_ref local_oid remote_ref remote_oid
|
||||||
|
do
|
||||||
|
if test "$local_oid" = "$zero"
|
||||||
|
then
|
||||||
|
# Handle delete
|
||||||
|
:
|
||||||
|
else
|
||||||
|
if test "$remote_oid" = "$zero"
|
||||||
|
then
|
||||||
|
# New branch, examine all commits
|
||||||
|
range="$local_oid"
|
||||||
|
else
|
||||||
|
# Update to existing branch, examine new commits
|
||||||
|
range="$remote_oid..$local_oid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for WIP commit
|
||||||
|
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||||
|
if test -n "$commit"
|
||||||
|
then
|
||||||
|
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
169
pkg/arch/doh-forwarder/hooks/pre-rebase.sample
Executable file
169
pkg/arch/doh-forwarder/hooks/pre-rebase.sample
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||||
|
#
|
||||||
|
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||||
|
# its job, and can prevent the command from running by exiting with
|
||||||
|
# non-zero status.
|
||||||
|
#
|
||||||
|
# The hook is called with the following parameters:
|
||||||
|
#
|
||||||
|
# $1 -- the upstream the series was forked from.
|
||||||
|
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||||
|
#
|
||||||
|
# This sample shows how to prevent topic branches that are already
|
||||||
|
# merged to 'next' branch from getting rebased, because allowing it
|
||||||
|
# would result in rebasing already published history.
|
||||||
|
|
||||||
|
publish=next
|
||||||
|
basebranch="$1"
|
||||||
|
if test "$#" = 2
|
||||||
|
then
|
||||||
|
topic="refs/heads/$2"
|
||||||
|
else
|
||||||
|
topic=`git symbolic-ref HEAD` ||
|
||||||
|
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$topic" in
|
||||||
|
refs/heads/??/*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exit 0 ;# we do not interrupt others.
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Now we are dealing with a topic branch being rebased
|
||||||
|
# on top of master. Is it OK to rebase it?
|
||||||
|
|
||||||
|
# Does the topic really exist?
|
||||||
|
git show-ref -q "$topic" || {
|
||||||
|
echo >&2 "No such branch $topic"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Is topic fully merged to master?
|
||||||
|
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||||
|
if test -z "$not_in_master"
|
||||||
|
then
|
||||||
|
echo >&2 "$topic is fully merged to master; better remove it."
|
||||||
|
exit 1 ;# we could allow it, but there is no point.
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||||
|
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||||
|
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||||
|
if test "$only_next_1" = "$only_next_2"
|
||||||
|
then
|
||||||
|
not_in_topic=`git rev-list "^$topic" master`
|
||||||
|
if test -z "$not_in_topic"
|
||||||
|
then
|
||||||
|
echo >&2 "$topic is already up to date with master"
|
||||||
|
exit 1 ;# we could allow it, but there is no point.
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||||
|
/usr/bin/perl -e '
|
||||||
|
my $topic = $ARGV[0];
|
||||||
|
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||||
|
my (%not_in_next) = map {
|
||||||
|
/^([0-9a-f]+) /;
|
||||||
|
($1 => 1);
|
||||||
|
} split(/\n/, $ARGV[1]);
|
||||||
|
for my $elem (map {
|
||||||
|
/^([0-9a-f]+) (.*)$/;
|
||||||
|
[$1 => $2];
|
||||||
|
} split(/\n/, $ARGV[2])) {
|
||||||
|
if (!exists $not_in_next{$elem->[0]}) {
|
||||||
|
if ($msg) {
|
||||||
|
print STDERR $msg;
|
||||||
|
undef $msg;
|
||||||
|
}
|
||||||
|
print STDERR " $elem->[1]\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$topic" "$not_in_next" "$not_in_master"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
<<\DOC_END
|
||||||
|
|
||||||
|
This sample hook safeguards topic branches that have been
|
||||||
|
published from being rewound.
|
||||||
|
|
||||||
|
The workflow assumed here is:
|
||||||
|
|
||||||
|
* Once a topic branch forks from "master", "master" is never
|
||||||
|
merged into it again (either directly or indirectly).
|
||||||
|
|
||||||
|
* Once a topic branch is fully cooked and merged into "master",
|
||||||
|
it is deleted. If you need to build on top of it to correct
|
||||||
|
earlier mistakes, a new topic branch is created by forking at
|
||||||
|
the tip of the "master". This is not strictly necessary, but
|
||||||
|
it makes it easier to keep your history simple.
|
||||||
|
|
||||||
|
* Whenever you need to test or publish your changes to topic
|
||||||
|
branches, merge them into "next" branch.
|
||||||
|
|
||||||
|
The script, being an example, hardcodes the publish branch name
|
||||||
|
to be "next", but it is trivial to make it configurable via
|
||||||
|
$GIT_DIR/config mechanism.
|
||||||
|
|
||||||
|
With this workflow, you would want to know:
|
||||||
|
|
||||||
|
(1) ... if a topic branch has ever been merged to "next". Young
|
||||||
|
topic branches can have stupid mistakes you would rather
|
||||||
|
clean up before publishing, and things that have not been
|
||||||
|
merged into other branches can be easily rebased without
|
||||||
|
affecting other people. But once it is published, you would
|
||||||
|
not want to rewind it.
|
||||||
|
|
||||||
|
(2) ... if a topic branch has been fully merged to "master".
|
||||||
|
Then you can delete it. More importantly, you should not
|
||||||
|
build on top of it -- other people may already want to
|
||||||
|
change things related to the topic as patches against your
|
||||||
|
"master", so if you need further changes, it is better to
|
||||||
|
fork the topic (perhaps with the same name) afresh from the
|
||||||
|
tip of "master".
|
||||||
|
|
||||||
|
Let's look at this example:
|
||||||
|
|
||||||
|
o---o---o---o---o---o---o---o---o---o "next"
|
||||||
|
/ / / /
|
||||||
|
/ a---a---b A / /
|
||||||
|
/ / / /
|
||||||
|
/ / c---c---c---c B /
|
||||||
|
/ / / \ /
|
||||||
|
/ / / b---b C \ /
|
||||||
|
/ / / / \ /
|
||||||
|
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||||
|
|
||||||
|
|
||||||
|
A, B and C are topic branches.
|
||||||
|
|
||||||
|
* A has one fix since it was merged up to "next".
|
||||||
|
|
||||||
|
* B has finished. It has been fully merged up to "master" and "next",
|
||||||
|
and is ready to be deleted.
|
||||||
|
|
||||||
|
* C has not merged to "next" at all.
|
||||||
|
|
||||||
|
We would want to allow C to be rebased, refuse A, and encourage
|
||||||
|
B to be deleted.
|
||||||
|
|
||||||
|
To compute (1):
|
||||||
|
|
||||||
|
git rev-list ^master ^topic next
|
||||||
|
git rev-list ^master next
|
||||||
|
|
||||||
|
if these match, topic has not merged in next at all.
|
||||||
|
|
||||||
|
To compute (2):
|
||||||
|
|
||||||
|
git rev-list master..topic
|
||||||
|
|
||||||
|
if this is empty, it is fully merged to "master".
|
||||||
|
|
||||||
|
DOC_END
|
||||||
24
pkg/arch/doh-forwarder/hooks/pre-receive.sample
Executable file
24
pkg/arch/doh-forwarder/hooks/pre-receive.sample
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to make use of push options.
|
||||||
|
# The example simply echoes all push options that start with 'echoback='
|
||||||
|
# and rejects all pushes when the "reject" push option is used.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "pre-receive".
|
||||||
|
|
||||||
|
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||||
|
then
|
||||||
|
i=0
|
||||||
|
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||||
|
do
|
||||||
|
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||||
|
case "$value" in
|
||||||
|
echoback=*)
|
||||||
|
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||||
|
;;
|
||||||
|
reject)
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
fi
|
||||||
42
pkg/arch/doh-forwarder/hooks/prepare-commit-msg.sample
Executable file
42
pkg/arch/doh-forwarder/hooks/prepare-commit-msg.sample
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to prepare the commit log message.
|
||||||
|
# Called by "git commit" with the name of the file that has the
|
||||||
|
# commit message, followed by the description of the commit
|
||||||
|
# message's source. The hook's purpose is to edit the commit
|
||||||
|
# message file. If the hook fails with a non-zero status,
|
||||||
|
# the commit is aborted.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||||
|
|
||||||
|
# This hook includes three examples. The first one removes the
|
||||||
|
# "# Please enter the commit message..." help message.
|
||||||
|
#
|
||||||
|
# The second includes the output of "git diff --name-status -r"
|
||||||
|
# into the message, just before the "git status" output. It is
|
||||||
|
# commented because it doesn't cope with --amend or with squashed
|
||||||
|
# commits.
|
||||||
|
#
|
||||||
|
# The third example adds a Signed-off-by line to the message, that can
|
||||||
|
# still be edited. This is rarely a good idea.
|
||||||
|
|
||||||
|
COMMIT_MSG_FILE=$1
|
||||||
|
COMMIT_SOURCE=$2
|
||||||
|
SHA1=$3
|
||||||
|
|
||||||
|
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||||
|
|
||||||
|
# case "$COMMIT_SOURCE,$SHA1" in
|
||||||
|
# ,|template,)
|
||||||
|
# /usr/bin/perl -i.bak -pe '
|
||||||
|
# print "\n" . `git diff --cached --name-status -r`
|
||||||
|
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||||
|
# *) ;;
|
||||||
|
# esac
|
||||||
|
|
||||||
|
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||||
|
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||||
|
# if test -z "$COMMIT_SOURCE"
|
||||||
|
# then
|
||||||
|
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||||
|
# fi
|
||||||
78
pkg/arch/doh-forwarder/hooks/push-to-checkout.sample
Executable file
78
pkg/arch/doh-forwarder/hooks/push-to-checkout.sample
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# An example hook script to update a checked-out tree on a git push.
|
||||||
|
#
|
||||||
|
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||||
|
# push and updates reference(s) in its repository, and when the push
|
||||||
|
# tries to update the branch that is currently checked out and the
|
||||||
|
# receive.denyCurrentBranch configuration variable is set to
|
||||||
|
# updateInstead.
|
||||||
|
#
|
||||||
|
# By default, such a push is refused if the working tree and the index
|
||||||
|
# of the remote repository has any difference from the currently
|
||||||
|
# checked out commit; when both the working tree and the index match
|
||||||
|
# the current commit, they are updated to match the newly pushed tip
|
||||||
|
# of the branch. This hook is to be used to override the default
|
||||||
|
# behaviour; however the code below reimplements the default behaviour
|
||||||
|
# as a starting point for convenient modification.
|
||||||
|
#
|
||||||
|
# The hook receives the commit with which the tip of the current
|
||||||
|
# branch is going to be updated:
|
||||||
|
commit=$1
|
||||||
|
|
||||||
|
# It can exit with a non-zero status to refuse the push (when it does
|
||||||
|
# so, it must not modify the index or the working tree).
|
||||||
|
die () {
|
||||||
|
echo >&2 "$*"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Or it can make any necessary changes to the working tree and to the
|
||||||
|
# index to bring them to the desired state when the tip of the current
|
||||||
|
# branch is updated to the new commit, and exit with a zero status.
|
||||||
|
#
|
||||||
|
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||||
|
# in order to emulate git fetch that is run in the reverse direction
|
||||||
|
# with git push, as the two-tree form of git read-tree -u -m is
|
||||||
|
# essentially the same as git switch or git checkout that switches
|
||||||
|
# branches while keeping the local changes in the working tree that do
|
||||||
|
# not interfere with the difference between the branches.
|
||||||
|
|
||||||
|
# The below is a more-or-less exact translation to shell of the C code
|
||||||
|
# for the default behaviour for git's push-to-checkout hook defined in
|
||||||
|
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||||
|
#
|
||||||
|
# Note that the hook will be executed from the repository directory,
|
||||||
|
# not from the working tree, so if you want to perform operations on
|
||||||
|
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||||
|
# by adding "cd .." or using relative paths.
|
||||||
|
|
||||||
|
if ! git update-index -q --ignore-submodules --refresh
|
||||||
|
then
|
||||||
|
die "Up-to-date check failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git diff-files --quiet --ignore-submodules --
|
||||||
|
then
|
||||||
|
die "Working directory has unstaged changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This is a rough translation of:
|
||||||
|
#
|
||||||
|
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||||
|
if git cat-file -e HEAD 2>/dev/null
|
||||||
|
then
|
||||||
|
head=HEAD
|
||||||
|
else
|
||||||
|
head=$(git hash-object -t tree --stdin </dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||||
|
then
|
||||||
|
die "Working directory has staged changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git read-tree -u -m "$commit"
|
||||||
|
then
|
||||||
|
die "Could not update working tree to new HEAD"
|
||||||
|
fi
|
||||||
77
pkg/arch/doh-forwarder/hooks/sendemail-validate.sample
Executable file
77
pkg/arch/doh-forwarder/hooks/sendemail-validate.sample
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# An example hook script to validate a patch (and/or patch series) before
|
||||||
|
# sending it via email.
|
||||||
|
#
|
||||||
|
# The hook should exit with non-zero status after issuing an appropriate
|
||||||
|
# message if it wants to prevent the email(s) from being sent.
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "sendemail-validate".
|
||||||
|
#
|
||||||
|
# By default, it will only check that the patch(es) can be applied on top of
|
||||||
|
# the default upstream branch without conflicts in a secondary worktree. After
|
||||||
|
# validation (successful or not) of the last patch of a series, the worktree
|
||||||
|
# will be deleted.
|
||||||
|
#
|
||||||
|
# The following config variables can be set to change the default remote and
|
||||||
|
# remote ref that are used to apply the patches against:
|
||||||
|
#
|
||||||
|
# sendemail.validateRemote (default: origin)
|
||||||
|
# sendemail.validateRemoteRef (default: HEAD)
|
||||||
|
#
|
||||||
|
# Replace the TODO placeholders with appropriate checks according to your
|
||||||
|
# needs.
|
||||||
|
|
||||||
|
validate_cover_letter () {
|
||||||
|
file="$1"
|
||||||
|
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_patch () {
|
||||||
|
file="$1"
|
||||||
|
# Ensure that the patch applies without conflicts.
|
||||||
|
git am -3 "$file" || return
|
||||||
|
# TODO: Replace with appropriate checks for this patch
|
||||||
|
# (e.g. checkpatch.pl).
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_series () {
|
||||||
|
# TODO: Replace with appropriate checks for the whole series
|
||||||
|
# (e.g. quick build, coding style checks, etc.).
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
# main -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||||
|
then
|
||||||
|
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||||
|
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||||
|
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||||
|
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||||
|
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||||
|
else
|
||||||
|
worktree=$(git config --get sendemail.validateWorktree)
|
||||||
|
fi || {
|
||||||
|
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
unset GIT_DIR GIT_WORK_TREE
|
||||||
|
cd "$worktree" &&
|
||||||
|
|
||||||
|
if grep -q "^diff --git " "$1"
|
||||||
|
then
|
||||||
|
validate_patch "$1"
|
||||||
|
else
|
||||||
|
validate_cover_letter "$1"
|
||||||
|
fi &&
|
||||||
|
|
||||||
|
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||||
|
then
|
||||||
|
git config --unset-all sendemail.validateWorktree &&
|
||||||
|
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||||
|
validate_series
|
||||||
|
fi
|
||||||
128
pkg/arch/doh-forwarder/hooks/update.sample
Executable file
128
pkg/arch/doh-forwarder/hooks/update.sample
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# An example hook script to block unannotated tags from entering.
|
||||||
|
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||||
|
#
|
||||||
|
# To enable this hook, rename this file to "update".
|
||||||
|
#
|
||||||
|
# Config
|
||||||
|
# ------
|
||||||
|
# hooks.allowunannotated
|
||||||
|
# This boolean sets whether unannotated tags will be allowed into the
|
||||||
|
# repository. By default they won't be.
|
||||||
|
# hooks.allowdeletetag
|
||||||
|
# This boolean sets whether deleting tags will be allowed in the
|
||||||
|
# repository. By default they won't be.
|
||||||
|
# hooks.allowmodifytag
|
||||||
|
# This boolean sets whether a tag may be modified after creation. By default
|
||||||
|
# it won't be.
|
||||||
|
# hooks.allowdeletebranch
|
||||||
|
# This boolean sets whether deleting branches will be allowed in the
|
||||||
|
# repository. By default they won't be.
|
||||||
|
# hooks.denycreatebranch
|
||||||
|
# This boolean sets whether remotely creating branches will be denied
|
||||||
|
# in the repository. By default this is allowed.
|
||||||
|
#
|
||||||
|
|
||||||
|
# --- Command line
|
||||||
|
refname="$1"
|
||||||
|
oldrev="$2"
|
||||||
|
newrev="$3"
|
||||||
|
|
||||||
|
# --- Safety check
|
||||||
|
if [ -z "$GIT_DIR" ]; then
|
||||||
|
echo "Don't run this script from the command line." >&2
|
||||||
|
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||||
|
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||||
|
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Config
|
||||||
|
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||||
|
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||||
|
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||||
|
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||||
|
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||||
|
|
||||||
|
# check for no description
|
||||||
|
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||||
|
case "$projectdesc" in
|
||||||
|
"Unnamed repository"* | "")
|
||||||
|
echo "*** Project description file hasn't been set" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- Check types
|
||||||
|
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||||
|
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||||
|
if [ "$newrev" = "$zero" ]; then
|
||||||
|
newrev_type=delete
|
||||||
|
else
|
||||||
|
newrev_type=$(git cat-file -t $newrev)
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$refname","$newrev_type" in
|
||||||
|
refs/tags/*,commit)
|
||||||
|
# un-annotated tag
|
||||||
|
short_refname=${refname##refs/tags/}
|
||||||
|
if [ "$allowunannotated" != "true" ]; then
|
||||||
|
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||||
|
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/tags/*,delete)
|
||||||
|
# delete tag
|
||||||
|
if [ "$allowdeletetag" != "true" ]; then
|
||||||
|
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/tags/*,tag)
|
||||||
|
# annotated tag
|
||||||
|
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo "*** Tag '$refname' already exists." >&2
|
||||||
|
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/heads/*,commit)
|
||||||
|
# branch
|
||||||
|
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||||
|
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/heads/*,delete)
|
||||||
|
# delete branch
|
||||||
|
if [ "$allowdeletebranch" != "true" ]; then
|
||||||
|
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
refs/remotes/*,commit)
|
||||||
|
# tracking branch
|
||||||
|
;;
|
||||||
|
refs/remotes/*,delete)
|
||||||
|
# delete tracking branch
|
||||||
|
if [ "$allowdeletebranch" != "true" ]; then
|
||||||
|
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Anything else (is there anything else?)
|
||||||
|
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- Finished
|
||||||
|
exit 0
|
||||||
1
pkg/arch/doh-forwarder/info/attributes
Normal file
1
pkg/arch/doh-forwarder/info/attributes
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* -export-subst -export-ignore
|
||||||
6
pkg/arch/doh-forwarder/info/exclude
Normal file
6
pkg/arch/doh-forwarder/info/exclude
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# git ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
# Lines that start with '#' are comments.
|
||||||
|
# For a project mostly in C, the following would be a good set of
|
||||||
|
# exclude patterns (uncomment them if you want to use them):
|
||||||
|
# *.[oa]
|
||||||
|
# *~
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
x+)JMU06d01000PH,J<>`<60>y<EFBFBD><79>|<7C>4<EFBFBD><34><EFBFBD>M%<25>gM<67>e<>ُ<01>
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4
pkg/arch/doh-forwarder/packed-refs
Normal file
4
pkg/arch/doh-forwarder/packed-refs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
2726400f5d2804683449d3e65f863515a68726a6 refs/heads/main
|
||||||
|
2726400f5d2804683449d3e65f863515a68726a6 refs/tags/v0.1.0
|
||||||
|
4c62b1436ae3b6770add63cce66312c478b1ef9e refs/tags/v0.1.0-rc.1
|
||||||
1
pkg/arch/doh-forwarder/refs/heads/main
Normal file
1
pkg/arch/doh-forwarder/refs/heads/main
Normal file
@@ -0,0 +1 @@
|
|||||||
|
b21ee5187d80c051928d1589287c815e6960cd0d
|
||||||
1
pkg/arch/doh-forwarder/refs/tags/v0.1.0
Normal file
1
pkg/arch/doh-forwarder/refs/tags/v0.1.0
Normal file
@@ -0,0 +1 @@
|
|||||||
|
b21ee5187d80c051928d1589287c815e6960cd0d
|
||||||
Reference in New Issue
Block a user