Automatically Call a Script when a New User Connects and Bandwidth Shape the Connection
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
Related videos on Youtube
Server Programmer
Updated on September 18, 2022Comments
-
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 eitherI 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 almost 8 yearsPlease also add the server log (
/var/log/openvpn.log
).
-
-
Server Programmer almost 8 yearsthanks @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 almost 8 yearsMay 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 almost 8 yearsYou're welcome @ServerProgrammer, I guess it is a permission problem, are you running OpenVPN with
user nobody
? -
Server Programmer almost 8 yearsyes, when I tested for whoami the script returned 'nobody'
-
Server Programmer almost 8 yearsAlso, in light of the error message above, would I add the 'duplicate-cn' to the vi /etc/openvpn/server.conf file?
-
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 almost 8 yearsThanks for the detailed answer, I will begin working on this and circle back
-
Server Programmer almost 8 yearsTo 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 almost 8 yearsYes 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 almost 8 yearsI 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 almost 8 yearsIf I don't add the (2) directives I can connect a client without using authentication
-
Server Programmer almost 8 yearsYes 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 almost 8 years
-
rda almost 8 yearsI updated my answer with a simple working solution using
up
,down
,client-connect
andclient-disconnect
directives. Once this works you could adapt it to the script from your last comment. -
Server Programmer almost 8 yearsThanks for the detailed answer - I will work on this through the weekend
-
Server Programmer almost 8 yearsIf 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 almost 8 yearsHere is the learn-address script: serverfault.com/questions/701194/…
-
Server Programmer almost 8 yearsOne 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 almost 8 yearsTo 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 almost 8 yearsIf 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
andclient-disconnect
) are happpening. Theprintenv
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 byclient-connect
andclient-disconnect
. -
Server Programmer almost 8 yearsLet us continue this discussion in chat.
-
Server Programmer almost 8 yearsHi rda, I just updated the chat discussion
-
Server Programmer almost 8 yearsJust updated private chat
-
Server Programmer over 7 yearsHi, I sent an email message to you a few days ago - did this reach you? Many thanks