This can somewhat be achieved using a separate user or group, iptables (to mark packets sent by that user or group) and a separate routing table (that picks up marked packets via ip rule). The problem that might or might not exist (I'm don't really remember anymore) is that packets might just not get marked (think ICMP) or might be routed through the normal network in case the VPN interface disappears.
The better alternative are Linux Network Namespaces. They completely isolate the network (including abstract unix sockets) and are usually connected again to the outside using a pair of virtual network devices (veth). The linked article describes how they are used in conjunction with network namespaces. I wanted to avoid using a veth pair, so I put the VPN's network device directly into namespace, which works almost well. For some reason it is not possible to add any (working) routes when using a tun device for the VPN. You can add them, but pinging anything other than the VPN peer address returns a "network unreachable" response from the kernel. If you know anything about that, let me know. The workaround around that was to simply use a tap device (OpenVPN: "dev tap", tinc: "Mode = switch" & "Interface = tap0" or removing the Interface line).
My tinc-up script then looks as follows:
ip netns add vpn ip link set dev $INTERFACE netns vpn netns="ip netns exec vpn" $netns ip link set dev lo up $netns ip link set dev $INTERFACE up $netns ip addr add 10.0.0.2/24 dev $INTERFACE $netns ip route add default dev $INTERFACE via 10.0.0.1
That's it! The device is added to the namespace, then the usual setup on it is done inside the namespace.
Running programs inside the network namespace is slightly more tricky: You can run them easily as root, using iproute2:
sudo -E ip netns exec vpn sudo -E -u minus curl ip.mnus.de
Though if you want to run a process as user then you'll need to run it with sudo (and have one or two sudo process linger around). To make launching programs inside the namespace more practical I wrote a little tool: netns-switch. It runs a program inside a network namespace, as fixed user/group (defined at compile time). With this tool running curl in a namespace now looks like this:
netns-switch curl ip.mnus.de
Now there's no sudo involved at all! netns-switch automatically starts as root (due to the setuid bit) and then drops those permissions again before executing the program.