mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
wip: debugging tor
This commit is contained in:
@@ -40,6 +40,8 @@ sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service
|
||||
sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service
|
||||
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service
|
||||
sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-container
|
||||
echo -e '#!/bin/bash\nexec start-container $@' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove
|
||||
sudo chmod +x tmp/combined/usr/bin/start-cli
|
||||
sudo chown 0:0 tmp/combined/usr/bin/start-container
|
||||
echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id
|
||||
cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash
|
||||
|
||||
738
core/Cargo.lock
generated
738
core/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
source ./core/builder-alias.sh
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
|
||||
@@ -26,7 +26,7 @@ if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
source ./core/builder-alias.sh
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
|
||||
@@ -31,7 +31,7 @@ if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
source ./core/builder-alias.sh
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
|
||||
@@ -26,7 +26,7 @@ if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
source ./core/builder-alias.sh
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
|
||||
@@ -26,7 +26,7 @@ if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
source ./core/builder-alias.sh
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
|
||||
3
core/builder-alias.sh
Normal file
3
core/builder-alias.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -e SCCACHE_GHA_ENABLED -e SCCACHE_GHA_VERSION -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$HOME/.cache/sccache":/root/.cache/sccache -v "$(pwd)":/home/rust/src -w /home/rust/src -P start9/rust-musl-cross:$ARCH-musl'
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
arti-client = { version = "0.33", features = ["full"] }
|
||||
arti-client = { version = "0.33", default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
axum = "0.8.4"
|
||||
base64 = "0.22.1"
|
||||
color-eyre = "0.6.2"
|
||||
|
||||
@@ -64,7 +64,9 @@ arti-client = { version = "0.33", features = [
|
||||
"static",
|
||||
"tokio",
|
||||
"ephemeral-keystore",
|
||||
], default-features = false }
|
||||
"onion-service-client",
|
||||
"onion-service-service",
|
||||
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
aes = { version = "0.7.5", features = ["ctr"] }
|
||||
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
|
||||
"use_rustls",
|
||||
@@ -195,7 +197,7 @@ rpassword = "7.2.0"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
|
||||
rust-argon2 = "2.0.0"
|
||||
rustyline-async = "0.4.1"
|
||||
safelog = "0.4.8"
|
||||
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
semver = { version = "1.0.20", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_cbor = { package = "ciborium", version = "0.2.1" }
|
||||
@@ -227,13 +229,22 @@ tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }
|
||||
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
|
||||
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
|
||||
tokio-util = { version = "0.7.9", features = ["io"] }
|
||||
tor-cell = { version = "0.33" }
|
||||
tor-hscrypto = { version = "0.33", features = ["full"] }
|
||||
tor-hsservice = { version = "0.33" }
|
||||
tor-keymgr = { version = "0.33", features = ["ephemeral-keystore"] }
|
||||
tor-llcrypto = { version = "0.33", features = ["full"] }
|
||||
tor-proto = { version = "0.33" }
|
||||
tor-rtcompat = { version = "0.33", features = ["tokio", "rustls"] }
|
||||
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-hscrypto = { version = "0.33", features = [
|
||||
"full",
|
||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-keymgr = { version = "0.33", features = [
|
||||
"ephemeral-keystore",
|
||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-llcrypto = { version = "0.33", features = [
|
||||
"full",
|
||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-rtcompat = { version = "0.33", features = [
|
||||
"tokio",
|
||||
"rustls",
|
||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tower-service = "0.3.3"
|
||||
tracing = "0.1.39"
|
||||
tracing-error = "0.2.0"
|
||||
|
||||
@@ -18,7 +18,7 @@ use models::{ActionId, PackageId};
|
||||
use reqwest::{Client, Proxy};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{CallRemote, Context, Empty};
|
||||
use tokio::sync::{Mutex, RwLock, broadcast, oneshot, watch};
|
||||
use tokio::sync::{RwLock, broadcast, oneshot, watch};
|
||||
use tokio::time::Instant;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::db::model::package::TaskSeverity;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::init::{InitResult, check_time_is_synchronized};
|
||||
use crate::install::PKG_ARCHIVE_DIR;
|
||||
use crate::lxc::{ContainerId, LxcContainer, LxcManager};
|
||||
use crate::lxc::LxcManager;
|
||||
use crate::net::net_controller::{NetController, NetService};
|
||||
use crate::net::utils::{find_eth_iface, find_wifi_iface};
|
||||
use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter};
|
||||
@@ -74,12 +74,6 @@ pub struct RpcContextSeed {
|
||||
pub client: Client,
|
||||
pub start_time: Instant,
|
||||
pub crons: SyncMutex<BTreeMap<Guid, NonDetachingJoinHandle<()>>>,
|
||||
// #[cfg(feature = "dev")]
|
||||
pub dev: Dev,
|
||||
}
|
||||
|
||||
pub struct Dev {
|
||||
pub lxc: Mutex<BTreeMap<ContainerId, LxcContainer>>,
|
||||
}
|
||||
|
||||
pub struct Hardware {
|
||||
@@ -262,10 +256,6 @@ impl RpcContext {
|
||||
.with_kind(crate::ErrorKind::ParseUrl)?,
|
||||
start_time: Instant::now(),
|
||||
crons,
|
||||
// #[cfg(feature = "dev")]
|
||||
dev: Dev {
|
||||
lxc: Mutex::new(BTreeMap::new()),
|
||||
},
|
||||
});
|
||||
|
||||
let res = Self(seed.clone());
|
||||
|
||||
@@ -19,8 +19,8 @@ use crate::account::AccountInfo;
|
||||
use crate::db::model::package::AllPackageData;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo};
|
||||
use crate::net::host::Host;
|
||||
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo};
|
||||
use crate::net::utils::ipv6_is_local;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -10,34 +10,34 @@ use futures::future::BoxFuture;
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use hickory_client::client::Client;
|
||||
use hickory_client::proto::DnsHandle;
|
||||
use hickory_client::proto::runtime::TokioRuntimeProvider;
|
||||
use hickory_client::proto::tcp::TcpClientStream;
|
||||
use hickory_client::proto::udp::UdpClientStream;
|
||||
use hickory_client::proto::xfer::{DnsExchangeBackground, DnsRequestOptions};
|
||||
use hickory_client::proto::DnsHandle;
|
||||
use hickory_server::ServerFuture;
|
||||
use hickory_server::authority::MessageResponseBuilder;
|
||||
use hickory_server::proto::op::{Header, ResponseCode};
|
||||
use hickory_server::proto::rr::{Name, Record, RecordType};
|
||||
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
|
||||
use hickory_server::ServerFuture;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use models::{GatewayId, OptionExt, PackageId};
|
||||
use rpc_toolkit::{
|
||||
from_fn_async, from_fn_blocking, Context, HandlerArgs, HandlerExt, ParentHandler,
|
||||
Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async, from_fn_blocking,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::{TcpListener, UdpSocket};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::db::model::Database;
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::net::gateway::NetworkInterfaceWatcher;
|
||||
use crate::prelude::*;
|
||||
use crate::util::io::file_string_stream;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
use crate::util::sync::{SyncRwLock, Watch};
|
||||
|
||||
pub fn dns_api<C: Context>() -> ParentHandler<C> {
|
||||
@@ -349,11 +349,7 @@ impl RequestHandler for Resolver {
|
||||
Header::response_from_request(request.header()),
|
||||
&ip.into_iter()
|
||||
.filter_map(|a| {
|
||||
if let IpAddr::V4(a) = a {
|
||||
Some(a)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if let IpAddr::V4(a) = a { Some(a) } else { None }
|
||||
})
|
||||
.map(|ip| {
|
||||
Record::from_rdata(
|
||||
@@ -377,11 +373,7 @@ impl RequestHandler for Resolver {
|
||||
Header::response_from_request(request.header()),
|
||||
&ip.into_iter()
|
||||
.filter_map(|a| {
|
||||
if let IpAddr::V6(a) = a {
|
||||
Some(a)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if let IpAddr::V6(a) = a { Some(a) } else { None }
|
||||
})
|
||||
.map(|ip| {
|
||||
Record::from_rdata(
|
||||
@@ -415,9 +407,7 @@ impl RequestHandler for Resolver {
|
||||
}
|
||||
} else {
|
||||
let query = query.original().clone();
|
||||
let mut streams = self
|
||||
.client
|
||||
.lookup(query, DnsRequestOptions::default());
|
||||
let mut streams = self.client.lookup(query, DnsRequestOptions::default());
|
||||
let mut err = None;
|
||||
for stream in streams.iter_mut() {
|
||||
match tokio::time::timeout(Duration::from_secs(5), stream.next()).await {
|
||||
|
||||
@@ -16,7 +16,7 @@ use itertools::Itertools;
|
||||
use models::GatewayId;
|
||||
use nix::net::if_::if_nametoindex;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
@@ -24,25 +24,25 @@ use tokio::process::Command;
|
||||
use ts_rs::TS;
|
||||
use zbus::proxy::{PropertyChanged, PropertyStream, SignalStream};
|
||||
use zbus::zvariant::{
|
||||
DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType, Value as ZValue,
|
||||
DICT_ENTRY_SIG_END_STR,
|
||||
DICT_ENTRY_SIG_END_STR, DeserializeDict, Dict, OwnedObjectPath, OwnedValue, Type as ZType,
|
||||
Value as ZValue,
|
||||
};
|
||||
use zbus::{proxy, Connection};
|
||||
use zbus::{Connection, proxy};
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||
use crate::db::model::Database;
|
||||
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||
use crate::net::gateway::device::DeviceProxy;
|
||||
use crate::net::utils::ipv6_is_link_local;
|
||||
use crate::net::web_server::Accept;
|
||||
use crate::prelude::*;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::collections::OrdMapIterMut;
|
||||
use crate::util::future::Until;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
use crate::util::sync::{SyncMutex, Watch};
|
||||
use crate::util::Invoke;
|
||||
|
||||
pub fn gateway_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use models::GatewayId;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
use crate::net::host::{all_hosts, HostApiKind};
|
||||
use crate::net::host::{HostApiKind, all_hosts};
|
||||
use crate::net::tor::OnionAddress;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -35,10 +36,10 @@ pub struct PublicDomainConfig {
|
||||
pub acme: Option<AcmeProvider>,
|
||||
}
|
||||
|
||||
fn check_duplicates(db: &DatabaseModel) -> Result<(), Error> {
|
||||
fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
||||
let mut onions = BTreeSet::<OnionAddress>::new();
|
||||
let mut domains = BTreeSet::<InternedString>::new();
|
||||
let mut check_onion = |onion: OnionAddress| {
|
||||
let check_onion = |onions: &mut BTreeSet<OnionAddress>, onion: OnionAddress| {
|
||||
if onions.contains(&onion) {
|
||||
return Err(Error::new(
|
||||
eyre!("onion address {onion} is already in use"),
|
||||
@@ -48,7 +49,7 @@ fn check_duplicates(db: &DatabaseModel) -> Result<(), Error> {
|
||||
onions.insert(onion);
|
||||
Ok(())
|
||||
};
|
||||
let mut check_domain = |domain: InternedString| {
|
||||
let check_domain = |domains: &mut BTreeSet<InternedString>, domain: InternedString| {
|
||||
if domains.contains(&domain) {
|
||||
return Err(Error::new(
|
||||
eyre!("domain {domain} is already in use"),
|
||||
@@ -58,23 +59,47 @@ fn check_duplicates(db: &DatabaseModel) -> Result<(), Error> {
|
||||
domains.insert(domain);
|
||||
Ok(())
|
||||
};
|
||||
let mut not_in_use = Vec::new();
|
||||
for host in all_hosts(db) {
|
||||
let host = host?;
|
||||
let in_use = host.as_bindings().de()?.values().any(|v| v.enabled);
|
||||
if !in_use {
|
||||
not_in_use.push(host);
|
||||
continue;
|
||||
}
|
||||
for onion in host.as_onions().de()? {
|
||||
check_onion(onion)?;
|
||||
check_onion(&mut onions, onion)?;
|
||||
}
|
||||
for domain in host.as_public_domains().keys()? {
|
||||
check_domain(domain)?;
|
||||
check_domain(&mut domains, domain)?;
|
||||
}
|
||||
for domain in host.as_private_domains().de()? {
|
||||
check_domain(domain)?;
|
||||
check_domain(&mut domains, domain)?;
|
||||
}
|
||||
}
|
||||
for host in not_in_use {
|
||||
host.as_onions_mut()
|
||||
.mutate(|o| Ok(o.retain(|o| !onions.contains(o))))?;
|
||||
host.as_public_domains_mut()
|
||||
.mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
|
||||
host.as_private_domains_mut()
|
||||
.mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?;
|
||||
|
||||
for onion in host.as_onions().de()? {
|
||||
check_onion(&mut onions, onion)?;
|
||||
}
|
||||
for domain in host.as_public_domains().keys()? {
|
||||
check_domain(&mut domains, domain)?;
|
||||
}
|
||||
for domain in host.as_private_domains().de()? {
|
||||
check_domain(&mut domains, domain)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn address_api<C: Context, Kind: HostApiKind>(
|
||||
) -> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
|
||||
pub fn address_api<C: Context, Kind: HostApiKind>()
|
||||
-> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
|
||||
ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new()
|
||||
.subcommand(
|
||||
"domain",
|
||||
@@ -209,12 +234,12 @@ pub struct AddPublicDomainParams {
|
||||
pub async fn add_public_domain<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
AddPublicDomainParams {
|
||||
ref fqdn,
|
||||
fqdn,
|
||||
acme,
|
||||
gateway,
|
||||
}: AddPublicDomainParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Option<Ipv4Addr>, Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if let Some(acme) = &acme {
|
||||
@@ -231,31 +256,35 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
||||
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_public_domains_mut()
|
||||
.insert(fqdn, &PublicDomainConfig { acme, gateway })?;
|
||||
check_duplicates(db)
|
||||
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
|
||||
handle_duplicates(db)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
tokio::task::spawn_blocking(|| {
|
||||
crate::net::dns::query_dns(ctx, crate::net::dns::QueryDnsParams { fqdn })
|
||||
})
|
||||
.await
|
||||
.with_kind(ErrorKind::Unknown)?
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct RemoveDomainParams {
|
||||
pub domain: InternedString,
|
||||
pub fqdn: InternedString,
|
||||
}
|
||||
|
||||
pub async fn remove_public_domain<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
RemoveDomainParams { domain }: RemoveDomainParams,
|
||||
RemoveDomainParams { fqdn }: RemoveDomainParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_public_domains_mut()
|
||||
.remove(&domain)
|
||||
.remove(&fqdn)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
@@ -279,7 +308,7 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_private_domains_mut()
|
||||
.mutate(|d| Ok(d.insert(fqdn)))?;
|
||||
check_duplicates(db)
|
||||
handle_duplicates(db)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
@@ -290,7 +319,7 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
||||
|
||||
pub async fn remove_private_domain<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
RemoveDomainParams { domain }: RemoveDomainParams,
|
||||
RemoveDomainParams { fqdn: domain }: RemoveDomainParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
@@ -332,7 +361,7 @@ pub async fn add_onion<Kind: HostApiKind>(
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_onions_mut()
|
||||
.mutate(|a| Ok(a.insert(onion)))?;
|
||||
check_duplicates(db)
|
||||
handle_duplicates(db)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use clap::Parser;
|
||||
use clap::builder::ValueParserFactory;
|
||||
use imbl::OrdSet;
|
||||
use models::{FromStrParser, GatewayId, HostId};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler};
|
||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::net::gateway::InterfaceFilter;
|
||||
use crate::net::host::HostApiKind;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde};
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
@@ -169,8 +169,8 @@ pub struct AddSslOptions {
|
||||
pub alpn: Option<AlpnInfo>,
|
||||
}
|
||||
|
||||
pub fn binding<C: Context, Kind: HostApiKind>(
|
||||
) -> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
|
||||
pub fn binding<C: Context, Kind: HostApiKind>()
|
||||
-> ParentHandler<C, Kind::Params, Kind::InheritedParams> {
|
||||
ParentHandler::<C, Kind::Params, Kind::InheritedParams>::new()
|
||||
.subcommand(
|
||||
"list",
|
||||
|
||||
@@ -127,14 +127,16 @@ pub fn host_for<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all_hosts(db: &DatabaseModel) -> impl Iterator<Item = Result<&Model<Host>, Error>> {
|
||||
[Ok(db.as_public().as_server_info().as_network().as_host())]
|
||||
pub fn all_hosts(db: &mut DatabaseModel) -> impl Iterator<Item = Result<&mut Model<Host>, Error>> {
|
||||
use patch_db::DestructureMut;
|
||||
let destructured = db.as_public_mut().destructure_mut();
|
||||
[Ok(destructured.server_info.as_network_mut().as_host_mut())]
|
||||
.into_iter()
|
||||
.chain(
|
||||
[db.as_public().as_package_data().as_entries()]
|
||||
[destructured.package_data.as_entries_mut()]
|
||||
.into_iter()
|
||||
.flatten_ok()
|
||||
.map(|entry| entry.and_then(|(_, v)| v.as_hosts().as_entries()))
|
||||
.map(|entry| entry.and_then(|(_, v)| v.as_hosts_mut().as_entries_mut()))
|
||||
.flatten_ok()
|
||||
.map_ok(|(_, v)| v),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use imbl::{vector, OrdMap};
|
||||
use imbl::{OrdMap, vector};
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::IpNet;
|
||||
use models::{HostId, OptionExt, PackageId};
|
||||
@@ -11,8 +11,9 @@ use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::HOST_IP;
|
||||
use crate::db::model::Database;
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::error::ErrorCollection;
|
||||
use crate::hostname::Hostname;
|
||||
use crate::net::dns::DnsController;
|
||||
@@ -23,7 +24,7 @@ use crate::net::gateway::{
|
||||
};
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||
use crate::net::host::{host_for, Host, Hosts};
|
||||
use crate::net::host::{Host, Hosts, host_for};
|
||||
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
|
||||
use crate::net::tor::{OnionAddress, TorController, TorSecretKey};
|
||||
use crate::net::utils::ipv6_is_local;
|
||||
@@ -31,7 +32,6 @@ use crate::net::vhost::{AlpnInfo, TargetInfo, VHostController};
|
||||
use crate::prelude::*;
|
||||
use crate::service::effects::callbacks::ServiceCallbacks;
|
||||
use crate::util::serde::MaybeUtf8String;
|
||||
use crate::HOST_IP;
|
||||
|
||||
pub struct NetController {
|
||||
pub(crate) db: TypedPatchDb<Database>,
|
||||
|
||||
@@ -9,24 +9,19 @@ use arti_client::{TorClient, TorClientConfig};
|
||||
use base64::Engine;
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::{FutureExt, Stream, StreamExt};
|
||||
use futures::StreamExt;
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use safelog::DisplayRedacted;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::oneshot;
|
||||
use tor_cell::relaycell::msg::Connected;
|
||||
use tor_hscrypto::pk::{HsId, HsIdKeypair};
|
||||
use tor_hsservice::status::State as ArtiOnionServiceState;
|
||||
use tor_hsservice::{HsNickname, RendRequest, RunningOnionService};
|
||||
use tor_hsservice::{HsNickname, RunningOnionService};
|
||||
use tor_keymgr::config::ArtiKeystoreKind;
|
||||
use tor_proto::stream::IncomingStreamRequest;
|
||||
use tor_proto::client::stream::IncomingStreamRequest;
|
||||
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -39,13 +34,11 @@ use crate::util::serde::{
|
||||
};
|
||||
use crate::util::sync::{SyncMutex, SyncRwLock};
|
||||
|
||||
const STARTING_HEALTH_TIMEOUT: u64 = 120; // 2min
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct OnionAddress(pub HsId);
|
||||
impl std::fmt::Display for OnionAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt_unredacted(f)
|
||||
safelog::DisplayRedacted::fmt_unredacted(&self.0, f)
|
||||
}
|
||||
}
|
||||
impl FromStr for OnionAddress {
|
||||
@@ -201,7 +194,7 @@ impl std::fmt::Debug for OnionStore {
|
||||
impl<'a> std::fmt::Debug for OnionStoreMap<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[derive(Debug)]
|
||||
struct KeyFor(OnionAddress);
|
||||
struct KeyFor(#[allow(unused)] OnionAddress);
|
||||
let mut map = f.debug_map();
|
||||
for (k, v) in self.0 {
|
||||
map.key(k);
|
||||
@@ -451,7 +444,6 @@ impl TorController {
|
||||
}
|
||||
|
||||
pub async fn reset(&self, wipe_state: bool) -> Result<(), Error> {
|
||||
// todo!()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -488,7 +480,7 @@ impl OnionService {
|
||||
.run_while(async {
|
||||
loop {
|
||||
if let Err(e) = async {
|
||||
let (new_service, mut stream) = client.peek(|c| {
|
||||
let (new_service, stream) = client.peek(|c| {
|
||||
c.launch_onion_service_with_hsid(
|
||||
OnionServiceConfigBuilder::default()
|
||||
.nickname(
|
||||
@@ -504,40 +496,54 @@ impl OnionService {
|
||||
)
|
||||
.with_kind(ErrorKind::Tor)
|
||||
})?;
|
||||
let addr = new_service.onion_address().map(|a| safelog::DisplayRedacted::display_unredacted(&a).to_string());
|
||||
let mut status_stream = new_service.status_events();
|
||||
bg.add_job(async move {
|
||||
while let Some(status) = status_stream.next().await {
|
||||
tracing::debug!("{addr:?} status: {status:?}");
|
||||
if let Some(err) = status.current_problem() {
|
||||
tracing::error!("{err:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
service.replace(Some(new_service));
|
||||
let mut stream = tor_hsservice::handle_rend_requests(stream);
|
||||
while let Some(req) = stream.next().await {
|
||||
bg.add_job({
|
||||
let bg = bg.clone();
|
||||
let bindings = bindings.clone();
|
||||
async move {
|
||||
if let Err(e) = async {
|
||||
let mut stream =
|
||||
req.accept().await.with_kind(ErrorKind::Tor)?;
|
||||
while let Some(req) = stream.next().await {
|
||||
let IncomingStreamRequest::Begin(begin) =
|
||||
req.request()
|
||||
else {
|
||||
continue; // TODO: reject instead?
|
||||
return req
|
||||
.reject(tor_cell::relaycell::msg::End::new_with_reason(
|
||||
tor_cell::relaycell::msg::EndReason::DONE,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Tor);
|
||||
};
|
||||
let Some(target) = bindings.peek(|b| {
|
||||
b.get(&begin.port()).and_then(|a| {
|
||||
a.iter()
|
||||
.find(|(_, rc)| {
|
||||
rc.strong_count() > 0
|
||||
})
|
||||
.find(|(_, rc)| rc.strong_count() > 0)
|
||||
.map(|(addr, _)| *addr)
|
||||
})
|
||||
}) else {
|
||||
continue; // TODO: reject instead?
|
||||
return req
|
||||
.reject(tor_cell::relaycell::msg::End::new_with_reason(
|
||||
tor_cell::relaycell::msg::EndReason::DONE,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Tor);
|
||||
};
|
||||
bg.add_job(async move {
|
||||
if let Err(e) = async {
|
||||
let mut outgoing =
|
||||
TcpStream::connect(target)
|
||||
.await
|
||||
.with_kind(
|
||||
ErrorKind::Network,
|
||||
)?;
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
let mut incoming = req
|
||||
.accept(Connected::new_empty())
|
||||
.await
|
||||
@@ -565,7 +571,6 @@ impl OnionService {
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
@@ -623,6 +628,7 @@ impl OnionService {
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) -> Result<(), Error> {
|
||||
self.0.service.replace(None);
|
||||
self.0._thread.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::Router;
|
||||
use futures::future::Either;
|
||||
use futures::FutureExt;
|
||||
use futures::future::Either;
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use hyper_util::rt::{TokioIo, TokioTimer};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
@@ -15,7 +15,7 @@ use tokio::sync::oneshot;
|
||||
|
||||
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
|
||||
use crate::net::gateway::{
|
||||
lookup_info_by_addr, NetworkInterfaceListener, SelfContainedNetworkInterfaceListener,
|
||||
NetworkInterfaceListener, SelfContainedNetworkInterfaceListener, lookup_info_by_addr,
|
||||
};
|
||||
use crate::net::static_server::{
|
||||
diagnostic_ui_router, init_ui_router, install_ui_router, main_ui_router, redirecter, refresher,
|
||||
|
||||
@@ -11,14 +11,14 @@ use crate::service::effects::prelude::*;
|
||||
use crate::service::persistent_container::Subcontainer;
|
||||
use crate::util::Invoke;
|
||||
|
||||
#[cfg(feature = "container-runtime")]
|
||||
#[cfg(feature = "start-container")]
|
||||
mod sync;
|
||||
|
||||
#[cfg(not(feature = "container-runtime"))]
|
||||
#[cfg(not(feature = "start-container"))]
|
||||
mod sync_dummy;
|
||||
|
||||
pub use sync::*;
|
||||
#[cfg(not(feature = "container-runtime"))]
|
||||
#[cfg(not(feature = "start-container"))]
|
||||
use sync_dummy as sync;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
|
||||
@@ -82,7 +82,7 @@ impl BackgroundJobRunner {
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
this.runner.poll(cx);
|
||||
let _ = this.runner.poll(cx);
|
||||
this.fut.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::os::unix::prelude::MetadataExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::task::{Poll, Waker};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -22,7 +22,7 @@ use nix::unistd::{Gid, Uid};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs::{File, OpenOptions};
|
||||
use tokio::io::{
|
||||
duplex, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf,
|
||||
AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf, duplex,
|
||||
};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Notify, OwnedMutexGuard};
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::task::{Poll, Waker};
|
||||
|
||||
use futures::stream::BoxStream;
|
||||
use futures::Stream;
|
||||
use futures::stream::BoxStream;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
2
patch-db
2
patch-db
Submodule patch-db updated: d2f38ef5f7...5b23a1eac6
@@ -193,7 +193,7 @@ export class PublicDomainService {
|
||||
),
|
||||
250,
|
||||
)
|
||||
} else if (ip === wanIp) {
|
||||
} else if (ip !== wanIp) {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.showDns(
|
||||
|
||||
@@ -167,10 +167,9 @@ export class InterfaceTorDomainsComponent {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
let onion = key
|
||||
const onion = key
|
||||
? await this.api.addTorKey({ key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddOnion({
|
||||
|
||||
@@ -114,14 +114,16 @@ export default class ServiceInterfaceRoute {
|
||||
|
||||
const { serviceInterfaces, hosts } = pkg
|
||||
const iFace = serviceInterfaces[this.interfaceId()]
|
||||
const key = iFace?.addressInfo.hostId || ''
|
||||
const key = iFace!.addressInfo.hostId || ''
|
||||
const host = hosts[key]
|
||||
const port = iFace?.addressInfo.internalPort
|
||||
const port = iFace!.addressInfo.internalPort
|
||||
|
||||
if (!host || !iFace || !port) {
|
||||
return
|
||||
}
|
||||
|
||||
const binding = host.bindings[port]
|
||||
|
||||
const gateways = this.gatewayService.gateways() || []
|
||||
|
||||
return {
|
||||
@@ -129,10 +131,13 @@ export default class ServiceInterfaceRoute {
|
||||
addresses: this.interfaceService.getAddresses(iFace, host, gateways),
|
||||
gateways:
|
||||
gateways.map(g => ({
|
||||
enabled: true,
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
...g,
|
||||
})) || [],
|
||||
torDomains: host.onions.map(o => `${o}.onion`),
|
||||
torDomains: host.onions,
|
||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||
privateDomains: host.privateDomains,
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ export default class StartOsUiComponent {
|
||||
|
||||
if (!network || !gateways) return
|
||||
|
||||
const binding = network.host.bindings['80']
|
||||
|
||||
return {
|
||||
...this.iface,
|
||||
addresses: this.interfaceService.getAddresses(
|
||||
@@ -92,10 +94,13 @@ export default class StartOsUiComponent {
|
||||
gateways,
|
||||
),
|
||||
gateways: gateways.map(g => ({
|
||||
enabled: true,
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
...g,
|
||||
})),
|
||||
torDomains: network.host.onions.map(o => `${o}.onion`),
|
||||
torDomains: network.host.onions,
|
||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||
privateDomains: network.host.privateDomains,
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ export namespace RR {
|
||||
key: string
|
||||
}
|
||||
export type GenerateTorKeyReq = {} // net.tor.key.generate
|
||||
export type AddTorKeyRes = string // onion address without .onion suffix
|
||||
export type AddTorKeyRes = string // onion address *with* .onion suffix
|
||||
|
||||
export type ServerBindingToggleGatewayReq = {
|
||||
// server.host.binding.set-gateway-enabled
|
||||
|
||||
@@ -1339,12 +1339,12 @@ export class MockApiService extends ApiService {
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'vanityabcdefghijklmnop'
|
||||
return 'vanityabcdefghijklmnop.onion'
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'abcdefghijklmnopqrstuv'
|
||||
return 'abcdefghijklmnopqrstuv.onion'
|
||||
}
|
||||
|
||||
async serverBindingToggleGateway(
|
||||
|
||||
Reference in New Issue
Block a user