Skip to content

Tracker Checker: handle UDP Tracker timeouts #682

@josecelano

Description

@josecelano

Parent issue: #677
Depends on: #681

You can run a Tracker Checker with:

TORRUST_CHECKER_CONFIG='{
    "udp_trackers": ["127.0.0.1:6969"],
    "http_trackers": [],
    "health_checks": []
}' cargo run --bin tracker_checker

The UDP Tracker client does have a timeout for sending and receiving responses. However, it panics when times out. See #681.

After implementing the issue #681 we should bubble the error up to this point:

(torrust_tracker::console::clients::udp::checker::Client)

pub struct Client {
    local_binding_address: Option<SocketAddr>,
    local_bound_address: Option<SocketAddr>,
    remote_socket: Option<SocketAddr>,
    udp_tracker_client: Option<UdpTrackerClient>,
}

impl Client {
    pub async fn bind_and_connect(&mut self, local_port: u16, remote_socket_addr: &SocketAddr) -> anyhow::Result<SocketAddr> {
        let bound_to = self.bind(local_port).await?;
        self.connect(remote_socket_addr).await?;
        Ok(bound_to)
    }

    async fn bind(&mut self, local_port: u16) -> anyhow::Result<SocketAddr> {
        let local_bind_to = format!("0.0.0.0:{local_port}");
        let binding_address = local_bind_to.parse().context("binding local address")?;

        debug!("Binding to: {local_bind_to}");
        let udp_client = UdpClient::bind(&local_bind_to).await;

        let bound_to = udp_client.socket.local_addr().context("bound local address")?;
        debug!("Bound to: {bound_to}");

        self.local_binding_address = Some(binding_address);
        self.local_bound_address = Some(bound_to);

        self.udp_tracker_client = Some(UdpTrackerClient { udp_client });

        Ok(bound_to)
    }

    async fn connect(&mut self, tracker_socket_addr: &SocketAddr) -> anyhow::Result<()> {
        debug!("Connecting to tracker: udp://{tracker_socket_addr}");

        match &self.udp_tracker_client {
            Some(client) => {
                client.udp_client.connect(&tracker_socket_addr.to_string()).await;
                self.remote_socket = Some(*tracker_socket_addr);
                Ok(())
            }
            None => Err(ClientError::NotBound.into()),
        }
    }

    pub async fn send_connection_request(&self, transaction_id: TransactionId) -> anyhow::Result<ConnectionId> {
        debug!("Sending connection request with transaction id: {transaction_id:#?}");

        let connect_request = ConnectRequest { transaction_id };

        match &self.udp_tracker_client {
            Some(client) => {
                client.send(connect_request.into()).await;

                let response = client.receive().await;

                debug!("connection request response:\n{response:#?}");

                match response {
                    Response::Connect(connect_response) => Ok(connect_response.connection_id),
                    _ => Err(ClientError::UnexpectedConnectionResponse.into()),
                }
            }
            None => Err(ClientError::NotConnected.into()),
        }
    }

    pub async fn send_announce_request(
        &self,
        connection_id: ConnectionId,
        transaction_id: TransactionId,
        info_hash: TorrustInfoHash,
        client_port: Port,
    ) -> anyhow::Result<Response> {
        debug!("Sending announce request with transaction id: {transaction_id:#?}");

        let announce_request = AnnounceRequest {
            connection_id,
            transaction_id,
            info_hash: InfoHash(info_hash.bytes()),
            peer_id: PeerId(*b"-qB00000000000000001"),
            bytes_downloaded: NumberOfBytes(0i64),
            bytes_uploaded: NumberOfBytes(0i64),
            bytes_left: NumberOfBytes(0i64),
            event: AnnounceEvent::Started,
            ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)),
            key: PeerKey(0u32),
            peers_wanted: NumberOfPeers(1i32),
            port: client_port,
        };

        match &self.udp_tracker_client {
            Some(client) => {
                client.send(announce_request.into()).await;

                let response = client.receive().await;

                debug!("announce request response:\n{response:#?}");

                Ok(response)
            }
            None => Err(ClientError::NotConnected.into()),
        }
    }

    pub async fn send_scrape_request(
        &self,
        connection_id: ConnectionId,
        transaction_id: TransactionId,
        info_hashes: Vec<TorrustInfoHash>,
    ) -> anyhow::Result<Response> {
        debug!("Sending scrape request with transaction id: {transaction_id:#?}");

        let scrape_request = ScrapeRequest {
            connection_id,
            transaction_id,
            info_hashes: info_hashes
                .iter()
                .map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes()))
                .collect(),
        };

        match &self.udp_tracker_client {
            Some(client) => {
                client.send(scrape_request.into()).await;

                let response = client.receive().await;

                debug!("scrape request response:\n{response:#?}");

                Ok(response)
            }
            None => Err(ClientError::NotConnected.into()),
        }
    }
}

We have to catch the error and add new types of errors:

#[derive(Error, Debug)]
pub enum ClientError {
    #[error("Local socket address is not bound yet. Try binding before connecting.")]
    NotBound,
    #[error("Not connected to remote tracker UDP socket. Try connecting before making requests.")]
    NotConnected,
    #[error("Unexpected response while connecting the the remote server.")]
    UnexpectedConnectionResponse,
}

At the top level the Tracker Checker should return a check error in the final report instead of panicking when a UDP client panics.

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions