handshake_proxy/
handshake_proxy.rs

1//! A "simple" server that gets login information and proxies connections.
2//! After login all connections are encrypted and Azalea cannot read them.
3
4use std::{error::Error, sync::LazyLock};
5
6use azalea_protocol::{
7    connect::Connection,
8    packets::{
9        ClientIntention, PROTOCOL_VERSION, VERSION_NAME,
10        handshake::{
11            ClientboundHandshakePacket, ServerboundHandshakePacket,
12            s_intention::ServerboundIntention,
13        },
14        login::{ServerboundLoginPacket, s_hello::ServerboundHello},
15        status::{
16            ServerboundStatusPacket,
17            c_pong_response::ClientboundPongResponse,
18            c_status_response::{ClientboundStatusResponse, Players, Version},
19        },
20    },
21    read::ReadPacketError,
22};
23use futures::FutureExt;
24use tokio::{
25    io::{self, AsyncWriteExt},
26    net::{TcpListener, TcpStream},
27};
28use tracing::{Level, error, info, warn};
29
30const LISTEN_ADDR: &str = "127.0.0.1:25566";
31const PROXY_ADDR: &str = "127.0.0.1:25565";
32
33const PROXY_DESC: &str = "An Azalea Minecraft Proxy";
34
35// String must be formatted like "data:image/png;base64,<data>"
36static PROXY_FAVICON: LazyLock<Option<String>> = LazyLock::new(|| None);
37
38static PROXY_VERSION: LazyLock<Version> = LazyLock::new(|| Version {
39    name: VERSION_NAME.to_string(),
40    protocol: PROTOCOL_VERSION,
41});
42
43const PROXY_PLAYERS: Players = Players {
44    max: 1,
45    online: 0,
46    sample: Vec::new(),
47};
48
49const PROXY_SECURE_CHAT: Option<bool> = Some(false);
50
51#[tokio::main]
52async fn main() -> anyhow::Result<()> {
53    tracing_subscriber::fmt().with_max_level(Level::INFO).init();
54
55    // Bind to an address and port
56    let listener = TcpListener::bind(LISTEN_ADDR).await?;
57
58    info!("Listening on {LISTEN_ADDR}, proxying to {PROXY_ADDR}");
59
60    loop {
61        // When a connection is made, pass it off to another thread
62        let (stream, _) = listener.accept().await?;
63        tokio::spawn(handle_connection(stream));
64    }
65}
66
67async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> {
68    stream.set_nodelay(true)?;
69    let ip = stream.peer_addr()?;
70    let mut conn: Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> =
71        Connection::wrap(stream);
72
73    // The first packet sent from a client is the intent packet.
74    // This specifies whether the client is pinging
75    // the server or is going to join the game.
76    let intent = match conn.read().await {
77        Ok(packet) => match packet {
78            ServerboundHandshakePacket::Intention(packet) => {
79                info!(
80                    "New connection from {}, hostname {:?}:{}, version {}, {:?}",
81                    ip.ip(),
82                    packet.hostname,
83                    packet.port,
84                    packet.protocol_version,
85                    packet.intention
86                );
87                packet
88            }
89        },
90        Err(e) => {
91            let e = e.into();
92            warn!("Error during intent: {e}");
93            return Err(e);
94        }
95    };
96
97    match intent.intention {
98        // If the client is pinging the proxy,
99        // reply with the information below.
100        ClientIntention::Status => {
101            let mut conn = conn.status();
102            loop {
103                match conn.read().await {
104                    Ok(p) => match p {
105                        ServerboundStatusPacket::StatusRequest(_) => {
106                            conn.write(ClientboundStatusResponse {
107                                description: PROXY_DESC.into(),
108                                favicon: PROXY_FAVICON.clone(),
109                                players: PROXY_PLAYERS.clone(),
110                                version: PROXY_VERSION.clone(),
111                                enforces_secure_chat: PROXY_SECURE_CHAT,
112                            })
113                            .await?;
114                        }
115                        ServerboundStatusPacket::PingRequest(p) => {
116                            conn.write(ClientboundPongResponse { time: p.time }).await?;
117                            break;
118                        }
119                    },
120                    Err(e) => match *e {
121                        ReadPacketError::ConnectionClosed => {
122                            break;
123                        }
124                        e => {
125                            warn!("Error during status: {e}");
126                            return Err(e.into());
127                        }
128                    },
129                }
130            }
131        }
132        // If the client intends to join the proxy,
133        // wait for them to send the `Hello` packet to
134        // log their username and uuid, then forward the
135        // connection along to the proxy target.
136        ClientIntention::Login => {
137            let mut conn = conn.login();
138            loop {
139                match conn.read().await {
140                    Ok(p) => {
141                        if let ServerboundLoginPacket::Hello(hello) = p {
142                            info!(
143                                "Player \'{0}\' from {1} logging in with uuid: {2}",
144                                hello.name,
145                                ip.ip(),
146                                hello.profile_id.to_string()
147                            );
148
149                            tokio::spawn(transfer(conn.unwrap()?, intent, hello).map(|r| {
150                                if let Err(e) = r {
151                                    error!("Failed to proxy: {e}");
152                                }
153                            }));
154
155                            break;
156                        }
157                    }
158                    Err(e) => match *e {
159                        ReadPacketError::ConnectionClosed => {
160                            break;
161                        }
162                        e => {
163                            warn!("Error during login: {e}");
164                            return Err(e.into());
165                        }
166                    },
167                }
168            }
169        }
170        ClientIntention::Transfer => {
171            warn!("Client attempted to join via transfer")
172        }
173    }
174
175    Ok(())
176}
177
178async fn transfer(
179    mut inbound: TcpStream,
180    intent: ServerboundIntention,
181    hello: ServerboundHello,
182) -> Result<(), Box<dyn Error>> {
183    let outbound = TcpStream::connect(PROXY_ADDR).await?;
184    let name = hello.name.clone();
185    outbound.set_nodelay(true)?;
186
187    // Repeat the intent and hello packet
188    // received earlier to the proxy target
189    let mut outbound_conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> =
190        Connection::wrap(outbound);
191    outbound_conn.write(intent).await?;
192
193    let mut outbound_conn = outbound_conn.login();
194    outbound_conn.write(hello).await?;
195
196    let mut outbound = outbound_conn.unwrap()?;
197
198    // Split the incoming and outgoing connections in
199    // halves and handle each pair on separate threads.
200    let (mut ri, mut wi) = inbound.split();
201    let (mut ro, mut wo) = outbound.split();
202
203    let client_to_server = async {
204        io::copy(&mut ri, &mut wo).await?;
205        wo.shutdown().await
206    };
207
208    let server_to_client = async {
209        io::copy(&mut ro, &mut wi).await?;
210        wi.shutdown().await
211    };
212
213    tokio::try_join!(client_to_server, server_to_client)?;
214    info!("Player \'{name}\' left the game");
215
216    Ok(())
217}