mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
Refactor trusted-proxies: modernize utils, improve safety, and fix clippy lints (#1693)
Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com> Co-authored-by: GatewayJ <835269233@qq.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
This commit is contained in:
1
.github/copilot-instructions.md
vendored
Symbolic link
1
.github/copilot-instructions.md
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../AGENTS.md
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,3 +39,4 @@ artifacts/
|
|||||||
PR_DESCRIPTION.md
|
PR_DESCRIPTION.md
|
||||||
IMPLEMENTATION_PLAN.md
|
IMPLEMENTATION_PLAN.md
|
||||||
scripts/s3-tests/selected_tests.txt
|
scripts/s3-tests/selected_tests.txt
|
||||||
|
docs
|
||||||
182
Cargo.lock
generated
182
Cargo.lock
generated
@@ -655,7 +655,7 @@ dependencies = [
|
|||||||
"aws-sdk-ssooidc",
|
"aws-sdk-ssooidc",
|
||||||
"aws-sdk-sts",
|
"aws-sdk-sts",
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-json",
|
"aws-smithy-json",
|
||||||
"aws-smithy-runtime",
|
"aws-smithy-runtime",
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
@@ -718,7 +718,7 @@ dependencies = [
|
|||||||
"aws-sigv4",
|
"aws-sigv4",
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-eventstream",
|
"aws-smithy-eventstream",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-runtime",
|
"aws-smithy-runtime",
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
@@ -745,7 +745,7 @@ dependencies = [
|
|||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-checksums",
|
"aws-smithy-checksums",
|
||||||
"aws-smithy-eventstream",
|
"aws-smithy-eventstream",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-json",
|
"aws-smithy-json",
|
||||||
"aws-smithy-observability",
|
"aws-smithy-observability",
|
||||||
"aws-smithy-runtime",
|
"aws-smithy-runtime",
|
||||||
@@ -777,7 +777,7 @@ dependencies = [
|
|||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-json",
|
"aws-smithy-json",
|
||||||
"aws-smithy-observability",
|
"aws-smithy-observability",
|
||||||
"aws-smithy-runtime",
|
"aws-smithy-runtime",
|
||||||
@@ -800,7 +800,7 @@ dependencies = [
|
|||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-json",
|
"aws-smithy-json",
|
||||||
"aws-smithy-observability",
|
"aws-smithy-observability",
|
||||||
"aws-smithy-runtime",
|
"aws-smithy-runtime",
|
||||||
@@ -823,7 +823,7 @@ dependencies = [
|
|||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-json",
|
"aws-smithy-json",
|
||||||
"aws-smithy-observability",
|
"aws-smithy-observability",
|
||||||
"aws-smithy-query",
|
"aws-smithy-query",
|
||||||
@@ -846,7 +846,7 @@ checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-smithy-eventstream",
|
"aws-smithy-eventstream",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -868,9 +868,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-async"
|
name = "aws-smithy-async"
|
||||||
version = "1.2.9"
|
version = "1.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e0e99800414b0c4cae85ed775a1559f8992f4e69f5ebafe9c936e29609eae78"
|
checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -883,7 +883,7 @@ version = "0.63.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23374b9170cbbcc6f5df8dc5ebb9b6c5c28a3c8f599f0e8b8b10eb6f4a5c6e74"
|
checksum = "23374b9170cbbcc6f5df8dc5ebb9b6c5c28a3c8f599f0e8b8b10eb6f4a5c6e74"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.62.6",
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"bytes",
|
"bytes",
|
||||||
"crc-fast",
|
"crc-fast",
|
||||||
@@ -899,9 +899,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-eventstream"
|
name = "aws-smithy-eventstream"
|
||||||
version = "0.60.16"
|
version = "0.60.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1adc2eb689a24741c9dcc21ec19be78839a7899594883d3305cf4c7abae9b3d"
|
checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -931,10 +931,31 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-http-client"
|
name = "aws-smithy-http"
|
||||||
version = "1.1.7"
|
version = "0.63.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23141f8daeab46574a1969ddb7316bc2928732e5721b3abfa8d1e16927ea9a52"
|
checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc"
|
||||||
|
dependencies = [
|
||||||
|
"aws-smithy-runtime-api",
|
||||||
|
"aws-smithy-types",
|
||||||
|
"bytes",
|
||||||
|
"bytes-utils",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.4.0",
|
||||||
|
"http-body 1.0.1",
|
||||||
|
"http-body-util",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-smithy-http-client"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
@@ -965,18 +986,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-observability"
|
name = "aws-smithy-observability"
|
||||||
version = "0.2.2"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112e30b3c5379273de88c8cedfc96ce0211e9af22115ceb6975b5c072eccdfb9"
|
checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-query"
|
name = "aws-smithy-query"
|
||||||
version = "0.60.11"
|
version = "0.60.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a57c5122eafc566cba4d3cbaacb53dd8a0cacd71155c728d1f4a9179cdd75ae"
|
checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
@@ -984,12 +1005,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-runtime"
|
name = "aws-smithy-runtime"
|
||||||
version = "1.9.8"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb5b6167fcdf47399024e81ac08e795180c576a20e4d4ce67949f9a88ae37dc1"
|
checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http 0.63.3",
|
||||||
"aws-smithy-http-client",
|
"aws-smithy-http-client",
|
||||||
"aws-smithy-observability",
|
"aws-smithy-observability",
|
||||||
"aws-smithy-runtime-api",
|
"aws-smithy-runtime-api",
|
||||||
@@ -1000,6 +1021,7 @@ dependencies = [
|
|||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
|
"http-body-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1008,9 +1030,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-runtime-api"
|
name = "aws-smithy-runtime-api"
|
||||||
version = "1.11.1"
|
version = "1.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d09ba34c17c65a53b65b0ec0d80cf7a934947d407cb7c4fb7753f96de147663"
|
checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-types",
|
"aws-smithy-types",
|
||||||
@@ -1025,9 +1047,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-types"
|
name = "aws-smithy-types"
|
||||||
version = "1.4.1"
|
version = "1.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8169129deda9dc18731b7b160f75121507e02f45f2101e48f0252dcd997e9da1"
|
checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1341,9 +1363,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.24.0"
|
version = "1.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -1727,18 +1749,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-str"
|
name = "const-str"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93e19f68b180ebff43d6d42005c4b5f046c65fcac28369ba8b3beaad633f9ec0"
|
checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-str-proc-macro",
|
"const-str-proc-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-str-proc-macro"
|
name = "const-str-proc-macro"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d3e0f24ee268386bd3ab4e04fc60df9a818ad801b5ffe592f388a6acc5053fb"
|
checksum = "1c7e7913ec01ed98b697e62f8d3fd63c86dc6cccaf983c7eebc64d0e563b0ad9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1786,6 +1808,15 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -3111,7 +3142,7 @@ version = "2.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case 0.10.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
@@ -4493,14 +4524,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.19"
|
version = "0.1.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
@@ -5294,9 +5324,9 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "local-ip-address"
|
name = "local-ip-address"
|
||||||
version = "0.6.9"
|
version = "0.6.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92488bc8a0f99ee9f23577bdd06526d49657df8bd70504c61f812337cdad01ab"
|
checksum = "79ef8c257c92ade496781a32a581d43e3d512cf8ce714ecf04ea80f93ed0ff4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"neli",
|
"neli",
|
||||||
@@ -6544,15 +6574,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.0"
|
version = "1.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
@@ -7354,7 +7384,7 @@ dependencies = [
|
|||||||
"pastey 0.2.1",
|
"pastey 0.2.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rmcp-macros",
|
"rmcp-macros",
|
||||||
"schemars 1.2.0",
|
"schemars 1.2.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -7660,6 +7690,7 @@ dependencies = [
|
|||||||
"rustfs-s3select-query",
|
"rustfs-s3select-query",
|
||||||
"rustfs-scanner",
|
"rustfs-scanner",
|
||||||
"rustfs-targets",
|
"rustfs-targets",
|
||||||
|
"rustfs-trusted-proxies",
|
||||||
"rustfs-utils",
|
"rustfs-utils",
|
||||||
"rustfs-zip",
|
"rustfs-zip",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -8021,7 +8052,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"schemars 1.2.0",
|
"schemars 1.2.1",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -8265,6 +8296,28 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustfs-trusted-proxies"
|
||||||
|
version = "0.0.5"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"http 1.4.0",
|
||||||
|
"ipnetwork",
|
||||||
|
"metrics",
|
||||||
|
"moka",
|
||||||
|
"regex",
|
||||||
|
"reqwest 0.13.1",
|
||||||
|
"rustfs-config",
|
||||||
|
"rustfs-utils",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-utils"
|
name = "rustfs-utils"
|
||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
@@ -8273,7 +8326,7 @@ dependencies = [
|
|||||||
"blake3",
|
"blake3",
|
||||||
"brotli",
|
"brotli",
|
||||||
"bytes",
|
"bytes",
|
||||||
"convert_case",
|
"convert_case 0.11.0",
|
||||||
"crc-fast",
|
"crc-fast",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -8511,7 +8564,7 @@ checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "s3s"
|
name = "s3s"
|
||||||
version = "0.13.0-alpha.2"
|
version = "0.13.0-alpha.2"
|
||||||
source = "git+https://github.com/s3s-project/s3s.git?rev=3cdb3fe22fe8a1b7fc3f71ead4beacac2683ba7f#3cdb3fe22fe8a1b7fc3f71ead4beacac2683ba7f"
|
source = "git+https://github.com/rustfs/s3s.git?branch=dependabot%2Fcargo%2Fdep-0202#13f968fc89e3bd286d308ca855456b23734d4f74"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
@@ -8554,6 +8607,7 @@ dependencies = [
|
|||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
"transform-stream",
|
"transform-stream",
|
||||||
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -8608,9 +8662,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.2.0"
|
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 = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
@@ -8622,9 +8676,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "1.2.0"
|
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 = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45"
|
checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -8845,7 +8899,7 @@ dependencies = [
|
|||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.2.0",
|
"schemars 1.2.1",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with_macros",
|
"serde_with_macros",
|
||||||
@@ -9054,9 +9108,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slog"
|
name = "slog"
|
||||||
@@ -9339,15 +9393,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starshard"
|
name = "starshard"
|
||||||
version = "0.6.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a88b6e011736aa3523f5962c02ba2d6c4cff35d97a0a9a3999afa6111d704a76"
|
checksum = "6b3a2034ea62d2981c3bdeb21002f07707952ff3bd4594aa39f86ae38ea27dc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9602,9 +9658,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
@@ -11145,18 +11201,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.36"
|
version = "0.8.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.36"
|
version = "0.8.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -11273,9 +11329,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.17"
|
version = "1.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -17,6 +17,7 @@ members = [
|
|||||||
"rustfs", # Core file system implementation
|
"rustfs", # Core file system implementation
|
||||||
"crates/appauth", # Application authentication and authorization
|
"crates/appauth", # Application authentication and authorization
|
||||||
"crates/audit", # Audit target management system with multi-target fan-out
|
"crates/audit", # Audit target management system with multi-target fan-out
|
||||||
|
"crates/checksums", # client checksums
|
||||||
"crates/common", # Shared utilities and data structures
|
"crates/common", # Shared utilities and data structures
|
||||||
"crates/config", # Configuration management
|
"crates/config", # Configuration management
|
||||||
"crates/credentials", # Credential management system
|
"crates/credentials", # Credential management system
|
||||||
@@ -24,26 +25,26 @@ members = [
|
|||||||
"crates/ecstore", # Erasure coding storage implementation
|
"crates/ecstore", # Erasure coding storage implementation
|
||||||
"crates/e2e_test", # End-to-end test suite
|
"crates/e2e_test", # End-to-end test suite
|
||||||
"crates/filemeta", # File metadata management
|
"crates/filemeta", # File metadata management
|
||||||
|
"crates/heal", # Erasure set and object healing
|
||||||
"crates/iam", # Identity and Access Management
|
"crates/iam", # Identity and Access Management
|
||||||
|
"crates/kms", # Key Management Service
|
||||||
"crates/lock", # Distributed locking implementation
|
"crates/lock", # Distributed locking implementation
|
||||||
"crates/madmin", # Management dashboard and admin API interface
|
"crates/madmin", # Management dashboard and admin API interface
|
||||||
|
"crates/mcp", # MCP server for S3 operations
|
||||||
"crates/notify", # Notification system for events
|
"crates/notify", # Notification system for events
|
||||||
"crates/obs", # Observability utilities
|
"crates/obs", # Observability utilities
|
||||||
"crates/policy", # Policy management
|
"crates/policy", # Policy management
|
||||||
"crates/protos", # Protocol buffer definitions
|
"crates/protos", # Protocol buffer definitions
|
||||||
"crates/rio", # Rust I/O utilities and abstractions
|
"crates/rio", # Rust I/O utilities and abstractions
|
||||||
"crates/targets", # Target-specific configurations and utilities
|
|
||||||
"crates/s3select-api", # S3 Select API interface
|
"crates/s3select-api", # S3 Select API interface
|
||||||
"crates/s3select-query", # S3 Select query engine
|
"crates/s3select-query", # S3 Select query engine
|
||||||
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
||||||
"crates/signer", # client signer
|
"crates/signer", # client signer
|
||||||
"crates/checksums", # client checksums
|
"crates/targets", # Target-specific configurations and utilities
|
||||||
|
"crates/trusted-proxies", # Trusted proxies management
|
||||||
"crates/utils", # Utility functions and helpers
|
"crates/utils", # Utility functions and helpers
|
||||||
"crates/workers", # Worker thread pools and task scheduling
|
"crates/workers", # Worker thread pools and task scheduling
|
||||||
"crates/zip", # ZIP file handling and compression
|
"crates/zip", # ZIP file handling and compression
|
||||||
"crates/heal", # Erasure set and object healing
|
|
||||||
"crates/mcp", # MCP server for S3 operations
|
|
||||||
"crates/kms", # Key Management Service
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.5" }
|
|||||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.5" }
|
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.5" }
|
||||||
rustfs-scanner = { path = "crates/scanner", version = "0.0.5" }
|
rustfs-scanner = { path = "crates/scanner", version = "0.0.5" }
|
||||||
rustfs-signer = { path = "crates/signer", version = "0.0.5" }
|
rustfs-signer = { path = "crates/signer", version = "0.0.5" }
|
||||||
|
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "0.0.5" }
|
||||||
rustfs-targets = { path = "crates/targets", version = "0.0.5" }
|
rustfs-targets = { path = "crates/targets", version = "0.0.5" }
|
||||||
rustfs-utils = { path = "crates/utils", version = "0.0.5" }
|
rustfs-utils = { path = "crates/utils", version = "0.0.5" }
|
||||||
rustfs-workers = { path = "crates/workers", version = "0.0.5" }
|
rustfs-workers = { path = "crates/workers", version = "0.0.5" }
|
||||||
@@ -109,7 +111,7 @@ futures-util = "0.3.31"
|
|||||||
pollster = "0.4.0"
|
pollster = "0.4.0"
|
||||||
hyper = { version = "1.8.1", features = ["http2", "http1", "server"] }
|
hyper = { version = "1.8.1", features = ["http2", "http1", "server"] }
|
||||||
hyper-rustls = { version = "0.27.7", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
|
hyper-rustls = { version = "0.27.7", default-features = false, features = ["native-tokio", "http1", "tls12", "logging", "http2", "aws-lc-rs", "webpki-roots"] }
|
||||||
hyper-util = { version = "0.1.19", features = ["tokio", "server-auto", "server-graceful", "tracing"] }
|
hyper-util = { version = "0.1.20", features = ["tokio", "server-auto", "server-graceful", "tracing"] }
|
||||||
http = "1.4.0"
|
http = "1.4.0"
|
||||||
http-body = "1.0.1"
|
http-body = "1.0.1"
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
@@ -140,7 +142,7 @@ rmp-serde = { version = "1.3.1" }
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.149", features = ["raw_value"] }
|
serde_json = { version = "1.0.149", features = ["raw_value"] }
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
schemars = "1.2.0"
|
schemars = "1.2.1"
|
||||||
|
|
||||||
# Cryptography and Security
|
# Cryptography and Security
|
||||||
aes-gcm = { version = "0.11.0-rc.2", features = ["rand_core"] }
|
aes-gcm = { version = "0.11.0-rc.2", features = ["rand_core"] }
|
||||||
@@ -175,15 +177,15 @@ atomic_enum = "0.3.0"
|
|||||||
aws-config = { version = "1.8.12" }
|
aws-config = { version = "1.8.12" }
|
||||||
aws-credential-types = { version = "1.2.11" }
|
aws-credential-types = { version = "1.2.11" }
|
||||||
aws-sdk-s3 = { version = "1.121.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
aws-sdk-s3 = { version = "1.121.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
||||||
aws-smithy-types = { version = "1.4.1" }
|
aws-smithy-types = { version = "1.4.3" }
|
||||||
backtrace = "0.3.76"
|
backtrace = "0.3.76"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
base64-simd = "0.8.0"
|
base64-simd = "0.8.0"
|
||||||
brotli = "8.0.2"
|
brotli = "8.0.2"
|
||||||
cfg-if = "1.0.4"
|
cfg-if = "1.0.4"
|
||||||
clap = { version = "4.5.56", features = ["derive", "env"] }
|
clap = { version = "4.5.56", features = ["derive", "env"] }
|
||||||
const-str = { version = "1.0.0", features = ["std", "proc"] }
|
const-str = { version = "1.1.0", features = ["std", "proc"] }
|
||||||
convert_case = "0.10.0"
|
convert_case = "0.11.0"
|
||||||
criterion = { version = "0.8", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
crossbeam-queue = "0.3.12"
|
crossbeam-queue = "0.3.12"
|
||||||
datafusion = "52.1.0"
|
datafusion = "52.1.0"
|
||||||
@@ -202,7 +204,7 @@ ipnetwork = { version = "0.21.1", features = ["serde"] }
|
|||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
libc = "0.2.180"
|
libc = "0.2.180"
|
||||||
libsystemd = "0.7.2"
|
libsystemd = "0.7.2"
|
||||||
local-ip-address = "0.6.9"
|
local-ip-address = "0.6.10"
|
||||||
lz4 = "1.28.1"
|
lz4 = "1.28.1"
|
||||||
matchit = "0.9.1"
|
matchit = "0.9.1"
|
||||||
md-5 = "0.11.0-rc.3"
|
md-5 = "0.11.0-rc.3"
|
||||||
@@ -227,7 +229,7 @@ rumqttc = { version = "0.25.1" }
|
|||||||
rustix = { version = "1.1.3", features = ["fs"] }
|
rustix = { version = "1.1.3", features = ["fs"] }
|
||||||
rust-embed = { version = "8.11.0" }
|
rust-embed = { version = "8.11.0" }
|
||||||
rustc-hash = { version = "2.1.1" }
|
rustc-hash = { version = "2.1.1" }
|
||||||
s3s = { version = "0.13.0-alpha.2", features = ["minio"], git = "https://github.com/s3s-project/s3s.git", rev = "3cdb3fe22fe8a1b7fc3f71ead4beacac2683ba7f" }
|
s3s = { version = "0.13.0-alpha.2", features = ["minio"], git = "https://github.com/rustfs/s3s.git", branch = "dependabot/cargo/dep-0202" }
|
||||||
serial_test = "3.3.1"
|
serial_test = "3.3.1"
|
||||||
shadow-rs = { version = "1.7.0", default-features = false }
|
shadow-rs = { version = "1.7.0", default-features = false }
|
||||||
siphasher = "1.0.2"
|
siphasher = "1.0.2"
|
||||||
@@ -235,7 +237,7 @@ smallvec = { version = "1.15.1", features = ["serde"] }
|
|||||||
smartstring = "1.0.1"
|
smartstring = "1.0.1"
|
||||||
snafu = "0.8.9"
|
snafu = "0.8.9"
|
||||||
snap = "1.1.1"
|
snap = "1.1.1"
|
||||||
starshard = { version = "0.6.0", features = ["rayon", "async", "serde"] }
|
starshard = { version = "1.1.0", features = ["rayon", "async", "serde"] }
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
strum = { version = "0.27.2", features = ["derive"] }
|
||||||
sysinfo = "0.38.0"
|
sysinfo = "0.38.0"
|
||||||
temp-env = "0.3.6"
|
temp-env = "0.3.6"
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ datas = "datas"
|
|||||||
bre = "bre"
|
bre = "bre"
|
||||||
abd = "abd"
|
abd = "abd"
|
||||||
mak = "mak"
|
mak = "mak"
|
||||||
|
gae = "gae"
|
||||||
|
GAE = "GAE"
|
||||||
# s3-tests original test names (cannot be changed)
|
# s3-tests original test names (cannot be changed)
|
||||||
nonexisted = "nonexisted"
|
nonexisted = "nonexisted"
|
||||||
|
consts = "consts"
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = []
|
extend-exclude = []
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ pub const RUSTFS_HTTP_PREFIX: &str = "http://";
|
|||||||
/// Default value: https://
|
/// Default value: https://
|
||||||
pub const RUSTFS_HTTPS_PREFIX: &str = "https://";
|
pub const RUSTFS_HTTPS_PREFIX: &str = "https://";
|
||||||
|
|
||||||
|
/// Environment variable for rustfs address
|
||||||
|
/// This is the environment variable for rustfs address.
|
||||||
|
/// It is used to bind the server to a specific address.
|
||||||
|
/// Example: RUSTFS_ADDRESS=":9000"
|
||||||
|
pub const ENV_RUSTFS_ADDRESS: &str = "RUSTFS_ADDRESS";
|
||||||
|
|
||||||
/// Default port for rustfs
|
/// Default port for rustfs
|
||||||
/// This is the default port for rustfs.
|
/// This is the default port for rustfs.
|
||||||
/// This is used to bind the server to a specific port.
|
/// This is used to bind the server to a specific port.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub(crate) mod heal;
|
|||||||
pub(crate) mod object;
|
pub(crate) mod object;
|
||||||
pub(crate) mod profiler;
|
pub(crate) mod profiler;
|
||||||
pub(crate) mod protocols;
|
pub(crate) mod protocols;
|
||||||
|
pub(crate) mod proxy;
|
||||||
pub(crate) mod quota;
|
pub(crate) mod quota;
|
||||||
pub(crate) mod runtime;
|
pub(crate) mod runtime;
|
||||||
pub(crate) mod scanner;
|
pub(crate) mod scanner;
|
||||||
|
|||||||
125
crates/config/src/constants/proxy.rs
Normal file
125
crates/config/src/constants/proxy.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::DEFAULT_LOG_LEVEL;
|
||||||
|
|
||||||
|
// ==================== Base Proxy Configuration ====================
|
||||||
|
/// Environment variable to enable the trusted proxy middleware.
|
||||||
|
pub const ENV_TRUSTED_PROXY_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_ENABLED";
|
||||||
|
/// Trusted proxy middleware is enabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_ENABLED: bool = true;
|
||||||
|
|
||||||
|
/// Environment variable for the proxy validation mode.
|
||||||
|
pub const ENV_TRUSTED_PROXY_VALIDATION_MODE: &str = "RUSTFS_TRUSTED_PROXY_VALIDATION_MODE";
|
||||||
|
/// Default validation mode is "hop_by_hop".
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_VALIDATION_MODE: &str = "hop_by_hop";
|
||||||
|
|
||||||
|
/// Environment variable to enable RFC 7239 "Forwarded" header support.
|
||||||
|
pub const ENV_TRUSTED_PROXY_ENABLE_RFC7239: &str = "RUSTFS_TRUSTED_PROXY_ENABLE_RFC7239";
|
||||||
|
/// RFC 7239 support is enabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_ENABLE_RFC7239: bool = true;
|
||||||
|
|
||||||
|
/// Environment variable for the maximum allowed proxy hops.
|
||||||
|
pub const ENV_TRUSTED_PROXY_MAX_HOPS: &str = "RUSTFS_TRUSTED_PROXY_MAX_HOPS";
|
||||||
|
/// Default maximum hops is 10.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_MAX_HOPS: usize = 10;
|
||||||
|
|
||||||
|
/// Environment variable to enable proxy chain continuity checks.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK: &str = "RUSTFS_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK";
|
||||||
|
/// Continuity checks are enabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK: bool = true;
|
||||||
|
|
||||||
|
/// Environment variable to enable logging of failed proxy validations.
|
||||||
|
pub const ENV_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS: &str = "RUSTFS_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS";
|
||||||
|
/// Logging of failed validations is enabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS: bool = true;
|
||||||
|
|
||||||
|
// ==================== Trusted Proxy Networks ====================
|
||||||
|
/// Environment variable for the list of trusted proxy networks (comma-separated IP/CIDR).
|
||||||
|
pub const ENV_TRUSTED_PROXY_PROXIES: &str = "RUSTFS_TRUSTED_PROXY_NETWORKS";
|
||||||
|
/// Default trusted networks include localhost and common private ranges.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_PROXIES: &str = "127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8";
|
||||||
|
|
||||||
|
/// Environment variable for additional trusted proxy networks (production specific).
|
||||||
|
pub const ENV_TRUSTED_PROXY_EXTRA_PROXIES: &str = "RUSTFS_TRUSTED_PROXY_EXTRA_NETWORKS";
|
||||||
|
/// No extra trusted networks by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_EXTRA_PROXIES: &str = "";
|
||||||
|
|
||||||
|
/// Environment variable for individual trusted proxy IPs.
|
||||||
|
pub const ENV_TRUSTED_PROXY_IPS: &str = "RUSTFS_TRUSTED_PROXY_IPS";
|
||||||
|
/// No individual trusted IPs by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_IPS: &str = "";
|
||||||
|
|
||||||
|
/// Environment variable for private network ranges used in internal validation.
|
||||||
|
pub const ENV_TRUSTED_PROXY_PRIVATE_NETWORKS: &str = "RUSTFS_TRUSTED_PROXY_PRIVATE_NETWORKS";
|
||||||
|
/// Default private networks include common RFC 1918 and RFC 4193 ranges.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS: &str = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8";
|
||||||
|
|
||||||
|
// ==================== Cache Configuration ====================
|
||||||
|
/// Environment variable for the proxy validation cache capacity.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CACHE_CAPACITY: &str = "RUSTFS_TRUSTED_PROXY_CACHE_CAPACITY";
|
||||||
|
/// Default cache capacity is 10,000 entries.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CACHE_CAPACITY: usize = 10_000;
|
||||||
|
|
||||||
|
/// Environment variable for the cache entry time-to-live (TTL) in seconds.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CACHE_TTL_SECONDS: &str = "RUSTFS_TRUSTED_PROXY_CACHE_TTL_SECONDS";
|
||||||
|
/// Default cache TTL is 300 seconds (5 minutes).
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CACHE_TTL_SECONDS: u64 = 300;
|
||||||
|
|
||||||
|
/// Environment variable for the cache cleanup interval in seconds.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL: &str = "RUSTFS_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL";
|
||||||
|
/// Default cleanup interval is 60 seconds.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL: u64 = 60;
|
||||||
|
|
||||||
|
// ==================== Monitoring Configuration ====================
|
||||||
|
/// Environment variable to enable Prometheus metrics.
|
||||||
|
pub const ENV_TRUSTED_PROXY_METRICS_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_METRICS_ENABLED";
|
||||||
|
/// Metrics are enabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_METRICS_ENABLED: bool = true;
|
||||||
|
|
||||||
|
/// Environment variable for the application log level.
|
||||||
|
pub const ENV_TRUSTED_PROXIES_LOG_LEVEL: &str = "RUSTFS_TRUSTED_PROXY_LOG_LEVEL";
|
||||||
|
/// Default log level is "info".
|
||||||
|
pub const DEFAULT_TRUSTED_PROXIES_LOG_LEVEL: &str = DEFAULT_LOG_LEVEL;
|
||||||
|
|
||||||
|
/// Environment variable to enable structured JSON logging.
|
||||||
|
pub const ENV_TRUSTED_PROXY_STRUCTURED_LOGGING: &str = "RUSTFS_TRUSTED_PROXY_STRUCTURED_LOGGING";
|
||||||
|
/// Structured logging is disabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_STRUCTURED_LOGGING: bool = false;
|
||||||
|
|
||||||
|
/// Environment variable to enable distributed tracing.
|
||||||
|
pub const ENV_TRUSTED_PROXY_TRACING_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_TRACING_ENABLED";
|
||||||
|
/// Tracing is enabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_TRACING_ENABLED: bool = true;
|
||||||
|
|
||||||
|
// ==================== Cloud Integration ====================
|
||||||
|
/// Environment variable to enable automatic cloud metadata discovery.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_CLOUD_METADATA_ENABLED";
|
||||||
|
/// Cloud metadata discovery is disabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED: bool = false;
|
||||||
|
|
||||||
|
/// Environment variable for the cloud metadata request timeout in seconds.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT: &str = "RUSTFS_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT";
|
||||||
|
/// Default cloud metadata timeout is 5 seconds.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT: u64 = 5;
|
||||||
|
|
||||||
|
/// Environment variable to enable Cloudflare IP range integration.
|
||||||
|
pub const ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED";
|
||||||
|
/// Cloudflare integration is disabled by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED: bool = false;
|
||||||
|
|
||||||
|
/// Environment variable to force a specific cloud provider (overrides auto-detection).
|
||||||
|
pub const ENV_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE: &str = "RUSTFS_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE";
|
||||||
|
/// No forced provider by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE: &str = "";
|
||||||
@@ -33,6 +33,8 @@ pub use constants::profiler::*;
|
|||||||
#[cfg(feature = "constants")]
|
#[cfg(feature = "constants")]
|
||||||
pub use constants::protocols::*;
|
pub use constants::protocols::*;
|
||||||
#[cfg(feature = "constants")]
|
#[cfg(feature = "constants")]
|
||||||
|
pub use constants::proxy::*;
|
||||||
|
#[cfg(feature = "constants")]
|
||||||
pub use constants::quota::*;
|
pub use constants::quota::*;
|
||||||
#[cfg(feature = "constants")]
|
#[cfg(feature = "constants")]
|
||||||
pub use constants::runtime::*;
|
pub use constants::runtime::*;
|
||||||
|
|||||||
@@ -40,11 +40,18 @@ impl ID {
|
|||||||
pub(crate) fn get_key(&self, password: &[u8], salt: &[u8]) -> Result<[u8; 32], crate::Error> {
|
pub(crate) fn get_key(&self, password: &[u8], salt: &[u8]) -> Result<[u8; 32], crate::Error> {
|
||||||
let mut key = [0u8; 32];
|
let mut key = [0u8; 32];
|
||||||
match self {
|
match self {
|
||||||
ID::Pbkdf2AESGCM => pbkdf2_hmac::<Sha256>(password, salt, 8192, &mut key),
|
ID::Pbkdf2AESGCM => {
|
||||||
_ => {
|
pbkdf2_hmac::<Sha256>(password, salt, 8192, &mut key);
|
||||||
let params = Params::new(64 * 1024, 1, 4, Some(32))?;
|
}
|
||||||
let argon_2id = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
ID::Argon2idAESGCM | ID::Argon2idChaCHa20Poly1305 => {
|
||||||
argon_2id.hash_password_into(password, salt, &mut key)?;
|
const ARGON2_MEMORY: u32 = 64 * 1024;
|
||||||
|
const ARGON2_ITERATIONS: u32 = 1;
|
||||||
|
const ARGON2_PARALLELISM: u32 = 4;
|
||||||
|
const ARGON2_OUTPUT_LEN: usize = 32;
|
||||||
|
|
||||||
|
let params = Params::new(ARGON2_MEMORY, ARGON2_ITERATIONS, ARGON2_PARALLELISM, Some(ARGON2_OUTPUT_LEN))?;
|
||||||
|
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||||
|
argon2.hash_password_into(password, salt, &mut key)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ fn test_encrypt_decrypt_binary_data() -> Result<(), crate::Error> {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt_unicode_data() -> Result<(), crate::Error> {
|
fn test_encrypt_decrypt_unicode_data() -> Result<(), crate::Error> {
|
||||||
let unicode_strings = [
|
let unicode_strings = [
|
||||||
"Hello, 世界! 🌍",
|
"Hello, 世界!🌍",
|
||||||
"Тест на русском языке",
|
"Тест на русском языке",
|
||||||
"العربية اختبار",
|
"العربية اختبار",
|
||||||
"🚀🔐💻🌟⭐",
|
"🚀🔐💻🌟⭐",
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ pub enum Error {
|
|||||||
#[error("invalid encryption algorithm ID: {0}")]
|
#[error("invalid encryption algorithm ID: {0}")]
|
||||||
ErrInvalidAlgID(u8),
|
ErrInvalidAlgID(u8),
|
||||||
|
|
||||||
|
#[error("invalid input: {0}")]
|
||||||
|
ErrInvalidInput(String),
|
||||||
|
|
||||||
|
#[error("invalid key length")]
|
||||||
|
ErrInvalidKeyLength,
|
||||||
|
|
||||||
#[cfg(any(test, feature = "crypto"))]
|
#[cfg(any(test, feature = "crypto"))]
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
ErrInvalidLength(#[from] sha2::digest::InvalidLength),
|
ErrInvalidLength(#[from] sha2::digest::InvalidLength),
|
||||||
@@ -38,4 +44,13 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("jwt err: {0}")]
|
#[error("jwt err: {0}")]
|
||||||
ErrJwt(#[from] jsonwebtoken::errors::Error),
|
ErrJwt(#[from] jsonwebtoken::errors::Error),
|
||||||
|
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
ErrIo(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("invalid signature")]
|
||||||
|
ErrInvalidSignature,
|
||||||
|
|
||||||
|
#[error("invalid token")]
|
||||||
|
ErrInvalidToken,
|
||||||
}
|
}
|
||||||
|
|||||||
52
crates/trusted-proxies/.env.example
Normal file
52
crates/trusted-proxies/.env.example
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Trusted Proxy Configuration
|
||||||
|
# Enable the trusted proxy middleware (default: true)
|
||||||
|
RUSTFS_TRUSTED_PROXY_ENABLED=true
|
||||||
|
|
||||||
|
# Comma-separated list of trusted CIDR ranges (default includes localhost and private networks)
|
||||||
|
RUSTFS_TRUSTED_PROXY_NETWORKS=127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8
|
||||||
|
|
||||||
|
# Additional trusted networks to append to the default list
|
||||||
|
RUSTFS_TRUSTED_PROXY_EXTRA_NETWORKS=
|
||||||
|
|
||||||
|
# Validation strategy: strict, lenient, or hop_by_hop (default: hop_by_hop)
|
||||||
|
RUSTFS_TRUSTED_PROXY_VALIDATION_MODE=hop_by_hop
|
||||||
|
|
||||||
|
# Enable support for RFC 7239 "Forwarded" header (default: true)
|
||||||
|
RUSTFS_TRUSTED_PROXY_ENABLE_RFC7239=true
|
||||||
|
|
||||||
|
# Maximum allowed proxy hops (default: 10)
|
||||||
|
RUSTFS_TRUSTED_PROXY_MAX_HOPS=10
|
||||||
|
|
||||||
|
# Check for continuity in the proxy chain (default: true)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK=true
|
||||||
|
|
||||||
|
# Log details about failed validations (default: true)
|
||||||
|
RUSTFS_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS=true
|
||||||
|
|
||||||
|
# Cache Configuration
|
||||||
|
# Max entries in the validation cache (default: 10000)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CACHE_CAPACITY=10000
|
||||||
|
# Cache TTL in seconds (default: 300)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CACHE_TTL_SECONDS=300
|
||||||
|
# Cache cleanup interval in seconds (default: 60)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL=60
|
||||||
|
|
||||||
|
# Monitoring Configuration
|
||||||
|
# Enable Prometheus metrics collection (default: true)
|
||||||
|
RUSTFS_TRUSTED_PROXY_METRICS_ENABLED=true
|
||||||
|
# Log level for the proxy module (default: info)
|
||||||
|
RUSTFS_TRUSTED_PROXY_LOG_LEVEL=info
|
||||||
|
# Enable structured JSON logging (default: false)
|
||||||
|
RUSTFS_TRUSTED_PROXY_STRUCTURED_LOGGING=false
|
||||||
|
# Enable distributed tracing (default: true)
|
||||||
|
RUSTFS_TRUSTED_PROXY_TRACING_ENABLED=true
|
||||||
|
|
||||||
|
# Cloud Integration
|
||||||
|
# Enable auto-discovery of cloud IP ranges (default: false)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CLOUD_METADATA_ENABLED=false
|
||||||
|
# Timeout for cloud metadata requests in seconds (default: 5)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT=5
|
||||||
|
# Enable Cloudflare IP range integration (default: false)
|
||||||
|
RUSTFS_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED=false
|
||||||
|
# Force a specific cloud provider (aws, azure, gcp) instead of auto-detection
|
||||||
|
RUSTFS_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE=
|
||||||
58
crates/trusted-proxies/Cargo.toml
Normal file
58
crates/trusted-proxies/Cargo.toml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "rustfs-trusted-proxies"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
description = " RustFS Trusted Proxies module provides secure and efficient management of trusted proxy servers within the RustFS ecosystem, enhancing network security and performance."
|
||||||
|
keywords = ["trusted-proxies", "network-security", "rustfs", "proxy-management"]
|
||||||
|
categories = ["network-programming", "security", "web-programming"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
axum = { workspace = true }
|
||||||
|
http = { workspace = true }
|
||||||
|
ipnetwork = { workspace = true }
|
||||||
|
metrics = { workspace = true }
|
||||||
|
moka = { workspace = true, features = ["future"] }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
rustfs-config = { workspace = true }
|
||||||
|
rustfs-utils = { workspace = true, features = ["net"] }
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time", "test-util"] }
|
||||||
|
tower = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { workspace = true, features = ["full", "test-util"] }
|
||||||
|
tower = { workspace = true, features = ["util"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "unit_tests"
|
||||||
|
path = "tests/unit/mod.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "integration_tests"
|
||||||
|
path = "tests/integration/mod.rs"
|
||||||
96
crates/trusted-proxies/README.md
Normal file
96
crates/trusted-proxies/README.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# RustFS Trusted Proxies
|
||||||
|
|
||||||
|
The `rustfs-trusted-proxies` module provides secure and efficient management of trusted proxy servers within the RustFS
|
||||||
|
ecosystem. It is designed to handle multi-layer proxy architectures, ensuring accurate client IP identification while
|
||||||
|
maintaining a zero-trust security model.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Layer Proxy Validation**: Supports `Strict`, `Lenient`, and `HopByHop` validation modes to accurately identify
|
||||||
|
the real client IP address.
|
||||||
|
- **Zero-Trust Security**: Verifies every hop in the proxy chain against a configurable list of trusted networks.
|
||||||
|
- **Cloud Integration**: Automatic discovery of trusted IP ranges for major cloud providers including AWS, Azure, and
|
||||||
|
GCP.
|
||||||
|
- **High Performance**: Utilizes the `moka` cache for fast lookup of validation results and `axum` for a
|
||||||
|
high-performance web interface.
|
||||||
|
- **Observability**: Built-in support for Prometheus metrics and structured JSON logging via `tracing`.
|
||||||
|
- **RFC 7239 Support**: Full support for the modern `Forwarded` header alongside legacy `X-Forwarded-For` headers.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The module is configured primarily through environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|-----------------------------------------------|---------------------|---------------------------------------------------------|
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_ENABLED` | `true` | Enable the trusted proxy middleware |
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_VALIDATION_MODE` | `hop_by_hop` | Validation strategy (`strict`, `lenient`, `hop_by_hop`) |
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_NETWORKS` | `127.0.0.1,::1,...` | Comma-separated list of trusted CIDR ranges |
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_MAX_HOPS` | `10` | Maximum allowed proxy hops |
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_CACHE_CAPACITY` | `10000` | Max entries in the validation cache |
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_METRICS_ENABLED` | `true` | Enable Prometheus metrics collection |
|
||||||
|
| `RUSTFS_TRUSTED_PROXY_CLOUD_METADATA_ENABLED` | `false` | Enable auto-discovery of cloud IP ranges |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
Initialize the global trusted proxy system at the start of your application (e.g., in `main.rs`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Initialize trusted proxies system
|
||||||
|
rustfs_trusted_proxies::init();
|
||||||
|
```
|
||||||
|
|
||||||
|
### As a Middleware
|
||||||
|
|
||||||
|
Integrate the trusted proxy validation into your Axum application or HTTP service stack:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use rustfs_trusted_proxies;
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(handler))
|
||||||
|
// Add the trusted proxy layer if enabled
|
||||||
|
.option_layer(if rustfs_trusted_proxies::is_enabled() {
|
||||||
|
Some(rustfs_trusted_proxies::layer().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing Client Info
|
||||||
|
|
||||||
|
Retrieve the verified client information in your handlers or other middleware:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use rustfs_trusted_proxies::ClientInfo;
|
||||||
|
|
||||||
|
async fn handler(req: Request) -> impl IntoResponse {
|
||||||
|
if let Some(client_info) = req.extensions().get::<ClientInfo>() {
|
||||||
|
println!("Real Client IP: {}", client_info.real_ip);
|
||||||
|
println!("Is Trusted: {}", client_info.is_from_trusted_proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Pre-Commit Checklist
|
||||||
|
|
||||||
|
Before committing, ensure all checks pass:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Run the test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --workspace --exclude e2e_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0.
|
||||||
257
crates/trusted-proxies/src/cloud/detector.rs
Normal file
257
crates/trusted-proxies/src/cloud/detector.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Cloud provider detection and metadata fetching.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::AppError;
|
||||||
|
|
||||||
|
/// Supported cloud providers.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum CloudProvider {
|
||||||
|
/// Amazon Web Services
|
||||||
|
Aws,
|
||||||
|
/// Microsoft Azure
|
||||||
|
Azure,
|
||||||
|
/// Google Cloud Platform
|
||||||
|
Gcp,
|
||||||
|
/// DigitalOcean
|
||||||
|
DigitalOcean,
|
||||||
|
/// Cloudflare
|
||||||
|
Cloudflare,
|
||||||
|
/// Unknown or custom provider.
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CloudProvider {
|
||||||
|
type Err = std::convert::Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(match s.to_lowercase().as_str() {
|
||||||
|
"aws" | "amazon" => Self::Aws,
|
||||||
|
"azure" | "microsoft" => Self::Azure,
|
||||||
|
"gcp" | "google" => Self::Gcp,
|
||||||
|
"digitalocean" | "do" => Self::DigitalOcean,
|
||||||
|
"cloudflare" | "cf" => Self::Cloudflare,
|
||||||
|
_ => Self::Unknown(s.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudProvider {
|
||||||
|
/// Detects the cloud provider based on environment variables.
|
||||||
|
pub fn detect_from_env() -> Option<Self> {
|
||||||
|
// Check for AWS environment variables.
|
||||||
|
if std::env::var("RUSTFS_AWS_EXECUTION_ENV").is_ok()
|
||||||
|
|| std::env::var("RUSTFS_AWS_REGION").is_ok()
|
||||||
|
|| std::env::var("RUSTFS_EC2_INSTANCE_ID").is_ok()
|
||||||
|
{
|
||||||
|
return Some(Self::Aws);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Azure environment variables.
|
||||||
|
if std::env::var("RUSTFS_WEBSITE_SITE_NAME").is_ok()
|
||||||
|
|| std::env::var("RUSTFS_WEBSITE_INSTANCE_ID").is_ok()
|
||||||
|
|| std::env::var("RUSTFS_APPSETTING_WEBSITE_SITE_NAME").is_ok()
|
||||||
|
{
|
||||||
|
return Some(Self::Azure);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for GCP environment variables.
|
||||||
|
if std::env::var("RUSTFS_GCP_PROJECT").is_ok()
|
||||||
|
|| std::env::var("RUSTFS_GOOGLE_CLOUD_PROJECT").is_ok()
|
||||||
|
|| std::env::var("RUSTFS_GAE_INSTANCE").is_ok()
|
||||||
|
{
|
||||||
|
return Some(Self::Gcp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for DigitalOcean environment variables.
|
||||||
|
if std::env::var("RUSTFS_DIGITALOCEAN_REGION").is_ok() {
|
||||||
|
return Some(Self::DigitalOcean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Cloudflare environment variables.
|
||||||
|
if std::env::var("RUSTFS_CF_PAGES").is_ok() || std::env::var("RUSTFS_CF_WORKERS").is_ok() {
|
||||||
|
return Some(Self::Cloudflare);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the canonical name of the cloud provider.
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Aws => "aws",
|
||||||
|
Self::Azure => "azure",
|
||||||
|
Self::Gcp => "gcp",
|
||||||
|
Self::DigitalOcean => "digitalocean",
|
||||||
|
Self::Cloudflare => "cloudflare",
|
||||||
|
Self::Unknown(name) => name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for fetching metadata from a specific cloud provider.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait CloudMetadataFetcher: Send + Sync {
|
||||||
|
/// Returns the name of the provider.
|
||||||
|
fn provider_name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Fetches the network CIDR ranges for the current instance.
|
||||||
|
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
|
||||||
|
|
||||||
|
/// Fetches the public IP ranges for the cloud provider.
|
||||||
|
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
|
||||||
|
|
||||||
|
/// Fetches all IP ranges that should be considered trusted proxies.
|
||||||
|
async fn fetch_trusted_proxy_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
|
||||||
|
match self.fetch_network_cidrs().await {
|
||||||
|
Ok(cidrs) => ranges.extend(cidrs),
|
||||||
|
Err(e) => warn!("Failed to fetch network CIDRs from {}: {}", self.provider_name(), e),
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.fetch_public_ip_ranges().await {
|
||||||
|
Ok(public_ranges) => ranges.extend(public_ranges),
|
||||||
|
Err(e) => warn!("Failed to fetch public IP ranges from {}: {}", self.provider_name(), e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ranges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detector for identifying the current cloud environment and fetching relevant metadata.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CloudDetector {
|
||||||
|
/// Whether cloud detection is enabled.
|
||||||
|
enabled: bool,
|
||||||
|
/// Timeout for metadata requests.
|
||||||
|
timeout: Duration,
|
||||||
|
/// Optionally force a specific provider.
|
||||||
|
forced_provider: Option<CloudProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudDetector {
|
||||||
|
/// Creates a new `CloudDetector`.
|
||||||
|
pub fn new(enabled: bool, timeout: Duration, forced_provider: Option<String>) -> Self {
|
||||||
|
let forced_provider = forced_provider.and_then(|s| CloudProvider::from_str(&s).ok());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
enabled,
|
||||||
|
timeout,
|
||||||
|
forced_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifies the current cloud provider.
|
||||||
|
pub fn detect_provider(&self) -> Option<CloudProvider> {
|
||||||
|
if !self.enabled {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(provider) = self.forced_provider.as_ref() {
|
||||||
|
return Some(provider.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
CloudProvider::detect_from_env()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches trusted IP ranges for the detected cloud provider.
|
||||||
|
pub async fn fetch_trusted_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
if !self.enabled {
|
||||||
|
debug!("Cloud metadata fetching is disabled");
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let provider = self.detect_provider();
|
||||||
|
|
||||||
|
match provider {
|
||||||
|
Some(CloudProvider::Aws) => {
|
||||||
|
info!("Detected AWS environment, fetching metadata");
|
||||||
|
let fetcher = crate::AwsMetadataFetcher::new(self.timeout);
|
||||||
|
fetcher.fetch_trusted_proxy_ranges().await
|
||||||
|
}
|
||||||
|
Some(CloudProvider::Azure) => {
|
||||||
|
info!("Detected Azure environment, fetching metadata");
|
||||||
|
let fetcher = crate::AzureMetadataFetcher::new(self.timeout);
|
||||||
|
fetcher.fetch_trusted_proxy_ranges().await
|
||||||
|
}
|
||||||
|
Some(CloudProvider::Gcp) => {
|
||||||
|
info!("Detected GCP environment, fetching metadata");
|
||||||
|
let fetcher = crate::GcpMetadataFetcher::new(self.timeout);
|
||||||
|
fetcher.fetch_trusted_proxy_ranges().await
|
||||||
|
}
|
||||||
|
Some(CloudProvider::Cloudflare) => {
|
||||||
|
info!("Detected Cloudflare environment");
|
||||||
|
let ranges = crate::CloudflareIpRanges::fetch().await?;
|
||||||
|
Ok(ranges)
|
||||||
|
}
|
||||||
|
Some(CloudProvider::DigitalOcean) => {
|
||||||
|
info!("Detected DigitalOcean environment");
|
||||||
|
let ranges = crate::DigitalOceanIpRanges::fetch().await?;
|
||||||
|
Ok(ranges)
|
||||||
|
}
|
||||||
|
Some(CloudProvider::Unknown(name)) => {
|
||||||
|
warn!("Unknown cloud provider detected: {}", name);
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("No cloud provider detected");
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to fetch metadata from all supported providers sequentially.
|
||||||
|
pub async fn try_all_providers(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
if !self.enabled {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let providers: Vec<Box<dyn CloudMetadataFetcher>> = vec![
|
||||||
|
Box::new(crate::AwsMetadataFetcher::new(self.timeout)),
|
||||||
|
Box::new(crate::AzureMetadataFetcher::new(self.timeout)),
|
||||||
|
Box::new(crate::GcpMetadataFetcher::new(self.timeout)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for provider in providers {
|
||||||
|
let provider_name = provider.provider_name();
|
||||||
|
debug!("Trying to fetch metadata from {}", provider_name);
|
||||||
|
|
||||||
|
match provider.fetch_trusted_proxy_ranges().await {
|
||||||
|
Ok(ranges) => {
|
||||||
|
if !ranges.is_empty() {
|
||||||
|
info!("Fetched {} IP ranges from {}", ranges.len(), provider_name);
|
||||||
|
return Ok(ranges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to fetch metadata from {}: {}", provider_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a default `CloudDetector` with detection disabled.
|
||||||
|
pub fn default_cloud_detector() -> CloudDetector {
|
||||||
|
CloudDetector::new(false, Duration::from_secs(5), None)
|
||||||
|
}
|
||||||
153
crates/trusted-proxies/src/cloud/metadata/aws.rs
Normal file
153
crates/trusted-proxies/src/cloud/metadata/aws.rs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! AWS metadata fetching implementation for identifying trusted proxy ranges.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
use crate::AppError;
|
||||||
|
use crate::CloudMetadataFetcher;
|
||||||
|
|
||||||
|
/// Fetcher for AWS-specific metadata.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AwsMetadataFetcher {
|
||||||
|
client: Client,
|
||||||
|
metadata_endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AwsMetadataFetcher {
|
||||||
|
/// Creates a new `AwsMetadataFetcher`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `timeout` - Duration to use for HTTP request timeouts.
|
||||||
|
///
|
||||||
|
/// Returns a new instance of `AwsMetadataFetcher`.
|
||||||
|
pub fn new(timeout: Duration) -> Self {
|
||||||
|
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
metadata_endpoint: "http://169.254.169.254".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves an IMDSv2 token for secure metadata access.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn get_metadata_token(&self) -> Result<String, AppError> {
|
||||||
|
let url = format!("{}/latest/api/token", self.metadata_endpoint);
|
||||||
|
|
||||||
|
match self
|
||||||
|
.client
|
||||||
|
.put(&url)
|
||||||
|
.header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let token = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to read IMDSv2 token: {}", e)))?;
|
||||||
|
Ok(token)
|
||||||
|
} else {
|
||||||
|
debug!("IMDSv2 token request failed with status: {}", response.status());
|
||||||
|
Err(AppError::cloud("Failed to obtain IMDSv2 token"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("IMDSv2 token request failed: {}", e);
|
||||||
|
Err(AppError::cloud(format!("IMDSv2 request failed: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CloudMetadataFetcher for AwsMetadataFetcher {
|
||||||
|
fn provider_name(&self) -> &str {
|
||||||
|
"aws"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
// Simplified implementation: returns standard AWS VPC private ranges.
|
||||||
|
let default_ranges = vec![
|
||||||
|
"10.0.0.0/8", // Large VPCs
|
||||||
|
"172.16.0.0/12", // Medium VPCs
|
||||||
|
"192.168.0.0/16", // Small VPCs
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = default_ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
debug!("Using default AWS VPC network ranges");
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse default AWS ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let url = "https://ip-ranges.amazonaws.com/ip-ranges.json";
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct AwsIpRanges {
|
||||||
|
prefixes: Vec<AwsPrefix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct AwsPrefix {
|
||||||
|
ip_prefix: String,
|
||||||
|
service: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.client.get(url).timeout(Duration::from_secs(5)).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let ip_ranges: AwsIpRanges = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to parse AWS IP ranges JSON: {}", e)))?;
|
||||||
|
|
||||||
|
let mut networks = Vec::new();
|
||||||
|
|
||||||
|
for prefix in ip_ranges.prefixes {
|
||||||
|
// Include EC2 and CloudFront ranges as potential trusted proxies.
|
||||||
|
if (prefix.service == "EC2" || prefix.service == "CLOUDFRONT")
|
||||||
|
&& let Ok(network) = ipnetwork::IpNetwork::from_str(&prefix.ip_prefix)
|
||||||
|
{
|
||||||
|
networks.push(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Successfully fetched {} AWS public IP ranges", networks.len());
|
||||||
|
Ok(networks)
|
||||||
|
} else {
|
||||||
|
debug!("Failed to fetch AWS IP ranges: HTTP {}", response.status());
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to fetch AWS IP ranges: {}", e);
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
308
crates/trusted-proxies/src/cloud/metadata/azure.rs
Normal file
308
crates/trusted-proxies/src/cloud/metadata/azure.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Azure Cloud metadata fetching implementation for identifying trusted proxy ranges.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::AppError;
|
||||||
|
use crate::CloudMetadataFetcher;
|
||||||
|
|
||||||
|
/// Fetcher for Azure-specific metadata.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AzureMetadataFetcher {
|
||||||
|
client: Client,
|
||||||
|
metadata_endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AzureMetadataFetcher {
|
||||||
|
/// Creates a new `AzureMetadataFetcher`.
|
||||||
|
pub fn new(timeout: Duration) -> Self {
|
||||||
|
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
metadata_endpoint: "http://169.254.169.254".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves metadata from the Azure Instance Metadata Service (IMDS).
|
||||||
|
async fn get_metadata(&self, path: &str) -> Result<String, AppError> {
|
||||||
|
let url = format!("{}/metadata/{}?api-version=2021-05-01", self.metadata_endpoint, path);
|
||||||
|
|
||||||
|
debug!("Fetching Azure metadata from: {}", url);
|
||||||
|
|
||||||
|
match self.client.get(&url).header("Metadata", "true").send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to read Azure metadata response: {}", e)))?;
|
||||||
|
Ok(text)
|
||||||
|
} else {
|
||||||
|
debug!("Azure metadata request failed with status: {}", response.status());
|
||||||
|
Err(AppError::cloud(format!("Azure metadata API returned status: {}", response.status())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Azure metadata request failed: {}", e);
|
||||||
|
Err(AppError::cloud(format!("Azure metadata request failed: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches Azure public IP ranges from the official Microsoft download source.
|
||||||
|
async fn fetch_azure_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
// Official Azure IP ranges download URL (periodically updated).
|
||||||
|
// See: https://www.microsoft.com/en-us/download/details.aspx?id=56519
|
||||||
|
let url =
|
||||||
|
"https://download.microsoft.com/download/7/1/D/71D86715-5596-4529-9B13-DA13A5DE5B63/ServiceTags_Public_20260126.json";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AzureServiceTags {
|
||||||
|
values: Vec<AzureServiceTag>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AzureServiceTag {
|
||||||
|
name: String,
|
||||||
|
properties: AzureServiceTagProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AzureServiceTagProperties {
|
||||||
|
address_prefixes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Fetching Azure IP ranges from: {}", url);
|
||||||
|
|
||||||
|
match self.client.get(url).timeout(Duration::from_secs(10)).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let service_tags: AzureServiceTags = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to parse Azure IP ranges JSON: {}", e)))?;
|
||||||
|
|
||||||
|
let mut networks = Vec::new();
|
||||||
|
|
||||||
|
for tag in service_tags.values {
|
||||||
|
// Include general Azure datacenter ranges, excluding specific internal services.
|
||||||
|
if tag.name.contains("Azure") && !tag.name.contains("ActiveDirectory") {
|
||||||
|
for prefix in tag.properties.address_prefixes {
|
||||||
|
if let Ok(network) = ipnetwork::IpNetwork::from_str(&prefix) {
|
||||||
|
networks.push(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Successfully fetched {} Azure public IP ranges", networks.len());
|
||||||
|
Ok(networks)
|
||||||
|
} else {
|
||||||
|
debug!("Failed to fetch Azure IP ranges: HTTP {}", response.status());
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to fetch Azure IP ranges: {}", e);
|
||||||
|
// Fallback to hardcoded ranges if the download fails.
|
||||||
|
Self::default_azure_ranges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a set of default Azure IP ranges as a fallback.
|
||||||
|
fn default_azure_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let ranges = vec![
|
||||||
|
"13.64.0.0/11",
|
||||||
|
"13.96.0.0/13",
|
||||||
|
"13.104.0.0/14",
|
||||||
|
"20.33.0.0/16",
|
||||||
|
"20.34.0.0/15",
|
||||||
|
"20.36.0.0/14",
|
||||||
|
"20.40.0.0/13",
|
||||||
|
"20.48.0.0/12",
|
||||||
|
"20.64.0.0/10",
|
||||||
|
"20.128.0.0/16",
|
||||||
|
"20.135.0.0/16",
|
||||||
|
"20.136.0.0/13",
|
||||||
|
"20.150.0.0/15",
|
||||||
|
"20.157.0.0/16",
|
||||||
|
"20.184.0.0/13",
|
||||||
|
"20.190.0.0/16",
|
||||||
|
"20.192.0.0/10",
|
||||||
|
"40.64.0.0/10",
|
||||||
|
"40.80.0.0/12",
|
||||||
|
"40.96.0.0/13",
|
||||||
|
"40.112.0.0/13",
|
||||||
|
"40.120.0.0/14",
|
||||||
|
"40.124.0.0/16",
|
||||||
|
"40.125.0.0/17",
|
||||||
|
"51.12.0.0/15",
|
||||||
|
"51.104.0.0/15",
|
||||||
|
"51.120.0.0/16",
|
||||||
|
"51.124.0.0/16",
|
||||||
|
"51.132.0.0/16",
|
||||||
|
"51.136.0.0/15",
|
||||||
|
"51.138.0.0/16",
|
||||||
|
"51.140.0.0/14",
|
||||||
|
"51.144.0.0/15",
|
||||||
|
"52.96.0.0/12",
|
||||||
|
"52.112.0.0/14",
|
||||||
|
"52.120.0.0/14",
|
||||||
|
"52.124.0.0/16",
|
||||||
|
"52.125.0.0/16",
|
||||||
|
"52.126.0.0/15",
|
||||||
|
"52.130.0.0/15",
|
||||||
|
"52.136.0.0/13",
|
||||||
|
"52.144.0.0/15",
|
||||||
|
"52.146.0.0/15",
|
||||||
|
"52.148.0.0/14",
|
||||||
|
"52.152.0.0/13",
|
||||||
|
"52.160.0.0/12",
|
||||||
|
"52.176.0.0/13",
|
||||||
|
"52.184.0.0/14",
|
||||||
|
"52.188.0.0/14",
|
||||||
|
"52.224.0.0/11",
|
||||||
|
"65.52.0.0/14",
|
||||||
|
"104.40.0.0/13",
|
||||||
|
"104.208.0.0/13",
|
||||||
|
"104.215.0.0/16",
|
||||||
|
"137.116.0.0/15",
|
||||||
|
"137.135.0.0/16",
|
||||||
|
"138.91.0.0/16",
|
||||||
|
"157.56.0.0/16",
|
||||||
|
"168.61.0.0/16",
|
||||||
|
"168.62.0.0/15",
|
||||||
|
"191.233.0.0/18",
|
||||||
|
"193.149.0.0/19",
|
||||||
|
"2603:1000::/40",
|
||||||
|
"2603:1010::/40",
|
||||||
|
"2603:1020::/40",
|
||||||
|
"2603:1030::/40",
|
||||||
|
"2603:1040::/40",
|
||||||
|
"2603:1050::/40",
|
||||||
|
"2603:1060::/40",
|
||||||
|
"2603:1070::/40",
|
||||||
|
"2603:1080::/40",
|
||||||
|
"2603:1090::/40",
|
||||||
|
"2603:10a0::/40",
|
||||||
|
"2603:10b0::/40",
|
||||||
|
"2603:10c0::/40",
|
||||||
|
"2603:10d0::/40",
|
||||||
|
"2603:10e0::/40",
|
||||||
|
"2603:10f0::/40",
|
||||||
|
"2603:1100::/40",
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
debug!("Using default Azure public IP ranges");
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse default Azure ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CloudMetadataFetcher for AzureMetadataFetcher {
|
||||||
|
fn provider_name(&self) -> &str {
|
||||||
|
"azure"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
// Attempt to fetch network interface information from Azure IMDS.
|
||||||
|
match self.get_metadata("instance/network/interface").await {
|
||||||
|
Ok(metadata) => {
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AzureNetworkInterface {
|
||||||
|
ipv4: AzureIpv4Info,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AzureIpv4Info {
|
||||||
|
subnet: Vec<AzureSubnet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AzureSubnet {
|
||||||
|
address: String,
|
||||||
|
prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let interfaces: Vec<AzureNetworkInterface> = serde_json::from_str(&metadata)
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to parse Azure network metadata JSON: {}", e)))?;
|
||||||
|
|
||||||
|
let mut cidrs = Vec::new();
|
||||||
|
for interface in interfaces {
|
||||||
|
for subnet in interface.ipv4.subnet {
|
||||||
|
let cidr = format!("{}/{}", subnet.address, subnet.prefix);
|
||||||
|
if let Ok(network) = ipnetwork::IpNetwork::from_str(&cidr) {
|
||||||
|
cidrs.push(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cidrs.is_empty() {
|
||||||
|
info!("Successfully fetched {} network CIDRs from Azure metadata", cidrs.len());
|
||||||
|
Ok(cidrs)
|
||||||
|
} else {
|
||||||
|
debug!("No network CIDRs found in Azure metadata, falling back to defaults");
|
||||||
|
Self::default_azure_network_ranges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to fetch Azure network metadata: {}", e);
|
||||||
|
Self::default_azure_network_ranges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
self.fetch_azure_ip_ranges().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AzureMetadataFetcher {
|
||||||
|
/// Returns a set of default Azure VNet ranges as a fallback.
|
||||||
|
fn default_azure_network_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let ranges = vec![
|
||||||
|
"10.0.0.0/8", // Large VNets
|
||||||
|
"172.16.0.0/12", // Medium VNets
|
||||||
|
"192.168.0.0/16", // Small VNets
|
||||||
|
"100.64.0.0/10", // Azure reserved range
|
||||||
|
"192.0.0.0/24", // Azure reserved
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
debug!("Using default Azure VNet network ranges");
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse default Azure network ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
309
crates/trusted-proxies/src/cloud/metadata/gcp.rs
Normal file
309
crates/trusted-proxies/src/cloud/metadata/gcp.rs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Google Cloud Platform (GCP) metadata fetching implementation for identifying trusted proxy ranges.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::AppError;
|
||||||
|
use crate::CloudMetadataFetcher;
|
||||||
|
|
||||||
|
/// Fetcher for GCP-specific metadata.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GcpMetadataFetcher {
|
||||||
|
client: Client,
|
||||||
|
metadata_endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GcpMetadataFetcher {
|
||||||
|
/// Creates a new `GcpMetadataFetcher`.
|
||||||
|
pub fn new(timeout: Duration) -> Self {
|
||||||
|
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
metadata_endpoint: "http://metadata.google.internal".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves metadata from the GCP Compute Engine metadata server.
|
||||||
|
async fn get_metadata(&self, path: &str) -> Result<String, AppError> {
|
||||||
|
let url = format!("{}/computeMetadata/v1/{}", self.metadata_endpoint, path);
|
||||||
|
|
||||||
|
debug!("Fetching GCP metadata from: {}", url);
|
||||||
|
|
||||||
|
match self.client.get(&url).header("Metadata-Flavor", "Google").send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to read GCP metadata response: {}", e)))?;
|
||||||
|
Ok(text)
|
||||||
|
} else {
|
||||||
|
debug!("GCP metadata request failed with status: {}", response.status());
|
||||||
|
Err(AppError::cloud(format!("GCP metadata API returned status: {}", response.status())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("GCP metadata request failed: {}", e);
|
||||||
|
Err(AppError::cloud(format!("GCP metadata request failed: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a dotted-decimal subnet mask to a CIDR prefix length.
|
||||||
|
fn subnet_mask_to_prefix_length(mask: &str) -> Result<u8, AppError> {
|
||||||
|
let parts: Vec<&str> = mask.split('.').collect();
|
||||||
|
if parts.len() != 4 {
|
||||||
|
return Err(AppError::cloud(format!("Invalid subnet mask format: {}", mask)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prefix_length = 0;
|
||||||
|
for part in parts {
|
||||||
|
let octet: u8 = part
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| AppError::cloud(format!("Invalid octet in subnet mask: {}", part)))?;
|
||||||
|
|
||||||
|
let mut remaining = octet;
|
||||||
|
while remaining > 0 {
|
||||||
|
if remaining & 0x80 == 0x80 {
|
||||||
|
prefix_length += 1;
|
||||||
|
remaining <<= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if remaining != 0 {
|
||||||
|
return Err(AppError::cloud("Non-contiguous subnet mask detected"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(prefix_length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CloudMetadataFetcher for GcpMetadataFetcher {
|
||||||
|
fn provider_name(&self) -> &str {
|
||||||
|
"gcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
// Attempt to list network interfaces from GCP metadata.
|
||||||
|
match self.get_metadata("instance/network-interfaces/").await {
|
||||||
|
Ok(interfaces_metadata) => {
|
||||||
|
let interface_indices: Vec<usize> = interfaces_metadata
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let line = line.trim().trim_end_matches('/');
|
||||||
|
if line.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
line.parse().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if interface_indices.is_empty() {
|
||||||
|
warn!("No network interfaces found in GCP metadata");
|
||||||
|
return Self::default_gcp_network_ranges();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cidrs = Vec::new();
|
||||||
|
|
||||||
|
for index in interface_indices {
|
||||||
|
// Try to get IP and subnet mask for each interface.
|
||||||
|
let ip_path = format!("instance/network-interfaces/{}/ip", index);
|
||||||
|
let mask_path = format!("instance/network-interfaces/{}/subnetmask", index);
|
||||||
|
|
||||||
|
match tokio::try_join!(self.get_metadata(&ip_path), self.get_metadata(&mask_path)) {
|
||||||
|
Ok((ip, mask)) => {
|
||||||
|
let ip = ip.trim();
|
||||||
|
let mask = mask.trim();
|
||||||
|
|
||||||
|
if let (Ok(ip_addr), Ok(prefix_len)) =
|
||||||
|
(std::net::Ipv4Addr::from_str(ip), Self::subnet_mask_to_prefix_length(mask))
|
||||||
|
{
|
||||||
|
let cidr_str = format!("{}/{}", ip_addr, prefix_len);
|
||||||
|
if let Ok(network) = ipnetwork::IpNetwork::from_str(&cidr_str) {
|
||||||
|
cidrs.push(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to get IP/mask for GCP interface {}: {}", index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cidrs.is_empty() {
|
||||||
|
warn!("Could not determine network CIDRs from GCP metadata, falling back to defaults");
|
||||||
|
Self::default_gcp_network_ranges()
|
||||||
|
} else {
|
||||||
|
info!("Successfully fetched {} network CIDRs from GCP metadata", cidrs.len());
|
||||||
|
Ok(cidrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to fetch GCP network metadata: {}", e);
|
||||||
|
Self::default_gcp_network_ranges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
self.fetch_gcp_ip_ranges().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GcpMetadataFetcher {
|
||||||
|
/// Fetches GCP public IP ranges from the official Google source.
|
||||||
|
async fn fetch_gcp_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let url = "https://www.gstatic.com/ipranges/cloud.json";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GcpIpRanges {
|
||||||
|
prefixes: Vec<GcpPrefix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GcpPrefix {
|
||||||
|
ipv4_prefix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Fetching GCP IP ranges from: {}", url);
|
||||||
|
|
||||||
|
match self.client.get(url).timeout(Duration::from_secs(10)).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let ip_ranges: GcpIpRanges = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to parse GCP IP ranges JSON: {}", e)))?;
|
||||||
|
|
||||||
|
let mut networks = Vec::new();
|
||||||
|
|
||||||
|
for prefix in ip_ranges.prefixes {
|
||||||
|
if let Some(ipv4_prefix) = prefix.ipv4_prefix
|
||||||
|
&& let Ok(network) = ipnetwork::IpNetwork::from_str(&ipv4_prefix)
|
||||||
|
{
|
||||||
|
networks.push(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Successfully fetched {} GCP public IP ranges", networks.len());
|
||||||
|
Ok(networks)
|
||||||
|
} else {
|
||||||
|
debug!("Failed to fetch GCP IP ranges: HTTP {}", response.status());
|
||||||
|
Self::default_gcp_ip_ranges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to fetch GCP IP ranges: {}", e);
|
||||||
|
Self::default_gcp_ip_ranges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a set of default GCP public IP ranges as a fallback.
|
||||||
|
fn default_gcp_ip_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let ranges = vec![
|
||||||
|
"8.34.208.0/20",
|
||||||
|
"8.35.192.0/20",
|
||||||
|
"8.35.208.0/20",
|
||||||
|
"23.236.48.0/20",
|
||||||
|
"23.251.128.0/19",
|
||||||
|
"34.0.0.0/15",
|
||||||
|
"34.2.0.0/16",
|
||||||
|
"34.3.0.0/23",
|
||||||
|
"34.4.0.0/14",
|
||||||
|
"34.8.0.0/13",
|
||||||
|
"34.16.0.0/12",
|
||||||
|
"34.32.0.0/11",
|
||||||
|
"34.64.0.0/10",
|
||||||
|
"34.128.0.0/10",
|
||||||
|
"35.184.0.0/13",
|
||||||
|
"35.192.0.0/14",
|
||||||
|
"35.196.0.0/15",
|
||||||
|
"35.198.0.0/16",
|
||||||
|
"35.200.0.0/13",
|
||||||
|
"35.208.0.0/12",
|
||||||
|
"35.224.0.0/12",
|
||||||
|
"35.240.0.0/13",
|
||||||
|
"104.154.0.0/15",
|
||||||
|
"104.196.0.0/14",
|
||||||
|
"107.167.160.0/19",
|
||||||
|
"107.178.192.0/18",
|
||||||
|
"108.59.80.0/20",
|
||||||
|
"108.170.192.0/18",
|
||||||
|
"108.177.0.0/17",
|
||||||
|
"130.211.0.0/16",
|
||||||
|
"136.112.0.0/12",
|
||||||
|
"142.250.0.0/15",
|
||||||
|
"146.148.0.0/17",
|
||||||
|
"172.217.0.0/16",
|
||||||
|
"172.253.0.0/16",
|
||||||
|
"173.194.0.0/16",
|
||||||
|
"192.178.0.0/15",
|
||||||
|
"209.85.128.0/17",
|
||||||
|
"216.58.192.0/19",
|
||||||
|
"216.239.32.0/19",
|
||||||
|
"2001:4860::/32",
|
||||||
|
"2404:6800::/32",
|
||||||
|
"2600:1900::/28",
|
||||||
|
"2607:f8b0::/32",
|
||||||
|
"2620:15c::/36",
|
||||||
|
"2800:3f0::/32",
|
||||||
|
"2a00:1450::/32",
|
||||||
|
"2c0f:fb50::/32",
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
debug!("Using default GCP public IP ranges");
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a set of default GCP VPC ranges as a fallback.
|
||||||
|
fn default_gcp_network_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
||||||
|
let ranges = vec![
|
||||||
|
"10.0.0.0/8", // Large VPCs
|
||||||
|
"172.16.0.0/12", // Medium VPCs
|
||||||
|
"192.168.0.0/16", // Small VPCs
|
||||||
|
"100.64.0.0/10", // GCP reserved range
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
debug!("Using default GCP VPC network ranges");
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP network ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/trusted-proxies/src/cloud/metadata/mod.rs
Normal file
26
crates/trusted-proxies/src/cloud/metadata/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Cloud provider metadata fetching
|
||||||
|
//!
|
||||||
|
//! This module contains implementations for fetching metadata
|
||||||
|
//! from various cloud providers.
|
||||||
|
|
||||||
|
mod aws;
|
||||||
|
mod azure;
|
||||||
|
mod gcp;
|
||||||
|
|
||||||
|
pub use aws::*;
|
||||||
|
pub use azure::*;
|
||||||
|
pub use gcp::*;
|
||||||
26
crates/trusted-proxies/src/cloud/mod.rs
Normal file
26
crates/trusted-proxies/src/cloud/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Cloud service integration module
|
||||||
|
//!
|
||||||
|
//! This module provides integration with various cloud providers
|
||||||
|
//! for automatic IP range detection and metadata fetching.
|
||||||
|
|
||||||
|
mod detector;
|
||||||
|
pub mod metadata;
|
||||||
|
mod ranges;
|
||||||
|
|
||||||
|
pub use detector::*;
|
||||||
|
pub use metadata::*;
|
||||||
|
pub use ranges::*;
|
||||||
216
crates/trusted-proxies/src/cloud/ranges.rs
Normal file
216
crates/trusted-proxies/src/cloud/ranges.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Static and dynamic IP range definitions for various cloud providers.
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use reqwest::Client;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
/// Utility for fetching Cloudflare IP ranges.
|
||||||
|
pub struct CloudflareIpRanges;
|
||||||
|
|
||||||
|
impl CloudflareIpRanges {
|
||||||
|
/// Returns a static list of Cloudflare IP ranges.
|
||||||
|
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
|
||||||
|
let ranges = vec![
|
||||||
|
// IPv4 ranges
|
||||||
|
"103.21.244.0/22",
|
||||||
|
"103.22.200.0/22",
|
||||||
|
"103.31.4.0/22",
|
||||||
|
"104.16.0.0/13",
|
||||||
|
"104.24.0.0/14",
|
||||||
|
"108.162.192.0/18",
|
||||||
|
"131.0.72.0/22",
|
||||||
|
"141.101.64.0/18",
|
||||||
|
"162.158.0.0/15",
|
||||||
|
"172.64.0.0/13",
|
||||||
|
"173.245.48.0/20",
|
||||||
|
"188.114.96.0/20",
|
||||||
|
"190.93.240.0/20",
|
||||||
|
"197.234.240.0/22",
|
||||||
|
"198.41.128.0/17",
|
||||||
|
// IPv6 ranges
|
||||||
|
"2400:cb00::/32",
|
||||||
|
"2606:4700::/32",
|
||||||
|
"2803:f800::/32",
|
||||||
|
"2405:b500::/32",
|
||||||
|
"2405:8100::/32",
|
||||||
|
"2a06:98c0::/29",
|
||||||
|
"2c0f:f248::/32",
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = ranges.into_iter().map(IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
info!("Loaded {} static Cloudflare IP ranges", networks.len());
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse static Cloudflare IP ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the latest Cloudflare IP ranges from their official API.
|
||||||
|
pub async fn fetch_from_api() -> Result<Vec<IpNetwork>, AppError> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to create HTTP client: {}", e)))?;
|
||||||
|
|
||||||
|
let urls = ["https://www.cloudflare.com/ips-v4", "https://www.cloudflare.com/ips-v6"];
|
||||||
|
|
||||||
|
let mut all_ranges = Vec::new();
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to read response from {}: {}", url, e)))?;
|
||||||
|
|
||||||
|
let ranges: Result<Vec<_>, _> = text
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim())
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.map(IpNetwork::from_str)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match ranges {
|
||||||
|
Ok(mut networks) => {
|
||||||
|
debug!("Fetched {} IP ranges from {}", networks.len(), url);
|
||||||
|
all_ranges.append(&mut networks);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to parse IP ranges from {}: {}", url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("Failed to fetch IP ranges from {}: HTTP {}", url, response.status());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to fetch from {}: {}", url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_ranges.is_empty() {
|
||||||
|
// Fallback to static list if API requests fail.
|
||||||
|
Self::fetch().await
|
||||||
|
} else {
|
||||||
|
info!("Successfully fetched {} Cloudflare IP ranges from API", all_ranges.len());
|
||||||
|
Ok(all_ranges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility for fetching DigitalOcean IP ranges.
|
||||||
|
pub struct DigitalOceanIpRanges;
|
||||||
|
|
||||||
|
impl DigitalOceanIpRanges {
|
||||||
|
/// Returns a static list of DigitalOcean IP ranges.
|
||||||
|
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
|
||||||
|
let ranges = vec![
|
||||||
|
// Datacenter IP ranges
|
||||||
|
"64.227.0.0/16",
|
||||||
|
"138.197.0.0/16",
|
||||||
|
"139.59.0.0/16",
|
||||||
|
"157.230.0.0/16",
|
||||||
|
"159.65.0.0/16",
|
||||||
|
"167.99.0.0/16",
|
||||||
|
"178.128.0.0/16",
|
||||||
|
"206.189.0.0/16",
|
||||||
|
"207.154.0.0/16",
|
||||||
|
"209.97.0.0/16",
|
||||||
|
// Load Balancer IP ranges
|
||||||
|
"144.126.0.0/16",
|
||||||
|
"143.198.0.0/16",
|
||||||
|
"161.35.0.0/16",
|
||||||
|
];
|
||||||
|
|
||||||
|
let networks: Result<Vec<_>, _> = ranges.into_iter().map(IpNetwork::from_str).collect();
|
||||||
|
|
||||||
|
match networks {
|
||||||
|
Ok(networks) => {
|
||||||
|
info!("Loaded {} static DigitalOcean IP ranges", networks.len());
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
Err(e) => Err(AppError::cloud(format!("Failed to parse static DigitalOcean IP ranges: {}", e))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility for fetching Google Cloud IP ranges.
|
||||||
|
pub struct GoogleCloudIpRanges;
|
||||||
|
|
||||||
|
impl GoogleCloudIpRanges {
|
||||||
|
/// Fetches the latest Google Cloud IP ranges from their official source.
|
||||||
|
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to create HTTP client: {}", e)))?;
|
||||||
|
|
||||||
|
let url = "https://www.gstatic.com/ipranges/cloud.json";
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct GoogleIpRanges {
|
||||||
|
prefixes: Vec<GooglePrefix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct GooglePrefix {
|
||||||
|
ipv4_prefix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().is_success() {
|
||||||
|
let ip_ranges: GoogleIpRanges = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::cloud(format!("Failed to parse Google IP ranges JSON: {}", e)))?;
|
||||||
|
|
||||||
|
let mut networks = Vec::new();
|
||||||
|
|
||||||
|
for prefix in ip_ranges.prefixes {
|
||||||
|
if let Some(ipv4_prefix) = prefix.ipv4_prefix
|
||||||
|
&& let Ok(network) = IpNetwork::from_str(&ipv4_prefix)
|
||||||
|
{
|
||||||
|
networks.push(network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Successfully fetched {} Google Cloud IP ranges from API", networks.len());
|
||||||
|
Ok(networks)
|
||||||
|
} else {
|
||||||
|
debug!("Failed to fetch Google IP ranges: HTTP {}", response.status());
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to fetch Google IP ranges: {}", e);
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
crates/trusted-proxies/src/config/env.rs
Normal file
90
crates/trusted-proxies/src/config/env.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Environment variable configuration constants and helpers for the trusted proxy system.
|
||||||
|
|
||||||
|
use crate::ConfigError;
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use rustfs_config::{
|
||||||
|
ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, ENV_TRUSTED_PROXY_ENABLE_RFC7239, ENV_TRUSTED_PROXY_ENABLED,
|
||||||
|
ENV_TRUSTED_PROXY_EXTRA_PROXIES, ENV_TRUSTED_PROXY_IPS, ENV_TRUSTED_PROXY_MAX_HOPS, ENV_TRUSTED_PROXY_PROXIES,
|
||||||
|
ENV_TRUSTED_PROXY_VALIDATION_MODE,
|
||||||
|
};
|
||||||
|
use std::str::FromStr;
|
||||||
|
// ==================== Helper Functions ====================
|
||||||
|
|
||||||
|
/// Parses a comma-separated list of IP/CIDR strings from an environment variable.
|
||||||
|
pub fn parse_ip_list_from_env(key: &str, default: &str) -> Result<Vec<IpNetwork>, ConfigError> {
|
||||||
|
let value = std::env::var(key).unwrap_or_else(|_| default.to_string());
|
||||||
|
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut networks = Vec::new();
|
||||||
|
for item in value.split(',') {
|
||||||
|
let item = item.trim();
|
||||||
|
if item.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match IpNetwork::from_str(item) {
|
||||||
|
Ok(network) => networks.push(network),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to parse network '{}' from environment variable {}: {}", item, key, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a comma-separated list of strings from an environment variable.
|
||||||
|
pub fn parse_string_list_from_env(key: &str, default: &str) -> Vec<String> {
|
||||||
|
let value = std::env::var(key).unwrap_or_else(|_| default.to_string());
|
||||||
|
|
||||||
|
value
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an environment variable is set.
|
||||||
|
pub fn is_env_set(key: &str) -> bool {
|
||||||
|
std::env::var(key).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of all proxy-related environment variables and their current values.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_all_proxy_env_vars() -> Vec<(String, String)> {
|
||||||
|
let vars = [
|
||||||
|
ENV_TRUSTED_PROXY_ENABLED,
|
||||||
|
ENV_TRUSTED_PROXY_VALIDATION_MODE,
|
||||||
|
ENV_TRUSTED_PROXY_ENABLE_RFC7239,
|
||||||
|
ENV_TRUSTED_PROXY_MAX_HOPS,
|
||||||
|
ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK,
|
||||||
|
ENV_TRUSTED_PROXY_PROXIES,
|
||||||
|
ENV_TRUSTED_PROXY_EXTRA_PROXIES,
|
||||||
|
ENV_TRUSTED_PROXY_IPS,
|
||||||
|
ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED,
|
||||||
|
ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED,
|
||||||
|
];
|
||||||
|
|
||||||
|
vars.iter()
|
||||||
|
.filter_map(|&key| std::env::var(key).ok().map(|value| (key.to_string(), value)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
233
crates/trusted-proxies/src/config/loader.rs
Normal file
233
crates/trusted-proxies/src/config/loader.rs
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Configuration loader for environment variables and files.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppConfig, CacheConfig, CloudConfig, ConfigError, MonitoringConfig, TrustedProxy, TrustedProxyConfig, ValidationMode,
|
||||||
|
parse_ip_list_from_env, parse_string_list_from_env,
|
||||||
|
};
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use rustfs_config::{
|
||||||
|
DEFAULT_TRUSTED_PROXIES_LOG_LEVEL, DEFAULT_TRUSTED_PROXY_CACHE_CAPACITY, DEFAULT_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CACHE_TTL_SECONDS, DEFAULT_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE, DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED,
|
||||||
|
DEFAULT_TRUSTED_PROXY_ENABLE_RFC7239, DEFAULT_TRUSTED_PROXY_EXTRA_PROXIES, DEFAULT_TRUSTED_PROXY_IPS,
|
||||||
|
DEFAULT_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS, DEFAULT_TRUSTED_PROXY_MAX_HOPS, DEFAULT_TRUSTED_PROXY_METRICS_ENABLED,
|
||||||
|
DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS, DEFAULT_TRUSTED_PROXY_PROXIES, DEFAULT_TRUSTED_PROXY_STRUCTURED_LOGGING,
|
||||||
|
DEFAULT_TRUSTED_PROXY_TRACING_ENABLED, DEFAULT_TRUSTED_PROXY_VALIDATION_MODE, ENV_TRUSTED_PROXIES_LOG_LEVEL,
|
||||||
|
ENV_TRUSTED_PROXY_CACHE_CAPACITY, ENV_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL, ENV_TRUSTED_PROXY_CACHE_TTL_SECONDS,
|
||||||
|
ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED, ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
ENV_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE, ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED, ENV_TRUSTED_PROXY_ENABLE_RFC7239,
|
||||||
|
ENV_TRUSTED_PROXY_EXTRA_PROXIES, ENV_TRUSTED_PROXY_IPS, ENV_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS, ENV_TRUSTED_PROXY_MAX_HOPS,
|
||||||
|
ENV_TRUSTED_PROXY_METRICS_ENABLED, ENV_TRUSTED_PROXY_PRIVATE_NETWORKS, ENV_TRUSTED_PROXY_PROXIES,
|
||||||
|
ENV_TRUSTED_PROXY_STRUCTURED_LOGGING, ENV_TRUSTED_PROXY_TRACING_ENABLED, ENV_TRUSTED_PROXY_VALIDATION_MODE,
|
||||||
|
};
|
||||||
|
use rustfs_utils::{get_env_bool, get_env_str, get_env_u64, get_env_usize, parse_and_resolve_address};
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// Loader for application configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConfigLoader;
|
||||||
|
|
||||||
|
impl ConfigLoader {
|
||||||
|
/// Loads the complete application configuration from environment variables.
|
||||||
|
pub fn from_env() -> Result<AppConfig, ConfigError> {
|
||||||
|
// Load proxy-specific configuration.
|
||||||
|
let proxy_config = Self::load_proxy_config()?;
|
||||||
|
|
||||||
|
// Load cache configuration.
|
||||||
|
let cache_config = Self::load_cache_config();
|
||||||
|
|
||||||
|
// Load monitoring and observability configuration.
|
||||||
|
let monitoring_config = Self::load_monitoring_config();
|
||||||
|
|
||||||
|
// Load cloud provider integration configuration.
|
||||||
|
let cloud_config = Self::load_cloud_config();
|
||||||
|
|
||||||
|
// Load server binding address.
|
||||||
|
let server_addr = Self::load_server_addr();
|
||||||
|
|
||||||
|
Ok(AppConfig::new(proxy_config, cache_config, monitoring_config, cloud_config, server_addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads trusted proxy configuration from environment variables.
|
||||||
|
fn load_proxy_config() -> Result<TrustedProxyConfig, ConfigError> {
|
||||||
|
let mut proxies = Vec::new();
|
||||||
|
|
||||||
|
// Parse base trusted proxies from environment.
|
||||||
|
let base_networks = parse_ip_list_from_env(ENV_TRUSTED_PROXY_PROXIES, DEFAULT_TRUSTED_PROXY_PROXIES)?;
|
||||||
|
for network in base_networks {
|
||||||
|
proxies.push(TrustedProxy::Cidr(network));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse extra trusted proxies from environment.
|
||||||
|
let extra_networks = parse_ip_list_from_env(ENV_TRUSTED_PROXY_EXTRA_PROXIES, DEFAULT_TRUSTED_PROXY_EXTRA_PROXIES)?;
|
||||||
|
for network in extra_networks {
|
||||||
|
proxies.push(TrustedProxy::Cidr(network));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse individual trusted proxy IPs.
|
||||||
|
let ip_strings = parse_string_list_from_env(ENV_TRUSTED_PROXY_IPS, DEFAULT_TRUSTED_PROXY_IPS);
|
||||||
|
for ip_str in ip_strings {
|
||||||
|
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||||
|
proxies.push(TrustedProxy::Single(ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine validation mode.
|
||||||
|
let validation_mode_str = get_env_str(ENV_TRUSTED_PROXY_VALIDATION_MODE, DEFAULT_TRUSTED_PROXY_VALIDATION_MODE);
|
||||||
|
let validation_mode = ValidationMode::from_str(&validation_mode_str)?;
|
||||||
|
|
||||||
|
// Load other proxy settings.
|
||||||
|
let enable_rfc7239 = get_env_bool(ENV_TRUSTED_PROXY_ENABLE_RFC7239, DEFAULT_TRUSTED_PROXY_ENABLE_RFC7239);
|
||||||
|
let max_hops = get_env_usize(ENV_TRUSTED_PROXY_MAX_HOPS, DEFAULT_TRUSTED_PROXY_MAX_HOPS);
|
||||||
|
let enable_chain_check =
|
||||||
|
get_env_bool(ENV_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK, DEFAULT_TRUSTED_PROXY_CHAIN_CONTINUITY_CHECK);
|
||||||
|
|
||||||
|
// Load private network ranges.
|
||||||
|
let private_networks =
|
||||||
|
parse_ip_list_from_env(ENV_TRUSTED_PROXY_PRIVATE_NETWORKS, DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS)?;
|
||||||
|
|
||||||
|
Ok(TrustedProxyConfig::new(
|
||||||
|
proxies,
|
||||||
|
validation_mode,
|
||||||
|
enable_rfc7239,
|
||||||
|
max_hops,
|
||||||
|
enable_chain_check,
|
||||||
|
private_networks,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads cache configuration from environment variables.
|
||||||
|
fn load_cache_config() -> CacheConfig {
|
||||||
|
CacheConfig {
|
||||||
|
capacity: get_env_usize(ENV_TRUSTED_PROXY_CACHE_CAPACITY, DEFAULT_TRUSTED_PROXY_CACHE_CAPACITY),
|
||||||
|
ttl_seconds: get_env_u64(ENV_TRUSTED_PROXY_CACHE_TTL_SECONDS, DEFAULT_TRUSTED_PROXY_CACHE_TTL_SECONDS),
|
||||||
|
cleanup_interval_seconds: get_env_u64(
|
||||||
|
ENV_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CACHE_CLEANUP_INTERVAL,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads monitoring configuration from environment variables.
|
||||||
|
fn load_monitoring_config() -> MonitoringConfig {
|
||||||
|
MonitoringConfig {
|
||||||
|
metrics_enabled: get_env_bool(ENV_TRUSTED_PROXY_METRICS_ENABLED, DEFAULT_TRUSTED_PROXY_METRICS_ENABLED),
|
||||||
|
log_level: get_env_str(ENV_TRUSTED_PROXIES_LOG_LEVEL, DEFAULT_TRUSTED_PROXIES_LOG_LEVEL),
|
||||||
|
structured_logging: get_env_bool(ENV_TRUSTED_PROXY_STRUCTURED_LOGGING, DEFAULT_TRUSTED_PROXY_STRUCTURED_LOGGING),
|
||||||
|
tracing_enabled: get_env_bool(ENV_TRUSTED_PROXY_TRACING_ENABLED, DEFAULT_TRUSTED_PROXY_TRACING_ENABLED),
|
||||||
|
log_failed_validations: get_env_bool(
|
||||||
|
ENV_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS,
|
||||||
|
DEFAULT_TRUSTED_PROXY_LOG_FAILED_VALIDATIONS,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads cloud configuration from environment variables.
|
||||||
|
fn load_cloud_config() -> CloudConfig {
|
||||||
|
let forced_provider_str = get_env_str(ENV_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE, DEFAULT_TRUSTED_PROXY_CLOUD_PROVIDER_FORCE);
|
||||||
|
let forced_provider = if forced_provider_str.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(forced_provider_str)
|
||||||
|
};
|
||||||
|
|
||||||
|
CloudConfig {
|
||||||
|
metadata_enabled: get_env_bool(
|
||||||
|
ENV_TRUSTED_PROXY_CLOUD_METADATA_ENABLED,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED,
|
||||||
|
),
|
||||||
|
metadata_timeout_seconds: get_env_u64(
|
||||||
|
ENV_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
),
|
||||||
|
cloudflare_ips_enabled: get_env_bool(
|
||||||
|
ENV_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED,
|
||||||
|
),
|
||||||
|
forced_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the server binding address from environment variables.
|
||||||
|
fn load_server_addr() -> SocketAddr {
|
||||||
|
let address = get_env_str("RUSTFS_ADDRESS", rustfs_config::DEFAULT_ADDRESS);
|
||||||
|
parse_and_resolve_address(&address)
|
||||||
|
.unwrap_or_else(|_| SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), rustfs_config::DEFAULT_PORT))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads configuration from environment, falling back to defaults on failure.
|
||||||
|
pub fn from_env_or_default() -> AppConfig {
|
||||||
|
match Self::from_env() {
|
||||||
|
Ok(config) => {
|
||||||
|
info!("Configuration loaded successfully from environment variables");
|
||||||
|
config
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to load configuration from environment: {}. Using defaults", e);
|
||||||
|
Self::default_config()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a default configuration.
|
||||||
|
pub fn default_config() -> AppConfig {
|
||||||
|
let proxy_config = TrustedProxyConfig::new(
|
||||||
|
vec![
|
||||||
|
TrustedProxy::Single(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||||
|
TrustedProxy::Single(IpAddr::V6(Ipv6Addr::LOCALHOST)),
|
||||||
|
],
|
||||||
|
ValidationMode::HopByHop,
|
||||||
|
true,
|
||||||
|
10,
|
||||||
|
true,
|
||||||
|
DEFAULT_TRUSTED_PROXY_PRIVATE_NETWORKS
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|s| s.trim().parse::<IpNetwork>().ok())
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
AppConfig::new(
|
||||||
|
proxy_config,
|
||||||
|
CacheConfig::default(),
|
||||||
|
MonitoringConfig::default(),
|
||||||
|
CloudConfig::default(),
|
||||||
|
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), rustfs_config::DEFAULT_PORT),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a summary of the configuration to the log.
|
||||||
|
pub fn print_summary(config: &AppConfig) {
|
||||||
|
info!("=== Application Configuration ===");
|
||||||
|
info!("Server: {}", config.server_addr);
|
||||||
|
info!("Trusted Proxies: {}", config.proxy.proxies.len());
|
||||||
|
info!("Validation Mode: {:?}", config.proxy.validation_mode);
|
||||||
|
info!("Cache Capacity: {}", config.cache.capacity);
|
||||||
|
info!("Metrics Enabled: {}", config.monitoring.metrics_enabled);
|
||||||
|
info!("Cloud Metadata: {}", config.cloud.metadata_enabled);
|
||||||
|
|
||||||
|
if config.monitoring.log_failed_validations {
|
||||||
|
info!("Failed validations will be logged");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.proxy.proxies.is_empty() {
|
||||||
|
tracing::debug!("Trusted networks: {:?}", config.proxy.get_network_strings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/trusted-proxies/src/config/mod.rs
Normal file
21
crates/trusted-proxies/src/config/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod env;
|
||||||
|
mod loader;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use env::*;
|
||||||
|
pub use loader::*;
|
||||||
|
pub use types::*;
|
||||||
302
crates/trusted-proxies/src/config/types.rs
Normal file
302
crates/trusted-proxies/src/config/types.rs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Configuration type definitions for the trusted proxy system.
|
||||||
|
|
||||||
|
use crate::ConfigError;
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use rustfs_config::{
|
||||||
|
DEFAULT_TRUSTED_PROXIES_LOG_LEVEL, DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED,
|
||||||
|
DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT, DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Proxy validation mode defining how the proxy chain is verified.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum ValidationMode {
|
||||||
|
/// Lenient mode: Accepts the entire chain as long as the last proxy is trusted.
|
||||||
|
Lenient,
|
||||||
|
/// Strict mode: Requires all proxies in the chain to be trusted.
|
||||||
|
Strict,
|
||||||
|
/// Hop-by-hop mode: Finds the first untrusted proxy from right to left.
|
||||||
|
/// This is the recommended mode for most production environments.
|
||||||
|
#[default]
|
||||||
|
HopByHop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ValidationMode {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"lenient" => Ok(Self::Lenient),
|
||||||
|
"strict" => Ok(Self::Strict),
|
||||||
|
"hop_by_hop" | "hopbyhop" => Ok(Self::HopByHop),
|
||||||
|
_ => Err(ConfigError::InvalidConfig(format!(
|
||||||
|
"Invalid validation mode: '{}'. Must be one of: lenient, strict, hop_by_hop",
|
||||||
|
s
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationMode {
|
||||||
|
/// Returns the string representation of the validation mode.
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Lenient => "lenient",
|
||||||
|
Self::Strict => "strict",
|
||||||
|
Self::HopByHop => "hop_by_hop",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a trusted proxy entry, which can be a single IP or a CIDR range.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum TrustedProxy {
|
||||||
|
/// A single IP address.
|
||||||
|
Single(IpAddr),
|
||||||
|
/// An IP network range (CIDR notation).
|
||||||
|
Cidr(IpNetwork),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TrustedProxy {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Single(ip) => write!(f, "{}", ip),
|
||||||
|
Self::Cidr(network) => write!(f, "{}", network),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustedProxy {
|
||||||
|
/// Checks if the given IP address matches this proxy configuration.
|
||||||
|
pub fn contains(&self, ip: &IpAddr) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Single(proxy_ip) => ip == proxy_ip,
|
||||||
|
Self::Cidr(network) => network.contains(*ip),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for trusted proxies and validation logic.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TrustedProxyConfig {
|
||||||
|
/// List of trusted proxy entries.
|
||||||
|
pub proxies: Vec<TrustedProxy>,
|
||||||
|
/// The validation mode to use for verifying proxy chains.
|
||||||
|
pub validation_mode: ValidationMode,
|
||||||
|
/// Whether to enable RFC 7239 "Forwarded" header support.
|
||||||
|
pub enable_rfc7239: bool,
|
||||||
|
/// Maximum allowed proxy hops in the chain.
|
||||||
|
pub max_hops: usize,
|
||||||
|
/// Whether to enable continuity checks for the proxy chain.
|
||||||
|
pub enable_chain_continuity_check: bool,
|
||||||
|
/// Private network ranges that should be treated with caution.
|
||||||
|
pub private_networks: Vec<IpNetwork>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustedProxyConfig {
|
||||||
|
/// Creates a new trusted proxy configuration.
|
||||||
|
pub fn new(
|
||||||
|
proxies: Vec<TrustedProxy>,
|
||||||
|
validation_mode: ValidationMode,
|
||||||
|
enable_rfc7239: bool,
|
||||||
|
max_hops: usize,
|
||||||
|
enable_chain_continuity_check: bool,
|
||||||
|
private_networks: Vec<IpNetwork>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
proxies,
|
||||||
|
validation_mode,
|
||||||
|
enable_rfc7239,
|
||||||
|
max_hops,
|
||||||
|
enable_chain_continuity_check,
|
||||||
|
private_networks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a SocketAddr originates from a trusted proxy.
|
||||||
|
pub fn is_trusted(&self, addr: &SocketAddr) -> bool {
|
||||||
|
let ip = addr.ip();
|
||||||
|
self.proxies.iter().any(|proxy| proxy.contains(&ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address belongs to a private network range.
|
||||||
|
pub fn is_private_network(&self, ip: &IpAddr) -> bool {
|
||||||
|
self.private_networks.iter().any(|network| network.contains(*ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of all network strings for debugging purposes.
|
||||||
|
pub fn get_network_strings(&self) -> Vec<String> {
|
||||||
|
self.proxies.iter().map(|p| p.to_string()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a summary of the configuration.
|
||||||
|
pub fn summary(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"TrustedProxyConfig {{ proxies: {}, mode: {}, max_hops: {} }}",
|
||||||
|
self.proxies.len(),
|
||||||
|
self.validation_mode.as_str(),
|
||||||
|
self.max_hops
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the internal caching mechanism.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CacheConfig {
|
||||||
|
/// Maximum number of entries in the cache.
|
||||||
|
pub capacity: usize,
|
||||||
|
/// Time-to-live for cache entries in seconds.
|
||||||
|
pub ttl_seconds: u64,
|
||||||
|
/// Interval for cache cleanup in seconds.
|
||||||
|
pub cleanup_interval_seconds: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CacheConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
capacity: 10_000,
|
||||||
|
ttl_seconds: 300,
|
||||||
|
cleanup_interval_seconds: 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheConfig {
|
||||||
|
/// Returns the TTL as a Duration.
|
||||||
|
pub fn ttl_duration(&self) -> Duration {
|
||||||
|
Duration::from_secs(self.ttl_seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cleanup interval as a Duration.
|
||||||
|
pub fn cleanup_interval(&self) -> Duration {
|
||||||
|
Duration::from_secs(self.cleanup_interval_seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for monitoring and observability.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MonitoringConfig {
|
||||||
|
/// Whether to enable Prometheus metrics.
|
||||||
|
pub metrics_enabled: bool,
|
||||||
|
/// The logging level (e.g., "info", "debug").
|
||||||
|
pub log_level: String,
|
||||||
|
/// Whether to use structured JSON logging.
|
||||||
|
pub structured_logging: bool,
|
||||||
|
/// Whether to enable distributed tracing.
|
||||||
|
pub tracing_enabled: bool,
|
||||||
|
/// Whether to log detailed information about failed validations.
|
||||||
|
pub log_failed_validations: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MonitoringConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
metrics_enabled: true,
|
||||||
|
log_level: DEFAULT_TRUSTED_PROXIES_LOG_LEVEL.to_string(),
|
||||||
|
structured_logging: false,
|
||||||
|
tracing_enabled: true,
|
||||||
|
log_failed_validations: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for cloud provider integration.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CloudConfig {
|
||||||
|
/// Whether to enable automatic cloud metadata discovery.
|
||||||
|
pub metadata_enabled: bool,
|
||||||
|
/// Timeout for cloud metadata requests in seconds.
|
||||||
|
pub metadata_timeout_seconds: u64,
|
||||||
|
/// Whether to automatically include Cloudflare IP ranges.
|
||||||
|
pub cloudflare_ips_enabled: bool,
|
||||||
|
/// Optionally force a specific cloud provider.
|
||||||
|
pub forced_provider: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CloudConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
metadata_enabled: DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_ENABLED,
|
||||||
|
metadata_timeout_seconds: DEFAULT_TRUSTED_PROXY_CLOUD_METADATA_TIMEOUT,
|
||||||
|
cloudflare_ips_enabled: DEFAULT_TRUSTED_PROXY_CLOUDFLARE_IPS_ENABLED,
|
||||||
|
forced_provider: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudConfig {
|
||||||
|
/// Returns the metadata timeout as a Duration.
|
||||||
|
pub fn metadata_timeout(&self) -> Duration {
|
||||||
|
Duration::from_secs(self.metadata_timeout_seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete application configuration.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
/// Trusted proxy settings.
|
||||||
|
pub proxy: TrustedProxyConfig,
|
||||||
|
/// Cache settings.
|
||||||
|
pub cache: CacheConfig,
|
||||||
|
/// Monitoring and observability settings.
|
||||||
|
pub monitoring: MonitoringConfig,
|
||||||
|
/// Cloud integration settings.
|
||||||
|
pub cloud: CloudConfig,
|
||||||
|
/// The address the server should bind to.
|
||||||
|
pub server_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
/// Creates a new application configuration.
|
||||||
|
pub fn new(
|
||||||
|
proxy: TrustedProxyConfig,
|
||||||
|
cache: CacheConfig,
|
||||||
|
monitoring: MonitoringConfig,
|
||||||
|
cloud: CloudConfig,
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
proxy,
|
||||||
|
cache,
|
||||||
|
monitoring,
|
||||||
|
cloud,
|
||||||
|
server_addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a summary of the application configuration.
|
||||||
|
pub fn summary(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"AppConfig {{\n\
|
||||||
|
\x20\x20proxy: {},\n\
|
||||||
|
\x20\x20cache_capacity: {},\n\
|
||||||
|
\x20\x20metrics: {},\n\
|
||||||
|
\x20\x20cloud_metadata: {}\n\
|
||||||
|
}}",
|
||||||
|
self.proxy.summary(),
|
||||||
|
self.cache.capacity,
|
||||||
|
self.monitoring.metrics_enabled,
|
||||||
|
self.cloud.metadata_enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
crates/trusted-proxies/src/error/config.rs
Normal file
82
crates/trusted-proxies/src/error/config.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Configuration error types for the trusted proxy system.
|
||||||
|
|
||||||
|
use std::net::AddrParseError;
|
||||||
|
|
||||||
|
/// Errors related to application configuration.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
/// Required environment variable is missing.
|
||||||
|
#[error("Missing environment variable: {0}")]
|
||||||
|
MissingEnvVar(String),
|
||||||
|
|
||||||
|
/// Environment variable exists but could not be parsed.
|
||||||
|
#[error("Failed to parse environment variable {0}: {1}")]
|
||||||
|
EnvParseError(String, String),
|
||||||
|
|
||||||
|
/// A configuration value is logically invalid.
|
||||||
|
#[error("Invalid configuration value for {0}: {1}")]
|
||||||
|
InvalidValue(String, String),
|
||||||
|
|
||||||
|
/// An IP address or CIDR range is malformed.
|
||||||
|
#[error("Invalid IP address or network: {0}")]
|
||||||
|
InvalidIp(String),
|
||||||
|
|
||||||
|
/// Configuration failed overall validation.
|
||||||
|
#[error("Configuration validation failed: {0}")]
|
||||||
|
ValidationFailed(String),
|
||||||
|
|
||||||
|
/// Two or more configuration settings are in conflict.
|
||||||
|
#[error("Configuration conflict: {0}")]
|
||||||
|
Conflict(String),
|
||||||
|
|
||||||
|
/// Error reading or parsing a configuration file.
|
||||||
|
#[error("Config file error: {0}")]
|
||||||
|
FileError(String),
|
||||||
|
|
||||||
|
/// General invalid configuration error.
|
||||||
|
#[error("Invalid config: {0}")]
|
||||||
|
InvalidConfig(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddrParseError> for ConfigError {
|
||||||
|
fn from(err: AddrParseError) -> Self {
|
||||||
|
Self::InvalidIp(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ipnetwork::IpNetworkError> for ConfigError {
|
||||||
|
fn from(err: ipnetwork::IpNetworkError) -> Self {
|
||||||
|
Self::InvalidIp(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigError {
|
||||||
|
/// Creates a `MissingEnvVar` error.
|
||||||
|
pub fn missing_env_var(key: &str) -> Self {
|
||||||
|
Self::MissingEnvVar(key.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `EnvParseError`.
|
||||||
|
pub fn env_parse(key: &str, value: &str) -> Self {
|
||||||
|
Self::EnvParseError(key.to_string(), value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `InvalidValue` error.
|
||||||
|
pub fn invalid_value(field: &str, value: &str) -> Self {
|
||||||
|
Self::InvalidValue(field.to_string(), value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
94
crates/trusted-proxies/src/error/mod.rs
Normal file
94
crates/trusted-proxies/src/error/mod.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Error types for the trusted proxy system.
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod proxy;
|
||||||
|
|
||||||
|
pub use config::*;
|
||||||
|
pub use proxy::*;
|
||||||
|
|
||||||
|
/// Unified error type for the application.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
/// Errors related to configuration.
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
Config(#[from] ConfigError),
|
||||||
|
|
||||||
|
/// Errors related to proxy validation.
|
||||||
|
#[error("Proxy validation error: {0}")]
|
||||||
|
Proxy(#[from] ProxyError),
|
||||||
|
|
||||||
|
/// Errors related to cloud service integration.
|
||||||
|
#[error("Cloud service error: {0}")]
|
||||||
|
Cloud(String),
|
||||||
|
|
||||||
|
/// General internal errors.
|
||||||
|
#[error("Internal error: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
|
||||||
|
/// Standard I/O errors.
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// Errors related to HTTP requests or responses.
|
||||||
|
#[error("HTTP error: {0}")]
|
||||||
|
Http(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
/// Creates a new `Cloud` error.
|
||||||
|
pub fn cloud(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Cloud(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Internal` error.
|
||||||
|
pub fn internal(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Internal(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Http` error.
|
||||||
|
pub fn http(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Http(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the error is considered recoverable.
|
||||||
|
pub fn is_recoverable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Config(_) => true,
|
||||||
|
Self::Proxy(e) => e.is_recoverable(),
|
||||||
|
Self::Cloud(_) => true,
|
||||||
|
Self::Internal(_) => false,
|
||||||
|
Self::Io(_) => true,
|
||||||
|
Self::Http(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for API error responses (Status Code, Error Message).
|
||||||
|
pub type ApiError = (http::StatusCode, String);
|
||||||
|
|
||||||
|
impl From<AppError> for ApiError {
|
||||||
|
fn from(err: AppError) -> Self {
|
||||||
|
match err {
|
||||||
|
AppError::Config(_) => (http::StatusCode::BAD_REQUEST, err.to_string()),
|
||||||
|
AppError::Proxy(_) => (http::StatusCode::BAD_REQUEST, err.to_string()),
|
||||||
|
AppError::Cloud(_) => (http::StatusCode::SERVICE_UNAVAILABLE, err.to_string()),
|
||||||
|
AppError::Internal(_) => (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
|
||||||
|
AppError::Io(_) => (http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()),
|
||||||
|
AppError::Http(_) => (http::StatusCode::BAD_GATEWAY, err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
crates/trusted-proxies/src/error/proxy.rs
Normal file
114
crates/trusted-proxies/src/error/proxy.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Proxy validation error types for the trusted proxy system.
|
||||||
|
|
||||||
|
use std::net::AddrParseError;
|
||||||
|
|
||||||
|
/// Errors that can occur during proxy chain validation.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ProxyError {
|
||||||
|
/// The X-Forwarded-For header is malformed or contains invalid data.
|
||||||
|
#[error("Invalid X-Forwarded-For header: {0}")]
|
||||||
|
InvalidXForwardedFor(String),
|
||||||
|
|
||||||
|
/// The RFC 7239 Forwarded header is malformed.
|
||||||
|
#[error("Invalid Forwarded header (RFC 7239): {0}")]
|
||||||
|
InvalidForwardedHeader(String),
|
||||||
|
|
||||||
|
/// General failure during proxy chain validation.
|
||||||
|
#[error("Proxy chain validation failed: {0}")]
|
||||||
|
ChainValidationFailed(String),
|
||||||
|
|
||||||
|
/// The number of proxy hops exceeds the configured limit.
|
||||||
|
#[error("Proxy chain too long: {0} hops (max: {1})")]
|
||||||
|
ChainTooLong(usize, usize),
|
||||||
|
|
||||||
|
/// The request originated from a proxy that is not in the trusted list.
|
||||||
|
#[error("Request from untrusted proxy: {0}")]
|
||||||
|
UntrustedProxy(String),
|
||||||
|
|
||||||
|
/// The proxy chain is not continuous (e.g., an untrusted IP is between trusted ones).
|
||||||
|
#[error("Proxy chain is not continuous")]
|
||||||
|
ChainNotContinuous,
|
||||||
|
|
||||||
|
/// An IP address in the chain could not be parsed.
|
||||||
|
#[error("Failed to parse IP address: {0}")]
|
||||||
|
IpParseError(String),
|
||||||
|
|
||||||
|
/// A header value could not be parsed as a string.
|
||||||
|
#[error("Failed to parse header: {0}")]
|
||||||
|
HeaderParseError(String),
|
||||||
|
|
||||||
|
/// Validation took too long and timed out.
|
||||||
|
#[error("Validation timeout")]
|
||||||
|
Timeout,
|
||||||
|
|
||||||
|
/// An unexpected internal error occurred during validation.
|
||||||
|
#[error("Internal validation error: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddrParseError> for ProxyError {
|
||||||
|
fn from(err: AddrParseError) -> Self {
|
||||||
|
Self::IpParseError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyError {
|
||||||
|
/// Creates an `InvalidXForwardedFor` error.
|
||||||
|
pub fn invalid_xff(msg: impl Into<String>) -> Self {
|
||||||
|
Self::InvalidXForwardedFor(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `InvalidForwardedHeader` error.
|
||||||
|
pub fn invalid_forwarded(msg: impl Into<String>) -> Self {
|
||||||
|
Self::InvalidForwardedHeader(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `ChainValidationFailed` error.
|
||||||
|
pub fn chain_failed(msg: impl Into<String>) -> Self {
|
||||||
|
Self::ChainValidationFailed(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `UntrustedProxy` error.
|
||||||
|
pub fn untrusted(proxy: impl Into<String>) -> Self {
|
||||||
|
Self::UntrustedProxy(proxy.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an `Internal` validation error.
|
||||||
|
pub fn internal(msg: impl Into<String>) -> Self {
|
||||||
|
Self::Internal(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if the error is recoverable, meaning the request can still be processed
|
||||||
|
/// (perhaps by falling back to the direct peer IP).
|
||||||
|
pub fn is_recoverable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
// These errors typically mean we should use the direct peer IP as a fallback.
|
||||||
|
Self::UntrustedProxy(_) => true,
|
||||||
|
Self::ChainTooLong(_, _) => true,
|
||||||
|
Self::ChainNotContinuous => true,
|
||||||
|
|
||||||
|
// These errors suggest malformed requests or severe configuration issues.
|
||||||
|
Self::InvalidXForwardedFor(_) => false,
|
||||||
|
Self::InvalidForwardedHeader(_) => false,
|
||||||
|
Self::ChainValidationFailed(_) => false,
|
||||||
|
Self::IpParseError(_) => false,
|
||||||
|
Self::HeaderParseError(_) => false,
|
||||||
|
Self::Timeout => true,
|
||||||
|
Self::Internal(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
crates/trusted-proxies/src/global.rs
Normal file
106
crates/trusted-proxies/src/global.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Global entry point for the Trusted Proxies module.
|
||||||
|
//!
|
||||||
|
//! This module provides a unified interface for initializing and using the
|
||||||
|
//! trusted proxy functionality within the RustFS server.
|
||||||
|
|
||||||
|
use crate::{AppConfig, ConfigLoader, ProxyMetrics, TrustedProxyLayer, default_proxy_metrics};
|
||||||
|
use rustfs_config::{DEFAULT_TRUSTED_PROXY_ENABLED, ENV_TRUSTED_PROXY_ENABLED};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Global instance of the application configuration.
|
||||||
|
static CONFIG: OnceLock<Arc<AppConfig>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Global instance of the metrics collector.
|
||||||
|
static METRICS: OnceLock<Option<ProxyMetrics>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Global instance of the trusted proxy layer.
|
||||||
|
static PROXY_LAYER: OnceLock<TrustedProxyLayer> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Global flag indicating if the trusted proxy middleware is enabled.
|
||||||
|
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Initializes the global trusted proxy system.
|
||||||
|
///
|
||||||
|
/// This function should be called once at the start of the application.
|
||||||
|
/// It loads the configuration, initializes metrics, and sets up the proxy layer.
|
||||||
|
pub fn init() {
|
||||||
|
// Check if the trusted proxy system is enabled via environment variable.
|
||||||
|
let enabled = rustfs_utils::get_env_bool(ENV_TRUSTED_PROXY_ENABLED, DEFAULT_TRUSTED_PROXY_ENABLED);
|
||||||
|
ENABLED.set(enabled).expect("Trusted proxy enabled flag already initialized");
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
tracing::info!("Trusted Proxies module is disabled via configuration");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration from environment variables.
|
||||||
|
let config = Arc::new(ConfigLoader::from_env_or_default());
|
||||||
|
CONFIG.set(config.clone()).expect("Trusted proxy config already initialized");
|
||||||
|
|
||||||
|
// Initialize metrics if enabled.
|
||||||
|
let metrics = if config.monitoring.metrics_enabled {
|
||||||
|
let m = default_proxy_metrics(enabled);
|
||||||
|
Some(m)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
METRICS
|
||||||
|
.set(metrics.clone())
|
||||||
|
.expect("Trusted proxy metrics already initialized");
|
||||||
|
|
||||||
|
// Initialize the trusted proxy layer.
|
||||||
|
let layer = TrustedProxyLayer::new(config.proxy.clone(), metrics, enabled);
|
||||||
|
PROXY_LAYER.set(layer).expect("Trusted proxy layer already initialized");
|
||||||
|
|
||||||
|
tracing::info!("Trusted Proxies module initialized");
|
||||||
|
ConfigLoader::print_summary(&config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the global trusted proxy layer.
|
||||||
|
///
|
||||||
|
/// This layer can be used to wrap Axum services or other Tower-compatible services.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `init()` has not been called.
|
||||||
|
pub fn layer() -> &'static TrustedProxyLayer {
|
||||||
|
PROXY_LAYER
|
||||||
|
.get()
|
||||||
|
.expect("Trusted proxy system not initialized. Call init() first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the global configuration.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `init()` has not been called.
|
||||||
|
pub fn config() -> &'static AppConfig {
|
||||||
|
CONFIG
|
||||||
|
.get()
|
||||||
|
.expect("Trusted proxy system not initialized. Call init() first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the global metrics collector, if enabled.
|
||||||
|
pub fn metrics() -> Option<&'static ProxyMetrics> {
|
||||||
|
METRICS.get().and_then(|m| m.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the trusted proxy system is enabled.
|
||||||
|
pub fn is_enabled() -> bool {
|
||||||
|
*ENABLED.get().unwrap_or(&false)
|
||||||
|
}
|
||||||
29
crates/trusted-proxies/src/lib.rs
Normal file
29
crates/trusted-proxies/src/lib.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod cloud;
|
||||||
|
mod config;
|
||||||
|
mod error;
|
||||||
|
mod global;
|
||||||
|
mod middleware;
|
||||||
|
mod proxy;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use cloud::*;
|
||||||
|
pub use config::*;
|
||||||
|
pub use error::*;
|
||||||
|
pub use global::{config as global_config, init, is_enabled, layer, metrics};
|
||||||
|
pub use middleware::{TrustedProxyLayer, TrustedProxyMiddleware};
|
||||||
|
pub use proxy::*;
|
||||||
|
pub use utils::*;
|
||||||
75
crates/trusted-proxies/src/middleware/layer.rs
Normal file
75
crates/trusted-proxies/src/middleware/layer.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Tower layer implementation for the trusted proxy middleware.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower::Layer;
|
||||||
|
|
||||||
|
use crate::ProxyMetrics;
|
||||||
|
use crate::ProxyValidator;
|
||||||
|
use crate::TrustedProxyConfig;
|
||||||
|
use crate::TrustedProxyMiddleware;
|
||||||
|
|
||||||
|
/// Tower Layer for the trusted proxy middleware.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrustedProxyLayer {
|
||||||
|
/// The validator used to verify proxy chains.
|
||||||
|
pub(crate) validator: Arc<ProxyValidator>,
|
||||||
|
/// Whether the middleware is enabled.
|
||||||
|
pub(crate) enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrustedProxyLayer {
|
||||||
|
/// Creates a new `TrustedProxyLayer`.
|
||||||
|
pub fn new(config: TrustedProxyConfig, metrics: Option<ProxyMetrics>, enabled: bool) -> Self {
|
||||||
|
let validator = ProxyValidator::new(config, metrics);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
validator: Arc::new(validator),
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `TrustedProxyLayer` that is enabled by default.
|
||||||
|
pub fn enabled(config: TrustedProxyConfig, metrics: Option<ProxyMetrics>) -> Self {
|
||||||
|
Self::new(config, metrics, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `TrustedProxyLayer` that is disabled.
|
||||||
|
pub fn disabled() -> Self {
|
||||||
|
Self::new(
|
||||||
|
TrustedProxyConfig::new(Vec::new(), crate::config::ValidationMode::Lenient, true, 10, true, Vec::new()),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the middleware is enabled.
|
||||||
|
pub fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Layer<S> for TrustedProxyLayer {
|
||||||
|
type Service = TrustedProxyMiddleware<S>;
|
||||||
|
|
||||||
|
fn layer(&self, inner: S) -> Self::Service {
|
||||||
|
TrustedProxyMiddleware {
|
||||||
|
inner,
|
||||||
|
validator: self.validator.clone(),
|
||||||
|
enabled: self.enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/trusted-proxies/src/middleware/mod.rs
Normal file
21
crates/trusted-proxies/src/middleware/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Middleware module for Axum web framework
|
||||||
|
|
||||||
|
mod layer;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
pub use layer::*;
|
||||||
|
pub use service::*;
|
||||||
130
crates/trusted-proxies/src/middleware/service.rs
Normal file
130
crates/trusted-proxies/src/middleware/service.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Tower service implementation for the trusted proxy middleware.
|
||||||
|
|
||||||
|
use crate::{ClientInfo, ProxyValidator, TrustedProxyLayer};
|
||||||
|
use http::Request;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
use tower::Service;
|
||||||
|
use tracing::{Span, debug, instrument};
|
||||||
|
|
||||||
|
/// Tower Service for the trusted proxy middleware.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TrustedProxyMiddleware<S> {
|
||||||
|
/// The inner service being wrapped.
|
||||||
|
pub(crate) inner: S,
|
||||||
|
/// The validator used to verify proxy chains.
|
||||||
|
pub(crate) validator: Arc<ProxyValidator>,
|
||||||
|
/// Whether the middleware is enabled.
|
||||||
|
pub(crate) enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> TrustedProxyMiddleware<S> {
|
||||||
|
/// Creates a new `TrustedProxyMiddleware`.
|
||||||
|
pub fn new(inner: S, validator: Arc<ProxyValidator>, enabled: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
validator,
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `TrustedProxyMiddleware` from a `TrustedProxyLayer`.
|
||||||
|
pub fn from_layer(inner: S, layer: &TrustedProxyLayer) -> Self {
|
||||||
|
Self::new(inner, layer.validator.clone(), layer.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, ReqBody> Service<Request<ReqBody>> for TrustedProxyMiddleware<S>
|
||||||
|
where
|
||||||
|
S: Service<Request<ReqBody>> + Clone + Send + 'static,
|
||||||
|
S::Future: Send,
|
||||||
|
{
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = S::Future;
|
||||||
|
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
name = "trusted_proxy_middleware",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
http.method = %req.method(),
|
||||||
|
http.uri = %req.uri(),
|
||||||
|
http.version = ?req.version(),
|
||||||
|
enabled = self.enabled,
|
||||||
|
peer.addr = tracing::field::Empty,
|
||||||
|
client.ip = tracing::field::Empty,
|
||||||
|
client.trusted = tracing::field::Empty,
|
||||||
|
client.hops = tracing::field::Empty,
|
||||||
|
error = tracing::field::Empty,
|
||||||
|
error.message = tracing::field::Empty,
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
|
||||||
|
let span = Span::current();
|
||||||
|
|
||||||
|
// If the middleware is disabled, pass the request through immediately.
|
||||||
|
if !self.enabled {
|
||||||
|
debug!("Trusted proxy middleware is disabled");
|
||||||
|
return self.inner.call(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
// Extract the direct peer address from the request extensions.
|
||||||
|
let peer_addr = req.extensions().get::<std::net::SocketAddr>().copied();
|
||||||
|
|
||||||
|
if let Some(addr) = peer_addr {
|
||||||
|
span.record("peer.addr", addr.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the request and extract client information.
|
||||||
|
match self.validator.validate_request(peer_addr, req.headers()) {
|
||||||
|
Ok(client_info) => {
|
||||||
|
span.record("client.ip", client_info.real_ip.to_string());
|
||||||
|
span.record("client.trusted", client_info.is_from_trusted_proxy);
|
||||||
|
span.record("client.hops", client_info.proxy_hops as i64);
|
||||||
|
|
||||||
|
// Insert the verified client info into the request extensions.
|
||||||
|
req.extensions_mut().insert(client_info);
|
||||||
|
|
||||||
|
let duration = start_time.elapsed();
|
||||||
|
debug!("Proxy validation successful in {:?}", duration);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
span.record("error", true);
|
||||||
|
span.record("error.message", err.to_string());
|
||||||
|
|
||||||
|
// If the error is recoverable, fallback to a direct connection info.
|
||||||
|
if err.is_recoverable() {
|
||||||
|
let client_info = ClientInfo::direct(
|
||||||
|
peer_addr.unwrap_or_else(|| std::net::SocketAddr::new(std::net::IpAddr::from([0, 0, 0, 0]), 0)),
|
||||||
|
);
|
||||||
|
req.extensions_mut().insert(client_info);
|
||||||
|
} else {
|
||||||
|
debug!("Unrecoverable proxy validation error: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the inner service.
|
||||||
|
self.inner.call(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
crates/trusted-proxies/src/proxy/cache.rs
Normal file
84
crates/trusted-proxies/src/proxy/cache.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! High-performance cache implementation for proxy validation results using Moka.
|
||||||
|
|
||||||
|
use moka::future::Cache;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Cache for storing IP validation results.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IpValidationCache {
|
||||||
|
/// The underlying Moka cache.
|
||||||
|
cache: Cache<IpAddr, bool>,
|
||||||
|
/// Whether the cache is enabled.
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpValidationCache {
|
||||||
|
/// Creates a new `IpValidationCache` using Moka.
|
||||||
|
pub fn new(capacity: usize, ttl: Duration, enabled: bool) -> Self {
|
||||||
|
let cache = Cache::builder().max_capacity(capacity as u64).time_to_live(ttl).build();
|
||||||
|
|
||||||
|
Self { cache, enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP is trusted, using the cache if available.
|
||||||
|
pub async fn is_trusted(&self, ip: &IpAddr, validator: impl FnOnce(&IpAddr) -> bool) -> bool {
|
||||||
|
if !self.enabled {
|
||||||
|
return validator(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to get the result from cache.
|
||||||
|
if let Some(is_trusted) = self.cache.get(ip).await {
|
||||||
|
metrics::counter!("rustfs_trusted_proxy_cache_hits").increment(1);
|
||||||
|
return is_trusted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss: perform validation and update cache.
|
||||||
|
metrics::counter!("rustfs_trusted_proxy_cache_misses").increment(1);
|
||||||
|
let is_trusted = validator(ip);
|
||||||
|
self.cache.insert(*ip, is_trusted).await;
|
||||||
|
|
||||||
|
is_trusted
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all entries from the cache.
|
||||||
|
pub async fn clear(&self) {
|
||||||
|
self.cache.invalidate_all();
|
||||||
|
metrics::gauge!("rustfs_trusted_proxy_cache_size").set(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns statistics about the current state of the cache.
|
||||||
|
pub fn stats(&self) -> CacheStats {
|
||||||
|
let entry_count = self.cache.entry_count();
|
||||||
|
|
||||||
|
CacheStats {
|
||||||
|
size: entry_count as usize,
|
||||||
|
// Moka doesn't expose max_capacity directly in a simple way after build,
|
||||||
|
// but we can track it if needed.
|
||||||
|
capacity: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistics about the IP validation cache.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CacheStats {
|
||||||
|
/// Current number of entries in the cache.
|
||||||
|
pub size: usize,
|
||||||
|
/// Maximum capacity of the cache.
|
||||||
|
pub capacity: usize,
|
||||||
|
}
|
||||||
257
crates/trusted-proxies/src/proxy/chain.rs
Normal file
257
crates/trusted-proxies/src/proxy/chain.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Proxy chain analysis and validation logic.
|
||||||
|
|
||||||
|
use crate::{ProxyError, TrustedProxyConfig, ValidationMode, is_valid_ip_address};
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
/// Result of analyzing a proxy chain.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChainAnalysis {
|
||||||
|
/// The identified real client IP address.
|
||||||
|
pub client_ip: IpAddr,
|
||||||
|
/// The number of validated proxy hops.
|
||||||
|
pub hops: usize,
|
||||||
|
/// Whether the proxy chain is continuous and trusted.
|
||||||
|
pub is_continuous: bool,
|
||||||
|
/// List of warnings generated during analysis.
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
/// The validation mode used for analysis.
|
||||||
|
pub validation_mode: ValidationMode,
|
||||||
|
/// The portion of the chain that consists of trusted proxies.
|
||||||
|
pub trusted_chain: Vec<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Analyzer for verifying the integrity of proxy chains.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProxyChainAnalyzer {
|
||||||
|
/// Configuration for trusted proxies.
|
||||||
|
config: TrustedProxyConfig,
|
||||||
|
/// Cache of trusted IP addresses for fast lookup.
|
||||||
|
trusted_ip_cache: HashSet<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyChainAnalyzer {
|
||||||
|
/// Creates a new `ProxyChainAnalyzer`.
|
||||||
|
pub fn new(config: TrustedProxyConfig) -> Self {
|
||||||
|
let mut trusted_ip_cache = HashSet::new();
|
||||||
|
|
||||||
|
for proxy in &config.proxies {
|
||||||
|
match proxy {
|
||||||
|
crate::TrustedProxy::Single(ip) => {
|
||||||
|
trusted_ip_cache.insert(*ip);
|
||||||
|
}
|
||||||
|
crate::TrustedProxy::Cidr(network) => {
|
||||||
|
// For small networks, cache all IPs to speed up lookups.
|
||||||
|
// Only cache IPv4 networks to avoid iterating huge IPv6 ranges.
|
||||||
|
if network.is_ipv4() && network.prefix() >= 24 {
|
||||||
|
for ip in network.iter() {
|
||||||
|
trusted_ip_cache.insert(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
trusted_ip_cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Analyzes a proxy chain to identify the real client IP and verify trust.
|
||||||
|
pub fn analyze_chain(
|
||||||
|
&self,
|
||||||
|
proxy_chain: &[IpAddr],
|
||||||
|
current_proxy_ip: IpAddr,
|
||||||
|
headers: &HeaderMap,
|
||||||
|
) -> Result<ChainAnalysis, ProxyError> {
|
||||||
|
trace!("Analyzing proxy chain: {:?} with current proxy: {}", proxy_chain, current_proxy_ip);
|
||||||
|
|
||||||
|
// Validate all IP addresses in the chain.
|
||||||
|
self.validate_ip_addresses(proxy_chain)?;
|
||||||
|
|
||||||
|
// Construct the full chain including the direct peer.
|
||||||
|
let mut full_chain = proxy_chain.to_vec();
|
||||||
|
full_chain.push(current_proxy_ip);
|
||||||
|
|
||||||
|
// Enforce maximum hop limit.
|
||||||
|
if full_chain.len() > self.config.max_hops {
|
||||||
|
return Err(ProxyError::ChainTooLong(full_chain.len(), self.config.max_hops));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze the chain based on the configured validation mode.
|
||||||
|
let (client_ip, trusted_chain, hops) = match self.config.validation_mode {
|
||||||
|
ValidationMode::Lenient => self.analyze_lenient(&full_chain),
|
||||||
|
ValidationMode::Strict => self.analyze_strict(&full_chain)?,
|
||||||
|
ValidationMode::HopByHop => self.analyze_hop_by_hop(&full_chain),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for chain continuity if enabled.
|
||||||
|
let is_continuous = if self.config.enable_chain_continuity_check {
|
||||||
|
self.check_chain_continuity(&full_chain, &trusted_chain)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect any warnings.
|
||||||
|
let warnings = self.collect_warnings(&full_chain, &trusted_chain, headers);
|
||||||
|
|
||||||
|
// Final validation of the identified client IP.
|
||||||
|
if !is_valid_ip_address(&client_ip) {
|
||||||
|
return Err(ProxyError::internal(format!("Invalid client IP identified: {}", client_ip)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ChainAnalysis {
|
||||||
|
client_ip,
|
||||||
|
hops,
|
||||||
|
is_continuous,
|
||||||
|
warnings,
|
||||||
|
validation_mode: self.config.validation_mode,
|
||||||
|
trusted_chain,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lenient mode: Accepts the entire chain if the last proxy is trusted.
|
||||||
|
fn analyze_lenient(&self, chain: &[IpAddr]) -> (IpAddr, Vec<IpAddr>, usize) {
|
||||||
|
if chain.is_empty() {
|
||||||
|
return (IpAddr::from([0, 0, 0, 0]), Vec::new(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(last_proxy) = chain.last()
|
||||||
|
&& self.is_ip_trusted(last_proxy)
|
||||||
|
{
|
||||||
|
let client_ip = chain.first().copied().unwrap_or(*last_proxy);
|
||||||
|
return (client_ip, chain.to_vec(), chain.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_ip = chain.first().copied().unwrap_or(IpAddr::from([0, 0, 0, 0]));
|
||||||
|
(client_ip, Vec::new(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strict mode: Requires every IP in the chain to be trusted.
|
||||||
|
fn analyze_strict(&self, chain: &[IpAddr]) -> Result<(IpAddr, Vec<IpAddr>, usize), ProxyError> {
|
||||||
|
if chain.is_empty() {
|
||||||
|
return Ok((IpAddr::from([0, 0, 0, 0]), Vec::new(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, ip) in chain.iter().enumerate() {
|
||||||
|
if !self.is_ip_trusted(ip) {
|
||||||
|
return Err(ProxyError::chain_failed(format!("Proxy at position {} ({}) is not trusted", i, ip)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_ip = chain.first().copied().unwrap_or(IpAddr::from([0, 0, 0, 0]));
|
||||||
|
Ok((client_ip, chain.to_vec(), chain.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hop-by-hop mode: Traverses the chain from right to left to find the first untrusted IP.
|
||||||
|
fn analyze_hop_by_hop(&self, chain: &[IpAddr]) -> (IpAddr, Vec<IpAddr>, usize) {
|
||||||
|
if chain.is_empty() {
|
||||||
|
return (IpAddr::from([0, 0, 0, 0]), Vec::new(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut trusted_chain = Vec::new();
|
||||||
|
let mut validated_hops = 0;
|
||||||
|
|
||||||
|
// Traverse from the most recent proxy back towards the client.
|
||||||
|
for ip in chain.iter().rev() {
|
||||||
|
if self.is_ip_trusted(ip) {
|
||||||
|
trusted_chain.insert(0, *ip);
|
||||||
|
validated_hops += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if trusted_chain.is_empty() {
|
||||||
|
let client_ip = *chain.last().unwrap();
|
||||||
|
(client_ip, vec![client_ip], 0)
|
||||||
|
} else {
|
||||||
|
let client_ip_index = chain.len().saturating_sub(trusted_chain.len());
|
||||||
|
let client_ip = if client_ip_index > 0 {
|
||||||
|
chain[client_ip_index - 1]
|
||||||
|
} else {
|
||||||
|
chain[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
(client_ip, trusted_chain, validated_hops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies that the trusted portion of the chain is a continuous suffix of the full chain.
|
||||||
|
fn check_chain_continuity(&self, full_chain: &[IpAddr], trusted_chain: &[IpAddr]) -> bool {
|
||||||
|
if full_chain.len() <= 1 || trusted_chain.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if trusted_chain.len() > full_chain.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_tail = &full_chain[full_chain.len() - trusted_chain.len()..];
|
||||||
|
expected_tail == trusted_chain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that IP addresses are not unspecified, multicast, or otherwise invalid.
|
||||||
|
fn validate_ip_addresses(&self, chain: &[IpAddr]) -> Result<(), ProxyError> {
|
||||||
|
for ip in chain {
|
||||||
|
if ip.is_unspecified() {
|
||||||
|
return Err(ProxyError::invalid_xff("IP address cannot be unspecified (0.0.0.0 or ::)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.is_multicast() {
|
||||||
|
return Err(ProxyError::invalid_xff("IP address cannot be multicast"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_valid_ip_address(ip) {
|
||||||
|
return Err(ProxyError::IpParseError(format!("Invalid IP address in chain: {}", ip)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is trusted based on the configuration.
|
||||||
|
fn is_ip_trusted(&self, ip: &IpAddr) -> bool {
|
||||||
|
if self.trusted_ip_cache.contains(ip) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.config.proxies.iter().any(|proxy| proxy.contains(ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects warnings about potential issues in the proxy chain.
|
||||||
|
fn collect_warnings(&self, full_chain: &[IpAddr], trusted_chain: &[IpAddr], headers: &HeaderMap) -> Vec<String> {
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
|
if !trusted_chain.is_empty() && !headers.contains_key("x-forwarded-for") && !headers.contains_key("forwarded") {
|
||||||
|
warnings.push("No proxy headers found for request from trusted proxy".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seen_ips = HashSet::new();
|
||||||
|
for ip in full_chain {
|
||||||
|
if !seen_ips.insert(ip) {
|
||||||
|
warnings.push(format!("Duplicate IP address detected in proxy chain: {}", ip));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings
|
||||||
|
}
|
||||||
|
}
|
||||||
219
crates/trusted-proxies/src/proxy/metrics.rs
Normal file
219
crates/trusted-proxies/src/proxy/metrics.rs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Metrics and monitoring for proxy validation performance and results.
|
||||||
|
|
||||||
|
use crate::{ProxyError, ValidationMode};
|
||||||
|
use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// Collector for proxy validation metrics.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProxyMetrics {
|
||||||
|
/// Whether metrics collection is enabled.
|
||||||
|
enabled: bool,
|
||||||
|
/// Application name used as a label for metrics.
|
||||||
|
app_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyMetrics {
|
||||||
|
/// Creates a new `ProxyMetrics` collector.
|
||||||
|
pub fn new(app_name: &str, enabled: bool) -> Self {
|
||||||
|
let metrics = Self {
|
||||||
|
enabled,
|
||||||
|
app_name: app_name.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register metric descriptions for Prometheus.
|
||||||
|
metrics.register_descriptions();
|
||||||
|
|
||||||
|
metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers descriptions for all metrics.
|
||||||
|
fn register_descriptions(&self) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe_counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_attempts_total",
|
||||||
|
"Total number of proxy validation attempts"
|
||||||
|
);
|
||||||
|
describe_counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_success_total",
|
||||||
|
"Total number of successful proxy validations"
|
||||||
|
);
|
||||||
|
describe_counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_failure_total",
|
||||||
|
"Total number of failed proxy validations"
|
||||||
|
);
|
||||||
|
describe_counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_failure_by_type_total",
|
||||||
|
"Total number of failed proxy validations categorized by error type"
|
||||||
|
);
|
||||||
|
describe_gauge!("rustfs_trusted_proxy_chain_length", "Current length of proxy chains being validated");
|
||||||
|
describe_histogram!(
|
||||||
|
"rustfs_trusted_proxy_validation_duration_seconds",
|
||||||
|
"Time taken to validate a proxy chain in seconds"
|
||||||
|
);
|
||||||
|
describe_gauge!(
|
||||||
|
"rustfs_trusted_proxy_cache_size",
|
||||||
|
"Current number of entries in the proxy validation cache"
|
||||||
|
);
|
||||||
|
describe_counter!("rustfs_trusted_proxy_cache_hits_total", "Total number of cache hits for proxy validation");
|
||||||
|
describe_counter!(
|
||||||
|
"rustfs_trusted_proxy_cache_misses_total",
|
||||||
|
"Total number of cache misses for proxy validation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increments the total number of validation attempts.
|
||||||
|
pub fn increment_validation_attempts(&self) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_attempts_total",
|
||||||
|
"app" => self.app_name.clone()
|
||||||
|
)
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records a successful validation.
|
||||||
|
pub fn record_validation_success(&self, from_trusted_proxy: bool, proxy_hops: usize, duration: Duration) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_success_total",
|
||||||
|
"app" => self.app_name.clone(),
|
||||||
|
"trusted" => from_trusted_proxy.to_string()
|
||||||
|
)
|
||||||
|
.increment(1);
|
||||||
|
|
||||||
|
gauge!(
|
||||||
|
"rustfs_trusted_proxy_chain_length",
|
||||||
|
"app" => self.app_name.clone()
|
||||||
|
)
|
||||||
|
.set(proxy_hops as f64);
|
||||||
|
|
||||||
|
histogram!(
|
||||||
|
"rustfs_trusted_proxy_validation_duration_seconds",
|
||||||
|
"app" => self.app_name.clone()
|
||||||
|
)
|
||||||
|
.record(duration.as_secs_f64());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records a failed validation with the specific error type.
|
||||||
|
pub fn record_validation_failure(&self, error: &ProxyError, duration: Duration) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_type = match error {
|
||||||
|
ProxyError::InvalidXForwardedFor(_) => "invalid_x_forwarded_for",
|
||||||
|
ProxyError::InvalidForwardedHeader(_) => "invalid_forwarded_header",
|
||||||
|
ProxyError::ChainValidationFailed(_) => "chain_validation_failed",
|
||||||
|
ProxyError::ChainTooLong(_, _) => "chain_too_long",
|
||||||
|
ProxyError::UntrustedProxy(_) => "untrusted_proxy",
|
||||||
|
ProxyError::ChainNotContinuous => "chain_not_continuous",
|
||||||
|
ProxyError::IpParseError(_) => "ip_parse_error",
|
||||||
|
ProxyError::HeaderParseError(_) => "header_parse_error",
|
||||||
|
ProxyError::Timeout => "timeout",
|
||||||
|
ProxyError::Internal(_) => "internal",
|
||||||
|
};
|
||||||
|
|
||||||
|
counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_failure_total",
|
||||||
|
"app" => self.app_name.clone(),
|
||||||
|
"error_type" => error_type
|
||||||
|
)
|
||||||
|
.increment(1);
|
||||||
|
|
||||||
|
counter!(
|
||||||
|
"rustfs_trusted_proxy_validation_failure_by_type_total",
|
||||||
|
"app" => self.app_name.clone(),
|
||||||
|
"error_type" => error_type
|
||||||
|
)
|
||||||
|
.increment(1);
|
||||||
|
|
||||||
|
histogram!(
|
||||||
|
"rustfs_trusted_proxy_validation_duration_seconds",
|
||||||
|
"app" => self.app_name.clone(),
|
||||||
|
"error_type" => error_type
|
||||||
|
)
|
||||||
|
.record(duration.as_secs_f64());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records the validation mode currently in use.
|
||||||
|
pub fn record_validation_mode(&self, mode: ValidationMode) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gauge!(
|
||||||
|
"rustfs_trusted_proxy_validation_mode",
|
||||||
|
"app" => self.app_name.clone(),
|
||||||
|
"mode" => mode.as_str()
|
||||||
|
)
|
||||||
|
.set(match mode {
|
||||||
|
ValidationMode::Lenient => 0.0,
|
||||||
|
ValidationMode::Strict => 1.0,
|
||||||
|
ValidationMode::HopByHop => 2.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records cache performance metrics.
|
||||||
|
pub fn record_cache_metrics(&self, hits: u64, misses: u64, size: usize) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter!("rustfs_trusted_proxy_cache_hits_total", "app" => self.app_name.clone()).increment(hits);
|
||||||
|
counter!("rustfs_trusted_proxy_cache_misses_total", "app" => self.app_name.clone()).increment(misses);
|
||||||
|
gauge!("rustfs_trusted_proxy_cache_size", "app" => self.app_name.clone()).set(size as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a summary of enabled metrics to the log.
|
||||||
|
pub fn print_summary(&self) {
|
||||||
|
if !self.enabled {
|
||||||
|
info!("Metrics collection is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Proxy metrics enabled for application: {}", self.app_name);
|
||||||
|
info!("Available metrics:");
|
||||||
|
info!(" - rustfs_trusted_proxy_validation_attempts_total");
|
||||||
|
info!(" - rustfs_trusted_proxy_validation_success_total");
|
||||||
|
info!(" - rustfs_trusted_proxy_validation_failure_total");
|
||||||
|
info!(" - rustfs_trusted_proxy_validation_failure_by_type_total");
|
||||||
|
info!(" - rustfs_trusted_proxy_chain_length");
|
||||||
|
info!(" - rustfs_trusted_proxy_validation_duration_seconds");
|
||||||
|
info!(" - rustfs_trusted_proxy_cache_size");
|
||||||
|
info!(" - rustfs_trusted_proxy_cache_hits_total");
|
||||||
|
info!(" - rustfs_trusted_proxy_cache_misses_total");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default application name for metrics.
|
||||||
|
const DEFAULT_APP_NAME: &str = "trusted-proxy";
|
||||||
|
|
||||||
|
/// Creates a default `ProxyMetrics` collector.
|
||||||
|
pub fn default_proxy_metrics(enabled: bool) -> ProxyMetrics {
|
||||||
|
ProxyMetrics::new(DEFAULT_APP_NAME, enabled)
|
||||||
|
}
|
||||||
28
crates/trusted-proxies/src/proxy/mod.rs
Normal file
28
crates/trusted-proxies/src/proxy/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Core proxy handling module
|
||||||
|
//!
|
||||||
|
//! This module contains the main logic for validating and processing
|
||||||
|
//! requests through trusted proxies.
|
||||||
|
|
||||||
|
mod cache;
|
||||||
|
mod chain;
|
||||||
|
mod metrics;
|
||||||
|
mod validator;
|
||||||
|
|
||||||
|
pub use cache::*;
|
||||||
|
pub use chain::*;
|
||||||
|
pub use metrics::*;
|
||||||
|
pub use validator::*;
|
||||||
337
crates/trusted-proxies/src/proxy/validator.rs
Normal file
337
crates/trusted-proxies/src/proxy/validator.rs
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Proxy validator for verifying proxy chains and extracting client information.
|
||||||
|
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::time::Instant;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
use crate::{ProxyChainAnalyzer, ProxyError, ProxyMetrics, TrustedProxyConfig, ValidationMode};
|
||||||
|
|
||||||
|
/// Information about the client extracted from the request and proxy headers.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ClientInfo {
|
||||||
|
/// The verified real IP address of the client.
|
||||||
|
pub real_ip: IpAddr,
|
||||||
|
/// The original host requested by the client (if provided by a trusted proxy).
|
||||||
|
pub forwarded_host: Option<String>,
|
||||||
|
/// The original protocol (http/https) used by the client (if provided by a trusted proxy).
|
||||||
|
pub forwarded_proto: Option<String>,
|
||||||
|
/// Whether the request was received from a trusted proxy.
|
||||||
|
pub is_from_trusted_proxy: bool,
|
||||||
|
/// The IP address of the proxy that directly connected to this server.
|
||||||
|
pub proxy_ip: Option<IpAddr>,
|
||||||
|
/// The number of proxy hops identified in the chain.
|
||||||
|
pub proxy_hops: usize,
|
||||||
|
/// The validation mode used for this request.
|
||||||
|
pub validation_mode: ValidationMode,
|
||||||
|
/// Any warnings generated during the validation process.
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientInfo {
|
||||||
|
/// Creates a `ClientInfo` for a direct connection without any proxies.
|
||||||
|
pub fn direct(addr: SocketAddr) -> Self {
|
||||||
|
Self {
|
||||||
|
real_ip: addr.ip(),
|
||||||
|
forwarded_host: None,
|
||||||
|
forwarded_proto: None,
|
||||||
|
is_from_trusted_proxy: false,
|
||||||
|
proxy_ip: None,
|
||||||
|
proxy_hops: 0,
|
||||||
|
validation_mode: ValidationMode::Lenient,
|
||||||
|
warnings: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a `ClientInfo` for a request received through a trusted proxy.
|
||||||
|
pub fn from_trusted_proxy(
|
||||||
|
real_ip: IpAddr,
|
||||||
|
forwarded_host: Option<String>,
|
||||||
|
forwarded_proto: Option<String>,
|
||||||
|
proxy_ip: IpAddr,
|
||||||
|
proxy_hops: usize,
|
||||||
|
validation_mode: ValidationMode,
|
||||||
|
warnings: Vec<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
real_ip,
|
||||||
|
forwarded_host,
|
||||||
|
forwarded_proto,
|
||||||
|
is_from_trusted_proxy: true,
|
||||||
|
proxy_ip: Some(proxy_ip),
|
||||||
|
proxy_hops,
|
||||||
|
validation_mode,
|
||||||
|
warnings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string representation of the client info for logging.
|
||||||
|
pub fn to_log_string(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"client_ip={}, proxy={:?}, hops={}, trusted={}, mode={:?}",
|
||||||
|
self.real_ip, self.proxy_ip, self.proxy_hops, self.is_from_trusted_proxy, self.validation_mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core validator that processes incoming requests to verify proxy chains.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProxyValidator {
|
||||||
|
/// Configuration for trusted proxies.
|
||||||
|
config: TrustedProxyConfig,
|
||||||
|
/// Analyzer for verifying the integrity of the proxy chain.
|
||||||
|
chain_analyzer: ProxyChainAnalyzer,
|
||||||
|
/// Metrics collector for observability.
|
||||||
|
metrics: Option<ProxyMetrics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyValidator {
|
||||||
|
/// Creates a new `ProxyValidator` with the given configuration and metrics.
|
||||||
|
pub fn new(config: TrustedProxyConfig, metrics: Option<ProxyMetrics>) -> Self {
|
||||||
|
let chain_analyzer = ProxyChainAnalyzer::new(config.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
chain_analyzer,
|
||||||
|
metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates an incoming request and extracts client information.
|
||||||
|
pub fn validate_request(&self, peer_addr: Option<SocketAddr>, headers: &HeaderMap) -> Result<ClientInfo, ProxyError> {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
|
||||||
|
// Record the start of the validation attempt.
|
||||||
|
self.record_metric_start();
|
||||||
|
|
||||||
|
// Perform the internal validation logic.
|
||||||
|
let result = self.validate_request_internal(peer_addr, headers);
|
||||||
|
|
||||||
|
// Record the result and duration.
|
||||||
|
let duration = start_time.elapsed();
|
||||||
|
self.record_metric_result(&result, duration);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal logic for request validation.
|
||||||
|
fn validate_request_internal(&self, peer_addr: Option<SocketAddr>, headers: &HeaderMap) -> Result<ClientInfo, ProxyError> {
|
||||||
|
// Fallback to unspecified address if peer address is missing.
|
||||||
|
let peer_addr = peer_addr.unwrap_or_else(|| SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0));
|
||||||
|
|
||||||
|
// Check if the direct peer is a trusted proxy.
|
||||||
|
if self.config.is_trusted(&peer_addr) {
|
||||||
|
debug!("Request received from trusted proxy: {}", peer_addr.ip());
|
||||||
|
|
||||||
|
// Parse and validate headers from the trusted proxy.
|
||||||
|
self.validate_trusted_proxy_request(&peer_addr, headers)
|
||||||
|
} else {
|
||||||
|
// Log a warning if the request is from a private network but not trusted.
|
||||||
|
if self.config.is_private_network(&peer_addr.ip()) {
|
||||||
|
warn!(
|
||||||
|
"Request from private network but not trusted: {}. This might indicate a configuration issue.",
|
||||||
|
peer_addr.ip()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat as a direct connection if the peer is not trusted.
|
||||||
|
Ok(ClientInfo::direct(peer_addr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a request that originated from a trusted proxy.
|
||||||
|
fn validate_trusted_proxy_request(&self, proxy_addr: &SocketAddr, headers: &HeaderMap) -> Result<ClientInfo, ProxyError> {
|
||||||
|
let proxy_ip = proxy_addr.ip();
|
||||||
|
|
||||||
|
// Prefer RFC 7239 "Forwarded" header if enabled, otherwise fallback to legacy headers.
|
||||||
|
let client_info = if self.config.enable_rfc7239 {
|
||||||
|
self.try_parse_rfc7239_headers(headers, proxy_ip)
|
||||||
|
.unwrap_or_else(|| self.parse_legacy_headers(headers))
|
||||||
|
} else {
|
||||||
|
self.parse_legacy_headers(headers)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Analyze the integrity and continuity of the proxy chain.
|
||||||
|
let chain_analysis = self
|
||||||
|
.chain_analyzer
|
||||||
|
.analyze_chain(&client_info.proxy_chain, proxy_ip, headers)?;
|
||||||
|
|
||||||
|
// Enforce maximum hop limit.
|
||||||
|
if chain_analysis.hops > self.config.max_hops {
|
||||||
|
return Err(ProxyError::ChainTooLong(chain_analysis.hops, self.config.max_hops));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce chain continuity if enabled.
|
||||||
|
if self.config.enable_chain_continuity_check && !chain_analysis.is_continuous {
|
||||||
|
return Err(ProxyError::ChainNotContinuous);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ClientInfo::from_trusted_proxy(
|
||||||
|
chain_analysis.client_ip,
|
||||||
|
client_info.forwarded_host,
|
||||||
|
client_info.forwarded_proto,
|
||||||
|
proxy_ip,
|
||||||
|
chain_analysis.hops,
|
||||||
|
self.config.validation_mode,
|
||||||
|
chain_analysis.warnings,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse the RFC 7239 "Forwarded" header.
|
||||||
|
fn try_parse_rfc7239_headers(&self, headers: &HeaderMap, proxy_ip: IpAddr) -> Option<ParsedHeaders> {
|
||||||
|
headers
|
||||||
|
.get("forwarded")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.and_then(|s| Self::parse_forwarded_header(s, proxy_ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses legacy proxy headers (X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto).
|
||||||
|
fn parse_legacy_headers(&self, headers: &HeaderMap) -> ParsedHeaders {
|
||||||
|
let forwarded_host = headers
|
||||||
|
.get("x-forwarded-host")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.map(String::from);
|
||||||
|
|
||||||
|
let forwarded_proto = headers
|
||||||
|
.get("x-forwarded-proto")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.map(String::from);
|
||||||
|
|
||||||
|
let proxy_chain = headers
|
||||||
|
.get("x-forwarded-for")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.map(Self::parse_x_forwarded_for)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
ParsedHeaders {
|
||||||
|
proxy_chain,
|
||||||
|
forwarded_host,
|
||||||
|
forwarded_proto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the RFC 7239 "Forwarded" header value.
|
||||||
|
fn parse_forwarded_header(header_value: &str, proxy_ip: IpAddr) -> Option<ParsedHeaders> {
|
||||||
|
// Simplified implementation: processes only the first entry in the header.
|
||||||
|
let first_part = header_value.split(',').next()?.trim();
|
||||||
|
|
||||||
|
let mut proxy_chain = Vec::new();
|
||||||
|
let mut forwarded_host = None;
|
||||||
|
let mut forwarded_proto = None;
|
||||||
|
|
||||||
|
for part in first_part.split(';') {
|
||||||
|
let part = part.trim();
|
||||||
|
if let Some((key, value)) = part.split_once('=') {
|
||||||
|
let key = key.trim().to_lowercase();
|
||||||
|
let value = value.trim().trim_matches('"');
|
||||||
|
|
||||||
|
match key.as_str() {
|
||||||
|
"for" => {
|
||||||
|
// Extract IP address, handling IPv6 addresses in brackets as per RFC 7239.
|
||||||
|
let ip_str = if value.starts_with('[') {
|
||||||
|
if let Some(end) = value.find(']') {
|
||||||
|
&value[1..end]
|
||||||
|
} else {
|
||||||
|
continue; // Invalid format, skip
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||||
|
value.split(':').next().unwrap_or(value)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||||
|
proxy_chain.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"host" => {
|
||||||
|
forwarded_host = Some(value.to_string());
|
||||||
|
}
|
||||||
|
"proto" => {
|
||||||
|
forwarded_proto = Some(value.to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to the proxy IP if no client IP was found in the header.
|
||||||
|
if proxy_chain.is_empty() {
|
||||||
|
proxy_chain.push(proxy_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ParsedHeaders {
|
||||||
|
proxy_chain,
|
||||||
|
forwarded_host,
|
||||||
|
forwarded_proto,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the X-Forwarded-For header into a list of IP addresses.
|
||||||
|
pub fn parse_x_forwarded_for(header_value: &str) -> Vec<IpAddr> {
|
||||||
|
header_value
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.filter_map(|s| {
|
||||||
|
// Handle IPv6 addresses in brackets, e.g., [::1]:8080
|
||||||
|
let ip_str = if s.starts_with('[') {
|
||||||
|
if let Some(end) = s.find(']') {
|
||||||
|
&s[1..end]
|
||||||
|
} else {
|
||||||
|
s // Invalid format, try parsing as is
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||||
|
s.split(':').next().unwrap_or(s)
|
||||||
|
};
|
||||||
|
ip_str.parse::<IpAddr>().ok()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records the start of a validation attempt in metrics.
|
||||||
|
fn record_metric_start(&self) {
|
||||||
|
if let Some(metrics) = &self.metrics {
|
||||||
|
metrics.increment_validation_attempts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records the result of a validation attempt in metrics.
|
||||||
|
fn record_metric_result(&self, result: &Result<ClientInfo, ProxyError>, duration: std::time::Duration) {
|
||||||
|
if let Some(metrics) = &self.metrics {
|
||||||
|
match result {
|
||||||
|
Ok(client_info) => {
|
||||||
|
metrics.record_validation_success(client_info.is_from_trusted_proxy, client_info.proxy_hops, duration);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
metrics.record_validation_failure(err, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal structure for holding parsed header information.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ParsedHeaders {
|
||||||
|
/// The chain of proxy IPs (client IP is typically the first).
|
||||||
|
proxy_chain: Vec<IpAddr>,
|
||||||
|
/// The original host requested.
|
||||||
|
forwarded_host: Option<String>,
|
||||||
|
/// The original protocol used.
|
||||||
|
forwarded_proto: Option<String>,
|
||||||
|
}
|
||||||
230
crates/trusted-proxies/src/utils/ip.rs
Normal file
230
crates/trusted-proxies/src/utils/ip.rs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! IP address utility functions for validation and classification.
|
||||||
|
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Collection of IP-related utility functions.
|
||||||
|
pub struct IpUtils;
|
||||||
|
|
||||||
|
impl IpUtils {
|
||||||
|
/// Checks if an IP address is valid for general use.
|
||||||
|
///
|
||||||
|
/// "Valid" here means the address is syntactically valid and not an unspecified or multicast
|
||||||
|
/// address. Classification (private/link-local/documentation/reserved) is handled separately.
|
||||||
|
pub fn is_valid_ip_address(ip: &IpAddr) -> bool {
|
||||||
|
!ip.is_unspecified() && !ip.is_multicast()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address belongs to a reserved range.
|
||||||
|
pub fn is_reserved_ip(ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4) => Self::is_reserved_ipv4(ipv4),
|
||||||
|
IpAddr::V6(ipv6) => Self::is_reserved_ipv6(ipv6),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IPv4 address belongs to a reserved range.
|
||||||
|
pub fn is_reserved_ipv4(ip: &Ipv4Addr) -> bool {
|
||||||
|
let octets = ip.octets();
|
||||||
|
|
||||||
|
// Check common reserved IPv4 ranges
|
||||||
|
matches!(
|
||||||
|
octets,
|
||||||
|
[0, _, _, _] | // 0.0.0.0/8
|
||||||
|
[10, _, _, _] | // 10.0.0.0/8
|
||||||
|
[100, 64, _, _] | // 100.64.0.0/10
|
||||||
|
[127, _, _, _] | // 127.0.0.0/8
|
||||||
|
[169, 254, _, _] | // 169.254.0.0/16
|
||||||
|
[172, 16..=31, _, _] | // 172.16.0.0/12
|
||||||
|
[192, 0, 0, _] | // 192.0.0.0/24
|
||||||
|
[192, 0, 2, _] | // 192.0.2.0/24
|
||||||
|
[192, 88, 99, _] | // 192.88.99.0/24
|
||||||
|
[192, 168, _, _] | // 192.168.0.0/16
|
||||||
|
[198, 18..=19, _, _] | // 198.18.0.0/15
|
||||||
|
[198, 51, 100, _] | // 198.51.100.0/24
|
||||||
|
[203, 0, 113, _] | // 203.0.113.0/24
|
||||||
|
[224..=239, _, _, _] | // 224.0.0.0/4
|
||||||
|
[240..=255, _, _, _] // 240.0.0.0/4
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IPv6 address belongs to a reserved range.
|
||||||
|
pub fn is_reserved_ipv6(ip: &Ipv6Addr) -> bool {
|
||||||
|
let segments = ip.segments();
|
||||||
|
|
||||||
|
// Check common reserved IPv6 ranges
|
||||||
|
matches!(
|
||||||
|
segments,
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 0] | // ::/128
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 1] | // ::1/128
|
||||||
|
[0x2001, 0xdb8, _, _, _, _, _, _] | // 2001:db8::/32
|
||||||
|
[0xfc00..=0xfdff, _, _, _, _, _, _, _] | // fc00::/7
|
||||||
|
[0xfe80..=0xfebf, _, _, _, _, _, _, _] | // fe80::/10
|
||||||
|
[0xff00..=0xffff, _, _, _, _, _, _, _] // ff00::/8
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is a private address.
|
||||||
|
pub fn is_private_ip(ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4) => Self::is_private_ipv4(ipv4),
|
||||||
|
IpAddr::V6(ipv6) => Self::is_private_ipv6(ipv6),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IPv4 address is a private address.
|
||||||
|
pub fn is_private_ipv4(ip: &Ipv4Addr) -> bool {
|
||||||
|
let octets = ip.octets();
|
||||||
|
|
||||||
|
matches!(
|
||||||
|
octets,
|
||||||
|
[10, _, _, _] | // 10.0.0.0/8
|
||||||
|
[172, 16..=31, _, _] | // 172.16.0.0/12
|
||||||
|
[192, 168, _, _] // 192.168.0.0/16
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IPv6 address is a private address.
|
||||||
|
pub fn is_private_ipv6(ip: &Ipv6Addr) -> bool {
|
||||||
|
let segments = ip.segments();
|
||||||
|
|
||||||
|
matches!(
|
||||||
|
segments,
|
||||||
|
[0xfc00..=0xfdff, _, _, _, _, _, _, _] // fc00::/7
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is a loopback address.
|
||||||
|
pub fn is_loopback_ip(ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4) => ipv4.is_loopback(),
|
||||||
|
IpAddr::V6(ipv6) => ipv6.is_loopback(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is a link-local address.
|
||||||
|
pub fn is_link_local_ip(ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4) => ipv4.is_link_local(),
|
||||||
|
IpAddr::V6(ipv6) => ipv6.is_unicast_link_local(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is a documentation address (TEST-NET).
|
||||||
|
pub fn is_documentation_ip(ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4) => {
|
||||||
|
let octets = ipv4.octets();
|
||||||
|
matches!(
|
||||||
|
octets,
|
||||||
|
[192, 0, 2, _] | // 192.0.2.0/24
|
||||||
|
[198, 51, 100, _] | // 198.51.100.0/24
|
||||||
|
[203, 0, 113, _] // 203.0.113.0/24
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IpAddr::V6(ipv6) => {
|
||||||
|
let segments = ipv6.segments();
|
||||||
|
matches!(segments, [0x2001, 0xdb8, _, _, _, _, _, _]) // 2001:db8::/32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an IP address or CIDR range from a string.
|
||||||
|
pub fn parse_ip_or_cidr(s: &str) -> Result<IpNetwork, String> {
|
||||||
|
IpNetwork::from_str(s).map_err(|e| format!("Failed to parse IP/CIDR '{}': {}", s, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a comma-separated list of IP addresses.
|
||||||
|
pub fn parse_ip_list(s: &str) -> Result<Vec<IpAddr>, String> {
|
||||||
|
let mut ips = Vec::new();
|
||||||
|
|
||||||
|
for part in s.split(',') {
|
||||||
|
let part = part.trim();
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match IpAddr::from_str(part) {
|
||||||
|
Ok(ip) => ips.push(ip),
|
||||||
|
Err(e) => return Err(format!("Failed to parse IP '{}': {}", part, e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a comma-separated list of IP networks (CIDR).
|
||||||
|
pub fn parse_network_list(s: &str) -> Result<Vec<IpNetwork>, String> {
|
||||||
|
let mut networks = Vec::new();
|
||||||
|
|
||||||
|
for part in s.split(',') {
|
||||||
|
let part = part.trim();
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match Self::parse_ip_or_cidr(part) {
|
||||||
|
Ok(network) => networks.push(network),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(networks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is contained within any of the given networks.
|
||||||
|
pub fn ip_in_networks(ip: &IpAddr, networks: &[IpNetwork]) -> bool {
|
||||||
|
networks.iter().any(|network| network.contains(*ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string description of the IP address type.
|
||||||
|
pub fn get_ip_type(ip: &IpAddr) -> &'static str {
|
||||||
|
if Self::is_private_ip(ip) {
|
||||||
|
"private"
|
||||||
|
} else if Self::is_loopback_ip(ip) {
|
||||||
|
"loopback"
|
||||||
|
} else if Self::is_link_local_ip(ip) {
|
||||||
|
"link_local"
|
||||||
|
} else if Self::is_documentation_ip(ip) {
|
||||||
|
"documentation"
|
||||||
|
} else if Self::is_reserved_ip(ip) {
|
||||||
|
"reserved"
|
||||||
|
} else {
|
||||||
|
"public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the canonical string representation of an IP address.
|
||||||
|
pub fn canonical_ip(ip: &IpAddr) -> String {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(ipv4) => ipv4.to_string(),
|
||||||
|
IpAddr::V6(ipv6) => {
|
||||||
|
// Use the standard library's Display implementation for canonical representation
|
||||||
|
ipv6.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is valid for general use.
|
||||||
|
///
|
||||||
|
/// "Valid" here means the address is syntactically valid and not an unspecified or multicast
|
||||||
|
/// address. Classification (private/link-local/documentation/reserved) is handled separately.
|
||||||
|
pub fn is_valid_ip_address(ip: &IpAddr) -> bool {
|
||||||
|
!ip.is_unspecified() && !ip.is_multicast()
|
||||||
|
}
|
||||||
21
crates/trusted-proxies/src/utils/mod.rs
Normal file
21
crates/trusted-proxies/src/utils/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Utility functions and helpers for the trusted proxy system.
|
||||||
|
|
||||||
|
mod ip;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
pub use ip::*;
|
||||||
|
pub use validation::*;
|
||||||
223
crates/trusted-proxies/src/utils/validation.rs
Normal file
223
crates/trusted-proxies/src/utils/validation.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Validation utility functions for various data types.
|
||||||
|
|
||||||
|
use http::HeaderMap;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||||
|
static URL_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||||
|
static SAFE_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Collection of validation utility functions.
|
||||||
|
pub struct ValidationUtils;
|
||||||
|
|
||||||
|
impl ValidationUtils {
|
||||||
|
/// Validates an email address format.
|
||||||
|
pub fn is_valid_email(email: &str) -> bool {
|
||||||
|
EMAIL_REGEX
|
||||||
|
.get_or_init(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").expect("Invalid email regex"))
|
||||||
|
.is_match(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a URL format.
|
||||||
|
pub fn is_valid_url(url: &str) -> bool {
|
||||||
|
URL_REGEX
|
||||||
|
.get_or_init(|| {
|
||||||
|
Regex::new(r"^(https?://)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}(/.*)?$")
|
||||||
|
.expect("Invalid URL regex")
|
||||||
|
})
|
||||||
|
.is_match(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the format of an X-Forwarded-For header value.
|
||||||
|
pub fn validate_x_forwarded_for(header_value: &str) -> bool {
|
||||||
|
if header_value.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ips: Vec<&str> = header_value.split(',').map(|s| s.trim()).collect();
|
||||||
|
|
||||||
|
for ip_str in ips {
|
||||||
|
if ip_str.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ip_part) = Self::extract_ip_part(ip_str) {
|
||||||
|
if IpAddr::from_str(ip_part).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the IP part from a string, handling brackets for IPv6.
|
||||||
|
pub fn extract_ip_part(ip_str: &str) -> Option<&str> {
|
||||||
|
if ip_str.starts_with('[') {
|
||||||
|
if let Some(end) = ip_str.find(']') {
|
||||||
|
Some(&ip_str[1..end])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For IPv4 or IPv6 without brackets, take the part before the first colon.
|
||||||
|
Some(ip_str.split(':').next().unwrap_or(ip_str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the format of an RFC 7239 Forwarded header value.
|
||||||
|
pub fn validate_forwarded_header(header_value: &str) -> bool {
|
||||||
|
if header_value.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts: Vec<&str> = header_value.split(';').collect();
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for part in parts {
|
||||||
|
let part = part.trim();
|
||||||
|
if !part.contains('=') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an IP address is within any of the specified CIDR ranges.
|
||||||
|
pub fn validate_ip_in_range(ip: &IpAddr, cidr_ranges: &[String]) -> bool {
|
||||||
|
for cidr in cidr_ranges {
|
||||||
|
if let Ok(network) = ipnetwork::IpNetwork::from_str(cidr)
|
||||||
|
&& network.contains(*ip)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a header value for security (length and control characters).
|
||||||
|
pub fn validate_header_value(value: &str) -> bool {
|
||||||
|
for c in value.chars() {
|
||||||
|
if c.is_control() && c != '\t' {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.len() > 8192 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates an entire HeaderMap for security.
|
||||||
|
pub fn validate_headers(headers: &HeaderMap) -> bool {
|
||||||
|
for (name, value) in headers {
|
||||||
|
let name_str = name.as_str();
|
||||||
|
if name_str.len() > 256 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value_str) = value.to_str() {
|
||||||
|
if !Self::validate_header_value(value_str) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if value.len() > 8192 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a port number.
|
||||||
|
pub fn validate_port(port: u16) -> bool {
|
||||||
|
port > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a CIDR notation string.
|
||||||
|
pub fn validate_cidr(cidr: &str) -> bool {
|
||||||
|
ipnetwork::IpNetwork::from_str(cidr).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the length of a proxy chain.
|
||||||
|
pub fn validate_proxy_chain_length(chain: &[IpAddr], max_length: usize) -> bool {
|
||||||
|
chain.len() <= max_length
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a proxy chain does not contain duplicate adjacent IPs.
|
||||||
|
pub fn validate_proxy_chain_continuity(chain: &[IpAddr]) -> bool {
|
||||||
|
if chain.len() < 2 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 1..chain.len() {
|
||||||
|
if chain[i] == chain[i - 1] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a string contains only safe characters for use in URLs or headers.
|
||||||
|
pub fn is_safe_string(s: &str) -> bool {
|
||||||
|
SAFE_REGEX
|
||||||
|
.get_or_init(|| Regex::new(r"^[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;=]+$").expect("Invalid safe string regex"))
|
||||||
|
.is_match(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates rate limiting parameters.
|
||||||
|
pub fn validate_rate_limit_params(requests: u32, period_seconds: u64) -> bool {
|
||||||
|
requests > 0 && requests <= 10000 && period_seconds > 0 && period_seconds <= 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates cache configuration parameters.
|
||||||
|
pub fn validate_cache_params(capacity: usize, ttl_seconds: u64) -> bool {
|
||||||
|
capacity > 0 && capacity <= 1000000 && ttl_seconds > 0 && ttl_seconds <= 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redacts sensitive information from a string based on provided patterns.
|
||||||
|
pub fn mask_sensitive_data(data: &str, sensitive_patterns: &[&str]) -> String {
|
||||||
|
let mut result = data.to_string();
|
||||||
|
|
||||||
|
for pattern in sensitive_patterns {
|
||||||
|
match Regex::new(&format!(r#"(?i)({})[:=]\s*([^&\s]+)"#, pattern)) {
|
||||||
|
Ok(regex) => {
|
||||||
|
result = regex
|
||||||
|
.replace_all(&result, |caps: ®ex::Captures| format!("{}:[REDACTED]", &caps[1]))
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Invalid sensitive pattern '{}': {}", pattern, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/trusted-proxies/tests/integration/cloud_tests.rs
Normal file
31
crates/trusted-proxies/tests/integration/cloud_tests.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use rustfs_trusted_proxies::AwsMetadataFetcher;
|
||||||
|
use rustfs_trusted_proxies::CloudDetector;
|
||||||
|
use rustfs_trusted_proxies::CloudMetadataFetcher;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cloud_detector_disabled() {
|
||||||
|
let detector = CloudDetector::new(false, Duration::from_secs(1), None);
|
||||||
|
let provider = detector.detect_provider();
|
||||||
|
assert!(provider.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_aws_metadata_fetcher() {
|
||||||
|
let fetcher = AwsMetadataFetcher::new(Duration::from_secs(5));
|
||||||
|
assert_eq!(fetcher.provider_name(), "aws");
|
||||||
|
}
|
||||||
19
crates/trusted-proxies/tests/integration/mod.rs
Normal file
19
crates/trusted-proxies/tests/integration/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Integration tests for the trusted proxy system.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod cloud_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod proxy_tests;
|
||||||
36
crates/trusted-proxies/tests/integration/proxy_tests.rs
Normal file
36
crates/trusted-proxies/tests/integration/proxy_tests.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::{Router, routing::get};
|
||||||
|
use rustfs_trusted_proxies::{TrustedProxy, TrustedProxyConfig, TrustedProxyLayer, ValidationMode};
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_proxy_validation_flow() {
|
||||||
|
let proxies = vec![TrustedProxy::Single("127.0.0.1".parse().unwrap())];
|
||||||
|
let config = TrustedProxyConfig::new(proxies, ValidationMode::HopByHop, true, 10, true, vec![]);
|
||||||
|
let proxy_layer = TrustedProxyLayer::enabled(config, None);
|
||||||
|
|
||||||
|
let app = Router::new().route("/test", get(|| async { "OK" })).layer(proxy_layer);
|
||||||
|
|
||||||
|
let request = axum::http::Request::builder()
|
||||||
|
.uri("/test")
|
||||||
|
.header("X-Forwarded-For", "203.0.113.195")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = app.oneshot(request).await.unwrap();
|
||||||
|
assert_eq!(response.status(), 200);
|
||||||
|
}
|
||||||
137
crates/trusted-proxies/tests/unit/config_tests.rs
Normal file
137
crates/trusted-proxies/tests/unit/config_tests.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use rustfs_config::{DEFAULT_TRUSTED_PROXY_PROXIES, ENV_TRUSTED_PROXY_PROXIES};
|
||||||
|
use rustfs_trusted_proxies::{ConfigLoader, TrustedProxy, TrustedProxyConfig, ValidationMode};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn test_config_loader_default() {
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(ENV_TRUSTED_PROXY_PROXIES);
|
||||||
|
}
|
||||||
|
let config = ConfigLoader::from_env_or_default();
|
||||||
|
assert_eq!(config.server_addr.port(), 9000);
|
||||||
|
assert!(!config.proxy.proxies.is_empty());
|
||||||
|
assert_eq!(config.proxy.validation_mode, ValidationMode::HopByHop);
|
||||||
|
assert!(config.proxy.enable_rfc7239);
|
||||||
|
assert_eq!(config.proxy.max_hops, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn test_config_loader_env_vars() {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(ENV_TRUSTED_PROXY_PROXIES, "192.168.1.0/24,10.0.0.0/8");
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("RUSTFS_TRUSTED_PROXY_VALIDATION_MODE", "strict");
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("RUSTFS_TRUSTED_PROXY_MAX_HOPS", "5");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = ConfigLoader::from_env();
|
||||||
|
|
||||||
|
if let Ok(config) = config {
|
||||||
|
assert_eq!(config.server_addr.port(), 9000);
|
||||||
|
assert_eq!(config.proxy.validation_mode, ValidationMode::Strict);
|
||||||
|
assert_eq!(config.proxy.max_hops, 5);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(ENV_TRUSTED_PROXY_PROXIES);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("RUSTFS_TRUSTED_PROXY_VALIDATION_MODE");
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("RUSTFS_TRUSTED_PROXY_MAX_HOPS");
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("SERVER_PORT");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Failed to load configuration from environment variables");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trusted_proxy_config() {
|
||||||
|
let proxies = vec![
|
||||||
|
TrustedProxy::Single("192.168.1.1".parse().unwrap()),
|
||||||
|
TrustedProxy::Cidr("10.0.0.0/8".parse().unwrap()),
|
||||||
|
];
|
||||||
|
|
||||||
|
let config = TrustedProxyConfig::new(proxies.clone(), ValidationMode::Strict, true, 10, true, vec![]);
|
||||||
|
|
||||||
|
assert_eq!(config.proxies.len(), 2);
|
||||||
|
assert_eq!(config.validation_mode, ValidationMode::Strict);
|
||||||
|
assert!(config.enable_rfc7239);
|
||||||
|
assert_eq!(config.max_hops, 10);
|
||||||
|
assert!(config.enable_chain_continuity_check);
|
||||||
|
|
||||||
|
let test_ip: IpAddr = "192.168.1.1".parse().unwrap();
|
||||||
|
let test_socket_addr = std::net::SocketAddr::new(test_ip, 8080);
|
||||||
|
assert!(config.is_trusted(&test_socket_addr));
|
||||||
|
|
||||||
|
let test_ip2: IpAddr = "10.0.1.1".parse().unwrap();
|
||||||
|
let test_socket_addr2 = std::net::SocketAddr::new(test_ip2, 8080);
|
||||||
|
assert!(config.is_trusted(&test_socket_addr2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trusted_proxy_contains() {
|
||||||
|
let single_proxy = TrustedProxy::Single("192.168.1.1".parse().unwrap());
|
||||||
|
let test_ip: IpAddr = "192.168.1.1".parse().unwrap();
|
||||||
|
let test_ip2: IpAddr = "192.168.1.2".parse().unwrap();
|
||||||
|
|
||||||
|
assert!(single_proxy.contains(&test_ip));
|
||||||
|
assert!(!single_proxy.contains(&test_ip2));
|
||||||
|
|
||||||
|
let cidr_proxy = TrustedProxy::Cidr("192.168.1.0/24".parse().unwrap());
|
||||||
|
assert!(cidr_proxy.contains(&test_ip));
|
||||||
|
assert!(cidr_proxy.contains(&test_ip2));
|
||||||
|
|
||||||
|
let test_ip3: IpAddr = "192.168.2.1".parse().unwrap();
|
||||||
|
assert!(!cidr_proxy.contains(&test_ip3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_private_network_check() {
|
||||||
|
let config = TrustedProxyConfig::new(
|
||||||
|
Vec::new(),
|
||||||
|
ValidationMode::Lenient,
|
||||||
|
true,
|
||||||
|
10,
|
||||||
|
true,
|
||||||
|
vec!["10.0.0.0/8".parse().unwrap(), "192.168.0.0/16".parse().unwrap()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let private_ip: IpAddr = "10.0.1.1".parse().unwrap();
|
||||||
|
let private_ip2: IpAddr = "192.168.1.1".parse().unwrap();
|
||||||
|
let public_ip: IpAddr = "8.8.8.8".parse().unwrap();
|
||||||
|
|
||||||
|
assert!(config.is_private_network(&private_ip));
|
||||||
|
assert!(config.is_private_network(&private_ip2));
|
||||||
|
assert!(!config.is_private_network(&public_ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_values() {
|
||||||
|
assert_eq!(
|
||||||
|
DEFAULT_TRUSTED_PROXY_PROXIES,
|
||||||
|
"127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8"
|
||||||
|
);
|
||||||
|
}
|
||||||
200
crates/trusted-proxies/tests/unit/ip_tests.rs
Normal file
200
crates/trusted-proxies/tests/unit/ip_tests.rs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use rustfs_trusted_proxies::IpUtils;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_valid_ip_address() {
|
||||||
|
let valid_ip: IpAddr = "192.168.1.1".parse().unwrap();
|
||||||
|
assert!(IpUtils::is_valid_ip_address(&valid_ip));
|
||||||
|
|
||||||
|
let unspecified_ip: IpAddr = "0.0.0.0".parse().unwrap();
|
||||||
|
assert!(!IpUtils::is_valid_ip_address(&unspecified_ip));
|
||||||
|
|
||||||
|
let multicast_ip: IpAddr = "224.0.0.1".parse().unwrap();
|
||||||
|
assert!(!IpUtils::is_valid_ip_address(&multicast_ip));
|
||||||
|
|
||||||
|
let valid_ipv6: IpAddr = "2001:db8::1".parse().unwrap();
|
||||||
|
assert!(IpUtils::is_valid_ip_address(&valid_ipv6));
|
||||||
|
|
||||||
|
let unspecified_ipv6: IpAddr = "::".parse().unwrap();
|
||||||
|
assert!(!IpUtils::is_valid_ip_address(&unspecified_ipv6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_reserved_ip() {
|
||||||
|
let private_ip: IpAddr = "10.0.0.1".parse().unwrap();
|
||||||
|
assert!(IpUtils::is_reserved_ip(&private_ip));
|
||||||
|
|
||||||
|
let loopback_ip: IpAddr = "127.0.0.1".parse().unwrap();
|
||||||
|
assert!(IpUtils::is_reserved_ip(&loopback_ip));
|
||||||
|
|
||||||
|
let link_local_ip: IpAddr = "169.254.0.1".parse().unwrap();
|
||||||
|
assert!(IpUtils::is_reserved_ip(&link_local_ip));
|
||||||
|
|
||||||
|
let documentation_ip: IpAddr = "192.0.2.1".parse().unwrap();
|
||||||
|
assert!(IpUtils::is_reserved_ip(&documentation_ip));
|
||||||
|
|
||||||
|
let public_ip: IpAddr = "8.8.8.8".parse().unwrap();
|
||||||
|
assert!(!IpUtils::is_reserved_ip(&public_ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_private_ip() {
|
||||||
|
assert!(IpUtils::is_private_ip(&"10.0.0.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_private_ip(&"10.255.255.254".parse().unwrap()));
|
||||||
|
|
||||||
|
assert!(IpUtils::is_private_ip(&"172.16.0.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_private_ip(&"172.31.255.254".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_private_ip(&"172.15.0.1".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_private_ip(&"172.32.0.1".parse().unwrap()));
|
||||||
|
|
||||||
|
assert!(IpUtils::is_private_ip(&"192.168.0.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_private_ip(&"192.168.255.254".parse().unwrap()));
|
||||||
|
|
||||||
|
assert!(!IpUtils::is_private_ip(&"8.8.8.8".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_private_ip(&"203.0.113.1".parse().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_loopback_ip() {
|
||||||
|
assert!(IpUtils::is_loopback_ip(&"127.0.0.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_loopback_ip(&"127.255.255.254".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_loopback_ip(&"::1".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_loopback_ip(&"192.168.1.1".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_loopback_ip(&"2001:db8::1".parse().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_link_local_ip() {
|
||||||
|
assert!(IpUtils::is_link_local_ip(&"169.254.0.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_link_local_ip(&"169.254.255.254".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_link_local_ip(&"fe80::1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_link_local_ip(&"fe80::abcd:1234:5678:9abc".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_link_local_ip(&"192.168.1.1".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_link_local_ip(&"2001:db8::1".parse().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_documentation_ip() {
|
||||||
|
assert!(IpUtils::is_documentation_ip(&"192.0.2.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_documentation_ip(&"198.51.100.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_documentation_ip(&"203.0.113.1".parse().unwrap()));
|
||||||
|
assert!(IpUtils::is_documentation_ip(&"2001:db8::1".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_documentation_ip(&"8.8.8.8".parse().unwrap()));
|
||||||
|
assert!(!IpUtils::is_documentation_ip(&"2001:4860::1".parse().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_ip_or_cidr() {
|
||||||
|
let result = IpUtils::parse_ip_or_cidr("192.168.1.1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_or_cidr("192.168.1.0/24");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_or_cidr("2001:db8::1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_or_cidr("2001:db8::/32");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_or_cidr("invalid");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_ip_list() {
|
||||||
|
let result = IpUtils::parse_ip_list("192.168.1.1, 10.0.0.1, 8.8.8.8");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let ips = result.unwrap();
|
||||||
|
assert_eq!(ips.len(), 3);
|
||||||
|
assert_eq!(ips[0], IpAddr::from_str("192.168.1.1").unwrap());
|
||||||
|
assert_eq!(ips[1], IpAddr::from_str("10.0.0.1").unwrap());
|
||||||
|
assert_eq!(ips[2], IpAddr::from_str("8.8.8.8").unwrap());
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_list("192.168.1.1,10.0.0.1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap().len(), 2);
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_list("");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().is_empty());
|
||||||
|
|
||||||
|
let result = IpUtils::parse_ip_list("192.168.1.1, invalid");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_network_list() {
|
||||||
|
let result = IpUtils::parse_network_list("192.168.1.0/24, 10.0.0.0/8");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let networks = result.unwrap();
|
||||||
|
assert_eq!(networks.len(), 2);
|
||||||
|
|
||||||
|
let result = IpUtils::parse_network_list("192.168.1.1");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap().len(), 1);
|
||||||
|
|
||||||
|
let result = IpUtils::parse_network_list("192.168.1.0/24, invalid");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ip_in_networks() {
|
||||||
|
let networks = vec!["10.0.0.0/8".parse().unwrap(), "192.168.1.0/24".parse().unwrap()];
|
||||||
|
|
||||||
|
let ip_in_network: IpAddr = "10.0.1.1".parse().unwrap();
|
||||||
|
let ip_in_network2: IpAddr = "192.168.1.100".parse().unwrap();
|
||||||
|
let ip_not_in_network: IpAddr = "8.8.8.8".parse().unwrap();
|
||||||
|
|
||||||
|
assert!(IpUtils::ip_in_networks(&ip_in_network, &networks));
|
||||||
|
assert!(IpUtils::ip_in_networks(&ip_in_network2, &networks));
|
||||||
|
assert!(!IpUtils::ip_in_networks(&ip_not_in_network, &networks));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_ip_type() {
|
||||||
|
assert_eq!(IpUtils::get_ip_type(&"10.0.0.1".parse().unwrap()), "private");
|
||||||
|
assert_eq!(IpUtils::get_ip_type(&"127.0.0.1".parse().unwrap()), "loopback");
|
||||||
|
assert_eq!(IpUtils::get_ip_type(&"169.254.0.1".parse().unwrap()), "link_local");
|
||||||
|
assert_eq!(IpUtils::get_ip_type(&"192.0.2.1".parse().unwrap()), "documentation");
|
||||||
|
assert_eq!(IpUtils::get_ip_type(&"224.0.0.1".parse().unwrap()), "reserved");
|
||||||
|
assert_eq!(IpUtils::get_ip_type(&"8.8.8.8".parse().unwrap()), "public");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_canonical_ip() {
|
||||||
|
// NOTE: std parsing rejects IPv4 octets with leading zeros (e.g. "001") on some platforms/
|
||||||
|
// Rust versions because of ambiguity with non-decimal notations. Use an unambiguous input.
|
||||||
|
let ipv4: IpAddr = "192.168.1.1".parse().unwrap();
|
||||||
|
assert_eq!(IpUtils::canonical_ip(&ipv4), "192.168.1.1");
|
||||||
|
|
||||||
|
let ipv6_full: IpAddr = "2001:0db8:0000:0000:0000:0000:0000:0001".parse().unwrap();
|
||||||
|
let ipv6_compressed: IpAddr = "2001:db8::1".parse().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(IpUtils::canonical_ip(&ipv6_full), "2001:db8::1");
|
||||||
|
assert_eq!(IpUtils::canonical_ip(&ipv6_compressed), "2001:db8::1");
|
||||||
|
|
||||||
|
// For IPv6 with multiple runs of zeros, the exact compression choice is an output formatting
|
||||||
|
// detail. Compare canonicalized address values instead of requiring a specific layout.
|
||||||
|
let ipv6_multi_zero: IpAddr = "2001:0db8:0000:0000:abcd:0000:0000:1234".parse().unwrap();
|
||||||
|
let canonical = IpUtils::canonical_ip(&ipv6_multi_zero);
|
||||||
|
let reparsed: IpAddr = canonical.parse().unwrap();
|
||||||
|
assert_eq!(reparsed, ipv6_multi_zero);
|
||||||
|
}
|
||||||
24
crates/trusted-proxies/tests/unit/mod.rs
Normal file
24
crates/trusted-proxies/tests/unit/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Unit tests for the trusted proxy system components.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod config_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod ip_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod validation_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod validator_tests;
|
||||||
65
crates/trusted-proxies/tests/unit/validation_tests.rs
Normal file
65
crates/trusted-proxies/tests/unit/validation_tests.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use rustfs_trusted_proxies::ValidationUtils;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_email_validation() {
|
||||||
|
assert!(ValidationUtils::is_valid_email("user@example.com"));
|
||||||
|
assert!(!ValidationUtils::is_valid_email("invalid-email"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_url_validation() {
|
||||||
|
assert!(ValidationUtils::is_valid_url("https://example.com"));
|
||||||
|
assert!(!ValidationUtils::is_valid_url("invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_x_forwarded_for_validation() {
|
||||||
|
assert!(ValidationUtils::validate_x_forwarded_for("203.0.113.195"));
|
||||||
|
assert!(!ValidationUtils::validate_x_forwarded_for("invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_forwarded_header_validation() {
|
||||||
|
assert!(ValidationUtils::validate_forwarded_header("for=192.0.2.60"));
|
||||||
|
assert!(!ValidationUtils::validate_forwarded_header("invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ip_in_range_validation() {
|
||||||
|
let cidr_ranges = vec!["10.0.0.0/8".to_string(), "192.168.0.0/16".to_string()];
|
||||||
|
let ip: IpAddr = "10.0.1.1".parse().unwrap();
|
||||||
|
assert!(ValidationUtils::validate_ip_in_range(&ip, &cidr_ranges));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_header_value_validation() {
|
||||||
|
assert!(ValidationUtils::validate_header_value("text/plain"));
|
||||||
|
assert!(!ValidationUtils::validate_header_value(&"a".repeat(8193)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_port_validation() {
|
||||||
|
assert!(ValidationUtils::validate_port(80));
|
||||||
|
assert!(!ValidationUtils::validate_port(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cidr_validation() {
|
||||||
|
assert!(ValidationUtils::validate_cidr("192.168.1.0/24"));
|
||||||
|
assert!(!ValidationUtils::validate_cidr("invalid"));
|
||||||
|
}
|
||||||
79
crates/trusted-proxies/tests/unit/validator_tests.rs
Normal file
79
crates/trusted-proxies/tests/unit/validator_tests.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use rustfs_trusted_proxies::{ClientInfo, ProxyChainAnalyzer, ProxyValidator, TrustedProxy, TrustedProxyConfig, ValidationMode};
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn create_test_config() -> TrustedProxyConfig {
|
||||||
|
let proxies = vec![
|
||||||
|
TrustedProxy::Single("192.168.1.100".parse().unwrap()),
|
||||||
|
TrustedProxy::Cidr("10.0.0.0/8".parse().unwrap()),
|
||||||
|
];
|
||||||
|
TrustedProxyConfig::new(proxies, ValidationMode::HopByHop, true, 5, true, vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_client_info_direct() {
|
||||||
|
let addr = SocketAddr::new(IpAddr::from([192, 168, 1, 1]), 8080);
|
||||||
|
let client_info = ClientInfo::direct(addr);
|
||||||
|
assert_eq!(client_info.real_ip, IpAddr::from([192, 168, 1, 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_x_forwarded_for() {
|
||||||
|
let header_value = "203.0.113.195, 198.51.100.1";
|
||||||
|
let result = ProxyValidator::parse_x_forwarded_for(header_value);
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proxy_chain_analyzer_hop_by_hop() {
|
||||||
|
let config = create_test_config();
|
||||||
|
let analyzer = ProxyChainAnalyzer::new(config);
|
||||||
|
let chain = vec![
|
||||||
|
IpAddr::from_str("203.0.113.195").unwrap(),
|
||||||
|
IpAddr::from_str("10.0.1.100").unwrap(),
|
||||||
|
];
|
||||||
|
let current_proxy = IpAddr::from_str("192.168.1.100").unwrap();
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
let result = analyzer.analyze_chain(&chain, current_proxy, &headers);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proxy_chain_too_long() {
|
||||||
|
let config = create_test_config();
|
||||||
|
let analyzer = ProxyChainAnalyzer::new(config);
|
||||||
|
let chain = vec![
|
||||||
|
IpAddr::from_str("203.0.113.195").unwrap(),
|
||||||
|
IpAddr::from_str("10.0.1.100").unwrap(),
|
||||||
|
IpAddr::from_str("10.0.1.101").unwrap(),
|
||||||
|
IpAddr::from_str("10.0.1.102").unwrap(),
|
||||||
|
IpAddr::from_str("10.0.1.103").unwrap(),
|
||||||
|
];
|
||||||
|
let current_proxy = IpAddr::from_str("192.168.1.100").unwrap();
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
// Total chain length is 6 (5 in chain + 1 current proxy), max hops is 5
|
||||||
|
let result = analyzer.analyze_chain(&chain, current_proxy, &headers);
|
||||||
|
assert!(result.is_err());
|
||||||
|
match result {
|
||||||
|
Err(rustfs_trusted_proxies::ProxyError::ChainTooLong(len, max)) => {
|
||||||
|
assert_eq!(len, 6);
|
||||||
|
assert_eq!(max, 5);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected ChainTooLong error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
12
rustfmt.toml
12
rustfmt.toml
@@ -15,3 +15,15 @@
|
|||||||
max_width = 130
|
max_width = 130
|
||||||
fn_call_width = 90
|
fn_call_width = 90
|
||||||
single_line_let_else_max_width = 100
|
single_line_let_else_max_width = 100
|
||||||
|
|
||||||
|
## Enable group import Preserve, StdExternalCrate, One
|
||||||
|
#group_imports = "StdExternalCrate"
|
||||||
|
#
|
||||||
|
## Import granularity control Preserve, Crate, Module, Item, One
|
||||||
|
#imports_granularity = "Crate"
|
||||||
|
#
|
||||||
|
## Sort alphabetically
|
||||||
|
#reorder_imports = true
|
||||||
|
#
|
||||||
|
## The instability feature needs to be enabled
|
||||||
|
#unstable_features = true
|
||||||
@@ -59,6 +59,7 @@ rustfs-rio.workspace = true
|
|||||||
rustfs-s3select-api = { workspace = true }
|
rustfs-s3select-api = { workspace = true }
|
||||||
rustfs-s3select-query = { workspace = true }
|
rustfs-s3select-query = { workspace = true }
|
||||||
rustfs-targets = { workspace = true }
|
rustfs-targets = { workspace = true }
|
||||||
|
rustfs-trusted-proxies = { workspace = true }
|
||||||
rustfs-utils = { workspace = true, features = ["full"] }
|
rustfs-utils = { workspace = true, features = ["full"] }
|
||||||
rustfs-zip = { workspace = true }
|
rustfs-zip = { workspace = true }
|
||||||
rustfs-scanner = { workspace = true }
|
rustfs-scanner = { workspace = true }
|
||||||
@@ -141,8 +142,7 @@ libsystemd.workspace = true
|
|||||||
[target.'cfg(not(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64")))'.dependencies]
|
[target.'cfg(not(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64")))'.dependencies]
|
||||||
mimalloc = { workspace = true }
|
mimalloc = { workspace = true }
|
||||||
|
|
||||||
# Note: If you want to *explicitly* exclude Windows from a target dependency set,
|
# Only enable pprof-based profiling on non-Windows targets.
|
||||||
# you can use a cfg like the following instead:
|
|
||||||
[target.'cfg(all(not(target_os = "windows"), not(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))))'.dependencies]
|
[target.'cfg(all(not(target_os = "windows"), not(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))))'.dependencies]
|
||||||
starshard = { workspace = true }
|
starshard = { workspace = true }
|
||||||
backtrace = { workspace = true }
|
backtrace = { workspace = true }
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ async fn async_main() -> Result<()> {
|
|||||||
// Initialize performance profiling if enabled
|
// Initialize performance profiling if enabled
|
||||||
profiling::init_from_env().await;
|
profiling::init_from_env().await;
|
||||||
|
|
||||||
|
// Initialize trusted proxies system
|
||||||
|
rustfs_trusted_proxies::init();
|
||||||
|
|
||||||
// Initialize TLS if a certificate path is provided
|
// Initialize TLS if a certificate path is provided
|
||||||
if let Some(tls_path) = &opt.tls_path {
|
if let Some(tls_path) = &opt.tls_path {
|
||||||
match init_cert(tls_path).await {
|
match init_cert(tls_path).await {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ use rustfs_common::GlobalReadiness;
|
|||||||
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||||
use rustfs_ecstore::rpc::{TONIC_RPC_PREFIX, verify_rpc_signature};
|
use rustfs_ecstore::rpc::{TONIC_RPC_PREFIX, verify_rpc_signature};
|
||||||
use rustfs_protos::proto_gen::node_service::node_service_server::NodeServiceServer;
|
use rustfs_protos::proto_gen::node_service::node_service_server::NodeServiceServer;
|
||||||
|
use rustfs_trusted_proxies::ClientInfo;
|
||||||
use rustfs_utils::net::parse_and_resolve_address;
|
use rustfs_utils::net::parse_and_resolve_address;
|
||||||
use rustls::ServerConfig;
|
use rustls::ServerConfig;
|
||||||
use s3s::{host::MultiDomain, service::S3Service, service::S3ServiceBuilder};
|
use s3s::{host::MultiDomain, service::S3Service, service::S3ServiceBuilder};
|
||||||
@@ -216,7 +217,7 @@ pub async fn start_http_server(
|
|||||||
b.set_access(store.clone());
|
b.set_access(store.clone());
|
||||||
b.set_route(admin::make_admin_route(opt.console_enable)?);
|
b.set_route(admin::make_admin_route(opt.console_enable)?);
|
||||||
|
|
||||||
// console server does not need to setup virtual-hosted-style requests
|
// Virtual-hosted-style requests are only set up for S3 API when server domains are configured and console is disabled
|
||||||
if !opt.server_domains.is_empty() && !opt.console_enable {
|
if !opt.server_domains.is_empty() && !opt.console_enable {
|
||||||
MultiDomain::new(&opt.server_domains).map_err(Error::other)?; // validate domains
|
MultiDomain::new(&opt.server_domains).map_err(Error::other)?; // validate domains
|
||||||
|
|
||||||
@@ -525,7 +526,7 @@ struct ConnectionContext {
|
|||||||
|
|
||||||
/// Process a single incoming TCP connection.
|
/// Process a single incoming TCP connection.
|
||||||
///
|
///
|
||||||
/// This function is executed in a new Tokio task and it will:
|
/// This function is executed in a new Tokio task, and it will:
|
||||||
/// 1. If TLS is configured, perform TLS handshake.
|
/// 1. If TLS is configured, perform TLS handshake.
|
||||||
/// 2. Build a complete service stack for this connection, including S3, RPC services, and all middleware.
|
/// 2. Build a complete service stack for this connection, including S3, RPC services, and all middleware.
|
||||||
/// 3. Use Hyper to handle HTTP requests on this connection.
|
/// 3. Use Hyper to handle HTTP requests on this connection.
|
||||||
@@ -562,11 +563,24 @@ fn process_connection(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let hybrid_service = ServiceBuilder::new()
|
let hybrid_service = ServiceBuilder::new()
|
||||||
|
// NOTE: Both extension types are intentionally inserted to maintain compatibility:
|
||||||
|
// 1. `Option<RemoteAddr>` - Used by existing admin/storage handlers throughout the codebase
|
||||||
|
// 2. `std::net::SocketAddr` - Required by TrustedProxyMiddleware for proxy validation
|
||||||
|
// This dual insertion is necessary because the middleware expects the raw SocketAddr type
|
||||||
|
// while our application code uses the RemoteAddr wrapper. Consolidating these would
|
||||||
|
// require either modifying the third-party middleware or refactoring all existing handlers.
|
||||||
|
.layer(AddExtensionLayer::new(remote_addr))
|
||||||
|
.option_layer(remote_addr.map(|ra| AddExtensionLayer::new(ra.0)))
|
||||||
|
// Add TrustedProxyLayer to handle X-Forwarded-For and other proxy headers
|
||||||
|
// This should be placed before TraceLayer so that logs reflect the real client IP
|
||||||
|
.option_layer(if rustfs_trusted_proxies::is_enabled() {
|
||||||
|
Some(rustfs_trusted_proxies::layer().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
|
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
|
||||||
.layer(CatchPanicLayer::new())
|
.layer(CatchPanicLayer::new())
|
||||||
.layer(AddExtensionLayer::new(remote_addr))
|
|
||||||
// CRITICAL: Insert ReadinessGateLayer before business logic
|
// CRITICAL: Insert ReadinessGateLayer before business logic
|
||||||
// This stops requests from hitting IAMAuth or Storage if they are not ready.
|
// This stops requests from hitting IAMAuth or Storage if they are not ready.
|
||||||
.layer(ReadinessGateLayer::new(readiness))
|
.layer(ReadinessGateLayer::new(readiness))
|
||||||
@@ -578,10 +592,18 @@ fn process_connection(
|
|||||||
.get(http::header::HeaderName::from_static("x-request-id"))
|
.get(http::header::HeaderName::from_static("x-request-id"))
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
// Extract real client IP from trusted proxy middleware if available
|
||||||
|
let client_info = request.extensions().get::<ClientInfo>();
|
||||||
|
let real_ip = client_info
|
||||||
|
.map(|info| info.real_ip.to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
let span = tracing::info_span!("http-request",
|
let span = tracing::info_span!("http-request",
|
||||||
trace_id = %trace_id,
|
trace_id = %trace_id,
|
||||||
status_code = tracing::field::Empty,
|
status_code = tracing::field::Empty,
|
||||||
method = %request.method(),
|
method = %request.method(),
|
||||||
|
real_ip = %real_ip,
|
||||||
uri = %request.uri(),
|
uri = %request.uri(),
|
||||||
version = ?request.version(),
|
version = ?request.version(),
|
||||||
);
|
);
|
||||||
@@ -773,7 +795,7 @@ fn get_listen_backlog() -> i32 {
|
|||||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||||
let mut name = [libc::CTL_KERN, libc::KERN_IPC, libc::KIPC_SOMAXCONN];
|
let mut name = [libc::CTL_KERN, libc::KERN_IPC, libc::KIPC_SOMAXCONN];
|
||||||
let mut buf = [0; 1];
|
let mut buf = [0; 1];
|
||||||
let mut buf_len = std::mem::size_of_val(&buf);
|
let mut buf_len = size_of_val(&buf);
|
||||||
|
|
||||||
if unsafe {
|
if unsafe {
|
||||||
libc::sysctl(
|
libc::sysctl(
|
||||||
|
|||||||
@@ -168,17 +168,27 @@ export RUSTFS_NS_SCANNER_INTERVAL=60 # Object scanning interval in seconds
|
|||||||
# - Minimum size threshold prevents compression of small files where overhead > benefit
|
# - Minimum size threshold prevents compression of small files where overhead > benefit
|
||||||
# - Wildcard patterns supported in MIME types (e.g., text/* matches text/plain, text/html, etc.)
|
# - Wildcard patterns supported in MIME types (e.g., text/* matches text/plain, text/html, etc.)
|
||||||
|
|
||||||
|
# Trusted Proxy Configuration
|
||||||
|
# export RUSTFS_TRUSTED_PROXY_ENABLED=true
|
||||||
|
# export RUSTFS_TRUSTED_PROXY_NETWORKS=127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8
|
||||||
|
# export RUSTFS_TRUSTED_PROXY_VALIDATION_MODE=hop_by_hop
|
||||||
|
# export RUSTFS_TRUSTED_PROXY_ENABLE_RFC7239=true
|
||||||
|
# export RUSTFS_TRUSTED_PROXY_MAX_HOPS=10
|
||||||
|
# export RUSTFS_TRUSTED_PROXY_METRICS_ENABLED=true
|
||||||
|
|
||||||
#export RUSTFS_REGION="us-east-1"
|
#export RUSTFS_REGION="us-east-1"
|
||||||
|
|
||||||
export RUSTFS_ENABLE_SCANNER=true
|
export RUSTFS_ENABLE_SCANNER=true
|
||||||
|
|
||||||
export RUSTFS_ENABLE_HEAL=false
|
export RUSTFS_ENABLE_HEAL=true
|
||||||
|
|
||||||
# Object cache configuration
|
# Object cache configuration
|
||||||
export RUSTFS_OBJECT_CACHE_ENABLE=true
|
export RUSTFS_OBJECT_CACHE_ENABLE=true
|
||||||
|
|
||||||
# Profiling configuration
|
# Profiling configuration
|
||||||
export RUSTFS_ENABLE_PROFILING=true
|
export RUSTFS_ENABLE_PROFILING=false
|
||||||
|
# Memory profiling periodic dump
|
||||||
|
export RUSTFS_PROF_MEM_PERIODIC=false
|
||||||
|
|
||||||
# Heal configuration queue size
|
# Heal configuration queue size
|
||||||
export RUSTFS_HEAL_QUEUE_SIZE=10000
|
export RUSTFS_HEAL_QUEUE_SIZE=10000
|
||||||
@@ -202,8 +212,6 @@ if [ -n "$1" ]; then
|
|||||||
export RUSTFS_VOLUMES="$1"
|
export RUSTFS_VOLUMES="$1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export RUSTFS_PROF_MEM_PERIODIC=true
|
|
||||||
|
|
||||||
# Enable jemalloc for memory profiling
|
# Enable jemalloc for memory profiling
|
||||||
# MALLOC_CONF parameters:
|
# MALLOC_CONF parameters:
|
||||||
# prof:true - Enable heap profiling
|
# prof:true - Enable heap profiling
|
||||||
|
|||||||
Reference in New Issue
Block a user