Logging WireGuard peer connects and disconnects

I would like to share a script that I wrote for logging WireGuard peers connect/disconnect events. Due to the nature of WG protocol, there is no logon or logoff events as such. However, the peers constantly perform handshakes and keep track of the time since the last handshake took place.

This makes it possible to determine if a peer is in the “connected” state, meaning the handshake was done recently. The protocol defines 180 seconds as the maximum time before a peer is considered “disconnected”.

Comparing this current peer state information with the previous state from one or several seconds ago, it is possible to detect the state changes like connected=>disconnected or disconnected=>connected. The disconnect event is not extremely precise since 3 minutes must pass before a peer is declared disconnected. Still, it is precise enough for logging purposes.

My script accomplishes everything described above. The only requirement is that the WireGuard peer names are unique. You need to add this script to System - Scripts and then schedule it to run every X seconds in System - Scheduler. How often you want to run it depends on how precise you want the logging (mostly applies to the connect events precision). It’s not a heavy script and can be run every second if needed.

Example:
image

Tested on RouterOS 7.19.

# Peer names must be unique.
:global wgpeers
/interface/wireguard/peers
:foreach p in=[find] do={
  :local name [get $p name]
  :local ip [get $p current-endpoint-address]
  :local last [:tonsec [get $p last-handshake]]
# WG session is 180s max.
  :if ($last > 180000000000 or [:len $last] = 0) do={
# The peer is disconnected (false). If changed from connected:
    :if ([:typeof ($wgpeers->$name)] = "bool" and ($wgpeers->$name) = true) do={
      :log info "WireGuard peer $name disconnected"
    }
    :set ($wgpeers->$name) false
  } else {
# The peer is connected (true). If changed from disconnected:
    :local connmsg "WireGuard peer $name connected from IP $ip"
    :if ([:typeof ($wgpeers->$name)] = "bool" and ($wgpeers->$name) = false) do={
      :log info $connmsg
    }
    :if ([:typeof ($wgpeers->$name)] != "bool") do={
      :log info ($connmsg . " (previous state was not stored)")
    }
    :set ($wgpeers->$name) true
  }
}
1 Like

Excellent. Thank you!

Beautiful scripting!

Thank you.

I do not like this line.

Why is that? I’m open to suggestions.

I suspect you don’t like “name” being used in both places, but it’s not an issue, since one is a variable, another is a property. There is no conflict here. Quite the opposite, it makes total sense to match the variable with the property. I didn’t do it for the other properties because I don’t like unnecessary long variable names.

1 Like

I see you understand the reason: it’s always best to avoid using the same name and setting an example for others to do the same thing.
Of course, it depends on each case, but it’s definitely best avoided.

Nice. :smile:

Just in case, unrelated - but not much - anecdata :wink: :