socks5 support (#113)

This commit is contained in:
mat 2024-04-20 03:40:59 +00:00
parent fa96af786b
commit 353eda21ac
9 changed files with 240 additions and 64 deletions

22
Cargo.lock generated
View file

@ -122,6 +122,12 @@ version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "as-any"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a30a44e99a1c83ccb2a6298c563c888952a1c9134953db26876528f84c93a"
[[package]]
name = "async-channel"
version = "2.2.0"
@ -353,6 +359,7 @@ dependencies = [
"serde",
"serde_json",
"simdnbt",
"socks5-impl",
"thiserror",
"tokio",
"tracing",
@ -495,6 +502,7 @@ dependencies = [
"serde",
"serde_json",
"simdnbt",
"socks5-impl",
"thiserror",
"tokio",
"tokio-util",
@ -2533,6 +2541,20 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "socks5-impl"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dfc11441196e51be4f48c72b075e7fff394a3c6a43f93420f907a2708079b27"
dependencies = [
"as-any",
"async-trait",
"byteorder",
"bytes",
"thiserror",
"tokio",
]
[[package]]
name = "spin"
version = "0.5.2"

View file

@ -43,6 +43,7 @@ azalea-entity = { version = "0.9.0", path = "../azalea-entity" }
serde_json = "1.0.113"
serde = "1.0.196"
minecraft_folder_path = "0.1.2"
socks5-impl = "0.5.6"
[features]
default = ["log"]

View file

@ -34,7 +34,7 @@ use azalea_entity::{
};
use azalea_physics::PhysicsPlugin;
use azalea_protocol::{
connect::{Connection, ConnectionError},
connect::{Connection, ConnectionError, Proxy},
packets::{
configuration::{
serverbound_client_information_packet::ClientInformation,
@ -183,6 +183,7 @@ impl Client {
pub async fn join(
account: &Account,
address: impl TryInto<ServerAddress>,
proxy: Option<Proxy>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
let resolved_address = resolver::resolve_address(&address).await?;
@ -200,6 +201,7 @@ impl Client {
account,
&address,
&resolved_address,
proxy,
run_schedule_sender,
)
.await
@ -212,6 +214,7 @@ impl Client {
account: &Account,
address: &ServerAddress,
resolved_address: &SocketAddr,
proxy: Option<Proxy>,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
// check if an entity with our uuid already exists in the ecs and if so then
@ -239,7 +242,11 @@ impl Client {
entity
};
let conn = Connection::new(resolved_address).await?;
let conn = if let Some(proxy) = proxy {
Connection::new_with_proxy(resolved_address, proxy).await?
} else {
Connection::new(resolved_address).await?
};
let (conn, game_profile) =
Self::handshake(ecs_lock.clone(), entity, conn, account, address).await?;

View file

@ -1,9 +1,12 @@
//! Ping Minecraft servers.
use azalea_protocol::{
connect::{Connection, ConnectionError},
connect::{Connection, ConnectionError, Proxy},
packets::{
handshaking::client_intention_packet::ClientIntentionPacket,
handshaking::{
client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket,
ServerboundHandshakePacket,
},
status::{
clientbound_status_response_packet::ClientboundStatusResponsePacket,
serverbound_status_request_packet::ServerboundStatusRequestPacket,
@ -47,11 +50,29 @@ pub async fn ping_server(
address: impl TryInto<ServerAddress>,
) -> Result<ClientboundStatusResponsePacket, PingError> {
let address: ServerAddress = address.try_into().map_err(|_| PingError::InvalidAddress)?;
let resolved_address = resolver::resolve_address(&address).await?;
let conn = Connection::new(&resolved_address).await?;
ping_server_with_connection(address, conn).await
}
let mut conn = Connection::new(&resolved_address).await?;
/// Ping a Minecraft server through a Socks5 proxy.
pub async fn ping_server_with_proxy(
address: impl TryInto<ServerAddress>,
proxy: Proxy,
) -> Result<ClientboundStatusResponsePacket, PingError> {
let address: ServerAddress = address.try_into().map_err(|_| PingError::InvalidAddress)?;
let resolved_address = resolver::resolve_address(&address).await?;
let conn = Connection::new_with_proxy(&resolved_address, proxy).await?;
ping_server_with_connection(address, conn).await
}
/// Ping a Minecraft server after we've already created a [`Connection`]. The
/// `Connection` must still be in the handshake state (which is the state it's
/// in immediately after it's created).
pub async fn ping_server_with_connection(
address: ServerAddress,
mut conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket>,
) -> Result<ClientboundStatusResponsePacket, PingError> {
// send the client intention packet and switch to the status state
conn.write(
ClientIntentionPacket {

View file

@ -48,6 +48,8 @@ trust-dns-resolver = { version = "^0.23.2", default-features = false, features =
uuid = "1.7.0"
log = "0.4.20"
socks5-impl = "0.5.6"
[features]
connecting = []
default = ["packets"]

View file

@ -20,7 +20,7 @@ use std::io::Cursor;
use std::marker::PhantomData;
use std::net::SocketAddr;
use thiserror::Error;
use tokio::io::AsyncWriteExt;
use tokio::io::{AsyncWriteExt, BufStream};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError};
use tokio::net::TcpStream;
use tracing::{error, info};
@ -257,6 +257,20 @@ pub enum ConnectionError {
Io(#[from] std::io::Error),
}
use socks5_impl::protocol::UserKey;
#[derive(Debug, Clone)]
pub struct Proxy {
pub addr: SocketAddr,
pub auth: Option<UserKey>,
}
impl Proxy {
pub fn new(addr: SocketAddr, auth: Option<UserKey>) -> Self {
Self { addr, auth }
}
}
impl Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> {
/// Create a new connection to the given address.
pub async fn new(address: &SocketAddr) -> Result<Self, ConnectionError> {
@ -265,6 +279,28 @@ impl Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> {
// enable tcp_nodelay
stream.set_nodelay(true)?;
Self::new_from_stream(stream).await
}
/// Create a new connection to the given address and Socks5 proxy. If you're
/// not using a proxy, use [`Self::new`] instead.
pub async fn new_with_proxy(
address: &SocketAddr,
proxy: Proxy,
) -> Result<Self, ConnectionError> {
let proxy_stream = TcpStream::connect(proxy.addr).await?;
let mut stream = BufStream::new(proxy_stream);
let _ = socks5_impl::client::connect(&mut stream, address, proxy.auth)
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Self::new_from_stream(stream.into_inner()).await
}
/// Create a new connection from an existing stream. Useful if you want to
/// set custom options on the stream. Otherwise, just use [`Self::new`].
pub async fn new_from_stream(stream: TcpStream) -> Result<Self, ConnectionError> {
let (read_stream, write_stream) = stream.into_split();
Ok(Connection {

View file

@ -181,10 +181,12 @@ async fn swarm_handle(
_state: SwarmState,
) -> anyhow::Result<()> {
match &event {
SwarmEvent::Disconnect(account) => {
SwarmEvent::Disconnect(account, join_opts) => {
println!("bot got kicked! {}", account.username);
tokio::time::sleep(Duration::from_secs(5)).await;
swarm.add_and_retry_forever(account, State::default()).await;
swarm
.add_and_retry_forever_with_opts(account, State::default(), join_opts)
.await;
}
SwarmEvent::Chat(chat) => {
if chat.message().to_string() == "The particle was not visible for anybody" {

View file

@ -38,6 +38,7 @@ pub use azalea_world as world;
pub use bot::*;
use ecs::component::Component;
use futures::{future::BoxFuture, Future};
use protocol::connect::Proxy;
use protocol::{resolver::ResolverError, ServerAddress};
use swarm::SwarmBuilder;
use thiserror::Error;
@ -185,30 +186,26 @@ where
account: Account,
address: impl TryInto<ServerAddress>,
) -> Result<!, StartError> {
self.swarm.accounts = vec![account];
self.swarm.accounts = vec![(account, JoinOpts::default())];
if self.swarm.states.is_empty() {
self.swarm.states = vec![S::default()];
}
self.swarm.start(address).await
}
/// Do the same as [`Self::start`], but allow passing in a custom resolved
/// address. This is useful if the address you're connecting to doesn't
/// resolve to anything, like if the server uses the address field to pass
/// custom data (like Bungeecord or Forge).
pub async fn start_with_custom_resolved_address(
/// Do the same as [`Self::start`], but allow passing in custom join
/// options.
pub async fn start_with_opts(
mut self,
account: Account,
address: impl TryInto<ServerAddress>,
resolved_address: SocketAddr,
opts: JoinOpts,
) -> Result<!, StartError> {
self.swarm.accounts = vec![account];
self.swarm.accounts = vec![(account, opts.clone())];
if self.swarm.states.is_empty() {
self.swarm.states = vec![S::default()];
}
self.swarm
.start_with_custom_resolved_address(address, resolved_address)
.await
self.swarm.start_with_default_opts(address, opts).await
}
}
impl Default for ClientBuilder<NoState> {
@ -224,3 +221,55 @@ impl Default for ClientBuilder<NoState> {
/// [`SwarmBuilder`]: swarm::SwarmBuilder
#[derive(Component, Clone, Default)]
pub struct NoState;
/// Optional settings when adding an account to a swarm or client.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct JoinOpts {
/// The Socks5 proxy that this bot will use.
pub proxy: Option<Proxy>,
/// Override the server address that this specific bot will send in the
/// handshake packet.
pub custom_address: Option<ServerAddress>,
/// Override the socket address that this specific bot will use to connect
/// to the server.
pub custom_resolved_address: Option<SocketAddr>,
}
impl JoinOpts {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, other: &Self) {
if let Some(proxy) = other.proxy.clone() {
self.proxy = Some(proxy);
}
if let Some(custom_address) = other.custom_address.clone() {
self.custom_address = Some(custom_address);
}
if let Some(custom_resolved_address) = other.custom_resolved_address {
self.custom_resolved_address = Some(custom_resolved_address);
}
}
/// Set the proxy that this bot will use.
#[must_use]
pub fn proxy(mut self, proxy: Proxy) -> Self {
self.proxy = Some(proxy);
self
}
/// Set the custom address that this bot will send in the handshake packet.
#[must_use]
pub fn custom_address(mut self, custom_address: ServerAddress) -> Self {
self.custom_address = Some(custom_address);
self
}
/// Set the custom resolved address that this bot will use to connect to the
/// server.
#[must_use]
pub fn custom_resolved_address(mut self, custom_resolved_address: SocketAddr) -> Self {
self.custom_resolved_address = Some(custom_resolved_address);
self
}
}

View file

@ -17,7 +17,7 @@ use std::{collections::HashMap, future::Future, net::SocketAddr, sync::Arc, time
use tokio::sync::mpsc;
use tracing::error;
use crate::{BoxHandleFn, DefaultBotPlugins, HandleFn, NoState, StartError};
use crate::{BoxHandleFn, DefaultBotPlugins, HandleFn, JoinOpts, NoState, StartError};
/// A swarm is a way to conveniently control many bots at once, while also
/// being able to control bots at an individual level when desired.
@ -51,8 +51,8 @@ where
SS: Default + Send + Sync + Clone + Resource + 'static,
{
pub(crate) app: App,
/// The accounts that are going to join the server.
pub(crate) accounts: Vec<Account>,
/// The accounts and proxies that are going to join the server.
pub(crate) accounts: Vec<(Account, JoinOpts)>,
/// The individual bot states. This must be the same length as `accounts`,
/// since each bot gets one state.
pub(crate) states: Vec<S>,
@ -257,8 +257,20 @@ where
/// Add an account with a custom initial state. Use just
/// [`Self::add_account`] to use the Default implementation for the state.
#[must_use]
pub fn add_account_with_state(mut self, account: Account, state: S) -> Self {
self.accounts.push(account);
pub fn add_account_with_state(self, account: Account, state: S) -> Self {
self.add_account_with_state_and_opts(account, state, JoinOpts::default())
}
/// Same as [`Self::add_account_with_state`], but allow passing in custom
/// join options.
#[must_use]
pub fn add_account_with_state_and_opts(
mut self,
account: Account,
state: S,
join_opts: JoinOpts,
) -> Self {
self.accounts.push((account, join_opts));
self.states.push(state);
self
}
@ -302,21 +314,16 @@ where
Err(_) => return Err(StartError::InvalidAddress),
};
// resolve the address
let resolved_address = resolver::resolve_address(&address).await?;
self.start_with_custom_resolved_address(address, resolved_address)
self.start_with_default_opts(address, JoinOpts::default())
.await
}
/// Do the same as [`Self::start`], but allow passing in a custom resolved
/// address. This is useful if the address you're connecting to doesn't
/// resolve to anything, like if the server uses the address field to pass
/// custom data (like Bungeecord or Forge).
pub async fn start_with_custom_resolved_address(
/// Do the same as [`Self::start`], but allow passing in default join
/// options for the bots.
pub async fn start_with_default_opts(
self,
address: impl TryInto<ServerAddress>,
resolved_address: SocketAddr,
default_join_opts: JoinOpts,
) -> Result<!, StartError> {
assert_eq!(
self.accounts.len(),
@ -325,11 +332,17 @@ where
);
// convert the TryInto<ServerAddress> into a ServerAddress
let address: ServerAddress = match address.try_into() {
let address = match address.try_into() {
Ok(address) => address,
Err(_) => return Err(StartError::InvalidAddress),
};
let address: ServerAddress = default_join_opts.custom_address.clone().unwrap_or(address);
let resolved_address: SocketAddr = match default_join_opts.custom_resolved_address {
Some(resolved_address) => resolved_address,
None => resolver::resolve_address(&address).await?,
};
let instance_container = Arc::new(RwLock::new(InstanceContainer::default()));
// we can't modify the swarm plugins after this
@ -378,24 +391,27 @@ where
tokio::spawn(async move {
if let Some(join_delay) = join_delay {
// if there's a join delay, then join one by one
for (account, state) in accounts.iter().zip(states) {
swarm_clone.add_and_retry_forever(account, state).await;
for ((account, bot_join_opts), state) in accounts.iter().zip(states) {
let mut join_opts = default_join_opts.clone();
join_opts.update(bot_join_opts);
swarm_clone
.add_and_retry_forever_with_opts(account, state, &join_opts)
.await;
tokio::time::sleep(join_delay).await;
}
} else {
// otherwise, join all at once
let swarm_borrow = &swarm_clone;
join_all(
accounts
.iter()
.zip(states)
.map(move |(account, state)| async {
swarm_borrow
.clone()
.add_and_retry_forever(account, state)
.await;
}),
)
join_all(accounts.iter().zip(states).map(
|((account, bot_join_opts), state)| async {
let mut join_opts = default_join_opts.clone();
join_opts.update(bot_join_opts);
swarm_borrow
.clone()
.add_and_retry_forever_with_opts(account, state, &join_opts)
.await;
},
))
.await;
}
@ -460,9 +476,9 @@ pub enum SwarmEvent {
Init,
/// A bot got disconnected from the server.
///
/// You can implement an auto-reconnect by calling [`Swarm::add`]
/// with the account from this event.
Disconnect(Box<Account>),
/// You can implement an auto-reconnect by calling [`Swarm::add_with_opts`]
/// with the account and options from this event.
Disconnect(Box<Account>, JoinOpts),
/// At least one bot received a chat message.
Chat(ChatPacket),
}
@ -544,31 +560,36 @@ impl Swarm {
account: &Account,
state: S,
) -> Result<Client, JoinError> {
let address = self.address.read().clone();
let resolved_address = *self.resolved_address.read();
self.add_with_custom_address(account, state, address, resolved_address)
self.add_with_opts(account, state, JoinOpts::default())
.await
}
/// Add a new account to the swarm, using the given host and socket
/// address. This is useful if you want bots in the same swarm to connect to
/// different addresses. Usually you'll just want [`Self::add`] though.
/// Add a new account to the swarm, using custom options. This is useful if
/// you want bots in the same swarm to connect to different addresses.
/// Usually you'll just want [`Self::add`] though.
///
/// # Errors
///
/// Returns an `Err` if the bot could not do a handshake successfully.
pub async fn add_with_custom_address<S: Component + Clone>(
pub async fn add_with_opts<S: Component + Clone>(
&mut self,
account: &Account,
state: S,
address: ServerAddress,
resolved_address: SocketAddr,
opts: JoinOpts,
) -> Result<Client, JoinError> {
let address = opts
.custom_address
.clone()
.unwrap_or_else(|| self.address.read().clone());
let resolved_address = opts
.custom_resolved_address
.unwrap_or_else(|| *self.resolved_address.read());
let (bot, mut rx) = Client::start_client(
self.ecs_lock.clone(),
account,
&address,
&resolved_address,
opts.proxy.clone(),
self.run_schedule_sender.clone(),
)
.await?;
@ -597,7 +618,7 @@ impl Swarm {
.get_component::<Account>()
.expect("bot is missing required Account component");
swarm_tx
.send(SwarmEvent::Disconnect(Box::new(account)))
.send(SwarmEvent::Disconnect(Box::new(account), opts))
.unwrap();
});
@ -613,10 +634,25 @@ impl Swarm {
&mut self,
account: &Account,
state: S,
) -> Client {
self.add_and_retry_forever_with_opts(account, state, &JoinOpts::default())
.await
}
/// Same as [`Self::add_and_retry_forever`], but allow passing custom join
/// options.
pub async fn add_and_retry_forever_with_opts<S: Component + Clone>(
&mut self,
account: &Account,
state: S,
opts: &JoinOpts,
) -> Client {
let mut disconnects = 0;
loop {
match self.add(account, state.clone()).await {
match self
.add_with_opts(account, state.clone(), opts.clone())
.await
{
Ok(bot) => return bot,
Err(e) => {
disconnects += 1;