Bash script noob - wireguard speedtest via curl

Hello,

i try to find the best wireguard/vpn server for certain endpoints at the moment, when im more familiar with this stuff i want this a bit more interactive and selective about the endpoints.
im reading the content of /etc/wireguard via ls into an array (ls for this should’nt be used, but works so far)
im looping through the config files and setup new vpn via wg-quick (only this supports Address= in config, but is limited to 16 bytes as config name, yey)
then i want to read the output of curl into a new array and want to output is after the loop again with the wireguard-config used. at the moment i echo the config name and curl speed, this worked. but somehow i cant get the 2nd loop to get working for doing the array of the speeds.
it seems that $avg_speed doenst count correctly when doing array output. my first time working with arrays and bash, had some pascal in school ages ago. made alot copy&paste for this. :sweat_smile:

Summary
#!/bin/bash
configs=( $(ls /etc/wireguard/ | sed 's/\.[^.]*$//') )
target="http://speedtest.fra1.de.leaseweb.net/10mb.bin"

for ((i=0; i<${#configs[@]};i++)); do
	wg-quick down "${configs[i]}" > /dev/null 2>&1
done
printf 'VPN-Count:%i\n' "${#configs[@]}"
for ((i=0; i<${#configs[@]};i++)); do
    #do something to each element of array
    wg-quick down "${configs[i]}" > /dev/null 2>&1
    echo "${configs[$i]}"
    wg-quick up "${configs[i]}" > /dev/null 2>&1
    sleep 2s
	for ((c=0; c<${#configs[@]}; seq (${#configs[@]})) do     
        avg_speed=( $(curl -Ssfq -w '%{speed_download}' --url $target --output "/dev/null"))
        echo "${avg_speed[$c]}"
   	echo "${#avg_speed[@]}"
        done 
    sleep 2s
    wg-quick down "${configs[i]}" > /dev/null 2>&1
    printf '%s,%f\n' "${configs[$i]}" "${avg_speed[$c]}"
done

dont mind all the echos there, trying to figure out when something happens.

i googled some more and found that i have to declare an array first and count the output of curl with +. used some other variation with.

#!/bin/bash
configs=( $(ls /etc/wireguard/ | sed 's/\.[^.]*$//'))
target="http://hel.icmp.hetzner.com/100MB.bin"
declare avg_speed=()

echo "removing all wireguardlinks..."
for ((i=0; i<${#configs[@]};i++)); do
        wg-quick down "${configs[i]}" > /dev/null 2>&1
done
echo "done."
sleep 1
for ((i=0; i<${#configs[@]};i++)); do
    #do something to each element of array
    wg-quick up "${configs[i]}" > /dev/null 2>&1
    sleep 5s
    avg_speed+=( $(curl -Ssfq -w '%{speed_download}' --url $target --output "/dev/null"))
#    printf '%i\n' "${avg_speed[$i]}"
    sleep 5s
    wg-quick down "${configs[i]}" > /dev/null 2>&1
    printf '%s-%.3f\n' "${configs[$i]}" "$(echo "${avg_speed[$i]}/1000000" | bc-l )"
    sleep 1s
done
echo "done"

trying to figure out how to modify the decimal radix in printf so i get mbits and not bytes.

edit//
it does what it should do, now ive to make it pretty.

*8 to get from Bytes to bits, and your average should be against the number of runs, not a million.

You appear to be pulling times per config based on the number of configs.
Consider setting the number of runs before your outer “i” loop.
runs_per_config=5;

Not sure about using seq in a loop constructor. Try:
for((c=0; c<${runs_per_config}; c++)); do

You don’t need an array to hold the individual times. Before the inner loop, set a var to zero then add to it:
ttl_speed=0;
(In fact, avg_speed+= can be problematic based on your version of bash. avg_speed[$i] may give better results.

Inside the inner loop:
cur_time=$(curl -Ssfq -w ‘%{speed_download}’ --url $target --output “/dev/null”);
ttl_time=$((ttl_speed + ((cur_speed * 8))));

The average gets set after the “c” loop completes:
average_time=$(echo -e “${ttl_time} / ${runs_per_config}” | bc -l);
print f’%s,%.3f\n’ “${configs[$i]}” “${average_time}”;

HTH

Why are you looping through configs twice? Also, this loop is a bit funky in general?

Typically you will do a C-style for-loop, or a foreach-loop, but here you appear to be conflating the two syntaxes.

Your bug is on this line. You’re declaring a new array with a single element each time through the loop. You want to append the array (or use the loop index) instead. Or you can declare an associative array.

I am avoiding work and need to brush up on my shell scripting, so I took a stab at writing this like I would for a client. I went a little wild with it but I was challenging myself and having fun. Take it or leave it. :slight_smile:

speedtests
#!/bin/bash
# N.B. https://gist.github.com/mohanpedala/1e2ff5661761d3abd0385e8223e16425
set -eEuo pipefail
IFS=$'\n\t'

readonly target="http://speedtest.fra1.de.leaseweb.net/10mb.bin"
readonly wireguard_dir="/etc/wireguard"

wg_quick() {
  # N.B. We want to capture the output of wg-quick and only show it if there's
  # an error.
  local tmpfile; tmpfile=$(mktemp)

  # N.B. This cleans up the temporary file whether the function returns zero
  # (success) or not.
  trap 'rm -f "$tmpfile"; trap - RETURN' RETURN

  local rc=0
  wg-quick "$2" "$1" >/dev/null 2>"$tmpfile" || rc=$?

  if (( rc != 0 )) ; then
    cat "$tmpfile" >&2
  fi

  return $rc
}

up() {
  wg_quick "$1" up
}

down() {
  wg_quick "$1" down
}

is_up() {
  local ifname="$1"

  local operstate_file="/sys/class/net/$ifname/operstate"
  if [[ ! -f "$operstate_file" || ! -r "$operstate_file" ]] ; then
    return 2
  fi

  local operstate; read -r operstate <"$operstate_file"
  if [[ "$operstate" == "down" ]] ; then
    return 1
  fi

  return 0
}

is_down() {
  ! is_up "$1"
}

down_if_up() {
  is_down "$1" || down "$1"
}

bytes_to_bps() {
  numfmt --to=iec --suffix=bps $(( $1 * 8 ))
}

curl_target() {
  curl -sSfq -w '%{speed_download}' --url "$target" --output /dev/null
}

speedtest() {
  local ifname=$1

  up "$ifname"
  results[$ifname]=$(bytes_to_bps $(curl_target))
  sleep 2
  down "$ifname"
}

log() {
  local level=${1^^}
  shift

  # N.B. Write to stderr so user can capture the results from stdout.
  echo "$level: $*" >&2
}

get_ifnames() {
  if [[ ! -d "$wireguard_dir" || ! -r "$wireguard_dir" ]] ; then
    log error "Unable to list configuration files in $wireguard_dir"

    return 2
  fi

  # N.B. This makes the wildcard work properly if there are no WireGuard
  # configuration files in the directory.
  shopt -s nullglob

  local config
  for config in "$wireguard_dir"/*.conf ; do
    basename "$config" .conf
  done

  shopt -u nullglob
}

loop() {
  # Run the given callback function for each loop interation. The default
  # callback function is the no-op bash function `:'.
  local __callback=${1:-:}

  if (( $# > 0 )) ; then
    # N.B. This lets us pass along any "extra" arguments to the callback
    # function.
    shift
  fi

  local ifname
  for ifname in "${ifnames[@]}" ; do
    log info "  * <$ifname>"
    "$__callback" "$ifname" "$@"
  done
}

main() {
  # We'll store speedtest results--in bytes/sec--in an associative array, indexed
  # by interface name.
  # TODO: Make these local variables and pass them by reference to `loop' and
  # `speedtest' or otherwise fix those methods to not directly reference them.
  declare -a ifnames=($(get_ifnames))
  declare -A results

  log info "Downing WireGuard interfaces..."
  # N.B. Escaping the callback function name here has the same effect as
  # quoting it, but I prefer this syntax to highlight the "oddity".
  loop \down_if_up
  log info "Done!"

  log info "Speed-testing WireGuard interfaces..."
  loop \speedtest
  log info "Done!"

  log info "Summarizing speedtest results..."
  # N.B. We could re-use `loop' with another callback function, or simply loop
  # through the associatve array as I've done here.
  for ifname in "${!results[@]}" ; do
    echo -e "$ifname\t${results[$ifname]}"
  done
  log info "Done!"
}

main

This text will be hidden

Here’s some example output:

$ ./speedtests >results.tsv
INFO: Downing WireGuard interfaces...
INFO:   * <bar>
INFO:   * <foo>
INFO: Done!
INFO: Speed-testing WireGuard interfaces...
INFO:   * <bar>
INFO:   * <foo>
INFO: Done!
INFO: Summarizing speedtest results...
INFO: Done!
$ cat results.tsv 
foo	58Mbps
bar	58Mbps
1 Like
curl -w '%{speed_download}'

gives output in bytes, according to guides/manpages, it should be correct.

i “benchmark all” servers of airvpn, they got a bunch. the first loop removing them is just for removing “dead” wireguard servers when i canceled the script.
the results in the array, yeah, writing down just a single var should be okay with printf >> results.log e.g. i wanted to sort them later on by speed with the corresponding server. avg_speed+= worked for me.

i wasnt to so far look more detailed why bash wants that many rounded braces. :joy:

yeah, it just gave the first result and every other result was empty/0. avg_speed+= worked. i copy&pasted alot of stuff from stack exchange.
i will read your bash script, it looks long but not that dense.
when i had pascal in school, wasnt that much, we just made basic stuff and now im eager to “solve” my problem. i think i can learn alot by your script, you doing cases/catches. and maybe i should stick to one language for a while, looking at different things. for e.g. looked at influxdb it messes up how to think about the problem.

regards.