Isolating App Traffic in a Network Namespace
Overview
This post takes Yunmai VPN as an example to demonstrate how to set up a Linux network namespace and run Yunmai as well as other processes inside the namespace, so that all network traffic of these processes will go through the VPN, while other processes outside the namespace will not be affected.
Prerequisites
Yunmai installed on the Linux system, and is properly configured to function as a VPN client (can successfully connect to the VPN server and access intranet resources).
Create A Network Namespace Setup Service
Create a script /usr/local/bin/yunmai-ns-setup.sh with the following content to set up the network namespace and routing rules:
#!/bin/bash
# 0. Clean up existing configuration
ip netns del yunmai_ns 2>/dev/null
ip link del veth_yunmai0 2>/dev/null
# 1. Create a network namespace
ip netns add yunmai_ns
# 2. Create a veth device pair
ip link add veth_yunmai0 type veth peer name veth_yunmai1
# 3. Move veth_yunmai1 into the namespace
ip link set veth_yunmai1 netns yunmai_ns
# 4. Configure IP addresses
ip addr add 10.200.1.1/24 dev veth_yunmai0
ip netns exec yunmai_ns ip addr add 10.200.1.2/24 dev veth_yunmai1
# 5. Bring the interfaces up
ip link set veth_yunmai0 up
ip netns exec yunmai_ns ip link set veth_yunmai1 up
ip netns exec yunmai_ns ip link set lo up
# 6. Configure the default route inside the namespace
ip netns exec yunmai_ns ip route add default via 10.200.1.1
# 7. Get the default network interface
IFACE=$(ip route get 8.8.8.8 | awk '{print $5; exit}')
# 8. Configure NAT and forwarding rules
iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -o "$IFACE" -j MASQUERADE
iptables -A FORWARD -i veth_yunmai0 -o "$IFACE" -j ACCEPT
iptables -A FORWARD -i "$IFACE" -o veth_yunmai0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# 9. Enable IP forwarding
sysctl -w net.ipv4.ip_forward=1Make the script executable:
sudo chmod +x /usr/local/bin/yunmai-ns-setup.shCreate a systemd service file /etc/systemd/system/yunmai-ns.service to run the above script at startup:
[Unit]
Description=Setup yunmai network namespace
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/yunmai-ns-setup.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable yunmai-ns.service
sudo systemctl start yunmai-ns.serviceRun VPN Services in the Namespace
There are two Yunmai services that need to run inside the yunmai_ns namespace: yunmai-daemon.service and yunmai-updater.service 1. We can modify their systemd service files to add the namespace configuration, so that they will automatically run inside the namespace when started.
1 There are two other Yunmai services: yunmai-screenshot.service, yunmai-cross.service; but they do not affect VPN connection and are not required to run in the namespace.
Use sudo systemctl edit <service-name> to create an override file for yunmai-daemon.service and yunmai-updater.service separately, and add the following content to the override files:
[Unit]
After=yunmai-ns.service
Requires=yunmai-ns.service
[Service]
NetworkNamespacePath=/run/netns/yunmai_ns
ReadOnlyPaths=/etc/resolv.conf
Here are some explanations on the use of ReadOnlyPaths under the [Service] section. VPN clients may require a proprietary DNS server to access network. In the case of Yunmai, its service puts the proprietary DNS server on the first line of /etc/resolv.conf, and keeps it this way by restoring the file whenever it detects modification. However, this may cause various DNS-related connection issues for the default network namespace. Therefore, it is necessary to keep the default setting of /etc/resolv.conf and make it readonly to Yunmai’s service.
On the other hand, to make the proprietary DNS server available to Yunmai in its own network namespace, create the namespace’s own resolv.conf file, and put the proprietary DNS server in it (or just copy the setting from /etc/resolv.conf when Yunmai is running normally in the default network namespace):
sudo mkdir -p /etc/netns/yunmai_ns/
sudo cp /etc/resolv.conf /etc/netns/yunmai_ns/resolv.conf # Make sure Yunmai is running normally in the default network namespace before executing this command.After the setup, reload systemd and restart the services:
sudo systemctl daemon-reload
sudo systemctl restart yunmai-daemon.service
sudo systemctl restart yunmai-updater.serviceSince Yunmai’s DNS server may still exist in /etc/resolv.conf, it’s advised to remove the DNS entry manually or reboot the operating system (some service like systemd-resolved.service should restore it after reboot).
Run Other Applications in the Namespace
Take google-chrome as an example, we can create a .desktop file (e.g., ~/.local/share/applications/yunmai-chrome.desktop) to run it in the namespace.
First, use mkdir -p ~/.config/google-chrome-yunmai to create a separate user data directory for this instance of chrome, to avoid conflicts with the normal chrome instance.
Then create the .desktop file with the following content:
[Desktop Entry]
Version=1.0
Name=Chrome-yunmai
GenericName=Web Browser in yunmai namespace
Comment=Access the Internet in yunmai network namespace
Exec=/bin/bash -c 'sudo /usr/sbin/ip netns exec yunmai_ns runuser -u "$USER" -- env DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" /usr/bin/google-chrome --class=Chrome-yunmai --user-data-dir="$HOME/.config/google-chrome-yunmai" %U'
StartupNotify=true
Terminal=false
Icon=/usr/share/icons/breeze/categories/32/applications-internet.svg
StartupWMClass=Chrome-yunmai
Type=Application
Categories=Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;
Note that /usr/sbin/ip netns exec requires root privilege, so we need to configure sudoers to allow the user to run this command without password. Add the following line to the sudoers file (using sudo visudo) (this line should be placed at the end of the file to avoid being overridden by pre-existing entries):
<user-name> ALL=(ALL) NOPASSWD: /usr/sbin/ip netns exec *
After the setup, when you launch the “Chrome-yunmai” application, it will run inside the same yunmai_ns namespace as the Yunmai VPN services, and all its network traffic will go through the Yunmai VPN.
If you want to launch an application in the namespace from the command line, a more convenient way would be to create a script (e.g., ~/.local/bin/ym):
#!/bin/bash
# Launch an application inside the yunmai_ns namespace
# Usage: ym <command> [args...]
if [ $# -eq 0 ]; then
echo "Usage: ym <command> [args...]" >&2
exit 1
fi
exec sudo /usr/sbin/ip netns exec yunmai_ns runuser -u "$USER" -- \
env DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" "$@"Then make it executable:
chmod u+x ~/.local/bin/ymNow you can launch the application with ym as the prefix.
Verify the Setup
You can use the following command to check whether the processes are running in the correct namespace:
for pid in $(pgrep -f 'yunmai-daemon|yunmai-updater|Chrome-yunmai'); do
ns_name=$(sudo ip netns identify $pid 2>/dev/null)
cmd=$(ps -p $pid -o cmd= 2>/dev/null | cut -c1-60)
echo "PID $pid [$ns_name]: $cmd"
doneIf the setup is correct, you should see that yunmai-daemon, yunmai-updater, and the “Chrome-yunmai” process are all running in the yunmai_ns namespace, and you can use the “Chrome-yunmai” application to access intranet resources through the Yunmai VPN.
Additional Notes
Here are some common issues and solutions related to network namespace:
- When an application (e.g. web browser) launches from a non-default namespace, the input method in the default namespace would not work, because
sudoresets the environment, preserving only variables on a built-in allow list. On Debian/Ubuntu,sudo’s PAM stack re-reads/etc/environment, so listing the input method variables (GTK_IM_MODULE,QT_IM_MODULEandXMODIFIERS, see this post) there also lets them survive. SinceDBUS_SESSION_BUS_ADDRESSandXDG_RUNTIME_DIRare not on the list but are also required to reach fcitx5 and are uid-specific, they are best forwarded from the session rather than written into/etc/environment.