the ugly org - bittorrent al toque

desu

El verdadero desafío del ingeniero es enfrentar la sombra de la resiliencia: el miedo al fracaso, la procrastinación que nos encadena, la duda constante sobre nuestras capacidades y el síndrome del impostor. Eres realmente un impostor? Afrontar la incertidumbre y superarla es nuestro reto. No debemos cuestionarnos si somos impostores, sino resolver esa duda y seguir adelante con convicción.

En esta película [Andrei Rublev], mi mensaje es que es imposible transmitir la experiencia a otros o aprender de otros. Debemos vivir nuestra propia experiencia, no podemos heredarlo. La gente a menudo dice: '¡Usa la experiencia de tus padres!', pero eso es demasiado fácil. Cada uno debe pasar por sus propias experiencias. Pero una vez que la tenemos, ya no tenemos tiempo para usarla. Y las nuevas generaciones con razón se niegan a escucharla. Quieren vivirla, pero también mueren. Esta es la ley de la vida, su verdadero significado: no podemos imponer nuestra experiencia a otras personas ni forzarlas a sentir emociones sugeridas. Solo a través de la experiencia personal entendemos la vida.

Andrei Tarkovsky

Enfrentar nuestros miedos requiere coraje. Solo viviendo nuestras propias experiencias podemos entender verdaderamente la vida. La resistencia que sentimos es la medida de lo que estamos dispuestos a arriesgar. Enfrentar nuestros miedos nos lleva al abismo. Y solo en el podemos descubrir nuestra verdadera fortaleza.

El hombre está condenado a ser libre; porque una vez arrojado al mundo, es responsable de todo lo que hace.

Jean-Paul Sartre

Sartre explora cómo esta responsabilidad puede ser abrumadora. El verdadero desafío del ingeniero es crear. Navegar entre el riesgo del fracaso y la recompensa, transformar la oportunidad en crecimiento. Y si nos equivocamos, seremos humanos.

—En Afganistán, tenemos una historia sobre un pájaro que olvida cómo cantar.
—¿Qué sucede?
—Muere. De tristeza.

desu

Capitulo 1 - bencode

BitTorrent is a protocol for distributing files. It identifies content by URL and is designed to integrate seamlessly with the web. Its advantage over plain HTTP is that when multiple downloads of the same file happen concurrently, the downloaders upload to each other, making it possible for the file source to support very large numbers of downloaders with only a modest increase in its load.

https://www.bittorrent.org/beps/bep_0003.html

Bencode (pronounced like Bee-encode) is the encoding used by the peer-to-peer file sharing system BitTorrent for storing and transmitting loosely structured data.[1]

https://en.wikipedia.org/wiki/Bencode

Googleamos unos tests para nuestra implementación:

https://www.nayuki.io/res/bittorrent-bencode-format-tools/bencode-test.py

Perfecto, tiene buena pinta, pillamos unos cuantos casos de uso y los metemos en nuestro código. Después usamos una librería para parsear. Cuando los tests pasen tenemos el primer paso.

Voy a usar este crate de Rust para que me serialize automáticamente el struct de información del Torrent.

https://crates.io/crates/serde_bencode
https://github.com/toby/serde-bencode/blob/HEAD/examples/parse_torrent.rs

La propia documentación me da los structs que necesitamos.

3 comentarios moderados
Jastro

a ver gente, que no estamos en Feda, si no teneis nada que aportar, no comenten, se que no os gusta mucho el OP, pero al menos fuera de feda, comportemonos.

PD: Son sin aggro.

1
desu

Pues he usado la libreria: https://github.com/toby/serde-bencode pero es malísima en los errores. Quería hacer una pull request para arreglarlo un poco pero la cantidad de sobre-enginieria es antológico. Menudo mierdon de codigo.

Queria arreglar esto:

 fn invalid_type(unexpected: Unexpected<'_>, exp: &dyn Expected) -> Self {
        Error::InvalidType(format!("Invalid Type: {unexpected} (expected: `{exp}`)"))
    }

fn invalid_value(unexpected: Unexpected<'_>, exp: &dyn Expected) -> Self {
    Error::InvalidValue(format!("Invalid Value: {unexpected} (expected: `{exp}`)"))
}

Estaba teniendo esto error:

Invalid Type: byte array (expected: `a sequence`)

Y la libreria si estuviese bien hecha me diria:

Invalid Type for field "pieces" : byte array (expected: `a sequence`)

He mirado la libreria y tiene dos metodos del estilo:

Sorpresa para nadie, estos dos funciones que hacen lo que quiero y te dicen si un Field esta mal nunca se utilizan...

Asi que me hubiese gustado hoy salvar el mundo, pero como esta libreria esta tan mal hecha con patrones de disenyo super complejos y no quiero perder la tarde, no lo arreglo.

Pero vamos, cuando veas una libreria un error asi de mal tirado que te dice ERROR, y no te dice el que. Es que quien la ha diseñado no tiene ni puta idea. Eso si, el codigo lleno de Visitor patterns y codigo generico que no entiende nadie, y algo super trivial como un parser que son 100 loc lo tiene en 1k de lineas de codigo genéricas bien bonitas jaja

Por ejemplo cuando serializas json o grpc te diría la columna o el campo que esta mal.. Es de 101 ABC de serializar jaja Y este perro te dice ERROR, y tu a comentar uno por uno los campos a ver cual te peta... madre mía.

Le he creado una issue y a otra cosa. Ahora ando leyendo la info del tracker y demas para hacer el handshake.

PS La verdad es que es una libreria horrible, lo mismo otro dia busco una alternativa mejor porque este codigo fpero da vergüenza.

desu
resp = "d14:failure reason25:provided invalid infohashe"

una vez parseamos el torrent tenemos que hacer una petición, https://www.bittorrent.org/beps/bep_0003.html#trackers que nos devolverá la lista de peers, pero me estoy encontrando en que calculo el infohash mal

The 20 byte sha1 hash of the bencoded form of the info value from the metainfo file. This value will almost certainly have to be escaped.

Note that this is a substring of the metainfo file. The info-hash must be the hash of the encoded form as found in the .torrent file, which is identical to bdecoding the metainfo file, extracting the info dictionary and encoding it if and only if the bdecoder fully validated the input (e.g. key ordering, absence of leading zeros). Conversely that means clients must either reject invalid metainfo files or extract the substring directly. They must not perform a decode-encode roundtrip on invalid data.

un poco lioso, mañana me lo revisaré de nuevo porque googleando he metido un hash de tests y "en teoría lo tengo bien", algo en mi encoding esta mal y estoy obviamente entendiendo mal el ejemplo también, xq le paso el ejemplo y me da error igual.

lo arreglo mañana y empezamos el peer to peer


edito, como mañana quiero ir a entrenar y la piscina, lo he arreglado ahora, y de paso he quitado la libreria esa que no me gustaba.

Menos de 30 lineas de codigo, tiene buena pinta. En menos de 30 lineas ya podemos empezar a hacer el handshake y nuestro peer to peer!

use lava_torrent::torrent::v1::Torrent;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let path = "sample.torrent";
    let torrent: Torrent = Torrent::read_from_file(path)?;
    let info_hash_bytes = torrent.info_hash_bytes();
    let info_hash: String = form_urlencoded::byte_serialize(&info_hash_bytes).collect();
    let client = reqwest::Client::new();
    let announce_url = &torrent.announce.clone().unwrap();
    let url = format!("{announce_url}/?info_hash={info_hash}");
    let r = client
        .get(url)
        .query(&[("peer_id", "00112233445566778899")])
        .query(&[("port", 6881)])
        .query(&[("uploaded", 0)])
        .query(&[("downloaded", 0)])
        .query(&[("left", &torrent.length)])
        .query(&[("compact", 1)])
        .send()
        .await?;
    if r.status().is_success() {
        let bytes = r.bytes().await?;
        let resp = lava_torrent::bencode::BencodeElem::from_bytes(bytes)?;
        dbg!(resp);
    }
    Ok(())

https://crates.io/crates/lava_torrent

A ver es interesante para un estudiante hacer el bencode mas a mano y no usar tanta libreria, en mi caso que he picado vario protocolo ya en mi vida... me daba pereza. Quiero dedicarme a avanzar el peer to peer.

El problema os podeis imaginar cual era con el encoding, he tardado un momento en arreglarlo, si no lo sabéis, pues picad el codigo a mano todo que os falta experiencia.

Otra cosa que me ha gustado mucho es esta API

r.status().is_success()

Normalmente estoy en contra de la sobre enginieria y tener patrones, pero en este caso es simple y bien tirado. Un struct con un simple metodo para algo super típico... Bien.

https://github.com/hyperium/http/blob/master/src/status.rs#L45

Y pista sobre el error que tuve: https://github.com/seanmonstar/reqwest/blob/master/src/async_impl/request.rs#L349
Error muy comun.

2
desu

Una mierda del Bencode es que debe estar en orden, https://www.bittorrent.org/beps/bep_0003.html#bencoding,

Keys must be strings and appear in sorted order (sorted as raw strings, not alphanumerics).

Entonces

[src/main.rs:30:9] &bytes = b"d5:peers18:\xa5\xe8!M\xc9\x0b\xb2>U\x14\xc9!\xb2>RY\xc8\xf88:completei4e10:incompletei1e8:intervali60e12:min intervali60ee"
[src/main.rs:30:9] &bytes = b"d10:incompletei1e8:intervali60e12:min intervali60e5:peers18:\xa5\xe8!M\xc9\x0b\xb2>U\x14\xc9!\xb2>RY\xc8\xf88:completei3ee"

petan

Lo que he hecho, meter la otra libreria que no me gustaba por la sobre enginieria XD, y usar esa para parsear esto... porque la otra lib no falla jaja.

desu

Ya ando metido en el fango del p2p.

Voy a empezar haciendo la descarga de la manera mas simple y tonta posible. Lineal, sin paralelizar, de un bloque en un bloque. Asi termino el BitTorrent de descarga en si.

Ahora, siguiente paso estoy pensando en métodos efectivos de tener algo simple y funcional, estoy pensando que una estrategia de pool de workers CSP. De esta manera sabemos que siempre trabajamos a un máximo bandwith IO. Los workers cogen trabajo del pool, donde le trabajo sera descargar un bloque pendiente del torrent, lo descargan y lo almacenan, cuando se libran siguen con otro bloque y así. Empezamos con un worker y un canal lógico unico por unidad de trabajo. En caso de fallo se re-spawnea el worker, y los casos de saturación y starving creo que se evitan teniendo el pool fijo con alguna heurística. Me gusta tener el pool centralizado por thread creo que tienes mas control y otros modelos de paralelismo darán problemas de sincronización.

Luego en el siguiente de el upload puede ser con un pool hermano, o haciendo que el pool actual haga ambas funciones.

Los choking algorithms los podemos mirar después al ver cuales son los puntos débiles de esta implementación, que a priori ya debe ser de lo mejor.

2
desu

Como funciona la comunicación torrent a un nivel simplificado? Tenemos 3 fases.

  • Fase 1: Hacemos un HANDSHAKE
  • Fase 2: El peer nos envía una lista de Piece, Piece[] que tiene del contenido que queremos, le decimos si estamos interesados o no en descargar y esperamos la respuesta UNCHOKE.
  • Fase 3: Descargar una Piece, cada Piece esta compuesta por N Bloques de tamaño fijo, para cada bloque, pedimos una REQUEST y obtenemos ese bloque.

  • La fase 1 establece una conexión TCP, en el handshake le indicamos el contenido que queremos mediante un hash.

  • La fase 2 el peer comparte que piezas tiene disponibles y el cliente decide si descargar de ese peer o no. Puede ser que 1 peer tenga todo, puede ser que multiples peers tengan todo, puede ser que distintos peers tengan distintas velocidades de transmisión, puede ser que un peer se desconecte en mitad de una transmisión, no lo sabemos y es responsabilidad del cliente del torrent ajustarse,

  • La fase 3 es descargar una PIece del peer. Cada PIece sera un conjunto de bloques de un tamaño fijo acordado en el protocolo. Cuando los tienes todos, puedes guardar la Piece y pedir otra. Y así. Como cada Piece y cada bloque tiene un tamaño y un orden que conocemos, esto nos permite continuar descargando de otros peers en esos casos.

Muy simple y fácil.


use anyhow::bail;

mod http;
mod torrent;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let path = "sample.torrent";
    let torrent: torrent::Torrent = torrent::Torrent::from_path(path)?;

let announce_response = http::try_announce(&torrent).await?;
assert!(!announce_response.peers.is_empty());

let peer = announce_response
    .peers
    .get(0)
    .expect("expected one peer at least");

let mut peer_stream = torrent::HandshakeMessage::new(torrent.info_hash_bytes)
    .send(peer)
    .await?;
dbg!(&peer_stream);

if let torrent::PeerMessage::Bitfield(payload) =
    torrent::PeerMessage::receive(&mut peer_stream.stream).await?
{
    dbg!(&payload);
} else {
    bail!("expected bitfield as first receive message from peer")
}

torrent::PeerMessage::Interested
    .send(&mut peer_stream.stream)
    .await?;
dbg!("interested send to peer");

if let torrent::PeerMessage::Unchoke =
    torrent::PeerMessage::receive(&mut peer_stream.stream).await?
{
    dbg!("unchocked successfully");
} else {
    bail!("expected unchock message from peer")
}

Ok(())
}

Mi codigo se ve tal que asi, el problema ahora mismo lo tengo en que el unchocked no se lee, entonces estoy pensando que estoy leyendo mal el stream de TCP seguramente... y se me queda bloqueado... asi que tendré que probar un par de maneras hasta dar con la tecla.

Una vez entienda como leer y escribir bien por stream, estoy usando Tokio, pobre pasar al siguiente paso que es pedir los bloques de la pieza.

SI os fijáis en el codigo es bastante legible y lo mas importante que trato de hacer es: 1) que el caller sepa siempre si algo puede fallar o es async, si hay que hacer await o un try (?) es explicito para el caller, y 2) que el caller tenga el control total de la interaccion con el peer, de esta manera no hay llamadas ocultas que no me interesen y puedo después realizar un modelo CSP para pedir bloques como dije.

Quizás no es el codigo perfecto aun, pero sin duda esta mucho mejor que varios ejemplos que he visto online que son un follon de entender. Ah, y punto numero 3, si os fijáis no hay nada de genérico con traits de rust ni lifetimes... también lo he evitado para demostrar que rust es realmente un lenguaje muy fácil si vas con calma y no haces cosas raras con referencias ni genéricos. Por ejemplo, el tcpstream de Tokio lo tengo en ownership en lugar de pasar por referencias, o a veces copio cosas de vectores a arrays a mano... ya lo veréis cuando termine y comparta todo.

pub struct Torrent {
    pub torrent: lava_torrent::torrent::v1::Torrent,
    pub info_hash: String,
    pub info_hash_bytes: [u8; 20],
}

Tambien sigo practicas como encapsular librerías con mis tipos, ya sea mediante typedef o un struct propio de wrapper como este Torrent, para no exponer librerías en la API del torrent. Y tambien como veis que cuando algo es publico, todos los campos son públicos, no uso getters/setters a la zig.

De momento mi conclusion es un poco lo que ya dije, no es tan difícil como parece ni complejo el BitTorrent a un nivel usable basico, sin tener en cuenta detalles puedes tener algo rodando muy rapido, lo mas complicado son los detalles de bajo nivel como parsear el protocolo, enviar paquetes y leerlos etc. Que ahi es donde pierdes todo el tiempo probando funciones hasta dar con la que tire bien. Le he dedicado 4 dias? pues 4 horitas... y si supiese bien Rust ya tendría el siguiente paso...

nerkaid

Aquí sería:

    bail!("expected unchocke message from peer")

unchocke

No te lo digo a malas, solo para que quede correcto, espero que no me oculte el mensaje Jastro.

desu

Volviendo a la idea de los side effects tenemos este codigo para el handshake:

spoiler

Fijaros que tenemos una función donde enviamos y recibimos bloqueando sin que el caller tenga control, mala practica. Como lo podemos resolver?

spoiler

Perfecto.

Estos son los pequeños detalles en el código que importan.

Clean code.

No solo hemos ganado una api mas limpia, una api mas fácil de entender y sin side effect, tenemos una api donde el caller tiene el control absoluto y mas eficiente en rendimiento.

Si el usuario de nuestra API quiere ahora puede crear el "send_and_receive" y llamarlo como quiera para ahorrarse dos lineas de código. Antes no podia.

1
desu

Ya he contado el error que cometia en el protocolo, primero he comprobado que estaba transmitiendo mi mensaje al peer:

            PeerMessage::Interested => stream.write_u8(2).await?,
            PeerMessage::Interested => stream.write_all(&[2]).await?,
            PeerMessage::Interested => stream.write_all(&[2]).await?,
            // al final asegurar el flush
            stream.flush().await?;

Y aqui al usar el write_all he visto que estaba mandando solo un 2, y el protocolo BitTorrent realmente es asi:

PeerMessage::Interested => stream.write_all(&[0, 0, 0, 1, 2]).await?,

Asique mi serializacion no era correcta.

Los primeros 4 bytes indican el tamaño delmensaje, en este caso 1, y el 2 es el tag del mensaje, 2 = interested.

Ahora ya funciona.

Y fijaros que [0,0,0,1,2] podria hacer:

            PeerMessage::Interested => {
                stream.write_u32(1).await?;
                stream.write_u8(2).await?;
            }

Y a mi me gusta escribir en los buffers y streams de esta manera, como en un builder, para dejar claro los tamaños y el protocolo, así que me quedo con este código, escribo 4 bytes para el 1, y 1 byte para el 2. u32 + u8.

Aqui el error es mio claramente, el protocolo lo deja claro:

Peer connections are symmetrical. Messages sent in both directions look the same, and data can flow in either direction.

Me fume un porro simplemente.

Como estaba recibiendo los keep alive de parte del peer, me estaba confundiendo... porque yo tenia un mensaje en transmisión incompleto y recibía igual los keep alive por el mismo stream...

desu

Ya tenemos el codigo mas difícil del MVP, descargar los bloques/piezas independientes según su indice. Si ahora hacemos un bucle for y descargamos todos los indices, guardamos en disco, ya hemos terminado.

spoiler

He estado 1h30min o asi en picarlo, mientras me dibujaba en la tablet los paquetes como cojones tenia que ordenarlos y como partir los bloques, un lio... muy fácil cagarla. Solo la cague en una linea de codigo, y no me daban los hashes pero revisando el codigo por suerte lo vi rapido... ademas el codigo que he hecho funciona para el caso de 1 archivo, que pasa si el torrent es 1 carpeta? no se si mi codigo esta bien o tiene errores...

                piece.splice(begin as usize..begin as usize, block.into_iter());

Esta linea en concreto la lie, estaba guardando los bloques mal en mi piece resultante... por suerte el codigo se lee muy fácil y se entiende bien... y lo he visto en 5-10 minutos... Este codigo es el típico como hagas guarradas o no sepas bien lo que estes haciendo y te pongas a copiar cosas de ChatGPT y tengas un off-one error o alguna mierda no lo arreglas en dias XD

A nivel de codigo, he hecho otro refactor, el tema de buffers para no mandar múltiples paquetes tcp:

    pub async fn send(&self, stream: &mut TcpStream) -> anyhow::Result<()> {
        let mut buffer = Vec::new();
        match self {
            PeerMessage::Choke => todo!(),
            PeerMessage::Unchoke => todo!(),
            PeerMessage::Interested => {
                buffer.write_u8(2).await?;
            }
            PeerMessage::NotInterested => todo!(),
            PeerMessage::Have => todo!(),
            PeerMessage::Bitfield(_) => todo!(),
            PeerMessage::Request {
                index,
                begin,
                length,
            } => {
                buffer.write_u8(6).await?;
                buffer.write_u32(*index).await?;
                buffer.write_u32(*begin).await?;
                buffer.write_u32(*length).await?;
            }
            PeerMessage::Piece {
                index,
                begin,
                block,
            } => todo!(),
            PeerMessage::Cancel => todo!(),
        }
        stream.write_u32(buffer.len() as u32).await?;
        stream.write_all(&buffer).await?;
        Ok(())
    }

Como veis inicializo un buffer como os comente atras, antes estaba escribiendo directo al fd, y ahora en ese buffer meto todo, y luego al final si que escribo en el fd del stream la longitud del buffer y el buffer, que podria hacerlo en 1 pero bueno, una micro - optimización para deberes. Confío en el kernel. Para quien no lo sepa el kernel tiene sus propios buffers internos y seguramente esto se encarga de mandarlo en un paquete, lo podríamos debuggar mirando las los paquetes que mandamos por network.

Y mi main se ve tal que asi, he cambiado los if let por un match asi tengo en el error el paseo que ha fallado... aun no tengo el diseño final y estoy probando a mejorar alguna cosa cuando la veo a ver como se siente usar la api:

spoiler

Una asignatura pendiente que me queda al final es ponerme a testear, que de momento todo lo estoy testando con integracion corriendo el main, pero molaría tener tests unitarios mas pequeños, sobretodo nos ayudara a limpiar el codigo una mas y no hacer chorradas. Y si ahora me pongo a hacer cosas mas complejas de peer (server) o descargar carpetas, estoy seguro que algun error tonto he cometido que aun no me ha saltado.

desu

Y voila, 5 minutos completar el bittorrent al toque.


pub async fn download(&self, stream: &mut TcpStream) -> anyhow::Result<()> {
    let mut downloaded_torrent = Vec::new();
    for (index, _) in self.torrent.pieces.iter().enumerate() {
        let piece = self.download_piece(stream, index as u32).await?;
        downloaded_torrent.push(piece);
        dbg!("piece downloaded successfully");
    }
    let output_path = "bittorrent_al_toque.txt";
    let mut f = std::fs::File::create(output_path)?;
    let bytes = downloaded_torrent.concat();
    f.write_all(&bytes)?;
    Ok(())
}

Todo perfecto.

Problemas de esta implementación? Muchos. Por ejemplo, sin ir mas lejos que pasa si el torrent que me acabo de descargar no cabe en memoria? Porque lo estoy metiendo en un vector como un campeón. Debería guardar las piezas una a una... ademas que podria cerrar el programa. Ah, y un detallado que mucha gente falla, que pasa si te quedas sin disco? antes de empezar, debes reservar memoria/disco para guardar las partes...

Pero bueno, finito. Tiempo total de desarrollo menos de una jornada laboral. Unas 6h haber tardado. Un buen proyectito sin duda que os recomiendo.

Ahora le voy a dedicar un tiempo a mejorarlo y ya me sirve de resume, que para algo lo he picado. Ya os comparto en unos dias el codigo mientras hare algunos updates de refactors y cosas interesantes que vea.

desu

Bueno, una vez tenemos un mvp funcional hay que llevarlo a prod para poder usarlo.

Que necesitamos? CI/CD, tests, logs y metrics. He metido todo salvo las métricas ya que no lo voy a desplegar.
Que más es necesario? Una CLI para poder correr el programa de manera fácil.

En la ci, hago build, fmt, clippy, test y audit para detectar vulnerabilidades.

Para la cli tengo esto con clap:

# Download a torrent file with default logging and output path
rust-bittorrent --file sample.torrent --output_path test.txt

# Download a torrent file with default logging and output path creating a folder
rust-bittorrent --file sample.torrent --output_path test_folder/test.txt

# Download a torrent file with verbose logging
rust-bittorrent --file sample.torrent --output_path test_folder/test.txt --verbose
desu

Ahora un refactor que es importante comentar por dos motivos.

Primero "inyección de dependencias" y desacoplamiento.
Segundo patron de diseño TryFrom/TryInto/From/Into.

Primero lo que he hecho en mi modulo http es cambiar el parámetro de entrada por un struct de request, igual que tenemos una respuesta es buena idea tener una request, esto nos permite desacoplar esta petición con los detalles de implementación de nuestro BitTorrent y ademas testear sin interfaces ni mocks...

pub async fn try_announce(request: AnnounceRequest) -> anyhow::Result<AnnounceResponse> {
   ...
}

Nos permite:

testear facilmente

Y segundo, el patron de diseño y estándar que usa Rust para convertir entre tipos T => R y que usa el try para cosas que son T => Result<R, Error> es un patron que todo el mundo debe conocer y aplicar en TODOS LOS LENGUAJES que useis. Yo este patron lo uso en python, java, go, typescript... No hay excusa.

Cuando una conversion T => Result<R, Error>

AnnounceRequest::try_from(&torrent)?

Cuando nunca puede fallar y es seguro, T => R

AnnounceRequest::from(&torrent)

Super importante este patron de diseño, si no construyes tipos asi y en su lugar estas usando builders o multiples constructores, lo estas haciendo muy mal.

desu

Por ultimo, la ultima mejora que he hecho en mi codigo y la mas importante los logs, el 99% de la gente no sabe escribir un log, un log no es un mensaje de error, un log sirve para cuando algo va mal COMPRENDER que esta pasando.

Te pongo un ejemplo, le he dedicado un post solo ha decir esto, es asi de importe, si de todo el hilo hay que aprender algo, que sea este post:

Antes:

[2024-06-24T07:51:06Z DEBUG rust_bittorrent::torrent] piece downloaded successfully
[2024-06-24T07:51:06Z DEBUG rust_bittorrent::torrent] piece downloaded successfully
[2024-06-24T07:51:06Z DEBUG rust_bittorrent::torrent] piece downloaded successfully
[2024-06-24T07:51:06Z INFO  rust_bittorrent::torrent] torrent downloaded successfully to test/test.txt

Despues:

[2024-06-24T07:53:15Z DEBUG rust_bittorrent::torrent] piece_index: 0 downloaded successfully for info_hash: %D6%9F%91%E6%B2%AELT%24h%D1%07%3Aq%D4%EA%13%87%9A%7F
[2024-06-24T07:53:16Z DEBUG rust_bittorrent::torrent] piece_index: 1 downloaded successfully for info_hash: %D6%9F%91%E6%B2%AELT%24h%D1%07%3Aq%D4%EA%13%87%9A%7F
[2024-06-24T07:53:16Z DEBUG rust_bittorrent::torrent] piece_index: 2 downloaded successfully for info_hash: %D6%9F%91%E6%B2%AELT%24h%D1%07%3Aq%D4%EA%13%87%9A%7F
[2024-06-24T07:53:16Z INFO  rust_bittorrent::torrent] torrent info_hash: %D6%9F%91%E6%B2%AELT%24h%D1%07%3Aq%D4%EA%13%87%9A%7F downloaded successfully to test/test.txt

De que cojones te sire saber que una descarga ha fallado? quieres saber QUE descarga, PARA QUE BLOQUE, PARA QUE INFO_HASH o TORRENT, porque vas a tener seguramente MULTIPLES descargas no?

En conclusion si tu log es una string de texto donde NO le pasas runtime info, ese log es INUTIL y esta MAL.

En conclusion TODO LOG debe tener runtime info o no existir, si NO TIENES RUNTIME INFO, ahi estas PROGRAMANDO MAL, piensa, PIENSA FPERO, que runtime info podrías usar para mejorar el log?

Y yo no lo tengo aun, pero lo suyo seria para cada proceso un request_Id, trace_id que puedas seguir para toda la ejecución del programa.

desu

https://github.com/vrnvu/rust-bittorrent

Cambios:

  • tenemos un workspace con dos proyectos, el bt es el cliente peer que os he mostrado, de momento descarga pero servirá para mandar info
  • tenemos en el segundo proyecto tracker un webserver que he creado para hacer tests de integración y hacer peer to peer en local para este proyecto. los clientes bt registraran sus torrents en este tracker y podremos descargarnos cosas. la idea es que cada bt tenga un directorio por ejemplo, y compartir las cosas entre peers para simular algo real...
  • protocolo de tracker TCP/HTTP completado, hay otros protocolos como el UDP que he medio empezado y otro que es el de poder hacer streaming de torrent en web... Aqui tenéis la lista: http://bittorrent.org/beps/bep_0000.html De momento mi objetivo sera hacer peer to peer completo y bi-direccional con mi tracker.
  • testeamos el handshake del torrent peer y estamos listos para mandar mensajes entre peers en bt/torrent.rs
  • mientras testeaba he arreglado algunas cosas de diseño y modelado de datos.

Ahora mi objetivo va a ser el tracker y la comunicación básica peer to peer. No me interesa soportar otros protocolos que no sean el tracker con UDP.

Empezare haciendo el tracker in memory como lo podeis ver, he buscado info y no hay un standard claro de como debe compararse un tracker de torrent. Asi que hare algo simple. Cada cliente bt para un announce con el torrent que tenga, lo almacenaré y luego subiremos y descargaremos bloques entre nuestros peers de lo que sea.

Aunque me molaría meter sqlite a futuro y para el protocolo del tracker molaría tener QUIC con HTTP3.

El motivo del UDP es muy interesante, http://bittorrent.org/beps/bep_0015.html

Using HTTP introduces significant overhead. There's overhead at the ethernet layer (14 bytes per packet), at the IP layer (20 bytes per packet), at the TCP layer (20 bytes per packet) and at the HTTP layer. About 10 packets are used for a request plus response containing 50 peers and the total number of bytes used is about 1206 [1]. This overhead can be reduced significantly by using a UDP based protocol. The protocol proposed here uses 4 packets and about 618 bytes, reducing traffic by 50%. For a client, saving 1 kbyte every hour isn't significant, but for a tracker serving a million peers, reducing traffic by 50% matters a lot. An additional advantage is that a UDP based binary protocol doesn't require a complex parser and no connection handling, reducing the complexity of tracker code and increasing it's performance.

QUIC y HTTP3 funcionan sobre UDP, asi que en teoría podemos tener BitTorrent usando este mecanismo de network y tener beneficios de rendimiento.

Pero bueno, ya veremos que acabo implementando, este post es un poco summary y futuros objetivos que se pueden realizar. Me interesa tener el entorno de integración local completo con mi tracker para empezar.

Ah, como habréis visto en la captura de arriba y comente en este otro hilo uso Flox: https://www.mediavida.com/foro/dev/the-ugly-org-como-gestionar-dependencias-operacionales-711211

Asi que os compartire mi flox para que podáis hacer git clone y que todo funcione automáticamente en vuestro local.

desu

Algun cambio que he hecho DDD (domain driven design).

La clave siempre es parsear los tipos de datos en los extremos de nuestra aplicación, IO network, disco...

En este caso, cuando leo el torrent por Path he calculado un par de cosas que usaba luego en mi programa, como si el protocolo del tracker es UDP o TCP, el announce_url y demas. Asi otro beneficio es que no exponemos la libreria interna que estamos usando que es un code smell feo, pero bueno, lo voy refactorizando según veo las cosas, tampoco me obsesiono en tener un codigo perfecto cuando aun estamos trabajando el MVP.

Asi que ahora cuando hacemos esto:

let torrent = Torrent::from_path(path)?;

Que es un Result que puede fallar, nos devolverá todos los errores posibles de parsing o validaciones que queramos hacer, por ejemplo, no soportamos nada fuera de udp o tcp.

Y un beneficio de este cambio a la DDD es que el try_from que os explique antes, ahora es un from, el try desaparece, porque ahora no puede fallar nunca esta operación. Asi que el codigo es mas seguro.

impl From<&Torrent> for AnnounceRequest {
    fn from(value: &Torrent) -> Self {
        let announce_url = value.announce_url.to_string();
        let info_hash = value.info_hash.clone();
        let left = value.torrent.length;
        AnnounceRequest {
            announce_url,
            info_hash,
            left,
        }
    }
}

Arreglando el código que teníamos en #18

Al final es un poco la idea de smart constructor, aplicativos (builders con validaciones) y demas... parsear en el constructor de un tipo y que este tipo sea siempre seguro.

1
8 días después
desu

Seguimos pronto!

Como curiosidad, por este proyecto, estoy entrevistando con una empresa jaja

Voy a volver a saltar al peer to peer creo. Arreglamos rapido nuestro tracker de announce local y empezamos a compartir cosas entre nuestros clientes.

1
desu

Estoy usando warp y la API es de lo peor que he visto...

Quieres devolver una respuesta solo con el status? Te dicen que hagas esto:

let route = warp::any()
    .map(warp::reply)
    .map(|reply| {
        warp::reply::with_status(reply, warp::http::StatusCode::CREATED)
    });

Al final uso este constructor de otro modulo para todo y a tomar por culo:

    warp::http::Response::builder()
        .status(warp::http::StatusCode::NO_CONTENT)
        .body("")

Pero vamos, es horrible la interface de Reply y como se compone con todo... menudo caos... claro ejemplo de mal over-engineering y mal default hell

Aqui teneis un ejemplo de tracker de bitorrent, no hay un standard claro de como debe comportarse en el "POST", o al menos no lo he visto. Asi que hago lo mas fácil.

https://github.com/vrnvu/rust-bittorrent/blob/7e2d309112983f187e0c391378fdb6a57b059a75/tracker/src/main.rs

Para el GET del /announce si que esta claro en el protocol, necesitamos aceptar todos estos query params:

 let r = client
        .get(&url)
        .query(&[("peer_id", "00112233445566778899")])
        .query(&[("port", 6881)])
        .query(&[("uploaded", 0)])
        .query(&[("downloaded", 0)])
        .query(&[("left", left)])
        .query(&[("compact", 1)])
        .send()
        .await
        .with_context(|| format!("failed to send request to {}", url))?;

que en mi caso voy a IGNORAR para mi implementación todos salvo que en el futuro decida quitar este POST.

El motivo por el que lo hago asi con un GET y un POST es que me es mas fácil realizar mi caso de uso de tener varios peers que compartan entre ellos SOLO las folders que yo quiera.

desu

Estoy pensando si dejarlo aqui y pasarme a libp2p y hacer algo mas avanzado pero a mas alto nivel utilizando librerías, como el BitTorrent, que ya estan desarrolladas.

Por ejemplo un Redis distribuido peer to peer, con mDNS y Kad, haciendo yo el redis simple a mano como este BitTorrent, pero usando el libp2p para hacerlo distribuido.

Sino tengo interes en el Kid ademas de mi propio tracker, puedo hacer algo como este ejemplo: https://github.com/libp2p/rust-libp2p/blob/master/examples/file-sharing/src/main.rs

Donde tiene un "file-sharing", tansolo tendria que meter el Kad a mi torrent.

1 respuesta
pantocreitor

#24 tengo hecho yo “un redis” distribuido para el curro y a no ser que lo quieras hacer por el tema de hacerlo tú, renta tener redis a pelo

1 respuesta
desu

#25 todo el material que estoy haciendo es didáctico.

hago lo que la gente me pide.

si quieres un redis distribuido puedes usar boltdb o leveldb

2
2 meses después
desu

Update del estado actual:


rust-bittorrent

A minimal BitTorrent client and tracker written in Rust, focusing on simplicity and functionality.

Project Structure

  • bt: The BitTorrent client that handles torrent parsing, peer communication, and file exchange.
  • tracker: The HTTP server that acts as a tracker for peers to announce themselves.
  • models: Shared data models used by both the client and tracker.

Features

BitTorrent Client (bt)

  • Torrent Parsing: Parses .torrent files.
  • Peer Communication: Connects to peers for file exchange.
  • Asynchronous IO: Utilizes Tokio for efficient networking.
  • Upload Mode: Allows sharing of files with other peers.
  • Download Mode: Fetches files from other peers.

Tracker

  • Peer Announcement: Accepts and manages peer announcements.
  • Peer List Management: Provides a list of peers to clients.
  • Content Discovery: Helps peers discover what content is available in the network.

Current State of the Project

The project currently supports basic BitTorrent functionality:

  1. The tracker allows peers to register the content they have available.
  2. Peers can operate in both upload and download modes.
  3. The tracker is used for peer discovery and content discovery.
  4. The client can parse .torrent files and communicate with peers.

Usage and Explanation

1. Tracker Setup

  1. Start the tracker on port 9999 (currently hardcoded):
       tracker 9999
  2. The tracker initializes and listens for HTTP requests from peers.

2. File Upload Process

  1. Run the upload command:
       bt upload --file <path_to_file> [--verbose]
  2. The client reads the file and generates an info hash.
  3. The client sends an HTTP announcement to the tracker with the info hash and peer information.
  4. The tracker stores this information (no health check implemented yet).
  5. The upload process currently blocks the peer process (background operation planned for future).

3. File Download Process

  1. Run the download command:
       bt download --file <path_to_torrent_file> --output_path <output_directory> [--verbose]
  2. The client parses the .torrent file to extract the info hash and tracker URL.
  3. The client sends an HTTP request to the tracker to get a list of peers for the desired file.
  4. The tracker responds with available peers for the requested info hash.
  5. The client initiates the BitTorrent protocol with the available peers:
    • Establishes connections
    • Performs handshakes
    • Exchanges piece information
    • Requests and receives file pieces
  6. The client assembles the received pieces and saves the complete file to the specified output path.

Important Notes

  • The current implementation only supports HTTP trackers.
  • The BitTorrent protocol implementation is functional and compatible with other peers/trackers but lacks advanced optimizations.
  • The upload process currently doesn't implement background seeding (planned for future updates).
  • The system doesn't currently implement peer health checks or sophisticated peer selection strategies.

Future Improvements

  • Implement background seeding for uploads
  • Add health checks for peers
  • Implement more sophisticated peer selection and piece selection algorithms
  • Support for other tracker protocols (e.g., UDP)
  • Implement DHT (Distributed Hash Table) for trackerless operation

TLDR:

  • download chustero de "trackers" publicos http
  • p2p usando mi propio tracker para content discovery
desu

Problemas técnicos que surgen:

Eres un peer que comparte un archivo, te van llegando solicitud de paquetes a transmitir, de varios peers, como lo gestionas? haces una syscall cada vez para abrir, mapeas tus archivos abiertos y tienes un limite?

Tambien importante re-usar los buffers, la gran mayoria de paquetes de torrent son de un tamaño fijo, con excepción del ultimo paquete que sera mas pequeño, por tanto puedes tener una pool de buffers y re-usarlo y no alocar memoria extra.

En el caso de los clientes torrent pues tiene sentido no mappear todo en memoria y saturar la maquina del usuario, pero imaginate que eres un client/server y quieres maximizar las transmisiones o utilizas el protocolo para sincronizar cosas a la rsync. Entonces puedes tener todo abierto y mappeado desde le principio.

desu

De la misma manera cuando es un peer que descarga (ahora mismo estoy trabajando en descargar de múltiple peers) es interesante pensar los pros/cons. La gran mayoría de clientes torrent siguen unos algoritmos que podemos interpretarlos como algoritmos pacíficos, no van a saturar los peers para joderles, no van a hacer peticiones que saturen la red. Pero podríamos tener lo contrario, ataques p2p o clientes que abusen el sistema.

Y como uploaders debemos defendernos de posibles ataques, cortar conexiones y descargas de abusos.

Y como downloaders tenemos que pensar como maximizar nuestras descargas, si uso mi bt con trackers publicos, para bajarte una película de YIFY por ejemplo, seria interesante que mi cliente fuera mas rápido que un QBitorrent o Transmission.

La descarga es: consultar tracker, obtener peers y descargar de peers siguiendo el protocolo. Pero no existe nada mas embebido en el protocolo que por ejemplo, si un peer me da mejor trip-time, abusarlo mientras este conectado.

Y si lo vemos del revés, un atacante podría encontrar nuestro "uploader" en la red p2p y abusar de el para modificar nuestro ratio de download/upload.

O por ejemplo, podríamos tener dentro de una sub-red colaborativa dentro de un tracker privado, para boostear al máximo la sub-red y tener mejores ratios y perjudicar a otros. + down - up.

No he mirado lo que hacen los clientes habituales, pero como digo, se busca un equilibrio y que sea pacifico y ademas prevenir abusos claros.

aren-pulid0

Muy interesante, con una búsqueda rápida en ChatGPT he encontrado que los clientes descargan los ficheros en partes (seeds), supongo que tendrás que mandar esa información también al tracker para saber los diferentes trozos en los que está partido el fichero.

Me causa curiosidad como se ensambla el fichero final 🤔, estaría bien que si desarrollas esta parte lo explicaras

1 respuesta