brainvision 0.0.1

Rust library and TUI for Brain Products BrainVision RDA EEG streams over TCP/IP
Documentation
//! Process-level network sandboxing.
//!
//! **Warning:** BrainVision RDA requires TCP connectivity to Recorder.
//! Calling [`block_internet`] before `connect()` will break streaming.
//!
//! Use [`allow_only_endpoint`] to enforce a library-level endpoint allowlist.

use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};

use crate::error::BrainVisionError;

static ALLOWLIST: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();

fn allowlist() -> &'static Mutex<HashSet<String>> {
    ALLOWLIST.get_or_init(|| Mutex::new(HashSet::new()))
}

/// Allow only a specific endpoint at the library level (`host:port`).
///
/// This is always enforced by `BrainVisionDevice::connect*` when the `sandbox`
/// feature is enabled.
pub fn allow_only_endpoint(host: &str, port: u16) -> Result<(), BrainVisionError> {
    let key = format!("{host}:{port}");
    let mut set = allowlist()
        .lock()
        .map_err(|_| BrainVisionError::NotSupported("allowlist lock poisoned".into()))?;
    set.clear();
    set.insert(key);
    Ok(())
}

/// Returns `true` if endpoint is allowed (or if no allowlist is configured).
pub(crate) fn endpoint_allowed(host: &str, port: u16) -> bool {
    let key = format!("{host}:{port}");
    if let Ok(set) = allowlist().lock() {
        if set.is_empty() {
            true
        } else {
            set.contains(&key)
        }
    } else {
        false
    }
}

#[cfg(target_os = "linux")]
pub fn block_internet() -> Result<(), BrainVisionError> {
    use std::sync::atomic::{AtomicBool, Ordering};
    static APPLIED: AtomicBool = AtomicBool::new(false);
    if APPLIED.load(Ordering::Relaxed) {
        return Ok(());
    }

    let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
    if ret != 0 {
        return Err(BrainVisionError::NotSupported("prctl failed".into()));
    }

    #[repr(C)]
    #[derive(Clone, Copy)]
    struct BpfInsn {
        code: u16,
        jt: u8,
        jf: u8,
        k: u32,
    }
    #[repr(C)]
    struct BpfProg {
        len: u16,
        filter: *const BpfInsn,
    }

    let filter: [BpfInsn; 7] = [
        BpfInsn {
            code: 0x20,
            jt: 0,
            jf: 0,
            k: 0,
        },
        BpfInsn {
            code: 0x15,
            jt: 0,
            jf: 3,
            k: 41,
        },
        BpfInsn {
            code: 0x20,
            jt: 0,
            jf: 0,
            k: 16,
        },
        BpfInsn {
            code: 0x15,
            jt: 2,
            jf: 0,
            k: 2,
        },
        BpfInsn {
            code: 0x15,
            jt: 1,
            jf: 0,
            k: 10,
        },
        BpfInsn {
            code: 0x06,
            jt: 0,
            jf: 0,
            k: 0x7fff_0000,
        },
        BpfInsn {
            code: 0x06,
            jt: 0,
            jf: 0,
            k: 0x0005_0001,
        },
    ];
    let prog = BpfProg {
        len: 7,
        filter: filter.as_ptr(),
    };
    let ret = unsafe { libc::syscall(libc::SYS_seccomp, 1u64, 0u64, &prog as *const _) };
    if ret != 0 {
        return Err(BrainVisionError::NotSupported(format!(
            "seccomp failed: {}",
            std::io::Error::last_os_error()
        )));
    }
    APPLIED.store(true, Ordering::Relaxed);
    Ok(())
}

#[cfg(not(target_os = "linux"))]
pub fn block_internet() -> Result<(), BrainVisionError> {
    Ok(())
}

pub fn is_sandboxed() -> bool {
    if let Ok(set) = allowlist().lock() {
        !set.is_empty()
    } else {
        false
    }
}