🎉 init
This commit is contained in:
commit
812c4adb15
16 changed files with 4631 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
4339
Cargo.lock
generated
Normal file
4339
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
35
Cargo.toml
Normal file
35
Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
name = "sheepd"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "sheepd"
|
||||||
|
path = "src/sheepd.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "homeserver"
|
||||||
|
path = "src/server.rs"
|
||||||
|
required-features = ["homeserver"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
homeserver = ["axum", "axum-client-ip"]
|
||||||
|
axum = ["dep:axum"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
argh = "0.1.13"
|
||||||
|
axum = { version = "0.8.3", optional = true, features = ["macros"] }
|
||||||
|
log = "0.4.27"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
tokio = { version = "1.44.2", features = ["full"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.19"
|
||||||
|
owl = { git = "ssh://git@git.hydrar.de/red/owl" }
|
||||||
|
axum-client-ip = { version = "1.0.0", optional = true }
|
||||||
|
toml = "0.8.21"
|
||||||
|
hex = "0.4.3"
|
||||||
|
rand = "0.9.1"
|
||||||
|
based = { git = "ssh://git@git.hydrar.de/jmarya/based", branch = "owl" }
|
||||||
|
http2 = "0.4.21"
|
||||||
|
ureq = { version = "3.0.11", features = ["json"] }
|
14
README.md
Normal file
14
README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 🐑 sheepd
|
||||||
|
<div style="overflow: hidden">
|
||||||
|
<img src="sheepd.png" height="180" style="float:left;">
|
||||||
|
`sheepd` is a tiny daemon on device which connects back to a home server. This enables remote management and telemetry.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- See status of your enrolled devices
|
||||||
|
- Gather Prometheus Endpoint Data
|
||||||
|
- Gather data via [osquery](https://www.osquery.io)
|
||||||
|
- Create policy rules based on query data
|
||||||
|
- Create policy sets to enforce multiple policies and assign roles
|
||||||
|
- Install updates & packages
|
||||||
|
- Run tasks
|
BIN
sheepd.png
Normal file
BIN
sheepd.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 937 KiB |
8
src/api.rs
Normal file
8
src/api.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use owl::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct JoinParams {
|
||||||
|
pub join_token: Option<String>,
|
||||||
|
pub machine_id: String,
|
||||||
|
pub hostname: String,
|
||||||
|
}
|
54
src/server.rs
Normal file
54
src/server.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
http::StatusCode,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
use axum_client_ip::{ClientIp, ClientIpSource};
|
||||||
|
use based::auth::{Sessions, User};
|
||||||
|
use owl::{prelude::*, save, set_global_db};
|
||||||
|
use rand::RngCore;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
mod api;
|
||||||
|
mod server_core;
|
||||||
|
use server_core::route::{join_device, login_user};
|
||||||
|
|
||||||
|
fn generate_token() -> String {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let mut token = vec![0u8; 32];
|
||||||
|
rng.fill_bytes(&mut token);
|
||||||
|
|
||||||
|
hex::encode(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let db = Database::in_memory();
|
||||||
|
set_global_db!(db);
|
||||||
|
|
||||||
|
User::create("admin".to_string(), "admin", based::auth::UserRole::Admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let device = Router::new()
|
||||||
|
.route("/join", post(join_device))
|
||||||
|
.layer(ClientIpSource::ConnectInfo.into_extension()); // Direct IP
|
||||||
|
// .layer(ClientIpSource::XRealIp.into_extension()) // Proxy
|
||||||
|
|
||||||
|
let user = Router::new().route("/login", post(login_user));
|
||||||
|
|
||||||
|
let app = Router::new().merge(device).merge(user);
|
||||||
|
|
||||||
|
log::info!("Starting server");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
|
||||||
|
axum::serve(
|
||||||
|
listener,
|
||||||
|
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
10
src/server_core/config.rs
Normal file
10
src/server_core/config.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Config {}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
toml::from_str(&std::fs::read_to_string("./config.toml").unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
3
src/server_core/mod.rs
Normal file
3
src/server_core/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod config;
|
||||||
|
pub mod model;
|
||||||
|
pub mod route;
|
22
src/server_core/model.rs
Normal file
22
src/server_core/model.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use crate::api;
|
||||||
|
use crate::generate_token;
|
||||||
|
use owl::prelude::*;
|
||||||
|
|
||||||
|
#[model]
|
||||||
|
pub struct Machine {
|
||||||
|
pub id: Id,
|
||||||
|
pub hostname: String,
|
||||||
|
pub token: String,
|
||||||
|
pub next_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Machine {
|
||||||
|
pub fn from_join_param(join: api::JoinParams) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::String(join.machine_id),
|
||||||
|
hostname: join.hostname.trim().to_string(),
|
||||||
|
token: generate_token(),
|
||||||
|
next_token: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/server_core/route.rs
Normal file
48
src/server_core/route.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use crate::api;
|
||||||
|
use crate::server_core::model::Machine;
|
||||||
|
use axum::Json;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_client_ip::ClientIp;
|
||||||
|
use based::auth::Sessions;
|
||||||
|
use based::auth::User;
|
||||||
|
use owl::save;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginParam {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_user(Json(payload): Json<LoginParam>) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
|
log::info!("Login attempt for {}", payload.username);
|
||||||
|
let u = User::find(&payload.username).await.unwrap();
|
||||||
|
if u.read().verify_pw(&payload.password) {
|
||||||
|
let ses = u.read().session().await;
|
||||||
|
(StatusCode::OK, Json(json!({"token": ses.read().token})))
|
||||||
|
} else {
|
||||||
|
(StatusCode::FORBIDDEN, Json(json!({"error": "invalid"})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn join_device(
|
||||||
|
ClientIp(ip): ClientIp,
|
||||||
|
Json(payload): Json<api::JoinParams>,
|
||||||
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
|
// TODO : check if exists already
|
||||||
|
|
||||||
|
// TODO : validate join token
|
||||||
|
log::info!(
|
||||||
|
"New device joined: {} [{}]",
|
||||||
|
payload.hostname.trim(),
|
||||||
|
payload.machine_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let machine = Machine::from_join_param(payload);
|
||||||
|
let new_token = machine.token.clone();
|
||||||
|
|
||||||
|
save!(machine);
|
||||||
|
|
||||||
|
(StatusCode::OK, Json(json!({"ok": new_token})))
|
||||||
|
}
|
18
src/sheepd.rs
Normal file
18
src/sheepd.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use sheepd_core::args::{SheepdArgs, SheepdCommand};
|
||||||
|
mod api;
|
||||||
|
mod sheepd_core;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let args: SheepdArgs = argh::from_env();
|
||||||
|
|
||||||
|
if let Some(command) = args.command {
|
||||||
|
match command {
|
||||||
|
SheepdCommand::Join(join_command) => sheepd_core::cmd::join(join_command),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::info!("Starting sheepd");
|
||||||
|
|
||||||
|
// TODO : daemon loop
|
||||||
|
}
|
||||||
|
}
|
23
src/sheepd_core/args.rs
Normal file
23
src/sheepd_core/args.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
#[derive(FromArgs)]
|
||||||
|
/// Sheep Daemon
|
||||||
|
pub struct SheepdArgs {
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub command: Option<SheepdCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub enum SheepdCommand {
|
||||||
|
Join(JoinCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
|
/// Join a herd
|
||||||
|
#[argh(subcommand, name = "join")]
|
||||||
|
pub struct JoinCommand {
|
||||||
|
#[argh(positional)]
|
||||||
|
/// home server domain
|
||||||
|
pub home: String,
|
||||||
|
}
|
37
src/sheepd_core/cmd.rs
Normal file
37
src/sheepd_core/cmd.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::{api, sheepd_core::config::AgentConfig};
|
||||||
|
|
||||||
|
use super::args::JoinCommand;
|
||||||
|
|
||||||
|
pub fn join(conf: JoinCommand) {
|
||||||
|
// TODO : check for root
|
||||||
|
// TODO : check if joined somewhere already
|
||||||
|
|
||||||
|
log::info!("Joining to {}", conf.home);
|
||||||
|
|
||||||
|
let url = format!("http://{}/join", conf.home);
|
||||||
|
println!("{url}");
|
||||||
|
let mut res = ureq::post(url)
|
||||||
|
.send_json(&api::JoinParams {
|
||||||
|
join_token: None,
|
||||||
|
machine_id: std::fs::read_to_string("/etc/machine-id").unwrap(),
|
||||||
|
hostname: std::fs::read_to_string("/etc/hostname").unwrap(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let res: serde_json::Value = res.body_mut().read_json().unwrap();
|
||||||
|
|
||||||
|
let token = res
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.get("ok")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
log::info!("Joined {} successfully", conf.home);
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
"/etc/sheepd.toml",
|
||||||
|
toml::to_string(&AgentConfig::new(&conf.home, token)).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
16
src/sheepd_core/config.rs
Normal file
16
src/sheepd_core/config.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use owl::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AgentConfig {
|
||||||
|
pub home: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentConfig {
|
||||||
|
pub fn new(home_server: &str, token: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
home: home_server.to_string(),
|
||||||
|
token: token.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/sheepd_core/mod.rs
Normal file
3
src/sheepd_core/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod args;
|
||||||
|
pub mod cmd;
|
||||||
|
pub mod config;
|
Loading…
Add table
Add a link
Reference in a new issue