mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-07 17:18:21 +00:00
Merge branch 'oxidation'
This commit is contained in:
commit
797c6403ba
21 changed files with 2583 additions and 7 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,9 +1,4 @@
|
||||||
config.json
|
config.json
|
||||||
pending.json
|
pending.json
|
||||||
nftables.conf
|
/target
|
||||||
interfaces.conf
|
|
||||||
go.work
|
|
||||||
nfsense
|
|
||||||
nfsense.exe
|
|
||||||
out/*
|
|
||||||
out
|
|
||||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -3,5 +3,9 @@
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer",
|
||||||
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
||||||
}
|
}
|
1638
Cargo.lock
generated
Normal file
1638
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "nfsense"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.74"
|
||||||
|
axum = "0.6.20"
|
||||||
|
custom_error = "1.9.2"
|
||||||
|
ipnet = { version = "2.8.0", features = ["serde"] }
|
||||||
|
jsonrpsee = { version = "0.20.3", features = ["server"] }
|
||||||
|
macaddr = { version = "1.0.1", features = ["serde"] }
|
||||||
|
pwhash = "1.0.0"
|
||||||
|
serde = { version = "1.0.189", features = ["derive"] }
|
||||||
|
serde_json = "1.0.107"
|
||||||
|
thiserror = "1.0.50"
|
||||||
|
tokio = { version = "1.33.0", features = ["full"] }
|
||||||
|
tower-cookies = "0.9.0"
|
||||||
|
tower-http = "0.4.4"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
uuid = { version = "1.5.0", features = ["v4"] }
|
||||||
|
validator = { version = "0.15", features = ["derive"] }
|
48
src/api/mod.rs
Normal file
48
src/api/mod.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
mod network;
|
||||||
|
mod system;
|
||||||
|
|
||||||
|
use crate::state::RpcState;
|
||||||
|
use jsonrpsee::{
|
||||||
|
types::{error::ErrorCode, ErrorObject},
|
||||||
|
RpcModule,
|
||||||
|
};
|
||||||
|
|
||||||
|
use custom_error::custom_error;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
custom_error! { pub ApiError
|
||||||
|
InvalidParams = "Invalid Parameters",
|
||||||
|
Leet = "1337",
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<ErrorObject<'static>> for ApiError {
|
||||||
|
fn into(self) -> ErrorObject<'static> {
|
||||||
|
match self {
|
||||||
|
Self::InvalidParams => ErrorCode::InvalidParams,
|
||||||
|
Self::Leet => ErrorCode::ServerError(1337),
|
||||||
|
_ => ErrorCode::InternalError,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_rpc_module(state: RpcState) -> RpcModule<RpcState> {
|
||||||
|
let mut module = RpcModule::new(state);
|
||||||
|
|
||||||
|
module
|
||||||
|
.register_method("ping", |_, _| {
|
||||||
|
info!("ping called");
|
||||||
|
"pong"
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
module
|
||||||
|
.register_method("system.get_users", system::get_users)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
module
|
||||||
|
.register_method("network.get_static_routes", network::get_static_routes)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
module
|
||||||
|
}
|
13
src/api/network.rs
Normal file
13
src/api/network.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use jsonrpsee::types::Params;
|
||||||
|
|
||||||
|
use crate::{definitions::network::StaticRoute, state::RpcState};
|
||||||
|
|
||||||
|
use super::ApiError;
|
||||||
|
|
||||||
|
pub fn get_static_routes(_: Params, state: &RpcState) -> Result<Vec<StaticRoute>, ApiError> {
|
||||||
|
Ok(state
|
||||||
|
.config_manager
|
||||||
|
.get_pending_config()
|
||||||
|
.network
|
||||||
|
.static_routes)
|
||||||
|
}
|
10
src/api/system.rs
Normal file
10
src/api/system.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{definitions::system::User, state::RpcState};
|
||||||
|
use jsonrpsee::types::Params;
|
||||||
|
|
||||||
|
use super::ApiError;
|
||||||
|
|
||||||
|
pub fn get_users(_: Params, state: &RpcState) -> Result<HashMap<String, User>, ApiError> {
|
||||||
|
Ok(state.config_manager.get_pending_config().system.users)
|
||||||
|
}
|
127
src/config_manager.rs
Normal file
127
src/config_manager.rs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use super::definitions::config::Config;
|
||||||
|
use std::fs;
|
||||||
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
use std::{io, result::Result};
|
||||||
|
|
||||||
|
use custom_error::custom_error;
|
||||||
|
|
||||||
|
use pwhash::sha512_crypt;
|
||||||
|
|
||||||
|
custom_error! { pub ConfigError
|
||||||
|
IoError{source: io::Error} = "io error",
|
||||||
|
SerdeError{source: serde_json::Error} = "serde json error",
|
||||||
|
ValidatonError{source: validator::ValidationErrors} = "validation failed",
|
||||||
|
HashError{source: pwhash::error::Error} = "password hash generation",
|
||||||
|
UnsupportedVersionError = "unsupported config version",
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CURRENT_CONFIG_PATH: &str = "config.json";
|
||||||
|
pub const PENDING_CONFIG_PATH: &str = "pending.json";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ConfigManager {
|
||||||
|
shared_data: Arc<Mutex<SharedData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SharedData {
|
||||||
|
current_config: Config,
|
||||||
|
pending_config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note, using unwarp on a mutex lock is ok since that only errors with mutex poisoning
|
||||||
|
|
||||||
|
impl ConfigManager {
|
||||||
|
pub fn new() -> Result<Self, ConfigError> {
|
||||||
|
Ok(Self {
|
||||||
|
shared_data: Arc::new(Mutex::new(SharedData {
|
||||||
|
current_config: read_file_to_config(CURRENT_CONFIG_PATH)?,
|
||||||
|
// TODO Dont Fail if pending config is missing, use current instead
|
||||||
|
pending_config: read_file_to_config(PENDING_CONFIG_PATH)?,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_config(&self) -> Config {
|
||||||
|
self.shared_data.lock().unwrap().current_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pending_config(&self) -> Config {
|
||||||
|
self.shared_data.lock().unwrap().pending_config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_pending_changes(&mut self) -> Result<(), ConfigError> {
|
||||||
|
let mut data = self.shared_data.lock().unwrap();
|
||||||
|
// TODO run Apply functions
|
||||||
|
// TODO Revert on Apply Failure and Return
|
||||||
|
write_config_to_file(CURRENT_CONFIG_PATH, data.pending_config.clone())?;
|
||||||
|
// TODO revert if config save fails
|
||||||
|
// TODO Remove Pending Config File
|
||||||
|
data.current_config = data.pending_config.clone();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discard_pending_changes(&mut self) -> Result<(), ConfigError> {
|
||||||
|
let mut data = self.shared_data.lock().unwrap();
|
||||||
|
// TODO Remove Pending Config File
|
||||||
|
|
||||||
|
data.pending_config = data.current_config.clone();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_transaction(&mut self) -> Result<ConfigTransaction, ConfigError> {
|
||||||
|
let data = self.shared_data.lock().unwrap();
|
||||||
|
|
||||||
|
Ok(ConfigTransaction {
|
||||||
|
finished: false,
|
||||||
|
changes: data.pending_config.clone(),
|
||||||
|
shared_data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConfigTransaction<'a> {
|
||||||
|
finished: bool,
|
||||||
|
shared_data: MutexGuard<'a, SharedData>,
|
||||||
|
pub changes: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConfigTransaction<'a> {
|
||||||
|
pub fn commit(mut self) -> Result<(), ConfigError> {
|
||||||
|
let ch = self.changes.clone();
|
||||||
|
ch.validate()?;
|
||||||
|
self.shared_data.pending_config = ch.clone();
|
||||||
|
write_config_to_file(PENDING_CONFIG_PATH, ch.clone())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_file_to_config(path: &str) -> Result<Config, ConfigError> {
|
||||||
|
let data = fs::read_to_string(path)?;
|
||||||
|
let conf: Config = serde_json::from_str(&data)?;
|
||||||
|
if conf.config_version != 1 {
|
||||||
|
return Err(ConfigError::UnsupportedVersionError);
|
||||||
|
}
|
||||||
|
Ok(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_config_to_file(path: &str, conf: Config) -> Result<(), ConfigError> {
|
||||||
|
let data: String = serde_json::to_string_pretty(&conf)?;
|
||||||
|
fs::write(path, data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_default_config(path: &str) -> Result<(), ConfigError> {
|
||||||
|
let mut conf = Config::default();
|
||||||
|
let hash = sha512_crypt::hash("nfsense")?;
|
||||||
|
conf.config_version = 1;
|
||||||
|
conf.system.users.insert(
|
||||||
|
"admin".to_string(),
|
||||||
|
crate::definitions::system::User {
|
||||||
|
comment: "Default Admin".to_string(),
|
||||||
|
hash: hash,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
write_config_to_file(path, conf)
|
||||||
|
}
|
20
src/definitions/config.rs
Normal file
20
src/definitions/config.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use super::firewall;
|
||||||
|
use super::network;
|
||||||
|
use super::object;
|
||||||
|
use super::service;
|
||||||
|
use super::system;
|
||||||
|
use super::vpn;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub config_version: u64,
|
||||||
|
pub network: network::Network,
|
||||||
|
pub object: object::Object,
|
||||||
|
pub system: system::System,
|
||||||
|
pub service: service::Service,
|
||||||
|
pub vpn: vpn::VPN,
|
||||||
|
pub firewall: firewall::Firewall,
|
||||||
|
}
|
61
src/definitions/firewall.rs
Normal file
61
src/definitions/firewall.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct Firewall {
|
||||||
|
pub forward_rules: Vec<ForwardRule>,
|
||||||
|
pub destination_nat_rules: Vec<DestinationNATRule>,
|
||||||
|
pub source_nat_rules: Vec<SourceNATRule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct ForwardRule {
|
||||||
|
pub name: String,
|
||||||
|
pub services: Vec<String>,
|
||||||
|
pub source_addresses: Vec<String>,
|
||||||
|
pub destination_addresses: Vec<String>,
|
||||||
|
pub comment: String,
|
||||||
|
pub counter: bool,
|
||||||
|
pub verdict: Verdict,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct DestinationNATRule {
|
||||||
|
pub name: String,
|
||||||
|
pub services: Vec<String>,
|
||||||
|
pub source_addresses: Vec<String>,
|
||||||
|
pub destination_addresses: Vec<String>,
|
||||||
|
pub comment: String,
|
||||||
|
pub counter: bool,
|
||||||
|
pub dnat_address: String,
|
||||||
|
pub dnat_service: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct SourceNATRule {
|
||||||
|
pub name: String,
|
||||||
|
pub services: Vec<String>,
|
||||||
|
pub source_addresses: Vec<String>,
|
||||||
|
pub destination_addresses: Vec<String>,
|
||||||
|
pub comment: String,
|
||||||
|
pub counter: bool,
|
||||||
|
pub snat_type: SNATType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Verdict {
|
||||||
|
Accept,
|
||||||
|
Drop,
|
||||||
|
Continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SNATType {
|
||||||
|
SNAT {
|
||||||
|
snat_address: String,
|
||||||
|
snat_service: String,
|
||||||
|
},
|
||||||
|
Masquerade,
|
||||||
|
}
|
7
src/definitions/mod.rs
Normal file
7
src/definitions/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
pub mod config;
|
||||||
|
pub mod firewall;
|
||||||
|
pub mod network;
|
||||||
|
pub mod object;
|
||||||
|
pub mod service;
|
||||||
|
pub mod system;
|
||||||
|
pub mod vpn;
|
44
src/definitions/network.rs
Normal file
44
src/definitions/network.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use ipnet::IpNet;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{collections::HashMap, net::IpAddr};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct Network {
|
||||||
|
pub interfaces: HashMap<String, NetworkInterface>,
|
||||||
|
pub static_routes: Vec<StaticRoute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct NetworkInterface {
|
||||||
|
pub alias: String,
|
||||||
|
pub comment: String,
|
||||||
|
pub interface_type: NetworkInterfaceType,
|
||||||
|
pub addressing_mode: AddressingMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum NetworkInterfaceType {
|
||||||
|
Hardware { device: String },
|
||||||
|
Vlan { id: i32, parent: String },
|
||||||
|
Bond { members: Vec<String> },
|
||||||
|
Bridge { members: Vec<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AddressingMode {
|
||||||
|
None,
|
||||||
|
Static { address: String },
|
||||||
|
DHCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct StaticRoute {
|
||||||
|
pub name: String,
|
||||||
|
pub interface: String,
|
||||||
|
pub gateway: IpAddr,
|
||||||
|
pub destination: IpNet,
|
||||||
|
pub metric: u64,
|
||||||
|
}
|
54
src/definitions/object.rs
Normal file
54
src/definitions/object.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use ipnet::IpNet;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{collections::HashMap, net::IpAddr};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct Object {
|
||||||
|
pub addresses: HashMap<String, Address>,
|
||||||
|
pub services: HashMap<String, Service>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct Address {
|
||||||
|
pub address_type: AddressType,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AddressType {
|
||||||
|
Host { host: String },
|
||||||
|
Range { range: IpAddr },
|
||||||
|
Network { network: IpNet },
|
||||||
|
Group { children: Vec<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct Service {
|
||||||
|
pub service_type: ServiceType,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ServiceType {
|
||||||
|
TCP {
|
||||||
|
source_port: u64,
|
||||||
|
source_port_end: Option<u64>,
|
||||||
|
destination_port: u64,
|
||||||
|
destination_port_end: Option<u64>,
|
||||||
|
},
|
||||||
|
UDP {
|
||||||
|
source_port: u64,
|
||||||
|
source_port_end: Option<u64>,
|
||||||
|
destination_port: u64,
|
||||||
|
destination_port_end: Option<u64>,
|
||||||
|
},
|
||||||
|
ICMP {
|
||||||
|
code: u8,
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
children: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
67
src/definitions/service.rs
Normal file
67
src/definitions/service.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use core::time;
|
||||||
|
use macaddr::MacAddr8;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct Service {
|
||||||
|
pub dhcp_servers: Vec<DHCPServer>,
|
||||||
|
pub dns_servers: Vec<DNSServer>,
|
||||||
|
pub ntp_servers: Vec<NTPServer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct DHCPServer {
|
||||||
|
pub interface: String,
|
||||||
|
pub pool: Vec<String>,
|
||||||
|
pub lease_time: time::Duration,
|
||||||
|
pub gateway_mode: GatewayMode,
|
||||||
|
pub dns_server_mode: DNSServerMode,
|
||||||
|
pub ntp_server_mode: NTPServerMode,
|
||||||
|
pub reservations: Vec<Reservation>,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct DNSServer {
|
||||||
|
pub interface: String,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct NTPServer {
|
||||||
|
pub interface: String,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum GatewayMode {
|
||||||
|
None,
|
||||||
|
Interface,
|
||||||
|
Specify { gateway: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum DNSServerMode {
|
||||||
|
None,
|
||||||
|
Interface,
|
||||||
|
Specify { dns_servers: Vec<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum NTPServerMode {
|
||||||
|
None,
|
||||||
|
Interface,
|
||||||
|
Specify { ntp_servers: Vec<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Reservation {
|
||||||
|
pub ip_address: IpAddr,
|
||||||
|
pub hardware_address: MacAddr8,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
14
src/definitions/system.rs
Normal file
14
src/definitions/system.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct System {
|
||||||
|
pub users: HashMap<String, User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
pub comment: String,
|
||||||
|
pub hash: String,
|
||||||
|
}
|
33
src/definitions/vpn.rs
Normal file
33
src/definitions/vpn.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct VPN {
|
||||||
|
pub wireguard: Wireguard,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
|
||||||
|
pub struct Wireguard {
|
||||||
|
pub interfaces: HashMap<String, WireguardInterface>,
|
||||||
|
pub peers: HashMap<String, WireguardPeer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct WireguardInterface {
|
||||||
|
pub public_key: String,
|
||||||
|
pub private_key: String,
|
||||||
|
pub listen_port: u64,
|
||||||
|
pub peers: Vec<String>,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
|
||||||
|
pub struct WireguardPeer {
|
||||||
|
pub public_key: String,
|
||||||
|
pub preshared_key: Option<String>,
|
||||||
|
pub allowed_ips: Vec<String>,
|
||||||
|
pub endpoint: Option<String>,
|
||||||
|
pub persistent_keepalive: Option<u64>,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
143
src/main.rs
Normal file
143
src/main.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::RpcState;
|
||||||
|
use axum::{middleware, Router};
|
||||||
|
use config_manager::ConfigManager;
|
||||||
|
use state::AppState;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use tower_cookies::CookieManagerLayer;
|
||||||
|
use tracing::info;
|
||||||
|
use tracing_subscriber;
|
||||||
|
use web::auth::SessionState;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod config_manager;
|
||||||
|
mod definitions;
|
||||||
|
mod state;
|
||||||
|
mod web;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
info!("Starting...");
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
if args.len() > 1 && args[1] == "generate-default" {
|
||||||
|
info!("Generating default config...");
|
||||||
|
config_manager::generate_default_config(config_manager::CURRENT_CONFIG_PATH).unwrap();
|
||||||
|
fs::copy(
|
||||||
|
config_manager::CURRENT_CONFIG_PATH,
|
||||||
|
config_manager::PENDING_CONFIG_PATH,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
info!("Done! Exiting...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Check Config Manager Setup Error
|
||||||
|
let config_manager = ConfigManager::new().unwrap();
|
||||||
|
let session_state = SessionState {
|
||||||
|
sessions: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_state = AppState {
|
||||||
|
config_manager: config_manager.clone(),
|
||||||
|
session_state: session_state.clone(),
|
||||||
|
rpc_module: api::new_rpc_module(RpcState {
|
||||||
|
config_manager,
|
||||||
|
session_state,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: The Router Works Bottom Up, So the auth middleware will only applies to everything above it.
|
||||||
|
let main_router = Router::new()
|
||||||
|
.merge(web::rpc::routes())
|
||||||
|
.layer(middleware::from_fn_with_state(
|
||||||
|
app_state.clone(),
|
||||||
|
web::auth::mw_auth,
|
||||||
|
))
|
||||||
|
.merge(web::auth::routes())
|
||||||
|
.with_state(app_state)
|
||||||
|
.layer(CookieManagerLayer::new());
|
||||||
|
// .fallback_service(service)
|
||||||
|
|
||||||
|
info!("Server started successfully");
|
||||||
|
axum::Server::bind(&"[::]:8080".parse().unwrap())
|
||||||
|
.serve(main_router.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
/*
|
||||||
|
let mut tx = config_manager.start_transaction().unwrap();
|
||||||
|
|
||||||
|
tx.changes
|
||||||
|
.firewall
|
||||||
|
.forward_rules
|
||||||
|
.push(definitions::firewall::ForwardRule {
|
||||||
|
name: "name".to_string(),
|
||||||
|
comment: "test".to_string(),
|
||||||
|
counter: true,
|
||||||
|
verdict: definitions::firewall::Verdict::Accept,
|
||||||
|
services: Vec::new(),
|
||||||
|
destination_addresses: Vec::new(),
|
||||||
|
source_addresses: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.commit().unwrap();
|
||||||
|
|
||||||
|
config_manager.apply_pending_changes().unwrap();
|
||||||
|
|
||||||
|
let mut tx2 = config_manager.start_transaction().unwrap();
|
||||||
|
|
||||||
|
tx2.changes.network.interfaces.insert(
|
||||||
|
"inter1".to_string(),
|
||||||
|
definitions::network::NetworkInterface {
|
||||||
|
alias: "test".to_owned(),
|
||||||
|
comment: "test comment".to_owned(),
|
||||||
|
interface_type: definitions::network::NetworkInterfaceType::Hardware {
|
||||||
|
device: "eth0".to_owned(),
|
||||||
|
},
|
||||||
|
addressing_mode: definitions::network::AddressingMode::None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx2.changes.network.interfaces.insert(
|
||||||
|
"inter2".to_string(),
|
||||||
|
definitions::network::NetworkInterface {
|
||||||
|
alias: "test2".to_owned(),
|
||||||
|
comment: "test comment".to_owned(),
|
||||||
|
interface_type: definitions::network::NetworkInterfaceType::Hardware {
|
||||||
|
device: "eth0".to_owned(),
|
||||||
|
},
|
||||||
|
addressing_mode: definitions::network::AddressingMode::Static {
|
||||||
|
address: "192.168.1.1".to_owned(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx2.changes
|
||||||
|
.network
|
||||||
|
.static_routes
|
||||||
|
.push(definitions::network::StaticRoute {
|
||||||
|
name: "test1".to_string(),
|
||||||
|
interface: "eth0".to_string(),
|
||||||
|
gateway: "192.168.1.1".parse().unwrap(),
|
||||||
|
destination: "10.42.42.0/24".parse().unwrap(),
|
||||||
|
metric: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
tx2.commit().unwrap();
|
||||||
|
|
||||||
|
config_manager.apply_pending_changes().unwrap();
|
||||||
|
|
||||||
|
let applied_config = config_manager.get_current_config();
|
||||||
|
info!("applied_config = {:#?}", applied_config);
|
||||||
|
*/
|
||||||
|
}
|
17
src/state.rs
Normal file
17
src/state.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use jsonrpsee::RpcModule;
|
||||||
|
|
||||||
|
use super::config_manager::ConfigManager;
|
||||||
|
use super::web::auth::SessionState;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub config_manager: ConfigManager,
|
||||||
|
pub session_state: SessionState,
|
||||||
|
pub rpc_module: RpcModule<RpcState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RpcState {
|
||||||
|
pub config_manager: ConfigManager,
|
||||||
|
pub session_state: SessionState,
|
||||||
|
}
|
153
src/web/auth.rs
Normal file
153
src/web/auth.rs
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::super::AppState;
|
||||||
|
use axum::routing::post;
|
||||||
|
use axum::{Json, Router};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tower_cookies::{Cookie, Cookies};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::Extension,
|
||||||
|
extract::State,
|
||||||
|
http::{Request, StatusCode},
|
||||||
|
middleware::{self, Next},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
|
||||||
|
use pwhash::sha512_crypt;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use custom_error::custom_error;
|
||||||
|
|
||||||
|
custom_error! { AuthError
|
||||||
|
NoSessionCookie = "No Session Cookie Found",
|
||||||
|
InvalidSession = "Invalid Session"
|
||||||
|
}
|
||||||
|
|
||||||
|
const SESSION_COOKIE: &str = "session";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SessionState {
|
||||||
|
pub sessions: Arc<RwLock<HashMap<String, Session>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Session {
|
||||||
|
pub username: String,
|
||||||
|
//expires: time,
|
||||||
|
// TODO have permissions here for fast access, update Permissions with a config manager apply function
|
||||||
|
// permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
struct LoginParameters {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/login", post(login_handler))
|
||||||
|
.route("/logout", post(logout_handler))
|
||||||
|
.route("/session", post(session_handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login_handler(
|
||||||
|
cookies: Cookies,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<LoginParameters>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if let Some(user) = state
|
||||||
|
.config_manager
|
||||||
|
.get_current_config()
|
||||||
|
.system
|
||||||
|
.users
|
||||||
|
.get(&payload.username.to_string())
|
||||||
|
{
|
||||||
|
if sha512_crypt::verify(payload.password, &user.hash) {
|
||||||
|
let mut sessions = state.session_state.sessions.write().unwrap();
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
sessions.insert(
|
||||||
|
id.clone(),
|
||||||
|
Session {
|
||||||
|
username: payload.username.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cookies.add(Cookie::new(SESSION_COOKIE, id));
|
||||||
|
info!("user logged in: {:?}", payload.username);
|
||||||
|
return StatusCode::OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("user login failed: {:?}", payload.username);
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout_handler(cookies: Cookies, state: State<AppState>) -> impl IntoResponse {
|
||||||
|
let session_cookie = cookies.get(SESSION_COOKIE);
|
||||||
|
match session_cookie {
|
||||||
|
Some(s) => {
|
||||||
|
let session_id = s.value();
|
||||||
|
|
||||||
|
let mut sessions = state.session_state.sessions.write().unwrap();
|
||||||
|
|
||||||
|
// TODO Fix Cookie remove
|
||||||
|
// cookies.remove(s.clone());
|
||||||
|
|
||||||
|
if let Some(session) = sessions.get(session_id) {
|
||||||
|
info!("user logged out: {:?}", session.username);
|
||||||
|
sessions.remove(session_id);
|
||||||
|
return StatusCode::OK;
|
||||||
|
}
|
||||||
|
return StatusCode::UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
None => return StatusCode::UNAUTHORIZED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_session(cookies: Cookies, state: SessionState) -> Result<Session, AuthError> {
|
||||||
|
let session_cookie = cookies.get(SESSION_COOKIE);
|
||||||
|
match session_cookie {
|
||||||
|
Some(s) => {
|
||||||
|
let session_id = s.value();
|
||||||
|
|
||||||
|
let sessions = state.sessions.write().unwrap();
|
||||||
|
|
||||||
|
if let Some(session) = sessions.get(session_id) {
|
||||||
|
return Ok(session.clone());
|
||||||
|
}
|
||||||
|
return Err(AuthError::InvalidSession);
|
||||||
|
}
|
||||||
|
None => return Err(AuthError::NoSessionCookie),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn session_handler(cookies: Cookies, State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
match get_session(cookies, state.session_state) {
|
||||||
|
// TODO Return build git commit hash as json result for frontend reloading
|
||||||
|
Ok(_) => return StatusCode::OK,
|
||||||
|
Err(_) => return StatusCode::UNAUTHORIZED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mw_auth<B>(
|
||||||
|
state: State<AppState>,
|
||||||
|
cookies: Cookies,
|
||||||
|
mut req: Request<B>,
|
||||||
|
next: Next<B>,
|
||||||
|
// session_state: SessionState,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
match get_session(cookies, state.session_state.clone()) {
|
||||||
|
Ok(session) => {
|
||||||
|
req.extensions_mut().insert(session.clone());
|
||||||
|
return Ok(next.run(req).await);
|
||||||
|
}
|
||||||
|
Err(_) => return Err(StatusCode::UNAUTHORIZED),
|
||||||
|
}
|
||||||
|
}
|
2
src/web/mod.rs
Normal file
2
src/web/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod auth;
|
||||||
|
pub mod rpc;
|
101
src/web/rpc.rs
Normal file
101
src/web/rpc.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
use crate::AppState;
|
||||||
|
use axum::routing::post;
|
||||||
|
use axum::{Json, Router};
|
||||||
|
use jsonrpsee::core::traits::ToRpcParams;
|
||||||
|
use jsonrpsee::core::Error;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::value::RawValue;
|
||||||
|
|
||||||
|
use axum::{extract::Extension, extract::State, response::IntoResponse};
|
||||||
|
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
// TODO fix this "workaround"
|
||||||
|
struct ParamConverter {
|
||||||
|
params: Option<Box<RawValue>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToRpcParams for ParamConverter {
|
||||||
|
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, Error> {
|
||||||
|
let s = String::from_utf8(serde_json::to_vec(&self.params)?);
|
||||||
|
match s {
|
||||||
|
Ok(s) => {
|
||||||
|
return RawValue::from_string(s)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(Error::ParseError)
|
||||||
|
}
|
||||||
|
// TODO make this a Parse error wrapping Utf8Error
|
||||||
|
Err(err) => return Err(Error::AlreadyStopped),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RpcRequest {
|
||||||
|
id: i64,
|
||||||
|
params: Option<Box<RawValue>>,
|
||||||
|
jsonrpc: String,
|
||||||
|
method: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
struct RpcResponse {
|
||||||
|
id: i64,
|
||||||
|
jsonrpc: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
result: Option<Box<RawValue>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
error: Option<RpcErrorObject>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
|
||||||
|
struct RpcErrorObject {
|
||||||
|
code: i64,
|
||||||
|
message: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
data: Option<Box<RawValue>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<super::super::AppState> {
|
||||||
|
Router::new().route("/api", post(api_handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
session: Extension<super::auth::Session>,
|
||||||
|
body: String,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
info!("api hit! user: {:?}", session.username);
|
||||||
|
|
||||||
|
// TODO handle Parse Error
|
||||||
|
let req: RpcRequest = serde_json::from_str(&body).unwrap();
|
||||||
|
|
||||||
|
// TODO check version
|
||||||
|
|
||||||
|
let params = ParamConverter { params: req.params };
|
||||||
|
|
||||||
|
// TODO check Permissions for method here?
|
||||||
|
|
||||||
|
let res: Result<Option<Box<RawValue>>, Error> =
|
||||||
|
state.rpc_module.call(&req.method, params).await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => Json(RpcResponse {
|
||||||
|
id: req.id,
|
||||||
|
jsonrpc: req.jsonrpc,
|
||||||
|
result: res,
|
||||||
|
error: None,
|
||||||
|
}),
|
||||||
|
Err(err) => Json(RpcResponse {
|
||||||
|
id: req.id,
|
||||||
|
jsonrpc: req.jsonrpc,
|
||||||
|
result: None,
|
||||||
|
error: Some(RpcErrorObject {
|
||||||
|
code: 10,
|
||||||
|
message: err.to_string(),
|
||||||
|
data: None,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue