Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Open a tunnel

SSH is most commonly used to execute commands on a remote server, but another important use of the protocol is for tunnelling TCP/IP connections. There are two ways to open a tunnel:

  • Local forwarding (aka ssh -L): the client asks the server to open a TCP connection to another host. In Makiko, this is implemented by Client::connect_tunnel(). In this chapter, we will learn how to use this method.
  • Remote forwarding (aka ssh -R): the client asks the server to listen on a port, and the server will open a tunnel for every TCP connection on this port. In Makiko, this is implemented by Client::bind_tunnel(), Client::unbind_tunnel() and the ClientEvent::Tunnel variant of ClientEvent. We won’t cover remote forwarding in this tutorial, please refer to the API documentation for details.

Open a tunnel

To demonstrate the use of tunnels, we will open a TCP/IP connection from the server to httpbin.org and we will manually send a simple HTTP request over this connection. To open the tunnel, we use the method Client::connect_tunnel(), which needs:

  • A ChannelConfig, which configures the underlying SSH channel. Similar to the previous chapter, you can change the configuration to tune performance, but the default configuration should be sufficient for now.
  • The address that the server will connect to, given as a pair of host and port. The host can be specified as an IP address or as a domain name. We will connect to "httpbin.org" on port 80.
  • The address of the “originator” of the connection. This is also specified as a pair of host and port, but the host should be an IP address. For example, ssh -L will set this to the remote address of the local connection that is forwarded to the server, but we will use the null IP address and port in this tutorial.
// Open a tunnel from the server.
let channel_config = makiko::ChannelConfig::default();
let connect_addr = ("httpbin.org".into(), 80);
let origin_addr = ("0.0.0.0".into(), 0);
let (tunnel, mut tunnel_rx) = client.connect_tunnel(channel_config, connect_addr, origin_addr).await
    .expect("Could not open a tunnel");

In a direct analogy to Client::open_session(), the Client::connect_tunnel() method returns a pair of objects: a Tunnel object to send requests to the tunnel, and a TunnelReceiver to receive events from the tunnel.

Handle tunnel events

We will use the same pattern as before to handle events from the tunnel: we spawn a task and receive the events, represented as enum TunnelEvent, using TunnelReceiver::recv(). This method returns None when the tunnel closes:

let tunnel_event_task = tokio::task::spawn(async move {
    loop {
        // Wait for the next event.
        let event = tunnel_rx.recv().await
            .expect("Error while receiving tunnel event");

        // Exit the loop when the tunnel has closed.
        let Some(event) = event else {
            break
        };

        match event {
            ... // Handle the event
        }
    }
});

As with all Receiver objects in Makiko, you must receive the events from the TunnelReceiver in a timely manner. Makiko uses a bounded buffer of events, which will become full if you don’t receive the event, causing the client to block.

Data received from the channel

Events on a tunnel are quite simple, you can either get a chunk of data with the Data variant, or an end-of-file event with the Eof variant:

match event {
    // Handle data received from the tunnel.
    makiko::TunnelEvent::Data(data) => {
        println!("Received: {:?}", data);
    },

    // Handle EOF from the tunnel.
    makiko::TunnelEvent::Eof => {
        println!("Received eof");
        break
    },

    _ => {},
}

Send data to the channel

Back on the main task, we can use the Tunnel::send_data() method to send bytes over the tunnel. In our case, we send a very simple HTTP request to httpbin.org/get:

// Send data to the tunnel
tunnel.send_data("GET /get HTTP/1.0\r\nhost: httpbin.org\r\n\r\n".into()).await
    .expect("Could not send data to the tunnel");

We can also close the tunnel for sending by calling Tunnel::send_eof(). However, the OpenSSH server will close the tunnel prematurely if we do so, so we comment out this call:

// Do not close the outbound side of the tunnel, because this causes OpenSSH to prematurely
// close the tunnel.
/*
tunnel.send_eof().await
    .expect("Could not send EOF to the tunnel");
*/

Finally, we wait until the tunnel is closed and the event handling task terminates:

// Wait for the task that handles tunnel events
tunnel_event_task.await.unwrap();

Full code for this tutorial can be found in examples/tutorial_6.rs. If you don’t use the example server for this tutorial, you may need to change the code to use a different username and password.

Next: Verify the server key