Community discussions

MikroTik App
 
boxcee
just joined
Topic Author
Posts: 21
Joined: Tue Oct 15, 2024 11:12 am

Trying to access a Luleey SFP module

Mon Jan 20, 2025 8:55 am

I recently bought a Luleey SFP GPON module as a replacement for the 'official' GPON module from my internet provider. This one here: https://www.luleey.com/product/2-5g-xpon-stick-sfp-onu/. The official one has connection issues sporadically, and the Luleey one is supposed to be more stable.

According to the Luleey guide, I should be able to access the module on 192.168.1.1 when putting my accessing device on the same network.

Here are the steps I tried and which yielded unsuccessful results:
# 1. Plug in the SFP module into the SFP slot of my RB4011iGS+
# 2. Assign an address and network to the module
/ip address
add address=192.168.1.2/24 comment=luleey interface=sfp-sfpplus1 network=\
    192.168.1.0
# 3. Add a firewall rule to masquerade outgoing traffic to the SFP module
/ip firewall nat
add action=masquerade chain=srcnat comment=luleey out-interface=\
    sfp-sfpplus1
# 4. Access device, no success
My firewall rules:
# 2025-01-20 07:18:24 by RouterOS 7.17
# software id = REDACTED
#
# model = RB4011iGS+
# serial number = REDACTED
/ip firewall address-list
add address=192.168.88.7 comment=REDACTED list=REDACTED
add address=192.168.88.12 comment=REDACTED list=REDACTED
add address=192.168.88.13 comment=nginx list=REDACTED
/ip firewall filter
add action=jump chain=forward comment="jump to kid-control rules" \
    jump-target=kid-control
add action=accept chain=input comment=\
    "defconf: accept established,related,untracked" connection-state=\
    established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
    invalid
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment=\
    "defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=accept chain=input comment="allow Wireguard" dst-port=13231 \
    protocol=udp
add action=accept chain=input comment="allow REDACTED (Wireguard)" dst-port=\
    13232 protocol=udp
add action=accept chain=input comment="allow REDACTED (Wireguard )" dst-port=\
    21841 protocol=udp
add action=drop chain=input comment="defconf: drop all not coming from LAN" \
    in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" \
    ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" \
    ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" \
    connection-state=established,related hw-offload=yes
add action=accept chain=forward comment=\
    "defconf: accept established,related, untracked" connection-state=\
    established,related,untracked
add action=drop chain=forward comment="allow REDACTED access only" \
    dst-address-list=!REDACTED in-interface=REDACTED
add action=drop chain=forward comment="defconf: drop invalid" \
    connection-state=invalid
add action=drop chain=forward comment=\
    "defconf: drop all from WAN not DSTNATed" connection-nat-state=!dstnat \
    connection-state=new in-interface-list=WAN
/ip firewall nat
add action=masquerade chain=srcnat comment=luleey disabled=yes out-interface=\
    sfp-sfpplus1
add action=masquerade chain=srcnat comment="defconf: masquerade" \
    ipsec-policy=out,none out-interface-list=WAN
add action=masquerade chain=srcnat comment=REDACTED out-interface=REDACTED
And interface-lists:
/interface list member
add comment=defconf interface=bridge list=LAN
add interface=sfp-sfpplus1 list=WAN
add comment=wireguard interface=wireguard1 list=LAN
add comment=REDACTED interface=REDACTED list=LAN
add interface=sfpv7 list=WAN
add interface=telekom list=WAN
First things first: Is this something which conceptually should work? Or is there an error in my logic here?
 
User avatar
TheCat12
Member
Member
Posts: 476
Joined: Fri Dec 31, 2021 9:13 pm

Re: Trying to access a Luleey SFP module

Mon Jan 20, 2025 10:38 am

It is hard to work with snippets since everything is interconnected, so kindly attach a full exported config of the router:

export file=anynameyouwish (minus sensitive info like serial numbers, public IPs, etc.)
 
boxcee
just joined
Topic Author
Posts: 21
Joined: Tue Oct 15, 2024 11:12 am

Re: Trying to access a Luleey SFP module

Mon Jan 20, 2025 10:57 am

Full config export:
# 2025-01-20 09:48:07 by RouterOS 7.17
# software id = REDACTED
#
# model = RB4011iGS+
# serial number = REDACTED
/interface bridge
add admin-mac=REDACTED auto-mac=no comment=defconf name=bridge
/interface ethernet
set [ find default-name=ether3 ] comment="b\C3\BCro"
set [ find default-name=ether6 ] comment=knx
/interface wireguard
add listen-port=13232 mtu=1420 name=REDACTED1
add listen-port=21841 mtu=1420 name=REDACTED1
add listen-port=13231 mtu=1420 name=wireguard1
/interface vlan
add interface=bridge name=guest vlan-id=300
add interface=bridge name=home vlan-id=200
add interface=bridge name=mgmt vlan-id=100
add interface=sfp-sfpplus1 name=sfpv7 vlan-id=7
/interface bonding
add mode=802.3ad name=nas slaves=ether7,ether8
add mode=balance-xor name=switch slaves=ether9,ether10 transmit-hash-policy=\
    layer-2-and-3
/interface pppoe-client
add add-default-route=yes allow=pap,chap,mschap2 disabled=no interface=sfpv7 \
    name=telekom use-peer-dns=yes user=REDACTED
/interface list
add comment=defconf name=WAN
add comment=defconf name=LAN
add name=GUEST
/interface wifi datapath
add bridge=bridge comment=defconf disabled=no name=capdp
add bridge=bridge comment=guest disabled=no name=guest-datapath vlan-id=300
/interface wifi security
add authentication-types=wpa2-psk,wpa3-psk connect-priority=0 disabled=no \
    name=family-sec
add authentication-types=wpa2-psk,wpa3-psk disabled=no name=guest-sec
/interface wifi configuration
add channel.reselect-interval=10m..30m comment=family disabled=no mode=ap \
    name=family security=family-sec security.connect-priority=0 .ft=yes \
    .ft-over-ds=yes ssid=BuddhasBlessedBunch
add comment=guest country=Germany datapath=guest-datapath disabled=yes mode=\
    ap name=guest security=guest-sec ssid=BuddhasBlessedGuests
/ip kid-control
add disabled=yes mon=9h-21h name="Stop Internet "
/ip pool
add name=dhcp ranges=192.168.88.100-192.168.88.254
add name=mgmt ranges=192.168.90.100-192.168.90.254
add name=home ranges=192.168.91.100-192.168.91.254
add name=guest ranges=192.168.92.100-192.168.92.254
/ip dhcp-server
add address-pool=dhcp interface=bridge lease-script=dhcp-to-dns lease-time=2h \
    name=defconf
add address-pool=mgmt interface=mgmt lease-script=dhcp-to-dns name=mgmt
add address-pool=home interface=home lease-script=dhcp-to-dns name=home
add address-pool=guest interface=guest lease-script=dhcp-to-dns name=guest
/port
set 0 name=serial0
set 1 name=serial1
/system logging action
set 3 remote=nas.lan
/interface bridge port
add bridge=bridge comment=defconf interface=ether2
add bridge=bridge comment=defconf interface=ether3
add bridge=bridge comment=defconf interface=ether4
add bridge=bridge comment=defconf interface=ether5
add bridge=bridge comment=defconf interface=ether6
add bridge=bridge comment=defconf interface=ether1
add bridge=bridge interface=nas
add bridge=bridge interface=switch
/ip neighbor discovery-settings
set discover-interface-list=LAN
/interface bridge vlan
add bridge=bridge comment=mgmt tagged="ether1,ether2,ether3,ether4,ether5,ethe\
    r6,ether7,ether8,ether9,ether10,bridge" vlan-ids=100
add bridge=bridge comment=home tagged="ether1,ether2,ether3,ether4,ether5,ethe\
    r6,ether7,ether8,ether9,ether10,bridge" vlan-ids=200
add bridge=bridge comment=guest tagged="ether1,ether2,ether3,ether4,ether5,eth\
    er6,ether7,ether8,ether9,ether10,bridge" vlan-ids=300
/interface list member
add comment=defconf interface=bridge list=LAN
add interface=sfp-sfpplus1 list=WAN
add comment=wireguard interface=wireguard1 list=LAN
add comment=REDACTED interface=REDACTED1 list=LAN
add interface=sfpv7 list=WAN
add interface=telekom list=WAN
/interface ovpn-server server
add mac-address=FE:97:BD:F3:AB:5D name=ovpn-server1
/interface wifi capsman
set ca-certificate=auto enabled=yes package-path="" require-peer-certificate=\
    no upgrade-policy=suggest-same-version
/interface wifi provisioning
add action=create-dynamic-enabled comment=5ghz disabled=no \
    master-configuration=family slave-configurations=guest supported-bands=\
    5ghz-ax
add action=create-dynamic-enabled comment=2ghx disabled=no \
    master-configuration=family slave-configurations=guest supported-bands=\
    2ghz-ax
/interface wireguard peers
add allowed-address=192.168.87.10/32 client-address=192.168.87.10/32 \
    client-dns=192.168.87.1 client-endpoint=REDACTEDREDACTED \
    interface=wireguard1 name=pixel preshared-key=\
    "REDACTED" private-key=\
    "REDACTED" public-key=\
    "zA3KVqkFdyeYs8SeH5bBty5q8a6aqZjHmywineHN0EQ="
add allowed-address=192.168.87.11/32 client-address=192.168.87.11/32 \
    client-dns=192.168.87.1 client-endpoint=REDACTEDREDACTED \
    interface=wireguard1 name=tuxedo preshared-key=\
    "REDACTED" private-key=\
    "REDACTED" public-key=\
    "hYoazZbXFmbE148jFN8s6v0D3cRCkTawVtaaXySosEE="
add allowed-address=192.168.87.12/32 client-address=192.168.87.12/32 \
    client-dns=192.168.87.1 client-endpoint=REDACTEDREDACTED \
    interface=wireguard1 name=travelrouter preshared-key=\
    "REDACTED" private-key=\
    "REDACTED" public-key=\
    "PaIB5Rp1hI1pRtadUrtSDAFEOI//urx6fhApJaZqrDM="
add allowed-address=192.168.87.13/32 client-address=192.168.87.13/32 \
    client-dns=192.168.87.1 client-endpoint=REDACTEDREDACTED \
    interface=wireguard1 name=iphone preshared-key=\
    "REDACTED" private-key=\
    "REDACTED" public-key=\
    "FHB9LwPAgM7MoR2pSOqO1RnvaAXy77XkpJ4Mo62qPis="
add allowed-address=192.168.86.11/32 client-address=192.168.86.11/32 \
    client-dns=192.168.86.1 client-endpoint=REDACTEDREDACTED \
    interface=REDACTED1 name=rieckmanns preshared-key=\
    "REDACTED" private-key=\
    "REDACTED" public-key=\
    "HJl4GXRRVzEdIlCNxw7c2k0oADNBxtkpxnT+C6h45Ss="
add allowed-address=0.0.0.0/0 endpoint-address=REDACTED endpoint-port=\
    51026 interface=REDACTED1 name=REDACTED public-key=\
    "Tq7MDaNyunVrfko5mLMWoN8rZ08hSJOCpJEPUfQcHVo="
/ip address
add address=192.168.88.1/24 comment=defconf interface=bridge network=\
    192.168.88.0
add address=192.168.87.1/24 comment=wireguard interface=wireguard1 network=\
    192.168.87.0
add address=10.102.6.2/24 comment=REDACTED interface=REDACTED1 network=\
    10.102.6.0
add address=192.168.86.1/24 comment=REDACTED interface=REDACTED1 network=\
    192.168.86.0
add address=192.168.90.1/24 comment=mgmt interface=mgmt network=192.168.90.0
add address=192.168.91.1/24 comment=home interface=home network=192.168.91.0
add address=192.168.92.1/24 comment=guest interface=guest network=\
    192.168.92.0
add address=192.168.1.2/24 comment=luleey interface=sfp-sfpplus1 network=\
    192.168.1.0
/ip dhcp-server network
add address=192.168.88.0/24 comment=defconf dns-server=192.168.88.14 domain=\
    lan gateway=192.168.88.1 netmask=24
add address=192.168.90.0/24 comment=mgmt dns-server=192.168.90.1 gateway=\
    192.168.90.1
add address=192.168.91.0/24 comment=home dns-server=192.168.90.1 gateway=\
    192.168.91.1
add address=192.168.92.0/24 comment=guest dns-server=192.168.90.1 gateway=\
    192.168.92.1
/ip dns
set allow-remote-requests=yes servers=9.9.9.9
/ip dns static
add address=192.168.88.110 comment=\
    "managed by dhcp-to-dns, macaddress=00:F6:20:4C:5E:0A, server=defconf" \
    name=00-F6-20-4C-5E-0A.lan ttl=5m type=A
add address=192.168.88.103 comment=\
    "managed by dhcp-to-dns, macaddress=48:A9:8A:A2:9D:29, server=defconf" \
    name=48-A9-8A-A2-9D-29.lan ttl=5m type=A
add address=192.168.88.179 comment=\
    "managed by dhcp-to-dns, macaddress=68:A4:0E:A4:14:C3, server=defconf" \
    name=68-A4-0E-A4-14-C3.lan ttl=5m type=A
add address=192.168.88.108 comment=\
    "managed by dhcp-to-dns, macaddress=84:0D:8E:B7:69:A0, server=defconf" \
    name=84-0D-8E-B7-69-A0.lan ttl=5m type=A
add address=192.168.88.105 comment=\
    "managed by dhcp-to-dns, macaddress=74:4D:BD:8A:19:00, server=defconf" \
    name=74-4D-BD-8A-19-00.lan ttl=5m type=A
add address=192.168.88.113 comment=\
    "managed by dhcp-to-dns, macaddress=58:E4:88:42:34:58, server=defconf" \
    name=58-E4-88-42-34-58.lan ttl=5m type=A
add address=192.168.88.101 comment=\
    "managed by dhcp-to-dns, macaddress=10:DA:43:00:AD:D9, server=defconf" \
    name=10-DA-43-00-AD-D9.lan ttl=5m type=A
add address=192.168.88.254 comment=\
    "managed by dhcp-to-dns, macaddress=CC:1B:E0:80:FB:09, server=defconf" \
    name=CC-1B-E0-80-FB-09.lan ttl=5m type=A
add address=192.168.88.112 comment=\
    "managed by dhcp-to-dns, macaddress=F0:EF:86:19:80:61, server=defconf" \
    name=F0-EF-86-19-80-61.lan ttl=5m type=A
add address=192.168.88.107 comment=\
    "managed by dhcp-to-dns, macaddress=98:F4:AB:B8:64:E1, server=defconf" \
    name=98-F4-AB-B8-64-E1.lan ttl=5m type=A
add address=192.168.88.106 comment=\
    "managed by dhcp-to-dns, macaddress=92:66:00:A0:AD:52, server=defconf" \
    name=92-66-00-A0-AD-52.lan ttl=5m type=A
add address=192.168.88.111 comment=\
    "managed by dhcp-to-dns, macaddress=F0:EF:86:16:D8:0C, server=defconf" \
    name=F0-EF-86-16-D8-0C.lan ttl=5m type=A
add address=192.168.88.133 comment=\
    "managed by dhcp-to-dns, macaddress=BC:24:11:32:1A:58, server=defconf" \
    name=BC-24-11-32-1A-58.lan ttl=5m type=A
add address=192.168.88.123 comment=\
    "managed by dhcp-to-dns, macaddress=BC:24:11:1D:7E:77, server=defconf" \
    name=BC-24-11-1D-7E-77.lan ttl=5m type=A
add address=192.168.88.128 comment=\
    "managed by dhcp-to-dns, macaddress=BC:24:11:5D:F2:F2, server=defconf" \
    name=BC-24-11-5D-F2-F2.lan ttl=5m type=A
add address=192.168.88.181 comment=\
    "managed by dhcp-to-dns, macaddress=56:E8:1C:8E:C6:DA, server=defconf" \
    name=56-E8-1C-8E-C6-DA.lan ttl=5m type=A
add address=192.168.88.174 comment=\
    "managed by dhcp-to-dns, macaddress=9C:2D:CD:B7:D5:DF, server=defconf" \
    name=9C-2D-CD-B7-D5-DF.lan ttl=5m type=A
add cname=00-F6-20-4C-5E-0A.lan comment=\
    "managed by dhcp-to-dns, macaddress=00:F6:20:4C:5E:0A, server=defconf" \
    name=Chromecast-Ultra.lan ttl=5m type=CNAME
add cname=48-A9-8A-A2-9D-29.lan comment=\
    "managed by dhcp-to-dns, macaddress=48:A9:8A:A2:9D:29, server=defconf" \
    name=AP-Hauswirtschaftsraum.lan ttl=5m type=CNAME
add cname=68-A4-0E-A4-14-C3.lan comment=\
    "managed by dhcp-to-dns, macaddress=68:A4:0E:A4:14:C3, server=defconf" \
    name=SIEMENS-EX877NX68E-68A40EA414C3.lan ttl=5m type=CNAME
add cname=84-0D-8E-B7-69-A0.lan comment=\
    "managed by dhcp-to-dns, macaddress=84:0D:8E:B7:69:A0, server=defconf" \
    name=tasmota-B769A0-2464.lan ttl=5m type=CNAME
add cname=74-4D-BD-8A-19-00.lan comment=\
    "managed by dhcp-to-dns, macaddress=74:4D:BD:8A:19:00, server=defconf" \
    name=ems-esp.lan ttl=5m type=CNAME
add cname=CC-1B-E0-80-FB-09.lan comment=\
    "managed by dhcp-to-dns, macaddress=CC:1B:E0:80:FB:09, server=defconf" \
    name=KNX-IPIF-80FB09.lan ttl=5m type=CNAME
add cname=F0-EF-86-19-80-61.lan comment=\
    "managed by dhcp-to-dns, macaddress=F0:EF:86:19:80:61, server=defconf" \
    name=Google-Home-Mini.lan ttl=5m type=CNAME
add cname=98-F4-AB-B8-64-E1.lan comment=\
    "managed by dhcp-to-dns, macaddress=98:F4:AB:B8:64:E1, server=defconf" \
    name=shellyplug-s-B864E1.lan ttl=5m type=CNAME
add cname=92-66-00-A0-AD-52.lan comment=\
    "managed by dhcp-to-dns, macaddress=92:66:00:A0:AD:52, server=defconf" \
    name=MacBookPro.lan ttl=5m type=CNAME
add cname=BC-24-11-32-1A-58.lan comment=\
    "managed by dhcp-to-dns, macaddress=BC:24:11:32:1A:58, server=defconf" \
    name=zigbee2mqtt.lan ttl=5m type=CNAME
add cname=BC-24-11-1D-7E-77.lan comment=\
    "managed by dhcp-to-dns, macaddress=BC:24:11:1D:7E:77, server=defconf" \
    name=paperless-ngx.lan ttl=5m type=CNAME
add cname=BC-24-11-5D-F2-F2.lan comment=\
    "managed by dhcp-to-dns, macaddress=BC:24:11:5D:F2:F2, server=defconf" \
    name=recyclarr.lan ttl=5m type=CNAME
add cname=56-E8-1C-8E-C6-DA.lan comment=\
    "managed by dhcp-to-dns, macaddress=56:E8:1C:8E:C6:DA, server=defconf" \
    name=Pixel-6.lan ttl=5m type=CNAME
add cname=9C-2D-CD-B7-D5-DF.lan comment=\
    "managed by dhcp-to-dns, macaddress=9C:2D:CD:B7:D5:DF, server=defconf" \
    name=DESKTOP-B0UL83K.lan ttl=5m type=CNAME
add address=192.168.88.116 comment=\
    "managed by dhcp-to-dns, macaddress=82:20:AC:54:BB:7F, server=defconf" \
    name=82-20-AC-54-BB-7F.lan ttl=5m type=A
add address=192.168.88.164 comment=\
    "managed by dhcp-to-dns, macaddress=E0:F6:B5:E3:A0:CF, server=defconf" \
    name=E0-F6-B5-E3-A0-CF.lan ttl=5m type=A
add cname=82-20-AC-54-BB-7F.lan comment=\
    "managed by dhcp-to-dns, macaddress=82:20:AC:54:BB:7F, server=defconf" \
    name=iPhone.lan ttl=5m type=CNAME
add address=192.168.88.100 comment=\
    "managed by dhcp-to-dns, macaddress=3C:A3:08:C2:81:43, server=defconf" \
    name=3C-A3-08-C2-81-43.lan ttl=5m type=A
add cname=3C-A3-08-C2-81-43.lan comment=\
    "managed by dhcp-to-dns, macaddress=3C:A3:08:C2:81:43, server=defconf" \
    name=SoundTouch-20.lan ttl=5m type=CNAME
add address=192.168.88.194 comment=\
    "managed by dhcp-to-dns, macaddress=8C:17:59:72:4D:EA, server=defconf" \
    name=8C-17-59-72-4D-EA.lan ttl=5m type=A
add address=192.168.88.175 comment=\
    "managed by dhcp-to-dns, macaddress=A4:83:E7:CA:88:9A, server=defconf" \
    name=A4-83-E7-CA-88-9A.lan ttl=5m type=A
add cname=A4-83-E7-CA-88-9A.lan comment=\
    "managed by dhcp-to-dns, macaddress=A4:83:E7:CA:88:9A, server=defconf" \
    name=Julias-MBP.lan ttl=5m type=CNAME
add address=192.168.88.104 comment=\
    "managed by dhcp-to-dns, macaddress=CE:A3:95:75:39:A4, server=defconf" \
    name=CE-A3-95-75-39-A4.lan ttl=5m type=A
add address=192.168.88.115 comment=\
    "managed by dhcp-to-dns, macaddress=5A:05:ED:04:D0:0E, server=defconf" \
    name=5A-05-ED-04-D0-0E.lan ttl=5m type=A
add cname=5A-05-ED-04-D0-0E.lan comment=\
    "managed by dhcp-to-dns, macaddress=5A:05:ED:04:D0:0E, server=defconf" \
    name=Watch.lan ttl=5m type=CNAME
add address=192.168.88.119 comment=\
    "managed by dhcp-to-dns, macaddress=4A:1B:F2:31:39:96, server=defconf" \
    name=4A-1B-F2-31-39-96.lan ttl=5m type=A
add cname=4A-1B-F2-31-39-96.lan comment=\
    "managed by dhcp-to-dns, macaddress=4A:1B:F2:31:39:96, server=defconf" \
    name=iPad.lan ttl=5m type=CNAME
add address=192.168.88.120 comment=\
    "managed by dhcp-to-dns, macaddress=DE:8C:E0:F5:51:8F, server=defconf" \
    name=DE-8C-E0-F5-51-8F.lan ttl=5m type=A
add address=192.168.88.102 comment=\
    "managed by dhcp-to-dns, macaddress=48:A9:8A:A2:8F:9C, server=defconf" \
    name=48-A9-8A-A2-8F-9C.lan ttl=5m type=A
add cname=48-A9-8A-A2-8F-9C.lan comment=\
    "managed by dhcp-to-dns, macaddress=48:A9:8A:A2:8F:9C, server=defconf" \
    name=AP-Dachboden.lan ttl=5m type=CNAME
add address=192.168.88.138 comment=\
    "managed by dhcp-to-dns, macaddress=A4:42:3B:6D:4F:57, server=defconf" \
    name=A4-42-3B-6D-4F-57.lan ttl=5m type=A
add cname=A4-42-3B-6D-4F-57.lan comment=\
    "managed by dhcp-to-dns, macaddress=A4:42:3B:6D:4F:57, server=defconf" \
    name=LAPTOP-RLPTO8GK.lan ttl=5m type=CNAME
add disabled=yes name="--- dhcp-to-dns above ---" type=NXDOMAIN
add address=192.168.88.1 comment=defconf name=router.lan type=A
add address=192.168.88.3 name=proxmox2.lan type=A
add address=192.168.88.6 name=nas.lan type=A
add address=192.168.88.7 name=REDACTED.lan type=A
add address=192.168.88.8 name=REDACTED.lan type=A
add address=192.168.88.11 name=box.lan type=A
add address=192.168.88.12 name=REDACTED.lan type=A
add address=192.168.88.14 name=homeassistant.lan type=A
add address=192.168.88.16 name=REDACTED.lan type=A
add address=192.168.88.17 name=zigbeecoordinator.lan type=A
add address=192.168.88.20 name=proxmox3.lan type=A
add address=192.168.88.21 name=proxmox.lan type=A
# bad CNAME data
add cname=nginx.lan. name=REDACTED.REDACTED type=CNAME
# bad CNAME data
add cname=nginx.lan. name=REDACTED.REDACTED type=CNAME
# bad CNAME data
add cname=nginx.lan. name=REDACTED.REDACTED type=CNAME
# bad CNAME data
add cname=nginx.lan. name=REDACTED.REDACTED type=CNAME
# bad CNAME data
add cname=homeassistant.lan. name=mqtt.lan type=CNAME
# bad CNAME data
add cname=homeassistant.lan. name=REDACTED.lan type=CNAME
# bad CNAME data
add cname=homeassistant.lan. name=REDACTED.lan type=CNAME
# bad CNAME data
add cname=homeassistant.lan. name=REDACTED.lan type=CNAME
# bad CNAME data
add cname=homeassistant.lan. name=nginx.lan type=CNAME
/ip firewall address-list
add address=192.168.88.7 comment=REDACTED list=REDACTED
add address=192.168.88.12 comment=REDACTED list=REDACTED
add address=192.168.88.13 comment=nginx list=REDACTED
/ip firewall filter
add action=jump chain=forward comment="jump to kid-control rules" \
    jump-target=kid-control
add action=accept chain=input comment=\
    "defconf: accept established,related,untracked" connection-state=\
    established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
    invalid
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment=\
    "defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=accept chain=input comment="allow Wireguard" dst-port=13231 \
    protocol=udp
add action=accept chain=input comment="allow REDACTED (Wireguard)" dst-port=\
    13232 protocol=udp
add action=accept chain=input comment="allow REDACTED (Wireguard )" dst-port=\
    21841 protocol=udp
add action=drop chain=input comment="defconf: drop all not coming from LAN" \
    in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" \
    ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" \
    ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" \
    connection-state=established,related hw-offload=yes
add action=accept chain=forward comment=\
    "defconf: accept established,related, untracked" connection-state=\
    established,related,untracked
add action=drop chain=forward comment="allow REDACTED access only" \
    dst-address-list=!REDACTED in-interface=REDACTED1
add action=drop chain=forward comment="defconf: drop invalid" \
    connection-state=invalid
add action=drop chain=forward comment=\
    "defconf: drop all from WAN not DSTNATed" connection-nat-state=!dstnat \
    connection-state=new in-interface-list=WAN
/ip firewall nat
add action=masquerade chain=srcnat comment=luleey disabled=yes out-interface=\
    sfp-sfpplus1
add action=masquerade chain=srcnat comment="defconf: masquerade" \
    ipsec-policy=out,none out-interface-list=WAN
add action=masquerade chain=srcnat comment=REDACTED out-interface=REDACTED1
/ip ipsec profile
set [ find default=yes ] dpd-interval=2m dpd-maximum-failures=5
/ip kid-control device
add mac-address=92:66:00:A0:AD:52 name="MacBook Pro " user="Stop Internet "
/ipv6 dhcp-client
add add-default-route=yes interface=telekom pool-name=pool-ipv6 request=\
    prefix
/ipv6 firewall address-list
add address=::/128 comment="defconf: unspecified address" list=bad_ipv6
add address=::1/128 comment="defconf: lo" list=bad_ipv6
add address=fec0::/10 comment="defconf: site-local" list=bad_ipv6
add address=::ffff:0.0.0.0/96 comment="defconf: ipv4-mapped" list=bad_ipv6
add address=::/96 comment="defconf: ipv4 compat" list=bad_ipv6
add address=100::/64 comment="defconf: discard only " list=bad_ipv6
add address=2001:db8::/32 comment="defconf: documentation" list=bad_ipv6
add address=2001:10::/28 comment="defconf: ORCHID" list=bad_ipv6
add address=3ffe::/16 comment="defconf: 6bone" list=bad_ipv6
/ipv6 firewall filter
add action=accept chain=input comment=\
    "defconf: accept established,related,untracked" connection-state=\
    established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
    invalid
add action=accept chain=input comment="defconf: accept ICMPv6" protocol=\
    icmpv6
add action=accept chain=input comment="defconf: accept UDP traceroute" \
    dst-port=33434-33534 protocol=udp
add action=accept chain=input comment=\
    "defconf: accept DHCPv6-Client prefix delegation." dst-port=546 protocol=\
    udp src-address=fe80::/10
add action=accept chain=input comment="defconf: accept IKE" dst-port=500,4500 \
    protocol=udp
add action=accept chain=input comment="defconf: accept ipsec AH" protocol=\
    ipsec-ah
add action=accept chain=input comment="defconf: accept ipsec ESP" protocol=\
    ipsec-esp
add action=accept chain=input comment=\
    "defconf: accept all that matches ipsec policy" ipsec-policy=in,ipsec
add action=drop chain=input comment=\
    "defconf: drop everything else not coming from LAN" in-interface-list=\
    !LAN
add action=accept chain=forward comment=\
    "defconf: accept established,related,untracked" connection-state=\
    established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" \
    connection-state=invalid
add action=drop chain=forward comment=\
    "defconf: drop packets with bad src ipv6" src-address-list=bad_ipv6
add action=drop chain=forward comment=\
    "defconf: drop packets with bad dst ipv6" dst-address-list=bad_ipv6
add action=drop chain=forward comment="defconf: rfc4890 drop hop-limit=1" \
    hop-limit=equal:1 protocol=icmpv6
add action=accept chain=forward comment="defconf: accept ICMPv6" protocol=\
    icmpv6
add action=accept chain=forward comment="defconf: accept HIP" protocol=139
add action=accept chain=forward comment="defconf: accept IKE" dst-port=\
    500,4500 protocol=udp
add action=accept chain=forward comment="defconf: accept ipsec AH" protocol=\
    ipsec-ah
add action=accept chain=forward comment="defconf: accept ipsec ESP" protocol=\
    ipsec-esp
add action=accept chain=forward comment=\
    "defconf: accept all that matches ipsec policy" ipsec-policy=in,ipsec
add action=drop chain=forward comment=\
    "defconf: drop everything else not coming from LAN" in-interface-list=\
    !LAN
/system clock
set time-zone-name=Europe/Berlin
/system identity
set name=Router
/system logging
add action=remote topics=critical,error,warning
/system note
set show-at-login=no
/system ntp client
set enabled=yes
/system ntp client servers
add address=0.de.pool.ntp.org
add address=1.de.pool.ntp.org
add address=2.de.pool.ntp.org
add address=3.de.pool.ntp.org
/system routerboard settings
set auto-upgrade=yes enter-setup-on=delete-key
/system scheduler
add comment=strato interval=1h name=dyndns on-event=\
    "/system script run dyndns" policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    start-date=2024-10-20 start-time=17:57:15
add name=global-scripts on-event=\
    "/system/script { run global-config; run global-functions; }" policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    start-time=startup
add interval=1d name=ScriptInstallUpdate on-event=\
    ":global ScriptInstallUpdate; \$ScriptInstallUpdate;" policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    start-time=startup
add interval=15m name=dhcp-to-dns on-event="/system/script/run dhcp-to-dns;" \
    policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    start-time=startup
/system script
add comment=strato dont-require-permissions=no name=dyndns owner=admin \
    policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    source=":global ddnsuser \"REDACTED\"\
    \n:global ddnspass \"REDACTED\"\
    \n:global theinterface \"telekom\"\
    \n:global ddnshost1 \"REDACTEDREDACTED\"\
    \n\
    \n:global ipddns\
    \n:global ipfresh [/ip address get [find where interface=\$theinterface] v\
    alue-name=address]\
    \n\
    \n:if ([ :typeof \$ipfresh ] = nil ) do={\
    \n  :log info (\"DynDNS: No ip address on \$theinterface.\")\
    \n} else={\
    \n  :for i from=( [:len \$ipfresh] - 1) to=0 do={ \
    \n    :if ( [:pick \$ipfresh \$i] = \"/\") do={\
    \n      :set ipfresh [:pick \$ipfresh 0 \$i];\
    \n    }\
    \n  }\
    \n  :if (\$ipddns != \$ipfresh) do={\
    \n    :log info (\"DynDNS: IP-DynDNS = \$ipddns\")\
    \n    :log info (\"DynDNS: IP-Fresh = \$ipfresh\")\
    \n    :log info (\"DynDNS: Update IP needed. Sending UPDATE...!\")\
    \n    :global str1 \"/nic/update\\\?hostname=\$ddnshost1&myip=\$ipfresh\"\
    \n    /tool fetch address=dyndns.strato.com src-path=\$str1 user=\$ddnsuse\
    r password=\$ddnspass mode=https dst-path=(\"/DynDNS.\$ddnshost1\")\
    \n    :delay 1\
    \n    :global str1 [/file find name=\"DynDNS.\$ddnshost1\"];\
    \n    /file remove \$str1\
    \n    :global ipddns \$ipfresh\
    \n    :log info \"DynDNS: IP updated to \$ipfresh!\"\
    \n  } else={\
    \n    :log info \"DynDNS: dont need changes\";\
    \n  }\
    \n}"
add dont-require-permissions=no name=global-config owner=global-config \
    policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    source="#!rsc by RouterOS\
    \n# RouterOS script: global-config\
    \n# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de>\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md\
    \n#\
    \n# global configuration\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/\
    \n\
    \n# Set this to 'true' to disable news and change notifications.\
    \n:global NoNewsAndChangesNotification false;\
    \n\
    \n# Add extra text (or emojis) in notification tags.\
    \n:global IdentityExtra \"\";\
    \n\
    \n# This is used in DNS scripts ('ipsec-to-dns' and fallback in 'dhcp-to-d\
    ns')\
    \n# and backup scripts for file names.\
    \n:global Domain \"example.com\";\
    \n\
    \n# You can send e-mail notifications. Configure the system's mail setting\
    s\
    \n# (/tool/e-mail), then install the module:\
    \n# \$ScriptInstallUpdate mod/notification-email\
    \n# The to-address needs to be filled; cc-address can be empty, one addres\
    s\
    \n# or a comma separated list of addresses.\
    \n:global EmailGeneralTo \"\";\
    \n:global EmailGeneralCc \"\";\
    \n#:global EmailGeneralTo \"mail@example.com\";\
    \n#:global EmailGeneralCc \"another@example.com,third@example.com\";\
    \n\
    \n# You can send Telegram notifications. Register a bot\
    \n# and add the token and chat ids here, then install the module:\
    \n# \$ScriptInstallUpdate mod/notification-telegram\
    \n:global TelegramTokenId \"\";\
    \n:global TelegramChatId \"\";\
    \n#:global TelegramTokenId \"123456:ABCDEF-GHI\";\
    \n#:global TelegramChatId \"12345678\";\
    \n# Using telegram-chat you have to define trusted chat ids (not group ids\
    !)\
    \n# or user names. Groups allow to chat with devices simultaneously.\
    \n#:global TelegramChatIdsTrusted {\
    \n#  \"12345678\";\
    \n#  \"example_user\";\
    \n#};\
    \n:global TelegramChatGroups \"(all)\";\
    \n#:global TelegramChatGroups \"(all|home|office)\";\
    \n\
    \n# You can send Matrix notifications. Configure these settings and\
    \n# install the module:\
    \n# \$ScriptInstallUpdate mod/notification-matrix\
    \n:global MatrixHomeServer \"\";\
    \n:global MatrixAccessToken \"\";\
    \n:global MatrixRoom \"\";\
    \n#:global MatrixHomeServer \"matrix.org\";\
    \n#:global MatrixAccessToken \"123456ABCDEFGHI...\";\
    \n#:global MatrixRoom \"!example:matrix.org\";\
    \n\
    \n# You can send Ntfy notifications. Configure these settings and\
    \n# install the module:\
    \n# \$ScriptInstallUpdate mod/notification-ntfy\
    \n:global NtfyServer \"ntfy.sh\";\
    \n:global NtfyServerUser [];\
    \n:global NtfyServerPass [];\
    \n:global NtfyServerToken [];\
    \n:global NtfyTopic \"\";\
    \n\
    \n# It is possible to override e-mail, Telegram, Matrix and Ntfy setting\
    \n# for every script. This is done in arrays, where 'Override' is appended\
    \n# to the variable name, like this:\
    \n#:global EmailGeneralToOverride {\
    \n#  \"check-certificates\"=\"override@example.com\";\
    \n#  \"backup-email\"=\"backup@example.com\";\
    \n#}\
    \n\
    \n# Toggle this to disable symbols in notifications.\
    \n:global NotificationsWithSymbols true;\
    \n# Toggle this to disable color output in terminal/cli.\
    \n:global TerminalColorOutput true;\
    \n\
    \n# This defines what backups to generate and what password to use.\
    \n:global BackupSendBinary false;\
    \n:global BackupSendExport true;\
    \n:global BackupSendGlobalConfig true;\
    \n:global BackupPassword \"v3ry-s3cr3t\";\
    \n:global BackupRandomDelay 0;\
    \n# These credentials are used to upload backup and config export files.\
    \n# SFTP authentication is tricky, you may have to limit authentication\
    \n# methods for your SSH server.\
    \n:global BackupUploadUrl \"sftp://example.com/backup/\";\
    \n:global BackupUploadUser \"mikrotik\";\
    \n:global BackupUploadPass \"v3ry-s3cr3t\";\
    \n# Copy the RouterOS installation to backup partition before feature upda\
    te.\
    \n:global BackupPartitionCopyBeforeFeatureUpdate false;\
    \n\
    \n# This defines the settings for firewall address-lists (fw-addr-lists).\
    \n:global FwAddrLists {\
    \n#  \"allow\"={\
    \n#    { url=\"https://git.eworm.de/cgit/routeros-scripts/plain/fw-addr-li\
    sts.d/allow\";\
    \n#      cert=\"ISRG Root X2\"; timeout=1w };\
    \n#  };\
    \n  \"block\"={\
    \n#    { url=\"https://git.eworm.de/cgit/routeros-scripts/plain/fw-addr-li\
    sts.d/block\";\
    \n#      cert=\"ISRG Root X2\" };\
    \n    { url=\"https://feodotracker.abuse.ch/downloads/ipblocklist_recommen\
    ded.txt\";\
    \n      cert=\"GlobalSign\" };\
    \n    { url=\"https://sslbl.abuse.ch/blacklist/sslipblacklist.txt\";\
    \n      cert=\"GlobalSign\" };\
    \n    { url=\"https://www.dshield.org/block.txt\"; cidr=\"/24\";\
    \n      cert=\"ISRG Root X1\" };\
    \n    { url=\"https://lists.blocklist.de/lists/strongips.txt\";\
    \n      cert=\"Certum Trusted Network CA\" };\
    \n#    { url=\"https://www.spamhaus.org/drop/drop_v4.json\";\
    \n#      cert=\"ISRG Root X1\" };\
    \n#    { url=\"https://www.spamhaus.org/drop/drop_v6.json\";\
    \n#      cert=\"ISRG Root X1\" };\
    \n  };\
    \n#  \"mikrotik\"={\
    \n#    { url=\"https://git.eworm.de/cgit/routeros-scripts/plain/fw-addr-li\
    sts.d/mikrotik\";\
    \n#      cert=\"ISRG Root X2\"; timeout=1w };\
    \n#  };\
    \n};\
    \n:global FwAddrListTimeOut 1d;\
    \n\
    \n# This defines what log messages to filter or include by topic or messag\
    e\
    \n# text. Regular expressions are supported. Do *NOT* set an empty string,\
    \n# that will filter or include everything!\
    \n# These are filters, so excluding messages from forwarding.\
    \n:global LogForwardFilter \"(debug|info|packet|raw)\";\
    \n:global LogForwardFilterMessage [];\
    \n#:global LogForwardFilterMessage \"message text\";\
    \n#:global LogForwardFilterMessage \"(message text|another text|...)\";\
    \n# ... and another setting with reverse logic. This includes messages eve\
    n\
    \n# if filtered above.\
    \n:global LogForwardInclude [];\
    \n:global LogForwardIncludeMessage [];\
    \n#:global LogForwardInclude \"account\";\
    \n#:global LogForwardIncludeMessage \"message text\";\
    \n\
    \n# Specify an address to enable auto update to version assumed safe.\
    \n# The configured channel (bugfix, current, release-candidate) is appende\
    d.\
    \n:global SafeUpdateUrl \"\";\
    \n#:global SafeUpdateUrl \"https://example.com/ros/safe-update/\";\
    \n# Allow to install patch updates automatically.\
    \n:global SafeUpdatePatch false;\
    \n# Allow to install updates automatically if seen in neighbor list.\
    \n:global SafeUpdateNeighbor false;\
    \n:global SafeUpdateNeighborIdentity \"\";\
    \n# Install *ALL* updates automatically!\
    \n# Set to all upper-case \"Yes, please!\" to enable.\
    \n:global SafeUpdateAll \"no\";\
    \n\
    \n# Defer the reboot for night on automatic (non-interactive) update\
    \n:global PackagesUpdateDeferReboot false;\
    \n\
    \n# These thresholds control when to send health notification\
    \n# on temperature and voltage.\
    \n:global CheckHealthTemperature {\
    \n  temperature=50;\
    \n  cpu-temperature=70;\
    \n  board-temperature1=50;\
    \n  board-temperature2=50;\
    \n};\
    \n# This is deviation on recovery threshold against notification flooding.\
    \n:global CheckHealthTemperatureDeviation 3;\
    \n:global CheckHealthVoltageLow 115;\
    \n:global CheckHealthVoltagePercent 10;\
    \n\
    \n# Access-list entries matching this comment are updated\
    \n# with daily pseudo-random PSK.\
    \n:global DailyPskMatchComment \"Daily PSK\";\
    \n:global DailyPskQrCodeUrl \"https://www.eworm.de/cgi-bin/cqrlogo-wifi.cg\
    i\";\
    \n:global DailyPskSecrets {\
    \n  { \"Abusive\"; \"Aggressive\"; \"Bored\"; \"Chemical\"; \"Cold\";\
    \n    \"Cruel\"; \"Curved\"; \"Delightful\"; \"Discreet\"; \"Elite\";\
    \n    \"Evasive\"; \"Faded\"; \"Flat\"; \"Future\"; \"Grandiose\";\
    \n    \"Hanging\"; \"Humorous\"; \"Interesting\"; \"Magenta\";\
    \n    \"Magnificent\"; \"Numerous\"; \"Optimal\"; \"Pathetic\";\
    \n    \"Possessive\"; \"Remarkable\"; \"Rightful\"; \"Ruthless\";\
    \n    \"Stale\"; \"Unusual\"; \"Useless\"; \"Various\" };\
    \n  { \"Adhesive\"; \"Amusing\"; \"Astonishing\"; \"Frantic\";\
    \n    \"Kindhearted\"; \"Limping\"; \"Roasted\"; \"Robust\";\
    \n    \"Staking\"; \"Thundering\"; \"Ultra\"; \"Unreal\" };\
    \n  { \"Belief\"; \"Button\"; \"Curtain\"; \"Edge\"; \"Jewel\";\
    \n    \"String\"; \"Whistle\" }\
    \n};\
    \n\
    \n# Specify how to assemble DNS names in ipsec-to-dns.\
    \n:global HostNameInZone true;\
    \n:global PrefixInZone true;\
    \n\
    \n# Run different commands with multiple mode-button presses.\
    \n:global ModeButton {\
    \n  1=\"/system/leds/settings/set all-leds-off=(({ \\\"never\\\"=\\\"immed\
    iate\\\"; \\\"immediate\\\"=\\\"never\\\" })->[ get all-leds-off ]);\";\
    \n  2=\":global Identity; :global SendNotification; :global SymbolForNotif\
    ication; \\\$SendNotification ([ \\\$SymbolForNotification \\\"earth\\\" ]\
    \_. \\\"Hello...\\\") (\\\"Hello world, \\\" . \\\$Identity . \\\" calling\
    !\\\");\";\
    \n  3=\"/system/shutdown;\";\
    \n  4=\"/system/reboot;\";\
    \n  5=\":global BridgePortVlan; \\\$BridgePortVlan alt;\";\
    \n# add more here...\
    \n};\
    \n# This led gives visual feedback if type is 'on' or 'off'.\
    \n:global ModeButtonLED \"user-led\";\
    \n\
    \n# Run commands on SMS action.\
    \n:global SmsAction {\
    \n  bridge-port-vlan-alt=\":global BridgePortVlan; \\\$BridgePortVlan alt;\
    \";\
    \n  reboot=\"/system/reboot;\";\
    \n  shutdown=\"/system/shutdown;\";\
    \n# add more here...\
    \n};\
    \n\
    \n# Run commands by hooking into SMS forward.\
    \n:global SmsForwardHooks {\
    \n  { match=\"magic string\";\
    \n    allowed-number=\"12345678\";\
    \n    command=\"/system/script/run ...\" };\
    \n# add more here...\
    \n};\
    \n\
    \n# This is the address used to send gps data to.\
    \n:global GpsTrackUrl \"https://example.com/index.php\";\
    \n\
    \n# This is the base url to fetch scripts from.\
    \n:global ScriptUpdatesBaseUrl \"https://git.eworm.de/cgit/routeros-script\
    s/plain/\";\
    \n# alternative urls - main: stable code - next: currently in development\
    \n#:global ScriptUpdatesBaseUrl \"https://raw.githubusercontent.com/eworm-\
    de/routeros-scripts/main/\";\
    \n#:global ScriptUpdatesBaseUrl \"https://raw.githubusercontent.com/eworm-\
    de/routeros-scripts/next/\";\
    \n#:global ScriptUpdatesBaseUrl \"https://gitlab.com/eworm-de/routeros-scr\
    ipts/raw/main/\";\
    \n#:global ScriptUpdatesBaseUrl \"https://gitlab.com/eworm-de/routeros-scr\
    ipts/raw/next/\";\
    \n:global ScriptUpdatesUrlSuffix \"\";\
    \n# use next branch with default url (git.eworm.de)\
    \n#:global ScriptUpdatesUrlSuffix \"\?h=next\";\
    \n\
    \n# Use this for defaults with \$ScriptRunOnce\
    \n# Install module with:\
    \n# \$ScriptInstallUpdate mod/scriptrunonce\
    \n:global ScriptRunOnceBaseUrl \"\";\
    \n:global ScriptRunOnceUrlSuffix \"\";\
    \n\
    \n# This project is developed in private spare time and usage is free of c\
    harge\
    \n# for you. If you like the scripts and think this is of value for you or\
    \_your\
    \n# business please consider a donation:\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/#donate\
    \n# Enable this to silence donation hint.\
    \n:global IDonate false;\
    \n\
    \n# Use this for certificate auto-renew\
    \n:global CertRenewUrl \"\";\
    \n#:global CertRenewUrl \"https://example.com/certificates/\";\
    \n:global CertRenewTime 3w;\
    \n:global CertRenewPass {\
    \n  \"v3ry-s3cr3t\";\
    \n  \"4n0th3r-s3cr3t\";\
    \n};\
    \n:global CertWarnTime 2w;\
    \n:global CertIssuedExportPass {\
    \n  \"cert1-cn\"=\"v3ry-s3cr3t\";\
    \n  \"cert2-cn\"=\"4n0th3r-s3cr3t\";\
    \n};\
    \n\
    \n# load custom settings from overlay and snippets\
    \n# Warning: Do *NOT* copy this code to overlay!\
    \n:foreach Script in=([ /system/script/find where name=\"global-config-ove\
    rlay\" ], \\\
    \n                    [ /system/script/find where name~\"^global-config-ov\
    erlay.d/\" ]) do={\
    \n  :do {\
    \n    /system/script/run \$Script;\
    \n  } on-error={\
    \n    :log error (\"Loading configuration from overlay or snippet \" . \\\
    \n        [ /system/script/get \$Script name ] . \" failed!\");\
    \n  }\
    \n}\
    \n"
add dont-require-permissions=no name=global-config-overlay owner=\
    global-config-overlay policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="#\
    \_Overlay for global configuration by RouterOS Scripts\
    \n# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de>\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md\
    \n#\
    \n# global configuration, custom overlay\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/#editing-configuratio\
    n\
    \n\
    \n# Copy relevant configuration from global-config, paste and modify it he\
    re.\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/global-config.rsc\
    \n\
    \n\
    \n# End of global-config-overlay\
    \n"
add dont-require-permissions=no name=global-functions owner=global-functions \
    policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    source="#!rsc by RouterOS\
    \n# RouterOS script: global-functions\
    \n# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de>\
    \n#                         Michael Gisbers <michael@gisbers.de>\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md\
    \n#\
    \n# requires RouterOS, version=7.14\
    \n#\
    \n# global functions\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/\
    \n\
    \n:local ScriptName [ :jobname ];\
    \n\
    \n# expected configuration version\
    \n:global ExpectedConfigVersion 131;\
    \n\
    \n# global variables not to be changed by user\
    \n:global GlobalFunctionsReady false;\
    \n:global Identity [ /system/identity/get name ];\
    \n\
    \n# global functions\
    \n:global AlignRight;\
    \n:global CertificateAvailable;\
    \n:global CertificateDownload;\
    \n:global CertificateNameByCN;\
    \n:global CharacterMultiply;\
    \n:global CharacterReplace;\
    \n:global CleanFilePath;\
    \n:global CleanName;\
    \n:global DeviceInfo;\
    \n:global Dos2Unix;\
    \n:global DownloadPackage;\
    \n:global EitherOr;\
    \n:global EscapeForRegEx;\
    \n:global ExitError;\
    \n:global FetchHuge;\
    \n:global FetchUserAgentStr;\
    \n:global FormatLine;\
    \n:global FormatMultiLines;\
    \n:global GetMacVendor;\
    \n:global GetRandom20CharAlNum;\
    \n:global GetRandom20CharHex;\
    \n:global GetRandomNumber;\
    \n:global Grep;\
    \n:global HexToNum;\
    \n:global HumanReadableNum;\
    \n:global IfThenElse;\
    \n:global IsDefaultRouteReachable;\
    \n:global IsDNSResolving;\
    \n:global IsFullyConnected;\
    \n:global IsMacLocallyAdministered;\
    \n:global IsTimeSync;\
    \n:global LogPrint;\
    \n:global LogPrintOnce;\
    \n:global MAX;\
    \n:global MIN;\
    \n:global MkDir;\
    \n:global NotificationFunctions;\
    \n:global ParseDate;\
    \n:global ParseKeyValueStore;\
    \n:global PrettyPrint;\
    \n:global ProtocolStrip;\
    \n:global RandomDelay;\
    \n:global RequiredRouterOS;\
    \n:global ScriptFromTerminal;\
    \n:global ScriptInstallUpdate;\
    \n:global ScriptLock;\
    \n:global SendNotification;\
    \n:global SendNotification2;\
    \n:global SymbolByUnicodeName;\
    \n:global SymbolForNotification;\
    \n:global Unix2Dos;\
    \n:global UrlEncode;\
    \n:global ValidateSyntax;\
    \n:global VersionToNum;\
    \n:global WaitDefaultRouteReachable;\
    \n:global WaitDNSResolving;\
    \n:global WaitForFile;\
    \n:global WaitFullyConnected;\
    \n:global WaitTimeSync;\
    \n\
    \n# align string to the right\
    \n:set AlignRight do={\
    \n  :local Input [ :tostr \$1 ];\
    \n  :local Len   [ :tonum \$2 ];\
    \n\
    \n  :global CharacterMultiply;\
    \n  :global EitherOr;\
    \n\
    \n  :set Len [ \$EitherOr \$Len 8 ];\
    \n  :local Spaces [ \$CharacterMultiply \" \" \$Len ];\
    \n\
    \n  :return ([ :pick \$Spaces 0 (\$Len - [ :len \$Input ]) ] . \$Input);\
    \n}\
    \n\
    \n# check and download required certificate\
    \n:set CertificateAvailable do={\
    \n  :local CommonName [ :tostr \$1 ];\
    \n\
    \n  :global CertificateDownload;\
    \n  :global LogPrint;\
    \n  :global ParseKeyValueStore;\
    \n\
    \n  :if ([ /system/resource/get free-hdd-space ] < 8388608 && \\\
    \n       [ /certificate/settings/get crl-download ] = true && \\\
    \n       [ /certificate/settings/get crl-store ] = \"system\") do={\
    \n    \$LogPrint warning \$0 (\"This system has low free flash space but \
    \" . \\\
    \n      \"is configured to download certificate CRLs to system!\");\
    \n  }\
    \n\
    \n  :if ([ :len \$CommonName ] = 0) do={\
    \n    \$LogPrint warning \$0 (\"No CommonName given!\");\
    \n    :return false;\
    \n  }\
    \n\
    \n  :if ([ :len [ /certificate/find where common-name=\$CommonName ] ] = 0\
    ) do={\
    \n    \$LogPrint info \$0 (\"Certificate with CommonName '\" . \$CommonNam\
    e . \"' not available.\");\
    \n    :if ([ \$CertificateDownload \$CommonName ] = false) do={\
    \n      :return false;\
    \n    }\
    \n  }\
    \n\
    \n  :local CertVal [ /certificate/get [ find where common-name=\$CommonNam\
    e ] ];\
    \n  :while ((\$CertVal->\"akid\") != \"\" && (\$CertVal->\"akid\") != (\$C\
    ertVal->\"skid\")) do={\
    \n    :if ([ :len [ /certificate/find where skid=(\$CertVal->\"akid\") ] ]\
    \_= 0) do={\
    \n      \$LogPrint info \$0 (\"Certificate chain for '\" . \$CommonName . \
    \\\
    \n        \"' is incomplete, missing '\" . ([ \$ParseKeyValueStore (\$Cert\
    Val->\"issuer\") ]->\"CN\") . \"'.\");\
    \n      :if ([ \$CertificateDownload \$CommonName ] = false) do={\
    \n        :return false;\
    \n      }\
    \n    }\
    \n    :set CertVal [ /certificate/get [ find where skid=(\$CertVal->\"akid\
    \") ] ];\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# download and import certificate\
    \n:set CertificateDownload do={\
    \n  :local CommonName [ :tostr \$1 ];\
    \n\
    \n  :global ScriptUpdatesBaseUrl;\
    \n  :global ScriptUpdatesUrlSuffix;\
    \n\
    \n  :global CertificateAvailable;\
    \n  :global CertificateNameByCN;\
    \n  :global CleanName;\
    \n  :global FetchUserAgentStr;\
    \n  :global LogPrint;\
    \n  :global WaitForFile;\
    \n\
    \n  \$LogPrint info \$0 (\"Downloading and importing certificate with \" .\
    \_\\\
    \n      \"CommonName '\" . \$CommonName . \"'.\");\
    \n  :local FileName ([ \$CleanName \$CommonName ] . \".pem\");\
    \n  :do {\
    \n    /tool/fetch check-certificate=yes-without-crl http-header-field=({ [\
    \_\$FetchUserAgentStr \$0 ] }) \\\
    \n      (\$ScriptUpdatesBaseUrl . \"certs/\" . \$FileName . \$ScriptUpdate\
    sUrlSuffix) \\\
    \n      dst-path=\$FileName as-value;\
    \n    \$WaitForFile \$FileName;\
    \n  } on-error={\
    \n    \$LogPrint warning \$0 (\"Failed downloading certificate with Common\
    Name '\" . \$CommonName . \\\
    \n      \"' from repository! Trying fallback to mkcert.org...\");\
    \n    :do {\
    \n      :if ([ \$CertificateAvailable \"ISRG Root X1\" ] = false) do={\
    \n        \$LogPrint error \$0 (\"Downloading required certificate failed.\
    \");\
    \n        :return false;\
    \n      }\
    \n      /tool/fetch check-certificate=yes-without-crl http-header-field=({\
    \_[ \$FetchUserAgentStr \$0 ] }) \\\
    \n        \"https://mkcert.org/generate/\" http-data=[ :serialize to=json \
    ({ \$CommonName }) ] \\\
    \n        dst-path=\$FileName as-value;\
    \n      \$WaitForFile \$FileName;\
    \n      :if ([ /file/get \$FileName size ] = 0) do={\
    \n        /file/remove \$FileName;\
    \n        :error false;\
    \n      }\
    \n    } on-error={\
    \n      \$LogPrint warning \$0 (\"Failed downloading certificate with Comm\
    onName '\" . \$CommonName . \"'!\");\
    \n      :return false;\
    \n    }\
    \n  }\
    \n\
    \n  /certificate/import file-name=\$FileName passphrase=\"\" as-value;\
    \n  :delay 1s;\
    \n  /file/remove [ find where name=\$FileName ];\
    \n\
    \n  :if ([ :len [ /certificate/find where common-name=\$CommonName ] ] = 0\
    ) do={\
    \n    /certificate/remove [ find where name~(\"^\" . \$FileName . \"_[0-9]\
    +\\\$\") ];\
    \n    \$LogPrint warning \$0 (\"Certificate with CommonName '\" . \$Common\
    Name . \"' still unavailable!\");\
    \n    :return false;\
    \n  }\
    \n\
    \n  :foreach Cert in=[ /certificate/find where name~(\"^\" . \$FileName . \
    \"_[0-9]+\\\$\") ] do={\
    \n    \$CertificateNameByCN [ /certificate/get \$Cert common-name ];\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# name a certificate by its common-name\
    \n:set CertificateNameByCN do={\
    \n  :local CommonName [ :tostr \$1 ];\
    \n\
    \n  :global CleanName;\
    \n\
    \n  :local Cert [ /certificate/find where common-name=\$CommonName ];\
    \n  /certificate/set \$Cert name=[ \$CleanName \$CommonName ];\
    \n}\
    \n\
    \n# multiply given character(s)\
    \n:set CharacterMultiply do={\
    \n  :local Return \"\";\
    \n  :for I from=1 to=\$2 do={\
    \n    :set Return (\$Return . \$1);\
    \n  }\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# character replace\
    \n:set CharacterReplace do={\
    \n  :local String [ :tostr \$1 ];\
    \n  :local ReplaceFrom [ :tostr \$2 ];\
    \n  :local ReplaceWith [ :tostr \$3 ];\
    \n  :local Return \"\";\
    \n\
    \n  :if (\$ReplaceFrom = \"\") do={\
    \n    :return \$String;\
    \n  }\
    \n\
    \n  :while ([ :typeof [ :find \$String \$ReplaceFrom ] ] != \"nil\") do={\
    \n    :local Pos [ :find \$String \$ReplaceFrom ];\
    \n    :set Return (\$Return . [ :pick \$String 0 \$Pos ] . \$ReplaceWith);\
    \n    :set String [ :pick \$String (\$Pos + [ :len \$ReplaceFrom ]) [ :len\
    \_\$String ] ];\
    \n  }\
    \n\
    \n  :return (\$Return . \$String);\
    \n}\
    \n\
    \n# clean file path\
    \n:set CleanFilePath do={\
    \n  :local Path [ :tostr \$1 ];\
    \n\
    \n  :global CharacterReplace;\
    \n\
    \n  :while (\$Path ~ \"//\") do={\
    \n    :set \$Path [ \$CharacterReplace \$Path \"//\" \"/\" ];\
    \n  }\
    \n  :if ([ :pick \$Path 0 ] = \"/\") do={\
    \n    :set Path [ :pick \$Path 1 [ :len \$Path ] ];\
    \n  }\
    \n  :if ([ :pick \$Path ([ :len \$Path ] - 1) ] = \"/\") do={\
    \n    :set Path [ :pick \$Path 0 ([ :len \$Path ] - 1) ];\
    \n  }\
    \n\
    \n  :return \$Path;\
    \n}\
    \n\
    \n# clean name for DNS, file and more\
    \n:set CleanName do={\
    \n  :local Input [ :tostr \$1 ];\
    \n\
    \n  :local Return \"\";\
    \n\
    \n  :for I from=0 to=([ :len \$Input ] - 1) do={\
    \n    :local Char [ :pick \$Input \$I ];\
    \n    :if ([ :typeof [ find \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr\
    stuvwxyz0123456789\" \$Char ] ] = \"nil\") do={\
    \n      :do {\
    \n        :if ([ :len \$Return ] = 0) do={\
    \n          :error true;\
    \n        }\
    \n        :if ([ :pick \$Return ([ :len \$Return ] - 1) ] = \"-\") do={\
    \n          :error true;\
    \n        }\
    \n        :set Char \"-\";\
    \n      } on-error={\
    \n        :set Char \"\";\
    \n      }\
    \n    }\
    \n    :set Return (\$Return . \$Char);\
    \n  }\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# get readable device info\
    \n:set DeviceInfo do={\
    \n  :global ExpectedConfigVersion;\
    \n  :global Identity;\
    \n\
    \n  :global IfThenElse;\
    \n  :global FormatLine;\
    \n\
    \n  :local License [ /system/license/get ];\
    \n  :local Resource [ /system/resource/get ];\
    \n  :local RouterBoard;\
    \n  :do {\
    \n    :set RouterBoard [[ :parse \"/system/routerboard/get\" ]];\
    \n  } on-error={ }\
    \n  :local Snmp [ /snmp/get ];\
    \n  :local Update [ /system/package/update/get ];\
    \n\
    \n  :return ( \\\
    \n    [ \$FormatLine \"Hostname\" \$Identity ] . \"\\n\" . \\\
    \n    [ \$IfThenElse ([ :len (\$Snmp->\"location\") ] > 0) \\\
    \n      ([ \$FormatLine \"Location\" (\$Snmp->\"location\") ] . \"\\n\") ]\
    \_. \\\
    \n    [ \$IfThenElse ([ :len (\$Snmp->\"contact\") ] > 0) \\\
    \n      ([ \$FormatLine \"Contact\" (\$Snmp->\"contact\") ] . \"\\n\") ] .\
    \_\\\
    \n    [ \$FormatLine \"Board name\" (\$Resource->\"board-name\") ] . \"\\n\
    \" . \\\
    \n    [ \$FormatLine \"Architecture\" (\$Resource->\"architecture-name\") \
    ] . \"\\n\" . \\\
    \n    [ \$IfThenElse (\$RouterBoard->\"routerboard\" = true) \\\
    \n      ([ \$FormatLine \"Model\" (\$RouterBoard->\"model\") ] . \\\
    \n       [ \$IfThenElse ([ :len (\$RouterBoard->\"revision\") ] > 0) \\\
    \n           (\" \" . \$RouterBoard->\"revision\") ] . \"\\n\" . \\\
    \n       [ \$FormatLine \"Serial number\" (\$RouterBoard->\"serial-number\
    \") ] . \"\\n\") ] . \\\
    \n    [ \$IfThenElse ([ :len (\$License->\"level\") ] > 0) \\\
    \n      ([ \$FormatLine \"License\" (\$License->\"level\") ] . \"\\n\") ] \
    . \\\
    \n    \"RouterOS:\\n\" . \\\
    \n    [ \$FormatLine \"    Channel\" (\$Update->\"channel\") ] . \"\\n\" .\
    \_\\\
    \n    [ \$FormatLine \"    Installed\" (\$Update->\"installed-version\") ]\
    \_. \"\\n\" . \\\
    \n    [ \$IfThenElse ([ :typeof (\$Update->\"latest-version\") ] != \"noth\
    ing\" && \\\
    \n        \$Update->\"installed-version\" != \$Update->\"latest-version\")\
    \_\\\
    \n      ([ \$FormatLine \"    Available\" (\$Update->\"latest-version\") ]\
    \_. \"\\n\") ] . \\\
    \n    [ \$IfThenElse (\$RouterBoard->\"routerboard\" = true && \\\
    \n        \$RouterBoard->\"current-firmware\" != \$RouterBoard->\"upgrade-\
    firmware\") \\\
    \n      ([ \$FormatLine \"    Firmware\" (\$RouterBoard->\"current-firmwar\
    e\") ] . \"\\n\") ] . \\\
    \n    \"RouterOS-Scripts:\\n\" . \\\
    \n    [ \$FormatLine \"    Version\" \$ExpectedConfigVersion ]);\
    \n}\
    \n\
    \n# convert line endings, DOS -> UNIX\
    \n:set Dos2Unix do={\
    \n  :return [ :tolf [ :tostr \$1 ] ];\
    \n}\
    \n\
    \n# download package from upgrade server\
    \n:set DownloadPackage do={\
    \n  :local PkgName [ :tostr \$1 ];\
    \n  :local PkgVer  [ :tostr \$2 ];\
    \n  :local PkgArch [ :tostr \$3 ];\
    \n  :local PkgDir  [ :tostr \$4 ];\
    \n\
    \n  :global CertificateAvailable;\
    \n  :global CleanFilePath;\
    \n  :global LogPrint;\
    \n  :global MkDir;\
    \n  :global WaitForFile;\
    \n\
    \n  :if ([ :len \$PkgName ] = 0) do={ :return false; }\
    \n  :if ([ :len \$PkgVer  ] = 0) do={ :set PkgVer  [ /system/package/updat\
    e/get installed-version ]; }\
    \n  :if ([ :len \$PkgArch ] = 0) do={ :set PkgArch [ /system/resource/get \
    architecture-name ]; }\
    \n\
    \n  :if (\$PkgName = \"system\") do={ :set PkgName \"routeros\"; }\
    \n\
    \n  :local PkgFile (\$PkgName . \"-\" . \$PkgVer . \"-\" . \$PkgArch . \".\
    npk\");\
    \n  :if (\$PkgArch = \"x86_64\") do={ :set PkgFile (\$PkgName . \"-\" . \$\
    PkgVer . \".npk\"); }\
    \n  :local PkgDest [ \$CleanFilePath (\$PkgDir . \"/\" . \$PkgFile) ];\
    \n\
    \n  :if ([ \$MkDir \$PkgDir ] = false) do={\
    \n    \$LogPrint warning \$0 (\"Failed creating directory, not downloading\
    \_package.\");\
    \n    :return false;\
    \n  }\
    \n\
    \n  :if ([ :len [ /file/find where name=\$PkgDest type=\"package\" ] ] > 0\
    ) do={\
    \n    \$LogPrint info \$0 (\"Package file \" . \$PkgName . \" already exis\
    ts.\");\
    \n    :return true;\
    \n  }\
    \n\
    \n  :if ([ \$CertificateAvailable \"ISRG Root X1\" ] = false) do={\
    \n    \$LogPrint error \$0 (\"Downloading required certificate failed.\");\
    \n    :return false;\
    \n  }\
    \n\
    \n  :local Url (\"https://upgrade.mikrotik.com/routeros/\" . \$PkgVer . \"\
    /\" . \$PkgFile);\
    \n  \$LogPrint info \$0 (\"Downloading package file '\" . \$PkgName . \"'.\
    ..\");\
    \n  \$LogPrint debug \$0 (\"... from url: \" . \$Url);\
    \n  :local Retry 3;\
    \n  :while (\$Retry > 0) do={\
    \n    :do {\
    \n      /tool/fetch check-certificate=yes-without-crl \$Url dst-path=\$Pkg\
    Dest;\
    \n      \$WaitForFile \$PkgDest;\
    \n\
    \n      :if ([ /file/get [ find where name=\$PkgDest ] type ] = \"package\
    \") do={\
    \n        :return true;\
    \n      }\
    \n    } on-error={\
    \n      \$LogPrint debug \$0 (\"Downloading package file failed.\");\
    \n    }\
    \n\
    \n    /file/remove [ find where name=\$PkgDest ];\
    \n    :set Retry (\$Retry - 1);\
    \n  }\
    \n\
    \n  \$LogPrint warning \$0 (\"Downloading package file '\" . \$PkgName . \
    \"' failed.\");\
    \n  :return false;\
    \n}\
    \n\
    \n# return either first (if \"true\") or second\
    \n:set EitherOr do={\
    \n  :global IfThenElse;\
    \n\
    \n  :if ([ :typeof \$1 ] = \"num\") do={\
    \n    :return [ \$IfThenElse (\$1 != 0) \$1 \$2 ];\
    \n  }\
    \n  :if ([ :typeof \$1 ] = \"time\") do={\
    \n    :return [ \$IfThenElse (\$1 > 0s) \$1 \$2 ];\
    \n  }\
    \n  # this works for boolean values, literal ones with parentheses\
    \n  :return [ \$IfThenElse ([ :len [ :tostr \$1 ] ] > 0) \$1 \$2 ];\
    \n}\
    \n\
    \n# escape for regular expression\
    \n:set EscapeForRegEx do={\
    \n  :local Input [ :tostr \$1 ];\
    \n\
    \n  :if ([ :len \$Input ] = 0) do={\
    \n    :return \"\";\
    \n  }\
    \n\
    \n  :local Return \"\";\
    \n  :local Chars (\"^.[]\\\$()|*+\?{}\\\\\");\
    \n\
    \n  :for I from=0 to=([ :len \$Input ] - 1) do={\
    \n    :local Char [ :pick \$Input \$I ];\
    \n    :if ([ :find \$Chars \$Char ]) do={\
    \n      :set Char (\"\\\\\" . \$Char);\
    \n    }\
    \n    :set Return (\$Return . \$Char);\
    \n  }\
    \n\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# simple macro to print error message on unintentional error\
    \n:set ExitError do={\
    \n  :local ExitOK [ :tostr \$1 ];\
    \n  :local Name   [ :tostr \$2 ];\
    \n\
    \n  :global IfThenElse;\
    \n  :global LogPrint; \
    \n\
    \n  :if (\$ExitOK = \"false\") do={\
    \n    \$LogPrint error \$Name ([ \$IfThenElse ([ :pick \$Name 0 1 ] = \"\\\
    \$\") \\\
    \n        \"Function\" \"Script\" ] . \" '\" . \$Name . \"' exited with er\
    ror.\");\
    \n  }\
    \n}\
    \n\
    \n# fetch huge data to file, read in chunks\
    \n:set FetchHuge do={\
    \n  :local ScriptName [ :tostr \$1 ];\
    \n  :local Url        [ :tostr \$2 ];\
    \n  :local CheckCert  [ :tostr \$3 ];\
    \n\
    \n  :global CleanName;\
    \n  :global FetchUserAgentStr;\
    \n  :global GetRandom20CharAlNum;\
    \n  :global IfThenElse;\
    \n  :global LogPrint;\
    \n  :global MkDir;\
    \n  :global WaitForFile;\
    \n\
    \n  :set CheckCert [ \$IfThenElse (\$CheckCert = \"false\") \"no\" \"yes-w\
    ithout-crl\" ];\
    \n\
    \n  :local DirName (\"tmpfs/\" . [ \$CleanName \$ScriptName ]);\
    \n  :if ([ \$MkDir \$DirName ] = false) do={\
    \n    \$LogPrint error \$0 (\"Failed creating directory!\");\
    \n    :return false;\
    \n  }\
    \n\
    \n  :local FileName (\$DirName . \"/\" . [ \$CleanName \$0 ] . \"-\" . [ \
    \$GetRandom20CharAlNum ]);\
    \n  :do {\
    \n    /tool/fetch check-certificate=\$CheckCert \$Url dst-path=\$FileName \
    \\\
    \n      http-header-field=({ [ \$FetchUserAgentStr \$ScriptName ] }) as-va\
    lue;\
    \n  } on-error={\
    \n    :if ([ \$WaitForFile \$FileName 500ms ] = true) do={\
    \n      /file/remove \$FileName;\
    \n    }\
    \n    \$LogPrint debug \$0 (\"Failed downloading from: \" . \$Url);\
    \n    /file/remove \$DirName;\
    \n    :return false;\
    \n  }\
    \n  \$WaitForFile \$FileName;\
    \n\
    \n  :local FileSize [ /file/get \$FileName size ];\
    \n  :local Return \"\";\
    \n  :local VarSize 0;\
    \n  :while (\$VarSize != \$FileSize) do={\
    \n    :set Return (\$Return . ([ /file/read offset=\$VarSize chunk-size=32\
    768 file=\$FileName as-value ]->\"data\"));\
    \n    :set FileSize [ /file/get \$FileName size ];\
    \n    :set VarSize [ :len \$Return ];\
    \n    :if (\$VarSize > \$FileSize) do={\
    \n      :delay 100ms;\
    \n    }\
    \n  }\
    \n  /file/remove \$DirName;\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# generate user agent string for fetch\
    \n:set FetchUserAgentStr do={\
    \n  :local Caller [ :tostr \$1 ];\
    \n\
    \n  :local Resource [ /system/resource/get ];\
    \n\
    \n  :return (\"User-Agent: Mikrotik/\" . \$Resource->\"version\" . \" \" .\
    \_\\\
    \n    \$Resource->\"architecture-name\" . \" \" . \$Caller . \"/Fetch (htt\
    ps://rsc.eworm.de/)\");\
    \n}\
    \n\
    \n# format a line for output\
    \n:set FormatLine do={\
    \n  :local Key    [ :tostr \$1 ];\
    \n  :local Value  [ :tostr \$2 ];\
    \n  :local Indent [ :tonum \$3 ];\
    \n  :local Spaces;\
    \n  :local Return \"\";\
    \n\
    \n  :global CharacterMultiply;\
    \n  :global EitherOr;\
    \n\
    \n  :set Indent [ \$EitherOr \$Indent 16 ];\
    \n  :local Spaces [ \$CharacterMultiply \" \" \$Indent ];\
    \n\
    \n  :if ([ :len \$Key ] > 0) do={ :set Return (\$Key . \":\"); }\
    \n  :if ([ :len \$Key ] > (\$Indent - 2)) do={\
    \n    :set Return (\$Return . \"\\n\" . [ :pick \$Spaces 0 \$Indent ] . \$\
    Value);\
    \n  } else={\
    \n    :set Return (\$Return . [ :pick \$Spaces 0 (\$Indent - [ :len \$Retu\
    rn ]) ] . \$Value);\
    \n  }\
    \n\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# format multiple lines for output\
    \n:set FormatMultiLines do={\
    \n  :local Key    [ :tostr   \$1 ];\
    \n  :local Values [ :toarray \$2 ];\
    \n  :local Indent [ :tonum   \$3 ];\
    \n  :local Return;\
    \n\
    \n  :global FormatLine;\
    \n\
    \n  :set Return [ \$FormatLine \$Key (\$Values->0) \$Indent ];\
    \n  :foreach Value in=[ :pick \$Values 1 [ :len \$Values ] ] do={\
    \n    :set Return (\$Return . \"\\n\" . [ \$FormatLine \"\" \$Value \$Inde\
    nt ]);\
    \n  }\
    \n\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# get MAC vendor\
    \n:set GetMacVendor do={\
    \n  :local Mac [ :tostr \$1 ];\
    \n\
    \n  :global CertificateAvailable;\
    \n  :global IsMacLocallyAdministered;\
    \n  :global LogPrint;\
    \n\
    \n  :if ([ \$IsMacLocallyAdministered \$Mac ] = true) do={\
    \n    :return \"locally administered\";\
    \n  }\
    \n\
    \n  :do {\
    \n    :if ([ \$CertificateAvailable \"GTS Root R4\" ] = false) do={\
    \n      \$LogPrint warning \$0 (\"Downloading required certificate failed.\
    \");\
    \n      :error false;\
    \n    }\
    \n    :local Vendor ([ /tool/fetch check-certificate=yes-without-crl \\\
    \n        (\"https://api.macvendors.com/\" . [ :pick \$Mac 0 8 ]) output=u\
    ser as-value ]->\"data\");\
    \n    :return \$Vendor;\
    \n  } on-error={\
    \n    :do {\
    \n      /tool/fetch check-certificate=yes-without-crl (\"https://api.macve\
    ndors.com/\") \\\
    \n        output=none as-value;\
    \n      \$LogPrint debug \$0 (\"The mac vendor is not known in database.\"\
    );\
    \n    } on-error={\
    \n      \$LogPrint warning \$0 (\"Failed getting mac vendor.\");\
    \n    }\
    \n    :return \"unknown vendor\";\
    \n  }\
    \n}\
    \n\
    \n# generate random 20 chars alphabetical (A-Z & a-z) and numerical (0-9)\
    \n:set GetRandom20CharAlNum do={\
    \n  :global EitherOr;\
    \n\
    \n  :return [ :rndstr length=[ \$EitherOr [ :tonum \$1 ] 20 ] from=\"ABCDE\
    FGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\" ];\
    \n}\
    \n\
    \n# generate random 20 chars hex (0-9 and a-f)\
    \n:set GetRandom20CharHex do={\
    \n  :global EitherOr;\
    \n\
    \n  :return [ :rndstr length=[ \$EitherOr [ :tonum \$1 ] 20 ] from=\"01234\
    56789abcdef\" ];\
    \n}\
    \n\
    \n# generate random number\
    \n:set GetRandomNumber do={\
    \n  :global EitherOr;\
    \n\
    \n  :return [ :rndnum from=0 to=[ \$EitherOr [ :tonum \$1 ] 4294967295 ] ]\
    ;\
    \n}\
    \n\
    \n# return first line that matches a pattern\
    \n:set Grep do={\
    \n  :local Input  ([ :tostr \$1 ] . \"\\n\");\
    \n  :local Pattern [ :tostr \$2 ];\
    \n\
    \n  :if ([ :typeof [ :find \$Input \$Pattern ] ] = \"nil\") do={\
    \n    :return [];\
    \n  }\
    \n\
    \n  :do {\
    \n    :local Line [ :pick \$Input 0 [ :find \$Input \"\\n\" ] ];\
    \n    :if ([ :typeof [ :find \$Line \$Pattern ] ] = \"num\") do={\
    \n      :return \$Line;\
    \n    }\
    \n    :set Input [ :pick \$Input ([ :find \$Input \"\\n\" ] + 1) [ :len \$\
    Input ] ];\
    \n  } while=([ :len \$Input ] > 0);\
    \n\
    \n  :return [];\
    \n}\
    \n\
    \n# convert from hex (string) to num\
    \n:set HexToNum do={\
    \n  :local Input [ :tostr \$1 ];\
    \n\
    \n  :global HexToNum;\
    \n\
    \n  :if ([ :pick \$Input 0 ] = \"*\") do={\
    \n    :return [ \$HexToNum [ :pick  \$Input 1 [ :len \$Input ] ] ];\
    \n  }\
    \n\
    \n  :return [ :tonum (\"0x\" . \$Input) ];\
    \n}\
    \n\
    \n# return human readable number\
    \n:set HumanReadableNum do={\
    \n  :local Input [ :tonum \$1 ];\
    \n  :local Base  [ :tonum \$2 ];\
    \n\
    \n  :global EitherOr;\
    \n  :global IfThenElse;\
    \n\
    \n  :local Prefix \"kMGTPE\";\
    \n  :local Pow 1;\
    \n\
    \n  :set Base [ \$EitherOr \$Base 1024 ];\
    \n  :local Bin [ \$IfThenElse (\$Base = 1024) \"i\" \"\" ];\
    \n\
    \n  :if (\$Input < \$Base) do={\
    \n    :return \$Input;\
    \n  }\
    \n\
    \n  :for I from=0 to=[ :len \$Prefix ] do={\
    \n    :set Pow (\$Pow * \$Base);\
    \n    :if (\$Input / \$Base < \$Pow) do={\
    \n      :set Prefix [ :pick \$Prefix \$I ];\
    \n      :local Tmp1 (\$Input * 100 / \$Pow);\
    \n      :local Tmp2 (\$Tmp1 / 100);\
    \n      :if (\$Tmp2 >= 100) do={\
    \n        :return (\$Tmp2 . \$Prefix . \$Bin);\
    \n      }\
    \n      :return (\$Tmp2 . \".\" . \\\
    \n          [ :pick \$Tmp1 [ :len \$Tmp2 ] ([ :len \$Tmp1 ] - [ :len \$Tmp\
    2 ] + 1) ] . \\\
    \n          \$Prefix . \$Bin);\
    \n    }\
    \n  }\
    \n}\
    \n\
    \n# mimic conditional/ternary operator (condition \? consequent : alternat\
    ive)\
    \n:set IfThenElse do={\
    \n  :if ([ :tostr \$1 ] = \"true\" || [ :tobool \$1 ] = true) do={\
    \n    :return \$2;\
    \n  }\
    \n  :return \$3;\
    \n}\
    \n\
    \n# check if default route is reachable\
    \n:set IsDefaultRouteReachable do={\
    \n  :if ([ :len [ /ip/route/find where dst-address=0.0.0.0/0 active routin\
    g-table=main ] ] > 0) do={\
    \n    :return true;\
    \n  }\
    \n  :return false;\
    \n}\
    \n\
    \n# check if DNS is resolving\
    \n:set IsDNSResolving do={\
    \n  :do {\
    \n    :resolve \"low-ttl.eworm.de\";\
    \n  } on-error={\
    \n    :return false;\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# check if system is is fully connected (default route reachable, DNS re\
    solving, time sync)\
    \n:set IsFullyConnected do={\
    \n  :global IsDefaultRouteReachable;\
    \n  :global IsDNSResolving;\
    \n  :global IsTimeSync;\
    \n\
    \n  :if ([ \$IsDefaultRouteReachable ] = false) do={\
    \n    :return false;\
    \n  }\
    \n  :if ([ \$IsDNSResolving ] = false) do={\
    \n    :return false;\
    \n  }\
    \n  :if ([ \$IsTimeSync ] = false) do={\
    \n    :return false;\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# check if mac address is locally administered\
    \n:set IsMacLocallyAdministered do={\
    \n  :if ([ :tonum (\"0x\" . [ :pick \$1 0 [ :find \$1 \":\" ] ]) ] & 2 = 2\
    ) do={\
    \n    :return true;\
    \n  }\
    \n  :return false;\
    \n}\
    \n\
    \n# check if system time is sync\
    \n:set IsTimeSync do={\
    \n  :global IsTimeSyncCached;\
    \n  :global IsTimeSyncResetNtp;\
    \n\
    \n  :global LogPrintOnce;\
    \n\
    \n  :if (\$IsTimeSyncCached = true) do={\
    \n    :return true;\
    \n  }\
    \n\
    \n  :if ([ /system/ntp/client/get enabled ] = true) do={\
    \n    :if ([ /system/ntp/client/get status ] = \"synchronized\") do={\
    \n      :set IsTimeSyncCached true;\
    \n      :return true;\
    \n    }\
    \n\
    \n    :local Uptime [ /system/resource/get uptime ];\
    \n    :if ([ :typeof \$IsTimeSyncResetNtp ] = \"nothing\") do={\
    \n      :set IsTimeSyncResetNtp \$Uptime;\
    \n    }\
    \n    :if (\$Uptime - \$IsTimeSyncResetNtp < 3m) do={\
    \n      :return false;\
    \n    }\
    \n\
    \n    \$LogPrintOnce warning \$0 (\"The ntp client is configured, but did \
    not sync.\");\
    \n    :set IsTimeSyncResetNtp \$Uptime;\
    \n    /system/ntp/client/set enabled=no;\
    \n    :delay 20ms;\
    \n    /system/ntp/client/set enabled=yes;\
    \n    :return false;\
    \n  }\
    \n\
    \n  :if ([ /system/license/get ]->\"level\" = \"free\" || \\\
    \n       [ /system/resource/get ]->\"board-name\" = \"x86\") do={\
    \n    \$LogPrintOnce debug \$0 (\"No ntp client configured, relying on RTC\
    \_for CHR free license and x86.\");\
    \n    :return true;\
    \n  }\
    \n\
    \n  :if ([ /ip/cloud/get update-time ] = true) do={\
    \n    :if ([ :typeof [ /ip/cloud/get public-address ] ] = \"ip\") do={\
    \n      :set IsTimeSyncCached true;\
    \n      :return true;\
    \n    }\
    \n    :return false;\
    \n  }\
    \n\
    \n  \$LogPrintOnce debug \$0 (\"No time source configured! Returning grace\
    fully...\");\
    \n  :return true;\
    \n}\
    \n\
    \n# log and print with same text\
    \n:set LogPrint do={\
    \n  :local Severity [ :tostr \$1 ];\
    \n  :local Name     [ :tostr \$2 ];\
    \n  :local Message  [ :tostr \$3 ];\
    \n\
    \n  :global PrintDebug;\
    \n  :global PrintDebugOverride;\
    \n\
    \n  :global EitherOr;\
    \n\
    \n  :local Debug [ \$EitherOr (\$PrintDebugOverride->\$Name) \$PrintDebug \
    ];\
    \n\
    \n  :local PrintSeverity do={\
    \n    :global TerminalColorOutput;\
    \n\
    \n    :if (\$TerminalColorOutput != true) do={\
    \n      :return \$1;\
    \n    }\
    \n\
    \n    :local Color { debug=96; info=97; warning=93; error=91 };\
    \n    :return (\"\\1B[\" . \$Color->\$1 . \"m\" . \$1 . \"\\1B[0m\");\
    \n  }\
    \n\
    \n  :local Log ([ \$EitherOr \$Name \"<unknown>\" ] . \": \" . \$Message);\
    \n  :if (\$Severity ~ (\"^(debug|error|info)\\\$\")) do={\
    \n    :if (\$Severity = \"debug\") do={ :log debug \$Log; }\
    \n    :if (\$Severity = \"error\") do={ :log error \$Log; }\
    \n    :if (\$Severity = \"info\" ) do={ :log info  \$Log; }\
    \n  } else={\
    \n    :log warning \$Log;\
    \n    :set Severity \"warning\";\
    \n  }\
    \n\
    \n  :if (\$Severity != \"debug\" || \$Debug = true) do={\
    \n    :put ([ \$PrintSeverity \$Severity ] . \": \" . \$Message);\
    \n  }\
    \n}\
    \n\
    \n# log and print, once until reboot\
    \n:set LogPrintOnce do={\
    \n  :local Severity [ :tostr \$1 ];\
    \n  :local Name     [ :tostr \$2 ];\
    \n  :local Message  [ :tostr \$3 ];\
    \n\
    \n  :global LogPrint;\
    \n\
    \n  :global LogPrintOnceMessages;\
    \n\
    \n  :if ([ :typeof \$LogPrintOnceMessages ] = \"nothing\") do={\
    \n    :set LogPrintOnceMessages ({});\
    \n  }\
    \n\
    \n  :if (\$LogPrintOnceMessages->\$Message = 1) do={\
    \n    :return false;\
    \n  }\
    \n\
    \n  :if ([ :len [ /log/find where message=(\$Name . \": \" . \$Message) ] \
    ] > 0) do={\
    \n    \$LogPrint warning \$0 \\\
    \n      (\"The message is already in log, scripting subsystem may have cra\
    shed before!\");\
    \n  }\
    \n\
    \n  :set (\$LogPrintOnceMessages->\$Message) 1;\
    \n  \$LogPrint \$Severity \$Name \$Message;\
    \n  :return true;\
    \n}\
    \n\
    \n# get max value\
    \n:set MAX do={\
    \n  :if (\$1 > \$2) do={ :return \$1; }\
    \n  :return \$2;\
    \n}\
    \n\
    \n# get min value\
    \n:set MIN do={\
    \n  :if (\$1 < \$2) do={ :return \$1; }\
    \n  :return \$2;\
    \n}\
    \n\
    \n# create directory\
    \n:set MkDir do={\
    \n  :local Path [ :tostr \$1 ];\
    \n\
    \n  :global CleanFilePath;\
    \n  :global LogPrint;\
    \n  :global WaitForFile;\
    \n\
    \n  :local MkTmpfs do={\
    \n    :global LogPrint;\
    \n    :global WaitForFile;\
    \n\
    \n    :local TmpFs [ /disk/find where slot=tmpfs type=tmpfs ];\
    \n    :if ([ :len \$TmpFs ] = 1) do={\
    \n      :if ([ /disk/get \$TmpFs disabled ] = true) do={\
    \n        \$LogPrint info \$0 (\"The tmpfs is disabled, enabling.\");\
    \n        /disk/enable \$TmpFs;\
    \n      }\
    \n      :return true;\
    \n    }\
    \n\
    \n    \$LogPrint info \$0 (\"Creating disk of type tmpfs.\");\
    \n    /file/remove [ find where name=\"tmpfs\" type=\"directory\" ];\
    \n    :do {\
    \n      /disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource\
    /get total-memory ] / 3);\
    \n      \$WaitForFile \"tmpfs\";\
    \n    } on-error={\
    \n      \$LogPrint warning \$0 (\"Creating disk of type tmpfs failed!\");\
    \n      :return false;\
    \n    }\
    \n    :return true;\
    \n  }\
    \n\
    \n  :set Path [ \$CleanFilePath \$Path ];\
    \n\
    \n  :if (\$Path = \"\") do={\
    \n    :return true;\
    \n  }\
    \n\
    \n  :if ([ :len [ /file/find where name=\$Path type=\"directory\" ] ] = 1)\
    \_do={\
    \n    :return true;\
    \n  }\
    \n\
    \n  :if ([ :pick \$Path 0 5 ] = \"tmpfs\") do={\
    \n    :if ([ \$MkTmpfs ] = false) do={\
    \n      :return false;\
    \n    }\
    \n  }\
    \n\
    \n  :do {\
    \n    :local File (\$Path . \"/file\");\
    \n    /file/add name=\$File;\
    \n    \$WaitForFile \$File;\
    \n    /file/remove \$File;\
    \n  } on-error={\
    \n    \$LogPrint warning \$0 (\"Making directory '\" . \$Path . \"' failed\
    !\");\
    \n    :return false;\
    \n  }\
    \n\
    \n  :return true;\
    \n}\
    \n\
    \n# prepare NotificationFunctions array\
    \n:if ([ :typeof \$NotificationFunctions ] != \"array\") do={\
    \n  :set NotificationFunctions ({});\
    \n}\
    \n\
    \n# parse the date and return a named array\
    \n:set ParseDate do={\
    \n  :local Date [ :tostr \$1 ];\
    \n\
    \n  :return ({ \"year\"=[ :tonum [ :pick \$Date 0 4 ] ];\
    \n            \"month\"=[ :tonum [ :pick \$Date 5 7 ] ];\
    \n              \"day\"=[ :tonum [ :pick \$Date 8 10 ] ] });\
    \n}\
    \n\
    \n# parse key value store\
    \n:set ParseKeyValueStore do={\
    \n  :local Source \$1;\
    \n\
    \n  :if ([ :pick \$Source 0 1 ] = \"{\") do={\
    \n    :do {\
    \n      :return [ :deserialize from=json \$Source ];\
    \n    } on-error={ }\
    \n  }\
    \n\
    \n  :if ([ :typeof \$Source ] != \"array\") do={\
    \n    :set Source [ :tostr \$1 ];\
    \n  }\
    \n  :local Result ({});\
    \n  :foreach KeyValue in=[ :toarray \$Source ] do={\
    \n    :if ([ :find \$KeyValue \"=\" ]) do={\
    \n      :local Key [ :pick \$KeyValue 0 [ :find \$KeyValue \"=\" ] ];\
    \n      :local Value [ :pick \$KeyValue ([ :find \$KeyValue \"=\" ] + 1) [\
    \_:len \$KeyValue ] ];\
    \n      :if (\$Value=\"true\") do={ :set Value true; }\
    \n      :if (\$Value=\"false\") do={ :set Value false; }\
    \n      :set (\$Result->\$Key) \$Value;\
    \n    } else={\
    \n      :set (\$Result->\$KeyValue) true;\
    \n    }\
    \n  }\
    \n  :return \$Result;\
    \n}\
    \n\
    \n# print lines with trailing carriage return\
    \n:set PrettyPrint do={\
    \n  :put [ :tocrlf [ :tostr \$1 ] ];\
    \n}\
    \n\
    \n# strip protocol from from url string\
    \n:set ProtocolStrip do={\
    \n  :local Input [ :tostr \$1 ];\
    \n\
    \n  :local Pos [ :find \$Input \"://\" ];\
    \n  :if ([ :typeof \$Pos ] = \"nil\") do={\
    \n    :return \$Input;\
    \n  }\
    \n  :return [ :pick \$Input (\$Pos + 3) [ :len \$Input ] ];\
    \n}\
    \n\
    \n# delay a random amount of seconds\
    \n:set RandomDelay do={\
    \n  :local Time [ :tonum \$1 ];\
    \n  :local Unit [ :tostr \$2 ];\
    \n\
    \n  :global EitherOr;\
    \n  :global GetRandomNumber;\
    \n  :global MAX;\
    \n\
    \n  :if (\$Time = 0) do={\
    \n    :return false;\
    \n  }\
    \n\
    \n  :delay ([ \$MAX 10 [ \$GetRandomNumber ([ :tonsec [ :totime (\$Time . \
    [ \$EitherOr \$Unit \"s\" ]) ] ] / 1000000) ] ] . \"ms\");\
    \n}\
    \n\
    \n# check for required RouterOS version\
    \n:set RequiredRouterOS do={\
    \n  :local Caller   [ :tostr \$1 ];\
    \n  :local Required [ :tostr \$2 ];\
    \n  :local Warn     [ :tostr \$3 ];\
    \n\
    \n  :global IfThenElse;\
    \n  :global LogPrint;\
    \n  :global VersionToNum;\
    \n\
    \n  :if (!(\$Required ~ \"^\\\\d+\\\\.\\\\d+((alpha|beta|rc|\\\\.)\\\\d+|)\
    \\\$\")) do={\
    \n    \$LogPrint error \$0 (\"No valid RouterOS version: \" . \$Required);\
    \n    :return false;\
    \n  }\
    \n\
    \n  :if ([ \$VersionToNum \$Required ] > [ \$VersionToNum [ /system/packag\
    e/update/get installed-version ] ]) do={\
    \n    :if (\$Warn = \"true\") do={\
    \n      \$LogPrint warning \$0 (\"This \" . [ \$IfThenElse ([ :pick \$Call\
    er 0 ] = (\"\\\$\")) \"function\" \"script\" ] . \\\
    \n        \" '\" . \$Caller . \"' (at least specific functionality) requir\
    es RouterOS \" . \$Required . \". Please update!\");\
    \n    }\
    \n    :return false;\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# check if script is run from terminal\
    \n:set ScriptFromTerminal do={\
    \n  :local Script [ :tostr \$1 ];\
    \n\
    \n  :global LogPrint;\
    \n  :global ScriptLock;\
    \n\
    \n  :if ([ \$ScriptLock \$Script ] = false) do={\
    \n    :return false;\
    \n  }\
    \n\
    \n  :foreach Job in=[ /system/script/job/find where script=\$Script ] do={\
    \n    :set Job [ /system/script/job/get \$Job ];\
    \n    :while ([ :typeof (\$Job->\"parent\") ] = \"id\") do={\
    \n      :set Job [ /system/script/job/get [ find where .id=(\$Job->\"paren\
    t\") ] ];\
    \n    }\
    \n    :if ((\$Job->\"type\") = \"login\") do={\
    \n      \$LogPrint debug \$0 (\"Script \" . \$Script . \" started from ter\
    minal.\");\
    \n      :return true;\
    \n    }\
    \n  }\
    \n\
    \n  \$LogPrint debug \$0 (\"Script \" . \$Script . \" NOT started from ter\
    minal.\");\
    \n  :return false;\
    \n}\
    \n\
    \n# install new scripts, update existing scripts\
    \n:set ScriptInstallUpdate do={ :do {\
    \n  :local Scripts    [ :toarray \$1 ];\
    \n  :local NewComment [ :tostr   \$2 ];\
    \n\
    \n  :global ExpectedConfigVersion;\
    \n  :global Identity;\
    \n  :global IDonate;\
    \n  :global NoNewsAndChangesNotification;\
    \n  :global ScriptUpdatesBaseUrl;\
    \n  :global ScriptUpdatesCRLF;\
    \n  :global ScriptUpdatesUrlSuffix;\
    \n\
    \n  :global CertificateAvailable;\
    \n  :global EitherOr;\
    \n  :global FetchUserAgentStr;\
    \n  :global Grep;\
    \n  :global IfThenElse;\
    \n  :global LogPrint;\
    \n  :global LogPrintOnce;\
    \n  :global ParseKeyValueStore;\
    \n  :global RequiredRouterOS;\
    \n  :global SendNotification2;\
    \n  :global SymbolForNotification;\
    \n  :global ValidateSyntax;\
    \n\
    \n  :if ([ \$CertificateAvailable \"ISRG Root X2\" ] = false) do={\
    \n    \$LogPrint warning \$0 (\"Downloading certificate failed, trying wit\
    hout.\");\
    \n  }\
    \n\
    \n  :foreach Script in=\$Scripts do={\
    \n    :if ([ :len [ /system/script/find where name=\$Script ] ] = 0) do={\
    \n      \$LogPrint info \$0 (\"Adding new script: \" . \$Script);\
    \n      /system/script/add name=\$Script owner=\$Script source=\"#!rsc by \
    RouterOS\\n\" comment=\$NewComment;\
    \n    }\
    \n  }\
    \n\
    \n  :local ExpectedConfigVersionBefore \$ExpectedConfigVersion;\
    \n  :local ReloadGlobalFunctions false;\
    \n  :local ReloadGlobalConfig false;\
    \n\
    \n  :foreach Script in=[ /system/script/find where source~\"^#!rsc by Rout\
    erOS\\r\?\\n\" ] do={\
    \n    :local ScriptVal [ /system/script/get \$Script ];\
    \n    :local ScriptInfo [ \$ParseKeyValueStore (\$ScriptVal->\"comment\") \
    ];\
    \n    :local SourceNew;\
    \n\
    \n    :foreach Scheduler in=[ /system/scheduler/find where on-event~(\"\\\
    \\b\" . \$ScriptVal->\"name\" . \"\\\\b\") ] do={\
    \n      :local SchedulerVal [ /system/scheduler/get \$Scheduler ];\
    \n      :if (\$ScriptVal->\"policy\" != \$SchedulerVal->\"policy\") do={\
    \n        \$LogPrint warning \$0 (\"Policies differ for script '\" . \$Scr\
    iptVal->\"name\" . \\\
    \n          \"' and its scheduler '\" . \$SchedulerVal->\"name\" . \"'!\")\
    ;\
    \n      }\
    \n    }\
    \n\
    \n    :if (!(\$ScriptInfo->\"ignore\" = true)) do={\
    \n      :do {\
    \n        :local BaseUrl [ \$EitherOr (\$ScriptInfo->\"base-url\") \$Scrip\
    tUpdatesBaseUrl ];\
    \n        :local UrlSuffix [ \$EitherOr (\$ScriptInfo->\"url-suffix\") \$S\
    criptUpdatesUrlSuffix ];\
    \n        :local Url (\$BaseUrl . \$ScriptVal->\"name\" . \".rsc\" . \$Url\
    Suffix);\
    \n        \$LogPrint debug \$0 (\"Fetching script '\" . \$ScriptVal->\"nam\
    e\" . \"' from url: \" . \$Url);\
    \n        :local Result [ /tool/fetch check-certificate=yes-without-crl \\\
    \n          http-header-field=({ [ \$FetchUserAgentStr \$0 ] }) \$Url outp\
    ut=user as-value ];\
    \n        :if (\$Result->\"status\" = \"finished\") do={\
    \n          :set SourceNew [ :tolf (\$Result->\"data\") ];\
    \n        }\
    \n      } on-error={\
    \n        :if (\$ScriptVal->\"source\" = \"#!rsc by RouterOS\\n\") do={\
    \n          \$LogPrint warning \$0 (\"Failed fetching script '\" . \$Scrip\
    tVal->\"name\" . \\\
    \n            \"', removing dummy. Typo on installation\?\");\
    \n          /system/script/remove \$Script;\
    \n        } else={\
    \n          \$LogPrint warning \$0 (\"Failed fetching script '\" . \$Scrip\
    tVal->\"name\" . \"'!\");\
    \n        }\
    \n      }\
    \n    }\
    \n\
    \n    :if ([ :len \$SourceNew ] > 0) do={\
    \n      :local SourceCRLF [ :tocrlf \$SourceNew ];\
    \n      :if (\$SourceNew != \$ScriptVal->\"source\" && \$SourceCRLF != \$S\
    criptVal->\"source\") do={\
    \n        :if ([ :pick \$SourceNew 0 18 ] = \"#!rsc by RouterOS\\n\") do={\
    \n          :local Required ([ \$ParseKeyValueStore [ \$Grep \$SourceNew (\
    \"\\23 requires RouterOS, \") ] ]->\"version\");\
    \n          :if ([ \$RequiredRouterOS \$0 [ \$EitherOr \$Required \"0.0\" \
    ] false ] = true) do={\
    \n            :if ([ \$ValidateSyntax \$SourceNew ] = true) do={\
    \n              \$LogPrint info \$0 (\"Updating script: \" . \$ScriptVal->\
    \"name\");\
    \n              /system/script/set owner=(\$ScriptVal->\"name\") \\\
    \n                  source=[ \$IfThenElse (\$ScriptUpdatesCRLF = true) \$S\
    ourceCRLF \$SourceNew ] \$Script;\
    \n              :if (\$ScriptVal->\"name\" = \"global-config\") do={\
    \n                :set ReloadGlobalConfig true;\
    \n              }\
    \n              :if (\$ScriptVal->\"name\" = \"global-functions\" || \$Scr\
    iptVal->\"name\" ~ (\"^mod/.\")) do={\
    \n                :set ReloadGlobalFunctions true;\
    \n              }\
    \n            } else={\
    \n              \$LogPrint warning \$0 (\"Syntax validation for script '\"\
    \_. \$ScriptVal->\"name\" . \\\
    \n                \"' failed! Ignoring!\");\
    \n            }\
    \n          } else={\
    \n            \$LogPrintOnce warning \$0 (\"The script '\" . \$ScriptVal->\
    \"name\" . \"' requires RouterOS \" . \\\
    \n              \$Required . \", which is not met by your installation. Ig\
    noring!\");\
    \n          }\
    \n        } else={\
    \n          \$LogPrint warning \$0 (\"Looks like new script '\" . \$Script\
    Val->\"name\" . \\\
    \n            \"' is not valid (missing shebang). Ignoring!\");\
    \n        }\
    \n      } else={\
    \n        \$LogPrint debug \$0 (\"Script '\" .  \$ScriptVal->\"name\" . \"\
    ' did not change.\");\
    \n      }\
    \n    } else={\
    \n      \$LogPrint debug \$0 (\"No update for script '\" . \$ScriptVal->\"\
    name\" . \"'.\");\
    \n    }\
    \n  }\
    \n\
    \n  :if (\$ReloadGlobalFunctions = true) do={\
    \n    \$LogPrint info \$0 (\"Reloading global functions.\");\
    \n    :do {\
    \n      /system/script/run global-functions;\
    \n    } on-error={\
    \n      \$LogPrint error \$0 (\"Reloading global functions failed!\");\
    \n    }\
    \n  }\
    \n\
    \n  :if (\$ReloadGlobalConfig = true) do={\
    \n    \$LogPrint info \$0 (\"Reloading global configuration.\");\
    \n    :do {\
    \n      /system/script/run global-config;\
    \n    } on-error={\
    \n      \$LogPrint error \$0 (\"Reloading global configuration failed!\" .\
    \_\\\
    \n        \" Syntax error or missing overlay\?\");\
    \n    }\
    \n  }\
    \n\
    \n  :if (\$ExpectedConfigVersionBefore > \$ExpectedConfigVersion) do={\
    \n    \$LogPrint warning \$0 (\"The configuration version decreased from \
    \" . \\\
    \n      \$ExpectedConfigVersionBefore . \" to \" . \$ExpectedConfigVersion\
    \_. \\\
    \n      \". Installed an older version\?\");\
    \n  }\
    \n\
    \n  :if (\$ExpectedConfigVersionBefore < \$ExpectedConfigVersion) do={\
    \n    :global GlobalConfigChanges;\
    \n    :global GlobalConfigMigration;\
    \n    :local ChangeLogCode;\
    \n\
    \n    :do {\
    \n      :local Url (\$ScriptUpdatesBaseUrl . \"news-and-changes.rsc\" . \$\
    ScriptUpdatesUrlSuffix);\
    \n      \$LogPrint debug \$0 (\"Fetching news, changes and migration: \" .\
    \_\$Url);\
    \n      :local Result [ /tool/fetch check-certificate=yes-without-crl \\\
    \n        http-header-field=({ [ \$FetchUserAgentStr \$0 ] }) \$Url output\
    =user as-value ];\
    \n      :if (\$Result->\"status\" = \"finished\") do={\
    \n        :set ChangeLogCode (\$Result->\"data\");\
    \n      }\
    \n    } on-error={\
    \n      \$LogPrint warning \$0 (\"Failed fetching news, changes and migrat\
    ion!\");\
    \n    }\
    \n\
    \n    :if ([ :len \$ChangeLogCode ] > 0) do={\
    \n      :if ([ \$ValidateSyntax \$ChangeLogCode ] = true) do={\
    \n        :do {\
    \n          [ :parse \$ChangeLogCode ];\
    \n        } on-error={\
    \n          \$LogPrint warning \$0 (\"The changelog failed to run!\");\
    \n        }\
    \n      } else={\
    \n        \$LogPrint warning \$0 (\"The changelog failed syntax validation\
    !\");\
    \n      }\
    \n    }\
    \n\
    \n    :if ([ :len \$GlobalConfigMigration ] > 0) do={\
    \n      :for I from=(\$ExpectedConfigVersionBefore + 1) to=\$ExpectedConfi\
    gVersion do={\
    \n        :local Migration (\$GlobalConfigMigration->[ :tostr \$I ]);\
    \n        :if ([ :typeof \$Migration ] = \"str\") do={\
    \n          :if ([ \$ValidateSyntax \$Migration ] = true) do={\
    \n            \$LogPrint info \$0 (\"Applying migration for change \" . \$\
    I . \": \" . \$Migration);\
    \n            :do {\
    \n              [ :parse \$Migration ];\
    \n            } on-error={\
    \n              \$LogPrint warning \$0 (\"Migration code for change \" . \
    \$I . \" failed to run!\");\
    \n            }\
    \n          } else={\
    \n            \$LogPrint warning \$0 (\"Migration code for change \" . \$I\
    \_. \" failed syntax validation!\");\
    \n          }\
    \n        }\
    \n      }\
    \n    }\
    \n\
    \n    :local NotificationMessage (\"The configuration version on \" . \$Id\
    entity . \" increased \" . \\\
    \n       \"to \" . \$ExpectedConfigVersion . \", current configuration may\
    \_need modification. \" . \\\
    \n       \"Please review and update global-config-overlay, then re-run glo\
    bal-config.\");\
    \n    \$LogPrint info \$0 (\$NotificationMessage);\
    \n\
    \n    :if ([ :len \$GlobalConfigChanges ] > 0) do={\
    \n      :set NotificationMessage (\$NotificationMessage . \"\\n\\nChanges:\
    \");\
    \n      :for I from=(\$ExpectedConfigVersionBefore + 1) to=\$ExpectedConfi\
    gVersion do={\
    \n        :local Change (\$GlobalConfigChanges->[ :tostr \$I ]);\
    \n        :set NotificationMessage (\$NotificationMessage . \"\\n \" . \\\
    \n            [ \$SymbolForNotification \"pushpin\" \"*\" ] . \$Change);\
    \n        \$LogPrint info \$0 (\"Change \" . \$I . \": \" . \$Change);\
    \n      }\
    \n    } else={\
    \n      :set NotificationMessage (\$NotificationMessage . \"\\n\\nNews and\
    \_changes are not available.\");\
    \n    }\
    \n\
    \n    :if (\$NoNewsAndChangesNotification != true) do={\
    \n      :local Link;\
    \n      :if (\$IDonate != true) do={\
    \n        :set NotificationMessage (\$NotificationMessage . \\\
    \n          \"\\n\\n==== donation hint ====\\n\" . \\\
    \n          \"This project is developed in private spare time and usage is\
    \_\" . \\\
    \n          \"free of charge for you. If you like the scripts and think th\
    is is \" . \\\
    \n          \"of value for you or your business please consider a donation\
    .\");\
    \n        :set Link \"https://rsc.eworm.de/#donate\";\
    \n      }\
    \n\
    \n      \$SendNotification2 ({ origin=\$0; \\\
    \n        subject=([ \$SymbolForNotification \"pushpin\" ] . \"News and co\
    nfiguration changes\"); \\\
    \n        message=\$NotificationMessage; link=\$Link });\
    \n    }\
    \n\
    \n    :set GlobalConfigChanges;\
    \n    :set GlobalConfigMigration;\
    \n  }\
    \n} on-error={\
    \n  :global ExitError; \$ExitError false \$0;\
    \n} }\
    \n\
    \n# lock script against multiple invocation\
    \n:set ScriptLock do={\
    \n  :local Script   [ :tostr \$1 ];\
    \n  :local WaitMax ([ :tonum \$3 ] * 10);\
    \n\
    \n  :global GetRandom20CharAlNum;\
    \n  :global IfThenElse;\
    \n  :global LogPrint;\
    \n\
    \n  :global ScriptLockOrder;\
    \n  :if ([ :typeof \$ScriptLockOrder ] = \"nothing\") do={\
    \n    :set ScriptLockOrder ({});\
    \n  }\
    \n  :if ([ :typeof (\$ScriptLockOrder->\$Script) ] = \"nothing\") do={\
    \n    :set (\$ScriptLockOrder->\$Script) ({});\
    \n  }\
    \n\
    \n  :local JobCount do={\
    \n    :local Script [ :tostr \$1 ];\
    \n\
    \n    :return [ :len [ /system/script/job/find where script=\$Script ] ];\
    \n  }\
    \n\
    \n  :local TicketCount do={\
    \n    :local Script [ :tostr \$1 ];\
    \n\
    \n    :global ScriptLockOrder;\
    \n\
    \n    :local Count 0;\
    \n    :foreach Ticket in=(\$ScriptLockOrder->\$Script) do={\
    \n      :if ([ :typeof \$Ticket ] != \"nothing\") do={\
    \n        :set Count (\$Count + 1);\
    \n      }\
    \n    }\
    \n    :return \$Count;\
    \n  }\
    \n\
    \n  :local IsFirstTicket do={\
    \n    :local Script [ :tostr \$1 ];\
    \n    :local Check  [ :tostr \$2 ];\
    \n\
    \n    :global ScriptLockOrder;\
    \n\
    \n    :foreach Ticket in=(\$ScriptLockOrder->\$Script) do={\
    \n      :if (\$Ticket = \$Check) do={ :return true; }\
    \n      :if ([ :typeof \$Ticket ] != \"nothing\" && \$Ticket != \$Check) d\
    o={ :return false; }\
    \n    }\
    \n    :return false;\
    \n  }\
    \n\
    \n  :local AddTicket do={\
    \n    :local Script [ :tostr \$1 ];\
    \n    :local Add    [ :tostr \$2 ];\
    \n\
    \n    :global ScriptLockOrder;\
    \n\
    \n    :while (true) do={\
    \n      :local Pos [ :len (\$ScriptLockOrder->\$Script) ];\
    \n      :set (\$ScriptLockOrder->\$Script->\$Pos) \$Add;\
    \n      :delay 10ms;\
    \n      :if ((\$ScriptLockOrder->\$Script->\$Pos) = \$Add) do={ :return tr\
    ue; }\
    \n    }\
    \n  }\
    \n\
    \n  :local RemoveTicket do={\
    \n    :local Script [ :tostr \$1 ];\
    \n    :local Remove [ :tostr \$2 ];\
    \n\
    \n    :global ScriptLockOrder;\
    \n\
    \n    :foreach Id,Ticket in=(\$ScriptLockOrder->\$Script) do={\
    \n      :while ((\$ScriptLockOrder->\$Script->\$Id) = \$Remove) do={\
    \n        :set (\$ScriptLockOrder->\$Script->\$Id);\
    \n        :delay 10ms;\
    \n      }\
    \n    }\
    \n  }\
    \n\
    \n  :local CleanupTickets do={\
    \n    :local Script [ :tostr \$1 ];\
    \n\
    \n    :global ScriptLockOrder;\
    \n\
    \n    :foreach Ticket in=(\$ScriptLockOrder->\$Script) do={\
    \n      :if ([ :typeof \$Ticket ] != \"nothing\") do={\
    \n        :return false;\
    \n      }\
    \n    }\
    \n\
    \n    :set (\$ScriptLockOrder->\$Script) ({});\
    \n  }\
    \n\
    \n  :if ([ :len [ /system/script/find where name=\$Script ] ] = 0) do={\
    \n    \$LogPrint error \$0 (\"A script named '\" . \$Script . \"' does not\
    \_exist!\");\
    \n    :error false;\
    \n  }\
    \n\
    \n  :if ([ \$JobCount \$Script ] = 0) do={\
    \n    \$LogPrint error \$0 (\"No script '\" . \$Script . \"' is running!\"\
    );\
    \n    :error false;\
    \n  }\
    \n\
    \n  :if ([ \$TicketCount \$Script ] >= [ \$JobCount \$Script ]) do={\
    \n    \$LogPrint error \$0 (\"More tickets than running scripts '\" . \$Sc\
    ript . \"', resetting!\");\
    \n    :set (\$ScriptLockOrder->\$Script) ({});\
    \n    /system/script/job/remove [ find where script=\$Script ];\
    \n  }\
    \n\
    \n  :local MyTicket [ \$GetRandom20CharAlNum 6 ];\
    \n  \$AddTicket \$Script \$MyTicket;\
    \n\
    \n  :local WaitCount 0;\
    \n  :while (\$WaitMax > \$WaitCount && \\\
    \n      ([ \$IsFirstTicket \$Script \$MyTicket ] = false || \\\
    \n      [ \$TicketCount \$Script ] < [ \$JobCount \$Script ])) do={\
    \n    :set WaitCount (\$WaitCount + 1);\
    \n    :delay 100ms;\
    \n  }\
    \n\
    \n  :if ([ \$IsFirstTicket \$Script \$MyTicket ] = true && \\\
    \n      [ \$TicketCount \$Script ] = [ \$JobCount \$Script ]) do={\
    \n    \$RemoveTicket \$Script \$MyTicket;\
    \n    \$CleanupTickets \$Script;\
    \n    :return true;\
    \n  }\
    \n\
    \n  \$RemoveTicket \$Script \$MyTicket;\
    \n  \$LogPrint debug \$0 (\"Script '\" . \$Script . \"' started more than \
    once\" . \\\
    \n    [ \$IfThenElse (\$WaitCount > 0) \" and timed out waiting for lock\"\
    \_\"\" ] . \"...\");\
    \n  :return false;\
    \n}\
    \n\
    \n# send notification via NotificationFunctions - expects at least two str\
    ing arguments\
    \n:set SendNotification do={ :do {\
    \n  :global SendNotification2;\
    \n\
    \n  \$SendNotification2 ({ origin=\$0; subject=\$1; message=\$2; link=\$3;\
    \_silent=\$4 });\
    \n} on-error={\
    \n  :global ExitError; \$ExitError false \$0;\
    \n} }\
    \n\
    \n# send notification via NotificationFunctions - expects one array argume\
    nt\
    \n:set SendNotification2 do={\
    \n  :local Notification \$1;\
    \n\
    \n  :global NotificationFunctions;\
    \n\
    \n  :foreach FunctionName,Discard in=\$NotificationFunctions do={\
    \n    (\$NotificationFunctions->\$FunctionName) \\\
    \n      (\"\\\$NotificationFunctions->\\\"\" . \$FunctionName . \"\\\"\") \
    \\\
    \n      \$Notification;\
    \n  }\
    \n}\
    \n\
    \n# return UTF-8 symbol for unicode name\
    \n:set SymbolByUnicodeName do={\
    \n  :local Name [ :tostr \$1 ];\
    \n\
    \n  :global LogPrintOnce;\
    \n\
    \n  :local Symbols {\
    \n    \"abacus\"=\"\\F0\\9F\\A7\\AE\";\
    \n    \"alarm-clock\"=\"\\E2\\8F\\B0\";\
    \n    \"arrow-down\"=\"\\E2\\AC\\87\";\
    \n    \"arrow-up\"=\"\\E2\\AC\\86\";\
    \n    \"calendar\"=\"\\F0\\9F\\93\\85\";\
    \n    \"card-file-box\"=\"\\F0\\9F\\97\\83\";\
    \n    \"chart-decreasing\"=\"\\F0\\9F\\93\\89\";\
    \n    \"chart-increasing\"=\"\\F0\\9F\\93\\88\";\
    \n    \"cloud\"=\"\\E2\\98\\81\";\
    \n    \"cross-mark\"=\"\\E2\\9D\\8C\";\
    \n    \"earth\"=\"\\F0\\9F\\8C\\8D\";\
    \n    \"fire\"=\"\\F0\\9F\\94\\A5\";\
    \n    \"floppy-disk\"=\"\\F0\\9F\\92\\BE\";\
    \n    \"gear\"=\"\\E2\\9A\\99\";\
    \n    \"heart\"=\"\\E2\\99\\A5\";\
    \n    \"high-voltage-sign\"=\"\\E2\\9A\\A1\";\
    \n    \"incoming-envelope\"=\"\\F0\\9F\\93\\A8\";\
    \n    \"information\"=\"\\E2\\84\\B9\";\
    \n    \"large-orange-circle\"=\"\\F0\\9F\\9F\\A0\";\
    \n    \"large-red-circle\"=\"\\F0\\9F\\94\\B4\";\
    \n    \"link\"=\"\\F0\\9F\\94\\97\";\
    \n    \"lock-with-ink-pen\"=\"\\F0\\9F\\94\\8F\";\
    \n    \"memo\"=\"\\F0\\9F\\93\\9D\";\
    \n    \"mobile-phone\"=\"\\F0\\9F\\93\\B1\";\
    \n    \"pushpin\"=\"\\F0\\9F\\93\\8C\";\
    \n    \"scissors\"=\"\\E2\\9C\\82\";\
    \n    \"smiley-partying-face\"=\"\\F0\\9F\\A5\\B3\";\
    \n    \"smiley-smiling-face\"=\"\\E2\\98\\BA\";\
    \n    \"smiley-winking-face-with-tongue\"=\"\\F0\\9F\\98\\9C\";\
    \n    \"sparkles\"=\"\\E2\\9C\\A8\";\
    \n    \"speech-balloon\"=\"\\F0\\9F\\92\\AC\";\
    \n    \"star\"=\"\\E2\\AD\\90\";\
    \n    \"warning-sign\"=\"\\E2\\9A\\A0\";\
    \n    \"white-heavy-check-mark\"=\"\\E2\\9C\\85\"\
    \n  }\
    \n\
    \n  :if ([ :len (\$Symbols->\$Name) ] = 0) do={\
    \n    \$LogPrintOnce warning \$0 (\"No symbol available for name '\" . \$N\
    ame . \"'!\");\
    \n    :return \"\";\
    \n  }\
    \n\
    \n  :return ((\$Symbols->\$Name) . \"\\EF\\B8\\8F\");\
    \n}\
    \n\
    \n# return symbol for notification\
    \n:set SymbolForNotification do={\
    \n  :global NotificationsWithSymbols;\
    \n  :global SymbolByUnicodeName;\
    \n  :global IfThenElse;\
    \n\
    \n  :if (\$NotificationsWithSymbols != true) do={\
    \n    :return [ \$IfThenElse ([ :len \$2 ] > 0) ([ :tostr \$2 ] . \" \") \
    \"\" ];\
    \n  }\
    \n  :local Return \"\";\
    \n  :foreach Symbol in=[ :toarray \$1 ] do={\
    \n    :set Return (\$Return . [ \$SymbolByUnicodeName \$Symbol ]);\
    \n  }\
    \n  :return (\$Return . \" \");\
    \n}\
    \n\
    \n# convert line endings, UNIX -> DOS\
    \n:set Unix2Dos do={\
    \n  :return [ :tocrlf [ :tostr \$1 ] ];\
    \n}\
    \n\
    \n# url encoding\
    \n:set UrlEncode do={\
    \n  :local Input [ :tostr \$1 ];\
    \n\
    \n  :if ([ :len \$Input ] = 0) do={\
    \n    :return \"\";\
    \n  }\
    \n\
    \n  :local Return \"\";\
    \n  :local Chars (\"\\n\\r !\\\"#\\\$%&'()*+,:;<=>\?@[\\\\]^`{|}~\");\
    \n  :local Subs { \"%0A\"; \"%0D\"; \"%20\"; \"%21\"; \"%22\"; \"%23\"; \"\
    %24\"; \"%25\"; \"%26\"; \"%27\";\
    \n         \"%28\"; \"%29\"; \"%2A\"; \"%2B\"; \"%2C\"; \"%3A\"; \"%3B\"; \
    \"%3C\"; \"%3D\"; \"%3E\"; \"%3F\";\
    \n         \"%40\"; \"%5B\"; \"%5C\"; \"%5D\"; \"%5E\"; \"%60\"; \"%7B\"; \
    \"%7C\"; \"%7D\"; \"%7E\" };\
    \n\
    \n  :for I from=0 to=([ :len \$Input ] - 1) do={\
    \n    :local Char [ :pick \$Input \$I ];\
    \n    :local Replace [ :find \$Chars \$Char ];\
    \n\
    \n    :if ([ :typeof \$Replace ] = \"num\") do={\
    \n      :set Char (\$Subs->\$Replace);\
    \n    }\
    \n    :set Return (\$Return . \$Char);\
    \n  }\
    \n\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# basic syntax validation\
    \n:set ValidateSyntax do={\
    \n  :local Code [ :tostr \$1 ];\
    \n\
    \n  :do {\
    \n    [ :parse (\":local Validate do={\\n\" . \$Code . \"\\n}\") ];\
    \n  } on-error={\
    \n    :return false;\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# convert version string to numeric value\
    \n:set VersionToNum do={\
    \n  :local Input [ :tostr \$1 ];\
    \n  :local Multi 0x1000000;\
    \n  :local Return 0;\
    \n\
    \n  :global CharacterReplace;\
    \n\
    \n  :set Input [ \$CharacterReplace \$Input \".\" \",\" ];\
    \n  :foreach I in={ \"zero\"; \"alpha\"; \"beta\"; \"rc\" } do={\
    \n    :set Input [ \$CharacterReplace \$Input \$I (\",\" . \$I . \",\") ];\
    \n  }\
    \n\
    \n  :foreach Value in=([ :toarray \$Input ], 0) do={\
    \n    :local Num [ :tonum \$Value ];\
    \n    :if (\$Multi = 0x100) do={\
    \n      :if ([ :typeof \$Num ] = \"num\") do={\
    \n        :set Return (\$Return + 0xff00);\
    \n        :set Multi (\$Multi / 0x100);\
    \n      } else={\
    \n        :if (\$Value = \"zero\") do={ }\
    \n        :if (\$Value = \"alpha\") do={ :set Return (\$Return + 0x3f00); \
    }\
    \n        :if (\$Value = \"beta\") do={ :set Return (\$Return + 0x5f00); }\
    \n        :if (\$Value = \"rc\") do={ :set Return (\$Return + 0x7f00); }\
    \n      }\
    \n    }\
    \n    :if ([ :typeof \$Num ] = \"num\") do={ :set Return (\$Return + (\$Va\
    lue * \$Multi)); }\
    \n    :set Multi (\$Multi / 0x100);\
    \n  }\
    \n\
    \n  :return \$Return;\
    \n}\
    \n\
    \n# wait for default route to be reachable\
    \n:set WaitDefaultRouteReachable do={\
    \n  :global IsDefaultRouteReachable;\
    \n\
    \n  :while ([ \$IsDefaultRouteReachable ] = false) do={\
    \n    :delay 1s;\
    \n  }\
    \n}\
    \n\
    \n# wait for DNS to resolve\
    \n:set WaitDNSResolving do={\
    \n  :global IsDNSResolving;\
    \n\
    \n  :while ([ \$IsDNSResolving ] = false) do={\
    \n    :delay 1s;\
    \n  }\
    \n}\
    \n\
    \n# wait for file to be available\
    \n:set WaitForFile do={\
    \n  :local FileName [ :tostr  \$1 ];\
    \n  :local WaitTime [ :totime \$2 ];\
    \n\
    \n  :global CleanFilePath;\
    \n  :global EitherOr;\
    \n  :global MAX;\
    \n\
    \n  :set FileName [ \$CleanFilePath \$FileName ];\
    \n  :local I 1;\
    \n  :local Delay ([ \$MAX [ \$EitherOr \$WaitTime 2s ] 100ms ] / 10);\
    \n\
    \n  :while ([ :len [ /file/find where name=\$FileName ] ] = 0) do={\
    \n    :if (\$I >= 10) do={\
    \n      :return false;\
    \n    }\
    \n    :delay \$Delay;\
    \n    :set I (\$I + 1);\
    \n  }\
    \n  :return true;\
    \n}\
    \n\
    \n# wait to be fully connected (default route is reachable, time is sync, \
    DNS resolves)\
    \n:set WaitFullyConnected do={\
    \n  :global WaitDefaultRouteReachable;\
    \n  :global WaitDNSResolving;\
    \n  :global WaitTimeSync;\
    \n\
    \n  \$WaitDefaultRouteReachable;\
    \n  \$WaitTimeSync;\
    \n  \$WaitDNSResolving;\
    \n}\
    \n\
    \n# wait for time to become synced\
    \n:set WaitTimeSync do={\
    \n  :global IsTimeSync;\
    \n\
    \n  :while ([ \$IsTimeSync ] = false) do={\
    \n    :delay 1s;\
    \n  }\
    \n}\
    \n\
    \n# load modules\
    \n:foreach Script in=[ /system/script/find where name ~ \"^mod/.\" ] do={\
    \n  :local ScriptVal [ /system/script/get \$Script ];\
    \n  :if ([ \$ValidateSyntax (\$ScriptVal->\"source\") ] = true) do={\
    \n    :do {\
    \n      /system/script/run \$Script;\
    \n    } on-error={\
    \n      \$LogPrint error \$0 (\"Module '\" . \$ScriptVal->\"name\" . \"' f\
    ailed to run.\");\
    \n    }\
    \n  } else={\
    \n    \$LogPrint error \$0 (\"Module '\" . \$ScriptVal->\"name\" . \"' fai\
    led syntax validation, skipping.\");\
    \n  }\
    \n}\
    \n\
    \n# Log success\
    \n:local Resource [ /system/resource/get ];\
    \n\$LogPrintOnce info \$ScriptName (\"Loaded on \" . \$Resource->\"board-n\
    ame\" . \\\
    \n  \" with RouterOS \" . \$Resource->\"version\" . \".\");\
    \n\
    \n# signal we are ready\
    \n:set GlobalFunctionsReady true;\
    \n"
add dont-require-permissions=no name=dhcp-to-dns owner=dhcp-to-dns policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="#\
    !rsc by RouterOS\
    \n# RouterOS script: dhcp-to-dns\
    \n# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de>\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md\
    \n#\
    \n# provides: lease-script, order=20\
    \n# requires RouterOS, version=7.16\
    \n#\
    \n# check DHCP leases and add/remove/update DNS entries\
    \n# https://git.eworm.de/cgit/routeros-scripts/about/doc/dhcp-to-dns.md\
    \n\
    \n:global GlobalFunctionsReady;\
    \n:while (\$GlobalFunctionsReady != true) do={ :delay 500ms; }\
    \n\
    \n:local ExitOK false;\
    \n:do {\
    \n  :local ScriptName [ :jobname ];\
    \n\
    \n  :global Domain;\
    \n  :global Identity;\
    \n\
    \n  :global CleanName;\
    \n  :global EitherOr;\
    \n  :global IfThenElse;\
    \n  :global LogPrint;\
    \n  :global LogPrintOnce;\
    \n  :global ParseKeyValueStore;\
    \n  :global ScriptLock;\
    \n\
    \n  :if ([ \$ScriptLock \$ScriptName 10 ] = false) do={\
    \n    :set ExitOK true;\
    \n    :error false;\
    \n  }\
    \n\
    \n  :local Ttl 5m;\
    \n  :local CommentPrefix (\"managed by \" . \$ScriptName);\
    \n  :local CommentString (\"--- \" . \$ScriptName . \" above ---\");\
    \n\
    \n  :if ([ :len [ /ip/dns/static/find where (name=\$CommentString or (comm\
    ent=\$CommentString and name=-)) type=NXDOMAIN disabled ] ] = 0) do={\
    \n    /ip/dns/static/add name=\$CommentString type=NXDOMAIN disabled=yes;\
    \n    \$LogPrint warning \$ScriptName (\"Added disabled static dns record \
    with name '\" . \$CommentString . \"'.\");\
    \n  }\
    \n  :local PlaceBefore ([ /ip/dns/static/find where (name=\$CommentString \
    or (comment=\$CommentString and name=-)) type=NXDOMAIN disabled ]->0);\
    \n\
    \n  :foreach DnsRecord in=[ /ip/dns/static/find where comment~(\"^\" . \$C\
    ommentPrefix . \"\\\\b\") type=A ] do={\
    \n    :local DnsRecordVal [ /ip/dns/static/get \$DnsRecord ];\
    \n    :local DnsRecordInfo [ \$ParseKeyValueStore (\$DnsRecordVal->\"comme\
    nt\") ];\
    \n    :local MacInServer (\$DnsRecordInfo->\"macaddress\" . \" in \" . \$D\
    nsRecordInfo->\"server\");\
    \n\
    \n    :if ([ :len [ /ip/dhcp-server/lease/find where active-mac-address=(\
    \$DnsRecordInfo->\"macaddress\") \\\
    \n         active-address=(\$DnsRecordVal->\"address\") server=(\$DnsRecor\
    dInfo->\"server\") status=bound ] ] > 0) do={\
    \n      \$LogPrint debug \$ScriptName (\"Lease for \" . \$MacInServer . \"\
    \_(\" . \$DnsRecordVal->\"name\" . \") still exists. Not deleting record.\
    \");\
    \n    } else={\
    \n      :local Found false;\
    \n      \$LogPrint info \$ScriptName (\"Lease expired for \" . \$MacInServ\
    er . \", deleting record (\" . \$DnsRecordVal->\"name\" . \").\");\
    \n      /ip/dns/static/remove \$DnsRecord;\
    \n      /ip/dns/static/remove [ find where type=CNAME comment=(\$DnsRecord\
    Val->\"comment\") ];\
    \n    }\
    \n  }\
    \n\
    \n  :foreach Lease in=[ /ip/dhcp-server/lease/find where status=bound ] do\
    ={\
    \n    :local LeaseVal;\
    \n    :do {\
    \n      :set LeaseVal [ /ip/dhcp-server/lease/get \$Lease ];\
    \n      :if ([ :len [ /ip/dhcp-server/lease/find where active-mac-address=\
    (\$LeaseVal->\"active-mac-address\") status=bound ] ] > 1) do={\
    \n        \$LogPrintOnce info \$ScriptName (\"Multiple bound leases found \
    for mac-address \" . (\$LeaseVal->\"active-mac-address\") . \"!\");\
    \n      }\
    \n    } on-error={\
    \n      \$LogPrint debug \$ScriptName (\"A lease just vanished, ignoring.\
    \");\
    \n    }\
    \n\
    \n    :if ([ :len (\$LeaseVal->\"active-address\") ] > 0) do={\
    \n      :local Comment (\$CommentPrefix . \", macaddress=\" . \$LeaseVal->\
    \"active-mac-address\" . \", server=\" . \$LeaseVal->\"server\");\
    \n      :local MacDash [ \$CleanName (\$LeaseVal->\"active-mac-address\") \
    ];\
    \n      :local HostName [ \$CleanName [ \$EitherOr ([ \$ParseKeyValueStore\
    \_(\$LeaseVal->\"comment\") ]->\"hostname\") (\$LeaseVal->\"host-name\") ]\
    \_];\
    \n      :local Network [ /ip/dhcp-server/network/find where (\$LeaseVal->\
    \"active-address\") in address ];\
    \n      :local NetworkVal;\
    \n      :if ([ :len \$Network ] > 0) do={\
    \n        :set NetworkVal [ /ip/dhcp-server/network/get (\$Network->0) ];\
    \n      }\
    \n      :local NetworkInfo [ \$ParseKeyValueStore (\$NetworkVal->\"comment\
    \") ];\
    \n      :local NetDomain ([ \$IfThenElse ([ :len (\$NetworkInfo->\"name-ex\
    tra\") ] > 0) (\$NetworkInfo->\"name-extra\" . \".\") ] . \\\
    \n        [ \$EitherOr [ \$EitherOr (\$NetworkInfo->\"domain\") (\$Network\
    Val->\"domain\") ] \$Domain ]);\
    \n      :local FullA (\$MacDash . \".\" . \$NetDomain);\
    \n      :local FullCN (\$HostName . \".\" . \$NetDomain);\
    \n      :local MacInServer (\$LeaseVal->\"active-mac-address\" . \" in \" \
    . \$LeaseVal->\"server\");\
    \n\
    \n      :local DnsRecord [ /ip/dns/static/find where comment=\$Comment typ\
    e=A ];\
    \n      :if ([ :len \$DnsRecord ] > 0) do={\
    \n        :local DnsRecordVal [ /ip/dns/static/get \$DnsRecord ];\
    \n\
    \n        :if (\$DnsRecordVal->\"address\" = \$LeaseVal->\"active-address\
    \" && \$DnsRecordVal->\"name\" = \$FullA) do={\
    \n          \$LogPrint debug \$ScriptName (\"The A record for \" . \$MacIn\
    Server . \" (\" . \$FullA . \") does not need updating.\");\
    \n        } else={\
    \n          \$LogPrint info \$ScriptName (\"Updating A record for \" . \$M\
    acInServer . \" (\" . \$FullA . \" -> \" . \$LeaseVal->\"active-address\" \
    . \").\");\
    \n          /ip/dns/static/set address=(\$LeaseVal->\"active-address\") na\
    me=\$FullA \$DnsRecord;\
    \n        }\
    \n\
    \n        :local CName [ /ip/dns/static/find where comment=\$Comment type=\
    CNAME ];\
    \n        :if ([ :len \$CName ] > 0) do={\
    \n          :local CNameVal [ /ip/dns/static/get \$CName ];\
    \n          :if (\$CNameVal->\"name\" != \$FullCN || \$CNameVal->\"cname\"\
    \_!= \$FullA) do={\
    \n            \$LogPrint info \$ScriptName (\"Deleting CNAME record with w\
    rong data for \" . \$MacInServer . \".\");\
    \n            /ip/dns/static/remove \$CName;\
    \n          }\
    \n        }\
    \n        :if ([ :len \$HostName ] > 0 && [ :len [ /ip/dns/static/find whe\
    re name=\$FullCN type=CNAME ] ] = 0) do={\
    \n          \$LogPrint info \$ScriptName (\"Adding CNAME record for \" . \
    \$MacInServer . \" (\" . \$FullCN . \" -> \" . \$FullA . \").\");\
    \n          /ip/dns/static/add name=\$FullCN type=CNAME cname=\$FullA ttl=\
    \$Ttl comment=\$Comment place-before=\$PlaceBefore;\
    \n        }\
    \n\
    \n      } else={\
    \n        \$LogPrint info \$ScriptName (\"Adding A record for \" . \$MacIn\
    Server . \" (\" . \$FullA . \" -> \" . \$LeaseVal->\"active-address\" . \"\
    ).\");\
    \n        /ip/dns/static/add name=\$FullA type=A address=(\$LeaseVal->\"ac\
    tive-address\") ttl=\$Ttl comment=\$Comment place-before=\$PlaceBefore;\
    \n        :if ([ :len \$HostName ] > 0 && [ :len [ /ip/dns/static/find whe\
    re name=\$FullCN type=CNAME ] ] = 0) do={\
    \n          \$LogPrint info \$ScriptName (\"Adding CNAME record for \" . \
    \$MacInServer . \" (\" . \$FullCN . \" -> \" . \$FullA . \").\");\
    \n          /ip/dns/static/add name=\$FullCN type=CNAME cname=\$FullA ttl=\
    \$Ttl comment=\$Comment place-before=\$PlaceBefore;\
    \n        }\
    \n      }\
    \n\
    \n      :if ([ :len [ /ip/dns/static/find where name=\$FullA type=A ] ] > \
    1) do={\
    \n        \$LogPrintOnce warning \$ScriptName (\"The name '\" . \$FullA . \
    \"' appeared in more than one A record!\");\
    \n      }\
    \n    } else={\
    \n      \$LogPrint debug \$ScriptName (\"No address available... Ignoring.\
    \");\
    \n    }\
    \n  }\
    \n} on-error={\
    \n  :global ExitError; \$ExitError \$ExitOK [ :jobname ];\
    \n}\
    \n"
/tool e-mail
set from="Router <mikrotik@REDACTED>" port=587 server=\
    smtp.protonmail.ch tls=yes user=mikrotik@REDACTED
/tool graphing interface
add allow-address=192.168.88.0/24 interface=sfp-sfpplus1
add allow-address=192.168.88.0/24 interface=nas
add allow-address=192.168.88.0/24 interface=switch
/tool graphing resource
add allow-address=192.168.88.0/24
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN
 
User avatar
TheCat12
Member
Member
Posts: 476
Joined: Fri Dec 31, 2021 9:13 pm

Re: Trying to access a Luleey SFP module

Mon Jan 20, 2025 8:10 pm

It's probably the NAT that's messing things up, so try adding the following rule on the top:
/ip firewall nat
add action=src-nat chain=srcnat dst-address=192.168.1.1 out-interface=sfp-sfpplus1 to-addresses=192.168.1.2
 
boxcee
just joined
Topic Author
Posts: 21
Joined: Tue Oct 15, 2024 11:12 am

Re: Trying to access a Luleey SFP module

Tue Jan 21, 2025 7:49 am

Thanks for the prompt reply, but unfortunately, no success. I added your rule, but nothing. No successful ping to 192.168.1.1.
 
User avatar
TheCat12
Member
Member
Posts: 476
Joined: Fri Dec 31, 2021 9:13 pm

Re: Trying to access a Luleey SFP module

Tue Jan 21, 2025 9:45 am

You added it as the first rule, right? Because rules are evaluated from top to bottom
 
boxcee
just joined
Topic Author
Posts: 21
Joined: Tue Oct 15, 2024 11:12 am

Re: Trying to access a Luleey SFP module

Tue Jan 21, 2025 10:12 am

Yes, looks like this:
Bildschirmfoto 2025-01-21 um 09.12.03.png
You do not have the required permissions to view the files attached to this post.
 
boxcee
just joined
Topic Author
Posts: 21
Joined: Tue Oct 15, 2024 11:12 am

Re: Trying to access a Luleey SFP module

Mon Jan 27, 2025 2:52 pm

I didn't have any success with configuring the router. I got a media converter instead and was able to access the SFP module.