Skip to content

Commit 9181009

Browse files
committed
feat: introduce gateway reverse proxy with SNI support, configuration, database, and UI management.
1 parent fb417e2 commit 9181009

File tree

42 files changed

+2896
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2896
-7
lines changed

Cargo.lock

Lines changed: 635 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ members = [
88
"landscape",
99
"landscape-common",
1010
"landscape-database",
11+
"landscape-gateway",
1112
"landscape-macro",
1213
"landscape-database/migration",
1314
"landscape-dns",
@@ -184,3 +185,4 @@ jemalloc-ctl = "0.5"
184185
bollard = "0.19.4"
185186
portable-pty = { version = "0.9.0" }
186187
dashmap = "6.1.0"
188+
pingora = { version = "0.8.0", default-features = false, features = ["proxy", "lb"] }
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
const DEFAULT_ENABLE: bool = false;
4+
const DEFAULT_HTTP_PORT: u16 = 80;
5+
const DEFAULT_HTTPS_PORT: u16 = 443;
6+
7+
/// TOML [gateway] section
8+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
9+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
10+
pub struct LandscapeGatewayConfig {
11+
#[serde(default, skip_serializing_if = "Option::is_none")]
12+
#[cfg_attr(feature = "openapi", schema(required = false, nullable = false))]
13+
pub enable: Option<bool>,
14+
#[serde(default, skip_serializing_if = "Option::is_none")]
15+
#[cfg_attr(feature = "openapi", schema(required = false, nullable = false))]
16+
pub http_port: Option<u16>,
17+
#[serde(default, skip_serializing_if = "Option::is_none")]
18+
#[cfg_attr(feature = "openapi", schema(required = false, nullable = false))]
19+
pub https_port: Option<u16>,
20+
}
21+
22+
/// Parsed runtime config
23+
#[derive(Debug, Clone)]
24+
pub struct GatewayRuntimeConfig {
25+
pub enable: bool,
26+
pub http_port: u16,
27+
pub https_port: u16,
28+
}
29+
30+
impl Default for GatewayRuntimeConfig {
31+
fn default() -> Self {
32+
Self {
33+
enable: DEFAULT_ENABLE,
34+
http_port: DEFAULT_HTTP_PORT,
35+
https_port: DEFAULT_HTTPS_PORT,
36+
}
37+
}
38+
}
39+
40+
impl GatewayRuntimeConfig {
41+
pub fn from_file_config(config: &LandscapeGatewayConfig) -> Self {
42+
Self {
43+
enable: config.enable.unwrap_or(DEFAULT_ENABLE),
44+
http_port: config.http_port.unwrap_or(DEFAULT_HTTP_PORT),
45+
https_port: config.https_port.unwrap_or(DEFAULT_HTTPS_PORT),
46+
}
47+
}
48+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
pub mod config;
2+
3+
use serde::{Deserialize, Serialize};
4+
use uuid::Uuid;
5+
6+
use crate::database::repository::LandscapeDBStore;
7+
use crate::store::storev2::LandscapeStore;
8+
use crate::utils::id::gen_database_uuid;
9+
use crate::utils::time::get_f64_timestamp;
10+
use crate::LdApiError;
11+
12+
use super::ConfigId;
13+
14+
#[derive(thiserror::Error, Debug, LdApiError)]
15+
#[api_error(crate_path = "crate")]
16+
pub enum GatewayError {
17+
#[error("Gateway rule '{0}' not found")]
18+
#[api_error(id = "gateway.rule_not_found", status = 404)]
19+
NotFound(ConfigId),
20+
21+
#[error("Host domain conflict: domain '{domain}' already used by rule '{rule_name}'")]
22+
#[api_error(id = "gateway.host_conflict", status = 409)]
23+
HostConflict { domain: String, rule_name: String },
24+
25+
#[error(
26+
"Wildcard domain '{wildcard}' covers specific domain '{domain}' in rule '{rule_name}'"
27+
)]
28+
#[api_error(id = "gateway.wildcard_covers_domain", status = 409)]
29+
WildcardCoversDomain { wildcard: String, domain: String, rule_name: String },
30+
31+
#[error("Path prefix '{new_prefix}' overlaps with '{existing_prefix}' in rule '{rule_name}'")]
32+
#[api_error(id = "gateway.path_prefix_overlap", status = 409)]
33+
PathPrefixOverlap { new_prefix: String, existing_prefix: String, rule_name: String },
34+
}
35+
36+
#[derive(Debug, Clone, Serialize, Deserialize)]
37+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
38+
pub struct HttpUpstreamRuleConfig {
39+
#[serde(default = "gen_database_uuid")]
40+
pub id: Uuid,
41+
pub enable: bool,
42+
pub name: String,
43+
pub match_rule: HttpUpstreamMatchRule,
44+
pub upstream: HttpUpstreamConfig,
45+
#[serde(default = "get_f64_timestamp")]
46+
pub update_at: f64,
47+
}
48+
49+
#[derive(Debug, Clone, Serialize, Deserialize)]
50+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51+
#[serde(tag = "t", rename_all = "snake_case")]
52+
pub enum HttpUpstreamMatchRule {
53+
Host { domains: Vec<String> },
54+
PathPrefix { prefix: String },
55+
SniProxy { domains: Vec<String> },
56+
}
57+
58+
#[derive(Debug, Clone, Serialize, Deserialize)]
59+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
60+
pub struct HttpUpstreamConfig {
61+
pub targets: Vec<HttpUpstreamTarget>,
62+
#[serde(default)]
63+
pub load_balance: LoadBalanceMethod,
64+
#[serde(default)]
65+
pub health_check: Option<HealthCheckConfig>,
66+
}
67+
68+
#[derive(Debug, Clone, Serialize, Deserialize)]
69+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
70+
pub struct HttpUpstreamTarget {
71+
pub address: String,
72+
pub port: u16,
73+
#[serde(default = "default_weight")]
74+
pub weight: u32,
75+
#[serde(default)]
76+
pub tls: bool,
77+
}
78+
79+
fn default_weight() -> u32 {
80+
1
81+
}
82+
83+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
84+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
85+
#[serde(rename_all = "snake_case")]
86+
pub enum LoadBalanceMethod {
87+
#[default]
88+
RoundRobin,
89+
Random,
90+
Consistent,
91+
}
92+
93+
#[derive(Debug, Clone, Serialize, Deserialize)]
94+
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
95+
pub struct HealthCheckConfig {
96+
pub interval_secs: u64,
97+
pub timeout_secs: u64,
98+
pub unhealthy_threshold: u32,
99+
pub healthy_threshold: u32,
100+
}
101+
102+
impl LandscapeStore for HttpUpstreamRuleConfig {
103+
fn get_store_key(&self) -> String {
104+
self.id.to_string()
105+
}
106+
}
107+
108+
impl LandscapeDBStore<Uuid> for HttpUpstreamRuleConfig {
109+
fn get_id(&self) -> Uuid {
110+
self.id
111+
}
112+
fn get_update_at(&self) -> f64 {
113+
self.update_at
114+
}
115+
fn set_update_at(&mut self, ts: f64) {
116+
self.update_at = ts;
117+
}
118+
}

landscape-common/src/config/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod dns;
22
pub mod firewall;
33
pub mod flow;
4+
pub mod gateway;
45
pub mod geo;
56
pub mod iface;
67
pub mod iface_ip;
@@ -27,6 +28,8 @@ use crate::enrolled_device::EnrolledDevice;
2728
use dns::DNSRuleConfig;
2829
use firewall::FirewallServiceConfig;
2930
use flow::FlowWanServiceConfig;
31+
use gateway::config::{GatewayRuntimeConfig, LandscapeGatewayConfig};
32+
use gateway::HttpUpstreamRuleConfig;
3033
use iface::NetworkIfaceConfig;
3134
use iface_ip::IfaceIpServiceConfig;
3235
use lan_ipv6::LanIPv6ServiceConfig;
@@ -131,6 +134,9 @@ pub struct InitConfig {
131134
pub cert_accounts: Vec<CertAccountConfig>,
132135
#[serde(skip_serializing_if = "Vec::is_empty")]
133136
pub certs: Vec<CertConfig>,
137+
138+
#[serde(skip_serializing_if = "Vec::is_empty")]
139+
pub gateway_rules: Vec<HttpUpstreamRuleConfig>,
134140
}
135141

136142
/// auth realte config
@@ -333,6 +339,9 @@ pub struct LandscapeConfig {
333339
#[serde(default)]
334340
#[cfg_attr(feature = "openapi", schema(required = true))]
335341
pub ui: LandscapeUIConfig,
342+
#[serde(default)]
343+
#[cfg_attr(feature = "openapi", schema(required = true))]
344+
pub gateway: LandscapeGatewayConfig,
336345
}
337346

338347
///
@@ -349,6 +358,7 @@ pub struct RuntimeConfig {
349358
pub metric: MetricRuntimeConfig,
350359
pub dns: DnsRuntimeConfig,
351360
pub ui: LandscapeUIConfig,
361+
pub gateway: GatewayRuntimeConfig,
352362
pub auto: bool,
353363
}
354364

@@ -504,6 +514,8 @@ impl RuntimeConfig {
504514
.unwrap_or_else(|| "/dns-query".to_string()),
505515
};
506516

517+
let gateway = GatewayRuntimeConfig::from_file_config(&config.gateway);
518+
507519
let runtime_config = RuntimeConfig {
508520
home_path,
509521
auth,
@@ -513,6 +525,7 @@ impl RuntimeConfig {
513525
metric,
514526
dns,
515527
ui: config.ui.clone(),
528+
gateway,
516529
file_config: config,
517530
auto: args.auto,
518531
};

landscape-database/migration/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod m20260222_171753_firewall_blacklist;
3535
mod m20260226_001739_pppd_plugin;
3636
mod m20260227_040525_lan_ipv6;
3737
mod m20260302_060012_cert_management;
38+
mod m20260308_101225_gateway_http_upstream;
3839
mod tables;
3940

4041
pub struct Migrator;
@@ -78,6 +79,7 @@ impl MigratorTrait for Migrator {
7879
Box::new(m20260226_001739_pppd_plugin::Migration),
7980
Box::new(m20260227_040525_lan_ipv6::Migration),
8081
Box::new(m20260302_060012_cert_management::Migration),
82+
Box::new(m20260308_101225_gateway_http_upstream::Migration),
8183
]
8284
}
8385
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
use super::tables::gateway::GatewayHttpUpstreamRules;
4+
5+
#[derive(DeriveMigrationName)]
6+
pub struct Migration;
7+
8+
#[async_trait::async_trait]
9+
impl MigrationTrait for Migration {
10+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
11+
manager
12+
.create_table(
13+
Table::create()
14+
.table(GatewayHttpUpstreamRules::Table)
15+
.if_not_exists()
16+
.col(
17+
ColumnDef::new(GatewayHttpUpstreamRules::Id)
18+
.uuid()
19+
.not_null()
20+
.primary_key(),
21+
)
22+
.col(ColumnDef::new(GatewayHttpUpstreamRules::Name).string().not_null())
23+
.col(
24+
ColumnDef::new(GatewayHttpUpstreamRules::Enable)
25+
.boolean()
26+
.not_null()
27+
.default(true),
28+
)
29+
.col(ColumnDef::new(GatewayHttpUpstreamRules::MatchRule).json().not_null())
30+
.col(ColumnDef::new(GatewayHttpUpstreamRules::Upstream).json().not_null())
31+
.col(
32+
ColumnDef::new(GatewayHttpUpstreamRules::UpdateAt)
33+
.double()
34+
.not_null()
35+
.default(0.0),
36+
)
37+
.to_owned(),
38+
)
39+
.await?;
40+
Ok(())
41+
}
42+
43+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
44+
manager.drop_table(Table::drop().table(GatewayHttpUpstreamRules::Table).to_owned()).await?;
45+
Ok(())
46+
}
47+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveIden)]
4+
pub enum GatewayHttpUpstreamRules {
5+
#[sea_orm(iden = "gateway_http_upstream_rules")]
6+
Table,
7+
Id,
8+
Name,
9+
Enable,
10+
MatchRule,
11+
Upstream,
12+
UpdateAt,
13+
}

landscape-database/migration/src/tables/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ pub mod route;
2222
pub mod cert;
2323
pub mod enrolled_device;
2424
pub mod firewall_blacklist;
25+
pub mod gateway;
2526
pub mod lan_ipv6;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use crate::repository::UpdateActiveModel;
2+
use landscape_common::config::gateway::HttpUpstreamRuleConfig;
3+
use sea_orm::{entity::prelude::*, ActiveValue::Set};
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::{DBId, DBJson, DBTimestamp};
7+
8+
pub type GatewayHttpUpstreamModel = Model;
9+
pub type GatewayHttpUpstreamEntity = Entity;
10+
pub type GatewayHttpUpstreamActiveModel = ActiveModel;
11+
12+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
13+
#[sea_orm(table_name = "gateway_http_upstream_rules")]
14+
pub struct Model {
15+
#[sea_orm(primary_key, auto_increment = false)]
16+
pub id: DBId,
17+
pub name: String,
18+
pub enable: bool,
19+
pub match_rule: DBJson,
20+
pub upstream: DBJson,
21+
pub update_at: DBTimestamp,
22+
}
23+
24+
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
25+
pub enum Relation {}
26+
27+
#[async_trait::async_trait]
28+
impl ActiveModelBehavior for ActiveModel {
29+
async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
30+
where
31+
C: ConnectionTrait,
32+
{
33+
if insert && self.id.is_not_set() {
34+
self.id = Set(Uuid::new_v4());
35+
}
36+
Ok(self)
37+
}
38+
}
39+
40+
impl From<Model> for HttpUpstreamRuleConfig {
41+
fn from(entity: Model) -> Self {
42+
HttpUpstreamRuleConfig {
43+
id: entity.id,
44+
name: entity.name,
45+
enable: entity.enable,
46+
match_rule: serde_json::from_value(entity.match_rule).unwrap(),
47+
upstream: serde_json::from_value(entity.upstream).unwrap(),
48+
update_at: entity.update_at,
49+
}
50+
}
51+
}
52+
53+
impl Into<ActiveModel> for HttpUpstreamRuleConfig {
54+
fn into(self) -> ActiveModel {
55+
let mut active = ActiveModel { id: Set(self.id), ..Default::default() };
56+
self.update(&mut active);
57+
active
58+
}
59+
}
60+
61+
impl UpdateActiveModel<ActiveModel> for HttpUpstreamRuleConfig {
62+
fn update(self, active: &mut ActiveModel) {
63+
active.name = Set(self.name);
64+
active.enable = Set(self.enable);
65+
active.match_rule = Set(serde_json::to_value(&self.match_rule).unwrap());
66+
active.upstream = Set(serde_json::to_value(&self.upstream).unwrap());
67+
active.update_at = Set(self.update_at);
68+
}
69+
}

0 commit comments

Comments
 (0)