DéveloppeurWeb.Com
    DéveloppeurWeb.Com
    • Agile Zone
    • AI Zone
    • Cloud Zone
    • Database Zone
    • DevOps Zone
    • Integration Zone
    • Web Dev Zone
    DéveloppeurWeb.Com
    Home»Uncategorized»Écrire un tunnel HTTP(S) moderne en Rust
    Uncategorized

    Écrire un tunnel HTTP(S) moderne en Rust

    février 1, 2023
    Écrire un tunnel HTTP(S) moderne en Rust
    Share
    Facebook Twitter Pinterest Reddit WhatsApp Email

    Apprenez à écrire rapidement des applications performantes et sûres dans Rust. Cet article vous guide dans la conception et la mise en œuvre d’un tunnel HTTP et couvre les bases de la création d’applications robustes, évolutives et observables.

    Rust : Performance, Fiabilité, Productivité

    Il y a environ un an, j’ai commencé à apprendre Rust. Les deux premières semaines ont été assez douloureuses. Rien compilé ; Je ne savais pas comment faire les opérations de base ; Je ne pouvais pas exécuter un programme simple. Mais petit à petit, j’ai commencé à comprendre ce que voulait le compilateur. Plus encore, j’ai réalisé que cela force la pensée juste et le comportement correct.

    Oui, parfois, vous devez écrire des constructions apparemment redondantes. Mais il vaut mieux ne pas compiler un programme correct que d’en compiler un incorrect. Cela rend les erreurs plus difficiles.

    Peu de temps après, je suis devenu plus ou moins productif et j’ai finalement pu faire ce que je voulais. Eh bien, la plupart du temps.

    Par curiosité, j’ai décidé de relever un défi un peu plus complexe : implémenter un tunnel HTTP dans Rust. Cela s’est avéré étonnamment facile à faire et a pris environ une journée, ce qui est assez impressionnant. J’ai assemblé tokio, clap, serde et plusieurs autres caisses très utiles. Permettez-moi de partager les connaissances que j’ai acquises au cours de ce défi passionnant et d’expliquer pourquoi j’ai organisé l’application de cette façon. J’espère que vous l’apprécierez.

    Qu’est-ce qu’un tunnel HTTP ?

    En termes simples, il s’agit d’un VPN léger que vous pouvez configurer avec votre navigateur afin que votre fournisseur d’accès Internet ne puisse pas bloquer ou suivre votre activité, et que les serveurs Web ne voient pas votre adresse IP.

    Si vous le souhaitez, vous pouvez le tester localement avec votre navigateur, par exemple avec Firefox (sinon, ignorez simplement cette section pour l’instant).

    Didacticiel

    1. Installez l’application à l’aide de Cargo

    $ cargo install http-tunnel

    2. Commencez

    $ http-tunnel --bind 0.0.0.0:8080 http

    Vous pouvez également consulter le référentiel GitHub du tunnel HTTP pour les instructions de construction/d’installation.

    Maintenant, vous pouvez aller dans votre navigateur et définir le HTTP Proxy pour localhost:8080. Par exemple, dans Firefox, recherchez proxy dans la section préférences :

    Recherche par procuration

    Trouvez les paramètres de proxy, spécifiez-le pour HTTP Proxyet vérifiez-le pour HTTPS:

    Paramètres de connexion

    Définissez le proxy sur juste construit http_tunnel.Vous pouvez visiter plusieurs pages Web et consulter les ./logs/application.log file—tout votre trafic passait par le tunnel. Par example:

    Circulation

    Maintenant, parcourons le processus depuis le début.

    Concevoir l’application

    Chaque application commence par une conception, ce qui signifie que nous devons définir les éléments suivants :

    • Exigences fonctionnelles.
    • Prérogatives non fonctionnelles.
    • Abstractions et composants d’application.

    Exigences fonctionnelles

    Nous devons suivre les spécifications décrites ici :

    Négocier un objectif avec un HTTP CONNECT demande. Par exemple, si le client souhaite créer un tunnel vers le site Web de Wikipédia, la requête ressemblera à ceci :

    CONNECT www.wikipedia.org:443 HTTP/1.1
    ...

    Suivi d’une réponse comme ci-dessous :

    Après ce point, il suffit de relayer le trafic TCP dans les deux sens jusqu’à ce que l’un des côtés le ferme ou qu’une erreur d’E/S se produise.

    Le tunnel HTTP devrait fonctionner pour les deux HTTP et HTTPS.

    Nous devrions également être en mesure de gérer les cibles d’accès/blocage (par exemple, pour bloquer les trackers).

    Prérogatives non fonctionnelles

    Le service ne doit enregistrer aucune information permettant d’identifier les utilisateurs.

    Il doit avoir un débit élevé et une faible latence (il doit être invisible pour les utilisateurs et relativement peu coûteux à exécuter).

    Idéalement, nous voulons qu’il soit résistant aux pics de trafic, fournisse une isolation bruyante des voisins et résiste aux attaques DDoS de base.

    La messagerie d’erreur doit être conviviale pour les développeurs. Nous voulons que le système soit observable pour le dépanner et le régler en production à grande échelle.

    Composants

    Lors de la conception des composants, nous devons décomposer l’application en un ensemble de responsabilités. Voyons d’abord à quoi ressemble notre organigramme :

    Représentation schématique

    Pour implémenter cela, nous pouvons introduire les quatre composants principaux suivants :

    1. Accepteur TCP/TLS
    2. HTTP CONNECT négociateur
    3. Connecteur cible
    4. Relais duplex intégral

    Mise en œuvre

    Accepteur TCP/TLS

    Lorsque nous savons à peu près comment organiser l’application, il est temps de décider quelles dépendances nous devons utiliser. Pour Rust, la meilleure bibliothèque d’E/S que je connaisse est tokio. Dans le tokio famille, il existe de nombreuses bibliothèques dont tokio-tls, ce qui rend les choses beaucoup plus simples. Ainsi, le code de l’accepteur TCP ressemblerait à ceci :

       let mut tcp_listener = TcpListener::bind(&proxy_configuration.bind_address)
            .await
            .map_err(|e| {
                error!(
                    "Error binding address {}: {}",
                    &proxy_configuration.bind_address, e
                );
                e
            })?;

    Et puis l’ensemble de la boucle accepteur + lancement des gestionnaires de connexion asynchrones serait :

        loop {
            // Asynchronously wait for an inbound socket.
            let socket = tcp_listener.accept().await;
    
            let dns_resolver_ref = dns_resolver.clone();
    
            match socket {
                Ok((stream, _)) => {
                    let config = config.clone();
                    // handle accepted connections asynchronously
                    tokio::spawn(async move { tunnel_stream(&config, stream, dns_resolver_ref).await });
                }
                Err(e) => error!("Failed TCP handshake {}", e),
            }
        }

    Décomposons ce qui se passe ici. Nous acceptons une connexion. Si l’opération a réussi, utilisez tokio::spawn pour créer une nouvelle tâche qui gérera cette connexion. La gestion de la mémoire/de la sécurité des threads se déroule en coulisses. La gestion des contrats à terme est masquée par le async/await sucre de syntaxe.

    Cependant, il y a une question. TcpStream et TlsStream sont des objets différents, mais la manipulation des deux est exactement la même. Peut-on réutiliser le même code ? Dans Rust, l’abstraction est réalisée via Traitsqui sont super pratiques :

    /// Tunnel via a client connection.
    async fn tunnel_stream<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
        config: &ProxyConfiguration,
        client_connection: C,
        dns_resolver: DnsResolver,
    ) -> io::Result<()> {...}

    Le flux doit implémenter :

    • AsyncRead /Write: nous permet de le lire/écrire de manière asynchrone.
    • Send: Pour pouvoir envoyer entre les threads.
    • Unpin: Être mobile (sinon, on ne pourra pas faire async move et tokio::spawn pour créer un async tâche).
    • 'static : Pour indiquer qu’il peut vivre jusqu’à l’arrêt de l’application et ne dépend pas de la destruction d’un autre objet.

    Que notre TCP/TLS flux sont exactement. Cependant, nous pouvons maintenant voir qu’il n’est pas nécessaire TCP/TLS ruisseaux. Ce code fonctionnerait pour UDP, QUICou alors ICMP. Par exemple, il peut envelopper n’importe quel protocole dans n’importe quel autre protocole ou lui-même.

    En d’autres termes, ce code est réutilisable, extensible et prêt pour la migration, qui se produit tôt ou tard.

    Négociateur de connexion HTTP et connecteur cible

    Arrêtons-nous une seconde et réfléchissons à un niveau supérieur. Et si nous pouvions faire abstraction du tunnel HTTP et que nous devions implémenter un tunnel générique ?

    Tunnel générique
    • Nous devons établir des connexions au niveau du transport (L4).
    • Négocier une cible (peu importe comment : HTTP, PPv2, etc.).
    • Établissez une connexion L4 à la cible.
    • Signalez le succès et commencez à relayer les données.

    Une cible pourrait être, par exemple, un autre tunnel. De plus, nous pouvons prendre en charge différents protocoles. Le noyau resterait le même.

    Nous avons déjà vu que le tunnel_stream la méthode fonctionne déjà avec n’importe quel L4 Client<->Tunnel lien.

    #[async_trait]
    pub trait TunnelTarget {
        type Addr;
        fn addr(&self) -> Self::Addr;
    }
    
    #[async_trait]
    pub trait TargetConnector {
        type Target: TunnelTarget + Send + Sync + Sized;
        type Stream: AsyncRead + AsyncWrite + Send + Sized + 'static;
    
        async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::Stream>;
    }

    Ici, nous spécifions deux abstractions :

    1. TunnelTarget est juste quelque chose qui a un Addr – peu importe ce que c’est.
    2. TargetConnector – peut se connecter à cela Addr et doit renvoyer un flux prenant en charge les E/S asynchrones.

    D’accord, mais qu’en est-il de la négociation cible ? Les tokio-utils crate a déjà une abstraction pour cela, nommée Framed flux (avec correspondant Encoder/Decoder traits). Nous devons les mettre en œuvre pour HTTP CONNECT (ou tout autre protocole proxy). Vous pouvez trouver la mise en œuvre ici.

    Relais

    Il ne nous reste qu’un seul composant majeur – qui relaie les données une fois la négociation du tunnel terminée. tokio fournit une méthode pour diviser un flux en deux moitiés : ReadHalf et WriteHalf. Nous pouvons séparer les connexions client et cible et les relayer dans les deux sens :

            let (client_recv, client_send) = io::split(client);
            let (target_recv, target_send) = io::split(target);
    
            let upstream_task =
                tokio::spawn(
                    async move { 
                        upstream_relay.relay_data(client_recv, target_send).await
                    });
    
            let downstream_task =
                tokio::spawn(
                    async move { 
                        downstream_relay.relay_data(target_recv, client_send).await 
                    });

    Où le relay_data(…) la définition ne nécessite rien de plus que la mise en œuvre des abstractions mentionnées ci-dessus. Par exemple, il peut connecter deux moitiés d’un flux :

    /// Relays data in a single direction. E.g.
    pub async fn relay_data<R: AsyncReadExt + Sized, W: AsyncWriteExt + Sized>(
            self,
            mut source: ReadHalf<R>,
            mut dest: WriteHalf<W>,
        ) -> io::Result<RelayStats> {...}

    Et enfin, au lieu d’un simple tunnel HTTP, nous avons un moteur qui peut être utilisé pour construire n’importe quel type de tunnels ou une chaîne de tunnels (par exemple, pour le routage en oignon) sur n’importe quel protocole de transport et proxy :

    /// A connection tunnel.
    ///
    /// # Parameters
    /// * `<H>` - proxy handshake codec for initiating a tunnel.
    ///    It extracts the request message, which contains the target, and, potentially policies.
    ///    It also takes care of encoding a response.
    /// * `<C>` - a connection from from client.
    /// * `<T>` - target connector. It takes result produced by the codec and...
    Share. Facebook Twitter Pinterest LinkedIn WhatsApp Reddit Email
    Add A Comment

    Leave A Reply Cancel Reply

    Catégories

    • Politique de cookies
    • Politique de confidentialité
    • CONTACT
    • Politique du DMCA
    • CONDITIONS D’UTILISATION
    • Avertissement
    © 2023 DéveloppeurWeb.Com.

    Type above and press Enter to search. Press Esc to cancel.