Files
FlClash/services/helper/src/service/peer.rs
T
chen08209 94a25f4827 fix(desktop): fix the system proxy, tray and Helper on Windows and Linux
Changelog: Windows and Linux desktop fixes
2026-10-11 12:28:52 +08:00

345 lines
11 KiB
Rust

use crate::service::hub::{
ensure_core_sha256_configured, log_message, release_managed_core_on_shutdown, routes,
};
use crate::service::owner::{client_pid, same_sid, TcpConnection};
use std::ffi::c_void;
use std::future::Future;
use std::io::{Error, Result as IoResult};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::pin::Pin;
use std::ptr::{null, null_mut};
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};
use tokio::time::Sleep;
use tokio_stream::Stream;
use windows_sys::Win32::Foundation::{
CloseHandle, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS, HANDLE, NO_ERROR,
};
use windows_sys::Win32::NetworkManagement::IpHelper::{
GetExtendedTcpTable, MIB_TCPTABLE_OWNER_PID, TCP_TABLE_OWNER_PID_ALL,
};
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows_sys::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER};
use windows_sys::Win32::System::Registry::{
RegCloseKey, RegCreateKeyExW, RegDeleteKeyW, RegSetValueExW, HKEY, HKEY_LOCAL_MACHINE,
KEY_SET_VALUE, REG_DWORD, REG_OPTION_VOLATILE,
};
use windows_sys::Win32::System::Threading::{
OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
};
const AF_INET: u32 = 2;
/// Hyper-V and WinNAT reserve ranges that can cover any fixed port; only admins
/// write this volatile key under the service's own, and a reboot clears it.
const PORT_KEY: &str = r"SYSTEM\CurrentControlSet\Services\FlClashHelperService\Runtime";
const PORT_VALUE: &str = "Port";
const TABLE_ATTEMPTS: usize = 5;
const ACCEPT_RETRY_DELAY: Duration = Duration::from_secs(1);
static REJECTION_LOGGED: AtomicBool = AtomicBool::new(false);
struct Handle(HANDLE);
impl Drop for Handle {
fn drop(&mut self) {
// SAFETY: the handle came from a successful Open* call.
unsafe {
CloseHandle(self.0);
}
}
}
pub fn process_sid(pid: u32) -> IoResult<String> {
// SAFETY: both handles are owned by a Handle; the u64 buffer gives the
// pointer-bearing TOKEN_USER the alignment it needs.
unsafe {
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if process == 0 {
return Err(Error::last_os_error());
}
let process = Handle(process);
let mut token: HANDLE = 0;
if OpenProcessToken(process.0, TOKEN_QUERY, &mut token) == 0 {
return Err(Error::last_os_error());
}
let token = Handle(token);
let mut length = 0u32;
GetTokenInformation(token.0, TokenUser, null_mut(), 0, &mut length);
if length == 0 {
return Err(Error::last_os_error());
}
let mut buffer = vec![0u64; (length as usize).div_ceil(8)];
if GetTokenInformation(
token.0,
TokenUser,
buffer.as_mut_ptr() as *mut c_void,
length,
&mut length,
) == 0
{
return Err(Error::last_os_error());
}
let user = &*(buffer.as_ptr() as *const TOKEN_USER);
sid_to_string(user.User.Sid)
}
}
unsafe fn sid_to_string(sid: *mut c_void) -> IoResult<String> {
let mut wide: *mut u16 = null_mut();
if ConvertSidToStringSidW(sid, &mut wide) == 0 {
return Err(Error::last_os_error());
}
let mut length = 0;
while *wide.add(length) != 0 {
length += 1;
}
let value = String::from_utf16_lossy(std::slice::from_raw_parts(wide, length));
LocalFree(wide as *mut c_void);
Ok(value)
}
fn tcp_connections() -> IoResult<Vec<TcpConnection>> {
let mut size = 0u32;
for _ in 0..TABLE_ATTEMPTS {
let mut buffer = vec![0u32; (size as usize).div_ceil(4)];
let pointer = if buffer.is_empty() {
null_mut()
} else {
buffer.as_mut_ptr() as *mut c_void
};
let status = unsafe {
GetExtendedTcpTable(pointer, &mut size, 0, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0)
};
match status {
NO_ERROR => return Ok(read_table(&buffer)),
ERROR_INSUFFICIENT_BUFFER => continue,
other => return Err(Error::from_raw_os_error(other as i32)),
}
}
Err(Error::other("the TCP table kept growing"))
}
fn read_table(buffer: &[u32]) -> Vec<TcpConnection> {
// SAFETY: a successful call left `dwNumEntries` rows in the buffer it was
// sized for.
unsafe {
let table = &*(buffer.as_ptr() as *const MIB_TCPTABLE_OWNER_PID);
std::slice::from_raw_parts(table.table.as_ptr(), table.dwNumEntries as usize)
.iter()
.map(|row| TcpConnection {
local: socket_address(row.dwLocalAddr, row.dwLocalPort),
remote: socket_address(row.dwRemoteAddr, row.dwRemotePort),
pid: row.dwOwningPid,
})
.collect()
}
}
/// Network byte order, with the port in the low 16 bits.
fn socket_address(address: u32, port: u32) -> SocketAddrV4 {
SocketAddrV4::new(
Ipv4Addr::from(u32::from_be(address)),
u16::from_be(port as u16),
)
}
fn is_owner_connection(stream: &TcpStream, owner_sid: &str) -> bool {
let (Ok(SocketAddr::V4(client)), Ok(SocketAddr::V4(server))) =
(stream.peer_addr(), stream.local_addr())
else {
return false;
};
let Ok(connections) = tcp_connections() else {
return false;
};
let Some(pid) = client_pid(&connections, client, server) else {
return false;
};
matches!(process_sid(pid), Ok(sid) if same_sid(&sid, owner_sid))
}
/// Loopback TCP has no peer credential, so the TCP table names the process
/// holding the client end, and that process must run as the account the Helper
/// was installed for.
struct AuthorizedIncoming {
listener: TcpListener,
owner_sid: String,
retry: Option<Pin<Box<Sleep>>>,
}
impl Stream for AuthorizedIncoming {
type Item = IoResult<TcpStream>;
/// hyper ends the whole server on an accept error, so retry after a pause.
fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
if let Some(retry) = self.retry.as_mut() {
match retry.as_mut().poll(context) {
Poll::Ready(()) => self.retry = None,
Poll::Pending => return Poll::Pending,
}
}
match self.listener.poll_accept(context) {
Poll::Ready(Ok((stream, _))) => {
if is_owner_connection(&stream, &self.owner_sid) {
return Poll::Ready(Some(Ok(stream)));
}
if !REJECTION_LOGGED.swap(true, Ordering::Relaxed) {
log_message(
"Helper refused a connection from an account other than its owner"
.to_string(),
);
}
}
Poll::Ready(Err(error)) => {
log_message(format!("Helper could not accept a connection: {error}"));
self.retry = Some(Box::pin(tokio::time::sleep(ACCEPT_RETRY_DELAY)));
}
Poll::Pending => return Poll::Pending,
}
}
}
}
fn wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
struct PublishedPort;
impl PublishedPort {
fn publish(port: u16) -> IoResult<Self> {
let key_path = wide(PORT_KEY);
let value_name = wide(PORT_VALUE);
let data = u32::from(port).to_le_bytes();
// SAFETY: the buffers outlive the calls, and the opened key is closed.
let status = unsafe {
let mut key: HKEY = 0;
let status = RegCreateKeyExW(
HKEY_LOCAL_MACHINE,
key_path.as_ptr(),
0,
null(),
REG_OPTION_VOLATILE,
KEY_SET_VALUE,
null(),
&mut key,
null_mut(),
);
if status != ERROR_SUCCESS {
return Err(Error::from_raw_os_error(status as i32));
}
let status = RegSetValueExW(
key,
value_name.as_ptr(),
0,
REG_DWORD,
data.as_ptr(),
data.len() as u32,
);
RegCloseKey(key);
status
};
if status != ERROR_SUCCESS {
return Err(Error::from_raw_os_error(status as i32));
}
Ok(Self)
}
}
impl Drop for PublishedPort {
fn drop(&mut self) {
let key_path = wide(PORT_KEY);
unsafe {
RegDeleteKeyW(HKEY_LOCAL_MACHINE, key_path.as_ptr());
}
}
}
pub(super) async fn serve_until<F, S>(
owner_sid: String,
shutdown: F,
on_started: S,
) -> anyhow::Result<()>
where
F: Future<Output = ()> + Send + 'static,
S: FnOnce() -> anyhow::Result<()>,
{
ensure_core_sha256_configured()?;
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
.await
.map_err(|error| anyhow::anyhow!("bind helper server: {error}"))?;
let port = listener
.local_addr()
.map_err(|error| anyhow::anyhow!("read helper server port: {error}"))?
.port();
let published = PublishedPort::publish(port)
.map_err(|error| anyhow::anyhow!("publish helper server port: {error}"))?;
on_started()?;
let incoming = AuthorizedIncoming {
listener,
owner_sid,
retry: None,
};
warp::serve(routes())
.serve_incoming_with_graceful_shutdown(incoming, shutdown)
.await;
drop(published);
release_managed_core_on_shutdown();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn v4(address: SocketAddr) -> SocketAddrV4 {
match address {
SocketAddr::V4(address) => address,
SocketAddr::V6(_) => panic!("expected an IPv4 address"),
}
}
#[test]
fn the_tcp_table_names_this_process_on_its_own_loopback_connection() {
let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let server = v4(listener.local_addr().unwrap());
let client = std::net::TcpStream::connect(server).unwrap();
let _accepted = listener.accept().unwrap();
let client = v4(client.local_addr().unwrap());
let connections = tcp_connections().unwrap();
assert_eq!(
client_pid(&connections, client, server),
Some(std::process::id())
);
}
#[test]
fn reads_the_account_of_a_process() {
let sid = process_sid(std::process::id()).unwrap();
assert!(sid.starts_with("S-1-5-"), "{sid}");
assert!(process_sid(u32::MAX - 3).is_err());
}
#[tokio::test]
async fn admits_the_owner_and_refuses_any_other_account() {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let _client = std::net::TcpStream::connect(listener.local_addr().unwrap()).unwrap();
let (accepted, _) = listener.accept().await.unwrap();
let owner = process_sid(std::process::id()).unwrap();
assert!(is_owner_connection(&accepted, &owner));
assert!(!is_owner_connection(&accepted, "S-1-5-21-1-2-3-1001"));
}
}