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 <vlan-id>
...and working VLAN should be added and you simply pvid= on a port in /interface/bridge/port
To "dump" the various config:
$catvlan <vlan-id>
...which will do various "print" to output the running state.
And to remove a VLAN that was created using $mkvlan:
$rmvlan <vlan-id>
It will automatically assign a unique IPv4 subnet based the PVID provided. So as part of the operation, another function name $pvid2array <vlan-id> 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):
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.{
"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
}
$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:
script source for automating VLAN - v1.3 code
# 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
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
And to confirm it's deleted another $catvlan 123 would show this after removal: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
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
Versions
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