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 byClient::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 byClient::bind_tunnel()
,Client::unbind_tunnel()
and theClientEvent::Tunnel
variant ofClientEvent
. 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 port80
. - 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.