mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
fix: add CONNMARK restore-mark to mangle OUTPUT chain
The CONNMARK --restore-mark rule was only in PREROUTING, which handles forwarded packets. Locally-bound listeners (e.g. vhost) generate replies through the OUTPUT chain, where the fwmark was never restored. This caused response packets to route via the default table instead of back through the originating interface.
This commit is contained in:
@@ -13,6 +13,7 @@ use openssl::hash::MessageDigest;
|
|||||||
use patch_db::{HasModel, Value};
|
use patch_db::{HasModel, Value};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::account::AccountInfo;
|
use crate::account::AccountInfo;
|
||||||
use crate::db::DbAccessByKey;
|
use crate::db::DbAccessByKey;
|
||||||
@@ -143,6 +144,7 @@ impl Public {
|
|||||||
zram: true,
|
zram: true,
|
||||||
governor: None,
|
governor: None,
|
||||||
smtp: None,
|
smtp: None,
|
||||||
|
ifconfig_url: default_ifconfig_url(),
|
||||||
ram: 0,
|
ram: 0,
|
||||||
devices: Vec::new(),
|
devices: Vec::new(),
|
||||||
kiosk,
|
kiosk,
|
||||||
@@ -164,6 +166,10 @@ fn get_platform() -> InternedString {
|
|||||||
(&*PLATFORM).into()
|
(&*PLATFORM).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_ifconfig_url() -> Url {
|
||||||
|
"https://ifconfig.co".parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
@@ -200,6 +206,9 @@ pub struct ServerInfo {
|
|||||||
pub zram: bool,
|
pub zram: bool,
|
||||||
pub governor: Option<Governor>,
|
pub governor: Option<Governor>,
|
||||||
pub smtp: Option<SmtpValue>,
|
pub smtp: Option<SmtpValue>,
|
||||||
|
#[serde(default = "default_ifconfig_url")]
|
||||||
|
#[ts(type = "string")]
|
||||||
|
pub ifconfig_url: Url,
|
||||||
#[ts(type = "number")]
|
#[ts(type = "number")]
|
||||||
pub ram: u64,
|
pub ram: u64,
|
||||||
pub devices: Vec<LshwDevice>,
|
pub devices: Vec<LshwDevice>,
|
||||||
|
|||||||
@@ -377,6 +377,13 @@ pub fn server<C: Context>() -> ParentHandler<C> {
|
|||||||
"host",
|
"host",
|
||||||
net::host::server_host_api::<C>().with_about("about.commands-host-system-ui"),
|
net::host::server_host_api::<C>().with_about("about.commands-host-system-ui"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"set-ifconfig-url",
|
||||||
|
from_fn_async(system::set_ifconfig_url)
|
||||||
|
.no_display()
|
||||||
|
.with_about("about.set-ifconfig-url")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"set-keyboard",
|
"set-keyboard",
|
||||||
from_fn_async(system::set_keyboard)
|
from_fn_async(system::set_keyboard)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use tokio::net::TcpListener;
|
|||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
use url::Url;
|
||||||
use visit_rs::{Visit, VisitFields};
|
use visit_rs::{Visit, VisitFields};
|
||||||
use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream};
|
use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream};
|
||||||
use zbus::zvariant::{
|
use zbus::zvariant::{
|
||||||
@@ -110,6 +111,13 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_about("about.rename-gateway")
|
.with_about("about.rename-gateway")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"check-port",
|
||||||
|
from_fn_async(check_port)
|
||||||
|
.with_display_serializable()
|
||||||
|
.with_about("about.check-port-reachability")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_interfaces(
|
async fn list_interfaces(
|
||||||
@@ -148,6 +156,73 @@ async fn set_name(
|
|||||||
ctx.net_controller.net_iface.set_name(&id, name).await
|
ctx.net_controller.net_iface.set_name(&id, name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
struct CheckPortParams {
|
||||||
|
#[arg(help = "help.arg.port")]
|
||||||
|
port: u16,
|
||||||
|
#[arg(help = "help.arg.gateway-id")]
|
||||||
|
gateway: GatewayId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct CheckPortRes {
|
||||||
|
pub ip: Ipv4Addr,
|
||||||
|
pub port: u16,
|
||||||
|
pub reachable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_port(
|
||||||
|
ctx: RpcContext,
|
||||||
|
CheckPortParams { port, gateway }: CheckPortParams,
|
||||||
|
) -> Result<CheckPortRes, Error> {
|
||||||
|
let db = ctx.db.peek().await;
|
||||||
|
let base_url = db
|
||||||
|
.as_public()
|
||||||
|
.as_server_info()
|
||||||
|
.as_ifconfig_url()
|
||||||
|
.de()?;
|
||||||
|
let gateways = db
|
||||||
|
.as_public()
|
||||||
|
.as_server_info()
|
||||||
|
.as_network()
|
||||||
|
.as_gateways()
|
||||||
|
.de()?;
|
||||||
|
let gw_info = gateways.get(&gateway).ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
eyre!("unknown gateway: {gateway}"),
|
||||||
|
ErrorKind::NotFound,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let ip_info = gw_info.ip_info.as_ref().ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
eyre!("gateway {gateway} has no IP info"),
|
||||||
|
ErrorKind::NotFound,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let iface = &*ip_info.name;
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder();
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let client = client.interface(iface);
|
||||||
|
let url = base_url
|
||||||
|
.join(&format!("/port/{port}"))
|
||||||
|
.with_kind(ErrorKind::ParseUrl)?;
|
||||||
|
let res: CheckPortRes = client
|
||||||
|
.build()?
|
||||||
|
.get(url)
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
#[proxy(
|
#[proxy(
|
||||||
interface = "org.freedesktop.NetworkManager",
|
interface = "org.freedesktop.NetworkManager",
|
||||||
default_service = "org.freedesktop.NetworkManager",
|
default_service = "org.freedesktop.NetworkManager",
|
||||||
@@ -371,6 +446,7 @@ impl<'a> StubStream<'a> for SignalStream<'a> {
|
|||||||
async fn watcher(
|
async fn watcher(
|
||||||
watch_ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
watch_ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||||
watch_activation: Watch<BTreeMap<GatewayId, bool>>,
|
watch_activation: Watch<BTreeMap<GatewayId, bool>>,
|
||||||
|
db: Option<TypedPatchDb<Database>>,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
let res: Result<(), Error> = async {
|
let res: Result<(), Error> = async {
|
||||||
@@ -444,6 +520,7 @@ async fn watcher(
|
|||||||
device_proxy.clone(),
|
device_proxy.clone(),
|
||||||
iface.clone(),
|
iface.clone(),
|
||||||
&watch_ip_info,
|
&watch_ip_info,
|
||||||
|
db.as_ref(),
|
||||||
)));
|
)));
|
||||||
ifaces.insert(iface);
|
ifaces.insert(iface);
|
||||||
}
|
}
|
||||||
@@ -474,33 +551,34 @@ async fn watcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_wan_ipv4(iface: &str) -> Result<Option<Ipv4Addr>, Error> {
|
async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, Error> {
|
||||||
let client = reqwest::Client::builder();
|
let client = reqwest::Client::builder();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let client = client.interface(iface);
|
let client = client.interface(iface);
|
||||||
Ok(client
|
let url = base_url.join("/ip").with_kind(ErrorKind::ParseUrl)?;
|
||||||
|
let text = client
|
||||||
.build()?
|
.build()?
|
||||||
.get("https://ip4only.me/api/")
|
.get(url)
|
||||||
.timeout(Duration::from_secs(10))
|
.timeout(Duration::from_secs(10))
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.error_for_status()?
|
.error_for_status()?
|
||||||
.text()
|
.text()
|
||||||
.await?
|
.await?;
|
||||||
.split(",")
|
let trimmed = text.trim();
|
||||||
.skip(1)
|
if trimmed.is_empty() {
|
||||||
.next()
|
return Ok(None);
|
||||||
.filter(|s| !s.is_empty())
|
}
|
||||||
.map(|s| s.parse())
|
Ok(Some(trimmed.parse()?))
|
||||||
.transpose()?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(connection, device_proxy, write_to))]
|
#[instrument(skip(connection, device_proxy, write_to, db))]
|
||||||
async fn watch_ip(
|
async fn watch_ip(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
device_proxy: device::DeviceProxy<'_>,
|
device_proxy: device::DeviceProxy<'_>,
|
||||||
iface: GatewayId,
|
iface: GatewayId,
|
||||||
write_to: &Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
write_to: &Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||||
|
db: Option<&TypedPatchDb<Database>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut until = Until::new()
|
let mut until = Until::new()
|
||||||
.with_stream(
|
.with_stream(
|
||||||
@@ -761,24 +839,28 @@ async fn watch_ip(
|
|||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure global CONNMARK restore rule in mangle PREROUTING
|
// Ensure global CONNMARK restore rules in mangle
|
||||||
// (restores fwmark from conntrack mark on reply packets)
|
// PREROUTING (forwarded packets) and OUTPUT (locally-generated replies).
|
||||||
if !Command::new("iptables")
|
// Both are needed: PREROUTING handles DNAT-forwarded traffic,
|
||||||
.arg("-t").arg("mangle")
|
// OUTPUT handles replies from locally-bound listeners (e.g. vhost).
|
||||||
.arg("-C").arg("PREROUTING")
|
for chain in ["PREROUTING", "OUTPUT"] {
|
||||||
.arg("-j").arg("CONNMARK")
|
if !Command::new("iptables")
|
||||||
.arg("--restore-mark")
|
|
||||||
.status().await
|
|
||||||
.map_or(false, |s| s.success())
|
|
||||||
{
|
|
||||||
Command::new("iptables")
|
|
||||||
.arg("-t").arg("mangle")
|
.arg("-t").arg("mangle")
|
||||||
.arg("-I").arg("PREROUTING").arg("1")
|
.arg("-C").arg(chain)
|
||||||
.arg("-j").arg("CONNMARK")
|
.arg("-j").arg("CONNMARK")
|
||||||
.arg("--restore-mark")
|
.arg("--restore-mark")
|
||||||
.invoke(ErrorKind::Network)
|
.status().await
|
||||||
.await
|
.map_or(false, |s| s.success())
|
||||||
.log_err();
|
{
|
||||||
|
Command::new("iptables")
|
||||||
|
.arg("-t").arg("mangle")
|
||||||
|
.arg("-I").arg(chain).arg("1")
|
||||||
|
.arg("-j").arg("CONNMARK")
|
||||||
|
.arg("--restore-mark")
|
||||||
|
.invoke(ErrorKind::Network)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark NEW connections arriving on this interface
|
// Mark NEW connections arriving on this interface
|
||||||
@@ -827,6 +909,17 @@ async fn watch_ip(
|
|||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let ifconfig_url = if let Some(db) = db {
|
||||||
|
db.peek()
|
||||||
|
.await
|
||||||
|
.as_public()
|
||||||
|
.as_server_info()
|
||||||
|
.as_ifconfig_url()
|
||||||
|
.de()
|
||||||
|
.unwrap_or_else(|_| crate::db::model::public::default_ifconfig_url())
|
||||||
|
} else {
|
||||||
|
crate::db::model::public::default_ifconfig_url()
|
||||||
|
};
|
||||||
let wan_ip = if !subnets.is_empty()
|
let wan_ip = if !subnets.is_empty()
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
device_type,
|
device_type,
|
||||||
@@ -835,7 +928,7 @@ async fn watch_ip(
|
|||||||
| NetworkInterfaceType::Loopback
|
| NetworkInterfaceType::Loopback
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
match get_wan_ipv4(iface.as_str()).await {
|
match get_wan_ipv4(iface.as_str(), &ifconfig_url).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
@@ -947,6 +1040,7 @@ impl NetworkInterfaceWatcher {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
seed: impl Future<Output = OrdMap<GatewayId, NetworkInterfaceInfo>> + Send + Sync + 'static,
|
seed: impl Future<Output = OrdMap<GatewayId, NetworkInterfaceInfo>> + Send + Sync + 'static,
|
||||||
watch_activated: impl IntoIterator<Item = GatewayId>,
|
watch_activated: impl IntoIterator<Item = GatewayId>,
|
||||||
|
db: TypedPatchDb<Database>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ip_info = Watch::new(OrdMap::new());
|
let ip_info = Watch::new(OrdMap::new());
|
||||||
let activated = Watch::new(watch_activated.into_iter().map(|k| (k, false)).collect());
|
let activated = Watch::new(watch_activated.into_iter().map(|k| (k, false)).collect());
|
||||||
@@ -958,7 +1052,7 @@ impl NetworkInterfaceWatcher {
|
|||||||
if !seed.is_empty() {
|
if !seed.is_empty() {
|
||||||
ip_info.send_replace(seed);
|
ip_info.send_replace(seed);
|
||||||
}
|
}
|
||||||
watcher(ip_info, activated).await
|
watcher(ip_info, activated, Some(db)).await
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
}
|
}
|
||||||
@@ -1105,6 +1199,7 @@ impl NetworkInterfaceController {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[InternedString::from_static(START9_BRIDGE_IFACE).into()],
|
[InternedString::from_static(START9_BRIDGE_IFACE).into()],
|
||||||
|
db.clone(),
|
||||||
);
|
);
|
||||||
let mut ip_info_watch = watcher.subscribe();
|
let mut ip_info_watch = watcher.subscribe();
|
||||||
ip_info_watch.mark_seen();
|
ip_info_watch.mark_seen();
|
||||||
@@ -1316,7 +1411,7 @@ impl WildcardListener {
|
|||||||
.with_kind(ErrorKind::Network)?;
|
.with_kind(ErrorKind::Network)?;
|
||||||
let ip_info = Watch::new(OrdMap::new());
|
let ip_info = Watch::new(OrdMap::new());
|
||||||
let watcher_handle =
|
let watcher_handle =
|
||||||
tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into();
|
tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()), None)).into();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
listener,
|
listener,
|
||||||
ip_info,
|
ip_info,
|
||||||
|
|||||||
@@ -1095,6 +1095,28 @@ pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser)]
|
||||||
|
pub struct SetIfconfigUrlParams {
|
||||||
|
#[arg(help = "help.arg.ifconfig-url")]
|
||||||
|
pub url: url::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_ifconfig_url(
|
||||||
|
ctx: RpcContext,
|
||||||
|
SetIfconfigUrlParams { url }: SetIfconfigUrlParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
ctx.db
|
||||||
|
.mutate(|db| {
|
||||||
|
db.as_public_mut()
|
||||||
|
.as_server_info_mut()
|
||||||
|
.as_ifconfig_url_mut()
|
||||||
|
.ser(&url)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@@ -268,7 +268,8 @@ export class InterfaceAddressesComponent {
|
|||||||
const [network, portPass] = await Promise.all([
|
const [network, portPass] = await Promise.all([
|
||||||
firstValueFrom(this.patch.watch$('serverInfo', 'network')),
|
firstValueFrom(this.patch.watch$('serverInfo', 'network')),
|
||||||
this.api
|
this.api
|
||||||
.testPortForward({ gateway: gatewayId, port: 443 })
|
.checkPort({ gateway: gatewayId, port: 443 })
|
||||||
|
.then(r => r.reachable)
|
||||||
.catch(() => false),
|
.catch(() => false),
|
||||||
])
|
])
|
||||||
const gateway = network.gateways[gatewayId]
|
const gateway = network.gateways[gatewayId]
|
||||||
|
|||||||
@@ -250,12 +250,12 @@ export class DomainValidationComponent {
|
|||||||
this.portLoading.set(true)
|
this.portLoading.set(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.api.testPortForward({
|
const result = await this.api.checkPort({
|
||||||
gateway: this.context.data.gateway.id,
|
gateway: this.context.data.gateway.id,
|
||||||
port: this.context.data.port,
|
port: this.context.data.port,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.portPass.set(result)
|
this.portPass.set(result.reachable)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -128,10 +128,9 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract queryDns(params: T.QueryDnsParams): Promise<string | null>
|
abstract queryDns(params: T.QueryDnsParams): Promise<string | null>
|
||||||
|
|
||||||
abstract testPortForward(params: {
|
abstract checkPort(
|
||||||
gateway: string
|
params: T.CheckPortParams,
|
||||||
port: number
|
): Promise<T.CheckPortRes>
|
||||||
}): Promise<boolean>
|
|
||||||
|
|
||||||
// smtp
|
// smtp
|
||||||
|
|
||||||
|
|||||||
@@ -276,10 +276,9 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async testPortForward(params: {
|
async checkPort(
|
||||||
gateway: string
|
params: T.CheckPortParams,
|
||||||
port: number
|
): Promise<T.CheckPortRes> {
|
||||||
}): Promise<boolean> {
|
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'net.gateway.check-port',
|
method: 'net.gateway.check-port',
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -497,13 +497,12 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async testPortForward(params: {
|
async checkPort(
|
||||||
gateway: string
|
params: T.CheckPortParams,
|
||||||
port: number
|
): Promise<T.CheckPortRes> {
|
||||||
}): Promise<boolean> {
|
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
return false
|
return { ip: '0.0.0.0', port: params.port, reachable: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// marketplace URLs
|
// marketplace URLs
|
||||||
|
|||||||
Reference in New Issue
Block a user