Logo

dev-resources.site

for different kinds of informations.

SSH port forwarding from within Raku code

Published at
1/6/2025
Categories
raku
ssh
Author
bbkr
Categories
2 categories in total
raku
open
ssh
open
Author
4 person written this
bbkr
open
SSH port forwarding from within Raku code

Reminder

In this series I demonstrate how to create on demand SSH tunnel to connect to service in another network directly inside your code. I will be referring to theory described in the previous post, so please make sure you read it first.

Preparation

We will be using SSH::LibSSH module that connects to system libssh C library through built-in NativeCall mechanism. Install Raku module by invoking zef install SSH::LibSSH. As for libssh it is very likely you have it already installed if you use any major Linux distribution.

Boilerplate

Let's start by including required module and setting necessary variables.

use SSH::LibSSH;

my Str $service-host = 'service';
my Int $service-port = 7878;
my Str $jump-host = 'jump';
my Int $jump-port = 22;
my Str $jump-user = 'me';
my Str $jump-private-key-file = '/home/me/.ssh/jump';
my Str $jump-private-key-password = 's3cret!';
my Str $local-host = '127.0.0.1';
my Int $local-port = 8080;
Enter fullscreen mode Exit fullscreen mode

Note is that $service-host is not resolvable or reachable from local network but must be resolvable and reachable from jump host.

Raku is gradually typed, so you can skip type annotations if you like but they are super useful to detect errors in compile time.

As mentioned in previous post - $jump-private-key-password should never be stored in code directly, this is for demonstration purposes only. You can have passwordless SSH key, in such case just skip all key-related variables from following code.

Connect to jump host

ssh-2

my $jump-connection = await SSH::LibSSH.connect(
    host => $jump-host, port => $jump-port, timeout => 4,
    user => $jump-user,
    private-key-file => $jump-private-key-file,
    private-key-file-password => $jump-private-key-password
);
Enter fullscreen mode Exit fullscreen mode

To confirm jump host authenticity you can provide 3 additional parameters: on-server-unknown, on-server-known-changed and on-server-found-other. Their value should be a subroutine accepting one $handler parameter. Jump host hash will be available at $handler.hash and you have $handler.accept-this-time() or $handler.decline() methods to decide if you want to proceed. Default implementations are provided.

Open local listener

ssh-3

There is one cool trick here - if you do not know free local port up front you can provide 0 and system will assign one for you.

my $local-server = IO::Socket::Async.listen($local-host, $local-port);
my $local-listener = $local-server.tap(
    ... # TODO
);
await $local-listener.socket-port;
Enter fullscreen mode Exit fullscreen mode

First we should open asynchronous socket that acts as a Supply of incoming connections, then tap it. Tap is just a subscription to a Supply and we will implement its guts in the next step.

But beware! Having local port listener does not mean we can immediately connect to this port, because those actions are asynchronous. That is what await $local-listener.socket-port part is for. It is a Promise that will be kept when port is actually opened. Promises are thread safe thingies that can be kept or broken once per their lifetime and can carry more info than just boolean state: if you used local port 0 that is how you will receive system assigned local port number - just check value kept by this Promise as $local-listener.socket-port.result.

Open channel from jump to service

ssh-4

my $local-listener = $local-server.tap(
    my $local-listener = $local-server.tap(
    -> $local-connection {
        my $jump-channel = await $jump-connection.forward(
            $service-host, $service-port,
            $local-host, $local-port
        );
        ...
    }
);
Enter fullscreen mode Exit fullscreen mode

Back to tap implementation. It expects closure with one parameter, which will be incoming connection to local. When we receive such connection we must ask jump host to create SSH channel that will pass TCP packets to service host.

Create bidirectional data flow between local connection and SSH channel

Image description

my $local-listener = $local-server.tap(
    my $local-listener = $local-server.tap(
    -> $local-connection {
        my $jump-channel = ...
        react {
            whenever $local-connection.Supply(:bin) {
                $jump-channel.write($_);
                LAST $jump-channel.close();
            }
            whenever $jump-channel.Supply(:bin) {
                $local-connection.write($_);
                LAST $local-connection.close();
            }
        }
    }
);
Enter fullscreen mode Exit fullscreen mode

To establish bidirectional data exchange we can use reactive programming. Both local connection and jump channel have Supplies that will give us incoming data. All we need to do it pass data from local connection to jump channel and from jump channel to local connection.

LAST phaser is used to gracefully close corresponding side when one of the Supplies closes. In happy path it is local connection that will be closing SSH channel.

To capture any errors you can use QUIT phasers.

Done

To recap here is whole code with marked spot where you can implement your own logic that requires connection to inaccessible service host but can now be achieved by connecting to local host that forwards it to service host through jump host.

use SSH::LibSSH;

my Str $service-host = 'service';
my Int $service-port = 7878;
my Str $jump-host = 'jump';
my Int $jump-port = 22;
my Str $jump-user = 'me';
my Str $jump-private-key-file = '/home/me/.ssh/jump';
my Str $jump-private-key-password = 's3cret!';
my Str $local-host = '127.0.0.1';
my Int $local-port = 8080;

my $jump-connection = await SSH::LibSSH.connect(
    host => $jump-host, port => $jump-port, timeout => 4,
    user => $jump-user,
    private-key-file => $jump-private-key-file,
    private-key-file-password => $jump-private-key-password
);

my $local-server = IO::Socket::Async.listen($local-host, $local-port);
my $local-listener = $local-server.tap(
    -> $local-connection {
        my $jump-channel = await $jump-connection.forward(
            $service-host, $service-port,
            $local-host, $local-port
        );
        react {
            whenever $local-connection.Supply(:bin) {
                $jump-channel.write($_);
                LAST $jump-channel.close();
            }
            whenever $jump-channel.Supply(:bin) {
                $local-connection.write($_);
                LAST $local-connection.close();
            }
        }
    }
);
await $local-listener.socket-port;

printf "Connect to %s:%d to actually connect to %s:%d.\n",
    $local-host, $local-listener.socket-port.result,
    $service-host, $service-port;

# your logic that requires service host goes here

# gracefully close jump SSH channel and connection when done
$local-listener.close();
$jump-connection.close();
Enter fullscreen mode Exit fullscreen mode

When using this flow make sure you truly closed local connection first before closing local listener and jump connection. Common mistake is forgetting for example that HTTP modules with keep-alive capability will keep connection open in background.

Alternatives

  • SSH::LibSSH::Tunnel module implements exactly the same solution but in different manner - it uses separate thread and single react block wrapping whole logic under the hood.
ssh Article's
30 articles in total
Favicon
How to Set Up Key-Based and Password-Based SSH for a Newly Created User on an EC2 Instance
Favicon
SSH Keys | Change the label of the public key
Favicon
่ฎฉๅฎ‰ๅ“ๆ‰‹ๆœบไธๅ†ๅƒ็ฐ๏ผšๅœจๅฎ‰ๅ“ๆ‰‹ๆœบไธŠๆญๅปบ Rust ๅผ€ๅ‘็Žฏๅขƒ
Favicon
SSH port forwarding from within code
Favicon
Mastering Ansible on macOS A Step by Step Guide
Favicon
kkTerminal โ€”โ€” A terminal for Web SSH connection
Favicon
Set Up SSH in 1 Minute Setup Like a Pro (With Some Fun Along the Way)
Favicon
How to Configure GitHub Authentication Using SSH Certificates
Favicon
Understanding SSH: Secure Shell Protocol
Favicon
Check gitlab ssh key auth
Favicon
How I Secured Port 22
Favicon
SSH port forwarding from within Raku code
Favicon
Changing an established SSH connection without disconnecting
Favicon
SSH port forwarding from within Rust code
Favicon
Configure SSH Passwordless Login from Windows to Linux
Favicon
Push to multiple GitHub accounts!
Favicon
Access to Google Cloud Virtual Machine through SSH
Favicon
Large file transfer from VPS to local machine
Favicon
Secure Your Ubuntu VPS: Restrict SSH Access to a Specific IP
Favicon
Accessing Remote Databases Without VPN Using SSH Tunnels
Favicon
Carla Simulator 1 : How to Set Up CARLA Simulator ๐ŸŽ๏ธ๐Ÿ”ฅ
Favicon
Getting Started with Oysape: Exploring Task and Pipeline
Favicon
Increase Debian based Linux VPS serverโ€™s security
Favicon
Splunk - SSH Dashboard Creation
Favicon
Debugging SSH connections: A Comprehensive Guide
Favicon
Understanding SSH Key Pairs: A Developer's Guide
Favicon
SSH Config File - Forgotten Gem
Favicon
SSH kalitini Github.com'ga qo'shish
Favicon
Using SSH to Connect Local Git to Remote Repositories
Favicon
Quickly and Easily Manage Multiple SSH and GPG Keys Across Git Repositories

Featured ones: