🧐 example of automating VLAN creation/removal/inspecting using $mkvlan & friends...

Index to VLAN Automation Scripts in thread

This thread has grown to include a collection of scripts, posted at random points.... Since some post deal with troubleshooting/bugs/commentary. I'll try to maintain this index to the relevant "sub-topics" here.

Please note that all scripts here require at least 7.17+ or some case 7.18+ to work as-is. Some script be adapted to work on older versions. But the goal here was to have "some example" of scripting VLAN stuff. For example, in my uses cases, it's lab/automated testing — generally when needing a lot of VLANs quick. To be clear, it is NOT the goal to be a formal "script library" for VLANs in a live data center, but feel free to take-and-adapt as needed from here.

$mkvlan/$rmvlan/$catvlan... (just below here)
These script deal generally deal with "adding a VLAN" to fresh RouterOS, including mapping IP subnets to VLAN IDs in orderly fashion and adding DHCP stuff. Showing nothing adding, as well as "debugging" in $catvlan, and removal in $rmvlan. There are pretty simple example of "script functions for config" - which easily means you can use function as "macros" for more complex commands, since function can take parameter and other-wise "act like" built-in commands same approach can work for other areas than VLANs.

Hint:
Even if you don't use these scripts, please take it as my strong advice to > have some "IP addressing plan" > when using VLAN (and VRRP, etc .etc). I'm not sure this advice is well captured in the various VLAN guides.

$mktrunk & $rmtrunk...
These script functions will "tag" (or untag) bridge port with a provided [/b] & vice versa with vlan-id= too. Note, there is no "access port" script, see below, but $mkvlan already has example of how to make an "access port" by setting PVID on /interface/bridge/port, using the "real" command seems appropriate, i.e.

EX: So to make 'ether3' an access point, only the following additional command is:
/interface/bridge/port set [find interface=ether3] pvid=123 frame-types=allow-only-untagged

i.e. IMO no need to wrap something if just one command, so above makes an a bridge port an "access port", at least in 7.16+. The UI allow same change using PVID under VLAN section of dialog for a port under Bridge > Port. Prior version would also require the bridge port be marked as tagged= too, so for "setting an access port" it's may be more than one command prior to 7.16+.

Note:
There is no $lsvlan in the UNIX® naming style for this collection. This is the "missing script" since that show summary of IP address/routing/etc for all VLANs, basically the missing $lsvlan be the Layer 3 compliment to the $lsbridge's Layer 2 view. I have some version of one that I'll get around to posting, but it won't be as pretty as $lsbridge as my idea for $lsvlan is as a way to "export VLANs to Excel".

$lsbridge for "inspecting" VLAN bridging...
To use this one, please see the instructions listed in Post #33 which has more details. But here is what $lsbridge will visualize for a simple bridge on KNOT:




$mkvlan/$rmvlan/$catvlan Examples

First, these function will only work on 7.17+, and must have a /interface/bridge with vlan-filtering=yes enabled.

The "functions" below wrap operations around VLANs – including DHCP server, interface list, address-list, etc. They employ a few "scripting tricks" internally (including <%% operator ) and other newer syntax features for example (:grep, :convert, :serialize, export where, etc).

For example, to create a new VLAN:
$mkvlan
...and working VLAN should be added and you simply pvid= on a port in /interface/bridge/port

To "dump" the various config:
$catvlan
...which will do various "print" to output the running state.

And to remove a VLAN that was created using $mkvlan:
$rmvlan

It will automatically assign a unique IPv4 subnet based the PVID provided. So as part of the operation, another function name $pvid2array is used to calculate the various IP things needed to create things. It can be used separately to "calculate" unique subnets – but have some known scheme to generate the various IP prefix/address/pool/etc things. Another function $prettyprint , included in code below, is used to format the output. In all case, $pvid2array will always generated the SAME subnet from SAME PVID/vlan-id, so it's safe to call multiple times to get same data.

For example, $prettyprint [$pvid2array 18] will generate the following (which is used internally by $mkvlan):

{
"basename": "vlan18",
"cidraddr": "192.168.18.1/24",
"cidrnet": "192.168.18.0/24",
"commenttag": "#autovlan 18",
"dhcpdns": "192.168.18.1",
"dhcpgw": "192.168.18.1",
"dhcppool": "192.168.18.10-192.168.18.249",
"ipprefix": "192.168.18",
"routerip": "192.168.18.1",
"vlanbridge": "bridge",
"vlanid": 18
}

Internally $pvid2array generates a 172.x.y.0/24 address for PVID > 256... So picking a random vlan-id above 256, you can see the difference.
$prettyprint [$pvid2array [:rndnum from=257 to=4094]]

{
"basename": "vlan2668",
"cidraddr": "172.25.108.1/24",
"cidrnet": "172.25.108.0/24",
"commenttag": "#autovlan 2668",
"dhcpdns": "172.25.108.1",
"dhcpgw": "172.25.108.1",
"dhcppool": "172.25.108.10-172.25.108.249",
"ipprefix": "172.25.108",
"routerip": "172.25.108.1",
"vlanbridge": "bridge",
"vlanid": 2668
}

To use them, you need add /system/script named "autovlan" with a cut-and-paste of following code to the source= & then from Terminal run "/system/script run autovlan" which will allow $mkvlan, $catvlan, $rmvlan, $prettyprint, and $pvid2array function from the Terminal:

VLAN Script — $mkvlan & friend

# USER SETTINGS
# autovlanstyle - overrides the default IP addressing scheme,
#                 valid values: "bytes", "bytes10", or "split10"
:global autovlanstyle
#       i.e. by adding a valid style to end of above, like this: 
# :global autovlanstyle "split10"


# FUNCTIONS AND CODE
:global pvid2array do={
    # process formating
    :global autovlanstyle
    :local schema "bytes"
    :if ([:typeof $style] = "str") do={
        :if ($style~"bytes|bytes10|split10") do={} else={
            :error "$0 style= must be either bytes | bytes10 | split10 "
        }
        :set schema $style
    } 
    :if ([:typeof $autovlanstyle] = "str") do={
        :if ($autovlanstyle~"bytes|bytes10|split10") do={
            :set schema $autovlanstyle
        }
    }

    # move first argument to function, the PVID, to a variable
    :local vlanid [:tonum $1]
    
    # check it PVID is valid, if not show help and error (which exits script)
    :if ([:typeof $vlanid] != "num" || $vlanid < 2 || $vlanid > 4094) do={
        :error "PVID must be valid as first argument to command function" 
    }
    
    # find the bridge interface ...
    :local bridgeid [/interface bridge find vlan-filtering=yes]
    :if ([:len $bridgeid] != 1) do={
        :error "A bridge with vlan-filtering=yes is required, and there can be only one for this script."
    }

    # uses :convert to break pvid into array with 2 elements between 0-256
    :local vlanbytes [:convert from=num to=byte-array $vlanid]  
    :local lowbits ($vlanbytes->0)
    :local highbits ($vlanbytes->1)

    # UGLY workaround for MIPSBE/other, detected when we don't get two parts from the vlan-id
    :if ([:len $vlanbytes]>2) do={
        :if ($vlanid > 255) do={
            # even worse workaround, normalize to 8 bytes - ros wrongly trims leading 0
            :if ([:len $vlanbytes]=7) do={ 
                # make it len=8 by pre-pending a 0 - so the swap below is correct
                :set vlanbytes (0,$vlanbytes) 
            }
            # now swap the high and low bytes
            :set lowbits ($vlanbytes->1)
            :set highbits ($vlanbytes->0)  
        } 
        # lowbits is right if under 256
    }

    :local ipprefix "0.0.0"
    :if ($schema = "bytes") do={
        # for pvid below 257, use 192.168.<pvid>.0/24 as base IP prefix
        # for others map pvid into unique /24 with 172.<lowbits+15>.<highbits>.0/24
        :if ($vlanid < 256) do={
            :set ipprefix "192.168.$vlanid"
        } else={
            :set ipprefix "172.$($lowbits + 15).$highbits" 
        }
    }
    :if ($schema = "bytes10") do={
        # map pvid into unique /24 with 10.<lowbits>.<highbits>.0/24
        :if ($vlanid < 256) do={
            :set ipprefix "10.0.$vlanid"
        } else={
            :set ipprefix "10.$($lowbits+1).$highbits" 
        }
    }
    :if ($schema = "split10") do={
        :if ($vlanid < 100) do={
            :set ipprefix "10.0.$vlanid" 
        } else={
            :set ipprefix "10.$[:tonum [:pick $vlanid 0 ([:len $vlanid]-2)]].$[:tonum [:pick $vlanid ([:len $vlanid]-2) [:len $vlanid]]]"
        }
    }

    # now calculate the various "formats" of a prefix for use in other scripts
    :return {
        "vlanid"="$vlanid";
        "basename"="vlan$vlanid";
        "commenttag"="mkvlan $vlanid";
        "vlanbridge"="$[/interface/bridge get $bridgeid name]";
        "ipprefix"="$ipprefix";
        "cidrnet"="$ipprefix.0/24";
        "cidraddr"="$ipprefix.1/24";
        "routerip"="$ipprefix.1"; 
        "dhcppool"="$ipprefix.10-$ipprefix.249"; 
        "dhcpgw"="$ipprefix.1"; 
        "dhcpdns"="$ipprefix.1"
    }
}


:global prettyprint do={
    :if ([:typeof $1]="nothing") do={
        :put "usage: $0 <data> - print provided <data>, including arrays, in a pretty format"
        :put "example: $0 {\"num\"=1;\"str\"=\"text\";\"float\"=\"0.123\"}"
        :error
    }
    :put [:serialize to=json options=json.pretty $1]
    :return $1
}


:global mkvlan do={
    :global pvid2array
    :global mkvlan

    :if ([:typeof [:tonum $1]]="num") do={
        :global mkvlan 
        :return ($mkvlan <%% [$pvid2array [:tonum $1]])
    }

    :put "starting VLAN network creation for $cidrnet using id $vlanid ..."
    
    :put " - adding $basename interface on $vlanbridge using vlan-id=$vlanid"
    /interface vlan add vlan-id=$vlanid interface=$vlanbridge name=$basename comment=$commenttag

    :put " - assigning IP address of $cidraddr for $basename"
    /ip address add interface=$basename address=$cidraddr comment=$commenttag

    :put " - adding IP address pool $dhcppool for DHCP"
    /ip pool add name=$basename ranges=$dhcppool comment=$commenttag

    :put " - adding dhcp-server $basename "
    /ip dhcp-server add address-pool=$basename disabled=no interface=$basename name=$basename comment=$commenttag 

    :put " - adding DHCP /24 network using gateway=$dhcpgw and dns-server=$dhcpdns"
    /ip dhcp-server network add address=$cidrnet gateway=$dhcpgw dns-server=$dhcpdns comment=$commenttag 

    :put " - add VLAN network to interface LAN list"
    :if ([:len [/interface list find name=LAN]] = 1) do={
        /interface list member add list=LAN interface=$basename comment=$commenttag 
    }

    :put " - create FW address-list for VLAN network for $cidrnet"
    /ip firewall address-list add list=$basename address=$cidrnet comment=$commenttag  

    :put " * NOTE: in 7.16+, the VLAN $vlanid is dynamically added to /interface/bridge/vlans with tagged=$vlanbridge "
    :put "         thus making an access port ONLY involves setting pvid=$vlanid on a /interface/bridge/port"
    :put " * EX:   So to make 'ether3' an access point, only the following additional command is:"
    :put "           /interface/bridge/port set [find interface=ether3] pvid=$vlanid frame-types=allow-only-untagged"

    /log info [:put "VLAN network created for $cidrnet for vlan-id=$vlanid"]
}


:global rmvlan do={
    :global pvid2array
    :global rmvlan
    :local tag "INVALID"
    :if ([:typeof [:tonum $1]]="num") do={
        :global rmvlan 
        :return ($rmvlan <%% [$pvid2array [:tonum $1]])
    }
    :if ([:typeof $comment]="str") do={
        :set tag $comment
    } else={
        :if ([:typeof $commenttag]="str") do={
            :set tag $commenttag 
        } else={
            :error "$0 requires with an tag provided by '$0 comment=mytag' or via '($0 <%% [$pvid2array 1001]"
        }
    }

    :put "starting VLAN network removal for comment=$tag"
    :put " - remove $basename interface on $vlanbridge using vlan-id=$vlanid"
    /interface vlan remove [find comment=$tag]

    :put " - remove IP address of $cidraddr for $basename"
    /ip address remove [find comment=$tag]

    :put " - remove IP address pool $dhcppool for DHCP"
    /ip pool remove [find comment=$tag]

    :put " - removing dhcp-server $basename "
    /ip dhcp-server remove [find comment=$tag] 

    :put " - remove DHCP /24 network using gateway=$dhcpgw and dns-server=$dhcpdns"
    /ip dhcp-server network remove [find comment=$tag] 

    :put " - remove VLAN network to interface LAN list"
    /interface list member remove [find comment=$tag] 

    :put " - create FW address-list for VLAN network for $cidrnet"
    /ip firewall address-list remove [find comment=$tag]  

    /log info [:put "VLAN network removed for comment=$tag"]
}



:global catvlan do={
    :global pvid2array
    :global catvlan
    :global prettyprint
    :local tag "INVALID"
    :local json [:toarray ""]
    :if ([:typeof [:tonum $1]]="num") do={
        :return [($catvlan <%% [$pvid2array [:tonum $1]])]
    }
    :if ([:typeof $comment]="str") do={
        :set tag $comment
    } else={
        :if ([:typeof $commenttag]="str") do={
            :set tag $commenttag 
        } else={
            :error "$0 requires with an tag provided by '$0 comment=mytag' or via '($0 <%% [$pvid2array 1001]"
        }
    }

    :set ($json->"/interface/vlan") [/interface vlan print detail as-value where comment=$tag]
    :set ($json->"/ip/address") [/ip address print detail as-value where comment=$tag]
    :set ($json->"/ip/pool") [/ip pool print detail as-value where comment=$tag]
    :set ($json->"/ip/dhcp-server") [/ip dhcp-server print detail as-value where comment=$tag] 
    :set ($json->"/ip/dhcp-server/network") [/ip dhcp-server network print detail as-value where comment=$tag] 
    :set ($json->"/interface/list/member") [/interface list member print detail as-value where comment=$tag]
    :set ($json->"/ip/firewall/address-list") [/ip firewall address-list print detail as-value where comment=$tag] 
    


    # This logic to use "export where" does not work, and causes wierd bug - disabling for now
    #:put "VLAN network config..."
    #:put ""
    #[:parse ":grep pattern=\"^/\" script={/interface/vlan/export terse where comment=\"$tag\"} "];
    #[:parse ":grep pattern=\"^/\" script={/ip/address/export terse where comment=\"$tag\"} "];
    #[:parse ":grep pattern=\"^/\" script={/ip/pool/export terse where comment=\"$tag\"} "];
    #[:parse ":grep pattern=\"^/\" script={:grep pattern=\"lease-time\" script={/ip/dhcp-server/export terse where comment=\"$tag\"}} "];
    #[[:parse ":grep pattern=\"^/\" script={/ip/dhcp-server/network/export terse where comment=\"$tag\"} "]]; 
    #[[:parse ":grep pattern=\"^/\" script={/interface/list/member/export terse where comment=\"$tag\"} "]];
    #[[:parse ":grep pattern=\"^/\" script={/ip firewall address-list/export terse where comment=\"$tag\"} "]]; 
    #:put ""

    :return [$prettyprint $json]
}

So after adding the "autovlan" functions above to a /system/script...you can use them like so

/system/script/run autovlan
$mkvlan 123

output

starting VLAN network creation for 192.168.123.0/24 using id 123 ...
- adding vlan123 interface on bridge using vlan-id=123
- assigning IP address of 192.168.123.1/24 for vlan123
- adding IP address pool 192.168.123.10-192.168.123.249 for DHCP
- adding dhcp-server vlan123
- adding DHCP /24 network using gateway=192.168.123.1 and dns-server=192.168.123.1
- add VLAN network to interface LAN list
- create FW address-list for VLAN network for 192.168.123.0/24
* NOTE: in 7.16+, the VLAN 123 is dynamically added to /interface/bridge/vlans with tagged=bridge
thus making an access port ONLY involves setting pvid=123 on a /interface/bridge/port
* EX:   So to make 'ether3' an access point, only the following additional command is:
/interface/bridge/port set [find interface=ether3] pvid=123 frame-types=allow-only-untagged
VLAN network created for 192.168.123.0/24 for vlan-id=123

To confirm it created it use:

$catvlan 123

VLAN network debugging...

- /interface/vlan
[
{
".id": "*138",
"arp": "enabled",
"arp-timeout": "auto",
"comment": "mkvlan 123",
"interface": "bridge",
"l2mtu": 1588,
"loop-protect": "default",
"loop-protect-disable-time": "1970-01-01 00:05:00",
"loop-protect-send-interval": "1970-01-01 00:00:05",
"loop-protect-status": "off",
"mac-address": "74:4D:28:38:B3:CD",
"mtu": 1500,
"mvrp": false,
"name": "vlan123",
"use-service-tag": false,
"vlan-id": 123
}
]
- /ip/address
[
{
".id": "*120",
"actual-interface": "vlan123",
"address": "192.168.123.1/24",
"comment": "mkvlan 123",
"interface": "vlan123",
"network": "192.168.123.0"
}
]
- /ip/pool
[
{
".id": "*13",
"comment": "mkvlan 123",
"name": "vlan123",
"ranges": [
"192.168.123.10-192.168.123.249"
]
}
]
- /ip/dhcp-server
[
{
".id": "*F",
"address-lists": [],
"address-pool": "vlan123",
"comment": "mkvlan 123",
"interface": "vlan123",
"lease-script": "",
"lease-time": "1970-01-01 00:30:00",
"name": "vlan123",
"use-radius": "no"
}
]
- /ip/dhcp-server/network
[
{
".id": "*F",
"address": "192.168.123.0/24",
"caps-manager": [],
"comment": "mkvlan 123",
"dhcp-option": [],
"dns-server": "192.168.123.1",
"gateway": [
"192.168.123.1"
],
"ntp-server": [],
"wins-server": []
}
]
- /interface/list/member
[
{
".id": "*6F",
"comment": "mkvlan 123",
"dynamic": false,
"interface": "vlan123",
"list": "LAN"
}
]
- /ip firewall address-list
[
{
".id": "*1D",
"address": "192.168.123.0/24",
"comment": "mkvlan 123",
"creation-time": "2025-01-24 16:31:46",
"dynamic": false,
"list": "vlan123"
}
]

VLAN network config...

/interface vlan add comment="mkvlan 123" interface=bridge name=vlan123 vlan-id=123
/ip address add address=192.168.123.1/24 comment="mkvlan 123" disabled=no interface=vlan123 network=192.168.123.0
/ip pool add comment="mkvlan 123" name=vlan123 ranges=192.168.123.10-192.168.123.249
/ip dhcp-server add address-lists="" address-pool=vlan123 comment="mkvlan 123" disabled=no interface=vlan123 lease-script="" lease-time=30m n
ame=vlan123 use-radius=no
/ip dhcp-server network add address=192.168.123.0/24 caps-manager="" comment="mkvlan 123" dhcp-option="" dns-server=192.168.123.1 gateway=192
.168.123.1 !next-server ntp-server="" wins-server=""
/interface list member add comment="mkvlan 123" disabled=no interface=vlan123 list=LAN
/ip firewall address-list add address=192.168.123.0/24 comment="mkvlan 123" disabled=no dynamic=no list=vlan123

And to remove it,

$rmvlan 123

output

starting VLAN network removal for comment=mkvlan 123
- remove vlan123 interface on bridge using vlan-id=123
- remove IP address of 192.168.123.1/24 for vlan123
- remove IP address pool 192.168.123.10-192.168.123.249 for DHCP
- removing dhcp-server vlan123
- remove DHCP /24 network using gateway=192.168.123.1 and dns-server=192.168.123.1
- remove VLAN network to interface LAN list
- create FW address-list for VLAN network for 192.168.123.0/24
VLAN network removed for comment=mkvlan 123

And to confirm it's deleted another $catvlan 123 would show this after removal:

VLAN network debugging...

- /interface/vlan
[]
- /ip/address
[]
- /ip/pool
[]
- /ip/dhcp-server
[]
- /ip/dhcp-server/network
[]
- /interface/list/member
[]
- /ip firewall address-list
[]

VLAN network config...

Feel free to modified the scripts to customize how the VLAN gets created and/or how IP prefix/etc gets calculated, add VRRP to VLANs, etc. Assume you added the function to a /system/script named "autovlan"... if you want to EDIT the file, it really best to do that in the Terminal editor (Ctrl-O will save, Ctrl-C will exit without saving, Ctrl-A goes to begin of line, Ctrl-E goes to line end, 2x Ctrl-E goes to end of doc)

Here are some example usages:

/system/script/edit autovlan source

The reason for this recommendation is the Terminal's edit will do syntax-coloring — so error will be see. If you use winbox or webfig to edit script, no realtime checking is done (and the proportional font make it hard to read too).

Versions
v1.4 - cleanup after import, no change other than formatting
v1.3 - handle buggy [:convert to=byte-array] on MIPSBE, see post #20
v1.2 - fix logic error when autovlanstyle is unset
v1.1 - fix when running as /system/script; remove catvlan export where; new ip subnet schemes; minor cleanup
v1.0 - initial

Well done Amm0.

Bookmarked for future use! :folded_hands:

It could still use cleanup, and alternative “numbering plans”…

Despite my commentary on the forum on esoteric scripting topics… my business use for scripting is automating doing config, or output setting without running a dozen CLI commands. Yet there actually aren’t a lot of example of automating one of more common topics like VLAN. So thought I start a discussion about it…

I realized it should have some choice of "style" for how the PVID/vlan-id is converted to an IP address. And, just note, you can always use that part of the $pvid2array and $prettyprint code without the rest of the automatic config stuff, like you want to a way to pick a subnet for a VLAN

Currently only one "style" is used, but I'm thing of adding some option to use a different scheme to convert the PVID/vlan-id into a subnet.

The current scheme is what I'm calling "bytes" – that will use 192.168.<0-254>.0 for VLAN IDs below 255. Above 255 to max of 4094, a 172.<16-30>.<0-254>.0 is used. Keep in mind in express in HEX would look like 0x0ABB – for the 172.0x0A.0xBB.0 (with the 0x0A being added by 0x0F... which makes it (base10) range 16-32. (With newer ":convert to=byte-array" breaking up the PVID in 2 parts).

Basically if always use VLAN ID below 255, you'll get a 192.168. to keep things one-to-one between subnet part and VLAN ID. Since the "bytes" does get confusing...

style=bytes
$prettyprint [$pvid2array style=bytes 99]
{
"cidrnet": "192.168.99.0/24",
"vlanid": 99
}
$prettyprint [$pvid2array style=bytes 1234]
{
"cidrnet": "172.19.210.0/24",
"vlanid": 1234
}

bytes are still linear :wink:... so for 123**5** it's still the next /24 subnet:

$prettyprint [$pvid2array style=bytes 123**5]
{
"cidrnet": "172.19.21
1**.0/24",
"vlanid": 1235
}

The newer schemes I came up with are "split10" and "bytes10", which use 10.x.x.x.

The "split10" uses the base10 parts of the PVID to make the IP address... so VLAN 123 is 10.1.23 - this skeps the binary/hex and make the VLAN readable just from the subnet.

style=split10
$prettyprint [$pvid2array style=split10 99]
{
"cidrnet": "10.0.99.0/24",
"vlanid": 99
}
$prettyprint [$pvid2array style=split10 1234]
{
"cidrnet": "10.12.34.0/24",
"vlanid": 1234
}

Likely less useful than above, the same scheme use in "default" bytes scheme, except it ALWAYS uses the 10.x.x.x private range:

style=bytes10
$prettyprint [$pvid2array style=bytes10 99]
{
"cidrnet": "10.0.99.0/24",
"vlanid": 99
}
$prettyprint [$pvid2array style=bytes10 1234]
{
"cidrnet": "10.5.210.0/24",
"vlanid": 1234
}


Anyway, if anyone has suggestion/improvement/bugs LMK here. I'll probably update the script with minor cleanup this weekend. And likely some "$lsvlan" to print a table of the various config (IP, dhcp, etc) for each VLAN interface in one list...

As a VLAN outsider, this all looks excellent to the point that I can see myself directing newbies at it and saying, “Just do it this way.” Save the fiddly details for those who actually need to know them.

One thing I don’t see, though: a canned way to reliably create the “management” VLAN without locking yourself out.

Hi,

as this looks promissing I gave it a try on my 7.18b2 test RB962
it gives me errors …

[eddie@hap] > /system/script/run autovlan
[eddie@hap] > $mkvlan 60
not an array
[eddie@hap] > $catvlan 99
not an array
[eddie@hap] >

vlan 99 already exits on that router.
it does not create or shows a vlan
suggestions ?

It turns out some code in catvlan might not work when run from /system/script (but did from when cut-and-paste code to CLI). I already had some other minor updates, so a posted a new version. I tested when run after loading from a /system/script – so it should work now. There is a new global “autovlanstyle” in this version that can control how address are generated (i.e. ‘split10’ would use 10.12.34.0/24 for vlan-id 1234, 10.0.2.0/24 for vlan-id 2), which I’ll explain how to use soon…

Also “$catvlan” will only work VLANs that are automatically generated by “$mkvlan”. So the $catvlan 60 would not have worked, but you ran into a bug that happens when functions are run from /system/script before you got to that one :wink:.

I have a “$lsvlan” that I should post (but need to test it) – which does list ALL VLANs (and associated DHCP/IP/etc stuff in one table). This one is actually more complex, since the trick with mkvlan/rmvlan is using comment= to align everything - but that’s not possible for an non-automated VLAN, so it take more scripting to build some “combined view” of ANY arbitrary VLAN.

Yeah I’m working towards that… As you see above, there might be bugs :wink:

And likely more cleanup/+options on the mkvlan/rmvlan/catvlan are need here. And still like to combine these my “TUI scripts” like $INQUIRE to have some holistic “scripted setup” – that prompt for some details before advocating this approach for newbies yet. For setting up test routers, it’s more handy today….

I have thought about a 7.16+ version @pcunite’s VLAN/multiwan guide, using scripting functions. Mikrotik has done good work in scripting – and VLAN bridging – of late… so some of these things are easier. i.e. “someone” puts in feature requests for the esoteric things seen in the “console -” release notes. I have a lot script functions, but I post them as way to force me to clean them up :wink: but the writeup takes longer (as I’m sure you know)…


Now that’s a different topic… I have thought about writing my own setup guide – at least for 7.16+. The approach I take – which may be controversial – is a few things different:

  1. Leave the any defconf untouched by scripts. So that means leaving IP address on bridge interface itself. Potentially setting the basics on QuickSet screen. If you want VLANs, you can use bridge interface for the management VLAN, instead of “LAN”. This also allows QuickSet to set the some management or any IP LAN safely. This tracks what UBNT does with its hybrid port scheme, which I think is reasonable. Basically… LAN are always VLAN, management port is the bridge interface. And these functions never touch comment=defconf - which is what makes QuickSet unsafe - so leave that stuff alone…

  2. Set “/interface/bridge/set [find] vlan-filtering=yes” as the FIRST thing – not @pcunite’s last thing. Reason the bridge’s pvid=1, and bridge ports are also pvid=1 – and RouterOS will automatically untag the bridge interface on those port WITHOUT any additional config. All just works. The only side effect is winbox MAY drop the connection, but will reconnect it quickly. My belief is this is safer to at the start with a default configuration and NO change – since it’s known harmless at that point. While setting vlan-filtering=yes at end of configuration mean any mistake will cause you do lose access – now this why the “off-bridge management port” is often recommended – but this has it’s drawbacks in that adds more complexity (and same risk for config error too) with only benefit of avoiding using bridge interface for L3 (or perhaps if you had complex topology than some LANs-as-VLANs).

  3. In 7.16+, there is NO need to explicitly set tagged=bridge for a vlan-id in /interface/bridge/vlan - that happens automatically. So some of the explaining just goes away as result. This leave ONLY setting /interface/bridge/port’s pvid= to make an access port. Only for trunk or hybrid ports is adding a vlan-id to /interface/bridge/vlan needed, and then it’s adding an trunk ports as tagged=. Basically there is a lot dynamic bridge configuration that happens that make it WAY easier to script (or even do “by hand”) that’s all new since 7.15/7.16. None of the guides really reflect this more streamlined process for bridge ports.

In addition, part of @pcunite’s guide covers multiple routers/switches/APs. One thing I’ve thought is this is where MVRP might be a useful addition – although I don’t handle it script above today. But MVRP is just a couple settings to enable and should enable “syncing VLANs” when right bridge setting. Also I’m also a fan of VRRP, so if you have multiple routers that should be covered since for LAN-side VLAN’s it’s actually pretty trivial configuration — and why I have a separate IP subnet generation function since VRRP add more variant IP things. To @pcunite’s multiwan I’d probably add some tangent about using LTE in routed mode with VRRP (instead of LTE passthrough) for some cases…but that’s a different topic.

Anyway more thinking than actions at this point, but building been requisite parts one function at time for a bit, so getting closer.

tnx for the arry fix, that one seems solved after I replaced the code in /system/scripts/autovlan and ran it …
but it creates a strange network …

[eddie@hap] > $mkvlan 60
starting VLAN network creation for 0.0.0.0/24 using id 60 ...
 - adding vlan60 interface on bridge using vlan-id=60
 - assigning IP address of 0.0.0.1/24 for vlan60
 - adding IP address pool 0.0.0.10-0.0.0.249 for DHCP
 - adding dhcp-server vlan60
 - adding DHCP /24 network using gateway=0.0.0.1 and dns-server=0.0.0.1
 - add VLAN network to interface LAN list
 - create FW address-list for VLAN network for 0.0.0.0/24
 * NOTE: in 7.16+, the VLAN 60 is dynamically added to /interface/bridge/vlans with tagged=bridge
         thus making an access port ONLY involves setting pvid=60 on a /interface/bridge/port
 * EX:   So to make 'ether3' an access point, only the following additional command is:
           /interface/bridge/port set [find interface=ether3] pvid=60 frame-types=allow-only-untagged
VLAN network created for 0.0.0.0/24 for vlan-id=60

[eddie@hap] > $rmvlan 60
starting VLAN network removal for comment=mkvlan 60
 - remove vlan60 interface on bridge using vlan-id=60
 - remove IP address of 0.0.0.1/24 for vlan60
 - remove IP address pool 0.0.0.10-0.0.0.249 for DHCP
 - removing dhcp-server vlan60
 - remove DHCP /24 network using gateway=0.0.0.1 and dns-server=0.0.0.1
 - remove VLAN network to interface LAN list
 - create FW address-list for VLAN network for 0.0.0.0/24
VLAN network removed for comment=mkvlan 60

My bad. And thanks for testing!!! I updated the script, v1.2 with a fix.

There was a new global, “autovlanstyle” also add in the update. But it had a logic error there but only when autovlanstyle is empty — and I didn’t notice since I had it set which avoid the logic issue. Basically I had a else={} block when it should have be a do={} block in the $pvid2array that generates the IP address

:if ($autovlanstyle~“bytes|bytes10|split10”) > do={} else=> {
:set schema $autovlanstyle
}

should have been

:if ($autovlanstyle~“bytes|bytes10|split10”) do={> } else=> {
:set schema $autovlanstyle
}

Anyway, you should be able to update the script from top post (v1.2), which has that fix, and try again — $pvid2array will JUST show what be used:

/system/script/run autovlan
$prettyprint [$pvid2array 60]



{
“basename”: “vlan60”,
“cidraddr”: “192.168.60.1/24”,
“cidrnet”: “192.168.60.0/24”,
“commenttag”: “mkvlan 60”,
“dhcpdns”: “192.168.60.1”,
“dhcpgw”: “192.168.60.1”,
“dhcppool”: “192.168.60.10-192.168.60.249”,
“ipprefix”: “192.168.60”,
“routerip”: “192.168.60.1”,
“vlanbridge”: “bridge”,
“vlanid”: 60
}

Also, you can set the “IP generation style” use the $autovlanstyle global which control what scheme is used to generate IPs - this uses the “split10” scheme:

:set autovlanstyle split10
$prettyprint [$pvid2array 60]



{
“basename”: “vlan60”,
“cidraddr”: “10.0.60.1/24”,
“cidrnet”: “10.0.60.0/24”,
“commenttag”: “mkvlan 60”,
“dhcpdns”: “10.0.60.1”,
“dhcpgw”: “10.0.60.1”,
“dhcppool”: “10.0.60.10-10.0.60.249”,
“ipprefix”: “10.0.60”,
“routerip”: “10.0.60.1”,
“vlanbridge”: “bridge”,
“vlanid”: 60
}

And the “vlanautostyle” effects the $mkvlan too (both the :set and /sys/script/run are only need once per terminal):

/system/script/run autovlan
:set autovlanstyle split10
$mkvlan 60
$catvlan 60



starting VLAN network creation for 10.0.60.0/24 using id 60 …

  • adding vlan60 interface on bridge using vlan-id=60
  • assigning IP address of 10.0.60.1/24 for vlan60
  • adding IP address pool 10.0.60.10-10.0.60.249 for DHCP
  • adding dhcp-server vlan60
  • adding DHCP /24 network using gateway=10.0.60.1 and dns-server=10.0.60.1
  • add VLAN network to interface LAN list
  • create FW address-list for VLAN network for 10.0.60.0/24
  • NOTE: in 7.16+, the VLAN 60 is dynamically added to /interface/bridge/vlans with tagged=bridge
    thus making an access port ONLY involves setting pvid=60 on a /interface/bridge/port
  • EX: So to make ‘ether3’ an access point, only the following additional command is:
    /interface/bridge/port set [find interface=ether3] pvid=60 frame-types=allow-only-untagged
    VLAN network created for 10.0.60.0/24 for vlan-id=60



    {
    “/interface/list/member”: [
    {
    “.id”: “*79”,
    “comment”: “mkvlan 60”,
    “dynamic”: false,
    “interface”: “vlan60”,
    “list”: “LAN”
    }
    ],
    “/interface/vlan”: [
    {
    “.id”: “*142”,
    “arp”: “enabled”,
    “arp-timeout”: “auto”,
    “comment”: “mkvlan 60”,
    “interface”: “bridge”,
    “l2mtu”: 1588,
    “loop-protect”: “default”,
    “loop-protect-disable-time”: “1970-01-01 00:05:00”,
    “loop-protect-send-interval”: “1970-01-01 00:00:05”,
    “loop-protect-status”: “off”,
    “mac-address”: “74:4D:28:38:B3:CD”,
    “mtu”: 1500,
    “mvrp”: false,
    “name”: “vlan60”,
    “use-service-tag”: false,
    “vlan-id”: 60
    }
    ],
    “/ip/address”: [
    {
    “.id”: “*12A”,
    “actual-interface”: “vlan60”,
    “address”: “10.0.60.1/24”,
    “comment”: “mkvlan 60”,
    “interface”: “vlan60”,
    “network”: “10.0.60.0”
    }
    ],
    “/ip/dhcp-server”: [
    {
    “.id”: “*1A”,
    “address-lists”: ,
    “address-pool”: “vlan60”,
    “comment”: “mkvlan 60”,
    “interface”: “vlan60”,
    “lease-script”: “”,
    “lease-time”: “1970-01-01 00:30:00”,
    “name”: “vlan60”,
    “use-radius”: “no”
    }
    ],
    “/ip/dhcp-server/network”: [
    {
    “.id”: “*19”,
    “address”: “10.0.60.0/24”,
    “caps-manager”: ,
    “comment”: “mkvlan 60”,
    “dhcp-option”: ,
    “dns-server”: “10.0.60.1”,
    “gateway”: [
    “10.0.60.1”
    ],
    “ntp-server”: ,
    “wins-server”:
    }
    ],
    “/ip/firewall/address-list”: [
    {
    “.id”: “*27”,
    “address”: “10.0.60.0/24”,
    “comment”: “mkvlan 60”,
    “creation-time”: “2025-01-27 05:12:08”,
    “dynamic”: false,
    “list”: “vlan60”
    }
    ],
    “/ip/pool”: [
    {
    “.id”: “*B”,
    “comment”: “mkvlan 60”,
    “name”: “vlan60”,
    “ranges”: [
    “10.0.60.10-10.0.60.249”
    ]
    }
    ]
    }

If you want to change the default IP address scheme, just change the variable “autovlanstyle” at the top of the script on your router, so the line:

:global autovlanstyle

becomes

:global autovlanstyle “split10”

to cause the scheme to be 10**.<pvid[3]><pvid[2]>.<pvid[1]><pvid[0]>.**0/24 - which use the base10 vlan-id, while the default uses base16 split subnets.

almost …

[eddie@hap] > $prettyprint [$pvid2array 60]
{
    "basename": "vlan60",
    "cidraddr": "172.75.0.1/24",
    "cidrnet": "172.75.0.0/24",
    "commenttag": "mkvlan 60",
    "dhcpdns": "172.75.0.1",
    "dhcpgw": "172.75.0.1",
    "dhcppool": "172.75.0.10-172.75.0.249",
    "ipprefix": "172.75.0",
    "routerip": "172.75.0.1",
    "vlanbridge": "bridge",
    "vlanid": 60
}

[eddie@hap] > $mkvlan 60
starting VLAN network creation for 172.75.0.0/24 using id 60 ...
 - adding vlan60 interface on bridge using vlan-id=60
 - assigning IP address of 172.75.0.1/24 for vlan60
 - adding IP address pool 172.75.0.10-172.75.0.249 for DHCP
 - adding dhcp-server vlan60
 - adding DHCP /24 network using gateway=172.75.0.1 and dns-server=172.75.0.1
 - add VLAN network to interface LAN list
 - create FW address-list for VLAN network for 172.75.0.0/24
 * NOTE: in 7.16+, the VLAN 60 is dynamically added to /interface/bridge/vlans with tagged=bridge
         thus making an access port ONLY involves setting pvid=60 on a /interface/bridge/port
 * EX:   So to make 'ether3' an access point, only the following additional command is:
           /interface/bridge/port set [find interface=ether3] pvid=60 frame-types=allow-only-untagged
VLAN network created for 172.75.0.0/24 for vlan-id=60

Hmm. Somehow the “highbits” in vlan-id are NOT nothing, which is how you ended up with that. But I don’t know how yet…
I do know the “75” comes from that it thinks highbits are the lowbits, so it’s adding +16 to get the 172 address to 172.16-31 - but it should NOT be using a 172 address, instead it SHOULD be get 192.168.60.0/24.

Can you try clearing out the variables using the following & then re-run “/system/script/run autovlan”?

:global mkvlan
:global catvlan
:global rmvlan
:global prettyprint 
:global pvid2array
:global autovlanstyle
:set mkvlan
:set catvlan
:set rmvlan
:set prettyprint 
:set pvid2array
:set autovlanstyle
/system/script/run autovlan
$prettyprint [$pvid2array 60]

If that does NOT work, can you try the following script at the TerminalCLI (and NOT in /system/script) & paste the result here. And to confirm you’re using 7.18beta? – which is what I’m testing too – so this should work.

{
    :put "== use :convert to make a byte-array so we can get at the IP parts"
    :local vbytes [:convert from=num to=byte-array 60]
     :local lowbits ($vbytes->0)
    :local highbits ($vbytes->1)
    :local ipprefix "0.0.0"
    :put "\t... got $[:tostr $vbytes]"

    :put "== verify :convert is working as expected"
    :put  "\tHIGH: $[:typeof ($vbytes->1)] $[:len ($vbytes->1)] $($vbytes->1)"
    :put  "\tLOW: $[:typeof ($vbytes->0)] $[:len ($vbytes->0)] $($vbytes->0)"
    :put "\t** HIGH should be 'nothing' - if it something, thats a bug" 

    :put "== replicate SAME code in pvid2array"  
    :if ([:typeof $highbits] = "nothing") do={
        :set ipprefix "192.168.$lowbits"
    } else={
        :set ipprefix "172.$($lowbits + 15).$highbits" 
    }
    :put "\t... which gets a prefix of: $ipprefix" 

    :put "== use DIFFERENT code in pvid2array to check instead for !num"  
    :if ([:typeof $highbits] != "num") do={
        :set ipprefix "192.168.$lowbits"
    } else={
        :set ipprefix "172.$($lowbits + 15).$highbits" 
    }
    :put "\t... which gets a prefix of: $ipprefix" 

}

part 1

[eddieb@hap] > :global mkvlan
[eddie@hap] > :global catvlan
[eddie@hap] > :global rmvlan
[eddie@hap] > :global prettyprint
[eddie@hap] > :global pvid2array
[eddie@hap] > :global autovlanstyle
[eddie@hap] > :set mkvlan
[eddie@hap] > :set catvlan
[eddie@hap] > :set rmvlan
[eddie@hap] > :set prettyprint
[eddie@hap] > :set pvid2array
[eddie@hap] > :set autovlanstyle
[eddie@hap] > /system/script/run autovlan
[eddie@hap] > $prettyprint [$pvid2array 60]
{
    "basename": "vlan60",
    "cidraddr": "172.75.0.1/24",
    "cidrnet": "172.75.0.0/24",
    "commenttag": "mkvlan 60",
    "dhcpdns": "172.75.0.1",
    "dhcpgw": "172.75.0.1",
    "dhcppool": "172.75.0.10-172.75.0.249",
    "ipprefix": "172.75.0",
    "routerip": "172.75.0.1",
    "vlanbridge": "bridge",
    "vlanid": 60
}

part 2

[eddie@hap] > {
{...     :put "== use :convert to make a byte-array so we can get at the IP parts"
{...     :local vbytes [:convert from=num to=byte-array 60]
{...      :local lowbits ($vbytes->0)
{...     :local highbits ($vbytes->1)
{...     :local ipprefix "0.0.0"
{...     :put "\t... got $[:tostr $vbytes]"
{...
{...     :put "== verify :convert is working as expected"
{...     :put  "\tHIGH: $[:typeof ($vbytes->1)] $[:len ($vbytes->1)] $($vbytes->1)"
{...     :put  "\tLOW: $[:typeof ($vbytes->0)] $[:len ($vbytes->0)] $($vbytes->0)"
{...     :put "\t** HIGH should be 'nothing' - if it something, thats a bug"
{...
{...     :put "== replicate SAME code in pvid2array"
{...     :if ([:typeof $highbits] = "nothing") do={
{{...         :set ipprefix "192.168.$lowbits"
{{...     } else={
{{...         :set ipprefix "172.$($lowbits + 15).$highbits"
{{...     }
{...     :put "\t... which gets a prefix of: $ipprefix"
{...
{...     :put "== use DIFFERENT code in pvid2array to check instead for !num"
{...     :if ([:typeof $highbits] != "num") do={
{{...         :set ipprefix "192.168.$lowbits"
{{...     } else={
{{...         :set ipprefix "172.$($lowbits + 15).$highbits"
{{...     }
{...     :put "\t... which gets a prefix of: $ipprefix"
{...
{... }
== use :convert to make a byte-array so we can get at the IP parts
	... got 60;0;0;0;0;0;0;0
== verify :convert is working as expected
	HIGH: num 1 0
	LOW: num 2 60
	** HIGH should be 'nothing' - if it something, thats a bug
== replicate SAME code in pvid2array
	... which gets a prefix of: 172.75.0
== use DIFFERENT code in pvid2array to check instead for !num
	... which gets a prefix of: 172.75.0

part 3

[eddie@hap] > /system/routerboard/print
       routerboard: yes
        board-name: hAP ac
             model: RB962UiGS-5HacT2HnT
     serial-number: xxxxxxxxxxx
     firmware-type: qca9550L
  factory-firmware: 3.31
  current-firmware: 7.18beta2
  upgrade-firmware: 7.18beta2

Thanks!

It seems some bug in [:convert to=byte-array]:

== use :convert to make a byte-array so we can get at the IP parts
… got 60;0;0;0;0;0;0;0

I’m using an ARM-based RB1100 mainly, and another ARM system to test.

== use :convert to make a byte-array so we can get at the IP parts
… got 60
== verify :convert is working as expected
HIGH: nothing 0
LOW: num 2 60

I’ll update the script in a bit to workaround this, I think… But if you can run this and post it that help narrow this down:

:foreach testnum in=(1,60,128,256,257,512,513,4094,4095,1024*1024,1024*1024*1024,1024*1024*1024*1024) do={
    :local bytearray [:convert from=num to=byte-array $testnum]
    :put "$testnum got byte-array: $[:tostr $bytearray] ($[:len $bytearray] $[:typeof $bytearray])"
}



1 got byte-array: 1 (1 array)
60 got byte-array: 60 (1 array)
128 got byte-array: 128 (1 array)
256 got byte-array: 1;0 (2 array)
257 got byte-array: 1;1 (2 array)
512 got byte-array: 2;0 (2 array)
513 got byte-array: 2;1 (2 array)
4094 got byte-array: 15;254 (2 array)
4095 got byte-array: 15;255 (2 array)
1048576 got byte-array: 16;0;0 (3 array)
1073741824 got byte-array: 64;0;0;0 (4 array)
1099511627776 got byte-array: 1;0;0;0;0;0 (6 array)

[eddie@hap] > :foreach testnum in=(1,60,128,256,257,512,513,4094,4095,1024*1024,1024*1024*1024,1024*1024*1024*1024) do={
{...     :local bytearray [:convert from=num to=byte-array $testnum]
{...     :put "$testnum got byte-array: $[:tostr $bytearray] ($[:len $bytearray] $[:typeof $bytearray])"
{... }
1 got byte-array: 1;0;0;0;0;0;0;0 (8 array)
60 got byte-array: 60;0;0;0;0;0;0;0 (8 array)
128 got byte-array: 128;0;0;0;0;0;0;0 (8 array)
256 got byte-array: 1;0;0;0;0;0;0 (7 array)
257 got byte-array: 1;1;0;0;0;0;0;0 (8 array)
512 got byte-array: 2;0;0;0;0;0;0 (7 array)
513 got byte-array: 1;2;0;0;0;0;0;0 (8 array)
4094 got byte-array: 254;15;0;0;0;0;0;0 (8 array)
4095 got byte-array: 255;15;0;0;0;0;0;0 (8 array)
1048576 got byte-array: 16;0;0;0;0;0 (6 array)
1073741824 got byte-array: 64;0;0;0;0 (5 array)
1099511627776 got byte-array: 1;0;0 (3 array)

Interesting, good find.

It’s the CPU, your hAPac is a MIPSBE, while I’m using ARM things on my two test devices for this. The geeky explanation is that “BE” in MIPSBE stand for big-endian, and this affects how numbers are stored in low-level memory blocks, which somehow effecting [:convert]. Although, in theory, RouterOS should normalize this either big/small endian in [:convert] — so I still think it’s a bug.

Anyway, I can workaround this. I’ll update the script in bit.

Internal Scripting Notes…

More convinced now that look in more detail. i.e. Even if I give Mikrotik that :convert follow CPU endianness, the array size should be consistently 8:

128 got byte-array: 128;0;0;0;0;0;0;0 (8 array)
256 got byte-array: 1;0;0;0;0;0;0 (> 7 > array)
257 got byte-array: 1;1;0;0;0;0;0;0 (8 array

— the 256 value shows the problem since that also be a length of 8 with a leading zero:
256 got byte-array: 0;1;0;0;0;0;0;0 (8 array)
… but it’s not a length of 8, so that’s not actually the byte-array in memory no matter how you slice it.

If anyone wants to try on your TILE router & post results (or other older CPUs), that be helpful:

:foreach testnum in=(1,60,128,256,257,512,513,4094,4095,1024*1024,1024*1024*1024,1024*1024*1024*1024) do={
    :local bytearray [:convert from=num to=byte-array $testnum]
    :put "$testnum got byte-array: $[:tostr $bytearray] ($[:len $bytearray] $[:typeof $bytearray])"
}

I’ve been playing with IOT stuff, and been using [:convert to=byte-array], so be good to know it works on OTHER CPUs.

UPDATED: a fix for this problem is in version 1.3 of script in #1 post. Largely adding this:

     # uses :convert to break pvid into array with 2 elements between 0-256
    :local vlanbytes [:convert from=num to=byte-array $vlanid]  
    :local lowbits ($vlanbytes->0)
    :local highbits ($vlanbytes->1)

    # UGLY workaround for MIPSBE/other, detected when we don't get two parts from the vlan-id
    :if ([:len $vlanbytes]>2) do={
        :if ($vlanid > 255) do={
            # even worse workaround, normalize to 8 bytes - ros wrongly trims leading 0
            :if ([:len $vlanbytes]=7) do={ 
                # make it len=8 by pre-pending a 0 - so the swap below is correct
                :set vlanbytes (0,$vlanbytes) 
            }
            # now swap the high and low bytes
            :set lowbits ($vlanbytes->1)
            :set highbits ($vlanbytes->0)  
        } 
        # lowbits is right if under 256
    }