Files
jmb-bar/src/backend/json_socket.rs
jmb05 1be6e51d02 Tailscale -> Netbird
Start Implement Idle Daemon with Dimming Display
2026-10-28 01:09:56 +01:00

266 lines
8.8 KiB
Rust

use crate::LOGGER;
use crate::backend::{BackendThread, Registrator};
use crossbeam_channel::{Receiver, Sender};
use http::Request;
use http_body_util::{BodyExt, Empty, Full};
use hyper::body::{Bytes, Incoming};
use hyper_util::rt::TokioIo;
use jmb_logger_rs::*;
use std::cmp::PartialEq;
use std::error::Error;
use std::process::Command;
use std::sync::{Arc, Mutex};
use tokio::net::UnixStream;
pub struct JsonSocketBackend {
subscribers: Vec<Sender<State>>,
state: State,
socket_address: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum State {
Connected,
Connecting,
Disconnected,
NeedsLogin,
}
impl State {
fn try_from_value(value: i64) -> Option<State> {
match value {
5 => Some(State::Connecting),
6 => Some(State::Connected),
_ => Some(State::Disconnected),
}
}
}
impl JsonSocketBackend {
async fn watch(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
let socket_address = instance
.lock()
.expect("Couldn't get lock")
.socket_address
.clone();
let socket = UnixStream::connect(&socket_address).await?;
let io = TokioIo::new(socket);
let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
tokio::spawn(async move {
if let Err(err) = connection.await {
warn!(&LOGGER, "Tailscale socket HTTP connection error: {err}");
}
});
let request = Request::builder()
.method("GET")
.uri("http://local-tailscaled.sock/localapi/v0/watch-ipn-bus")
.header("Host", "local-tailscaled.sock")
.header("Accept", "application/json")
.body(Empty::<Bytes>::new())?;
let response = sender.send_request(request).await?;
if !response.status().is_success() {
return Err(format!("Tailscale API returned {}", response.status()).into());
}
let mut body: Incoming = response.into_body();
let mut pending = Vec::<u8>::new();
while let Some(frame_result) = body.frame().await {
let frame = frame_result?;
if let Ok(data) = frame.into_data() {
pending.extend_from_slice(&data);
while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
let line: Vec<u8> = pending.drain(..=newline).collect();
let line = &line[..line.len() - 1];
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
match serde_json::from_slice::<serde_json::Value>(line) {
Ok(notification) => {
if let Some(state) = notification.get("State")
&& !state.is_null()
{
let state = State::try_from_value(state.as_i64().unwrap_or(4))
.unwrap_or(State::Disconnected);
instance.lock().expect("Couldn't get lock").publish(state);
}
}
Err(err) => {
warn!(&LOGGER, "Invalid Tailscale notification: {err}");
}
}
}
}
}
Ok(())
}
async fn watch_netbird(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
let socket_address = instance
.lock()
.expect("Couldn't get lock")
.socket_address
.clone();
let socket = UnixStream::connect(&socket_address).await?;
let io = TokioIo::new(socket);
let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
tokio::spawn(async move {
if let Err(err) = connection.await {
warn!(&LOGGER, "Netbird socket HTTP connection error: {err}");
}
});
let request = Request::builder()
.method("POST")
.uri("http://localhost/daemon.DaemonService/SubscribeStatus")
.header("Host", "localhost")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(Full::new(Bytes::from_static(b"{}")))?;
let response = sender.send_request(request).await?;
if !response.status().is_success() {
return Err(format!("NetBird API returned {}", response.status()).into());
}
let mut body = response.into_body();
let mut pending = Vec::<u8>::new();
while let Some(frame_result) = body.frame().await {
let frame = frame_result?;
if let Ok(data) = frame.into_data() {
pending.extend_from_slice(&data);
while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
let line: Vec<u8> = pending.drain(..=newline).collect();
let line = &line[..line.len() - 1];
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
match serde_json::from_slice::<serde_json::Value>(line) {
Ok(notification) => {
let Some(status) = notification
.get("result")
.and_then(|v| v.get("status").and_then(|v| v.as_str()))
else {
continue;
};
let state = match status {
"Connected" => State::Connected,
"Connecting" => State::Connecting,
"Idle" => State::Disconnected,
"NeedsLogin" => State::NeedsLogin,
other => {
warn!(&LOGGER, "Unknown NetBird status: {other}");
continue;
}
};
instance.lock().expect("Couldn't get lock").publish(state);
}
Err(err) => {
warn!(&LOGGER, "Invalid NetBird notification: {err}");
}
}
}
}
}
Ok(())
}
}
impl BackendThread<State, ()> for JsonSocketBackend {
fn subscribe(registrator: Registrator<State>) -> (Receiver<State>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<State>();
let _ = registrator.send(tx);
(rx, ())
}
fn subscribers(&self) -> &Vec<Sender<State>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<State>>) {
let state = initial_state_tailscale();
let instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
state,
socket_address: String::from("/var/run/netbird-http.sock"),
}));
let instance_clone = instance.clone();
std::thread::spawn(move || {
while let Ok(tx) = registration_rx.recv() {
trace!(&LOGGER, "Added NetBird Subscriber: {tx:?}");
let _ = tx.send(
instance_clone
.lock()
.expect("Couldn't get lock")
.state
.clone(),
);
instance_clone
.lock()
.expect("Couldn't get lock")
.subscribers
.push(tx);
}
});
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
runtime.block_on(async move {
loop {
if let Err(e) = Self::watch_netbird(instance.clone()).await {
warn!(&LOGGER, "Error: {e:?}");
}
}
});
});
}
}
fn initial_state_tailscale() -> State {
if let Ok(output) = Command::new("tailscale")
.arg("status")
.arg("--json")
.output()
{
let val: Result<serde_json::Value, _> = serde_json::from_slice(&output.stdout);
if let Ok(val) = val {
if let Some(res) = val.get("BackendState").and_then(|v| v.as_str())
&& res.eq("Running")
{
return State::Connected;
}
} else {
error!(&LOGGER, "{:?}", val.err());
}
}
State::Connecting
}