Sometimes we are given access via ssh to nodes that do not have, for policy or technical reasons, access to the internet (i.e. they cannot make outbound connections). Depending on the policies, we may be able to open reverse SSH tunnels, so things are not so bad.

Recently I discovered that OpenSSH comes with a SOCKS proxy server integrated. This is probably a well known feature of OpenSSH but I thought it was interesting to share how it can be used.

SOCKS

Nowadays, access to the Internet is ubiquitous and most of the time assumed as a fact. However, in some circumstances, direct access to the internet is not available or not desirable. In those cases we can resort on proxy servers that act as intermediaries between the Internet and the node without direct access.

Many tools used commonly assume one is connected to the Internet: package managers such as pip and cargo can automatically download the files required to install a package. If no outbound connection is possible, software deployment and installation becomes complicated.

However, most of the time, those tools only require HTTP/HTTPS support. So a proxy that only forwards HTTP and HTTPS requests is enough. Examples of these kind of proxies are tinyproxy and squid.

SOCKS, is a general proxy protocol that can be used for any TCP connection, not only those for HTTP/HTTPS. An interesting thing is that ssh comes with an integrated SOCKS proxy which is relatively easy to use. Often most tools that can use a HTTP/HTTPS proxy can also use a SOCKS proxy so this is a handy option to consider.

Example: Installing Rust through a proxy

If we try to install Rust on a machine that does not allow outbound connections, this is what happens. (Let’s ignore the question whether piping a download directly to the shell is a reasonable thing to do).

user@no-internet$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

This command will likely time out after a long time because outbound connections are silently dropped and the installation will fail.

Set up proxy server

To address this, let’s first open a SOCKS proxy using ssh on our local machine (with-internet). This machine must have internet access (change user to your username). ssh will request you to authenticate (via password or ssh key).

user@with-internet$ ssh -N -D 127.0.0.1:12345 user@localhost

The flag -N means not to execute a command and -D interface:port means to open the port bound to the interface. This is the SOCKS proxy. In this example we are opening port 12345 and binding it to the 127.0.0.1 (localhost) interface. We are using the same machine as the proxy, hence user@localhost (it is possible to use another node, but we don’t have to given that with-internet already can connect to the internet). This must stay running so you will have to open another terminal and set up the reverse tunnel.

To set up the reverse tunnel do the following.

user@with-internet$ ssh -R 127.0.0.1:9999:127.0.0.1:12345 -N user@no-internet

This opens the port 9999 in the host without internet (no-internet) and binds it to its localhost (i.e. the localhost of no-internet) then it tunnels it to the port 12345 bound to the interface 127.0.0.1 of our local node (with-internet). Again this will not run any command (due to -N) and the syntax of -R is -R remote-interface:remote-port:local-interface:local-port. Keep this command running.

Note: Because we are using an unprivileged port on no-internet and the -D option does not allow setting authentication, anyone in no-internet could proxy connections through with-internet. Do this only on a no-internet host you trust.

Proxy configuration

Now we can setup curl to use a socks proxy. We do this with the --proxy-option. For convenience we will first download the installation script into a file.

user@no-internet$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
                       --proxy socks5://localhost:9999 -o  install-rust.sh

We can do a quick check that it contains what we expect

user@no-internet$ head install-rust.sh 
#!/bin/sh
# shellcheck shell=dash

# This is just a little script that can be downloaded from the internet to
# install rustup. It just does platform detection, downloads the installer
# and runs it.

# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local`
# extension. Note: Most shells limit `local` to 1 var per line, contra bash.

Install Rust

We can set up https_proxy environment variable to point to the SOCKS server so it is used by the installation script.

user@no-internet$ export https_proxy=socks5://localhost:9999

Now we are read to install Rust using the script we downloaded.

user@no-internet $ bash install-rust.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.

Rustup metadata and toolchains will be installed into the Rustup
home directory, located at:

  /home/user/.rustup

This can be modified with the RUSTUP_HOME environment variable.

The Cargo home directory located at:

  /home/user/.cargo

This can be modified with the CARGO_HOME environment variable.

The cargo, rustc, rustup and other commands will be added to
Cargo's bin directory, located at:

  /home/user/.cargo/bin

This path will then be added to your PATH environment variable by
modifying the profile files located at:

  /home/user/.profile
  /home/user/.zshenv

You can uninstall at any time with rustup self uninstall and
these changes will be reverted.

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
>1

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 2021-12-02, rust version 1.57.0 (f1edd0429 2021-11-29)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
 24.9 MiB /  24.9 MiB (100 %)  19.9 MiB/s in  1s ETA:  0s
info: downloading component 'rustc'
 53.9 MiB /  53.9 MiB (100 %)  20.1 MiB/s in  2s ETA:  0s
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
  5.3 MiB /  17.9 MiB ( 29 %)   1.7 MiB/s in  6s ETA:  7s
...

Once Rust is installed, you can setup cargo so it always uses this proxy.

Example: Using pip using SOCKS

pip is used to install Python packages. Unfortunately pip does not support SOCKS by default. If you try to install yapf using the configuration above this happens:

user@no-internet$ pip install --proxy=socks5://localhost:9999 yapf
Collecting yapf
ERROR: Could not install packages due to an EnvironmentError: Missing dependencies for SOCKS support.

Based on this answer from Stack Overflow we need to first install pysocks. Now we have a chicken-and-egg situation that we need to solve: we cannot download pysocks on the no-internet machine! To solve it, download pysocks locally:

user@with-internet$ python3 -m pip download pysocks
Collecting pysocks
  Downloading PySocks-1.7.1-py3-none-any.whl (16 kB)
Saved ./PySocks-1.7.1-py3-none-any.whl
Successfully downloaded pysocks

Copy this python wheels file to no-internet, for instance using scp.

user@with-internet$ scp PySocks-1.7.1-py3-none-any.whl user@no-internet

And install it manually there. I’m installing it in the user environment (--user flag) because in this machine I don’t have enough permissions, but your mileage may vary here.

user@no-internet$ pip install --user PySocks-1.7.1-py3-none-any.whl 
Processing ./PySocks-1.7.1-py3-none-any.whl
Installing collected packages: PySocks
Successfully installed PySocks-1.7.1

If we use pip and SOCKS, now we succeed.

user@no-internet$ pip install --user --proxy=socks5://localhost:9999 yapf
Collecting yapf
  Downloading https://files.pythonhosted.org/packages/47/88/843c2e68f18a5879b4fbf37cb99fbabe1ffc4343b2e63191c8462235c008/yapf-0.32.0-py2.py3-none-any.whl (190kB)
     |████████████████████████████████| 194kB 933kB/s 
Installing collected packages: yapf
Successfully installed yapf-0.32.0

Yay!

Cleanup

Recall that we have two connections opened: one is the SOCKS proxy (-D) and the other the reverse tunnel (-R). Just end them both with Ctrl-C and you are done. I’m sure this can be scripted somehow but given that the ssh commands may require password input, this is not a trivial thing to do.