Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ jobs:
- name: Run Axum adapter tests
run: cargo test-axum

- name: Test native Geo without CloudFront
run: cargo test --locked -p trusted-server-adapter-axum --features geo-maxmind

- name: Test both native Geo implementations
run: cargo test --locked -p trusted-server-adapter-axum --features geo-maxmind,geo-cloudfront

- name: Test template build digest inputs (native)
run: cargo test -p trusted-server-core --test template_build_digest

Expand Down
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ FROM rust:1.98.1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288
COPY --from=node /usr/local/ /usr/local/
WORKDIR /src
COPY . .
# AWS providers and CloudFront Geo are default-off. Forward them explicitly.
# AWS providers and native Geo implementations are default-off. Forward them explicitly.
ARG NATIVE_FEATURES=""
# Optional existing build metadata input; .git never enters the build context.
ARG TRUSTED_SERVER__GIT_VERSION=""
Expand All @@ -19,8 +19,8 @@ feature_ifs=$IFS
IFS=,
for feature in $NATIVE_FEATURES; do
case "$feature" in
aws-appconfig-agent|aws-secrets-manager|aws-dynamodb-ec|geo-cloudfront) ;;
*) echo 'Only the declared native AWS store and CloudFront Geo features are supported' >&2; exit 1 ;;
aws-appconfig-agent|aws-secrets-manager|aws-dynamodb-ec|geo-cloudfront|geo-maxmind) ;;
*) echo 'Only the declared native AWS store and Geo features are supported' >&2; exit 1 ;;
esac
done
IFS=$feature_ifs
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-adapter-axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ aws-appconfig-agent = ["edgezero-adapter-axum/aws-appconfig-agent"]
aws-secrets-manager = ["edgezero-adapter-axum/aws-secrets-manager"]
aws-dynamodb-ec = ["dep:aws-config", "dep:aws-sdk-dynamodb"]
geo-cloudfront = ["edgezero-adapter-axum/geo-cloudfront"]
geo-maxmind = ["edgezero-adapter-axum/geo-maxmind"]

[dependencies]
async-trait = { workspace = true }
Expand Down
99 changes: 97 additions & 2 deletions crates/trusted-server-adapter-axum/src/native_geo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
//!
//! EdgeZero does not load a Geo environment or store binding. This optional TS
//! boot profile selects its Rust options before store preparation and listening.
//! It contains a credential removal name, never the expected origin credential.
//! Local databases use checked reader IPs and the existing trusted proxy policy.
//! Protected assertions contain a credential removal name, never its value.

use std::fmt;
use std::fs;
use std::path::PathBuf;

use edgezero_adapter_axum::geo::{NativeGeoSelection, ProtectedCloudFrontAlb};
use edgezero_adapter_axum::geo::{
MaxMindGeoConfig, NativeGeoSelection, ProtectedCloudFrontAlb, ReaderIpExpectation,
};
use serde::Deserialize;

/// Path to the separately delivered TS native Geo boot profile.
Expand Down Expand Up @@ -53,6 +57,18 @@ enum GeoSelection {
alb_peer_cidrs: Vec<String>,
origin_credential_header_name: String,
},
#[serde(rename = "maxmind")]
MaxMind {
database_path: PathBuf,
reader_ip_expectation: ReaderExpectation,
},
}

#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum ReaderExpectation {
PeerAllowed,
ForwardedRequired,
}

/// Capture the application-owned selection once before starting the runner.
Expand Down Expand Up @@ -85,11 +101,25 @@ fn parse_selection(bytes: &str) -> Result<NativeGeoSelection, NativeGeoProfileEr
} => ProtectedCloudFrontAlb::new(alb_peer_cidrs, &origin_credential_header_name)
.map(|policy| NativeGeoSelection::CloudFront { policy })
.map_err(|_| NativeGeoProfileError::InvalidPolicy),
GeoSelection::MaxMind {
database_path,
reader_ip_expectation,
} => Ok(NativeGeoSelection::MaxMind {
config: MaxMindGeoConfig::new(
database_path,
match reader_ip_expectation {
ReaderExpectation::PeerAllowed => ReaderIpExpectation::PeerAllowed,
ReaderExpectation::ForwardedRequired => ReaderIpExpectation::ForwardedRequired,
},
),
}),
}
}

#[cfg(test)]
mod tests {
use std::path::Path;

use edgezero_adapter_axum::dev_server::AxumRunOptions;
use edgezero_adapter_axum::geo::NativeGeoCapability;

Expand All @@ -102,6 +132,71 @@ alb_peer_cidrs = ["127.0.0.1/32"]
origin_credential_header_name = "x-origin-fixture-private-name"
"#;

const MAXMIND_PROFILE: &str = r#"version = 1
[geo]
provider = "maxmind"
database_path = "/geo/GeoLite2-City.mmdb"
reader_ip_expectation = "forwarded_required"
"#;

#[test]
fn maxmind_profile_selects_local_database_and_explicit_reader_expectation() {
for (name, expected) in [
("forwarded_required", ReaderIpExpectation::ForwardedRequired),
("peer_allowed", ReaderIpExpectation::PeerAllowed),
] {
let selection = parse_selection(&MAXMIND_PROFILE.replace("forwarded_required", name))
.expect("should parse local database selection");
let NativeGeoSelection::MaxMind { config } = selection else {
unreachable!("should select MaxMind");
};
assert_eq!(config.path, Path::new("/geo/GeoLite2-City.mmdb"));
assert_eq!(config.expectation, expected);
}
}

#[test]
fn maxmind_profile_rejects_missing_or_unknown_expectation_and_foreign_policy_fields() {
for bytes in [
MAXMIND_PROFILE.replace("reader_ip_expectation = \"forwarded_required\"", ""),
MAXMIND_PROFILE.replace("forwarded_required", "automatic"),
MAXMIND_PROFILE.replace("database_path = \"/geo/GeoLite2-City.mmdb\"", ""),
format!("{MAXMIND_PROFILE}alb_peer_cidrs = ['127.0.0.1/32']\n"),
format!("{MAXMIND_PROFILE}origin_credential_header_name = 'x-origin-test'\n"),
] {
assert!(matches!(
parse_selection(&bytes),
Err(NativeGeoProfileError::InvalidProfile)
));
}
}

#[test]
fn maxmind_capability_validates_compiled_provider_without_opening_database() {
let selection = parse_selection(MAXMIND_PROFILE)
.expect("should parse selection without opening a database");
let options = AxumRunOptions::new("127.0.0.1:8787".parse().expect("should parse address"))
.expect("should construct options")
.with_geo(selection);
#[cfg(all(feature = "geo-maxmind", unix))]
assert!(matches!(
options
.geo_capability()
.expect("should validate available provider"),
NativeGeoCapability::Selected {
input: edgezero_adapter_axum::geo::NativeGeoInputContract::ReaderIp {
expectation: ReaderIpExpectation::ForwardedRequired
},
..
}
));
#[cfg(not(all(feature = "geo-maxmind", unix)))]
assert!(
options.geo_capability().is_err(),
"should reject unavailable MaxMind"
);
}

#[test]
fn absent_profile_retains_off() {
temp_env::with_var_unset(PROFILE_FILE_ENV, || {
Expand Down
13 changes: 13 additions & 0 deletions crates/trusted-server-adapter-axum/tests/fixtures/geoip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Synthetic GeoIP fixtures

These original Apache-2.0 test fixtures come from the pinned EdgeZero revision
`fda87556f5aab160711eb5244c1d1065a6bb48d6`, under
`crates/edgezero-adapter-axum/tests/fixtures/geoip/`. They contain no production
GeoLite data. EdgeZero's `scripts/generate_geoip_fixtures.mjs` regenerates them.

- `city.mmdb` maps loopback and 192.0.2.1 to US/CA, and 192.0.2.2 to country-only GB.
- `country.mmdb` exercises country-only projection.
- `ipv6.mmdb` maps 2001:db8::/32 to GB/ENG.

Loopback's synthetic location makes accidental proxy-IP lookup observable.
Real operator datasets stay outside this repository.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading