lockdown and the di-semi-default route

i’ve been working on a vpn client lately. i’ve invented (i think) two pretty simple tricks that are worth sharing.

lockdown

the first thing is a method for locking down a process to not have any filesystem access. the idea is a pretty simple twist on chroot() to an empty directory.

  • chdir() to /tmp
  • make a directory in /tmp with mkdtemp()
  • chroot() to this new directory
  • rmdir() the temporary directory (the current directory, unaffected by the chroot, is is still /tmp)
  • chdir(“/”) to drop any reference to the old filesystem
  • setuid() to a non-privileged account

effectively you now have your process’s root directory as a non-existent directory.

this seems pretty secure. even access(“/”) fails. it also has the added advantage of not requiring a static empty chroot directory (ala /var/run/sshd).

di-semi-default route

one problem faced by vpn clients that want to set the default route is how to manage to ensure packets still get delivered over the normal network to the vpn server (ie: no infinite loop). another problem is how to restore the normal default route when the vpn client exits (or crashes).

the first problem is usually solved by adding an explicit route to the vpn server using the default gateway. for example, if the default gateway on the network was 192.168.0.1 and the vpn server had an address of 209.132.176.176 then one would add a route for 209.132.176.176 gateway 192.168.0.1. no changes here.

the normal method of setting the default route is to delete the current default route (perhaps remembering what it was) and then setting a new route to the network interface created by the vpn program. when the program exits it may restore the old default route. if the program crashes or is kill()ed then you lose.

my approach is to setup something that i’m humourously calling the “di-semi-default route”. essentially, instead of deleting the old default route and replacing it, you add two new half-default routes. say the vpn interface is called vpn0:

  • route 0.0.0.0 netmask 128.0.0.0 to vpn0
  • route 128.0.0.0 netmask 128.0.0.0 to vpn0

these routes do not conflict with the default route and because the kernel matches routes with tighter netmasks first, they will get matched before the default route. together, they cover the entire ip address space (the first covers all addresses starting with 0-127 and the second covers all address from 128-255). the really nice thing is that when the ‘vpn0’ interface disappears then so do the routes, re-exposing the normal default route.

update: an attentive commenter, craig box, noted that the “usual” method that i use (and is used by software that he packages for ubuntu) is flawed. it fails to take into account the case where the vpn server is on the same local network as the laptop. in this case, it is an error to send the packets to the default gateway.

the method i now use to deal with this is open /proc/net/route and walk through it until i hit a match for the ip of the vpn server (it is sorted by netmask). once i hit a match i only add the new route if the line i hit was the default route.

thanks, craig :)

12 thoughts on “lockdown and the di-semi-default route”

  1. I maintain nm-pptp packages for Ubuntu, and people have bought up the case of “your VPN server is on your LAN, not behind the gateway”. Setting a route to the VPN server over the default GW is incorrect in this part.

    I’m not sure if there’s a better way than “check to see if it’s local, and if so, don’t create that route”; if there is, you might be able to think of it now you’ve heard of the problem. Otherwise, you might also need to consider checking for this case – the PPTP plugin for NM doesn’t, at this point.

    (Now, if only the upstream author wasn’t AFK…)

  2. craig: i didn’t consider this problem.

    i think the only reasonable thing to do here is to query the route table to see what route will match the ip of the vpn server. if it is the default route, then add another rule — if not, then do nothing.

    thanks for the note. i was so excited about the other part that i didn’t give this part due consideration. i now have something to think about :)

  3. You might also want to set setrlimit(RLIMIT_FSIZE) to 0. That way, a process can no longer create any files on disk anymore.

    Also, consider setting RLIMIT_NOFILE to your highest fd+1. That way, the process may not create any further file descriptors.

    Just paranoia, though.

  4. Dude, that routing stuff you are suggesting is pure crack!

    Use route metrics for this! That way you may have multiple default routes, and can specify which one is the higher priority one. It’s easy-to-use and actually invinted for cases like this one.

  5. blah: the problem with using route metrics is that in almost every case ever the default route already has metric 0 :(

  6. di-semi-default route I never thought of that. Learned something handy today.

  7. desrt: Also, on systems like Debian you can set the metric for each interface explicitly by using the “metric” stanza in /etc/network/interfaces.

    And there is this nifty tool called “ifmetric” which modifies all current routes on a specific interface to a certain value.

  8. If you want to isolate a process, you also need to make
    sure it cannot attach as a debugger to any process
    outside the jail.

  9. Regarding finding the route through which the VPN is reached, you should probably check what “ip route get 1.2.3.4” does and do the same. It might work just as you do, by manually evaluating the routing rules in userspace, but perhaps it does something smarter, taking the more-complex routing options into account.

Comments are closed.