diff --git a/Dockerfile b/Dockerfile index 24e59702..82e91542 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN git clone https://github.com/balena-io-modules/connect-disconnect-script-ope FROM plugin-builder as learn-address-plugin -ENV LEARN_ADDRESS_PLUGIN_COMMIT=9ee777205f54de62413fe5d0267eac426f6aae06 +ENV LEARN_ADDRESS_PLUGIN_COMMIT=32c796f930a592a37f3d047dfdc3caffbde61ccd RUN git clone https://github.com/balena-io-modules/learn-address-script-openvpn.git \ && cd learn-address-script-openvpn \ && git checkout ${LEARN_ADDRESS_PLUGIN_COMMIT} \ diff --git a/README.md b/README.md index f240fc1f..958b3e6e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Note that the dhcp pool size will also dictate the max clients per process, with the max clients per server being `max_clients_per_instance * VPN_INSTANCE_COUNT` and not the size of the base subnet. A VLSM of `20` will allow for 4,094 clients per instance, and a -base subnet of size `/10` will allow for a total of a total of 4,194,302 clients. +base subnet of size `/10` will allow for a total of 4,194,302 clients. Base ports are increments by the process instance ID (1-indexed) to calculate the port for that instance. diff --git a/automation/test.sh b/automation/test.sh index 8c7c3de7..e72e7350 100755 --- a/automation/test.sh +++ b/automation/test.sh @@ -29,6 +29,7 @@ cd "${WORKDIR}" test_id=$(docker run --privileged -d \ --tmpfs /run \ --tmpfs /sys/fs/cgroup \ + --cap-add=NET_ADMIN \ -e PRODUCTION_MODE=false \ -e API_HOST=api.balena.test \ -e VPN_PORT=443 \ @@ -37,6 +38,8 @@ test_id=$(docker run --privileged -d \ -e VPN_BASE_PORT=10000 \ -e VPN_BASE_MANAGEMENT_PORT=20000 \ -e VPN_API_PORT=30000 \ + -e VPN_DOWNRATE=5mbit \ + -e VPN_UPRATE=5mbit \ -e VPN_HOST=127.0.0.1 \ -e VPN_CONNECT_INSTANCE_COUNT=1 \ -e VPN_CONNECT_PROXY_PORT=3128 \ @@ -63,4 +66,5 @@ docker exec "${test_id}" /bin/sh -ec ' echo "127.0.0.1 deadbeef.vpn" >> /etc/hosts npm install npm run test-unit - npx mocha test/app.ts' + npx mocha test/app.ts + cat /tmp/learn-address/status.log' diff --git a/config/confd/templates/env.tmpl b/config/confd/templates/env.tmpl index 9d8927e2..61a98d43 100644 --- a/config/confd/templates/env.tmpl +++ b/config/confd/templates/env.tmpl @@ -47,3 +47,6 @@ VPN_HAPROXY_SOCKET={{getenv "VPN_HAPROXY_SOCKET" "/run/haproxy.sock"}} {{if getenv "TRUST_PROXY"}}TRUST_PROXY={{getenv "TRUST_PROXY"}}{{end}} {{if getenv "VPN_AUTH_CACHE_TIMEOUT"}}VPN_AUTH_CACHE_TIMEOUT={{getenv "VPN_AUTH_CACHE_TIMEOUT"}}{{end}} + +VPN_DOWNRATE=5mbit +VPN_UPRATE=5mbit \ No newline at end of file diff --git a/openvpn/scripts/learn-address.sh b/openvpn/scripts/learn-address.sh old mode 100644 new mode 100755 index 6eb6aeab..3ed53e68 --- a/openvpn/scripts/learn-address.sh +++ b/openvpn/scripts/learn-address.sh @@ -2,15 +2,62 @@ #shamelessly stolen from https://serverfault.com/questions/701194/limit-throttle-per-user-openvpn-bandwidth-using-tc -statedir=/tmp/ +#$1 = downrate # from VPN server to the client, e.g. 5mbit +#$2 = uprate # from client to the VPN server, e.g. 5mbit +#$3 = action (add, update, delete) +#$4 = IP or MAC +#$5 = client_common name #Not used for rate limiting + +#set -eu + +DEBUG=0 +statedir=/tmp/learn-address/ +mkdir -p $statedir + +if [[ $DEBUG -eq 1 ]]; then + log=$statedir/status.log + touch $log + echo "****************" &>> $log + echo "Starting $0: $# [$@]" &>> $log +fi + # downrate: from VPN server to the client +downrate=$1 # uprate: from client to the VPN server -downrate=5mbit -uprate=5mbit +uprate=$2 + +function trace() { + if [[ $DEBUG -eq 1 ]]; then + place=$1 + dev=$2 + echo "*** $place" &>> $log + if [[ $dev ]]; then + echo "tc -s qdisc show dev $dev" &>> $log + tc -s qdisc show dev $dev &>> $log + echo "tc -s class show dev $dev" &>> $log + c -s class show dev $detv &>> $log + echo "tc -s filter show dev $dev" &>> $log + tc -s class show dev $dev &>> $log + else + echo "No dev!" &>> $log + fi + echo "***" &>> $log + fi +} + +function pre() { + trace "PRE" $1 +} + +function post() { + trace "POST" $1 +} function bwlimit-enable() { ip=$1 + pre $dev + # Disable if already enabled. bwlimit-disable $ip @@ -29,15 +76,20 @@ function bwlimit-enable() { fi # Limit traffic from VPN server to client - tc class add dev $dev parent 1: classid 1:$classid htb rate $downrate - tc filter add dev $dev protocol all parent 1:0 prio 1 u32 match ip dst $ip/32 flowid 1:$classid + echo "tc class add dev $dev parent 1: classid 1:$classid htb rate $downrate" &>> $log + tc class add dev $dev parent 1: classid 1:$classid htb rate $downrate &>> $log + echo "tc filter add dev $dev protocol all parent 1:0 prio 1 u32 match ip dst $ip/32 flowid 1:$classid" &>> $log + tc filter add dev $dev protocol all parent 1:0 prio 1 u32 match ip dst $ip/32 flowid 1:$classid &>> $log # Limit traffic from client to VPN server - tc filter add dev $dev parent ffff: protocol all prio 1 u32 match ip src $ip/32 police rate $uprate burst 80k drop flowid :$classid - + tc filter add dev $dev parent ffff: protocol all prio 1 u32 match ip src $ip/32 police rate $uprate burst 80k drop flowid :$classid &>> $log + echo "tc filter add dev $dev parent ffff: protocol all prio 1 u32 match ip src $ip/32 police rate $uprate burst 80k drop flowid :$classid" &>> $log + # Store classid and dev for further use. echo $classid > $statedir/$ip.classid echo $dev > $statedir/$ip.dev + + post $dev } function bwlimit-disable() { @@ -53,6 +105,8 @@ function bwlimit-disable() { classid=`cat $statedir/$ip.classid` dev=`cat $statedir/$ip.dev` + pre $dev + tc filter del dev $dev protocol all parent 1:0 prio 1 u32 match ip dst $ip/32 tc class del dev $dev classid 1:$classid @@ -60,23 +114,26 @@ function bwlimit-disable() { # Remove .dev but keep .classid so it can be reused. rm $statedir/$ip.dev + + post $dev } -# Make sure queueing discipline is enabled. -tc qdisc add dev $dev root handle 1: htb 2>/dev/null || /bin/true -tc qdisc add dev $dev handle ffff: ingress 2>/dev/null || /bin/true +if [[ $dev ]]; then + # Make sure queueing discipline is enabled. + tc qdisc add dev $dev root handle 1: htb 2>/dev/null || /bin/true + tc qdisc add dev $dev handle ffff: ingress 2>/dev/null || /bin/true +fi -case "$1" in +case "$3" in add|update) - bwlimit-enable $2 + bwlimit-enable $4 ;; delete) - bwlimit-disable $2 + bwlimit-disable $4 ;; *) - echo "$0: unknown operation [$1]" >&2 + echo "$0: unknown operation [$3]" >&2 exit 1 ;; esac - exit 0 \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index 89a05412..3bdea23b 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -61,6 +61,8 @@ const getIPv4InterfaceInfo = (iface?: string): os.NetworkInterfaceInfo[] => { export const TRUST_PROXY = trustProxyVar('TRUST_PROXY', false); export const VPN_API_PORT = intVar('VPN_API_PORT'); +export const VPN_DOWNRATE = optionalVar('VPN_DOWNRATE'); +export const VPN_UPRATE = optionalVar('VPN_UPRATE'); // milliseconds export const DEFAULT_SIGTERM_TIMEOUT = diff --git a/src/utils/openvpn.ts b/src/utils/openvpn.ts index fcb7a316..5498b529 100644 --- a/src/utils/openvpn.ts +++ b/src/utils/openvpn.ts @@ -26,7 +26,7 @@ const Telnet = // tslint:disable-next-line:no-var-requires require('telnet-client') as typeof import('telnet-client').default; -import { VPN_API_PORT } from './config'; +import { VPN_API_PORT, VPN_DOWNRATE, VPN_UPRATE } from './config'; import { Netmask } from './netmask'; export interface VpnClientUntrustedData { @@ -194,6 +194,8 @@ export class VpnManager extends EventEmitter { '--plugin', '/etc/openvpn/plugins/openvpn-plugin-learn-address-script.so', '/etc/openvpn/scripts/learn-address.sh', + `${VPN_DOWNRATE}`, + `${VPN_UPRATE}`, '--plugin', '/etc/openvpn/plugins/openvpn-plugin-auth-script.so', '/etc/openvpn/scripts/auth', diff --git a/test/app.ts b/test/app.ts index ec1a4089..69521217 100644 --- a/test/app.ts +++ b/test/app.ts @@ -115,28 +115,46 @@ describe('VPN Events', function () { .reply(200, 'OK'); }); - it('should send a client-connect event', function () { - const connectEvent = getEvent('connect').then((body) => { - expect(body).to.have.property('serviceId').that.equals(instance.getId()); - expect(body).to.have.property('uuids').that.deep.equals(['user2']); - expect(body).to.not.have.property('real_address'); - expect(body).to.not.have.property('virtual_address'); - }); + async function verifyEvent(event: string) { + const body = await getEvent(event); + expect(body).to.have.property('serviceId').that.equals(instance.getId()); + expect(body).to.have.property('uuids').that.deep.equals(['user2']); + expect(body).to.not.have.property('real_address'); + expect(body).to.not.have.property('virtual_address'); + } + it('should send a client-connect event', function () { this.client = vpnClient.create(vpnDefaultOpts); this.client.authenticate('user2', 'pass'); - return this.client.connect().return(connectEvent); + return this.client.connect().return(verifyEvent('connect')); }); it('should send a client-disconnect event', function () { - const disconnectEvent = getEvent('disconnect').then((body) => { - expect(body).to.have.property('serviceId').that.equals(instance.getId()); - expect(body).to.have.property('uuids').that.deep.equals(['user2']); - expect(body).to.not.have.property('real_address'); - expect(body).to.not.have.property('virtual_address'); - }); + return this.client.disconnect().return(verifyEvent('disconnect')); + }); +}); + +describe('More than one client', function () { + this.timeout(30 * 1000); - return this.client.disconnect().return(disconnectEvent); + before(() => { + nock(BALENA_API_INTERNAL_HOST) + .get(/\/services\/vpn\/auth\/user[23]/) + .twice() + .reply(200, 'OK'); + }); + it('should connect two clients', async function () { + this.client = vpnClient.create(vpnDefaultOpts); + this.client.authenticate('user2', 'pass'); + + this.anotherClient = vpnClient.create(vpnDefaultOpts); + this.anotherClient.authenticate('user3', 'pass'); + await this.client.connect(); + return this.anotherClient.connect(); + }); + it('should disconnect two clients', async function () { + await this.client.disconnect(); + return this.anotherClient.disconnect(); }); }); @@ -377,4 +395,4 @@ describe('VPN proxy', function () { }); }); }); -}); + });