refactor: rename to gpcommon

This commit is contained in:
Kevin Yue
2023-05-24 10:07:42 +08:00
parent ec0bff1e36
commit f42f0d248e
28 changed files with 30 additions and 30 deletions

2
gpcommon/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
/Cargo.lock

21
gpcommon/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "gpcommon"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.14", features = ["full"] }
tokio-util = "0.7"
thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] }
bytes = "1.0"
serde_json = "1.0"
async-trait = "0.1"
ring = "0.16"
data-encoding = "2.3"
log = "0.4"
[build-dependencies]
cc = "1.0"

12
gpcommon/build.rs Normal file
View File

@@ -0,0 +1,12 @@
fn main() {
// Link to the native openconnect library
println!("cargo:rustc-link-lib=openconnect");
println!("cargo:rerun-if-changed=src/vpn/vpn.c");
println!("cargo:rerun-if-changed=src/vpn/vpn.h");
// Compile the vpn.c file
cc::Build::new()
.file("src/vpn/vpn.c")
.include("src/vpn")
.compile("vpn");
}

262
gpcommon/src/client.rs Normal file
View File

@@ -0,0 +1,262 @@
use crate::cmd::{Connect, Disconnect, Status};
use crate::reader::Reader;
use crate::request::CommandPayload;
use crate::response::ResponseData;
use crate::writer::Writer;
use crate::RequestPool;
use crate::Response;
use crate::SOCKET_PATH;
use crate::{Request, VpnStatus};
use log::{info, warn, debug};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::sync::Arc;
use tokio::io::{self, ReadHalf, WriteHalf};
use tokio::net::UnixStream;
use tokio::sync::{mpsc, Mutex, RwLock};
use tokio_util::sync::CancellationToken;
#[derive(Debug)]
enum ServerEvent {
Response(Response),
ServerDisconnected,
}
impl From<Response> for ServerEvent {
fn from(response: Response) -> Self {
Self::Response(response)
}
}
#[derive(Debug)]
pub struct Client {
// pool of requests that are waiting for responses
request_pool: Arc<RequestPool>,
// tx for sending requests to the channel
request_tx: mpsc::Sender<Request>,
// rx for receiving requests from the channel
request_rx: Arc<Mutex<mpsc::Receiver<Request>>>,
// tx for sending responses to the channel
server_event_tx: mpsc::Sender<ServerEvent>,
// rx for receiving responses from the channel
server_event_rx: Arc<Mutex<mpsc::Receiver<ServerEvent>>>,
is_healthy: Arc<RwLock<bool>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerApiError {
pub message: String,
}
impl Display for ServerApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{message}", message = self.message)
}
}
impl From<String> for ServerApiError {
fn from(message: String) -> Self {
Self { message }
}
}
impl From<&str> for ServerApiError {
fn from(message: &str) -> Self {
Self {
message: message.to_string(),
}
}
}
impl Default for Client {
fn default() -> Self {
let (request_tx, request_rx) = mpsc::channel::<Request>(32);
let (server_event_tx, server_event_rx) = mpsc::channel::<ServerEvent>(32);
Self {
request_pool: Default::default(),
request_tx,
request_rx: Arc::new(Mutex::new(request_rx)),
server_event_tx,
server_event_rx: Arc::new(Mutex::new(server_event_rx)),
is_healthy: Default::default(),
}
}
}
impl Client {
pub fn subscribe_status(&self, callback: impl Fn(VpnStatus) + Send + Sync + 'static) {
let server_event_rx = self.server_event_rx.clone();
tokio::spawn(async move {
loop {
let mut server_event_rx = server_event_rx.lock().await;
if let Some(server_event) = server_event_rx.recv().await {
match server_event {
ServerEvent::ServerDisconnected => {
callback(VpnStatus::Disconnected);
}
ServerEvent::Response(response) => {
if let ResponseData::Status(vpn_status) = response.data() {
callback(vpn_status);
}
}
}
}
}
});
}
pub async fn run(&self) {
info!("Connecting to the background service...");
// TODO exit the loop properly
loop {
match self.connect_to_server().await {
Ok(_) => {
debug!("Disconnected from server, reconnecting...");
}
Err(err) => {
debug!("Error connecting to server, retrying, error: {:?}", err)
}
}
// wait for a second before trying to reconnect
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
async fn connect_to_server(&self) -> Result<(), Box<dyn std::error::Error>> {
let stream = UnixStream::connect(SOCKET_PATH).await?;
let (read_stream, write_stream) = io::split(stream);
let cancel_token = CancellationToken::new();
let read_handle = tokio::spawn(handle_read(
read_stream,
self.request_pool.clone(),
self.server_event_tx.clone(),
cancel_token.clone(),
));
let write_handle = tokio::spawn(handle_write(
write_stream,
self.request_rx.clone(),
cancel_token,
));
*self.is_healthy.write().await = true;
info!("Connected to the background service");
let _ = tokio::join!(read_handle, write_handle);
*self.is_healthy.write().await = false;
// TODO connection was lost, cleanup the request pool and notify the UI
Ok(())
}
async fn send_command<T: TryFrom<ResponseData>>(
&self,
payload: CommandPayload,
) -> Result<T, ServerApiError> {
if !*self.is_healthy.read().await {
return Err("Background service is not running".into());
}
let (request, response_rx) = self.request_pool.create_request(payload).await;
if let Err(err) = self.request_tx.send(request).await {
return Err(format!("Error sending request to the channel: {}", err).into());
}
if let Ok(response) = response_rx.await {
if response.success() {
match response.data().try_into() {
Ok(it) => Ok(it),
Err(_) => Err("Error parsing response data".into()),
}
} else {
Err(response.message().into())
}
} else {
Err("Error receiving response from the channel".into())
}
}
pub async fn connect(&self, server: String, cookie: String) -> Result<(), ServerApiError> {
self.send_command(Connect::new(server, cookie).into()).await
}
pub async fn disconnect(&self) -> Result<(), ServerApiError> {
self.send_command(Disconnect.into()).await
}
pub async fn status(&self) -> Result<VpnStatus, ServerApiError> {
self.send_command(Status.into()).await
}
}
async fn handle_read(
read_stream: ReadHalf<UnixStream>,
request_pool: Arc<RequestPool>,
server_event_tx: mpsc::Sender<ServerEvent>,
cancel_token: CancellationToken,
) {
let mut reader: Reader = read_stream.into();
loop {
match reader.read_multiple::<Response>().await {
Ok(responses) => {
for response in responses {
match response.request_id() {
Some(id) => request_pool.complete_request(id, response).await,
None => {
if let Err(err) = server_event_tx.send(response.into()).await {
warn!("Error sending response to output channel: {}", err);
}
}
}
}
}
Err(err) if err.kind() == io::ErrorKind::ConnectionAborted => {
warn!("Disconnected from the background service");
if let Err(err) = server_event_tx.send(ServerEvent::ServerDisconnected).await {
warn!(
"Error sending server disconnected event to channel: {}",
err
);
}
cancel_token.cancel();
break;
}
Err(err) => {
warn!("Error reading from server: {}", err);
}
}
}
}
async fn handle_write(
write_stream: WriteHalf<UnixStream>,
request_rx: Arc<Mutex<mpsc::Receiver<Request>>>,
cancel_token: CancellationToken,
) {
let mut writer: Writer = write_stream.into();
loop {
let mut request_rx = request_rx.lock().await;
tokio::select! {
Some(request) = request_rx.recv() => {
if let Err(err) = writer.write(&request).await {
warn!("Error writing to server: {}", err);
}
}
_ = cancel_token.cancelled() => {
info!("The read loop has been cancelled, exiting the write loop");
break;
}
else => {
warn!("Error reading command from channel");
}
}
}
}

View File

@@ -0,0 +1,34 @@
use super::{Command, CommandContext, CommandError};
use crate::{ResponseData, VpnStatus};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Connect {
server: String,
cookie: String,
}
impl Connect {
pub fn new(server: String, cookie: String) -> Self {
Self { server, cookie }
}
}
#[async_trait]
impl Command for Connect {
async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError> {
let vpn = context.server_context.vpn();
let status = vpn.status().await;
if status != VpnStatus::Disconnected {
return Err(format!("VPN is already in state: {:?}", status).into());
}
if let Err(err) = vpn.connect(&self.server, &self.cookie).await {
return Err(err.to_string().into());
}
Ok(ResponseData::Empty)
}
}

View File

@@ -0,0 +1,15 @@
use super::{Command, CommandContext, CommandError};
use crate::ResponseData;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Disconnect;
#[async_trait]
impl Command for Disconnect {
async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError> {
context.server_context.vpn().disconnect().await;
Ok(ResponseData::Empty)
}
}

54
gpcommon/src/cmd/mod.rs Normal file
View File

@@ -0,0 +1,54 @@
use crate::{response::ResponseData, server::ServerContext};
use async_trait::async_trait;
use core::fmt::Debug;
use std::{
fmt::{self, Display},
sync::Arc,
};
mod connect;
mod disconnect;
mod status;
pub use connect::Connect;
pub use disconnect::Disconnect;
pub use status::Status;
#[derive(Debug)]
pub(crate) struct CommandContext {
server_context: Arc<ServerContext>,
}
impl From<Arc<ServerContext>> for CommandContext {
fn from(server_context: Arc<ServerContext>) -> Self {
Self { server_context }
}
}
#[derive(Debug)]
pub(crate) struct CommandError {
message: String,
}
impl From<String> for CommandError {
fn from(message: String) -> Self {
Self { message }
}
}
impl Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CommandError {:#?}", self.message)
}
}
#[async_trait]
pub(crate) trait Command: Send + Sync {
async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError>;
}
impl Debug for dyn Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Command")
}
}

View File

@@ -0,0 +1,16 @@
use super::{Command, CommandContext, CommandError};
use crate::ResponseData;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Status;
#[async_trait]
impl Command for Status {
async fn handle(&self, context: CommandContext) -> Result<ResponseData, CommandError> {
let status = context.server_context.vpn().status().await;
Ok(ResponseData::Status(status))
}
}

178
gpcommon/src/connection.rs Normal file
View File

@@ -0,0 +1,178 @@
use crate::request::Request;
use crate::server::ServerContext;
use crate::Reader;
use crate::Response;
use crate::ResponseData;
use crate::VpnStatus;
use crate::Writer;
use log::{debug, info, warn};
use std::sync::Arc;
use tokio::io::{self, ReadHalf, WriteHalf};
use tokio::net::UnixStream;
use tokio::sync::{mpsc, watch};
use tokio_util::sync::CancellationToken;
async fn handle_read(
read_stream: ReadHalf<UnixStream>,
server_context: Arc<ServerContext>,
response_tx: mpsc::Sender<Response>,
peer_pid: Option<i32>,
cancel_token: CancellationToken,
) {
let mut reader: Reader = read_stream.into();
let mut authenticated: Option<bool> = None;
loop {
match reader.read::<Request>().await {
Ok(request) => {
if authenticated.is_none() {
authenticated = Some(authenticate(peer_pid));
}
if !authenticated.unwrap_or(false) {
warn!("Client not authenticated, closing connection");
cancel_token.cancel();
break;
}
debug!("Received client request: {:?}", request);
let command = request.command();
let context = server_context.clone().into();
let mut response = match command.handle(context).await {
Ok(data) => Response::from(data),
Err(err) => Response::from(err.to_string()),
};
response.set_request_id(request.id());
let _ = response_tx.send(response).await;
}
Err(err) if err.kind() == io::ErrorKind::ConnectionAborted => {
info!("Client disconnected");
cancel_token.cancel();
break;
}
Err(err) => {
warn!("Error receiving request: {:?}", err);
}
}
}
}
async fn handle_write(
write_stream: WriteHalf<UnixStream>,
mut response_rx: mpsc::Receiver<Response>,
cancel_token: CancellationToken,
) {
let mut writer: Writer = write_stream.into();
loop {
tokio::select! {
Some(response) = response_rx.recv() => {
debug!("Sending response: {:?}", response);
if let Err(err) = writer.write(&response).await {
warn!("Error sending response: {:?}", err);
}
}
_ = cancel_token.cancelled() => {
info!("Exiting the write loop");
break;
}
else => {
warn!("Error receiving response from channel");
}
}
}
}
async fn handle_status_change(
mut status_rx: watch::Receiver<VpnStatus>,
response_tx: mpsc::Sender<Response>,
cancel_token: CancellationToken,
) {
// Send the initial status
send_status(&status_rx, &response_tx).await;
debug!("Waiting for status change");
let start_time = std::time::Instant::now();
loop {
tokio::select! {
_ = status_rx.changed() => {
debug!("Status changed: {:?}", start_time.elapsed());
send_status(&status_rx, &response_tx).await;
}
_ = cancel_token.cancelled() => {
info!("Exiting the status loop");
break;
}
else => {
warn!("Error receiving status from channel");
}
}
}
}
async fn send_status(status_rx: &watch::Receiver<VpnStatus>, response_tx: &mpsc::Sender<Response>) {
let status = *status_rx.borrow();
if let Err(err) = response_tx
.send(Response::from(ResponseData::Status(status)))
.await
{
warn!("Error sending status: {:?}", err);
}
}
pub(crate) async fn handle_connection(socket: UnixStream, context: Arc<ServerContext>) {
let peer_pid = peer_pid(&socket);
let (read_stream, write_stream) = io::split(socket);
let (response_tx, response_rx) = mpsc::channel::<Response>(32);
let cancel_token = CancellationToken::new();
let status_rx = context.vpn().status_rx().await;
// Read requests from the client
let read_handle = tokio::spawn(handle_read(
read_stream,
context.clone(),
response_tx.clone(),
peer_pid,
cancel_token.clone(),
));
// Write responses to the client
let write_handle = tokio::spawn(handle_write(
write_stream,
response_rx,
cancel_token.clone(),
));
// Watch for status changes
let status_handle = tokio::spawn(handle_status_change(
status_rx,
response_tx.clone(),
cancel_token,
));
let _ = tokio::join!(read_handle, write_handle, status_handle);
debug!("Client connection closed");
}
fn peer_pid(socket: &UnixStream) -> Option<i32> {
match socket.peer_cred() {
Ok(ucred) => ucred.pid(),
Err(_) => None,
}
}
// TODO - Implement authentication
fn authenticate(peer_pid: Option<i32>) -> bool {
if let Some(pid) = peer_pid {
info!("Peer PID: {}", pid);
true
} else {
false
}
}

50
gpcommon/src/lib.rs Normal file
View File

@@ -0,0 +1,50 @@
use data_encoding::HEXUPPER;
use ring::digest::{Context, SHA256};
use std::{
fs::File,
io::{BufReader, Read},
path::Path,
};
pub const SOCKET_PATH: &str = "/tmp/gpservice.sock";
mod client;
mod cmd;
mod connection;
mod reader;
mod request;
mod response;
pub mod server;
mod vpn;
mod writer;
pub(crate) use request::Request;
pub(crate) use request::RequestPool;
pub use response::Response;
pub use response::ResponseData;
pub use response::TryFromResponseDataError;
pub(crate) use reader::Reader;
pub(crate) use writer::Writer;
pub use client::Client;
pub use client::ServerApiError;
pub use vpn::VpnStatus;
pub fn sha256_digest<P: AsRef<Path>>(file_path: P) -> Result<String, std::io::Error> {
let input = File::open(file_path)?;
let mut reader = BufReader::new(input);
let mut context = Context::new(&SHA256);
let mut buffer = [0; 1024];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(HEXUPPER.encode(context.finish().as_ref()))
}

59
gpcommon/src/reader.rs Normal file
View File

@@ -0,0 +1,59 @@
use serde::Deserialize;
use tokio::io::{self, AsyncReadExt, ReadHalf};
use tokio::net::UnixStream;
pub(crate) struct Reader {
stream: ReadHalf<UnixStream>,
}
impl From<ReadHalf<UnixStream>> for Reader {
fn from(stream: ReadHalf<UnixStream>) -> Self {
Self { stream }
}
}
impl Reader {
pub async fn read<T: for<'a> Deserialize<'a>>(&mut self) -> Result<T, io::Error> {
let mut buffer = [0; 2048];
match self.stream.read(&mut buffer).await {
Ok(0) => Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"Peer disconnected",
)),
Ok(bytes_read) => {
let data = serde_json::from_slice::<T>(&buffer[..bytes_read])?;
Ok(data)
}
Err(err) => Err(err),
}
}
pub async fn read_multiple<T: for<'a> Deserialize<'a>>(&mut self) -> Result<Vec<T>, io::Error> {
let mut buffer = [0; 2048];
match self.stream.read(&mut buffer).await {
Ok(0) => Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"Peer disconnected",
)),
Ok(bytes_read) => {
let response_str = String::from_utf8_lossy(&buffer[..bytes_read]);
let responses: Vec<&str> = response_str.split("\n\n").collect();
let responses = responses
.iter()
.filter_map(|r| {
if !r.is_empty() {
serde_json::from_str(r).ok()
} else {
None
}
})
.collect::<Vec<T>>();
Ok(responses)
}
Err(err) => Err(err),
}
}
}

105
gpcommon/src/request.rs Normal file
View File

@@ -0,0 +1,105 @@
use crate::cmd::{Command, Connect, Disconnect, Status};
use crate::Response;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{oneshot, RwLock};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Request {
id: u64,
payload: CommandPayload,
}
impl Request {
fn new(id: u64, payload: CommandPayload) -> Self {
Self { id, payload }
}
pub fn id(&self) -> u64 {
self.id
}
pub fn command(&self) -> Box<dyn Command> {
match &self.payload {
CommandPayload::Status(status) => Box::new(status.clone()),
CommandPayload::Connect(connect) => Box::new(connect.clone()),
CommandPayload::Disconnect(disconnect) => Box::new(disconnect.clone()),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum CommandPayload {
Status(Status),
Connect(Connect),
Disconnect(Disconnect),
}
impl From<Status> for CommandPayload {
fn from(status: Status) -> Self {
Self::Status(status)
}
}
impl From<Connect> for CommandPayload {
fn from(connect: Connect) -> Self {
Self::Connect(connect)
}
}
impl From<Disconnect> for CommandPayload {
fn from(disconnect: Disconnect) -> Self {
Self::Disconnect(disconnect)
}
}
#[derive(Debug)]
struct RequestHandle {
id: u64,
response_tx: oneshot::Sender<Response>,
}
#[derive(Debug, Default)]
struct IdGenerator {
current_id: u64,
}
impl IdGenerator {
fn next(&mut self) -> u64 {
let current_id = self.current_id;
self.current_id = self.current_id.wrapping_add(1);
current_id
}
}
#[derive(Debug, Default)]
pub(crate) struct RequestPool {
id_generator: Arc<RwLock<IdGenerator>>,
request_handles: Arc<RwLock<Vec<RequestHandle>>>,
}
impl RequestPool {
pub async fn create_request(
&self,
payload: CommandPayload,
) -> (Request, oneshot::Receiver<Response>) {
let id = self.id_generator.write().await.next();
let (response_tx, response_rx) = oneshot::channel();
let request_handle = RequestHandle { id, response_tx };
self.request_handles.write().await.push(request_handle);
(Request::new(id, payload), response_rx)
}
pub async fn complete_request(&self, id: u64, response: Response) {
let mut request_handles = self.request_handles.write().await;
let request_handle = request_handles
.iter()
.position(|handle| handle.id == id)
.map(|index| request_handles.remove(index));
if let Some(request_handle) = request_handle {
let _ = request_handle.response_tx.send(response);
}
}
}

113
gpcommon/src/response.rs Normal file
View File

@@ -0,0 +1,113 @@
use crate::vpn::VpnStatus;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Response {
request_id: Option<u64>,
success: bool,
message: String,
data: ResponseData,
}
impl From<ResponseData> for Response {
fn from(data: ResponseData) -> Self {
Self {
request_id: None,
success: true,
message: String::from("Success"),
data,
}
}
}
impl From<String> for Response {
fn from(message: String) -> Self {
Self {
request_id: None,
success: false,
message,
data: ResponseData::Empty,
}
}
}
impl Response {
pub fn success(&self) -> bool {
self.success
}
pub fn message(&self) -> &str {
&self.message
}
pub fn set_request_id(&mut self, command_id: u64) {
self.request_id = Some(command_id);
}
pub fn request_id(&self) -> Option<u64> {
self.request_id
}
pub fn data(&self) -> ResponseData {
self.data
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum ResponseData {
Status(VpnStatus),
Empty,
}
impl From<VpnStatus> for ResponseData {
fn from(status: VpnStatus) -> Self {
Self::Status(status)
}
}
impl From<()> for ResponseData {
fn from(_: ()) -> Self {
Self::Empty
}
}
#[derive(Debug)]
pub struct TryFromResponseDataError {
message: String,
}
impl std::fmt::Display for TryFromResponseDataError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid ResponseData: {}", self.message)
}
}
impl From<&str> for TryFromResponseDataError {
fn from(message: &str) -> Self {
Self {
message: message.into(),
}
}
}
impl TryFrom<ResponseData> for VpnStatus {
type Error = TryFromResponseDataError;
fn try_from(value: ResponseData) -> Result<Self, Self::Error> {
match value {
ResponseData::Status(status) => Ok(status),
_ => Err("ResponseData is not a VpnStatus".into()),
}
}
}
impl TryFrom<ResponseData> for () {
type Error = TryFromResponseDataError;
fn try_from(value: ResponseData) -> Result<Self, Self::Error> {
match value {
ResponseData::Empty => Ok(()),
_ => Err("ResponseData is not empty".into()),
}
}
}

91
gpcommon/src/server.rs Normal file
View File

@@ -0,0 +1,91 @@
use crate::{connection::handle_connection, vpn::Vpn};
use log::{warn, info};
use std::{future::Future, os::unix::prelude::PermissionsExt, path::Path, sync::Arc};
use tokio::fs;
use tokio::net::{UnixListener, UnixStream};
#[derive(Debug, Default)]
pub(crate) struct ServerContext {
vpn: Arc<Vpn>,
}
struct Server {
socket_path: String,
context: Arc<ServerContext>,
}
impl ServerContext {
pub fn vpn(&self) -> Arc<Vpn> {
self.vpn.clone()
}
}
impl Server {
fn new(socket_path: String) -> Self {
Self {
socket_path,
context: Default::default(),
}
}
// Check if an instance of the server is already running.
// by trying to connect to the socket.
async fn is_running(&self) -> bool {
UnixStream::connect(&self.socket_path).await.is_ok()
}
async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(&self.socket_path).exists() {
fs::remove_file(&self.socket_path).await?;
}
let listener = UnixListener::bind(&self.socket_path)?;
info!("Listening on socket: {:?}", listener.local_addr()?);
let metadata = fs::metadata(&self.socket_path).await?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o666);
fs::set_permissions(&self.socket_path, permissions).await?;
loop {
match listener.accept().await {
Ok((socket, _)) => {
info!("Accepted connection: {:?}", socket.peer_addr()?);
tokio::spawn(handle_connection(socket, self.context.clone()));
}
Err(err) => {
warn!("Error accepting connection: {:?}", err);
}
}
}
}
async fn stop(&self) -> Result<(), Box<dyn std::error::Error>> {
self.context.vpn().disconnect().await;
fs::remove_file(&self.socket_path).await?;
Ok(())
}
}
pub async fn run(
socket_path: &str,
shutdown: impl Future,
) -> Result<(), Box<dyn std::error::Error>> {
let server = Server::new(socket_path.to_string());
if server.is_running().await {
return Err("Another instance of the server is already running".into());
}
tokio::select! {
res = server.start() => {
res?
},
_ = shutdown => {
info!("Shutting down the server...");
server.stop().await?;
},
}
Ok(())
}

56
gpcommon/src/vpn/ffi.rs Normal file
View File

@@ -0,0 +1,56 @@
use log::{debug, info, trace, warn};
use std::ffi::c_void;
use tokio::sync::mpsc;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub(crate) struct Options {
pub server: *const ::std::os::raw::c_char,
pub cookie: *const ::std::os::raw::c_char,
pub script: *const ::std::os::raw::c_char,
pub user_data: *mut c_void,
}
#[link(name = "vpn")]
extern "C" {
#[link_name = "vpn_connect"]
pub(crate) fn connect(options: *const Options) -> ::std::os::raw::c_int;
#[link_name = "vpn_disconnect"]
pub(crate) fn disconnect();
}
#[no_mangle]
extern "C" fn on_vpn_connected(value: i32, sender: *mut c_void) {
let sender = unsafe { &*(sender as *const mpsc::Sender<i32>) };
sender
.blocking_send(value)
.expect("Failed to send VPN connection code");
}
// Logger used in the C code.
// level: 0 = error, 1 = info, 2 = debug, 3 = trace
// map the error level log in openconnect to the warning level
#[no_mangle]
extern "C" fn vpn_log(level: i32, message: *const ::std::os::raw::c_char) {
let message = unsafe { std::ffi::CStr::from_ptr(message) };
let message = message.to_str().unwrap_or("Invalid log message");
// Strip the trailing newline
let message = message.trim_end_matches('\n');
if level == 0 {
warn!("{}", message);
} else if level == 1 {
info!("{}", message);
} else if level == 2 {
debug!("{}", message);
} else if level == 3 {
trace!("{}", message);
} else {
warn!(
"Unknown log level: {}, enable DEBUG log level to see more details",
level
);
debug!("{}", message);
}
}

153
gpcommon/src/vpn/mod.rs Normal file
View File

@@ -0,0 +1,153 @@
use log::{warn, info, debug};
use serde::{Deserialize, Serialize};
use std::ffi::{c_void, CString};
use std::sync::Arc;
use std::thread;
use tokio::sync::watch;
use tokio::sync::{mpsc, Mutex};
mod ffi;
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VpnStatus {
Disconnected,
Connecting,
Connected,
Disconnecting,
}
#[derive(Debug)]
struct StatusHolder {
status: VpnStatus,
status_tx: watch::Sender<VpnStatus>,
status_rx: watch::Receiver<VpnStatus>,
}
impl Default for StatusHolder {
fn default() -> Self {
let (status_tx, status_rx) = watch::channel(VpnStatus::Disconnected);
Self {
status: VpnStatus::Disconnected,
status_tx,
status_rx,
}
}
}
impl StatusHolder {
fn set(&mut self, status: VpnStatus) {
self.status = status;
if let Err(err) = self.status_tx.send(status) {
warn!("Failed to send VPN status: {}", err);
}
}
fn status_rx(&self) -> watch::Receiver<VpnStatus> {
self.status_rx.clone()
}
}
#[derive(Debug)]
pub(crate) struct VpnOptions {
server: CString,
cookie: CString,
script: CString,
}
impl VpnOptions {
fn as_oc_options(&self, user_data: *mut c_void) -> ffi::Options {
ffi::Options {
server: self.server.as_ptr(),
cookie: self.cookie.as_ptr(),
script: self.script.as_ptr(),
user_data,
}
}
fn to_cstr(value: &str) -> CString {
CString::new(value.to_string()).expect("Failed to convert to CString")
}
}
#[derive(Debug, Default)]
pub(crate) struct Vpn {
status_holder: Arc<Mutex<StatusHolder>>,
vpn_options: Arc<Mutex<Option<VpnOptions>>>,
}
impl Vpn {
pub async fn status_rx(&self) -> watch::Receiver<VpnStatus> {
self.status_holder.lock().await.status_rx()
}
pub async fn connect(
&self,
server: &str,
cookie: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Save the VPN options so we can use them later, e.g. reconnect
*self.vpn_options.lock().await = Some(VpnOptions {
server: VpnOptions::to_cstr(server),
cookie: VpnOptions::to_cstr(cookie),
script: VpnOptions::to_cstr("/usr/share/vpnc-scripts/vpnc-script")
});
let vpn_options = self.vpn_options.clone();
let status_holder = self.status_holder.clone();
let (vpn_tx, mut vpn_rx) = mpsc::channel::<i32>(1);
thread::spawn(move || {
let vpn_tx = &vpn_tx as *const _ as *mut c_void;
let oc_options = vpn_options
.blocking_lock()
.as_ref()
.expect("Failed to unwrap vpn_options")
.as_oc_options(vpn_tx);
// Start the VPN connection, this will block until the connection is closed
status_holder.blocking_lock().set(VpnStatus::Connecting);
let ret = unsafe { ffi::connect(&oc_options) };
info!("VPN connection closed with code: {}", ret);
status_holder.blocking_lock().set(VpnStatus::Disconnected);
});
info!("Waiting for the VPN connection...");
if let Some(cmd_pipe_fd) = vpn_rx.recv().await {
info!("VPN connection started, cmd_pipe_fd: {}", cmd_pipe_fd);
self.status_holder.lock().await.set(VpnStatus::Connected);
} else {
warn!("VPN connection failed to start");
}
Ok(())
}
pub async fn disconnect(&self) {
if self.status().await == VpnStatus::Disconnected {
info!("VPN is not connected, nothing to do");
return;
}
info!("Disconnecting VPN...");
unsafe { ffi::disconnect() };
let mut status_rx = self.status_rx().await;
debug!("Waiting for the VPN to disconnect...");
while status_rx.changed().await.is_ok() {
if *status_rx.borrow() == VpnStatus::Disconnected {
info!("VPN disconnected");
break;
}
}
}
pub async fn status(&self) -> VpnStatus {
self.status_holder.lock().await.status
}
}

120
gpcommon/src/vpn/vpn.c Normal file
View File

@@ -0,0 +1,120 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <openconnect.h>
#include "vpn.h"
void *g_user_data;
static int g_cmd_pipe_fd;
const char *g_vpnc_script;
/* Validate the peer certificate */
static int validate_peer_cert(__attribute__((unused)) void *_vpninfo, const char *reason)
{
INFO("Validating peer cert: %s", reason);
return 0;
}
/* Print progress messages */
static void print_progress(__attribute__((unused)) void *_vpninfo, int level, const char *format, ...)
{
va_list args;
va_start(args, format);
char *message = format_message(format, args);
va_end(args);
if (message == NULL)
{
ERROR("Failed to format log message");
}
else
{
LOG(level, message);
free(message);
}
}
static void setup_tun_handler(void *_vpninfo)
{
openconnect_setup_tun_device(_vpninfo, g_vpnc_script, NULL);
on_vpn_connected(g_cmd_pipe_fd, g_user_data);
}
/* Initialize VPN connection */
int vpn_connect(const vpn_options *options)
{
struct openconnect_info *vpninfo;
struct utsname utsbuf;
g_user_data = options->user_data;
g_vpnc_script = options->script;
vpninfo = openconnect_vpninfo_new("PAN GlobalProtect", validate_peer_cert, NULL, NULL, print_progress, NULL);
if (!vpninfo)
{
ERROR("openconnect_vpninfo_new failed");
return 1;
}
openconnect_set_loglevel(vpninfo, 1);
openconnect_init_ssl();
openconnect_set_protocol(vpninfo, "gp");
openconnect_set_hostname(vpninfo, options->server);
openconnect_set_cookie(vpninfo, options->cookie);
g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
if (g_cmd_pipe_fd < 0)
{
ERROR("openconnect_setup_cmd_pipe failed");
return 1;
}
if (!uname(&utsbuf))
{
openconnect_set_localname(vpninfo, utsbuf.nodename);
}
// Essential step
if (openconnect_make_cstp_connection(vpninfo) != 0)
{
ERROR("openconnect_make_cstp_connection failed");
return 1;
}
if (openconnect_setup_dtls(vpninfo, 60) != 0)
{
openconnect_disable_dtls(vpninfo);
}
// Essential step
openconnect_set_setup_tun_handler(vpninfo, setup_tun_handler);
while (1)
{
int ret = openconnect_mainloop(vpninfo, 300, 10);
if (ret)
{
INFO("openconnect_mainloop returned %d, exiting", ret);
openconnect_vpninfo_free(vpninfo);
return ret;
}
INFO("openconnect_mainloop returned 0, reconnecting");
}
}
/* Stop the VPN connection */
void vpn_disconnect()
{
char cmd = OC_CMD_CANCEL;
if (write(g_cmd_pipe_fd, &cmd, 1) < 0)
{
ERROR("Failed to write to command pipe, VPN connection may not be stopped");
}
}

62
gpcommon/src/vpn/vpn.h Normal file
View File

@@ -0,0 +1,62 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <openconnect.h>
typedef struct vpn_options
{
const char *server;
const char *cookie;
const char *script;
void *user_data;
} vpn_options;
int vpn_connect(const vpn_options *options);
void vpn_disconnect();
extern void on_vpn_connected(int cmd_pipe_fd, void *user_data);
extern void vpn_log(int level, const char *msg);
static char *format_message(const char *format, va_list args)
{
va_list args_copy;
va_copy(args_copy, args);
int len = vsnprintf(NULL, 0, format, args_copy);
va_end(args_copy);
char *buffer = malloc(len + 1);
if (buffer == NULL)
{
return NULL;
}
vsnprintf(buffer, len + 1, format, args);
return buffer;
}
static void _log(int level, ...)
{
va_list args;
va_start(args, level);
char *format = va_arg(args, char *);
char *message = format_message(format, args);
va_end(args);
if (message == NULL)
{
vpn_log(PRG_ERR, "Failed to format log message");
}
else
{
vpn_log(level, message);
free(message);
}
}
#define LOG(level, ...) _log(level, __VA_ARGS__)
#define ERROR(...) LOG(PRG_ERR, __VA_ARGS__)
#define INFO(...) LOG(PRG_INFO, __VA_ARGS__)
#define DEBUG(...) LOG(PRG_DEBUG, __VA_ARGS__)
#define TRACE(...) LOG(PRG_TRACE, __VA_ARGS__)

24
gpcommon/src/writer.rs Normal file
View File

@@ -0,0 +1,24 @@
use serde::Serialize;
use tokio::io::{self, AsyncWriteExt, WriteHalf};
use tokio::net::UnixStream;
pub(crate) struct Writer {
stream: WriteHalf<UnixStream>,
}
impl From<WriteHalf<UnixStream>> for Writer {
fn from(stream: WriteHalf<UnixStream>) -> Self {
Self { stream }
}
}
impl Writer {
pub async fn write<T: Serialize>(&mut self, data: &T) -> Result<(), io::Error> {
let data = serde_json::to_string(data)?;
let data = format!("{}\n\n", data);
self.stream.write_all(data.as_bytes()).await?;
self.stream.flush().await?;
Ok(())
}
}