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 pname [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->$pname)] = "bool" and ($wgpeers->$pname) = true) do={
      :log info "WireGuard peer $pname disconnected"
    }
    :set ($wgpeers->$pname) false
  } else {
# The peer is connected (true). If changed from disconnected:
    :local connmsg "WireGuard peer $pname connected from IP $ip"
    :if ([:typeof ($wgpeers->$pname)] = "bool" and ($wgpeers->$pname) = false) do={
      :log info $connmsg
    }
    :if ([:typeof ($wgpeers->$pname)] != "bool") do={
      :log info ($connmsg . " (previous state was not stored)")
    }
    :set ($wgpeers->$pname) true
  }
}

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.

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: :

All peer names should be escaped: instead of

there must be

$wgpeers->"$pname"

as peer names can include variable name invalid characters and[:typeof ($wgpeers->$pname)] results in nothing.

@Venenarius Stop writing bllsht.

{
    :local wgpeers [:toarray ""]
    :local pname " % && 1010 -^z"
    :set ($wgpeers->$pname) "myvalue"
    :put ($wgpeers->$pname)
}
[test@test] > {
{...     :local wgpeers [:toarray ""]
{...     :local pname " % && 1010 -^z"         
{...     :set ($wgpeers->$pname) "myvalue"
{...     :put ($wgpeers->$pname)          
{... }                          
myvalue
[test@test] > 

If you flagged the post, it will disappear soon, and your reply will later appear out-of-place :stuck_out_tongue:

(You probably should have quoted the wrong part of that post)

I add @Venenarius for be clear,

It said 1) that names must be escaped, 2) that the variable name must be enclosed in quotes (including the $),
otherwise it won't work.

Probably some shty bot...

Sorry, my bad. Getting this branch of script running in case of peer name contains dash, ends with digit, and if the script is running on schedule. Every scheduled run get this message despite peer state (for both connected and disconnected).

    :if ([:typeof ($wgpeers->$pname)] != "bool") do={
      :log info ($connmsg . " (previous state was not stored)")
    }

Don't you understand the concept of stop writing bllsht?

{
    :local wgpeers [:toarray ""]
    :local pname " _ - _ - 66 6"
    :set ($wgpeers->$pname) "myvalue"
    :put ($wgpeers->$pname)
}
[test@test] > {
{...     :local wgpeers [:toarray ""]
{...     :local pname " _ - _ - 66 6"         
{...     :set ($wgpeers->$pname) "myvalue"
{...     :put ($wgpeers->$pname)          
{... }                          
myvalue
[test@test] > 

Now what do you add to the list, not added on first post, that there must have just been a solar eclipse?

The reason is logical, it is clearly visible in the script, it is a logical error, and it has nothing to do with anything else.

...
    :if ([:typeof ($wgpeers->$pname)] != "bool") do={
      :log info ($connmsg . " (previous state was not stored)")
    }
    :log info ("Peer $pname type " . ([:typeof ($wgpeers->$pname)]) . " and value " . (:put ($wgpeers->$pname)))
    :set ($wgpeers->$pname) true // <== 1
}

My bad, seems to be some kind of limitation for scheduled run. Last set command (marked <== 1) seems to be not modifying globals on scheduled run with read and write only policy set (same as script itself). Below are logs for 2 manual script runs (first), scheduled run (second), and system summary.

Manual run log
2026-08-21 17:07:54 script,info WireGuard peer wg-sf connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
2026-08-21 17:07:54 script,info Peer wg-sf type nothing and value
2026-08-21 17:07:54 script,info WireGuard peer wg-off-2 connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
2026-08-21 17:07:54 script,info Peer wg-off-2 type nothing and value
2026-08-21 17:07:54 script,info WireGuard peer wg-monitor-node connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
2026-08-21 17:08:10 script,info Peer wg-sf type bool and value true
2026-08-21 17:08:10 script,info Peer wg-off-2 type bool and value true
2026-08-21 17:08:10 script,info Peer wg-monitor-node type bool and value true
Scheduled run
 2026-08-21 17:19:50 script,info WireGuard peer wg-sf connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:50 script,info Peer wg-sf type nothing and value 
 2026-08-21 17:19:50 script,info WireGuard peer wg-off-4 connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:50 script,info Peer wg-off-4 type nothing and value 
 2026-08-21 17:19:50 script,info WireGuard peer wg-off-2 connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:50 script,info Peer wg-off-2 type nothing and value 
 2026-08-21 17:19:50 script,info WireGuard peer wg-monitor-node connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:50 script,info Peer wg-monitor-node type nothing and value 
 2026-08-21 17:19:55 script,info WireGuard peer wg-sf connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:55 script,info Peer wg-sf type nothing and value 
 2026-08-21 17:19:55 script,info WireGuard peer wg-off-4 connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:55 script,info Peer wg-off-4 type nothing and value 
 2026-08-21 17:19:55 script,info WireGuard peer wg-off-2 connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:55 script,info Peer wg-off-2 type nothing and value 
 2026-08-21 17:19:55 script,info WireGuard peer wg-monitor-node connected from IP xxx.xxx.xxx.xxx (previous state was not stored)
 2026-08-21 17:19:55 script,info Peer wg-monitor-node type nothing and value 
System/Settings summary
/system/routerboard/print 
       routerboard: yes        
             model: RB5009UG+S+
     serial-number: HH40ACZ2KWG
     firmware-type: 70x0       
  minimum-firmware: 7.15.2     
  current-firmware: 7.24       
  upgrade-firmware: 7.24
/system/script/print proplist=name,policy,run-count,last-started 
1  name="wg-log" policy=read,write run-count=1737 last-started=2026-08-21 17:19:55
/system/scheduler/print proplist=name,interval,policy,on-event,run-count 
Flags: X - DISABLED
Columns: NAME, INTERVAL, POLICY, ON-EVENT, RUN-COUNT
#   NAME             INTERVAL  POLICY  ON-EVENT                                                  RUN-COUNT
...
6   wg-log-sched     5s        read    /system/script/run wg-log                                      1698
                               write                                                                      

Problem is not with script itself. Case closed.

See Scheduler does not persist global variables (environment) between runs

So all these "must-haves" had nothing to do with it? :rofl:

Instead of wasting time writing bllsht, you should have read the documentation,
which clearly states that to use global variables, the user running the script must have policy and test permissions.

Then the logic errors in the script are another thing.