Network namespaces and public IP

namespacenetworking

To start, I've read this great article on network namespaces so I know more or less what network namespaces do and how to configure them.

The actual problem I'm facing is this:

  • I want to run some Valve game servers (CS, CS:GO, TF2) on 1 physical machine in LAN mode. So I want every client in the LAN to list all servers in the LAN, because this is the best user experience opposed to manually connecting to an ip:port.
  • The client software looks for LAN servers by broadcasting to ports 27015 – 27020, so 6 ports in total are available to run the server on, otherwise the servers won't be listed in the LAN browser. But, I have more than 6 servers so I need to use more than 1 IP for the same phyical server. The actual plan is to have 1 IP per game.
  • I cannot, and I repeat, cannot tell the game servers to bind to a specific IP as this stops the servers from being listed in the LAN browser at the client sides, even if I tell it explicitly to act like a LAN server.

(People that have been trying to run a CS 1.6, CSGO or TF2 server will probably recognize the "+ip <ip address>" problem here)

By having multiple IP's I cannot solve the problem, because I cannot tell the game server to bind to a specific IP address – the software always takes the primary IP so this can't work. (There is a port clash, or the software takes a port 27020+ which makes the server invisible in the LAN browser)

I want to solve it by using network namespaces – 1 network namespace per game:

  • In the "csgo" network namespace I will run 5 instances of CSGO. (27015 – 27019)
  • In the "tf2" network namespace I will run 1 instance of TF2. (27015)
  • In the "cs16" network namespace I will run 2 instances of CS 1.6. (27015 – 27016)

Because I would be running the game software in a namespace, the software will only see 1 IP and will automatically take that one. (Well, that's what I'm thinking!).

So 4 network namespaces in total ("default", "csgo", "tf2" and "cs16"). The configuration would look like:

- eth0 / 192.168.0.160 ("default" ns, internet access)
- veth0:0 / 192.168.0.161 ("default" ns) <======> veth0:1 / 192.168.0.171 ("csgo" NS)
- veth1:0 / 192.168.0.162 ("default" ns) <======> veth1:1 / 192.168.0.172 ("tf2" NS)
- veth2:0 / 192.168.0.163 ("default" ns) <======> veth2:1 / 192.168.0.173 ("cs16" NS)

Now the question is, can and will this work? If I run the CSGO server software in namespace "csgo", will the public IP of the LAN server then be 192.168.0.171? Or will it be 192.168.0.160? Or maybe 192.168.0.161? As per above, I really need a separate IP address for each game, to make all 9 server appear in the LAN browser.

If not, can this problem actually be solved by using network namespaces?

Best Answer

Binding applications to a specific IP address is a notoriously difficult problem: not all applications are as kind as ssh which allows you to specify the IP address you want to bind to by means of the -b option. For instance, Firefox and Chrome are notoriously impervious to this.

Luckily, there is a solution: this guy has modified the bind.so system library to allow one to specify the binding address on the command line, as follows:

$ BIND_ADDR="192.0.2.100" LD_PRELOAD=/usr/lib/bind.so firefox

By preloading the bind shared object, you bypass the system version which chooses the interface to bind to differently.

This is a heck of a lot easier and lighter on system resources than running multiple network spaces simultaneously.

The Web page above gives both instructions on how to compile the module and this link to pre-compiled 32- and 64-bit versions.

(Just for reference: I know you are not interested, but the code can be easily modified to force binding to a specific port).

EDIT:

I completely forgot that games would most likely use UDP, while the trick above only works for TCP connections. I am leaving my answer in place, in the hope of helping someone with TCP problems of this sort, but as an answer to Timmos this is completely useless.

To make up for my mistake, I am passing you a (very simple!) script I wrote which sets up one of (possibly many) network namespaces.

#!/bin/bash
#
# This script will setup a network namespace with a macvlan
# which obtains its IP address via dhclient from the LAN on which the host is
# placed
#

set -x

# It will open an xterm window in the new network namespace; if anything
# else is required, change the statement below.

export XTERM1=xterm

# The script will temporarily activate ip forwarding for you. If you
# do not wish to retain this feature, you will have to issue, at the 
# end of this session, the command
# echo 0 > /proc/sys/net/ipv4/ip_forward 
# yourself. 

###############################################################################

export WHEREIS=/usr/bin/whereis

# First of all, check that the script is run by root:

[ "root" != "$USER" ] && exec sudo $0 "$@"

if [ $# != 2 ]; then 
    echo "Usage $0 name action"
    echo "where name is the network namespace name,"
    echo " and action is one of start| stop| reload."
    exit 1
fi

# Do we have all it takes?

IERROR1=0
IERROR2=0
IERROR3=0

export IP=$($WHEREIS -b ip | /usr/bin/awk '{print $2}')
export IPTABLES=$($WHEREIS -b iptables | /usr/bin/awk '{print $2}')
export XTERM=$($WHEREIS -b $XTERM1 | /usr/bin/awk '{print $2}')

if [ "x$IP" = "x" ] ; then
    echo "please install the iproute2 package"
    IERROR1=1
fi

if [ "x$IPTABLES" = "x" ] ; then
    echo "please install the iptables package"
    IERROR2=1
fi

if [ "x$XTERM" = "x" ] ; then
    echo "please install the xterm package"
    IERROR3=1
fi

if [[ $IERROR1 == 0 && $IERROR2 == 0 && $IERROR3 == 0 ]] 
then
    :   
else
    exit 1
fi


prelim() {

# Perform some preliminary setup. First, clear the proposed 
# namespace name of blank characters; then create a directory
# for logging info, and a pid file in it; lastly, enable IPv4 
# forwarding. 

    VAR=$1
    export NNSNAME=${VAR//[[:space:]]}

    export OUTDIR=/var/log/newns/$NNSNAME

    if [ ! -d $OUTDIR ]; then
        /bin/mkdir -p $OUTDIR
    fi
    export PID=$OUTDIR/pid$NNSNAME

    echo 1 > /proc/sys/net/ipv4/ip_forward

}

start_nns() {

# Check whether a namespace with the same name already exists. 

    $IP netns list | /bin/grep $1 2> /dev/null
    if [ $? == 0 ]; then 
        echo "Network namespace $1 already exists,"
        echo "please choose another name"
        exit 1
    fi

# Here we take care of DNS

    /bin/mkdir -p /etc/netns/$1
    echo "nameserver 8.8.8.8" > /etc/netns/$1/resolv.conf
    echo "nameserver 8.8.4.4" >> /etc/netns/$1/resolv.conf

# The following creates the new namespace, and the macvlan interface

    $IP netns add $1
    $IP link add link eth0 mac$1 type macvlan mode bridge

# This assigns the macvlan interface, mac$1, to the new 
# namespace, asks for an IP address via a call to dhclient,
# brings up this and the (essential) lo interface, 
# creates a new terminal in the new namespace and 
# stores its pid for the purpose of tearing it cleanly, later. 

    $IP link set mac$1 netns $1
    $IP netns exec $1 /sbin/dhclient -v mac$1 1> /dev/null 2>&1
    $IP netns exec $1 $IP link set dev lo up
    $IP netns exec $1 su -c $XTERM $SUDO_USER &
    $IP netns exec $1 echo "$!" > $PID


}

stop_nns() {

# Check that the namespace to be torn down really exists

    $IP netns list | /bin/grep $1 2>&1 1> /dev/null
    if [ ! $? == 0 ]; then 
        echo "Network namespace $1 does not exist,"
        echo "please choose another name"
        exit 1
    fi

# This kills the terminal in the separate namespace and
# removes the file and the directory where it is stored.

    /bin/kill -TERM $(cat $PID) 2> /dev/null 1> /dev/null
    /bin/rm $PID
    /bin/rmdir $OUTDIR  
    $IP netns del $1

# This deletes the file and direcotory connected with the DNSes. 

    /bin/rm /etc/netns/$1/resolv.conf
    /bin/rmdir /etc/netns/$1

}


case $2 in
    start)
        prelim "$1"
        start_nns $NNSNAME
        ;;
    stop)
        prelim "$1"
        stop_nns $NNSNAME
        ;;
    reload)
        prelim "$1"
        stop_nns $NNSNAME
        prelim "$1"
        start_nns $NNSNAME
        ;;
    *) 
# This removes the absolute path from the command name

        NAME1=$0
        NAMESHORT=${NAME1##*/}

        echo "Usage:" $NAMESHORT "name action,"
        echo "where name is the name of the network namespace,"
        echo "and action is one of start|stop|reload"
        ;;
esac

It assumes your main interface is called eth0 (if yours is called differently, change the single reference to it accordingly), and uses macvlan interfaces, which means you can use the script only with an ethernet connection. Also, it does not need to use bridges.

You start/stop a separate network namespace as follows (I call the script nns, but you can call it whatever you like):

nns network_namespace_1 start
nns network_namespace_2 stop

You can have as many different network namespaces as your local DHCP server allows, since each macvlan interface gets an IP address from your LAN DHCP server. If a network namespace with the same name already exists, you will have to pick a different name.

All network namespaces can talk to each other, courtesy of the mode bridge option in their creation command. The script opens an xterm terminal in the new network namespace (I like xterm, if you do not you can change that at the top of the script) so that, from within the xterm you can start your applications.

I left the debugging option, set -x, in the script, which may help you iron out some initial problem. When done, just remove that line.

Cheers.

Related Question