Automatically Call a Script when a New User Connects and Bandwidth Shape the Connection

11,301

OpenVPN per client traffic control

To have a simple solution for traffic control on a per client basis, you could do something like the following. This solution only works for a /24 VPN subnet. Tested on Ubuntu 14.04.

OpenVPN server example configuration:

port 1194
proto udp
dev tun
topology subnet
server 10.8.0.0 255.255.255.0
keepalive 10 60
comp-lzo
persist-key
persist-tun
log /var/log/openvpn.log
verb 3
#user openvpn
#group nogroup
script-security 2
down-pre
up /etc/openvpn/tc.sh
down /etc/openvpn/tc.sh
client-connect /etc/openvpn/tc.sh
client-disconnect /etc/openvpn/tc.sh

Traffic control script /etc/openvpn/tc.sh:

#!/bin/bash
TC=$(which tc)

interface="$dev"
interface_speed="100mbit"
client_ip="$trusted_ip"
client_ip_vpn="$ifconfig_pool_remote_ip"
download_limit="512kbit"
upload_limit="10mbit"
handle=`echo "$client_ip_vpn" | cut -d. -f4`

function start_tc {
  tc qdisc show dev $interface | grep -q "qdisc pfifo_fast 0"
  [ "$?" -gt "0" ] && tc qdisc del dev $interface root; sleep 1

  $TC qdisc add dev $interface root handle 1: htb default 30
  $TC class add dev $interface parent 1: classid 1:1 htb rate $interface_speed burst 15k
  $TC class add dev $interface parent 1:1 classid 1:10 htb rate $download_limit burst 15k
  $TC class add dev $interface parent 1:1 classid 1:20 htb rate $upload_limit burst 15k
  $TC qdisc add dev $interface parent 1:10 handle 10: sfq perturb 10
  $TC qdisc add dev $interface parent 1:20 handle 20: sfq perturb 10
}

function stop_tc {
  tc qdisc show dev $interface | grep -q "qdisc pfifo_fast 0"
  [ "$?" -gt "0" ] && tc qdisc del dev $interface root
}

function filter_add {
  $TC filter add dev $interface protocol ip handle ::${handle} parent 1: prio 1 u32 match ip ${1} ${2}/32 flowid 1:${3}
}

function filter_del {
  $TC filter del dev $interface protocol ip handle 800::${handle} parent 1: prio 1 u32
}

function ip_add {
  filter_add "dst" $client_ip_vpn "10"
  filter_add "src" $client_ip_vpn "20"
}

function ip_del {
  filter_del
  filter_del
}

if [ "$script_type" == "up" ]; then
        start_tc
elif [ "$script_type" == "down" ]; then
        stop_tc
elif [ "$script_type" == "client-connect" ]; then
        ip_add
elif [ "$script_type" == "client-disconnect" ]; then
        ip_del
fi

Note, this a very simple script for tc testing purposes, a more sophisticated approach for OpenVPN traffic control can be found in this answer.

Make the script executable:

chmod +x /etc/openvpn/tc.sh

Running script as root in unprivileged mode

If you run OpenVPN in unprivileged mode and the script needs to be run as root, modify the following directives in the server configuration:

user openvpn
group nogroup
up "/usr/bin/sudo /etc/openvpn/tc.sh"
down "/usr/bin/sudo /etc/openvpn/tc.sh"
client-connect "/usr/bin/sudo /etc/openvpn/tc.sh"
client-disconnect "/usr/bin/sudo /etc/openvpn/tc.sh"

Add an unprivileged user named openvpn:

useradd -s /usr/sbin/nologin -r -M -d /dev/null openvpn

Edit /etc/sudoers with command visudo, add the following line:

# User privilege specification
openvpn ALL=NOPASSWD: /etc/openvpn/tc.sh

Save and quit with Ctrl+x, y

Make the script only writable by root:

chown root:root /etc/openvpn/tc.sh
chmod 700 /etc/openvpn/tc.sh

Please note that this might open a security hole and could possibly be comparable to running OpenVPN as root. Although it looks quite safe to me, but there are always people with better eyes :)

Troubleshooting

The script should be run as root now, you can troubleshoot it by adding the following lines to the beginning of your tc.sh script:

#!/bin/bash
exec >>/tmp/ov.log 2>&1
chmod 666 /tmp/ov.log 2>/dev/null
echo
date
id
echo "PATH=$PATH"
printenv

As soon as the server is started for the first time, you can tail the logs:

tail -f /var/log/openvpn.log /tmp/ov.log
Share:
11,301

Related videos on Youtube

Server Programmer
Author by

Server Programmer

Updated on September 18, 2022

Comments

  • Server Programmer
    Server Programmer over 1 year

    I hope this is easy

    The following script called up.sh works perfect when I run it from the command line as root.

    However, instead of manually calling this script each time a new user connects to OpenVPN to individually limit the bandwidth, delay, etc for each new user (User1, User2, User3 to infinity) via tc (qdisc), I would like the script to be called each time a new user connects to OpenVPN and when the new user connects have the ability to individually shape the bandwidth, delay, etc of the new user without affecting the bandwidth, delay, etc of the current users (which could be 100's or 1000's)

    I tried moving the script to the following folder /etc/network/if-up.d to have it executed when a new user connects to OpenVPN, however for some reason the script does not get called (it makes no changes to qdisc), yet it is exactly the same script and works perfect when I execute it from the command line.

    I also tried renaming the script to learn-address.sh and placed it in the following folder /etc/openvpn/netem/learn-address.sh to automatically get called when OpenVPN learns a new address but this doesn't work either

    I have also updated the server.conf file to read as follows

    script-security 3

    learn-address /etc/openvpn/netem/learn-address.sh

    And

    script-security 3

    up /etc/network/if-up.d/up.sh

    But it didn't work either

    Lastly, I have also tried updating the /etc/sudoers.tmp file to give permissions to the scripts and this does not seem to help either (see at the end of the post)

    I am running Ubuntu 14.04

    Many thanks for your help

    Here is the script called up.sh that works when I call it from the command line:

    #!/bin/bash  
    # Full path to tc binary 
    
    TC=$(which tc)
    
    #
    # NETWORK CONFIGURATION
    # interface - name of your interface device
    # interface_speed - speed in mbit of your $interface
    # ip - IP address of your server, change this if you don't want to use
    #      the default catch all filters.
    #
    interface=eth0
    interface_speed=100mbit
    ip=4.1.2.3 # The IP address bound to the interface
    
    # Define the upload and download speed limit, follow units can be 
    # passed as a parameter:
    # kbps: Kilobytes per second
    # mbps: Megabytes per second
    # kbit: kilobits per second
    # mbit: megabits per second
    # bps: Bytes per second
    download_limit=512kbit
    upload_limit=10mbit    
    
    
    # Filter options for limiting the intended interface.
    FILTER="$TC filter add dev $interface protocol ip parent 1: prio 1 u32"
    
    #
    # This function starts the TC rules and limits the upload and download speed
    # per already configured earlier.
    # 
    
    function start_tc { 
        tc qdisc show dev $interface | grep -q "qdisc pfifo_fast 0"  
        [ "$?" -gt "0" ] && tc qdisc del dev $interface root; sleep 1  
    
        # start the tc configuration
        $TC qdisc add dev $interface root handle 1: htb default 30
        $TC class add dev $interface parent 1: classid 1:1 htb rate $interface_speed burst 15k
    
        $TC class add dev $interface parent 1:1 classid 1:10 htb rate $download_limit burst 15k
        $TC class add dev $interface parent 1:1 classid 1:20 htb rate $upload_limit burst 15k
    
        $TC qdisc add dev $interface parent 1:10 handle 10: sfq perturb 10
        $TC qdisc add dev $interface parent 1:20 handle 20: sfq perturb 10
    
        # Apply the filter rules
        
        # Catch-all IP rules, which will set global limit on the server
        # for all IP addresses on the server. 
        $FILTER match ip dst 0.0.0.0/0 flowid 1:10
        $FILTER match ip src 0.0.0.0/0 flowid 1:20
    
        # If you want to limit the upload/download limit based on specific IP address
        # you can comment the above catch-all filter and uncomment these:
        #
        # $FILTER match ip dst $ip/32 flowid 1:10
        # $FILTER match ip src $ip/32 flowid 1:20
    }
    
    #
    # Removes the network speed limiting and restores the default TC configuration
    #
    function stop_tc {
        tc qdisc show dev $interface | grep -q "qdisc pfifo_fast 0"
        [ "$?" -gt "0" ] && tc qdisc del dev $interface root
    }
    
    function show_status {
            $TC -s qdisc ls dev $interface
    }
    #
    # Display help 
    #
    function display_help {
            echo "Usage: tc [OPTION]"
            echo -e "\tstart - Apply the tc limit"
            echo -e "\tstop - Remove the tc limit"
            echo -e "\tstatus - Show status"
    }
    
    # Start
    if [ -z "$1" ]; then
            display_help
    elif [ "$1" == "start" ]; then
            start_tc
    elif [ "$1" == "stop" ]; then
            stop_tc
    elif [ "$1" == "status" ]; then
            show_status
    fi
    

    Here is the following file I also updated:

    /etc/sudoers.tmp

    #
    # This file MUST be edited with the 'visudo' command as root.
    #
    # Please consider adding local content in /etc/sudoers.d/ instead of
    # directly modifying this file.
    #
    # See the man page for details on how to write a sudoers file.
    #
    Defaults        env_reset
    Defaults        mail_badpass
    Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    
    # Host alias specification
    
    # User alias specification
    
    # Cmnd alias specification    
    
    # User privilege specification
    root    ALL=(ALL:ALL) ALL
    #nobody ALL=(ALL) NOPASSWD: /usr/lib/tc
    nobody ALL=(ALL) NOPASSWD: /usr/lib/tc
    www-data ALL=NOPASSWD: /user/lib/tc
    root ALL=NOPASSWD: /user/lib/tc
    root    ALL=(ALL:ALL) ALL
    nobody  ALL=(ALL) NOPASSWD
    nobody  ALL=(ALL) NOPASSWD: /etc/openvpn/netem/learn-address.sh
    root  ALL=(ALL) NOPASSWD: /etc/openvpn/netem/learn-address.sh
    www-data  ALL=(ALL) NOPASSWD: /etc/openvpn/netem/learn-address.sh
    nobody  ALL=(ALL) NOPASSWD: /etc/openvpn/netem/up.sh
    www-data  ALL=(ALL) NOPASSWD: /etc/openvpn/netem/up.sh
    root  ALL=(ALL) NOPASSWD: /etc/openvpn/netem/up.sh
    nobody  ALL=(ALL) NOPASSWD: /etc/network/if-up.d/up.sh
    www-data  ALL=(ALL) NOPASSWD: /etc/network/if-up.d/up.sh
    root  ALL=(ALL) NOPASSWD: /etc/network/if-up.d/up.sh  
        
    # Members of the admin group may gain root privileges
    %admin ALL=(ALL) ALL
    
    # Allow members of group sudo to execute any command
    %sudo   ALL=(ALL:ALL) ALL
    
    # See sudoers(5) for more information on "#include" directives:
    
    #includedir /etc/sudoers.d
    

    Here is the server.conf

    port 1194
    proto udp
    dev tun
    sndbuf 0
    rcvbuf 0
    ca ca.crt
    cert server.crt
    key server.key
    dh dh.pem
    tls-auth ta.key 0
    topology subnet
    server 10.8.0.0 255.255.255.0
    ifconfig-pool-persist ipp.txt
    push "redirect-gateway def1 bypass-dhcp"
    push "dhcp-option DNS 8.8.8.8"
    push "dhcp-option DNS 8.8.4.4"
    keepalive 10 120
    cipher AES-128-CBC
    comp-lzo
    #user nobody
    #user openvpn
    #group nogroup
    persist-key
    persist-tun
    status openvpn-status.log
    verb 3
    crl-verify crl.pem
    script-security 2
    down-pre
    up /etc/openvpn/tc.sh
    down /etc/openvpn/tc.sh
    client-connect /etc/openvpn/tc.sh
    client-disconnect /etc/openvpn/tc.sh
    log /var/log/openvpn.log
    
    • rda
      rda almost 8 years
      Please also add the server log (/var/log/openvpn.log).
  • Server Programmer
    Server Programmer almost 8 years
    thanks @rda, I added the above directives to the following file <vi /etc/openvpn/server.conf> and when I run the following <grep ovpn /var/log/syslog> I receive the following error:
  • Server Programmer
    Server Programmer almost 8 years
    May 26 17:10:28 OpenVPN ovpn-server[18174]: MULTI: new connection by client 'JAN2' will cause previous active sessions by this client to be dropped. Remember to use the --duplicate-cn option if you want multiple clients using the same certificate or username to concurrently connect. May 26 17:10:28 OpenVPN ovpn-server[18174]: MULTI_sva: pool returned IPv4=10.8.0.3, IPv6=(Not enabled) May 26 17:10:28 OpenVPN ovpn-server[18174]: WARNING: Failed running command (--client-connect): external program exited with error status: 1
  • rda
    rda almost 8 years
    You're welcome @ServerProgrammer, I guess it is a permission problem, are you running OpenVPN with user nobody?
  • Server Programmer
    Server Programmer almost 8 years
    yes, when I tested for whoami the script returned 'nobody'
  • Server Programmer
    Server Programmer almost 8 years
    Also, in light of the error message above, would I add the 'duplicate-cn' to the vi /etc/openvpn/server.conf file?
  • rda
    rda almost 8 years
    @ServerProgrammer, No, do not add duplicate-cn unless you really want to allow clients to be connected simultaneously with the same CN-name. This is just a warning message and appears if two or more clients connect with the same CN-name or if a previous client connection by the same client is not yet timed out.
  • Server Programmer
    Server Programmer almost 8 years
    Thanks for the detailed answer, I will begin working on this and circle back
  • Server Programmer
    Server Programmer almost 8 years
    To clarify: by default OpenVPN is run as root user, should I change this to instead run as an unprivileged user "openvpn" instead? If we run OpenVPN as root user and not as an unprivileged user, how does your answer change?
  • rda
    rda almost 8 years
    Yes you are completely right. I suspected that the reason for your script to fail was a permission problem based on the assumption that you run OpenVPN in unprivileged mode. If you run it as root, the problem lies elsewhere and the first listing of the 2 directives should be enough. I will think about how to troubleshoot it further and come back to you. Running OpenVPN in unprivileged mode is safer, i.e. for the unlikely case that OpenVPN gets hacked, the attacker will not have root privileges.
  • Server Programmer
    Server Programmer almost 8 years
    I just setup a brand new install of OpenVPN on Digital Cloud (I assume OpenVPN is runnnig as root) and installed the script above using the (2) directives you outlined script-security 2 -- client-connect /etc/openvpn/client-connect.sh and when I connect a client to OpenVPN it now asks for a username and password
  • Server Programmer
    Server Programmer almost 8 years
    If I don't add the (2) directives I can connect a client without using authentication
  • Server Programmer
    Server Programmer almost 8 years
    Yes I agree RDA (I believe the problem rests in permissions) - the final goal was once I could successfully run the script above when a user connects I wanted to apply the permissions to have the following script also run when a client connects called learn-address (see answer half way down the page)
  • Server Programmer
    Server Programmer almost 8 years
  • rda
    rda almost 8 years
    I updated my answer with a simple working solution using up, down, client-connect and client-disconnect directives. Once this works you could adapt it to the script from your last comment.
  • Server Programmer
    Server Programmer almost 8 years
    Thanks for the detailed answer - I will work on this through the weekend
  • Server Programmer
    Server Programmer almost 8 years
    If you want to look at a second question I have open that relates to the exact same problem to have a script called when a user connects (this time using a script called learn-address), we could answer (2) questions: serverfault.com/questions/777875/…
  • Server Programmer
    Server Programmer almost 8 years
    Here is the learn-address script: serverfault.com/questions/701194/…
  • Server Programmer
    Server Programmer almost 8 years
    One question, if we are going to have an unlimited number of users connecting via OpenVPN, how would we change the above script? If I understand this correct, a VPN-only subnet of size /24 (example CIDR: 10.0.1.0/24) only allows a Max of 256 private IP addresses, is this true?
  • Server Programmer
    Server Programmer almost 8 years
    To expand the number of users that can connect to OpenVPN with the above script, could we instead use the MAC address of the connecting device or optionally the name of the client certificate (for example User1, User2, User3, User4 to infinity)? How would we modify the above script? If I am following this correct, couldn't we simply amend the following: client_ip="$trusted_ip" And Change this to: client_MAC="$client_MAC"
  • rda
    rda almost 8 years
    If you use the OpenVPN config from my answer and create a script tc.sh with just the troubleshooting script part in the file and tail the log /tmp/ov.log, then you will see what environment variables are passed over to the script when each of the 4 calls (up, down, client-connect and client-disconnect) are happpening. The printenv line will print all the environment variables. One of these variables, namely $ifconfig_pool_remote_ip is set to the remote VPN endpoint's IP and passed to the scripts by client-connect and client-disconnect.
  • Server Programmer
    Server Programmer almost 8 years
  • Server Programmer
    Server Programmer almost 8 years
    Hi rda, I just updated the chat discussion
  • Server Programmer
    Server Programmer almost 8 years
    Just updated private chat
  • Server Programmer
    Server Programmer over 7 years
    Hi, I sent an email message to you a few days ago - did this reach you? Many thanks