October 2008

Tunneling afp over ssh

(Also check out my "tunnelopen" script!)

You're at home, and you want to mount a disk from a Mac at work onto your Mac at home, but work has a firewall. Your attempts to use the afp file serving protocol are thwarted because the afp port (548) is blocked.

What can you do? Notice that in the above diagram, the firewall does not block ssh connections. If this is true in your case, the easy answer is that you should be able to make a tunnel with ssh to forward your connection. You start an ssh to the remote host, and tell ssh to forward some random local port (for example, 15548) to the remote host's 548 port. Then instead of trying to open afp to the remote host, you open afp to the localhost, but specify the unuual port. Ssh takes care of the rest.

Under Tiger, this worked just fine. But if the remote host is running Leopard, there seems to be a problem. Leopard apparently won't accept connections to the afp server from itself (when you use ssh tunneling as in the above example, ssh on the remote host end effectively starts a connection from the remotehost back to the remotehost to complete the tunnel).

If your local system is also Leopard, you get a message with "error -36" in it. If your local system is Tiger, then the message will be a complaint about looking up "localhost:15548". But in both cases, the problem is at the remote host end. There's an easy solution, but there has to be more than one computer at the remote host end, which we'll call the third host (and it doesn't have to be running afp, and doesn't even need to be a mac).

Look carefully at the ssh command in the above figure. We are still setting up the tunnel (the -L option) to remotehost. But we are not logging in to remotehost, we are logging into thirdhost. The forwarding still works - ssh on thirdhost knows to forward incoming tunnels from 15548 to remotehost port 548. This makes Leopard happy.

There is a simple two-host solution for afp tunneling to Leopard, described in the next section.

hostname versus localhost

On both ends of the connection (for two-party tunneling), you choose when you set up the tunnel whether each end talks to the real hostname and IP address of the system, or "localhost", which is always a name for the local system, and (almost) always is tied to the special address On most operating systems, the "localhost" address is special, as it is handled completely internally on the machine, and skips going through additional networking steps.

In the second diagram above, my tunnel sets the remote end to point to "remotehost", i.e. the actual hostname and IP address of the remote host. When ssh forwards a connection, it remote ssh end start up a connection from remotehost, back to remotehost. This is what upsets Leopard. It's happy to receive a local mount request going through the "localhost" name and address ( But it gets upset if a local connection goes through "remotehost". So the solution is:

That looks odd because there's two different localhosts in there. It's important to realize that they refer to two different machines. The localhost in the ssh command is actually "localhost" on the remote host. ssh sets up the tunnel to remotehost, and on the remote end, forwards things to whatever there is called "localhost". Meanwhile, when we open the afp connection to localhost, it's localhost on "myhost", in other words it really is the local host, to the starting end of the ssh tunnel.

This is actually the simplest way to set up most tunnels, and probably should be the default procedure. But you never know when some operating system for whatever reason is going to work one way, and not the other, so it's worth trying both.

Just to make things more complicated, the local end of the ssh connection can be bound to either the localhost address (the default method), or to the hostname of the local system. Binding to the hostname address is necessary for shared tunneling, described below.

For three-party tunneling, the remote end has to use a hostname/IP address, because the tunnel does not go to the localhost system at the remote end of the ssh.

Multiple connections

You might need to connect to more than one firewalled afp server at a time. When doing this, you'll need to use a different local port for each different server you want to tunnel a connection to. In other words, you can't just blindly always use 15548. If you do this, you'll get a "bind: address already in use" error. And, if you're using the ssh options I'm using (more on this in a minute), you'll also leave an extra useless ssh process lying around:

myhost$ ssh -L 15548:remotehost:548 -f -N user@thirdhost
myhost$ ssh -L 15548:remotehost2:548 -f -N user@thirdhost
bind: Address already in use
channel_setup_fwd_listener: cannot listen to port: 15548
Could not request local forwarding.
myhost$ ps -ax | grep ssh
13599 ??         0:00.02 /usr/bin/ssh-agent -l
43210 ??         0:00.00 ssh -L 15548:remotehost:548 -f -N user@thirdhost
43212 ??         0:00.00 ssh -L 15548:remotehost2:548 -f -N user@thirdhost
61158 ??         0:00.06 /usr/sbin/sshd -i
61184 ??         0:00.43 /usr/sbin/sshd -i
43226 ttys000    0:00.00 grep ssh
myhost$ kill 43212

Terminating the ssh

Another thing to keep in mind is that if you use the options as shown, the ssh tunnel runs as long as both systems and the network in between stay up. You can reconnect to the afp server as many times as you like, and the tunnel will be there. But if you forget it's running and you try to run it again, you get that bind error shown above, and you end up with an extra process.

You may not like these backgrounded forever running ssh processes, because it's easy to forget they're there, and it's a nuisance to have to kill them by hand. The simplest alternative is to take out the -f (background) and -N (no command) options. You'll get a regular ssh shell login. When you're done, just exit, and the tunnel goes away too (eventually - the ssh actually keeps running until there are no active tunnels).

But that may not be so great either. You could easily use the ssh login, and forget that it has a tunnel attached (there are ssh escape sequences to find such things, but why bother?). Maybe you think of a tunnel as a one-shot deal, you want to use the tunnel once, and have the tunnel go away. If this is your desire, you can create ssh connections that are good for one connection or one minute (whichever is longer) by doing this:

myhost$ ssh -L 15548:remotehost:548 -f user@thirdhost sleep 60
The remote command sleeps for 60 seconds, and then exits. But it only exits if there is no active tunnel. So that gives you one minute to connect, and then when you're done using the connection, it disappears automatically.

Sharing your tunnel

This can be VERY dangerous. If you have multiple machines at home or wherever your local end of the network is, then an option can be set that lets all your local machines use the same tunnel. If you don't have a good firewall in place, then you will be sharing your tunnel with the entire internet, effectively defeating the firewall at the remote end. SO DON'T DO THAT. Only use this method if you are sure that your local network is secure.

So here goes. Let's suppose you have a tunnel set up on myhost as shown above. If you add "-o 'GatewayPorts yes'" to the command, then other hosts can share this tunnel:

myfirsthost$ ssh -o 'GatewayPorts yes' -L 15548:remotehost:548 -f -N user@thirdhost
myhost$ open afp://localhost:15548/
myhost$ ssh myotherhost
myotherhost$ open afp://myhost:15548/
And away we go. Now both myhost and myotherhost are mounting a filesystem from remotehost, and they're using the same tunnel to do it.

Just for completeness, the GatewayPorts option works by listing for connections to the tunnel on the "myhost" hostname and address, in addition to "localhost". Normally, with it only listing on "localhost", there's no way for other machines on the network to connect to that address (if they tried, they'd get their own localhost).

Simple Tunnel Setup

Check out my script, tunnelopen. It takes care of all this port stuff and ssh options so you don't have to think about them. Usually, you just run "tunnelopen afp://remotehost/" and it does the rest. Or if you're dealing with the Leopard problem you can use "tunnelopen -l user@thirdhost afp://remotehost".

Reader Comments (Experimental. Moderated, expect delays. Posts may be edited or ignored. I reserve the right to remove any or all comments, at any time.)


At 2012/10/04 16:35
Brian wrote:

Brilliant, thank you for documenting this. I forgot that OS X would simply allow the AFP client to map to another port just by putting it into the URL. It was a lot cleaner than mapping the remote server to the local port 548 with sudo.

End Comments

Add a comment

More Mac OS X Stuff

Tom Fine's Home Send Me Email