commit cb6a0f8d2764dfefd14e3e25cdc8129897e0a68c Author: Krystian Dużyński Date: Fri Nov 4 21:20:16 2022 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..02530c2 --- /dev/null +++ b/LICENSE.txt @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..781a8d2 --- /dev/null +++ b/README.md @@ -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 %} +``` \ No newline at end of file diff --git a/example/0_0-initial.rsc b/example/0_0-initial.rsc new file mode 100644 index 0000000..9ae1f51 --- /dev/null +++ b/example/0_0-initial.rsc @@ -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 + diff --git a/example/0_1-security.rsc b/example/0_1-security.rsc new file mode 100644 index 0000000..9d35965 --- /dev/null +++ b/example/0_1-security.rsc @@ -0,0 +1 @@ +{% include 'security.rsc' %} diff --git a/example/0_2-firewall.rsc b/example/0_2-firewall.rsc new file mode 100644 index 0000000..f964ee4 --- /dev/null +++ b/example/0_2-firewall.rsc @@ -0,0 +1 @@ +{% include 'firewall_router.rsc' %} diff --git a/example/1-logging.rsc b/example/1-logging.rsc new file mode 100644 index 0000000..a02bd19 --- /dev/null +++ b/example/1-logging.rsc @@ -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 diff --git a/example/2-ntp.rsc b/example/2-ntp.rsc new file mode 100644 index 0000000..05c6fd8 --- /dev/null +++ b/example/2-ntp.rsc @@ -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") }} diff --git a/example/3-port-forwarding.rsc b/example/3-port-forwarding.rsc new file mode 100644 index 0000000..92821a4 --- /dev/null +++ b/example/3-port-forwarding.rsc @@ -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") }} \ No newline at end of file diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..992b5a7 --- /dev/null +++ b/example/README.md @@ -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. diff --git a/example/common/firewall_router.rsc b/example/common/firewall_router.rsc new file mode 100644 index 0000000..85389bd --- /dev/null +++ b/example/common/firewall_router.rsc @@ -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 %} diff --git a/example/common/firewall_switch.rsc b/example/common/firewall_switch.rsc new file mode 100644 index 0000000..f3fe9f7 --- /dev/null +++ b/example/common/firewall_switch.rsc @@ -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" diff --git a/example/common/security.rsc b/example/common/security.rsc new file mode 100644 index 0000000..ddcd2c2 --- /dev/null +++ b/example/common/security.rsc @@ -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 diff --git a/example/config.yml b/example/config.yml new file mode 100644 index 0000000..86ff9a5 --- /dev/null +++ b/example/config.yml @@ -0,0 +1,9 @@ +has_flash: false + +host: 192.168.1.1 + +include_dirs: + - common/ + +variables: + admin_pass: "pass" diff --git a/example/host-keys/ssh_host_private_key_dsa b/example/host-keys/ssh_host_private_key_dsa new file mode 100644 index 0000000..eb094bf --- /dev/null +++ b/example/host-keys/ssh_host_private_key_dsa @@ -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----- diff --git a/example/host-keys/ssh_host_private_key_dsa.pub b/example/host-keys/ssh_host_private_key_dsa.pub new file mode 100644 index 0000000..f43b501 --- /dev/null +++ b/example/host-keys/ssh_host_private_key_dsa.pub @@ -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 diff --git a/example/host-keys/ssh_host_private_key_rsa b/example/host-keys/ssh_host_private_key_rsa new file mode 100644 index 0000000..7d3c25e --- /dev/null +++ b/example/host-keys/ssh_host_private_key_rsa @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +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 +k438geLC6lhQaxj0UMS1/PvURQ== +-----END PRIVATE KEY----- diff --git a/example/host-keys/ssh_host_private_key_rsa.pub b/example/host-keys/ssh_host_private_key_rsa.pub new file mode 100644 index 0000000..0d19421 --- /dev/null +++ b/example/host-keys/ssh_host_private_key_rsa.pub @@ -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----- diff --git a/intellij/README.md b/intellij/README.md new file mode 100644 index 0000000..12dd4aa --- /dev/null +++ b/intellij/README.md @@ -0,0 +1,14 @@ +## Configuration dialog + +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 + +highlight diff --git a/intellij/config.jpg b/intellij/config.jpg new file mode 100644 index 0000000..f19b5f9 Binary files /dev/null and b/intellij/config.jpg differ diff --git a/intellij/highlight.jpg b/intellij/highlight.jpg new file mode 100644 index 0000000..59aaae6 Binary files /dev/null and b/intellij/highlight.jpg differ diff --git a/intellij/keywords-1.txt b/intellij/keywords-1.txt new file mode 100644 index 0000000..4219d7a --- /dev/null +++ b/intellij/keywords-1.txt @@ -0,0 +1,4 @@ +:delay +:do +:local +:put \ No newline at end of file diff --git a/intellij/keywords-2.txt b/intellij/keywords-2.txt new file mode 100644 index 0000000..8e67924 --- /dev/null +++ b/intellij/keywords-2.txt @@ -0,0 +1,5 @@ +add +find +remove +set +disable \ No newline at end of file diff --git a/intellij/keywords-3.txt b/intellij/keywords-3.txt new file mode 100644 index 0000000..14e41ac --- /dev/null +++ b/intellij/keywords-3.txt @@ -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 diff --git a/intellij/keywords-4.txt b/intellij/keywords-4.txt new file mode 100644 index 0000000..3d4e4f3 --- /dev/null +++ b/intellij/keywords-4.txt @@ -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 diff --git a/mikrotik_configurator/__init__.py b/mikrotik_configurator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mikrotik_configurator/__main__.py b/mikrotik_configurator/__main__.py new file mode 100644 index 0000000..a72ce1d --- /dev/null +++ b/mikrotik_configurator/__main__.py @@ -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() diff --git a/mikrotik_configurator/generator.py b/mikrotik_configurator/generator.py new file mode 100644 index 0000000..f3fdead --- /dev/null +++ b/mikrotik_configurator/generator.py @@ -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 diff --git a/mikrotik_configurator/utils.py b/mikrotik_configurator/utils.py new file mode 100644 index 0000000..cccbffc --- /dev/null +++ b/mikrotik_configurator/utils.py @@ -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") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..df67e0d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +jinja2 +pyyaml \ No newline at end of file diff --git a/tools/gen_ssh_host_keys.sh b/tools/gen_ssh_host_keys.sh new file mode 100755 index 0000000..c3e42b3 --- /dev/null +++ b/tools/gen_ssh_host_keys.sh @@ -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