mirror of
https://github.com/KrystianD/mikrotik_configurator.git
synced 2025-06-21 01:15:45 +02:00
initial commit
This commit is contained in:
commit
cb6a0f8d27
31 changed files with 880 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/.idea/
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Krystian Dużyński
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
129
README.md
Normal file
129
README.md
Normal file
|
@ -0,0 +1,129 @@
|
|||
Mikrotik configurator
|
||||
======
|
||||
|
||||
A utility for generating and applying RouterOS / Mikrotik configuration files (.rsc).
|
||||
|
||||
It uses Jinja2 as a template engine with few additional helpers.
|
||||
|
||||
Features:
|
||||
|
||||
* Applying configuration over SSH,
|
||||
* Support for applying only part of the whole configuration (e.g. if only firewall settings have changed).
|
||||
|
||||
Helpers:
|
||||
|
||||
* Escaping string and blocks,
|
||||
* Embedding files (e.g. SSH keys),
|
||||
* Cleanup functions (executed in reverse order on configuration re-apply),
|
||||
* Rolling back firewall chains.
|
||||
|
||||
Additional:
|
||||
|
||||
* [IntelliJ color schema configuration](intellij) for .rsc files,
|
||||
* [A script](tools/gen_ssh_host_keys.sh) for generating RouterOS-compatible SSH host key files.
|
||||
|
||||
# Usage
|
||||
|
||||
Prepare template files, like from [the embedded example](example):
|
||||
|
||||
* 0_0-initial.rsc
|
||||
* 0_1-security.rsc
|
||||
* 0_2-firewall.rsc
|
||||
* 1-logging.rsc
|
||||
* 2-ntp.rsc
|
||||
* 3-port-forwarding.rsc
|
||||
|
||||
Naming is important:
|
||||
|
||||
* filenames starting with `0_` are considered reset configuration that is being applied after
|
||||
`/system reset-configuration` command is executed,
|
||||
* filenames starting with other numbers can be applied to actively running Mikrotik router,
|
||||
* adding numbers help with applying the configuration in proper order.
|
||||
|
||||
### Configuration
|
||||
|
||||
```yaml
|
||||
has_flash: false # some RouterOS devices Flash directory is
|
||||
# accessible via explicit /flash/ prefix, for some just /
|
||||
|
||||
host: 192.168.1.1 # IP of the Mikrotik device, can be overriden with --override-id CLI argument
|
||||
|
||||
include_dirs: # search paths for templates including
|
||||
- common/
|
||||
|
||||
variables: # additonal Jinja2 variables
|
||||
admin_pass: "pass"
|
||||
```
|
||||
|
||||
### Reset configuration and apply
|
||||
|
||||
```shell
|
||||
cd example/
|
||||
python ../mikrotik_configurator [--dry-run] --reset *.rsc
|
||||
```
|
||||
|
||||
### Part of the configuration re-applying
|
||||
|
||||
```shell
|
||||
cd example/
|
||||
python ../mikrotik_configurator [--dry-run] 3-port-forwarding.rsc
|
||||
```
|
||||
|
||||
# Examples
|
||||
|
||||
## Setting admin password from config file
|
||||
|
||||
File `config.yaml`
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
admin_pass: "pass"
|
||||
```
|
||||
|
||||
File `1-example.rsc`
|
||||
|
||||
```text
|
||||
/user set admin password="{{ admin_pass }}"
|
||||
```
|
||||
|
||||
## SSH public key loading
|
||||
|
||||
File `1-example.rsc`
|
||||
|
||||
```text
|
||||
{{ load_file("~/.ssh/id_rsa.pub", "pcl_id_rsa.pub.txt") }}
|
||||
/user ssh-keys import user=admin public-key-file=pcl_id_rsa.pub.txt
|
||||
```
|
||||
|
||||
## Creating a firewall target with rollback
|
||||
|
||||
```text
|
||||
/ip firewall filter
|
||||
add chain="user-input" action=jump jump-target="user-input-ntp" comment="NTP rules"
|
||||
add chain="user-input-ntp" action=accept in-interface-list=LAN protocol=udp dst-port=123 comment="accept NTP (LAN)"
|
||||
{{ rollback_delete_chain("user-input-ntp") }}
|
||||
```
|
||||
|
||||
## Custom cleanup
|
||||
|
||||
```text
|
||||
/interface ovpn-client add name=my-ovpn \
|
||||
connect-to=myhost.com port=1190 \
|
||||
user=$vpnusername \
|
||||
password=$vpnpassword \
|
||||
verify-server-certificate=yes \
|
||||
cipher=aes256
|
||||
|
||||
{% call register_cleanup() %}
|
||||
/interface ovpn-client remove my-ovpn
|
||||
{% endcall %}
|
||||
```
|
||||
|
||||
## Script escape
|
||||
|
||||
```text
|
||||
/system script
|
||||
add dont-require-permissions=yes name="ddns-update" owner=admin policy=read,test source={% call escape_string() %}
|
||||
/tool fetch output=none mode=https url="https://my.own.dynamic.dns.site\?domain={{ public_domain }}" http-method=post http-data="token={{ ddns_token }}"
|
||||
{% endcall %}
|
||||
```
|
42
example/0_0-initial.rsc
Normal file
42
example/0_0-initial.rsc
Normal file
|
@ -0,0 +1,42 @@
|
|||
/system identity set name="MT"
|
||||
|
||||
/interface bridge
|
||||
add name="bridge"
|
||||
|
||||
/interface ethernet
|
||||
set [ find default-name=sfp1 ] disabled=yes
|
||||
|
||||
/interface list
|
||||
add name=LAN
|
||||
add name=WAN
|
||||
|
||||
/ip address
|
||||
add address=192.168.1.1/24 interface="bridge"
|
||||
|
||||
/interface list member
|
||||
add interface="bridge" list=LAN
|
||||
|
||||
/interface bridge port
|
||||
add bridge="bridge" interface=ether2
|
||||
add bridge="bridge" interface=ether3
|
||||
add bridge="bridge" interface=ether4
|
||||
add bridge="bridge" interface=ether5
|
||||
add bridge="bridge" interface=ether6
|
||||
add bridge="bridge" interface=ether7
|
||||
add bridge="bridge" interface=ether8
|
||||
add bridge="bridge" interface=ether9
|
||||
add bridge="bridge" interface=ether10
|
||||
|
||||
/user set admin password="{{ admin_pass }}"
|
||||
|
||||
#######################
|
||||
# SSH
|
||||
#######################
|
||||
{{ load_file("host-keys/ssh_host_private_key_dsa", "ssh_host_private_key_dsa.txt") }}
|
||||
{{ load_file("host-keys/ssh_host_private_key_rsa", "ssh_host_private_key_rsa.txt") }}
|
||||
/ip ssh import-host-key private-key-file=ssh_host_private_key_dsa.txt
|
||||
/ip ssh import-host-key private-key-file=ssh_host_private_key_rsa.txt
|
||||
|
||||
{{ load_file("~/.ssh/id_rsa.pub", "pcl_id_rsa.pub.txt") }}
|
||||
/user ssh-keys import user=admin public-key-file=pcl_id_rsa.pub.txt
|
||||
|
1
example/0_1-security.rsc
Normal file
1
example/0_1-security.rsc
Normal file
|
@ -0,0 +1 @@
|
|||
{% include 'security.rsc' %}
|
1
example/0_2-firewall.rsc
Normal file
1
example/0_2-firewall.rsc
Normal file
|
@ -0,0 +1 @@
|
|||
{% include 'firewall_router.rsc' %}
|
11
example/1-logging.rsc
Normal file
11
example/1-logging.rsc
Normal file
|
@ -0,0 +1,11 @@
|
|||
/system logging action remove [find default=no]
|
||||
/system logging remove [find default=no]
|
||||
|
||||
/system logging action
|
||||
add name=pc2 remote=192.168.1.2 remote-port=1514 target=remote
|
||||
|
||||
/system logging
|
||||
add action=pc2 prefix=MT topics=critical
|
||||
add action=pc2 prefix=MT topics=error
|
||||
add action=pc2 prefix=MT topics=warning
|
||||
add action=pc2 prefix=MT topics=info
|
13
example/2-ntp.rsc
Normal file
13
example/2-ntp.rsc
Normal file
|
@ -0,0 +1,13 @@
|
|||
/system ntp client
|
||||
set enabled=yes primary-ntp=162.159.200.123 secondary-ntp=162.159.200.1
|
||||
|
||||
/system ntp server
|
||||
set enabled=yes
|
||||
|
||||
/ip firewall filter
|
||||
add chain="user-input" action=jump jump-target="user-input-ntp" comment="NTP rules"
|
||||
add chain="user-input-ntp" \
|
||||
action=accept \
|
||||
in-interface-list=LAN protocol=udp dst-port=123 \
|
||||
comment="accept NTP (LAN)"
|
||||
{{ rollback_delete_chain("user-input-ntp") }}
|
7
example/3-port-forwarding.rsc
Normal file
7
example/3-port-forwarding.rsc
Normal file
|
@ -0,0 +1,7 @@
|
|||
/ip firewall nat
|
||||
add chain="user-dstnat" action=jump jump-target="user-dstnat-port-forwarding" comment="port forwarding"
|
||||
|
||||
add chain="user-dstnat-port-forwarding" action=dst-nat comment="port forwarding -> HTTP" dst-port=80,443 in-interface=ether1-WAN protocol=tcp to-addresses=192.168.1.2
|
||||
add chain="user-dstnat-port-forwarding" action=dst-nat comment="port forwarding -> SSH" dst-port=1234 in-interface-list=WAN protocol=tcp to-addresses=192.168.1.2 to-ports=22322
|
||||
|
||||
{{ rollback_delete_chain("user-dstnat-port-forwarding") }}
|
6
example/README.md
Normal file
6
example/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Important notes
|
||||
|
||||
* These example files are provided **ONLY** as an example of project structure.
|
||||
Although it contains a lot of solutions from my configuration files,
|
||||
it is not meant to be used as an example Mikrotik configuration,
|
||||
* Example generated SSH keys **SHOULD NOT** be used in your project.
|
83
example/common/firewall_router.rsc
Normal file
83
example/common/firewall_router.rsc
Normal file
|
@ -0,0 +1,83 @@
|
|||
:do {
|
||||
/ip firewall filter
|
||||
remove [find chain=input]
|
||||
remove [find chain=forward action!=passthrough]
|
||||
remove [find chain=output]
|
||||
|
||||
remove [find chain=icmp]
|
||||
remove [find jump-target="user-input"]
|
||||
remove [find jump-target="user-forward"]
|
||||
remove [find jump-target="user-output"]
|
||||
|
||||
remove [find chain="core-icmp"]
|
||||
|
||||
/ip firewall nat
|
||||
remove [find chain=srcnat]
|
||||
remove [find chain=dstnat]
|
||||
} on-error={}
|
||||
|
||||
/ip firewall filter
|
||||
# INPUT
|
||||
add chain=input action=accept comment="accept established,related,untracked" connection-state=established,related,untracked
|
||||
add chain=input action=drop comment="drop connection-state=invalid" connection-state=invalid
|
||||
add chain=input action=drop comment="drop banned" src-address-list=bans
|
||||
add chain=input action=jump comment="check ICMP" jump-target="core-icmp" protocol=icmp
|
||||
add chain=input action=accept comment="accept SSH and HTTP (LAN only)" in-interface-list=LAN protocol=tcp dst-port=22,80
|
||||
add chain=input action=accept comment="accept WinBox and API (LAN only)" in-interface-list=LAN protocol=tcp dst-port=8291,8728
|
||||
add chain=input action=accept comment="accept DNS and DHCP (LAN only)" in-interface-list=LAN protocol=udp dst-port=53,67,68
|
||||
# add chain=input action=accept comment="accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
|
||||
|
||||
add chain=input action=jump jump-target="user-input" comment="forward to user-input"
|
||||
add chain=input action=drop comment="drop all not coming from LAN" in-interface-list=!LAN
|
||||
add chain=input action=drop comment="drop all other"
|
||||
|
||||
# FORWARD
|
||||
add chain=forward action=fasttrack-connection comment="fasttrack" connection-state=established,related
|
||||
add chain=forward action=accept comment="accept established,related,untracked" connection-state=established,related,untracked
|
||||
add chain=forward action=drop comment="drop connection-state=invalid" connection-state=invalid
|
||||
add chain=forward action=drop comment="drop banned" src-address-list=bans
|
||||
add chain=forward action=jump comment="check ICMP" jump-target="core-icmp" src-address-list=lan in-interface-list=LAN protocol=icmp
|
||||
add chain=forward action=accept comment="accept DSTNATed from WAN to LAN" connection-state=new in-interface-list=WAN out-interface-list=LAN dst-address-list=lan connection-nat-state=dstnat
|
||||
add chain=forward action=accept comment="accept all from LAN to WAN" connection-state=new in-interface-list=LAN src-address-list=lan out-interface-list=WAN
|
||||
add chain=forward action=accept comment="accept all between LAN interfaces" connection-state=new in-interface-list=LAN src-address-list=lan out-interface-list=LAN dst-address-list=lan
|
||||
# add chain=forward action=accept comment="accept in ipsec policy" ipsec-policy=in,ipsec
|
||||
# add chain=forward action=accept comment="accept out ipsec policy" ipsec-policy=out,ipsec
|
||||
|
||||
add chain=forward action=jump jump-target="user-forward" comment="forward to user-forward"
|
||||
add chain=forward action=drop comment="drop all other"
|
||||
|
||||
# OUTPUT
|
||||
add chain=output action=jump jump-target="user-output" comment="forward to user-output"
|
||||
|
||||
add chain="core-icmp" protocol=icmp icmp-options=0:0 action=accept comment="echo reply"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=3:0 action=accept comment="net unreachable"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=3:1 action=accept comment="host unreachable"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=3:4 action=accept comment="host unreachable fragmentation required"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=8:0 action=accept comment="allow echo request"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=11:0 action=accept comment="allow time exceed"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=12:0 action=accept comment="allow parameter bad"
|
||||
add chain="core-icmp" action=drop comment="deny all other types"
|
||||
|
||||
/ip firewall nat
|
||||
add chain=srcnat action=masquerade comment="masquerade to WAN" ipsec-policy=out,none out-interface-list=WAN
|
||||
|
||||
add chain=srcnat action=jump jump-target="user-srcnat" comment="forward to user-srcnat"
|
||||
add chain=dstnat action=jump jump-target="user-dstnat" comment="forward to user-dstnat"
|
||||
|
||||
# Address lists
|
||||
/ip firewall address-list
|
||||
add list=bogons address=0.0.0.0/8 comment="Self-Identification [RFC 3330]"
|
||||
add list=bogons address=127.0.0.0/8 comment="Loopback [RFC 3330]"
|
||||
add list=bogons address=10.0.0.0/8 comment="Private[RFC 1918] - CLASS A" disabled=no
|
||||
add list=bogons address=172.16.0.0/12 comment="Private[RFC 1918] - CLASS B" disabled=no
|
||||
add list=bogons address=192.168.0.0/16 comment="Private[RFC 1918] - CLASS C" disabled=yes
|
||||
add list=bogons address=169.254.0.0/16 comment="Link Local [RFC 3330]"
|
||||
add list=bogons address=192.88.99.0/24 comment="6to4 Relay Anycast [RFC 3068]"
|
||||
add list=bogons address=198.18.0.0/15 comment="NIDB Testing"
|
||||
add list=bogons address=192.0.2.0/24 comment="Reserved - IANA - TestNet1"
|
||||
add list=bogons address=198.51.100.0/24 comment="Reserved - IANA - TestNet2"
|
||||
add list=bogons address=203.0.113.0/24 comment="Reserved - IANA - TestNet3"
|
||||
add list=bogons address=224.0.0.0/4 comment="MC, Class D, IANA" disabled=no
|
||||
{% call register_cleanup() %}
|
||||
/ip firewall address-list remove [find list=bogons]
|
||||
{% endcall %}
|
59
example/common/firewall_switch.rsc
Normal file
59
example/common/firewall_switch.rsc
Normal file
|
@ -0,0 +1,59 @@
|
|||
:do {
|
||||
/ip firewall filter
|
||||
remove [find chain=input]
|
||||
remove [find chain=forward action!=passthrough]
|
||||
remove [find chain=output]
|
||||
|
||||
remove [find chain=icmp]
|
||||
remove [find jump-target="user-input"]
|
||||
remove [find jump-target="user-forward"]
|
||||
remove [find jump-target="user-output"]
|
||||
|
||||
remove [find chain="core-icmp"]
|
||||
|
||||
/ip firewall nat
|
||||
remove [find chain=srcnat]
|
||||
remove [find chain=dstnat]
|
||||
} on-error={}
|
||||
|
||||
/ip firewall filter
|
||||
# INPUT
|
||||
add chain=input action=accept comment="accept established,related,untracked" connection-state=established,related,untracked
|
||||
add chain=input action=drop comment="drop connection-state=invalid" connection-state=invalid
|
||||
add chain=input action=drop comment="drop banned" src-address-list=bans
|
||||
add chain=input action=jump comment="check ICMP" jump-target="core-icmp" protocol=icmp
|
||||
add chain=input action=accept comment="accept SSH and HTTP (LAN only)" in-interface-list=LAN protocol=tcp dst-port=22,80
|
||||
add chain=input action=accept comment="accept WinBox and API (LAN only)" in-interface-list=LAN protocol=tcp dst-port=8291,8728
|
||||
|
||||
# add chain=input action=accept comment="accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
|
||||
|
||||
add chain=input action=jump jump-target="user-input" comment="forward to user-input"
|
||||
add chain=input action=drop comment="drop all not coming from LAN" in-interface-list=!LAN
|
||||
add chain=input action=drop comment="drop all other"
|
||||
|
||||
# FORWARD
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
add chain=forward action=jump jump-target="user-forward" comment="forward to user-forward"
|
||||
add chain=forward action=drop comment="drop all other"
|
||||
|
||||
# OUTPUT
|
||||
add chain=output action=jump jump-target="user-output" comment="forward to user-output"
|
||||
|
||||
add chain="core-icmp" protocol=icmp icmp-options=0:0 action=accept comment="echo reply"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=3:0 action=accept comment="net unreachable"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=3:1 action=accept comment="host unreachable"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=3:4 action=accept comment="host unreachable fragmentation required"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=8:0 action=accept comment="allow echo request"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=11:0 action=accept comment="allow time exceed"
|
||||
add chain="core-icmp" protocol=icmp icmp-options=12:0 action=accept comment="allow parameter bad"
|
||||
add chain="core-icmp" action=drop comment="deny all other types"
|
24
example/common/security.rsc
Normal file
24
example/common/security.rsc
Normal file
|
@ -0,0 +1,24 @@
|
|||
{ :local ver [/system resource get version]; :global vermajor [:pick $ver 0 [:find $ver "."]] }
|
||||
|
||||
/ip neighbor discovery-settings set discover-interface-list=none
|
||||
|
||||
/ip ipsec policy set 0 disabled=yes
|
||||
|
||||
:if ($vermajor = 7) do={ /ipv6 settings set disable-ipv6=yes }
|
||||
:if ($vermajor = 6) do={ /system package disable ipv6 }
|
||||
|
||||
/tool mac-server set allowed-interface-list=none
|
||||
/tool mac-server mac-winbox set allowed-interface-list=none
|
||||
/tool mac-server ping set enabled=no
|
||||
|
||||
/ip service set api disabled=yes
|
||||
/ip service set api-ssl disabled=yes
|
||||
/ip service set ftp disabled=yes
|
||||
/ip service set telnet disabled=yes
|
||||
/ip service set winbox disabled=no
|
||||
|
||||
/tool bandwidth-server set enabled=no
|
||||
|
||||
/ip ssh set strong-crypto=yes host-key-size=4096 forwarding-enabled=both always-allow-password-login=yes
|
||||
|
||||
/ip settings set rp-filter={{ rp_filter | default("strict") }} secure-redirects=no tcp-syncookies=yes
|
9
example/config.yml
Normal file
9
example/config.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
has_flash: false
|
||||
|
||||
host: 192.168.1.1
|
||||
|
||||
include_dirs:
|
||||
- common/
|
||||
|
||||
variables:
|
||||
admin_pass: "pass"
|
9
example/host-keys/ssh_host_private_key_dsa
Normal file
9
example/host-keys/ssh_host_private_key_dsa
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAI3m06Sl+2TgMzx9an3BRwuMD56J
|
||||
VrrWrTlVdcNYaxgquiv3osjFHs+kEAn8jY+pPFKCM9lpfoTj8FT0qPkKtsZ+LcB9
|
||||
YrxS4bsW8LGvnQScpZcrqFze4Ec0AF+7vYhP9pBHESlxlgEsOIDZvBsVErS/U7WM
|
||||
wgnIgrY0e/i2GaJZAhUAi0LIo6w4dKsjKCMN/j6X5/1YKAkCgYA8Hr5VfkCNYbMf
|
||||
J7amr+SilmkBLuQUn0+pV4FvGdPCa9EY0gxCP+0N82aintU7HfqOqE9pdtwFrbXU
|
||||
+/GyXJIuNME3y4JAvdFkJ18vAUVM0+7rGQ22BxdwrkBT3DUXV+9xjkYAh+6mpWOf
|
||||
S6Iyga1TE3nNlHtaP6KgBS7tdlcBxwQWAhRurYPs9TMeP1mlge45J3hbBO4Jbg==
|
||||
-----END PRIVATE KEY-----
|
1
example/host-keys/ssh_host_private_key_dsa.pub
Normal file
1
example/host-keys/ssh_host_private_key_dsa.pub
Normal file
|
@ -0,0 +1 @@
|
|||
ssh-dss AAAAB3NzaC1kc3MAAACBAI3m06Sl+2TgMzx9an3BRwuMD56JVrrWrTlVdcNYaxgquiv3osjFHs+kEAn8jY+pPFKCM9lpfoTj8FT0qPkKtsZ+LcB9YrxS4bsW8LGvnQScpZcrqFze4Ec0AF+7vYhP9pBHESlxlgEsOIDZvBsVErS/U7WMwgnIgrY0e/i2GaJZAAAAFQCLQsijrDh0qyMoIw3+Ppfn/VgoCQAAAIA8Hr5VfkCNYbMfJ7amr+SilmkBLuQUn0+pV4FvGdPCa9EY0gxCP+0N82aintU7HfqOqE9pdtwFrbXU+/GyXJIuNME3y4JAvdFkJ18vAUVM0+7rGQ22BxdwrkBT3DUXV+9xjkYAh+6mpWOfS6Iyga1TE3nNlHtaP6KgBS7tdlcBxwAAAIBFHqQTevlpTRONMZ40oxL4/DOz8If1ja7WTx4hH3HXWtvDEZii57FDv/xpUkCpP0dqp4NcaPWxZ5XH8+mdKR3zPxD8L+1S4NnleoPFtSMQU5v/Zcq7R79FGC0kaocjQN/a2XQOzifaeXx0nSD49l36bpkkMXYCBH0/l9HOD3xRkg== krystiand@PCL
|
40
example/host-keys/ssh_host_private_key_rsa
Normal file
40
example/host-keys/ssh_host_private_key_rsa
Normal file
|
@ -0,0 +1,40 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
k438geLC6lhQaxj0UMS1/PvURQ==
|
||||
-----END PRIVATE KEY-----
|
11
example/host-keys/ssh_host_private_key_rsa.pub
Normal file
11
example/host-keys/ssh_host_private_key_rsa.pub
Normal file
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA87rfcjP7LwChfd4uvxOx
|
||||
8ZJw3SnUWQ0uL2I2gZIhIPmyiOGmx/z3gzdN/aI7px0u9a4aDs2lNAlN3B+a/8n9
|
||||
Bu7d8k42fhwLcHV/o2VBiExRKxE7XgHGK8G/2heaMT+ivdr4eRH8o8sAHfZeJ/2W
|
||||
I6Md2/9lL5+wuNvmpbUnxDpF0bbE36PRp5JEWBhT8gvlN0Tdxlz9uBpu1tmQ0ZoN
|
||||
Ya1JFWjRqsc/qMgslBXr7KY0clwzJp4cBvCZevO/yZXeDjpoQ/vjL/KlF8WlTqxe
|
||||
WnsvRz2byQiJLXqRTT1V3cdX9M6Zqu1FPHZIISvdP+JMaBhXuU7fSsYeLPYwCnUU
|
||||
YQop012Servr6wwd9Bb+bUb28GtXheUddo02ry0g7yo0+GjAVw71yzugfnZLxIHa
|
||||
xTK2j/4rljfRH3L+Mx/8/FGBOddKLth/hcI0QUq6kTs59sGjbGNZyx6A4XD93K8e
|
||||
shwALGu8cVEreaobdmiahctD5wiONY/qqQhPlWk+l6TtAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
14
intellij/README.md
Normal file
14
intellij/README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
## Configuration dialog
|
||||
|
||||
<img src="config.jpg" alt="config" />
|
||||
|
||||
## Keywords
|
||||
|
||||
* Tab 1 - [keywords-1.txt](keywords-1.txt)
|
||||
* Tab 2 - [keywords-2.txt](keywords-2.txt)
|
||||
* Tab 3 - [keywords-3.txt](keywords-3.txt)
|
||||
* Tab 4 - [keywords-4.txt](keywords-4.txt)
|
||||
|
||||
## Highlighting example
|
||||
|
||||
<img src="highlight.jpg" alt="highlight" />
|
BIN
intellij/config.jpg
Normal file
BIN
intellij/config.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
intellij/highlight.jpg
Normal file
BIN
intellij/highlight.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
4
intellij/keywords-1.txt
Normal file
4
intellij/keywords-1.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
:delay
|
||||
:do
|
||||
:local
|
||||
:put
|
5
intellij/keywords-2.txt
Normal file
5
intellij/keywords-2.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
add
|
||||
find
|
||||
remove
|
||||
set
|
||||
disable
|
54
intellij/keywords-3.txt
Normal file
54
intellij/keywords-3.txt
Normal file
|
@ -0,0 +1,54 @@
|
|||
/certificate
|
||||
/certificate import
|
||||
/file
|
||||
/file print
|
||||
/interface
|
||||
/interface bridge
|
||||
/interface bridge filter
|
||||
/interface bridge port
|
||||
/interface ethernet
|
||||
/interface ethernet
|
||||
/interface list
|
||||
/interface list member
|
||||
/interface ovpn-client
|
||||
/interface ovpn-client remove
|
||||
/interface ovpn-server
|
||||
/interface vlan
|
||||
/interface wireless
|
||||
/interface wireless security-profiles
|
||||
/ip
|
||||
/ip address
|
||||
/ip dhcp-server
|
||||
/ip dhcp-server lease
|
||||
/ip dhcp-server network
|
||||
/ip dns
|
||||
/ip dns static
|
||||
/ip firewall
|
||||
/ip firewall address-list
|
||||
/ip firewall filter
|
||||
/ip firewall nat
|
||||
/ip firewall nat
|
||||
/ip ipsec
|
||||
/ip ipsec policy
|
||||
/ip neighbor
|
||||
/ip neighbor discovery-settings
|
||||
/ip pool
|
||||
/ip route
|
||||
/ip service
|
||||
/ip settings
|
||||
/ip ssh
|
||||
/ip ssh import-host-key
|
||||
/lcd pin
|
||||
/ppp profile
|
||||
/ppp secret
|
||||
/system identity
|
||||
/system logging
|
||||
/system logging action
|
||||
/system package
|
||||
/system scheduler
|
||||
/system script
|
||||
/tool bandwidth-server
|
||||
/tool fetch
|
||||
/tool mac-server
|
||||
/user
|
||||
/user ssh-keys import
|
99
intellij/keywords-4.txt
Normal file
99
intellij/keywords-4.txt
Normal file
|
@ -0,0 +1,99 @@
|
|||
action
|
||||
address
|
||||
address-pool
|
||||
allow-remote-requests
|
||||
allowed-interface-list
|
||||
auth
|
||||
authentication-types
|
||||
band
|
||||
band
|
||||
bridge
|
||||
cache-size
|
||||
certificate
|
||||
chain
|
||||
channel-width
|
||||
cipher
|
||||
client-id
|
||||
comment
|
||||
connect-to
|
||||
connection-nat-state
|
||||
connection-state
|
||||
contents
|
||||
default
|
||||
default-forwarding
|
||||
default-name
|
||||
default-profile
|
||||
disabled
|
||||
discover-interface-list
|
||||
dns-server
|
||||
dont-require-permissions
|
||||
dst-address
|
||||
dst-port
|
||||
enabled
|
||||
file
|
||||
frequency
|
||||
gateway
|
||||
host-key-size
|
||||
http-data
|
||||
http-method
|
||||
in-interface
|
||||
in-interface-list
|
||||
interface
|
||||
interval
|
||||
ipsec-policy
|
||||
jump-target
|
||||
list
|
||||
local-address
|
||||
mac-address
|
||||
management-protection
|
||||
master-interface
|
||||
mode
|
||||
name
|
||||
on-error
|
||||
on-event
|
||||
out-interface
|
||||
out-interface-list
|
||||
output
|
||||
owner
|
||||
passphrase
|
||||
password
|
||||
pin-number
|
||||
poe-out
|
||||
policy
|
||||
port
|
||||
prefix
|
||||
private-key-file
|
||||
profile
|
||||
protocol
|
||||
public-key-file
|
||||
pvid
|
||||
ranges
|
||||
remote
|
||||
remote-address
|
||||
remote-port
|
||||
rp-filter
|
||||
secure-redirects
|
||||
security-profile
|
||||
server
|
||||
servers
|
||||
service
|
||||
skip-dfs-channels
|
||||
source
|
||||
src-address
|
||||
ssid
|
||||
start-date
|
||||
start-time
|
||||
strong-crypto
|
||||
supplicant-identity
|
||||
target
|
||||
tcp-syncookies
|
||||
to-addresses
|
||||
to-ports
|
||||
topics
|
||||
ttl
|
||||
url
|
||||
user
|
||||
verify-server-certificate
|
||||
vlan-id
|
||||
wpa2-pre-shared-key
|
||||
wps-mode
|
0
mikrotik_configurator/__init__.py
Normal file
0
mikrotik_configurator/__init__.py
Normal file
106
mikrotik_configurator/__main__.py
Normal file
106
mikrotik_configurator/__main__.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import yaml
|
||||
|
||||
import generator
|
||||
from utils import query_yes_no
|
||||
|
||||
|
||||
def main():
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument('-c', '--config', default="config.yml", type=str, metavar="PATH")
|
||||
argparser.add_argument('-n', '--dry-run', action='store_true')
|
||||
argparser.add_argument('--reset', action='store_true')
|
||||
argparser.add_argument('--override-ip', type=str)
|
||||
argparser.add_argument('files', type=str, nargs="+", metavar="NAME")
|
||||
args = argparser.parse_args()
|
||||
|
||||
dry_run = args.dry_run
|
||||
cfg = yaml.load(open(args.config, "rt"), Loader=yaml.FullLoader)
|
||||
|
||||
host = cfg["host"]
|
||||
has_flash = cfg.get("has_flash", False)
|
||||
|
||||
if args.override_ip is not None:
|
||||
host = args.override_ip
|
||||
|
||||
files = args.files
|
||||
|
||||
orders = [float(os.path.basename(x).split("-")[0].replace("_", ".")) for x in files]
|
||||
if orders != list(sorted(orders)):
|
||||
print("mixed up order")
|
||||
exit(1)
|
||||
|
||||
if args.reset and orders[0] != 0:
|
||||
print("reset must start with 0_0")
|
||||
exit(1)
|
||||
|
||||
if not args.reset and orders[0] == 0:
|
||||
print("not reset can't start with 0_0")
|
||||
exit(1)
|
||||
|
||||
if not dry_run and args.reset:
|
||||
if not query_yes_no("Are you sure you want to reset configuration?", "no"):
|
||||
exit(1)
|
||||
|
||||
def gen(x):
|
||||
s = f'\n/log info message="starting {x}..."\n'
|
||||
s += generator.render_file(x, cfg.get("include_dirs", []), cfg.get("variables", {}))
|
||||
s += f'\n/log info message="finished {x}"\n'
|
||||
return s
|
||||
|
||||
script = "\n".join(gen(x) for x in files)
|
||||
if args.reset:
|
||||
script = ":delay 7s\n" + script
|
||||
|
||||
script += "\n/log info message=\"CONFIGURATION DONE\"\n"
|
||||
|
||||
base_path = "flash/" if has_flash else ""
|
||||
script_name = "output.rsc"
|
||||
|
||||
print(script)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="wt") as f:
|
||||
f.write(script)
|
||||
f.flush()
|
||||
|
||||
cargs = [
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=false",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAcceptedKeyTypes=+ssh-rsa",
|
||||
f.name,
|
||||
f"admin@{host}:{base_path}{script_name}"
|
||||
]
|
||||
print(" ".join(cargs))
|
||||
if not dry_run:
|
||||
subprocess.check_call(cargs)
|
||||
|
||||
if args.reset:
|
||||
cmd = f"/system reset-configuration no-defaults=yes skip-backup=yes run-after-reset={base_path}{script_name}"
|
||||
else:
|
||||
cmd = f"/import file={base_path}{script_name}"
|
||||
cargs = [
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=false",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "PubkeyAcceptedKeyTypes=+ssh-rsa",
|
||||
f"admin@{host}",
|
||||
cmd,
|
||||
]
|
||||
print(" ".join(cargs))
|
||||
if not dry_run:
|
||||
if args.reset:
|
||||
subprocess.run(cargs)
|
||||
else:
|
||||
out = subprocess.check_output(cargs).decode("utf-8")
|
||||
|
||||
if "Script file loaded and executed successfully" not in out:
|
||||
print("Script error")
|
||||
exit(1)
|
||||
|
||||
|
||||
main()
|
89
mikrotik_configurator/generator.py
Normal file
89
mikrotik_configurator/generator.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import os
|
||||
from textwrap import indent
|
||||
from typing import Dict, List
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from utils import read_text_file
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
cleanups = []
|
||||
|
||||
|
||||
def escape_for_mikrotik(cnt):
|
||||
return cnt \
|
||||
.replace('\\', '\\\\') \
|
||||
.replace("\t", "\\t") \
|
||||
.replace("\r", "\\r") \
|
||||
.replace("\n", "\\n") \
|
||||
.replace('"', '\\"')
|
||||
|
||||
|
||||
def load_file(path, name):
|
||||
_, ext = os.path.splitext(name)
|
||||
assert ext == ".txt"
|
||||
file_cnt = escape_for_mikrotik(read_text_file(os.path.expanduser(path)).strip())
|
||||
esc = escape_for_mikrotik(file_cnt)
|
||||
|
||||
cnt = f"""
|
||||
:execute script=":put \\"{esc}\\"" file="{name}"
|
||||
:delay 1000ms
|
||||
"""
|
||||
return cnt
|
||||
|
||||
|
||||
def generate_catch_block(body):
|
||||
return f""":do {{
|
||||
{indent(body.strip(), " ")}
|
||||
}} on-error={{}}
|
||||
"""
|
||||
|
||||
|
||||
def register_cleanup(caller):
|
||||
body = caller()
|
||||
|
||||
body = generate_catch_block(body)
|
||||
|
||||
cleanups.insert(0, body)
|
||||
return ""
|
||||
|
||||
|
||||
def escape_string(caller):
|
||||
body = caller().strip()
|
||||
return f'"{escape_for_mikrotik(body)}"'
|
||||
|
||||
|
||||
def rollback_delete_chain(name):
|
||||
body = generate_catch_block(f"""
|
||||
/ip firewall filter remove [find chain="{name}"]
|
||||
/ip firewall filter remove [find jump-target="{name}"]
|
||||
/ip firewall nat remove [find chain="{name}"]
|
||||
/ip firewall nat remove [find jump-target="{name}"]
|
||||
/ip firewall mangle remove [find chain="{name}"]
|
||||
/ip firewall mangle remove [find jump-target="{name}"]
|
||||
""")
|
||||
cleanups.insert(0, body)
|
||||
return ""
|
||||
|
||||
|
||||
def render_file(path: str, include_dirs: List[str], variables: Dict[str, str]):
|
||||
global cleanups
|
||||
cleanups = []
|
||||
|
||||
env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
os.path.join("."),
|
||||
*include_dirs,
|
||||
]),
|
||||
)
|
||||
env.globals['register_cleanup'] = register_cleanup
|
||||
env.globals['escape_string'] = escape_string
|
||||
env.globals['rollback_delete_chain'] = rollback_delete_chain
|
||||
env.globals = {**env.globals, **variables}
|
||||
|
||||
content = env.get_template(os.path.basename(path))
|
||||
content = content.render(load_file=load_file)
|
||||
|
||||
content = "\n".join(cleanups) + "\n\n" + content
|
||||
return content
|
35
mikrotik_configurator/utils.py
Normal file
35
mikrotik_configurator/utils.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import sys
|
||||
|
||||
|
||||
def write_text_file(path, content):
|
||||
with open(path, "wt", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def read_text_file(path):
|
||||
with open(path, "rt") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def query_yes_no(question, default="yes"):
|
||||
valid = {"yes": True, "y": True, "ye": True,
|
||||
"no": False, "n": False}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
prompt = " [Y/n] "
|
||||
elif default == "no":
|
||||
prompt = " [y/N] "
|
||||
else:
|
||||
raise ValueError("invalid default answer: '%s'" % default)
|
||||
|
||||
while True:
|
||||
sys.stdout.write(question + prompt)
|
||||
choice = input().lower()
|
||||
if default is not None and choice == '':
|
||||
return valid[default]
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "
|
||||
"(or 'y' or 'n').\n")
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
jinja2
|
||||
pyyaml
|
4
tools/gen_ssh_host_keys.sh
Executable file
4
tools/gen_ssh_host_keys.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
ssh-keygen -N "" -f ssh_host_private_key_dsa -t dsa -m pkcs8
|
||||
ssh-keygen -N "" -f ssh_host_private_key_rsa -t rsa -m pkcs8
|
||||
ssh-keygen -e -f ssh_host_private_key_rsa.pub -m pkcs8 > ssh_host_private_key_rsa.pub.tmp
|
||||
mv ssh_host_private_key_rsa.pub.tmp ssh_host_private_key_rsa.pub
|
Loading…
Add table
Add a link
Reference in a new issue