Page 1 of 1

My gratuitous arp script

Posted: Mon May 16, 2022 5:34 pm
by mfedotov
I wanted to share my gratuitous arp script. It is based on the packet format from https://wiki.mikrotik.com/wiki/Manual:I ... uitous_ARP

As a background, I wanted to use l3hw routing, and needed to be able to provide failover between my two routers. Unfortunately vrrp-based default gateway is not going to work with l3hw, so the regular IP addresses on VLAN interfaces had to be used. I established vrrp relation on a dedicated VLAN just to detect the events and used onmaster/onbackup scripts to create the gateway IP addresses on the master router and destroy them on the backup router. Then, whenever the gateway address is moved between the routers, sending a gratuitous arp is supposed to reduce the time for the network devices to start using the new gateway.

Here is an example of the onmaster script sending the garp after bringing up the gateways:
/ip/address/add address=10.255.202.1/24 interface=vlan1202
/ip/address/add address=192.168.222.1/24 interface=vlan222

:global sendgarp
:if ([:typeof $sendgarp]="nothing") do={ /system/script/run sendgarp };

$sendgarp "192.168.222.1/24";
$sendgarp "10.255.202.1/24";

This is the sendgarp script implementing this functionality

sendgarp
:global split;
:if ([:typeof $split]="nothing") do={ /system script run myfunctions };

:global sendgarp do={
  # Send garp out of interface with the IP in $1 (should be in "IP/MASK" format, so that find address finds it)

  :local routerip $1;
  :if ([:find $routerip "/"]) do={} else={
    :log error [:put "sendgarp: expecting parameter to be in  <ipaddress>/<mask> format, got: $routerip"];
    :return nil;
  }
  :if ([:len [/ip address find address=$routerip]] = 0) do={
    :log error [:put "sendgarp: Cannot find interface with address $routerip"]
    :return nil;
  };

  :local routeriface [/ip address get [find address=$routerip] interface];
#  :put "routeriface: $routeriface";
  :global lowercase
  :global join;
  :global split;
  :global map;
  :global byte2hex;
  :local routermac [$join "" [$split ":" [$lowercase [/interface get [find name=$routeriface] mac-address]]]];
#  :put "Router MAC: $routermac";

  :local destmac "ffffffffffff"
  :local ipheader "08060001080006040001"
  :local padding "000000000000000000000000000000000000"
  :local routeriphex [$join "" [$split "." [:pick $routerip 0 [:find $routerip "/"]] $byte2hex]]
#  :put "routeriphex: $routeriphex";
  :local content "$destmac$routermac$ipheader$routermac$routeriphex$destmac$routeriphex$padding"
  /tool traffic-generator inject interface=$routeriface data=$content
  :log info [:put "sendgarp: sent to $routerip on $routeriface: $content"] 
};

And those are the functions (including my versions of split, join and map operations) I created to help with the implementation and that I use with other scripts

myfunctions
:global split do={
 # Split string in $2 by delimiter $1. Return array (list of strings)
 # As optimization if $3 is provided it is used as a lambda to process each token before it's put into the array.
  :local str $2;
  :local out [:toarray ""];
  :local lambda $3;
  :if ([:typeof $lambda] = "nothing") do={
    :set lambda do={:return $1;}
  };

  :if ($1 = "") do={
    # Special case - empty delimiter - split by every character
    :for i from=0 to=([:len $str] - 1) do={
      :set ($out->[:len $out]) [$lambda [:pick $str $i]]
    };
    :return $out;
  };

 do {
   :local pos [:find $str $1];
   :if ($pos=[]) do {
     :set ($out->[:len $out]) [$lambda $str];
     :return $out;
    }
  
  :set ($out->[:len $out]) [$lambda [:pick $str 0 $pos]]
  :set str [:pick $str ($pos+1) [:len $str]];
  } while (true)

  :return $out;
}

:global join do={
  # Join array in $2 by delimiter in $1
  :local ret "";
  :local delim $1;
  :local arr $2;
  :foreach i in=$arr do={
    :if ($ret="") do={
      :set ret $i;
    } else={
      :set ret ($ret.$delim.$i);
    };
  };
  
  :return $ret;
}

:global map do={
 # Execute function $1 on each item in array $2. Return the result array
 # If $2 is a string, execute $1 on each character of the string, return the resulting string
  :local out [:toarray ""];
  :local action $1;
  :local in $2;
  :if ([:typeof $in]="str") do={
    :local result "";
    :for i from=0 to=([:len $in] - 1) do={
      :set result ($result . [$action [:pick $in $i]]);
    };
    :return $result;
  };

  :foreach i in=$arr do={
    :set ($out->[:len $out]) [$action $i]
  };
 
  :return $out;
}

:global "_lower" "0123456789abcdefghijklmnopqrstuvwxyz";
:global "_upper" "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

:global byte2hex do={ 
   :if ($1 > 255) do={:return "--"};
   :global "_lower";
   :return ( ([:pick $"_lower" ($1/16)]) . ([:pick $"_lower" ($1-($1/16)*16)]) );

}


:global upperchar do={
  :global "_lower";
  :global "_upper";
  :local pos [:find $"_lower" $1];
  :if ($pos > -1) do={:return [:pick $"_upper" $pos];};

  :return $1;
};

:global lowerchar do={
  :global "_lower"
  :global "_upper"
  :local pos [:find $"_upper" $1];
  :if ($pos > -1) do={:return [:pick $"_lower" $pos];};

  :return $1;
};

:global uppercase do={
  :global map
  :global upperchar
  :return [$map $upperchar $1]
};

:global lowercase do={
  :global map
  :global lowerchar
  :return [$map $lowerchar $1]
};

I developed it on RouterOS v7, but tried (and adjusted the syntax a bit) on v6.48 and it seems to work there.

Hope somebody may find it useful

Re: My gratuitous arp script

Posted: Mon May 16, 2022 5:56 pm
by rextended
If can help... a shorter and simpler way for some functions....
:global ip2hex do={
  :local number  [:tonum [:toip $1]]
  :local hexadec "0"
  :local remainder 0
  :local hexChars "0123456789abcdef"
  :if ($number > 0) do={:set hexadec ""}
  :while ( $number > 0 ) do={
        :set remainder ($number % 16)
        :set number (($number-$remainder) / 16)
        :set hexadec ([:pick $hexChars $remainder].$hexadec)
  } 
  :set hexadec "00000000$hexadec"
  :set hexadec [:pick $hexadec ([:len $hexadec] - 8) [:len $hexadec]]
  :return $hexadec
}


:global lmac do={
  :local string    [:tostr $1]
  :local result    ""
  :local fromChars "0123456789ABCDEFabcdef:"
  :local toChars   "0123456789abcdefabcdef"
  :local len       [:len $string]
  :local pos       0
  do {
      :local char [:pick $string $pos ($pos + 1)]
      :local char [:pick $toChars [:find $fromChars $char -1]]
      :if ([:typeof $char] != "nil") do={:set result "$result$char"}
      :set pos ($pos + 1)
  } while=($pos < 17)
  :return $result
}

:global sendgarp do={
    :global ip2hex
    :global lmac
    :local routerip    $1
    :local routeriphex [$ip2hex [:pick $routerip 0 [:find $routerip "/"]]]
    :local routeriface [/ip address get [find where address=$routerip] interface]
    :local routermac   [$lmac [/interface get [find where name=$routeriface] mac-address] ]
    :local destmac     "ffffffffffff"
    :local ipheader    "08060001080006040001"
    :local padding     "000000000000000000000000000000000000"

    :local content "$destmac$routermac$ipheader$routermac$routeriphex$destmac$routeriphex$padding"
    /tool traffic-generator inject interface=$routeriface data=$content
}

Re: My gratuitous arp script

Posted: Mon May 16, 2022 11:57 pm
by mfedotov
If can help... a shorter and simpler way for some functions....

Sure, don't want to say my implementation is the best. But I guess your implementation was theoretical, right?
I see that ip2hex does not provide the correct result. I had to put
[:tonum [:toip$1]]
to make it work (thank, btw, did not know about this trick).

And the sendgarp function needs
:global lmac;
before the invocation to make it work (I hit this issue myself during my development cycle)

Still split/join implementation makes it easier to understand (to its developer :D ). I wish Mikrotik had their own implementation built in the scripting language...

Regards
Mike

Re: My gratuitous arp script

Posted: Tue May 17, 2022 1:00 am
by rextended
[...] I guess your implementation was theoretical, right? [...]
Really... not... why theoretical? (tested on on 6.48.6 terminal):
the [:toip ] is present on sendgarp before I move on ip2hex, but I forgot to update the first part of the script on forum.
I do not know why you need to put :global xxx inside last part, because on terminal work,
probably because I test the script on terminal and not on scheduler or script, but is easy fixable.

Still split/join implementation makes it easier to understand (to its developer :D )
Yes, but the single function $lmac with a single cycle inside is more simple and easy understandable,
compared with 3 functions with one "nested" with another, and each with multiple "if" and "cycle" used inside...

However I prefer to have everything in the same script, changing something in the general functions in other scripts to be loaded previously, to adapt them to the needs, often breaks the original functioning and the previous scripts that called the function may no longer work correctly...

Thanks for sharing your works, and I hope to suggest something for you.

Re: My gratuitous arp script

Posted: Tue Sep 06, 2022 2:31 pm
by Sertik
:global map do={
# Execute function $1 on each item in array $2. Return the result array
# If $2 is a string, execute $1 on each character of the string, return the resulting string
:local out [:toarray ""];
:local action $1;
:local in $2;
:if ([:typeof $in]="str") do={
:local result "";
:for i from=0 to=([:len $in] - 1) do={
:set result ($result . [$action [:pick $in $i]]);
};
:return $result;
};

:foreach i in=$arr do={
:set ($out->[:len $out]) [$action $i]
};

:return $out;
}
An error has been made in the function. The $arr variable is not defined. We need to replace it with $2.

Re: My gratuitous arp script

Posted: Wed Dec 13, 2023 4:57 pm
by jaclaz
Maybe useful, maybe not, I am attaching a batch script (tested on Windows XP and later) to create the "data" to be sent with Traffic-Generator tool according to:
https://wiki.mikrotik.com/wiki/Manual:I ... uitous_ARP

It contains some explanation of what the packet is composed of and lists possibly needed on some systems alternative packets (different Target MAC and reply instead of request).

The extension of the file needs to be changed from .txt to .cmd to become executable..

I am also attaching a small spreadsheet in .xls format to the same effect and a screenshot.

Re: My gratuitous arp script

Posted: Tue Jan 09, 2024 1:03 pm
by jaclaz
Only as a side note, possibly useful to some other member, I did some experiments with sending Gratuitous Arp via script.

It wasn't working from script, while working fine from command line.

I thought that the issue was since I was using it from Netwatch the need to add the dont-require-permissions=yes, but in itself didn't make any difference.

But after a quite few tests, in order to actually run properly the script (for *whatever* reason) needs the "sniff" permission (in addition to the normal "policy=write, read, test, reboot").

And of course, if run from Netwatch also the dont-require-permissions=yes.

I understand now that the "dont-require-permissions=" is related to the permissions of the "user" (netwatch in this case) whilst the "policy=" is related to the permissions of the actual "script" (of the commands in it), something that I had completely missed.

Still the "sniff" policy is defined in the Mikrotik documentation I read as "sniff - policy that grants rights to use packet sniffer tool", it doesn't look as connected to "/tool traffic-generator inject " command :? .