Here I describe one of those amazing things you can do with SSH. Until I discovered this I was always doing port forwarding to communicate over SSH. This requires you to setup many ports if you need to forward something like NFS, for instance. With a tunnel you get a virtual ethernet device, with it's own IP address on each end. Then communication can happen on this device as if the two machines where physically connected. It's a VPN, using nothing more than an SSH connection.
So in this description I have a server and a client. The server is a CentOS 5.4 machine, and the client an Ubuntu 10.04 machine.
Client Setup
Client probably has the more complex setup of the two. This is mainly because of the script that allows you to have more than one endpoint configured, and then select which at the time of bringing up the tunnel.
If you have more than one endpoint in the script, this script only works when manually bringing up the tunnel from a terminal, since it will prompt you to which one to connect. If you wish to bring up the tunnel at system boot time this won't work, so you will need to copy the script for each tunnel you wish to bring up, and then list only one tunnel in each script, referencing the scripts in the correct interface configurations as needed.
First things first. Generate the key pair. Navigate to /etc/ssh and run the following, entering tunnel_rsa for the key name:
ssh-keygen -b 2048
Then create the file /etc/ssh/tunnel-connect.sh, giving it the following contents:
#!/bin/bash
TARGET_LIST=(
tunnel-host1.domain.com
tunnel-host2.domain.com
)
function connect()
{
local count=${#TARGET_LIST[@]}
local nr desc target
if [ $count -lt 1 ]
then
echo "No tunnel endpoints configured!" >&2
exit 1
elif [ $count -gt 1 ]
then
while true
do
echo "Tunnel Endpoints:" >&2
for ((i=0; i<$count; i++)) {
nr=$((i+1))
desc=${TARGET_LIST[$i]}
echo " $nr. $desc" >&2
}
echo >&2
read -p "Please enter number of endpoint to connect to (-1 to quit): " choice >&2
# no choice
if [ -z "$choice" ]
then
echo -e "\033[31mYou have to select a choice, or enter -1 to quit.\033[0m" >&2
echo >&2
continue
fi
# quit
if [ "$choice" = "-1" ]
then
echo -e "\033[31mQuitting...\033[0m" >&2
echo >&2
exit 1
fi
# validate choice
if [[ $choice = *[^0-9]* ]] || [ $choice -lt 1 -o $choice -gt $count ]
then
echo -e "\033[31mNot a valid choice '$choice'.\033[0m" >&2
echo >&2
continue
fi
target=${TARGET_LIST[$((choice-1))]}
break
done
else
target=${TARGET_LIST[0]}
fi
mkdir -p /var/run/sshd/tunnel-control
ssh -M -f -S /var/run/sshd/tunnel-control/tun0 -i /etc/ssh/tunnel_rsa -w 0:0 root@$target true
sleep 5
}
function disconnect()
{
ssh -S /var/run/sshd/tunnel-control/tun0 -O exit true
}
case "$1" in
"--connect")
connect
;;
"--disconnect")
disconnect
;;
*)
echo "Invalid option: $1" >&2
echo "Valid options are --connect or --disconnect." >&2
exit 1
esacAt the top of the script you can see a list of hosts in the TARGET_LIST variable. Remove the contents of the example, and enter the hostnames/IPs of the server host(s) you will be connecting to. If you have more than one host, the script will print a list of them and prompt which one to connect to. If you have only one host then it will automatically use that host.
Give the script execute permissions:
chmod a+x /etc/ssh/tunnel-connect.sh
Finally, edit /etc/network/interfaces and add the following:
iface tun0 inet static address 10.18.0.225 pointopoint 10.18.0.1 netmask 255.255.255.0 pre-up /etc/ssh/tunnel-connect.sh --connect post-down /etc/ssh/tunnel-connect.sh --disconnect
This will configure the tunnel to take on the IP address 10.18.0.225 for the client and 10.18.0.1 for the tunnel endpoint.
Server Setup
First we have to configure the tunnel device by creating a file /etc/sysconfig/network-scripts/ifcfg-tun0, assuming you have no previously configured tunnels. If you do, adapt the device names appropriately. The contents of the file is as follows:
NAME="SSH Tunnel Device" DEVICE=tun0 IPADDR=10.18.0.1 NETMASK=255.255.255.0 ONBOOT=no BOOTPROTO=none PEERDNS=no
Here we configure a device named tun0 with IP address 10.18.0.1. It's also instructed to not load at boot time, and given a friendly name.
Then, edit /etc/ssh/sshd_config and enable root login with the forced-commands-only option. You have to enable root login, since it's needed to bring up the tunnel interface. On top of this the PermitTunnel option must be set to yes. If you're using the AllowUsers option, remember to ensure the root username is listed in it as well. Your sshd configuration should have these 2 options afterwards:
PermitRootLogin forced-commands-only PermitTunnel yes
Finally, put the contents of the public key generated on the client (/etc/ssh/tunnel_rsa.pub) into /root/.ssh/authorized_keys, with the forced-commands options prepended to it, so it looks something like this
command="/sbin/ifdown tun0; /sbin/ifup tun0",no-port-forwarding,no-pty,no-X11-forwarding,no-agent-forwarding ssh-rsa AAAAB3...mNc= user@client-hostname
The part from ssh-rsa onwards is the public key you generated.
If you created the authorized_keys file, ensure the ownership and permissions of the /root/.ssh directory and it's contents are correct. Safe bet is to just run:
chown -R root:root /root/.ssh chmod -R og-rwx /root/.ssh
Setup the Firewall
Update your firewall on both the server and client. Something like the following will allow all traffic to/from the tun0 interface. It's recommended to use this only for testing. Replace it with stricter rules once everything is working.
iptables -I INPUT -i tun0 -j ACCEPT iptables -I OUTPUT -o tun0 -j ACCEPT
Testing the Connection
Back on the client, to connect the tunnel you can just run:
ifup tun0
If you have more than one destination server configured, you should be prompted to which server to connect.
After the connection is established, you can test it by running this on the client:
ping 10.18.0.1
Now all communication that would normally have to go to the server, would just need to go to the 10.18.0.1 IP address. You can make this transparent by adding some clever NAT entries with iptables.
To disconnect, run:
ifdown tun0
Example Usage of the Tunnel
Say you're server is db-server with IP address 10.10.10.200, and it is running MySQL. If the MySQL port 3306 is closed on it's firewall, you can create a tunnel to the server, and then just connect to 10.18.0.1 for access to MySQL. Now you're past the firewall.
But if you have software which is already configured, to avoid reconfiguring it everytime you try and test it, you have 2 options.
If the software uses the hostname db-server instead of the IP itself, you can add an entry to /etc/hosts which maps the db-server hostname to the 10.18.0.1 IP address.
If this isn't an option, you can also make the following DNAT entry:
iptables -A OUTPUT -d 10.10.10.200 -p tcp -m tcp --dport 3306 -j DNAT --to-destination 10.18.0.1 iptables -A POSTROUTING -o tun0 -j MASQUERADE
This will now cause any traffic to 10.10.10.200 for port 3306/TCP to instead be instructed to goto 10.18.0.1. So the traffic is transparently rerouted via the tunnel.
These rules will only work for traffic originating from the same machine as the one with the tunnel. If the machine with the tunnel is a gateway for other machines, then you can add the same rule above for the OUTPUT chain, to the PREROUTING chain as well, for example:
iptables -A PREROUTING -d 10.10.10.200 -p tcp -m tcp --dport 3306 -j DNAT --to-destination 10.18.0.1
There is so much you can do with this. For example if the tunnel endpoint does IPv4 packet forwarding, then you can setup a static route instead of a DNAT, and so forth. The power of this cannot be described in one sentence, let alone a thousand. Enjoy.
And remember, with great power comes great hacks!

0 comments:
Post a Comment