Skip to content
Merged
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
738 changes: 722 additions & 16 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"landscape",
"landscape-common",
"landscape-database",
"landscape-gateway",
"landscape-macro",
"landscape-database/migration",
"landscape-dns",
Expand Down Expand Up @@ -184,3 +185,4 @@ jemalloc-ctl = "0.5"
bollard = "0.19.4"
portable-pty = { version = "0.9.0" }
dashmap = "6.1.0"
pingora = { version = "0.8.0", default-features = false, features = ["proxy", "lb", "rustls"] }
48 changes: 48 additions & 0 deletions landscape-common/src/config/gateway/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};

const DEFAULT_ENABLE: bool = false;
const DEFAULT_HTTP_PORT: u16 = 80;
const DEFAULT_HTTPS_PORT: u16 = 443;

/// TOML [gateway] section
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct LandscapeGatewayConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(required = false, nullable = false))]
pub enable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(required = false, nullable = false))]
pub http_port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(required = false, nullable = false))]
pub https_port: Option<u16>,
}

/// Parsed runtime config
#[derive(Debug, Clone)]
pub struct GatewayRuntimeConfig {
pub enable: bool,
pub http_port: u16,
pub https_port: u16,
}

impl Default for GatewayRuntimeConfig {
fn default() -> Self {
Self {
enable: DEFAULT_ENABLE,
http_port: DEFAULT_HTTP_PORT,
https_port: DEFAULT_HTTPS_PORT,
}
}
}

impl GatewayRuntimeConfig {
pub fn from_file_config(config: &LandscapeGatewayConfig) -> Self {
Self {
enable: config.enable.unwrap_or(DEFAULT_ENABLE),
http_port: config.http_port.unwrap_or(DEFAULT_HTTP_PORT),
https_port: config.https_port.unwrap_or(DEFAULT_HTTPS_PORT),
}
}
}
118 changes: 118 additions & 0 deletions landscape-common/src/config/gateway/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
pub mod config;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::database::repository::LandscapeDBStore;
use crate::store::storev2::LandscapeStore;
use crate::utils::id::gen_database_uuid;
use crate::utils::time::get_f64_timestamp;
use crate::LdApiError;

use super::ConfigId;

#[derive(thiserror::Error, Debug, LdApiError)]
#[api_error(crate_path = "crate")]
pub enum GatewayError {
#[error("Gateway rule '{0}' not found")]
#[api_error(id = "gateway.rule_not_found", status = 404)]
NotFound(ConfigId),

#[error("Host domain conflict: domain '{domain}' already used by rule '{rule_name}'")]
#[api_error(id = "gateway.host_conflict", status = 409)]
HostConflict { domain: String, rule_name: String },

#[error(
"Wildcard domain '{wildcard}' covers specific domain '{domain}' in rule '{rule_name}'"
)]
#[api_error(id = "gateway.wildcard_covers_domain", status = 409)]
WildcardCoversDomain { wildcard: String, domain: String, rule_name: String },

#[error("Path prefix '{new_prefix}' overlaps with '{existing_prefix}' in rule '{rule_name}'")]
#[api_error(id = "gateway.path_prefix_overlap", status = 409)]
PathPrefixOverlap { new_prefix: String, existing_prefix: String, rule_name: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HttpUpstreamRuleConfig {
#[serde(default = "gen_database_uuid")]
pub id: Uuid,
pub enable: bool,
pub name: String,
pub match_rule: HttpUpstreamMatchRule,
pub upstream: HttpUpstreamConfig,
#[serde(default = "get_f64_timestamp")]
pub update_at: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "t", rename_all = "snake_case")]
pub enum HttpUpstreamMatchRule {
Host { domains: Vec<String> },
PathPrefix { prefix: String },
SniProxy { domains: Vec<String> },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HttpUpstreamConfig {
pub targets: Vec<HttpUpstreamTarget>,
#[serde(default)]
pub load_balance: LoadBalanceMethod,
#[serde(default)]
pub health_check: Option<HealthCheckConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HttpUpstreamTarget {
pub address: String,
pub port: u16,
#[serde(default = "default_weight")]
pub weight: u32,
#[serde(default)]
pub tls: bool,
}

fn default_weight() -> u32 {
1
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum LoadBalanceMethod {
#[default]
RoundRobin,
Random,
Consistent,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HealthCheckConfig {
pub interval_secs: u64,
pub timeout_secs: u64,
pub unhealthy_threshold: u32,
pub healthy_threshold: u32,
}

impl LandscapeStore for HttpUpstreamRuleConfig {
fn get_store_key(&self) -> String {
self.id.to_string()
}
}

impl LandscapeDBStore<Uuid> for HttpUpstreamRuleConfig {
fn get_id(&self) -> Uuid {
self.id
}
fn get_update_at(&self) -> f64 {
self.update_at
}
fn set_update_at(&mut self, ts: f64) {
self.update_at = ts;
}
}
13 changes: 13 additions & 0 deletions landscape-common/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod dns;
pub mod firewall;
pub mod flow;
pub mod gateway;
pub mod geo;
pub mod iface;
pub mod iface_ip;
Expand All @@ -27,6 +28,8 @@ use crate::enrolled_device::EnrolledDevice;
use dns::DNSRuleConfig;
use firewall::FirewallServiceConfig;
use flow::FlowWanServiceConfig;
use gateway::config::{GatewayRuntimeConfig, LandscapeGatewayConfig};
use gateway::HttpUpstreamRuleConfig;
use iface::NetworkIfaceConfig;
use iface_ip::IfaceIpServiceConfig;
use lan_ipv6::LanIPv6ServiceConfig;
Expand Down Expand Up @@ -131,6 +134,9 @@ pub struct InitConfig {
pub cert_accounts: Vec<CertAccountConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub certs: Vec<CertConfig>,

#[serde(skip_serializing_if = "Vec::is_empty")]
pub gateway_rules: Vec<HttpUpstreamRuleConfig>,
}

/// auth realte config
Expand Down Expand Up @@ -333,6 +339,9 @@ pub struct LandscapeConfig {
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(required = true))]
pub ui: LandscapeUIConfig,
#[serde(default)]
#[cfg_attr(feature = "openapi", schema(required = true))]
pub gateway: LandscapeGatewayConfig,
}

///
Expand All @@ -349,6 +358,7 @@ pub struct RuntimeConfig {
pub metric: MetricRuntimeConfig,
pub dns: DnsRuntimeConfig,
pub ui: LandscapeUIConfig,
pub gateway: GatewayRuntimeConfig,
pub auto: bool,
}

Expand Down Expand Up @@ -504,6 +514,8 @@ impl RuntimeConfig {
.unwrap_or_else(|| "/dns-query".to_string()),
};

let gateway = GatewayRuntimeConfig::from_file_config(&config.gateway);

let runtime_config = RuntimeConfig {
home_path,
auth,
Expand All @@ -513,6 +525,7 @@ impl RuntimeConfig {
metric,
dns,
ui: config.ui.clone(),
gateway,
file_config: config,
auto: args.auto,
};
Expand Down
2 changes: 2 additions & 0 deletions landscape-database/migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod m20260222_171753_firewall_blacklist;
mod m20260226_001739_pppd_plugin;
mod m20260227_040525_lan_ipv6;
mod m20260302_060012_cert_management;
mod m20260308_101225_gateway_http_upstream;
mod tables;

pub struct Migrator;
Expand Down Expand Up @@ -78,6 +79,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260226_001739_pppd_plugin::Migration),
Box::new(m20260227_040525_lan_ipv6::Migration),
Box::new(m20260302_060012_cert_management::Migration),
Box::new(m20260308_101225_gateway_http_upstream::Migration),
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use sea_orm_migration::prelude::*;

use super::tables::gateway::GatewayHttpUpstreamRules;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(GatewayHttpUpstreamRules::Table)
.if_not_exists()
.col(
ColumnDef::new(GatewayHttpUpstreamRules::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(GatewayHttpUpstreamRules::Name).string().not_null())
.col(
ColumnDef::new(GatewayHttpUpstreamRules::Enable)
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(GatewayHttpUpstreamRules::MatchRule).json().not_null())
.col(ColumnDef::new(GatewayHttpUpstreamRules::Upstream).json().not_null())
.col(
ColumnDef::new(GatewayHttpUpstreamRules::UpdateAt)
.double()
.not_null()
.default(0.0),
)
.to_owned(),
)
.await?;
Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(GatewayHttpUpstreamRules::Table).to_owned()).await?;
Ok(())
}
}
13 changes: 13 additions & 0 deletions landscape-database/migration/src/tables/gateway.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveIden)]
pub enum GatewayHttpUpstreamRules {
#[sea_orm(iden = "gateway_http_upstream_rules")]
Table,
Id,
Name,
Enable,
MatchRule,
Upstream,
UpdateAt,
}
1 change: 1 addition & 0 deletions landscape-database/migration/src/tables/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ pub mod route;
pub mod cert;
pub mod enrolled_device;
pub mod firewall_blacklist;
pub mod gateway;
pub mod lan_ipv6;
69 changes: 69 additions & 0 deletions landscape-database/src/gateway/entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::repository::UpdateActiveModel;
use landscape_common::config::gateway::HttpUpstreamRuleConfig;
use sea_orm::{entity::prelude::*, ActiveValue::Set};
use serde::{Deserialize, Serialize};

use crate::{DBId, DBJson, DBTimestamp};

pub type GatewayHttpUpstreamModel = Model;
pub type GatewayHttpUpstreamEntity = Entity;
pub type GatewayHttpUpstreamActiveModel = ActiveModel;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "gateway_http_upstream_rules")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: DBId,
pub name: String,
pub enable: bool,
pub match_rule: DBJson,
pub upstream: DBJson,
pub update_at: DBTimestamp,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if insert && self.id.is_not_set() {
self.id = Set(Uuid::new_v4());
}
Ok(self)
}
}

impl From<Model> for HttpUpstreamRuleConfig {
fn from(entity: Model) -> Self {
HttpUpstreamRuleConfig {
id: entity.id,
name: entity.name,
enable: entity.enable,
match_rule: serde_json::from_value(entity.match_rule).unwrap(),
upstream: serde_json::from_value(entity.upstream).unwrap(),
update_at: entity.update_at,
}
}
}

impl Into<ActiveModel> for HttpUpstreamRuleConfig {
fn into(self) -> ActiveModel {
let mut active = ActiveModel { id: Set(self.id), ..Default::default() };
self.update(&mut active);
active
}
}

impl UpdateActiveModel<ActiveModel> for HttpUpstreamRuleConfig {
fn update(self, active: &mut ActiveModel) {
active.name = Set(self.name);
active.enable = Set(self.enable);
active.match_rule = Set(serde_json::to_value(&self.match_rule).unwrap());
active.upstream = Set(serde_json::to_value(&self.upstream).unwrap());
active.update_at = Set(self.update_at);
}
}
2 changes: 2 additions & 0 deletions landscape-database/src/gateway/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod entity;
pub mod repository;
Loading