Developing over SSH

This article is part of the Building a Rust service with Nix series.

With the previous part's VM still running, let's try connecting to our machine over SSH.

Network addresses, loopback and IP nets

Normally, to connect to a machine, you'd find its IP address. On Linux, a decade ago, you would've used ifconfig. Nowadays you can use ip addr:

The ip addr command output, run in VirtualBox

The loopback interface (lo) is local, so it's not useful to reach the box from the outside: you can see it can be accessed over IPv4 at address but not just! What we're reading here is, which corresponds to the range -


This is useful, as it lets us have services that listen on different loopback IPv4 addresses, and apply different routing rules to different addresses.

For IPv6 however, ::1/128 indicates a single address, ::1 (which is the short form of 0:0:0:0:0:0:0:1):

Pinging localhost over IPv6, at ::1

We can use any of these addresses to connect to our VM over SSH, from our VM.

Having the VM SSH to itself by doing ssh ::1, and entering a password

...but that's not very useful. We already had terminal access there, through the VirtualBox GUI.

What we'd like is to connect to it from the outside. We saw earlier that we had another network interface, enp0s3. These used to be called something like eth0 (for ethernet), but now the scheme is:

You can read more about the new naming scheme if you're curious.

My VM had an IPv4 address of (with a range of, according to an IP subnet calculator such as this one), and I am able to ping that address from the VM (from the "guest"):

ping works from the inside

But not from the host (still Windows 11, here with the wonderful Windows Terminal and the latest PowerShell 7):

ping doesn't work from the host

And that is working as designed. The guest is on its own, separate network. In fact, my other VM also has an address of on its own network.

That's why we set up port forwarding in the previous part: it has VirtualBox listen on port 2223, and forward connections to the VM's private network.

We can make sure it's set up correctly with PowerShell (this is Windows-specific, but there's equivalents on Linux and macOS that are a web search away):

Get-NetTCPConnection -State Listen -LocalPort 2223 and Get-Process -Id (previous command).OwningProcess show that VBoxHeadless is listening on port 2223 on address

And sure enough, it accepts TCP connections on that port:

Test-NetConnection -ComputerName -Port 2223 succeeds

Connecting over SSH

We can connect to it with ssh (again, it's an optional Windows component now, it ships with macOS by default, and look to your Linux distribution's package manager search functionality) like so:

Shell session
$ ssh amos@ -p 2223
The authenticity of host '[]:2223 ([]:2223)' can't be established.
ED25519 key fingerprint is SHA256:zwxa3nLGjzTOLg2m3+jN91fpMH7BWVJkow89tYcygtE.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?

It asks us whether we want to trust that server key, and we do, so we can type "yes" and press enter:

Shell session
$ yes
Warning: Permanently added '[]:2223' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-53-generic x86_64)

 * Documentation:
 * Management:
 * Support:

  System information as of Wed Nov 23 10:48:00 AM UTC 2022

  System load:  0.0                Processes:               107
  Usage of /:   14.3% of 47.93GB   Users logged in:         1
  Memory usage: 1%                 IPv4 address for enp0s3:
  Swap usage:   0%

 * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s
   just raised the bar for easy, resilient and secure K8s cluster deployment.

0 updates can be applied immediately.

Last login: Wed Nov 23 09:59:30 2022 from ::1
amos@miles:~$ whoami

(Press Ctrl-D to exit out of the SSH session. This indicates "end of file").

However, it's not very convenient to have to type that whole ssh amos@ -p 2223 command every time, so we can add an entry to the host's ~/.ssh/config config file instead.

Cool bear's hot tip

~ denotes your home directory, and it'll be familiar to you if you're on Linux or macOS. It also works in PowerShell, which means you can do something like code ~/.ssh/config to edit it.

If the ~/.ssh directory doesn't exist yet, mkdir ~/.ssh should work on all three OSes, again — there's a Windows alias for it.

The config section looks like:

# in ~/.ssh/config

Host miles
	Port 2223
	User amos
	ForwardAgent yes

Mhh what's that ForwardAgent thingy doing here?

Well, you know how, from our host operating system, we have an SSH keypair, added to an SSH agent, that we use to authenticate with GitHub and connect to our VM?


I mean... oh, yes okay, Windows ships with ssh-agent too nowadays, or you can use 1Password for this, so yes I guess so?

Yes it does:

Shell session
$ echo $env:OS

$ ssh-add -l
2048 SHA256:4txMCM8iFJaOmrB7qVAMNwSdy7KUbVvqMrcBdLd/VXo teleport:fasterthanlime (RSA-CERT)
256 SHA256:IEoy+ad7M0Mcy7lts4KLk2q0ca+i/9yyUBmd0+Cy9rY july-2020 (ED25519)
4096 SHA256:r8YfVEk6CVCO9S4ykJZew2qM+cSR/nFWLs8Ovul6hMk amos@tails (RSA)

Well look at this: if we use the VirtualBox GUI to access the VM and try to talk to GitHub, it doesn't know who we are:

ssh -T from the guest prints: Permission denied (public key)

But if we connect from our host to our guest, then from that SSH session, try to talk to GitHub, it does!

Shell session
$ ~
❯ ssh miles
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-53-generic x86_64)

(cut: spammy Ubuntu banner)

Last login: Wed Nov 23 11:00:26 2022 from
amos@miles:~$ ssh -T
Hi fasterthanlime! You've successfully authenticated, but GitHub does not provide shell access.
Cool bear's hot tip

If this doesn't work for you, make sure ssh -T works from the host first.

The GitHub docs are helpful there!

Copying files

Now that we're connected to our VM, we can do something useful there!

For example, we can create a text file:

Shell session
amos@miles:~$ echo "I was made inside the guest" > hello.txt
amos@miles:~$ cat hello.txt
I was made inside the guest

And then copy it from the guest to the host. This is run from the host:

Shell session
❯ scp miles:~/hello.txt .
hello.txt                                                                                         100%   28     0.0KB/s   00:00

~ took 4s
❯ cat hello.txt
I was made inside the guest

That's it. scp stands for "secure copy" and uses ssh for data transfer, uses the same authentication mechanisms, etc.

You can also copy files to the guest, with scp ./some-host-file.txt miles:~/path-on-guest.

Installing a Rust toolchain

From the guest, let's install a rust toolchain with rustup:

Shell session
amos@miles:~$ curl --proto '=https' --tlsv1.2 -sSf | sh
info: downloading installer

Welcome to Rust!

This will download and install the official compiler for the Rust
programming language, and its package manager, Cargo.


Current installation options:

   default host triple: x86_64-unknown-linux-gnu
     default toolchain: stable (default)
               profile: default
  modify PATH variable: yes

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation

info: profile set to 'default'
info: default host triple is x86_64-unknown-linux-gnu
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
info: latest update on 2022-11-03, rust version 1.65.0 (897e37553 2022-11-02)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: default toolchain set to 'stable-x86_64-unknown-linux-gnu'

  stable-x86_64-unknown-linux-gnu installed - rustc 1.65.0 (897e37553 2022-11-02)

Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, run:
source "$HOME/.cargo/env"

Like the installer just said, we can source ~/.cargo/env to get "cargo" in our $PATH.


Well, it currently isn't:

Shell session
amos@miles:~$ cargo
Command 'cargo' not found, but can be installed with:
sudo snap install rustup  # version 1.24.3, or
sudo apt  install cargo   # version 0.60.0ubuntu1-0ubuntu1~22.04.1
See 'snap info rustup' for additional versions.
amos@miles:~$ which cargo

But if we source ~/.cargo/env, then it is!

Shell session
amos@miles:~$ source ~/.cargo/env
amos@miles:~$ which cargo
amos@miles:~$ cargo -vV
cargo 1.65.0 (4bc8f24d3 2022-10-20)
release: 1.65.0
commit-hash: 4bc8f24d3e899462e43621aab981f6383a370365
commit-date: 2022-10-20
host: x86_64-unknown-linux-gnu
libgit2: 1.5.0 (sys:0.15.0 vendored)
libcurl: 7.83.1-DEV (sys:0.4.55+curl-7.83.1 vendored ssl:OpenSSL/1.1.1q)
os: Ubuntu 22.04 (jammy) [64-bit]

Installing a web server

Now, we can use cargo to install a web server, like, say, sfz:

Shell session
amos@miles:~$ cargo install sfz
    Updating index
       Fetch [===>                     ]  16.54%, 11.34MiB/s

...because it's the first time we need the index, it has to download the whole thing. This might take a while, even with fast internet.

Wait, it has to download an index of all crates ever published on

For now, yes. There's a sparse_index RFC I like a lot, but we're on a stable version of cargo right now, so we don't get to use it, yet.

Eventually, our command... fails with this:

Shell session
  Downloaded hyper v0.14.23
  Downloaded chrono-tz-build v0.0.3
  Downloaded 130 crates (9.0 MB) in 0.67s (largest was `brotli` at 1.4 MB)
   Compiling version_check v0.9.4
   Compiling proc-macro2 v1.0.47
error: linker `cc` not found
  = note: No such file or directory (os error 2)

error: could not compile `proc-macro2` due to previous error
error: failed to compile `sfz v0.7.1`, intermediate artifacts can be found at `/tmp/cargo-installsTSzh7`

Oh no.

Never fear, we just need to ask our friendly neighborhood package manager to install a couple things.

It says cc but really, we want gcc here.

Shell session
amos@miles:~$ sudo apt install gcc
[sudo] password for amos:
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages were automatically installed and are no longer required:
  libflashrom1 libftdi1-2
Use 'sudo apt autoremove' to remove them.
The following additional packages will be installed:
  cpp cpp-11 fontconfig-config fonts-dejavu-core gcc-11 gcc-11-base libasan6 libatomic1 libc-dev-bin libc-devtools
  libc6-dev libcc1-0 libcrypt-dev libdeflate0 libfontconfig1 libgcc-11-dev libgd3 libgomp1 libisl23 libitm1 libjbig0
  libjpeg-turbo8 libjpeg8 liblsan0 libmpc3 libnsl-dev libquadmath0 libtiff5 libtirpc-dev libtsan0 libubsan1 libwebp7
  libxpm4 linux-libc-dev manpages-dev rpcsvc-proto
Suggested packages:
  cpp-doc gcc-11-locales gcc-multilib make autoconf automake libtool flex bison gdb gcc-doc gcc-11-multilib gcc-11-doc
  glibc-doc libgd-tools
The following NEW packages will be installed:
  cpp cpp-11 fontconfig-config fonts-dejavu-core gcc gcc-11 gcc-11-base libasan6 libatomic1 libc-dev-bin libc-devtools
  libc6-dev libcc1-0 libcrypt-dev libdeflate0 libfontconfig1 libgcc-11-dev libgd3 libgomp1 libisl23 libitm1 libjbig0
  libjpeg-turbo8 libjpeg8 liblsan0 libmpc3 libnsl-dev libquadmath0 libtiff5 libtirpc-dev libtsan0 libubsan1 libwebp7
  libxpm4 linux-libc-dev manpages-dev rpcsvc-proto
0 upgraded, 37 newly installed, 0 to remove and 4 not upgraded.
Need to get 48.6 MB of archives.
After this operation, 152 MB of additional disk space will be used.
Do you want to continue? [Y/n]

Since Y (yes) is capitalized here, we can simply press "Enter" to continue.

And just like that we have a cc command in $PATH:

Shell session
amos@miles:~$ which cc

It's.. a symbolic link:

Shell session
amos@miles:~$ ls -lhA /usr/bin/cc
lrwxrwxrwx 1 root root 20 Nov 23 11:19 /usr/bin/cc -> /etc/alternatives/cc

...which is managed by update-alternatives, a very Debian thing:

Shell session
$ update-alternatives --list cc

And, yeah. It's just gcc.

Let's try again!

Shell session
$ cargo install sfz
   Compiling qstring v0.7.2
   Compiling tokio-util v0.7.4
   Compiling sfz v0.7.1
    Finished release [optimized] target(s) in 2m 02s
  Installing /home/amos/.cargo/bin/sfz
   Installed package `sfz v0.7.1` (executable `sfz`)

And this time it works!

It's in $PATH, too, because we sourced ~/.cargo/env earlier:

Shell session
amos@miles:~$ which sfz

Let's run it:

Shell session
amos@miles:~$ sfz
Files served on
Cool bear's hot tip

You can exit out of this with Ctrl+C, which sends an interrupt signal (SIGINT) to sfz.

The difference with "exiting out of an SSH session" is that sfz isn't reading the standard input, so it won't detect an EOF there. But the default signal handler will quit the (otherwise long-running) program.

And, from another terminal also in the guest, we can see that it does work:

Shell session
amos@miles:~$ curl -I 0:5000
HTTP/1.1 200 OK
server: sfz/0.7.1
accept-ranges: bytes
content-type: text/html; charset=utf-8
content-length: 2166
date: Wed, 23 Nov 2022 11:26:46 GMT

Accessing our web server from the host

However, the question is... how are we going to access all this from the host?

Well, for starters, it's listening on, which is definitely not accessible from the outside.

You're right — we can have sfz listen on instead, which means "all network interfaces".

Shell session
amos@miles:~$ sfz -b
Files served on

...but it won't help much. Remember: the VM's enp0s3 interface is connected to a network that's private to this VM. We can't reach it even from the host:

Shell session
$ curl -v -I
*   Trying
* connect to port 5000 failed: Timed out
* Failed to connect to port 5000 after 21001 ms: Timed out
* Closing connection 0
curl: (28) Failed to connect to port 5000 after 21001 ms: Timed out

There's a couple options here: we could add another port forwarding rule in the VirtualBox network configuration. Or we could forward that port over SSH!

Let's exit out of sfz with Ctrl+C and out of our current SSH session with Ctrl+D:

Shell session
amos@miles:~$ sfz -b
Files served on
Connection to closed.

And then log back into our VM, but asking SSH to forward something for us:

Shell session
$ ssh -L 5000:localhost:5000 miles

Last login: Wed Nov 23 11:26:31 2022 from

This has the ssh client listen on port 5000 on the host, and whenever it accepts connections on there, it forwards them to localhost:5000 from the perspective of the guest.

Cool bear's hot tip

It seems odd at first that we have to specify localhost in there, because for simple scenarios, it's what we want. But imagine we're connecting to some host that's part of a private network, and we're simply using it as a relay to reach a third host.

We might end up doing something like ssh -L 5000:third-host:5000 relay-host.

Now, we can run sfz again:

Shell session
$ amos@miles:~$ sfz
Files served on

And access it from our.. host OS!

It's confusing because "host" means "some device on a network" and also "the operating system that runs the VM hypervisor, in which the guest OS is executed".

In this case, I mean the latter.

Shell session
❯ curl -I http://localhost:5000
HTTP/1.1 200 OK
server: sfz/0.7.1
accept-ranges: bytes
content-type: text/html; charset=utf-8
content-length: 2166
date: Wed, 23 Nov 2022 11:42:27 GMT

In fact, we can open it in a browser:

A firefox window open to localhost:5000 showing a file index, with amos/ and a hello.txt file

In fact, here's our hello.txt file. Hello to you too!

The same firefox window now showing /hello.txt

VSCode's Remote SSH feature

You now know enough to develop server applications remotely on your Linux VM.

However, because this is my series and I do what I want, I'll show you why it's nice to use VSCode for this.

First off, you'll need to download / install VSCode, through the official website or some package manager.

Cool bear's hot tip

There's open-source distributions of VSCode like VSCodium (similar to Chrome -> Chromium), but they might not have the extensions we want, so, if you pick that, you're on your own.

Make sure you have the "Remote - SSH" extension installed (from the command menu, pick "Install extensions", or click on the icon from the leftmost bar that looks like a package: a grid of four squares, one of them trying to break free):

VS Code, with the Remote - SSH extension page open

(There's also "Remote Development" group that includes "Remote - SSH", "Remote - WSL" and "Dev Containers", if you're interested in that).

Once the extension is installed, you can open the command palette (F1 on Linux/Windows, Shift+Cmd+P on macOS), start typing "Connec...", and pick "Remote-SSH: Connect to host"

VSCode's command palette showing the mentioned command

Next up, VSCode will read your ~/.ssh/config and suggest picking one of the hosts it found there:

Mine shows sonic (my main VM), miles (which we just set up) and brw (a remote dev environment)

Picking "miles" opens a new window, with another prompt asking us to select the platform, let's pick Linux:

A platform picker, there's Linux, Windows and macOS

VSCode will then download its server component on the remote machine (our guest):

Setting up SSH Host miles: Downloading VS Code Server

And we're good to go!

We can open the built-in terminal with CmdOrCtrl+Backquote, or clicking this little icon, then selecting the Terminal tab:

A button in VSCode's bottom bar that shows errors and warnings. Hovering it shows 'No problems', which, good!

And tada, we have a terminal on our guest VM:

VSCode's bottom pane is opened, the Terminal tab is focused, and I ran the 'hostname' and 'whoami' command to show that it does work

From there, we can run sfz again:

Cool bear's hot tip

If you get "Server error: error creating server listener: Address already in use (os error 98)", that probably means you still have sfz running in another terminal session.

Make sure to exit out of it first! (If everything else fails, you can try killall sfz).

Shell session
amos@miles:~$ sfz
Files served on

Now let's try opening it in a browser...

And it works!

Wait, didn't we forget something?

Forget something? Like what?

Well, when we did it with SSH in command line, we had to forward port 5000 from the host to the guest. Is that still active somehow?

Oh, no, VSCode just detected that something inside the VM is listening on port 5000, and it set up port forwarding for us, over the same SSH connection.

VSCode's bottom tab, but this time the "Ports" tab is active, showing that port 5000 is forwarded

Making VSCode all comfy and nice

I use the excellent Iosevka font, paired with either of "GitHub Light" / "Github Dark" (following the time of day), and these extensions:

I have a lot more extensions, some minor QoL things (like "FontSize Shortcuts", "TODO Highlight" or "vscode-position"), some language-specific ("x86 and x86_64 Assembly", "Nix", "systemd-unit-file"), and some bigger, like "Git Blame", "GitHub Pull Requests and Issues".

This article is part 2 of the Building a Rust service with Nix series.

Read the next part

If you liked what you saw, please support my work!

Github logo Donate on GitHub Patreon logo Donate on Patreon

Looking for the homepage?
Another article: Rust modules vs files